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.
- caraer_cli/__init__.py +4 -0
- caraer_cli/api/__init__.py +1 -0
- caraer_cli/api/apps.py +78 -0
- caraer_cli/api/auth.py +53 -0
- caraer_cli/api/client.py +179 -0
- caraer_cli/api/functions.py +72 -0
- caraer_cli/api/installation.py +93 -0
- caraer_cli/api/integration_runtime.py +77 -0
- caraer_cli/api/projects.py +213 -0
- caraer_cli/api/sandboxes.py +29 -0
- caraer_cli/api/webhooks.py +59 -0
- caraer_cli/app_sync.py +697 -0
- caraer_cli/apps_local.py +274 -0
- caraer_cli/commands/__init__.py +1 -0
- caraer_cli/commands/apps.py +2025 -0
- caraer_cli/commands/auth.py +173 -0
- caraer_cli/commands/company.py +68 -0
- caraer_cli/commands/installation.py +182 -0
- caraer_cli/commands/profile.py +239 -0
- caraer_cli/commands/publish.py +40 -0
- caraer_cli/commands/sandbox.py +92 -0
- caraer_cli/commands/skill.py +165 -0
- caraer_cli/commands/webhooks.py +68 -0
- caraer_cli/completion.py +77 -0
- caraer_cli/completion_callbacks.py +162 -0
- caraer_cli/context.py +41 -0
- caraer_cli/errors.py +84 -0
- caraer_cli/formatters/__init__.py +1 -0
- caraer_cli/formatters/output.py +381 -0
- caraer_cli/formatters/timestamps.py +98 -0
- caraer_cli/install_completion.py +38 -0
- caraer_cli/local_app.py +135 -0
- caraer_cli/main.py +104 -0
- caraer_cli/models/__init__.py +1 -0
- caraer_cli/models/api.py +52 -0
- caraer_cli/project/__init__.py +17 -0
- caraer_cli/project/app_bars_sync.py +154 -0
- caraer_cli/project/app_manifest_template.py +68 -0
- caraer_cli/project/inbound_sync.py +192 -0
- caraer_cli/project/json_schemas.py +66 -0
- caraer_cli/project/lifecycle_sync.py +119 -0
- caraer_cli/project/local_dev.py +825 -0
- caraer_cli/project/manifest_edit.py +51 -0
- caraer_cli/project/marketplace_assemble.py +165 -0
- caraer_cli/project/oauth_providers_sync.py +180 -0
- caraer_cli/project/paths.py +102 -0
- caraer_cli/project/pricing_sync.py +81 -0
- caraer_cli/project/push_plan.py +590 -0
- caraer_cli/project/release.py +128 -0
- caraer_cli/project/scaffold.py +537 -0
- caraer_cli/project/schedules_sync.py +170 -0
- caraer_cli/project/schema.py +160 -0
- caraer_cli/project/settings_sync.py +77 -0
- caraer_cli/project/state.py +51 -0
- caraer_cli/project/sync.py +308 -0
- caraer_cli/project/typegen.py +278 -0
- caraer_cli/project/validate_app.py +1215 -0
- caraer_cli/project/webhooks_sync.py +233 -0
- caraer_cli/resolve.py +55 -0
- caraer_cli/schemas/app.caraer.schema.json +175 -0
- caraer_cli/schemas/function.caraer.schema.json +29 -0
- caraer_cli/schemas/inbound.caraer.schema.json +49 -0
- caraer_cli/schemas/lifecycle.caraer.schema.json +50 -0
- caraer_cli/schemas/schedule.caraer.schema.json +39 -0
- caraer_cli/schemas/webhook.caraer.schema.json +50 -0
- caraer_cli/skills/.gitkeep +2 -0
- caraer_cli/skills/caraer-apps/SKILL.md +172 -0
- caraer_cli/skills/caraer-apps/reference.md +163 -0
- caraer_cli/state/__init__.py +1 -0
- caraer_cli/state/config.py +148 -0
- caraer_cli/state/session.py +98 -0
- caraer_cli/utils.py +40 -0
- caraer_cli/wizard/__init__.py +9 -0
- caraer_cli/wizard/catalog.py +170 -0
- caraer_cli/wizard/marketplace.py +350 -0
- caraer_cli/wizard/prompts.py +99 -0
- caraer_cli/wizard/public_app.py +519 -0
- caraer_cli/wizard/scope_macros.py +214 -0
- caraer_cli-0.1.2.dist-info/METADATA +266 -0
- caraer_cli-0.1.2.dist-info/RECORD +83 -0
- caraer_cli-0.1.2.dist-info/WHEEL +4 -0
- caraer_cli-0.1.2.dist-info/entry_points.txt +2 -0
- caraer_cli-0.1.2.dist-info/licenses/LICENSE +8 -0
caraer_cli/apps_local.py
ADDED
|
@@ -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."""
|