caraer-cli 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. caraer_cli/__init__.py +4 -0
  2. caraer_cli/api/__init__.py +1 -0
  3. caraer_cli/api/apps.py +78 -0
  4. caraer_cli/api/auth.py +53 -0
  5. caraer_cli/api/client.py +179 -0
  6. caraer_cli/api/functions.py +72 -0
  7. caraer_cli/api/installation.py +93 -0
  8. caraer_cli/api/integration_runtime.py +77 -0
  9. caraer_cli/api/projects.py +213 -0
  10. caraer_cli/api/sandboxes.py +29 -0
  11. caraer_cli/api/webhooks.py +59 -0
  12. caraer_cli/app_sync.py +697 -0
  13. caraer_cli/apps_local.py +274 -0
  14. caraer_cli/commands/__init__.py +1 -0
  15. caraer_cli/commands/apps.py +2025 -0
  16. caraer_cli/commands/auth.py +173 -0
  17. caraer_cli/commands/company.py +68 -0
  18. caraer_cli/commands/installation.py +182 -0
  19. caraer_cli/commands/profile.py +239 -0
  20. caraer_cli/commands/publish.py +40 -0
  21. caraer_cli/commands/sandbox.py +92 -0
  22. caraer_cli/commands/skill.py +165 -0
  23. caraer_cli/commands/webhooks.py +68 -0
  24. caraer_cli/completion.py +77 -0
  25. caraer_cli/completion_callbacks.py +162 -0
  26. caraer_cli/context.py +41 -0
  27. caraer_cli/errors.py +84 -0
  28. caraer_cli/formatters/__init__.py +1 -0
  29. caraer_cli/formatters/output.py +381 -0
  30. caraer_cli/formatters/timestamps.py +98 -0
  31. caraer_cli/install_completion.py +38 -0
  32. caraer_cli/local_app.py +135 -0
  33. caraer_cli/main.py +104 -0
  34. caraer_cli/models/__init__.py +1 -0
  35. caraer_cli/models/api.py +52 -0
  36. caraer_cli/project/__init__.py +17 -0
  37. caraer_cli/project/app_bars_sync.py +154 -0
  38. caraer_cli/project/app_manifest_template.py +68 -0
  39. caraer_cli/project/inbound_sync.py +192 -0
  40. caraer_cli/project/json_schemas.py +66 -0
  41. caraer_cli/project/lifecycle_sync.py +119 -0
  42. caraer_cli/project/local_dev.py +825 -0
  43. caraer_cli/project/manifest_edit.py +51 -0
  44. caraer_cli/project/marketplace_assemble.py +165 -0
  45. caraer_cli/project/oauth_providers_sync.py +180 -0
  46. caraer_cli/project/paths.py +102 -0
  47. caraer_cli/project/pricing_sync.py +81 -0
  48. caraer_cli/project/push_plan.py +590 -0
  49. caraer_cli/project/release.py +128 -0
  50. caraer_cli/project/scaffold.py +537 -0
  51. caraer_cli/project/schedules_sync.py +170 -0
  52. caraer_cli/project/schema.py +160 -0
  53. caraer_cli/project/settings_sync.py +77 -0
  54. caraer_cli/project/state.py +51 -0
  55. caraer_cli/project/sync.py +308 -0
  56. caraer_cli/project/typegen.py +278 -0
  57. caraer_cli/project/validate_app.py +1215 -0
  58. caraer_cli/project/webhooks_sync.py +233 -0
  59. caraer_cli/resolve.py +55 -0
  60. caraer_cli/schemas/app.caraer.schema.json +175 -0
  61. caraer_cli/schemas/function.caraer.schema.json +29 -0
  62. caraer_cli/schemas/inbound.caraer.schema.json +49 -0
  63. caraer_cli/schemas/lifecycle.caraer.schema.json +50 -0
  64. caraer_cli/schemas/schedule.caraer.schema.json +39 -0
  65. caraer_cli/schemas/webhook.caraer.schema.json +50 -0
  66. caraer_cli/skills/.gitkeep +2 -0
  67. caraer_cli/skills/caraer-apps/SKILL.md +172 -0
  68. caraer_cli/skills/caraer-apps/reference.md +163 -0
  69. caraer_cli/state/__init__.py +1 -0
  70. caraer_cli/state/config.py +148 -0
  71. caraer_cli/state/session.py +98 -0
  72. caraer_cli/utils.py +40 -0
  73. caraer_cli/wizard/__init__.py +9 -0
  74. caraer_cli/wizard/catalog.py +170 -0
  75. caraer_cli/wizard/marketplace.py +350 -0
  76. caraer_cli/wizard/prompts.py +99 -0
  77. caraer_cli/wizard/public_app.py +519 -0
  78. caraer_cli/wizard/scope_macros.py +214 -0
  79. caraer_cli-0.1.2.dist-info/METADATA +266 -0
  80. caraer_cli-0.1.2.dist-info/RECORD +83 -0
  81. caraer_cli-0.1.2.dist-info/WHEEL +4 -0
  82. caraer_cli-0.1.2.dist-info/entry_points.txt +2 -0
  83. caraer_cli-0.1.2.dist-info/licenses/LICENSE +8 -0
