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,170 @@
|
|
|
1
|
+
"""Static marketplace catalogs mirrored from the Caraer backend."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class Subcategory:
|
|
10
|
+
key: str
|
|
11
|
+
label: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class MainCategory:
|
|
16
|
+
key: str
|
|
17
|
+
label: str
|
|
18
|
+
subcategories: tuple[Subcategory, ...]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _slugify(label: str) -> str:
|
|
22
|
+
normalized = label.strip().lower().replace("&", "and")
|
|
23
|
+
chars: list[str] = []
|
|
24
|
+
prev_underscore = False
|
|
25
|
+
for ch in normalized:
|
|
26
|
+
if ch.isalnum():
|
|
27
|
+
chars.append(ch)
|
|
28
|
+
prev_underscore = False
|
|
29
|
+
elif not prev_underscore:
|
|
30
|
+
chars.append("_")
|
|
31
|
+
prev_underscore = True
|
|
32
|
+
return "".join(chars).strip("_")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _category(label: str, sub_labels: list[str]) -> MainCategory:
|
|
36
|
+
return MainCategory(
|
|
37
|
+
key=_slugify(label),
|
|
38
|
+
label=label,
|
|
39
|
+
subcategories=tuple(Subcategory(key=_slugify(s), label=s) for s in sub_labels),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
MAIN_CATEGORIES: tuple[MainCategory, ...] = (
|
|
44
|
+
_category(
|
|
45
|
+
"Productivity",
|
|
46
|
+
["Agenda", "Tasks", "Notes", "Documents", "Files", "Automation", "Forms"],
|
|
47
|
+
),
|
|
48
|
+
_category(
|
|
49
|
+
"Communication",
|
|
50
|
+
[
|
|
51
|
+
"Email",
|
|
52
|
+
"Chat",
|
|
53
|
+
"Meetings",
|
|
54
|
+
"Calling",
|
|
55
|
+
"Notifications",
|
|
56
|
+
"Customer Conversations",
|
|
57
|
+
"Social Messaging",
|
|
58
|
+
],
|
|
59
|
+
),
|
|
60
|
+
_category(
|
|
61
|
+
"Recruitment",
|
|
62
|
+
[
|
|
63
|
+
"ATS",
|
|
64
|
+
"Job Posting",
|
|
65
|
+
"Sourcing",
|
|
66
|
+
"Candidate Communication",
|
|
67
|
+
"Career Sites",
|
|
68
|
+
"Screening",
|
|
69
|
+
"Interview Scheduling",
|
|
70
|
+
"Analytics",
|
|
71
|
+
],
|
|
72
|
+
),
|
|
73
|
+
_category(
|
|
74
|
+
"HR",
|
|
75
|
+
["HRIS", "Onboarding", "Payroll", "Contracts", "Learning", "Engagement", "Compliance"],
|
|
76
|
+
),
|
|
77
|
+
_category(
|
|
78
|
+
"Marketing",
|
|
79
|
+
[
|
|
80
|
+
"Email Marketing",
|
|
81
|
+
"Social Media",
|
|
82
|
+
"Campaigns",
|
|
83
|
+
"Content",
|
|
84
|
+
"SEO",
|
|
85
|
+
"Advertising",
|
|
86
|
+
"Automation",
|
|
87
|
+
"Branding",
|
|
88
|
+
],
|
|
89
|
+
),
|
|
90
|
+
_category(
|
|
91
|
+
"Sales",
|
|
92
|
+
[
|
|
93
|
+
"CRM",
|
|
94
|
+
"Lead Generation",
|
|
95
|
+
"Enrichment",
|
|
96
|
+
"Outreach",
|
|
97
|
+
"Proposals",
|
|
98
|
+
"Account Management",
|
|
99
|
+
"Analytics",
|
|
100
|
+
],
|
|
101
|
+
),
|
|
102
|
+
_category(
|
|
103
|
+
"Data",
|
|
104
|
+
[
|
|
105
|
+
"Dashboards",
|
|
106
|
+
"Reporting",
|
|
107
|
+
"Spreadsheets",
|
|
108
|
+
"Visualization",
|
|
109
|
+
"Data Management",
|
|
110
|
+
"Predictions",
|
|
111
|
+
],
|
|
112
|
+
),
|
|
113
|
+
_category(
|
|
114
|
+
"Finance",
|
|
115
|
+
["Invoicing", "Payments", "Bookkeeping", "Expenses", "Budgeting", "Reporting"],
|
|
116
|
+
),
|
|
117
|
+
_category(
|
|
118
|
+
"Customer Support",
|
|
119
|
+
["Helpdesk", "Ticketing", "Live Chat", "Knowledge Base", "Feedback", "Analytics"],
|
|
120
|
+
),
|
|
121
|
+
_category(
|
|
122
|
+
"Developer Tools",
|
|
123
|
+
[
|
|
124
|
+
"Code",
|
|
125
|
+
"Repositories",
|
|
126
|
+
"APIs",
|
|
127
|
+
"Webhooks",
|
|
128
|
+
"DevOps",
|
|
129
|
+
"Monitoring",
|
|
130
|
+
"Testing",
|
|
131
|
+
"No-code",
|
|
132
|
+
],
|
|
133
|
+
),
|
|
134
|
+
_category(
|
|
135
|
+
"Education & Learning",
|
|
136
|
+
[
|
|
137
|
+
"Courses",
|
|
138
|
+
"Training",
|
|
139
|
+
"Onboarding",
|
|
140
|
+
"Coaching",
|
|
141
|
+
"Assessments",
|
|
142
|
+
"Knowledge Sharing",
|
|
143
|
+
],
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
APP_BAR_LOCATIONS: tuple[tuple[str, str], ...] = (
|
|
148
|
+
("RECORD_PREVIEW", "Record preview (action)"),
|
|
149
|
+
("RECORD_OVERVIEW", "Record overview (action)"),
|
|
150
|
+
("RECORD_DETAIL", "Record detail (iframe)"),
|
|
151
|
+
("TOOL_BAR", "Tool bar (iframe)"),
|
|
152
|
+
("TRAIT_BAR", "Trait bar (iframe)"),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
ACTION_BASED_LOCATIONS = {"RECORD_PREVIEW", "RECORD_OVERVIEW"}
|
|
156
|
+
|
|
157
|
+
SETTING_FIELD_TYPES: tuple[tuple[str, str], ...] = (
|
|
158
|
+
("SINGLE_LINE", "Single line text"),
|
|
159
|
+
("MULTI_LINE", "Multi-line text"),
|
|
160
|
+
("SWITCH", "Switch / boolean"),
|
|
161
|
+
("SINGLE_SELECT", "Single select"),
|
|
162
|
+
("MULTI_SELECT", "Multi select"),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def find_main_category(key: str) -> MainCategory | None:
|
|
167
|
+
for category in MAIN_CATEGORIES:
|
|
168
|
+
if category.key == key:
|
|
169
|
+
return category
|
|
170
|
+
return None
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""Interactive prompts for marketplace fields (settings, pricing, app bars, lifecycle)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from questionary import Choice
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from caraer_cli.project.lifecycle_sync import LIFECYCLE_HOOKS
|
|
11
|
+
from caraer_cli.wizard.catalog import (
|
|
12
|
+
ACTION_BASED_LOCATIONS,
|
|
13
|
+
APP_BAR_LOCATIONS,
|
|
14
|
+
SETTING_FIELD_TYPES,
|
|
15
|
+
)
|
|
16
|
+
from caraer_cli.wizard.prompts import ask_confirm, ask_select, ask_text
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _optional(value: str | None) -> str | None:
|
|
22
|
+
text = (value or "").strip()
|
|
23
|
+
return text or None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _coerce_number(raw: str) -> int | float | str:
|
|
27
|
+
text = raw.strip()
|
|
28
|
+
try:
|
|
29
|
+
if "." in text:
|
|
30
|
+
return float(text)
|
|
31
|
+
return int(text)
|
|
32
|
+
except ValueError:
|
|
33
|
+
return text
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def prompt_setting_field(
|
|
37
|
+
*,
|
|
38
|
+
name: str | None = None,
|
|
39
|
+
label: str | None = None,
|
|
40
|
+
field_type: str | None = None,
|
|
41
|
+
required: bool | None = None,
|
|
42
|
+
help_text: str | None = None,
|
|
43
|
+
default_value: str | None = None,
|
|
44
|
+
) -> dict[str, Any]:
|
|
45
|
+
"""Prompt for one settingsSchema field (wizard-style)."""
|
|
46
|
+
resolved_name = (name or "").strip() or ask_text("Field name", required=True)
|
|
47
|
+
resolved_label = (
|
|
48
|
+
label
|
|
49
|
+
if label is not None and str(label).strip()
|
|
50
|
+
else ask_text("Field label", default=resolved_name)
|
|
51
|
+
)
|
|
52
|
+
resolved_type = (
|
|
53
|
+
(field_type or "").strip().upper()
|
|
54
|
+
or ask_select(
|
|
55
|
+
"Field type",
|
|
56
|
+
[Choice(title=lbl, value=key) for key, lbl in SETTING_FIELD_TYPES],
|
|
57
|
+
default="SINGLE_LINE",
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
resolved_required = (
|
|
61
|
+
required
|
|
62
|
+
if required is not None
|
|
63
|
+
else ask_confirm("Required?", default=False)
|
|
64
|
+
)
|
|
65
|
+
resolved_help = (
|
|
66
|
+
help_text
|
|
67
|
+
if help_text is not None
|
|
68
|
+
else ask_text("Help text (optional)", default="")
|
|
69
|
+
)
|
|
70
|
+
resolved_default = (
|
|
71
|
+
default_value
|
|
72
|
+
if default_value is not None
|
|
73
|
+
else ask_text("Default value (optional)", default="")
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
field: dict[str, Any] = {
|
|
77
|
+
"name": resolved_name.strip(),
|
|
78
|
+
"label": (resolved_label or resolved_name).strip(),
|
|
79
|
+
"type": resolved_type,
|
|
80
|
+
"required": bool(resolved_required),
|
|
81
|
+
}
|
|
82
|
+
if _optional(resolved_help):
|
|
83
|
+
field["helpText"] = resolved_help.strip()
|
|
84
|
+
if _optional(resolved_default):
|
|
85
|
+
if resolved_type == "SWITCH":
|
|
86
|
+
field["defaultValue"] = resolved_default.strip().lower() in {
|
|
87
|
+
"1",
|
|
88
|
+
"true",
|
|
89
|
+
"yes",
|
|
90
|
+
"y",
|
|
91
|
+
}
|
|
92
|
+
else:
|
|
93
|
+
field["defaultValue"] = resolved_default.strip()
|
|
94
|
+
|
|
95
|
+
if resolved_type in {"SINGLE_SELECT", "MULTI_SELECT"}:
|
|
96
|
+
options_raw = ask_text(
|
|
97
|
+
"Options (comma-separated label=value or label)",
|
|
98
|
+
required=True,
|
|
99
|
+
)
|
|
100
|
+
options: list[dict[str, str]] = []
|
|
101
|
+
for part in options_raw.split(","):
|
|
102
|
+
item = part.strip()
|
|
103
|
+
if not item:
|
|
104
|
+
continue
|
|
105
|
+
if "=" in item:
|
|
106
|
+
option_label, option_value = item.split("=", 1)
|
|
107
|
+
else:
|
|
108
|
+
option_label, option_value = item, item
|
|
109
|
+
options.append(
|
|
110
|
+
{"label": option_label.strip(), "value": option_value.strip()}
|
|
111
|
+
)
|
|
112
|
+
field["options"] = options
|
|
113
|
+
return field
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def prompt_pricing_tier() -> dict[str, Any]:
|
|
117
|
+
tier: dict[str, Any] = {
|
|
118
|
+
"startUnits": _coerce_number(
|
|
119
|
+
ask_text("Start units", default="0", required=True)
|
|
120
|
+
),
|
|
121
|
+
}
|
|
122
|
+
end_units = ask_text("End units (blank = unlimited)", default="")
|
|
123
|
+
if _optional(end_units):
|
|
124
|
+
tier["endUnits"] = _coerce_number(end_units)
|
|
125
|
+
price_month = ask_text("Price per month (optional)", default="")
|
|
126
|
+
price_year = ask_text("Price per year (optional)", default="")
|
|
127
|
+
price_extra = ask_text("Price per extra unit (optional)", default="")
|
|
128
|
+
if _optional(price_month):
|
|
129
|
+
tier["pricePerMonth"] = _coerce_number(price_month)
|
|
130
|
+
if _optional(price_year):
|
|
131
|
+
tier["pricePerYear"] = _coerce_number(price_year)
|
|
132
|
+
if _optional(price_extra):
|
|
133
|
+
tier["pricePerExtraUnit"] = _coerce_number(price_extra)
|
|
134
|
+
return tier
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def prompt_pricing_plan(
|
|
138
|
+
*,
|
|
139
|
+
title: str | None = None,
|
|
140
|
+
pricing_type: str | None = None,
|
|
141
|
+
price_per_unit: float | None = None,
|
|
142
|
+
unit: str | None = None,
|
|
143
|
+
description: str | None = None,
|
|
144
|
+
) -> dict[str, Any]:
|
|
145
|
+
"""Prompt for one pricing plan (wizard-style)."""
|
|
146
|
+
resolved_type = (
|
|
147
|
+
(pricing_type or "").strip().upper()
|
|
148
|
+
or ask_select(
|
|
149
|
+
"Pricing type",
|
|
150
|
+
[
|
|
151
|
+
Choice(title="FLAT — fixed price per unit", value="FLAT"),
|
|
152
|
+
Choice(title="TIERED — volume tiers", value="TIERED"),
|
|
153
|
+
],
|
|
154
|
+
default="FLAT",
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
resolved_title = (title or "").strip() or ask_text("Plan title", required=True)
|
|
158
|
+
plan: dict[str, Any] = {
|
|
159
|
+
"title": resolved_title,
|
|
160
|
+
"pricingType": resolved_type,
|
|
161
|
+
}
|
|
162
|
+
resolved_description = (
|
|
163
|
+
description
|
|
164
|
+
if description is not None
|
|
165
|
+
else ask_text("Plan description (optional)", default="")
|
|
166
|
+
)
|
|
167
|
+
if _optional(resolved_description):
|
|
168
|
+
plan["description"] = resolved_description.strip()
|
|
169
|
+
|
|
170
|
+
if resolved_type == "FLAT":
|
|
171
|
+
if price_per_unit is not None and title is not None and unit is not None:
|
|
172
|
+
# Non-interactive-ish path with CLI flags.
|
|
173
|
+
plan["pricePerUnit"] = price_per_unit
|
|
174
|
+
plan["unit"] = (unit or "installations").strip() or "installations"
|
|
175
|
+
else:
|
|
176
|
+
price = ask_text(
|
|
177
|
+
"Price per unit (e.g. 0 or 10.00)",
|
|
178
|
+
default="" if price_per_unit is None else str(price_per_unit),
|
|
179
|
+
)
|
|
180
|
+
resolved_unit = ask_text(
|
|
181
|
+
"Unit (e.g. installations, seats)",
|
|
182
|
+
default=(unit or "installations"),
|
|
183
|
+
)
|
|
184
|
+
free_units = ask_text("Free units (optional)", default="")
|
|
185
|
+
free_period = ask_text("Free units period (optional)", default="")
|
|
186
|
+
if _optional(price):
|
|
187
|
+
plan["pricePerUnit"] = _coerce_number(price)
|
|
188
|
+
elif price_per_unit is not None:
|
|
189
|
+
plan["pricePerUnit"] = price_per_unit
|
|
190
|
+
else:
|
|
191
|
+
plan["pricePerUnit"] = 0
|
|
192
|
+
plan["unit"] = (resolved_unit or "installations").strip()
|
|
193
|
+
if _optional(free_units):
|
|
194
|
+
plan["freeUnits"] = _coerce_number(free_units)
|
|
195
|
+
if _optional(free_period):
|
|
196
|
+
plan["freeUnitsPeriod"] = free_period.strip()
|
|
197
|
+
else:
|
|
198
|
+
tiers: list[dict[str, Any]] = []
|
|
199
|
+
console.print("[dim]Add at least one tier.[/dim]")
|
|
200
|
+
while True:
|
|
201
|
+
tiers.append(prompt_pricing_tier())
|
|
202
|
+
if not ask_confirm("Add another tier?", default=False):
|
|
203
|
+
break
|
|
204
|
+
plan["tiers"] = tiers
|
|
205
|
+
return plan
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def prompt_app_bar(
|
|
209
|
+
*,
|
|
210
|
+
label: str | None = None,
|
|
211
|
+
location: str | None = None,
|
|
212
|
+
function_name: str | None = None,
|
|
213
|
+
iframe_url: str | None = None,
|
|
214
|
+
function_choices: list[str] | None = None,
|
|
215
|
+
) -> dict[str, Any]:
|
|
216
|
+
"""Prompt for one app bar (wizard-style)."""
|
|
217
|
+
resolved_location = (
|
|
218
|
+
(location or "").strip().upper()
|
|
219
|
+
or ask_select(
|
|
220
|
+
"Location",
|
|
221
|
+
[Choice(title=lbl, value=key) for key, lbl in APP_BAR_LOCATIONS],
|
|
222
|
+
default="RECORD_PREVIEW",
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
resolved_label = (label or "").strip() or ask_text("Label", required=True)
|
|
226
|
+
bar: dict[str, Any] = {
|
|
227
|
+
"location": resolved_location,
|
|
228
|
+
"label": resolved_label,
|
|
229
|
+
}
|
|
230
|
+
tooltip = ask_text("Tooltip label (optional)", default="")
|
|
231
|
+
if _optional(tooltip):
|
|
232
|
+
bar["tooltipLabel"] = tooltip.strip()
|
|
233
|
+
|
|
234
|
+
if resolved_location in ACTION_BASED_LOCATIONS:
|
|
235
|
+
description = ask_text("Description (optional)", default="")
|
|
236
|
+
action_label = ask_text(
|
|
237
|
+
"Action button label (optional)", default=resolved_label
|
|
238
|
+
)
|
|
239
|
+
if _optional(description):
|
|
240
|
+
bar["description"] = description.strip()
|
|
241
|
+
if _optional(action_label):
|
|
242
|
+
bar["actionLabel"] = action_label.strip()
|
|
243
|
+
|
|
244
|
+
delivery = ask_select(
|
|
245
|
+
"Action delivery",
|
|
246
|
+
[
|
|
247
|
+
Choice(title="SERVERLESS — local function", value="SERVERLESS"),
|
|
248
|
+
Choice(title="HTTP — external URL", value="HTTP"),
|
|
249
|
+
],
|
|
250
|
+
default="SERVERLESS",
|
|
251
|
+
)
|
|
252
|
+
if delivery == "HTTP":
|
|
253
|
+
url = ask_text("Webhook URL", required=True)
|
|
254
|
+
bar["webhook"] = {
|
|
255
|
+
"topic": "app.bar.triggered",
|
|
256
|
+
"deliveryMode": "HTTP",
|
|
257
|
+
"enabled": True,
|
|
258
|
+
"url": url.strip(),
|
|
259
|
+
}
|
|
260
|
+
else:
|
|
261
|
+
names = function_choices or []
|
|
262
|
+
if function_name and function_name.strip():
|
|
263
|
+
fn = function_name.strip()
|
|
264
|
+
elif names:
|
|
265
|
+
fn = ask_select(
|
|
266
|
+
"Function",
|
|
267
|
+
[Choice(title=n, value=n) for n in names],
|
|
268
|
+
default=names[0],
|
|
269
|
+
)
|
|
270
|
+
else:
|
|
271
|
+
fn = ask_text(
|
|
272
|
+
"Function name",
|
|
273
|
+
default="on-app-bar",
|
|
274
|
+
required=True,
|
|
275
|
+
)
|
|
276
|
+
bar["webhook"] = {
|
|
277
|
+
"topic": "app.bar.triggered",
|
|
278
|
+
"deliveryMode": "SERVERLESS",
|
|
279
|
+
"enabled": True,
|
|
280
|
+
"serverlessFunction": {"name": fn},
|
|
281
|
+
}
|
|
282
|
+
else:
|
|
283
|
+
url = (iframe_url or "").strip() or ask_text("Iframe URL", required=True)
|
|
284
|
+
icon = ask_text("Icon (optional)", default="")
|
|
285
|
+
bar["iframeUrl"] = url
|
|
286
|
+
if _optional(icon):
|
|
287
|
+
bar["icon"] = icon.strip()
|
|
288
|
+
return bar
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def prompt_lifecycle_hook(
|
|
292
|
+
*,
|
|
293
|
+
event: str | None = None,
|
|
294
|
+
function_name: str | None = None,
|
|
295
|
+
create_function: bool | None = None,
|
|
296
|
+
) -> dict[str, Any]:
|
|
297
|
+
"""Prompt for lifecycle hook options. Returns keys used by the scaffold."""
|
|
298
|
+
events = sorted(LIFECYCLE_HOOKS.keys())
|
|
299
|
+
resolved_event = (
|
|
300
|
+
(event or "").strip().lower()
|
|
301
|
+
or ask_select(
|
|
302
|
+
"Lifecycle event",
|
|
303
|
+
[
|
|
304
|
+
Choice(title=f"{stem} ({LIFECYCLE_HOOKS[stem][1]})", value=stem)
|
|
305
|
+
for stem in events
|
|
306
|
+
],
|
|
307
|
+
default="install",
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
if resolved_event not in LIFECYCLE_HOOKS:
|
|
311
|
+
raise ValueError(
|
|
312
|
+
f"Unknown lifecycle event '{resolved_event}'. "
|
|
313
|
+
f"Expected one of: {', '.join(events)}."
|
|
314
|
+
)
|
|
315
|
+
_manifest_key, topic = LIFECYCLE_HOOKS[resolved_event]
|
|
316
|
+
delivery = ask_select(
|
|
317
|
+
"Delivery mode",
|
|
318
|
+
[
|
|
319
|
+
Choice(title="SERVERLESS — local function", value="SERVERLESS"),
|
|
320
|
+
Choice(title="HTTP — external URL", value="HTTP"),
|
|
321
|
+
],
|
|
322
|
+
default="SERVERLESS",
|
|
323
|
+
)
|
|
324
|
+
enabled = ask_confirm("Enabled?", default=True)
|
|
325
|
+
result: dict[str, Any] = {
|
|
326
|
+
"event": resolved_event,
|
|
327
|
+
"topic": topic,
|
|
328
|
+
"deliveryMode": delivery,
|
|
329
|
+
"enabled": enabled,
|
|
330
|
+
"create_function": False,
|
|
331
|
+
"function_name": None,
|
|
332
|
+
"url": None,
|
|
333
|
+
}
|
|
334
|
+
if delivery == "HTTP":
|
|
335
|
+
result["url"] = ask_text("Webhook URL", required=True).strip()
|
|
336
|
+
else:
|
|
337
|
+
default_fn = f"on-{resolved_event}"
|
|
338
|
+
result["function_name"] = (
|
|
339
|
+
(function_name or "").strip()
|
|
340
|
+
or ask_text("Function name", default=default_fn, required=True)
|
|
341
|
+
)
|
|
342
|
+
result["create_function"] = (
|
|
343
|
+
create_function
|
|
344
|
+
if create_function is not None
|
|
345
|
+
else ask_confirm(
|
|
346
|
+
f"Scaffold function folder functions/{result['function_name']}/?",
|
|
347
|
+
default=True,
|
|
348
|
+
)
|
|
349
|
+
)
|
|
350
|
+
return result
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Thin wrappers around questionary with cancel handling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Any, Sequence
|
|
7
|
+
|
|
8
|
+
import questionary
|
|
9
|
+
import typer
|
|
10
|
+
from questionary import Choice, Style
|
|
11
|
+
|
|
12
|
+
PROMPT_STYLE = Style(
|
|
13
|
+
[
|
|
14
|
+
("qmark", "fg:#E74363 bold"),
|
|
15
|
+
("question", "bold"),
|
|
16
|
+
("answer", "fg:#E74363"),
|
|
17
|
+
("pointer", "fg:#E74363 bold"),
|
|
18
|
+
("highlighted", "fg:#E74363 bold"),
|
|
19
|
+
("selected", "fg:#66bb6a"),
|
|
20
|
+
]
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class WizardCancelled(typer.Exit):
|
|
25
|
+
"""Raised when the user cancels an interactive prompt."""
|
|
26
|
+
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
super().__init__(code=1)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _or_cancel(value: Any) -> Any:
|
|
32
|
+
if value is None:
|
|
33
|
+
raise WizardCancelled()
|
|
34
|
+
return value
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def ask_text(message: str, *, default: str = "", required: bool = False) -> str:
|
|
38
|
+
while True:
|
|
39
|
+
value = _or_cancel(
|
|
40
|
+
questionary.text(message, default=default, style=PROMPT_STYLE).ask()
|
|
41
|
+
)
|
|
42
|
+
text = str(value).strip()
|
|
43
|
+
if text or not required:
|
|
44
|
+
return text
|
|
45
|
+
print("This field is required.")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def require_text(
|
|
49
|
+
value: str | None,
|
|
50
|
+
message: str,
|
|
51
|
+
*,
|
|
52
|
+
default: str = "",
|
|
53
|
+
flag: str | None = None,
|
|
54
|
+
) -> str:
|
|
55
|
+
"""Return a stripped value, or interactively ask when missing.
|
|
56
|
+
|
|
57
|
+
In non-interactive environments (no TTY), raises ``ValueError`` instead of
|
|
58
|
+
prompting so CI/scripts still fail clearly.
|
|
59
|
+
"""
|
|
60
|
+
if value is not None and str(value).strip():
|
|
61
|
+
return str(value).strip()
|
|
62
|
+
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
|
63
|
+
hint = flag or message
|
|
64
|
+
raise ValueError(f"Missing required value: {hint}")
|
|
65
|
+
return ask_text(message, default=default, required=True)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def ask_confirm(message: str, *, default: bool = False) -> bool:
|
|
69
|
+
return bool(
|
|
70
|
+
_or_cancel(questionary.confirm(message, default=default, style=PROMPT_STYLE).ask())
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def ask_select(
|
|
75
|
+
message: str,
|
|
76
|
+
choices: Sequence[str | Choice],
|
|
77
|
+
*,
|
|
78
|
+
default: str | None = None,
|
|
79
|
+
) -> str:
|
|
80
|
+
kwargs: dict[str, Any] = {"style": PROMPT_STYLE}
|
|
81
|
+
if default is not None:
|
|
82
|
+
kwargs["default"] = default
|
|
83
|
+
return str(_or_cancel(questionary.select(message, choices=list(choices), **kwargs).ask()))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def ask_checkbox(
|
|
87
|
+
message: str,
|
|
88
|
+
choices: Sequence[str | Choice],
|
|
89
|
+
*,
|
|
90
|
+
min_selected: int = 0,
|
|
91
|
+
) -> list[str]:
|
|
92
|
+
while True:
|
|
93
|
+
selected = _or_cancel(
|
|
94
|
+
questionary.checkbox(message, choices=list(choices), style=PROMPT_STYLE).ask()
|
|
95
|
+
)
|
|
96
|
+
values = [str(item) for item in selected]
|
|
97
|
+
if len(values) >= min_selected:
|
|
98
|
+
return values
|
|
99
|
+
print(f"Select at least {min_selected}.")
|