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
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from caraer_cli.api import functions as functions_api
|
|
9
|
+
from caraer_cli.api.client import CaraerApiClient
|
|
10
|
+
from caraer_cli.project.paths import functions_dir
|
|
11
|
+
from caraer_cli.project.schema import (
|
|
12
|
+
FunctionManifest,
|
|
13
|
+
ProjectConfig,
|
|
14
|
+
load_function_manifest,
|
|
15
|
+
save_function_manifest,
|
|
16
|
+
)
|
|
17
|
+
from caraer_cli.project.state import load_state, save_state
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _hash_text(value: str) -> str:
|
|
21
|
+
return hashlib.sha256(value.encode("utf-8")).hexdigest()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def discover_local_functions(
|
|
25
|
+
root: Path, config: ProjectConfig
|
|
26
|
+
) -> list[tuple[FunctionManifest, Path, str, dict[str, str]]]:
|
|
27
|
+
base = functions_dir(root, config.srcDir)
|
|
28
|
+
if not base.is_dir():
|
|
29
|
+
return []
|
|
30
|
+
found: list[tuple[FunctionManifest, Path, str, dict[str, str]]] = []
|
|
31
|
+
for child in sorted(base.iterdir()):
|
|
32
|
+
if not child.is_dir():
|
|
33
|
+
continue
|
|
34
|
+
manifest_path = child / "function.caraer.json"
|
|
35
|
+
if not manifest_path.is_file():
|
|
36
|
+
continue
|
|
37
|
+
manifest = load_function_manifest(manifest_path)
|
|
38
|
+
entry_path = child / manifest.resolved_entry()
|
|
39
|
+
if not entry_path.is_file():
|
|
40
|
+
raise FileNotFoundError(f"Missing entry file for function '{manifest.name}': {entry_path}")
|
|
41
|
+
code = entry_path.read_text(encoding="utf-8")
|
|
42
|
+
source_files = _collect_source_files(child, entry_path)
|
|
43
|
+
found.append((manifest, entry_path, code, source_files))
|
|
44
|
+
return found
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _collect_source_files(folder: Path, entry_path: Path) -> dict[str, str]:
|
|
48
|
+
"""All non-manifest files in the function folder except the entry file."""
|
|
49
|
+
files: dict[str, str] = {}
|
|
50
|
+
entry_resolved = entry_path.resolve()
|
|
51
|
+
for path in sorted(folder.rglob("*")):
|
|
52
|
+
if not path.is_file():
|
|
53
|
+
continue
|
|
54
|
+
if path.name == "function.caraer.json":
|
|
55
|
+
continue
|
|
56
|
+
if path.resolve() == entry_resolved:
|
|
57
|
+
continue
|
|
58
|
+
relative = path.relative_to(folder).as_posix()
|
|
59
|
+
if ".." in relative.split("/"):
|
|
60
|
+
continue
|
|
61
|
+
files[relative] = path.read_text(encoding="utf-8")
|
|
62
|
+
return files
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def list_local_function_names(root: Path, config: ProjectConfig) -> list[str]:
|
|
66
|
+
return [manifest.name for manifest, _, _, _ in discover_local_functions(root, config)]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def resolve_local_function_name(
|
|
70
|
+
root: Path,
|
|
71
|
+
config: ProjectConfig,
|
|
72
|
+
function: str | None = None,
|
|
73
|
+
*,
|
|
74
|
+
interactive: bool = True,
|
|
75
|
+
) -> str:
|
|
76
|
+
"""Resolve a function name from an explicit arg or the local functions folder."""
|
|
77
|
+
names = list_local_function_names(root, config)
|
|
78
|
+
if function and function.strip():
|
|
79
|
+
name = function.strip()
|
|
80
|
+
if name not in names and names:
|
|
81
|
+
raise ValueError(
|
|
82
|
+
f"Unknown function '{name}'. Available: {', '.join(names)}"
|
|
83
|
+
)
|
|
84
|
+
return name
|
|
85
|
+
if not names:
|
|
86
|
+
raise ValueError(
|
|
87
|
+
"No local functions found under src/app/functions/. "
|
|
88
|
+
"Pass --function <name> or add a function folder."
|
|
89
|
+
)
|
|
90
|
+
if len(names) == 1:
|
|
91
|
+
return names[0]
|
|
92
|
+
if interactive:
|
|
93
|
+
try:
|
|
94
|
+
from caraer_cli.wizard.prompts import WizardCancelled, ask_select
|
|
95
|
+
|
|
96
|
+
return ask_select("Select a function", names)
|
|
97
|
+
except WizardCancelled:
|
|
98
|
+
raise ValueError("No function selected.") from None
|
|
99
|
+
except Exception:
|
|
100
|
+
pass
|
|
101
|
+
raise ValueError(
|
|
102
|
+
"Multiple functions found; pass --function <name>. "
|
|
103
|
+
f"Available: {', '.join(names)}"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def upload_functions(
|
|
108
|
+
client: CaraerApiClient,
|
|
109
|
+
root: Path,
|
|
110
|
+
config: ProjectConfig,
|
|
111
|
+
*,
|
|
112
|
+
delete_missing: bool = False,
|
|
113
|
+
) -> dict[str, Any]:
|
|
114
|
+
if not config.appUuid:
|
|
115
|
+
raise ValueError("App has no remote UUID. Run 'caraer apps select' or 'caraer apps push'.")
|
|
116
|
+
|
|
117
|
+
remote = functions_api.list_functions(client, config.appUuid, page=1, limit=200)
|
|
118
|
+
remote_items = remote.get("data") or []
|
|
119
|
+
by_name = {str(item.get("name") or ""): item for item in remote_items if isinstance(item, dict)}
|
|
120
|
+
by_uuid = {str(item.get("uuid") or ""): item for item in remote_items if isinstance(item, dict)}
|
|
121
|
+
|
|
122
|
+
state = load_state(root)
|
|
123
|
+
fn_state: dict[str, Any] = state.setdefault("functions", {})
|
|
124
|
+
results: list[dict[str, Any]] = []
|
|
125
|
+
local_names: set[str] = set()
|
|
126
|
+
|
|
127
|
+
for manifest, _entry_path, code, source_files in discover_local_functions(root, config):
|
|
128
|
+
local_names.add(manifest.name)
|
|
129
|
+
extras_blob = json.dumps(source_files, sort_keys=True) if source_files else ""
|
|
130
|
+
content_hash = _hash_text(
|
|
131
|
+
f"{manifest.runtime}\n{manifest.description}\n{code}\n{extras_blob}"
|
|
132
|
+
)
|
|
133
|
+
existing_state = fn_state.get(manifest.name) or {}
|
|
134
|
+
existing_uuid = existing_state.get("uuid")
|
|
135
|
+
remote_item = by_uuid.get(existing_uuid) if existing_uuid else by_name.get(manifest.name)
|
|
136
|
+
|
|
137
|
+
payload: dict[str, Any] = {
|
|
138
|
+
"name": manifest.name,
|
|
139
|
+
"label": manifest.name,
|
|
140
|
+
"runtime": manifest.runtime,
|
|
141
|
+
"code": code,
|
|
142
|
+
"description": manifest.description or manifest.name,
|
|
143
|
+
"sourceFiles": source_files,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
remote_code = remote_item.get("code") if remote_item else None
|
|
147
|
+
remote_files = remote_item.get("sourceFiles") if remote_item else None
|
|
148
|
+
if remote_item and remote_item.get("uuid"):
|
|
149
|
+
uuid = str(remote_item["uuid"])
|
|
150
|
+
if (
|
|
151
|
+
existing_state.get("hash") == content_hash
|
|
152
|
+
and remote_code == code
|
|
153
|
+
and (remote_files or {}) == source_files
|
|
154
|
+
):
|
|
155
|
+
results.append({"name": manifest.name, "uuid": uuid, "action": "unchanged"})
|
|
156
|
+
else:
|
|
157
|
+
response = functions_api.update_function(client, config.appUuid, uuid, payload)
|
|
158
|
+
data = response.get("data") or {}
|
|
159
|
+
results.append({"name": manifest.name, "uuid": data.get("uuid", uuid), "action": "updated"})
|
|
160
|
+
fn_state[manifest.name] = {"uuid": data.get("uuid", uuid), "hash": content_hash}
|
|
161
|
+
else:
|
|
162
|
+
response = functions_api.create_function(client, config.appUuid, payload)
|
|
163
|
+
data = response.get("data") or {}
|
|
164
|
+
uuid = data.get("uuid")
|
|
165
|
+
results.append({"name": manifest.name, "uuid": uuid, "action": "created"})
|
|
166
|
+
if uuid:
|
|
167
|
+
fn_state[manifest.name] = {"uuid": uuid, "hash": content_hash}
|
|
168
|
+
|
|
169
|
+
deleted: list[dict[str, Any]] = []
|
|
170
|
+
if delete_missing:
|
|
171
|
+
for name, meta in list(fn_state.items()):
|
|
172
|
+
if name in local_names:
|
|
173
|
+
continue
|
|
174
|
+
uuid = meta.get("uuid")
|
|
175
|
+
if uuid:
|
|
176
|
+
functions_api.delete_function(client, config.appUuid, str(uuid))
|
|
177
|
+
deleted.append({"name": name, "uuid": uuid, "action": "deleted"})
|
|
178
|
+
fn_state.pop(name, None)
|
|
179
|
+
|
|
180
|
+
save_state(root, state)
|
|
181
|
+
return {"functions": results, "deleted": deleted}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def pull_functions(client: CaraerApiClient, root: Path, config: ProjectConfig) -> dict[str, Any]:
|
|
185
|
+
if not config.appUuid:
|
|
186
|
+
raise ValueError("App has no remote UUID. Run 'caraer apps select' or 'caraer apps push'.")
|
|
187
|
+
|
|
188
|
+
remote = functions_api.list_functions(client, config.appUuid, page=1, limit=200)
|
|
189
|
+
remote_items = remote.get("data") or []
|
|
190
|
+
base = functions_dir(root, config.srcDir)
|
|
191
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
192
|
+
state = load_state(root)
|
|
193
|
+
fn_state: dict[str, Any] = state.setdefault("functions", {})
|
|
194
|
+
pulled: list[dict[str, Any]] = []
|
|
195
|
+
|
|
196
|
+
for item in remote_items:
|
|
197
|
+
if not isinstance(item, dict):
|
|
198
|
+
continue
|
|
199
|
+
name = str(item.get("name") or item.get("uuid") or "function")
|
|
200
|
+
runtime = str(item.get("runtime") or "nodejs22")
|
|
201
|
+
description = str(item.get("description") or "")
|
|
202
|
+
code = str(item.get("code") or "")
|
|
203
|
+
source_files = item.get("sourceFiles") if isinstance(item.get("sourceFiles"), dict) else {}
|
|
204
|
+
uuid = str(item.get("uuid") or "")
|
|
205
|
+
folder = base / name
|
|
206
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
207
|
+
manifest = FunctionManifest(name=name, runtime=runtime, description=description)
|
|
208
|
+
save_function_manifest(folder / "function.caraer.json", manifest)
|
|
209
|
+
entry = folder / manifest.resolved_entry()
|
|
210
|
+
entry.write_text(code, encoding="utf-8")
|
|
211
|
+
for relative, content in source_files.items():
|
|
212
|
+
if not isinstance(relative, str) or not relative.strip() or ".." in relative.split("/"):
|
|
213
|
+
continue
|
|
214
|
+
target = folder / relative
|
|
215
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
216
|
+
target.write_text(str(content or ""), encoding="utf-8")
|
|
217
|
+
extras_blob = json.dumps(source_files, sort_keys=True) if source_files else ""
|
|
218
|
+
fn_state[name] = {
|
|
219
|
+
"uuid": uuid,
|
|
220
|
+
"hash": _hash_text(f"{runtime}\n{description}\n{code}\n{extras_blob}"),
|
|
221
|
+
}
|
|
222
|
+
pulled.append({"name": name, "uuid": uuid, "path": str(folder)})
|
|
223
|
+
|
|
224
|
+
save_state(root, state)
|
|
225
|
+
return {"functions": pulled}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def status_summary(client: CaraerApiClient, root: Path, config: ProjectConfig) -> dict[str, Any]:
|
|
229
|
+
local = discover_local_functions(root, config)
|
|
230
|
+
local_names = {m.name for m, _, _, _ in local}
|
|
231
|
+
remote_items: list[dict[str, Any]] = []
|
|
232
|
+
remote_app: dict[str, Any] = {}
|
|
233
|
+
if config.appUuid:
|
|
234
|
+
remote = functions_api.list_functions(client, config.appUuid, page=1, limit=200)
|
|
235
|
+
remote_items = [i for i in (remote.get("data") or []) if isinstance(i, dict)]
|
|
236
|
+
try:
|
|
237
|
+
from caraer_cli.api import apps as apps_api
|
|
238
|
+
|
|
239
|
+
app_response = apps_api.get_public_app(client, config.appUuid)
|
|
240
|
+
if isinstance(app_response.get("data"), dict):
|
|
241
|
+
remote_app = app_response["data"]
|
|
242
|
+
except Exception: # noqa: BLE001
|
|
243
|
+
remote_app = {}
|
|
244
|
+
remote_names = {str(i.get("name") or "") for i in remote_items}
|
|
245
|
+
state = load_state(root)
|
|
246
|
+
summary: dict[str, Any] = {
|
|
247
|
+
"appUuid": config.appUuid,
|
|
248
|
+
"projectUuid": state.get("projectUuid"),
|
|
249
|
+
"platformVersion": config.platformVersion,
|
|
250
|
+
"appPlatformVersion": remote_app.get("platformVersion") or config.api_platform_version(),
|
|
251
|
+
"localFunctions": sorted(local_names),
|
|
252
|
+
"remoteFunctions": sorted(n for n in remote_names if n),
|
|
253
|
+
"onlyLocal": sorted(local_names - remote_names),
|
|
254
|
+
"onlyRemote": sorted(remote_names - local_names),
|
|
255
|
+
"tracked": state.get("functions") or {},
|
|
256
|
+
"lastBuildUuid": state.get("lastBuildUuid"),
|
|
257
|
+
"lastDeployUuid": state.get("lastDeployUuid"),
|
|
258
|
+
}
|
|
259
|
+
if config.is_app_platform_v2() or remote_app.get("platformVersion") == 2:
|
|
260
|
+
summary["runtime"] = remote_app.get("runtime") or config.runtime
|
|
261
|
+
summary["runtimeStatus"] = remote_app.get("runtimeStatus")
|
|
262
|
+
summary["runtimeBaseUrl"] = remote_app.get("runtimeBaseUrl")
|
|
263
|
+
summary["runtimeRevision"] = remote_app.get("runtimeRevision")
|
|
264
|
+
summary["runtimeError"] = remote_app.get("runtimeError")
|
|
265
|
+
return summary
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def scaffold_function(
|
|
269
|
+
root: Path,
|
|
270
|
+
config: ProjectConfig,
|
|
271
|
+
name: str,
|
|
272
|
+
runtime: str = "nodejs22",
|
|
273
|
+
*,
|
|
274
|
+
description: str | None = None,
|
|
275
|
+
force: bool = False,
|
|
276
|
+
) -> Path:
|
|
277
|
+
"""Create ``src/app/functions/<name>/`` with manifest + entry source."""
|
|
278
|
+
folder = functions_dir(root, config.srcDir) / name
|
|
279
|
+
manifest_path = folder / "function.caraer.json"
|
|
280
|
+
if folder.exists() and any(folder.iterdir()) and not force:
|
|
281
|
+
raise FileExistsError(
|
|
282
|
+
f"Function folder already exists: {folder}. Use --force to overwrite scaffold files."
|
|
283
|
+
)
|
|
284
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
285
|
+
manifest = FunctionManifest(
|
|
286
|
+
name=name,
|
|
287
|
+
runtime=runtime,
|
|
288
|
+
description=description if description is not None else name,
|
|
289
|
+
)
|
|
290
|
+
save_function_manifest(manifest_path, manifest)
|
|
291
|
+
entry = folder / manifest.resolved_entry()
|
|
292
|
+
if force or not entry.exists():
|
|
293
|
+
if runtime.startswith("python"):
|
|
294
|
+
entry.write_text(
|
|
295
|
+
'def handler(request):\n'
|
|
296
|
+
' """Caraer serverless entrypoint."""\n'
|
|
297
|
+
' return {"statusCode": 200, "body": {"ok": True}}\n',
|
|
298
|
+
encoding="utf-8",
|
|
299
|
+
)
|
|
300
|
+
else:
|
|
301
|
+
entry.write_text(
|
|
302
|
+
"/**\n * Caraer serverless entrypoint.\n */\n"
|
|
303
|
+
"exports.handler = async (req, res) => {\n"
|
|
304
|
+
" res.status(200).json({ ok: true });\n"
|
|
305
|
+
"};\n",
|
|
306
|
+
encoding="utf-8",
|
|
307
|
+
)
|
|
308
|
+
return folder
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Generate typed helpers for Caraer serverless function payloads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
from caraer_cli.project.schema import ProjectConfig
|
|
9
|
+
|
|
10
|
+
RuntimeKind = Literal["nodejs", "python"]
|
|
11
|
+
|
|
12
|
+
TYPESCRIPT_FILENAME = "caraer.d.ts"
|
|
13
|
+
PYTHON_FILENAME = "caraer_types.py"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def resolve_runtime_kind(runtime: str | None) -> RuntimeKind:
|
|
17
|
+
text = (runtime or "nodejs22").strip().lower()
|
|
18
|
+
if text.startswith("python"):
|
|
19
|
+
return "python"
|
|
20
|
+
return "nodejs"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def types_dir(root: Path, config: ProjectConfig) -> Path:
|
|
24
|
+
return root / config.srcDir / "types"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def typegen_output_path(root: Path, config: ProjectConfig) -> Path:
|
|
28
|
+
kind = resolve_runtime_kind(config.runtime)
|
|
29
|
+
base = types_dir(root, config)
|
|
30
|
+
if kind == "python":
|
|
31
|
+
return base / PYTHON_FILENAME
|
|
32
|
+
return base / TYPESCRIPT_FILENAME
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def generate_types(
|
|
36
|
+
root: Path,
|
|
37
|
+
config: ProjectConfig,
|
|
38
|
+
*,
|
|
39
|
+
force: bool = False,
|
|
40
|
+
) -> dict[str, Any]:
|
|
41
|
+
"""Write typed helpers under ``src/types/`` for the app runtime."""
|
|
42
|
+
kind = resolve_runtime_kind(config.runtime)
|
|
43
|
+
out = typegen_output_path(root, config)
|
|
44
|
+
if out.exists() and not force:
|
|
45
|
+
raise FileExistsError(
|
|
46
|
+
f"Typegen output already exists: {out}. Use --force to overwrite."
|
|
47
|
+
)
|
|
48
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
if kind == "python":
|
|
50
|
+
out.write_text(_python_types(), encoding="utf-8")
|
|
51
|
+
else:
|
|
52
|
+
out.write_text(_typescript_types(), encoding="utf-8")
|
|
53
|
+
return {
|
|
54
|
+
"path": out,
|
|
55
|
+
"runtime": kind,
|
|
56
|
+
"relative": str(out.relative_to(root)),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _typescript_types() -> str:
|
|
61
|
+
return '''\
|
|
62
|
+
/**
|
|
63
|
+
* Generated by `caraer apps typegen`. Do not edit by hand.
|
|
64
|
+
*
|
|
65
|
+
* Common serverless / installation payload shapes for Caraer apps.
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
/** Installation settings field as delivered in lifecycle / webhook bodies. */
|
|
69
|
+
export interface SettingFieldValue {
|
|
70
|
+
name: string;
|
|
71
|
+
label?: string;
|
|
72
|
+
type?: string;
|
|
73
|
+
value?: unknown;
|
|
74
|
+
defaultValue?: unknown;
|
|
75
|
+
hasValue?: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Approximate lifecycle hook body (install / update / uninstall / rotate). */
|
|
79
|
+
export interface LifecyclePayload {
|
|
80
|
+
event?: "Installed" | "Updated" | "Uninstalled" | "Rotated" | string;
|
|
81
|
+
timestamp?: number;
|
|
82
|
+
appUuid?: string;
|
|
83
|
+
appName?: string;
|
|
84
|
+
appLabel?: string;
|
|
85
|
+
privateApp?: boolean;
|
|
86
|
+
authMethod?: "OAUTH2" | "API_KEY" | string;
|
|
87
|
+
userUuid?: string;
|
|
88
|
+
companyUuid?: string;
|
|
89
|
+
installationToken?: string;
|
|
90
|
+
oauthConnected?: boolean;
|
|
91
|
+
settingsSchema?: SettingFieldValue[];
|
|
92
|
+
scopes?: string[];
|
|
93
|
+
settingsChanged?: boolean;
|
|
94
|
+
scopesChanged?: boolean;
|
|
95
|
+
filtersChanged?: boolean;
|
|
96
|
+
[key: string]: unknown;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Record / domain webhook body (USER_FRIENDLY shape varies by topic). */
|
|
100
|
+
export interface WebhookPayload {
|
|
101
|
+
topic?: string;
|
|
102
|
+
event?: string;
|
|
103
|
+
timestamp?: number;
|
|
104
|
+
appUuid?: string;
|
|
105
|
+
companyUuid?: string;
|
|
106
|
+
installationToken?: string;
|
|
107
|
+
record?: Record<string, unknown>;
|
|
108
|
+
objectName?: string;
|
|
109
|
+
settingsSchema?: SettingFieldValue[];
|
|
110
|
+
[key: string]: unknown;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Cron / schedule invocation body. */
|
|
114
|
+
export interface SchedulePayload {
|
|
115
|
+
scheduleName?: string;
|
|
116
|
+
name?: string;
|
|
117
|
+
appUuid?: string;
|
|
118
|
+
companyUuid?: string;
|
|
119
|
+
installationToken?: string;
|
|
120
|
+
scheduledAt?: string | number;
|
|
121
|
+
settingsSchema?: SettingFieldValue[];
|
|
122
|
+
[key: string]: unknown;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Inbound HTTP route body (provider-specific; wrapped by the platform). */
|
|
126
|
+
export interface InboundPayload {
|
|
127
|
+
appUuid?: string;
|
|
128
|
+
companyUuid?: string;
|
|
129
|
+
installationToken?: string;
|
|
130
|
+
routeName?: string;
|
|
131
|
+
body?: unknown;
|
|
132
|
+
headers?: Record<string, string>;
|
|
133
|
+
[key: string]: unknown;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Async installation job payload (function receives this as req.body). */
|
|
137
|
+
export interface JobPayload {
|
|
138
|
+
jobId?: string;
|
|
139
|
+
functionName?: string;
|
|
140
|
+
appUuid?: string;
|
|
141
|
+
companyUuid?: string;
|
|
142
|
+
installationToken?: string;
|
|
143
|
+
[key: string]: unknown;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Installation state document (arbitrary JSON map). */
|
|
147
|
+
export type InstallationState = Record<string, unknown>;
|
|
148
|
+
|
|
149
|
+
/** Installation secrets map (values are strings when readable). */
|
|
150
|
+
export type InstallationSecrets = Record<string, string>;
|
|
151
|
+
|
|
152
|
+
/** POST /installation/jobs request body. */
|
|
153
|
+
export interface EnqueueJobRequest {
|
|
154
|
+
functionName: string;
|
|
155
|
+
payload?: Record<string, unknown>;
|
|
156
|
+
delaySeconds?: number;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Installation job status document. */
|
|
160
|
+
export interface InstallationJob {
|
|
161
|
+
jobId: string;
|
|
162
|
+
status: "QUEUED" | "RUNNING" | "SUCCEEDED" | "FAILED" | string;
|
|
163
|
+
functionName?: string;
|
|
164
|
+
payload?: Record<string, unknown>;
|
|
165
|
+
result?: unknown;
|
|
166
|
+
error?: string;
|
|
167
|
+
createdAt?: string | number;
|
|
168
|
+
updatedAt?: string | number;
|
|
169
|
+
[key: string]: unknown;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Wrapper returned by many Caraer API endpoints. */
|
|
173
|
+
export interface ApiSuccess<T = unknown> {
|
|
174
|
+
message?: string;
|
|
175
|
+
data?: T;
|
|
176
|
+
}
|
|
177
|
+
'''
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _python_types() -> str:
|
|
181
|
+
return '''\
|
|
182
|
+
"""Generated by `caraer apps typegen`. Do not edit by hand.
|
|
183
|
+
|
|
184
|
+
Common serverless / installation payload shapes for Caraer apps.
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
from __future__ import annotations
|
|
188
|
+
|
|
189
|
+
from typing import Any, NotRequired, TypedDict
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class SettingFieldValue(TypedDict, total=False):
|
|
193
|
+
name: str
|
|
194
|
+
label: str
|
|
195
|
+
type: str
|
|
196
|
+
value: Any
|
|
197
|
+
defaultValue: Any
|
|
198
|
+
hasValue: bool
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class LifecyclePayload(TypedDict, total=False):
|
|
202
|
+
event: str
|
|
203
|
+
timestamp: int | float
|
|
204
|
+
appUuid: str
|
|
205
|
+
appName: str
|
|
206
|
+
appLabel: str
|
|
207
|
+
privateApp: bool
|
|
208
|
+
authMethod: str
|
|
209
|
+
userUuid: str
|
|
210
|
+
companyUuid: str
|
|
211
|
+
installationToken: str
|
|
212
|
+
oauthConnected: bool
|
|
213
|
+
settingsSchema: list[SettingFieldValue]
|
|
214
|
+
scopes: list[str]
|
|
215
|
+
settingsChanged: bool
|
|
216
|
+
scopesChanged: bool
|
|
217
|
+
filtersChanged: bool
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class WebhookPayload(TypedDict, total=False):
|
|
221
|
+
topic: str
|
|
222
|
+
event: str
|
|
223
|
+
timestamp: int | float
|
|
224
|
+
appUuid: str
|
|
225
|
+
companyUuid: str
|
|
226
|
+
installationToken: str
|
|
227
|
+
record: dict[str, Any]
|
|
228
|
+
objectName: str
|
|
229
|
+
settingsSchema: list[SettingFieldValue]
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class SchedulePayload(TypedDict, total=False):
|
|
233
|
+
scheduleName: str
|
|
234
|
+
name: str
|
|
235
|
+
appUuid: str
|
|
236
|
+
companyUuid: str
|
|
237
|
+
installationToken: str
|
|
238
|
+
scheduledAt: str | int | float
|
|
239
|
+
settingsSchema: list[SettingFieldValue]
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class InboundPayload(TypedDict, total=False):
|
|
243
|
+
appUuid: str
|
|
244
|
+
companyUuid: str
|
|
245
|
+
installationToken: str
|
|
246
|
+
routeName: str
|
|
247
|
+
body: Any
|
|
248
|
+
headers: dict[str, str]
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class JobPayload(TypedDict, total=False):
|
|
252
|
+
jobId: str
|
|
253
|
+
functionName: str
|
|
254
|
+
appUuid: str
|
|
255
|
+
companyUuid: str
|
|
256
|
+
installationToken: str
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
InstallationState = dict[str, Any]
|
|
260
|
+
InstallationSecrets = dict[str, str]
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class EnqueueJobRequest(TypedDict):
|
|
264
|
+
functionName: str
|
|
265
|
+
payload: NotRequired[dict[str, Any]]
|
|
266
|
+
delaySeconds: NotRequired[float]
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class InstallationJob(TypedDict, total=False):
|
|
270
|
+
jobId: str
|
|
271
|
+
status: str
|
|
272
|
+
functionName: str
|
|
273
|
+
payload: dict[str, Any]
|
|
274
|
+
result: Any
|
|
275
|
+
error: str
|
|
276
|
+
createdAt: str | int | float
|
|
277
|
+
updatedAt: str | int | float
|
|
278
|
+
'''
|