@@ -0,0 +1,274 @@
1
+ """Pull a remote public app into a local app folder."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from caraer_cli.api import apps as apps_api
10
+ from caraer_cli.api.client import CaraerApiClient
11
+ from caraer_cli.local_app import load_local_app, resolve_app_file_path
12
+ from caraer_cli.project.app_manifest_template import (
13
+ APP_MANIFEST_JSON,
14
+ APP_MANIFEST_YAML,
15
+ render_app_manifest,
16
+ )
17
+ from caraer_cli.project.paths import (
18
+ app_manifest_path,
19
+ find_project_root,
20
+ has_workspace_file,
21
+ )
22
+ from caraer_cli.project.scaffold import scaffold_app_project
23
+ from caraer_cli.project.schema import load_workspace
24
+
25
+ # Fields useful for local editing / push. Drop nested runtime noise.
26
+ LOCAL_APP_KEYS = (
27
+ "uuid",
28
+ "label",
29
+ "name",
30
+ "runtime",
31
+ "authMethod",
32
+ "oauthRedirectUris",
33
+ "details",
34
+ "requiredScopes",
35
+ "settingsSchema",
36
+ "pricingPlans",
37
+ "appBars",
38
+ "webhookRateLimitPerMinute",
39
+ "jobRateLimitPerMinute",
40
+ "billFailedWebhookRequests",
41
+ "installWebhook",
42
+ "uninstallWebhook",
43
+ "rotateWebhook",
44
+ "updateWebhook",
45
+ "externalOAuthProviders",
46
+ )
47
+
48
+
49
+ def _normalize_app_name(value: str) -> str:
50
+ normalized = value.strip().lower()
51
+ normalized = re.sub(r"[^a-z0-9]+", "_", normalized)
52
+ normalized = re.sub(r"_+", "_", normalized).strip("_")
53
+ return normalized
54
+
55
+
56
+ def sanitize_remote_app_payload(data: dict[str, Any]) -> dict[str, Any]:
57
+ """Reduce a remote app DTO to a local editable app.caraer.yaml payload."""
58
+ payload: dict[str, Any] = {}
59
+ for key in LOCAL_APP_KEYS:
60
+ if key in data and data[key] is not None:
61
+ payload[key] = data[key]
62
+ # Marketplace publish state and privateApp are server-managed; omit from local files.
63
+ return payload
64
+
65
+
66
+ def _payload_uuid(payload: dict[str, Any]) -> str | None:
67
+ value = str(payload.get("uuid") or "").strip()
68
+ return value or None
69
+
70
+
71
+ def _local_uuid_from_app_file(app_file: Path) -> str | None:
72
+ try:
73
+ data = load_local_app(app_file)
74
+ except (FileNotFoundError, ValueError, OSError):
75
+ return None
76
+ value = str(data.get("uuid") or "").strip()
77
+ return value or None
78
+
79
+
80
+ def _local_uuid_from_project_root(root: Path) -> str | None:
81
+ try:
82
+ config = load_workspace(root)
83
+ value = str(config.appUuid or "").strip()
84
+ if value:
85
+ return value
86
+ except (FileNotFoundError, ValueError, OSError):
87
+ pass
88
+ try:
89
+ return _local_uuid_from_app_file(app_manifest_path(root))
90
+ except (FileNotFoundError, ValueError, OSError):
91
+ return None
92
+
93
+
94
+ def _new_project_dir_for_payload(payload: dict[str, Any], *, base: Path) -> Path:
95
+ name = _normalize_app_name(str(payload.get("name") or payload.get("label") or "app")) or "app"
96
+ candidate = (base / name).resolve()
97
+ if not candidate.exists():
98
+ return candidate
99
+
100
+ # Avoid clobbering an unrelated existing folder with the same name.
101
+ existing_uuid = _local_uuid_from_project_root(candidate) if has_workspace_file(candidate) else None
102
+ pulled_uuid = _payload_uuid(payload)
103
+ if existing_uuid and pulled_uuid and existing_uuid == pulled_uuid:
104
+ return candidate
105
+ if has_workspace_file(candidate) or (candidate / "src" / "app").exists():
106
+ suffix = (pulled_uuid or "app")[:8]
107
+ return (base / f"{name}_{suffix}").resolve()
108
+ return candidate
109
+
110
+
111
+ def resolve_pull_target_file(
112
+ payload: dict[str, Any],
113
+ *,
114
+ file: str | Path | None = None,
115
+ directory: str | Path | None = None,
116
+ existing_app_file: str | None = None,
117
+ ) -> Path:
118
+ """Choose where to write the pulled app.caraer.yaml.
119
+
120
+ Reuses the selected / current app folder only when it already belongs to the
121
+ same remote app. Pulling a different app always scaffolds a new folder
122
+ (sibling of the current project when cwd is inside one).
123
+ """
124
+ if file:
125
+ return Path(file).expanduser().resolve()
126
+
127
+ pulled_uuid = _payload_uuid(payload)
128
+
129
+ if directory:
130
+ root = Path(directory).expanduser().resolve()
131
+ if has_workspace_file(root) or (root / "src" / "app").exists():
132
+ return app_manifest_path(root).resolve()
133
+ return (root / "src" / "app" / APP_MANIFEST_YAML).resolve()
134
+
135
+ if existing_app_file:
136
+ try:
137
+ existing = resolve_app_file_path(existing_app_file)
138
+ existing_uuid = _local_uuid_from_app_file(existing)
139
+ if pulled_uuid and existing_uuid and existing_uuid == pulled_uuid:
140
+ return existing
141
+ except (FileNotFoundError, ValueError):
142
+ pass
143
+
144
+ base = Path.cwd().resolve()
145
+ try:
146
+ current_root = find_project_root(Path.cwd())
147
+ current_uuid = _local_uuid_from_project_root(current_root)
148
+ if pulled_uuid and current_uuid and pulled_uuid == current_uuid:
149
+ return app_manifest_path(current_root).resolve()
150
+ # Different (or unknown) app: create a sibling folder, not nest/overwrite.
151
+ base = current_root.parent
152
+ except FileNotFoundError:
153
+ pass
154
+
155
+ root = _new_project_dir_for_payload(payload, base=base)
156
+ if has_workspace_file(root) or (root / "src" / "app").exists():
157
+ return app_manifest_path(root).resolve()
158
+ return (root / "src" / "app" / APP_MANIFEST_YAML).resolve()
159
+
160
+
161
+ def write_pulled_app(
162
+ payload: dict[str, Any],
163
+ target: Path,
164
+ *,
165
+ force: bool = True,
166
+ platform_version: str | None = None,
167
+ runtime: str | None = None,
168
+ ) -> Path:
169
+ """Write payload to target, scaffolding an app folder around it when needed."""
170
+ from caraer_cli.project.schema import PLATFORM_VERSION, PLATFORM_VERSION_V1
171
+
172
+ target = target.expanduser().resolve()
173
+ resolved_platform = platform_version or PLATFORM_VERSION
174
+ if resolved_platform not in {PLATFORM_VERSION, PLATFORM_VERSION_V1}:
175
+ resolved_platform = PLATFORM_VERSION
176
+ resolved_runtime = (runtime or payload.get("runtime") or "nodejs22")
177
+ if isinstance(resolved_runtime, str):
178
+ resolved_runtime = resolved_runtime.strip().lower() or "nodejs22"
179
+ else:
180
+ resolved_runtime = "nodejs22"
181
+
182
+ # If writing into .../src/app/app.caraer.* without a workspace, scaffold.
183
+ if target.parent.name == "app" and target.name in {APP_MANIFEST_YAML, APP_MANIFEST_JSON}:
184
+ project_root = target.parents[2] if len(target.parents) >= 3 else target.parent
185
+ if not has_workspace_file(project_root):
186
+ scaffold_app_project(
187
+ project_root,
188
+ app_payload=payload,
189
+ project_name=str(payload.get("name") or project_root.name),
190
+ app_uuid=str(payload["uuid"]) if payload.get("uuid") else None,
191
+ sample_function=None,
192
+ runtime=resolved_runtime,
193
+ platform_version=resolved_platform,
194
+ force=force,
195
+ )
196
+ return app_manifest_path(project_root).resolve()
197
+
198
+ # Prefer YAML on write; migrate away from legacy JSON targets.
199
+ if target.name == APP_MANIFEST_JSON:
200
+ target = target.with_name(APP_MANIFEST_YAML)
201
+
202
+ target.parent.mkdir(parents=True, exist_ok=True)
203
+ if target.exists() and not force:
204
+ raise FileExistsError(f"{target} already exists. Use --force to overwrite.")
205
+ # Pulled remote data is authoritative; skip scaffold example comments.
206
+ include_examples = not any(
207
+ isinstance(payload.get(key), list) and payload.get(key)
208
+ for key in ("pricingPlans", "appBars", "settingsSchema", "requiredScopes")
209
+ )
210
+ target.write_text(
211
+ render_app_manifest(payload, include_examples=include_examples),
212
+ encoding="utf-8",
213
+ )
214
+ return target
215
+
216
+
217
+ def pull_remote_app(
218
+ client: CaraerApiClient,
219
+ app_uuid: str,
220
+ *,
221
+ file: str | Path | None = None,
222
+ directory: str | Path | None = None,
223
+ existing_app_file: str | None = None,
224
+ force: bool = True,
225
+ ) -> tuple[dict[str, Any], Path]:
226
+ """Fetch a remote public app and write it to a local app.caraer.yaml."""
227
+ from caraer_cli.project.schema import PLATFORM_VERSION, PLATFORM_VERSION_V1
228
+
229
+ response = apps_api.get_public_app(client, app_uuid)
230
+ data = response.get("data")
231
+ if not isinstance(data, dict):
232
+ raise ValueError("Remote app payload was not an object.")
233
+ payload = sanitize_remote_app_payload(data)
234
+ if not payload.get("uuid"):
235
+ payload["uuid"] = app_uuid
236
+
237
+ remote_platform = data.get("platformVersion")
238
+ if remote_platform == 2 or remote_platform == "2":
239
+ platform_version = PLATFORM_VERSION
240
+ elif remote_platform == 1 or remote_platform == "1":
241
+ platform_version = PLATFORM_VERSION_V1
242
+ else:
243
+ platform_version = PLATFORM_VERSION
244
+ runtime = data.get("runtime") or payload.get("runtime") or "nodejs22"
245
+
246
+ target = resolve_pull_target_file(
247
+ payload,
248
+ file=file,
249
+ directory=directory,
250
+ existing_app_file=existing_app_file,
251
+ )
252
+ # Prefer modular files on disk; keep empty arrays in YAML.
253
+ from caraer_cli.project.marketplace_assemble import split_marketplace_to_disk
254
+ from caraer_cli.project.schema import load_workspace
255
+
256
+ project_root = (
257
+ target.parents[2]
258
+ if target.parent.name == "app" and len(target.parents) >= 3
259
+ else target.parent
260
+ )
261
+ try:
262
+ config = load_workspace(project_root)
263
+ payload = split_marketplace_to_disk(project_root, config, payload)
264
+ except (FileNotFoundError, ValueError, OSError):
265
+ pass
266
+
267
+ written = write_pulled_app(
268
+ payload,
269
+ target,
270
+ force=force,
271
+ platform_version=platform_version,
272
+ runtime=str(runtime) if runtime else None,
273
+ )
274
+ return payload, written
@@ -0,0 +1 @@
1
+ """Typer command modules."""