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,519 @@
|
|
|
1
|
+
"""Interactive public-app JSON wizard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from questionary import Choice
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
|
|
15
|
+
from caraer_cli.wizard.catalog import (
|
|
16
|
+
MAIN_CATEGORIES,
|
|
17
|
+
find_main_category,
|
|
18
|
+
)
|
|
19
|
+
from caraer_cli.wizard.marketplace import (
|
|
20
|
+
prompt_app_bar as _prompt_app_bar,
|
|
21
|
+
prompt_pricing_plan as _prompt_pricing_plan,
|
|
22
|
+
prompt_setting_field as _prompt_setting_field,
|
|
23
|
+
)
|
|
24
|
+
from caraer_cli.wizard.prompts import (
|
|
25
|
+
WizardCancelled,
|
|
26
|
+
ask_checkbox,
|
|
27
|
+
ask_confirm,
|
|
28
|
+
ask_select,
|
|
29
|
+
ask_text,
|
|
30
|
+
)
|
|
31
|
+
from caraer_cli.wizard.scope_macros import (
|
|
32
|
+
COMMON_OBJECT_NAMES,
|
|
33
|
+
EXACT_SCOPE_EXAMPLES,
|
|
34
|
+
KNOWN_TOOL_NAMES,
|
|
35
|
+
SCOPE_MACRO_PRESETS,
|
|
36
|
+
find_preset,
|
|
37
|
+
materialize_macro,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
console = Console()
|
|
41
|
+
|
|
42
|
+
DEFAULT_BRAND_COLOR = "#E74363"
|
|
43
|
+
DEFAULT_TEXT_COLOR = "#FFFFFF"
|
|
44
|
+
DEFAULT_APP_FILE = "app.caraer.yaml"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def normalize_app_name(value: str) -> str:
|
|
48
|
+
normalized = value.strip().lower()
|
|
49
|
+
normalized = re.sub(r"[^a-z0-9]+", "_", normalized)
|
|
50
|
+
normalized = re.sub(r"_+", "_", normalized).strip("_")
|
|
51
|
+
return normalized
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _print_section(title: str) -> None:
|
|
55
|
+
console.print()
|
|
56
|
+
console.print(Panel(title, expand=False, border_style="magenta"))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _print_overview(title: str, rows: list[dict[str, Any]], columns: list[str]) -> None:
|
|
60
|
+
console.print()
|
|
61
|
+
if not rows:
|
|
62
|
+
console.print(f"[dim]{title}: (none yet)[/dim]")
|
|
63
|
+
return
|
|
64
|
+
table = Table(title=title, show_header=True, header_style="bold")
|
|
65
|
+
for column in columns:
|
|
66
|
+
table.add_column(column)
|
|
67
|
+
for index, row in enumerate(rows, start=1):
|
|
68
|
+
values = [str(index)]
|
|
69
|
+
for column in columns[1:]:
|
|
70
|
+
value = row.get(column, "")
|
|
71
|
+
if isinstance(value, list):
|
|
72
|
+
value = ", ".join(str(item) for item in value)
|
|
73
|
+
values.append("" if value is None else str(value))
|
|
74
|
+
table.add_row(*values)
|
|
75
|
+
console.print(table)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _optional(value: str | None) -> str | None:
|
|
79
|
+
text = (value or "").strip()
|
|
80
|
+
return text or None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _prompt_core() -> tuple[str, str]:
|
|
84
|
+
_print_section("Core")
|
|
85
|
+
label = ask_text("Label", required=True)
|
|
86
|
+
default_name = normalize_app_name(label) or "my_app"
|
|
87
|
+
name_raw = ask_text("Name (Enter to keep default)", default=default_name, required=True)
|
|
88
|
+
name = normalize_app_name(name_raw) or default_name
|
|
89
|
+
if name != name_raw.strip():
|
|
90
|
+
console.print(f"[dim]Normalized name to[/dim] [bold]{name}[/bold]")
|
|
91
|
+
return label, name
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _prompt_details() -> dict[str, Any]:
|
|
95
|
+
_print_section("Marketplace details")
|
|
96
|
+
title = ask_text("Title", default="")
|
|
97
|
+
description = ask_text("Description", default="")
|
|
98
|
+
|
|
99
|
+
category_key = ask_select(
|
|
100
|
+
"Main category",
|
|
101
|
+
[Choice(title=f"{c.label}", value=c.key) for c in MAIN_CATEGORIES],
|
|
102
|
+
default=MAIN_CATEGORIES[0].key,
|
|
103
|
+
)
|
|
104
|
+
category = find_main_category(category_key)
|
|
105
|
+
assert category is not None
|
|
106
|
+
|
|
107
|
+
subcategory_keys = ask_checkbox(
|
|
108
|
+
"Subcategories (space to toggle, enter to confirm)",
|
|
109
|
+
[Choice(title=s.label, value=s.key) for s in category.subcategories],
|
|
110
|
+
min_selected=1,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
url = ask_text("Details URL (optional)", default="")
|
|
114
|
+
image = ask_text("Image URL (optional)", default="")
|
|
115
|
+
brand_color = ask_text("Brand color (#RRGGBB)", default=DEFAULT_BRAND_COLOR)
|
|
116
|
+
text_color = ask_text("Text color (#RRGGBB)", default=DEFAULT_TEXT_COLOR)
|
|
117
|
+
|
|
118
|
+
details: dict[str, Any] = {
|
|
119
|
+
"title": title or None,
|
|
120
|
+
"description": description or None,
|
|
121
|
+
"category": category_key,
|
|
122
|
+
"subcategories": subcategory_keys,
|
|
123
|
+
"brandColor": brand_color or DEFAULT_BRAND_COLOR,
|
|
124
|
+
"textColor": text_color or DEFAULT_TEXT_COLOR,
|
|
125
|
+
}
|
|
126
|
+
if _optional(url):
|
|
127
|
+
details["url"] = url.strip()
|
|
128
|
+
if _optional(image):
|
|
129
|
+
details["image"] = image.strip()
|
|
130
|
+
return {key: value for key, value in details.items() if value is not None}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _print_scope_overview(scopes: list[str]) -> None:
|
|
134
|
+
rows = [{"scope": scope, "kind": _scope_kind(scope)} for scope in scopes]
|
|
135
|
+
_print_overview("Required scopes", rows, ["#", "scope", "kind"])
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _scope_kind(scope: str) -> str:
|
|
139
|
+
parts = scope.split(".")
|
|
140
|
+
if scope == "global.all":
|
|
141
|
+
return "macro"
|
|
142
|
+
if len(parts) == 3 and parts[0] == "tools" and parts[2] == "all":
|
|
143
|
+
return "macro"
|
|
144
|
+
if len(parts) == 3 and parts[0] == "records" and parts[2] in {
|
|
145
|
+
"all",
|
|
146
|
+
"properties_all",
|
|
147
|
+
"relations_all",
|
|
148
|
+
}:
|
|
149
|
+
return "macro"
|
|
150
|
+
return "exact"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _print_macro_help(preset_id: str) -> None:
|
|
154
|
+
preset = find_preset(preset_id)
|
|
155
|
+
if preset is None:
|
|
156
|
+
return
|
|
157
|
+
console.print(
|
|
158
|
+
Panel(
|
|
159
|
+
f"[bold]{preset.title}[/bold]\n"
|
|
160
|
+
f"Pattern: [cyan]{preset.pattern}[/cyan]\n"
|
|
161
|
+
f"Example: [cyan]{preset.example}[/cyan]\n\n"
|
|
162
|
+
f"{preset.description}\n\n"
|
|
163
|
+
f"[dim]When to use:[/dim] {preset.when_to_use}",
|
|
164
|
+
title="Macro help",
|
|
165
|
+
border_style="cyan",
|
|
166
|
+
expand=False,
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _ask_tool_name() -> str:
|
|
172
|
+
choice = ask_select(
|
|
173
|
+
"Tool name",
|
|
174
|
+
[
|
|
175
|
+
*[Choice(title=name, value=name) for name in KNOWN_TOOL_NAMES],
|
|
176
|
+
Choice(title="Other (type custom tool name)…", value="__custom__"),
|
|
177
|
+
],
|
|
178
|
+
default="forms",
|
|
179
|
+
)
|
|
180
|
+
if choice == "__custom__":
|
|
181
|
+
return ask_text("Custom tool name (e.g. forms)", required=True)
|
|
182
|
+
return choice
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _ask_object_name() -> str:
|
|
186
|
+
choice = ask_select(
|
|
187
|
+
"Object name",
|
|
188
|
+
[
|
|
189
|
+
*[Choice(title=name, value=name) for name in COMMON_OBJECT_NAMES],
|
|
190
|
+
Choice(title="Other (type custom object name)…", value="__custom__"),
|
|
191
|
+
],
|
|
192
|
+
default="candidate",
|
|
193
|
+
)
|
|
194
|
+
if choice == "__custom__":
|
|
195
|
+
return ask_text("Custom object name (e.g. candidate)", required=True)
|
|
196
|
+
return choice
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _add_unique(scopes: list[str], items: list[str]) -> list[str]:
|
|
200
|
+
existing = set(scopes)
|
|
201
|
+
for item in items:
|
|
202
|
+
normalized = item.strip()
|
|
203
|
+
if normalized and normalized not in existing:
|
|
204
|
+
scopes.append(normalized)
|
|
205
|
+
existing.add(normalized)
|
|
206
|
+
return scopes
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _prompt_add_macro(scopes: list[str]) -> list[str]:
|
|
210
|
+
preset_id = ask_select(
|
|
211
|
+
"Choose a scope macro",
|
|
212
|
+
[
|
|
213
|
+
Choice(
|
|
214
|
+
title=f"{preset.title} ({preset.pattern})",
|
|
215
|
+
value=preset.id,
|
|
216
|
+
)
|
|
217
|
+
for preset in SCOPE_MACRO_PRESETS
|
|
218
|
+
],
|
|
219
|
+
)
|
|
220
|
+
_print_macro_help(preset_id)
|
|
221
|
+
preset = find_preset(preset_id)
|
|
222
|
+
assert preset is not None
|
|
223
|
+
|
|
224
|
+
tool: str | None = None
|
|
225
|
+
object_name: str | None = None
|
|
226
|
+
if preset.needs_tool:
|
|
227
|
+
tool = _ask_tool_name()
|
|
228
|
+
if preset.needs_object:
|
|
229
|
+
object_name = _ask_object_name()
|
|
230
|
+
|
|
231
|
+
added = materialize_macro(preset, tool=tool, object_name=object_name)
|
|
232
|
+
console.print(f"[dim]Adding:[/dim] {', '.join(added)}")
|
|
233
|
+
return _add_unique(scopes, added)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _prompt_add_exact_scopes(scopes: list[str]) -> list[str]:
|
|
237
|
+
mode = ask_select(
|
|
238
|
+
"How do you want to add exact scopes?",
|
|
239
|
+
[
|
|
240
|
+
Choice(title="Pick from common examples", value="pick"),
|
|
241
|
+
Choice(title="Type one scope string", value="type"),
|
|
242
|
+
],
|
|
243
|
+
default="pick",
|
|
244
|
+
)
|
|
245
|
+
if mode == "pick":
|
|
246
|
+
selected = ask_checkbox(
|
|
247
|
+
"Exact scopes (space to toggle)",
|
|
248
|
+
[Choice(title=scope, value=scope) for scope in EXACT_SCOPE_EXAMPLES],
|
|
249
|
+
)
|
|
250
|
+
return _add_unique(scopes, selected)
|
|
251
|
+
|
|
252
|
+
while True:
|
|
253
|
+
scope = ask_text(
|
|
254
|
+
"Exact scope (e.g. tools.forms.read or records.contact.property.email.read)",
|
|
255
|
+
required=True,
|
|
256
|
+
)
|
|
257
|
+
scopes = _add_unique(scopes, [scope])
|
|
258
|
+
if not ask_confirm("Add another exact scope?", default=False):
|
|
259
|
+
break
|
|
260
|
+
return scopes
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _prompt_scopes() -> list[str]:
|
|
264
|
+
_print_section("Scopes")
|
|
265
|
+
console.print(
|
|
266
|
+
"[dim]Prefer macros (e.g. tools.forms.all, records.candidate.properties_all) "
|
|
267
|
+
"over long exact lists. Macros expand per tenant at install time.[/dim]"
|
|
268
|
+
)
|
|
269
|
+
if not ask_confirm("Configure required scopes?", default=True):
|
|
270
|
+
return []
|
|
271
|
+
|
|
272
|
+
scopes: list[str] = []
|
|
273
|
+
while True:
|
|
274
|
+
if scopes:
|
|
275
|
+
_print_scope_overview(scopes)
|
|
276
|
+
action = ask_select(
|
|
277
|
+
"Scopes action",
|
|
278
|
+
[
|
|
279
|
+
Choice(title="Add a macro (recommended)", value="macro"),
|
|
280
|
+
Choice(title="Add exact scope(s)", value="exact"),
|
|
281
|
+
Choice(title="Remove a scope", value="remove"),
|
|
282
|
+
Choice(title="Done with scopes", value="done"),
|
|
283
|
+
],
|
|
284
|
+
default="macro" if not scopes else "done",
|
|
285
|
+
)
|
|
286
|
+
if action == "done":
|
|
287
|
+
break
|
|
288
|
+
if action == "macro":
|
|
289
|
+
scopes = _prompt_add_macro(scopes)
|
|
290
|
+
continue
|
|
291
|
+
if action == "exact":
|
|
292
|
+
scopes = _prompt_add_exact_scopes(scopes)
|
|
293
|
+
continue
|
|
294
|
+
if action == "remove":
|
|
295
|
+
if not scopes:
|
|
296
|
+
console.print("[yellow]No scopes to remove.[/yellow]")
|
|
297
|
+
continue
|
|
298
|
+
to_remove = ask_checkbox(
|
|
299
|
+
"Select scopes to remove",
|
|
300
|
+
[Choice(title=scope, value=scope) for scope in scopes],
|
|
301
|
+
min_selected=1,
|
|
302
|
+
)
|
|
303
|
+
scopes = [scope for scope in scopes if scope not in set(to_remove)]
|
|
304
|
+
|
|
305
|
+
if scopes:
|
|
306
|
+
_print_scope_overview(scopes)
|
|
307
|
+
return scopes
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _prompt_settings_schema() -> list[dict[str, Any]]:
|
|
311
|
+
_print_section("Settings schema")
|
|
312
|
+
fields: list[dict[str, Any]] = []
|
|
313
|
+
while ask_confirm("Add a settings field?", default=False):
|
|
314
|
+
fields.append(_prompt_setting_field())
|
|
315
|
+
_print_overview(
|
|
316
|
+
"Settings schema",
|
|
317
|
+
fields,
|
|
318
|
+
["#", "name", "label", "type", "required", "defaultValue"],
|
|
319
|
+
)
|
|
320
|
+
return fields
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _prompt_pricing_plans() -> list[dict[str, Any]]:
|
|
324
|
+
_print_section("Pricing plans")
|
|
325
|
+
plans: list[dict[str, Any]] = []
|
|
326
|
+
while ask_confirm("Add a pricing plan?", default=False):
|
|
327
|
+
plans.append(_prompt_pricing_plan())
|
|
328
|
+
_print_overview(
|
|
329
|
+
"Pricing plans",
|
|
330
|
+
plans,
|
|
331
|
+
["#", "title", "pricingType", "pricePerUnit", "unit", "freeUnits"],
|
|
332
|
+
)
|
|
333
|
+
return plans
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _prompt_app_bars() -> list[dict[str, Any]]:
|
|
337
|
+
_print_section("App bars")
|
|
338
|
+
bars: list[dict[str, Any]] = []
|
|
339
|
+
while ask_confirm("Add an app bar?", default=False):
|
|
340
|
+
bars.append(_prompt_app_bar())
|
|
341
|
+
_print_overview(
|
|
342
|
+
"App bars",
|
|
343
|
+
bars,
|
|
344
|
+
["#", "label", "location", "iframeUrl", "tooltipLabel"],
|
|
345
|
+
)
|
|
346
|
+
return bars
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _prompt_webhook_controls() -> dict[str, Any]:
|
|
350
|
+
_print_section("Webhook controls")
|
|
351
|
+
controls: dict[str, Any] = {}
|
|
352
|
+
rate = ask_text("Webhook rate limit per minute (optional)", default="100")
|
|
353
|
+
if _optional(rate):
|
|
354
|
+
try:
|
|
355
|
+
controls["webhookRateLimitPerMinute"] = int(rate.strip())
|
|
356
|
+
except ValueError:
|
|
357
|
+
console.print("[yellow]Ignored invalid rate limit; leaving unset.[/yellow]")
|
|
358
|
+
bill_failed = ask_select(
|
|
359
|
+
"Bill failed webhook requests?",
|
|
360
|
+
[
|
|
361
|
+
Choice(title="No", value="false"),
|
|
362
|
+
Choice(title="Yes", value="true"),
|
|
363
|
+
Choice(title="Skip / leave unset", value=""),
|
|
364
|
+
],
|
|
365
|
+
default="false",
|
|
366
|
+
)
|
|
367
|
+
if bill_failed:
|
|
368
|
+
controls["billFailedWebhookRequests"] = bill_failed == "true"
|
|
369
|
+
return controls
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def build_public_app_payload_from_answers(
|
|
373
|
+
*,
|
|
374
|
+
label: str,
|
|
375
|
+
name: str,
|
|
376
|
+
details: dict[str, Any],
|
|
377
|
+
required_scopes: list[str],
|
|
378
|
+
settings_schema: list[dict[str, Any]],
|
|
379
|
+
pricing_plans: list[dict[str, Any]],
|
|
380
|
+
app_bars: list[dict[str, Any]],
|
|
381
|
+
webhook_controls: dict[str, Any],
|
|
382
|
+
runtime: str = "nodejs22",
|
|
383
|
+
auth_method: str = "OAUTH2",
|
|
384
|
+
oauth_redirect_uris: list[str] | None = None,
|
|
385
|
+
) -> dict[str, Any]:
|
|
386
|
+
from caraer_cli.commands.apps import DEFAULT_OAUTH_CALLBACK
|
|
387
|
+
|
|
388
|
+
method = (auth_method or "OAUTH2").strip().upper()
|
|
389
|
+
redirects = [
|
|
390
|
+
uri.strip()
|
|
391
|
+
for uri in (oauth_redirect_uris or [DEFAULT_OAUTH_CALLBACK])
|
|
392
|
+
if uri and uri.strip()
|
|
393
|
+
]
|
|
394
|
+
payload: dict[str, Any] = {
|
|
395
|
+
"label": label,
|
|
396
|
+
"name": name,
|
|
397
|
+
"runtime": runtime,
|
|
398
|
+
"authMethod": method,
|
|
399
|
+
"oauthRedirectUris": redirects,
|
|
400
|
+
"details": details,
|
|
401
|
+
"requiredScopes": required_scopes,
|
|
402
|
+
"settingsSchema": settings_schema,
|
|
403
|
+
"pricingPlans": pricing_plans,
|
|
404
|
+
"appBars": app_bars,
|
|
405
|
+
}
|
|
406
|
+
payload.update(webhook_controls)
|
|
407
|
+
return payload
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def run_public_app_wizard(
|
|
411
|
+
*,
|
|
412
|
+
output_dir: str | Path | None = None,
|
|
413
|
+
force: bool = False,
|
|
414
|
+
sample_function: str | None = "hello-world",
|
|
415
|
+
runtime: str = "nodejs22",
|
|
416
|
+
) -> Path:
|
|
417
|
+
"""Run the interactive public-app wizard and scaffold a local app folder.
|
|
418
|
+
|
|
419
|
+
Returns the path to ``src/app/app.caraer.yaml`` inside the new app folder.
|
|
420
|
+
"""
|
|
421
|
+
from caraer_cli.project.scaffold import scaffold_app_project
|
|
422
|
+
|
|
423
|
+
console.print(Panel.fit("Create Public App Wizard", border_style="magenta"))
|
|
424
|
+
|
|
425
|
+
from caraer_cli.commands.apps import DEFAULT_OAUTH_CALLBACK
|
|
426
|
+
|
|
427
|
+
label, name = _prompt_core()
|
|
428
|
+
details = _prompt_details()
|
|
429
|
+
required_scopes = _prompt_scopes()
|
|
430
|
+
settings_schema = _prompt_settings_schema()
|
|
431
|
+
pricing_plans = _prompt_pricing_plans()
|
|
432
|
+
app_bars = _prompt_app_bars()
|
|
433
|
+
webhook_controls = _prompt_webhook_controls()
|
|
434
|
+
|
|
435
|
+
auth_method = ask_select(
|
|
436
|
+
"Auth method",
|
|
437
|
+
[
|
|
438
|
+
Choice(title="OAuth 2.0", value="OAUTH2"),
|
|
439
|
+
Choice(title="API key", value="API_KEY"),
|
|
440
|
+
],
|
|
441
|
+
default="OAUTH2",
|
|
442
|
+
)
|
|
443
|
+
oauth_redirect_uris = [
|
|
444
|
+
ask_text(
|
|
445
|
+
"OAuth callback / redirect URL",
|
|
446
|
+
default=DEFAULT_OAUTH_CALLBACK,
|
|
447
|
+
required=True,
|
|
448
|
+
)
|
|
449
|
+
]
|
|
450
|
+
|
|
451
|
+
_print_section("App")
|
|
452
|
+
console.print(
|
|
453
|
+
f"- label: [bold]{label}[/bold]\n"
|
|
454
|
+
f"- name: [bold]{name}[/bold]\n"
|
|
455
|
+
f"- settings: {len(settings_schema)}\n"
|
|
456
|
+
f"- pricingPlans: {len(pricing_plans)}\n"
|
|
457
|
+
f"- appBars: {len(app_bars)}"
|
|
458
|
+
)
|
|
459
|
+
console.print(
|
|
460
|
+
"[dim]App definition goes in src/app/app.caraer.yaml. "
|
|
461
|
+
"Functions live under src/app/functions/; webhooks under src/app/webhooks/.[/dim]"
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
default_dir = str(output_dir or name)
|
|
465
|
+
project_dir = Path(ask_text("App directory", default=default_dir, required=True))
|
|
466
|
+
runtime = ask_select(
|
|
467
|
+
"App runtime",
|
|
468
|
+
[
|
|
469
|
+
Choice(title="Node.js 22", value="nodejs22"),
|
|
470
|
+
Choice(title="Python 3.12", value="python312"),
|
|
471
|
+
Choice(title="Skip sample function", value=""),
|
|
472
|
+
],
|
|
473
|
+
default=runtime if sample_function else "",
|
|
474
|
+
)
|
|
475
|
+
function_name = sample_function if runtime else None
|
|
476
|
+
if runtime and sample_function:
|
|
477
|
+
function_name = ask_text("Sample function name", default=sample_function, required=True)
|
|
478
|
+
|
|
479
|
+
resolved_runtime = runtime or "nodejs22"
|
|
480
|
+
payload = build_public_app_payload_from_answers(
|
|
481
|
+
label=label,
|
|
482
|
+
name=name,
|
|
483
|
+
details=details,
|
|
484
|
+
required_scopes=required_scopes,
|
|
485
|
+
settings_schema=settings_schema,
|
|
486
|
+
pricing_plans=pricing_plans,
|
|
487
|
+
app_bars=app_bars,
|
|
488
|
+
webhook_controls=webhook_controls,
|
|
489
|
+
runtime=resolved_runtime,
|
|
490
|
+
auth_method=auth_method,
|
|
491
|
+
oauth_redirect_uris=oauth_redirect_uris,
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
if project_dir.exists() and any(project_dir.iterdir()) and not force:
|
|
495
|
+
if not ask_confirm(
|
|
496
|
+
f"{project_dir} already exists and is not empty. Continue / overwrite app files?",
|
|
497
|
+
default=False,
|
|
498
|
+
):
|
|
499
|
+
raise WizardCancelled()
|
|
500
|
+
|
|
501
|
+
result = scaffold_app_project(
|
|
502
|
+
project_dir,
|
|
503
|
+
app_payload=payload,
|
|
504
|
+
project_name=name,
|
|
505
|
+
sample_function=function_name,
|
|
506
|
+
runtime=resolved_runtime,
|
|
507
|
+
force=True,
|
|
508
|
+
)
|
|
509
|
+
app_file: Path = result["app_file"]
|
|
510
|
+
console.print()
|
|
511
|
+
console.print(f"[green]Created app at {result['root']}[/green]")
|
|
512
|
+
console.print(f"[dim]App definition:[/dim] {app_file}")
|
|
513
|
+
console.print(f"[dim]Functions:[/dim] {result['functions_dir']}")
|
|
514
|
+
console.print(f"[dim]Webhooks:[/dim] {result['webhooks_dir']}")
|
|
515
|
+
console.print(
|
|
516
|
+
"[dim]Next:[/dim] cd "
|
|
517
|
+
f"{result['root'].name} && caraer apps push"
|
|
518
|
+
)
|
|
519
|
+
return app_file
|