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/local_app.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Helpers for local app YAML/JSON selection and discovery."""
|
|
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.project.app_manifest_template import APP_MANIFEST_NAMES
|
|
10
|
+
from caraer_cli.project.paths import (
|
|
11
|
+
LEGACY_PROJECT_FILE,
|
|
12
|
+
WORKSPACE_FILE,
|
|
13
|
+
app_manifest_path,
|
|
14
|
+
)
|
|
15
|
+
from caraer_cli.utils import load_structured_file
|
|
16
|
+
|
|
17
|
+
UUID_RE = re.compile(
|
|
18
|
+
r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
|
|
19
|
+
)
|
|
20
|
+
APP_FILE_NAMES = APP_MANIFEST_NAMES
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def looks_like_uuid(value: str) -> bool:
|
|
24
|
+
return bool(UUID_RE.match(value.strip()))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def looks_like_local_app_ref(value: str) -> bool:
|
|
28
|
+
"""Heuristic: path-like refs should be treated as local app files/dirs."""
|
|
29
|
+
text = value.strip()
|
|
30
|
+
if not text:
|
|
31
|
+
return False
|
|
32
|
+
if looks_like_uuid(text):
|
|
33
|
+
return False
|
|
34
|
+
path = Path(text)
|
|
35
|
+
if path.exists() or (Path.cwd() / text).exists():
|
|
36
|
+
return True
|
|
37
|
+
suffix = path.suffix.lower()
|
|
38
|
+
if suffix in {".json", ".yaml", ".yml"}:
|
|
39
|
+
return True
|
|
40
|
+
if "/" in text or "\\" in text:
|
|
41
|
+
return True
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _candidate_app_files_in_dir(directory: Path) -> list[Path]:
|
|
46
|
+
candidates: list[Path] = []
|
|
47
|
+
if (directory / WORKSPACE_FILE).is_file() or (directory / LEGACY_PROJECT_FILE).is_file():
|
|
48
|
+
candidates.append(app_manifest_path(directory))
|
|
49
|
+
for name in APP_FILE_NAMES:
|
|
50
|
+
candidates.append(directory / name)
|
|
51
|
+
candidates.append(directory / "src" / "app" / name)
|
|
52
|
+
return candidates
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def resolve_app_file_path(path: str | Path) -> Path:
|
|
56
|
+
"""Resolve a local app file, accepting an app directory or direct file path."""
|
|
57
|
+
raw = Path(path).expanduser()
|
|
58
|
+
resolved = raw.resolve() if raw.exists() else raw
|
|
59
|
+
|
|
60
|
+
if resolved.is_file():
|
|
61
|
+
return resolved.resolve()
|
|
62
|
+
|
|
63
|
+
if resolved.is_dir() or not resolved.suffix:
|
|
64
|
+
directory = resolved if resolved.is_dir() else raw
|
|
65
|
+
# Prefer resolving relative to cwd when the directory does not exist yet.
|
|
66
|
+
search_root = directory if directory.is_dir() else Path.cwd() / directory
|
|
67
|
+
if search_root.is_dir():
|
|
68
|
+
for candidate in _candidate_app_files_in_dir(search_root.resolve()):
|
|
69
|
+
if candidate.is_file():
|
|
70
|
+
return candidate.resolve()
|
|
71
|
+
raise FileNotFoundError(
|
|
72
|
+
f"No app.caraer.yaml found under app directory: {search_root}"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if not resolved.exists():
|
|
76
|
+
raise FileNotFoundError(f"Local app file not found: {resolved}")
|
|
77
|
+
raise ValueError(f"Local app path is not a file: {resolved}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def load_local_app(path: str | Path) -> dict[str, Any]:
|
|
81
|
+
payload = load_structured_file(str(resolve_app_file_path(path)))
|
|
82
|
+
if not isinstance(payload, dict):
|
|
83
|
+
raise ValueError("Local app file must contain a YAML/JSON object.")
|
|
84
|
+
return payload
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def local_app_summary(path: Path, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
88
|
+
data = payload if payload is not None else load_local_app(path)
|
|
89
|
+
return {
|
|
90
|
+
"source": "local",
|
|
91
|
+
"path": str(path),
|
|
92
|
+
"name": data.get("name"),
|
|
93
|
+
"label": data.get("label"),
|
|
94
|
+
"uuid": data.get("uuid"),
|
|
95
|
+
"privateApp": data.get("privateApp"),
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def discover_local_app_files(start: Path | None = None) -> list[Path]:
|
|
100
|
+
"""Find candidate local app definition files near the current directory."""
|
|
101
|
+
root = (start or Path.cwd()).resolve()
|
|
102
|
+
found: list[Path] = []
|
|
103
|
+
seen: set[Path] = set()
|
|
104
|
+
|
|
105
|
+
def _add(path: Path) -> None:
|
|
106
|
+
if not path.is_file():
|
|
107
|
+
return
|
|
108
|
+
# Ignore function manifests.
|
|
109
|
+
if path.name == "function.caraer.json":
|
|
110
|
+
return
|
|
111
|
+
resolved = path.resolve()
|
|
112
|
+
if resolved in seen:
|
|
113
|
+
return
|
|
114
|
+
seen.add(resolved)
|
|
115
|
+
found.append(resolved)
|
|
116
|
+
|
|
117
|
+
for name in APP_FILE_NAMES:
|
|
118
|
+
_add(root / name)
|
|
119
|
+
|
|
120
|
+
for name in APP_FILE_NAMES:
|
|
121
|
+
_add(root / "src" / "app" / name)
|
|
122
|
+
|
|
123
|
+
# Child app folders: <dir>/caraer.json → src/app/app.caraer.yaml
|
|
124
|
+
for marker in (WORKSPACE_FILE, LEGACY_PROJECT_FILE):
|
|
125
|
+
for workspace in sorted(root.glob(f"*/{marker}")):
|
|
126
|
+
_add(app_manifest_path(workspace.parent))
|
|
127
|
+
|
|
128
|
+
# Also allow immediate child app manifests.
|
|
129
|
+
for name in APP_FILE_NAMES:
|
|
130
|
+
for path in sorted(root.glob(f"*/{name}")):
|
|
131
|
+
_add(path)
|
|
132
|
+
for path in sorted(root.glob(f"*/src/app/{name}")):
|
|
133
|
+
_add(path)
|
|
134
|
+
|
|
135
|
+
return found
|
caraer_cli/main.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from caraer_cli import __version__
|
|
6
|
+
from caraer_cli.commands import (
|
|
7
|
+
apps,
|
|
8
|
+
auth,
|
|
9
|
+
company,
|
|
10
|
+
profile,
|
|
11
|
+
publish,
|
|
12
|
+
sandbox,
|
|
13
|
+
skill,
|
|
14
|
+
webhooks,
|
|
15
|
+
)
|
|
16
|
+
from caraer_cli.completion_callbacks import complete_output_format, complete_profile
|
|
17
|
+
from caraer_cli.context import AppContext
|
|
18
|
+
from caraer_cli.errors import ApiError
|
|
19
|
+
from caraer_cli.formatters.output import print_error
|
|
20
|
+
from caraer_cli.state.config import active_profile, load_config
|
|
21
|
+
from caraer_cli.state.session import get_session_token
|
|
22
|
+
|
|
23
|
+
app = typer.Typer(
|
|
24
|
+
help="Caraer external developer CLI.",
|
|
25
|
+
no_args_is_help=True,
|
|
26
|
+
add_completion=False,
|
|
27
|
+
)
|
|
28
|
+
app.add_typer(auth.app, name="auth")
|
|
29
|
+
app.add_typer(company.app, name="company")
|
|
30
|
+
app.add_typer(apps.app, name="apps")
|
|
31
|
+
app.add_typer(webhooks.app, name="webhooks")
|
|
32
|
+
app.add_typer(publish.app, name="publish")
|
|
33
|
+
app.add_typer(sandbox.app, name="sandbox")
|
|
34
|
+
app.add_typer(profile.app, name="profile")
|
|
35
|
+
app.add_typer(skill.app, name="skill")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _version_callback(value: bool) -> None:
|
|
39
|
+
if value:
|
|
40
|
+
typer.echo(f"caraer-cli {__version__}")
|
|
41
|
+
raise typer.Exit()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@app.callback()
|
|
45
|
+
def root(
|
|
46
|
+
ctx: typer.Context,
|
|
47
|
+
profile: str = typer.Option(
|
|
48
|
+
None,
|
|
49
|
+
"--profile",
|
|
50
|
+
help="Override config profile for this command.",
|
|
51
|
+
autocompletion=complete_profile,
|
|
52
|
+
),
|
|
53
|
+
output: str = typer.Option(
|
|
54
|
+
None,
|
|
55
|
+
"--output",
|
|
56
|
+
help="table|json|yaml",
|
|
57
|
+
autocompletion=complete_output_format,
|
|
58
|
+
),
|
|
59
|
+
debug: bool = typer.Option(False, "--debug", help="Show debug details on API errors."),
|
|
60
|
+
version: bool = typer.Option(
|
|
61
|
+
False,
|
|
62
|
+
"--version",
|
|
63
|
+
help="Show the CLI version and exit.",
|
|
64
|
+
callback=_version_callback,
|
|
65
|
+
is_eager=True,
|
|
66
|
+
),
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Caraer external developer CLI."""
|
|
69
|
+
# During shell completion Click/Typer sets resilient_parsing; keep side effects light.
|
|
70
|
+
if ctx.resilient_parsing:
|
|
71
|
+
ctx.obj = None
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
cfg = load_config()
|
|
75
|
+
profile_name, profile_cfg = active_profile(cfg, profile)
|
|
76
|
+
selected_output = output or profile_cfg.output
|
|
77
|
+
token = get_session_token(profile_name)
|
|
78
|
+
ctx.obj = AppContext(
|
|
79
|
+
config=cfg,
|
|
80
|
+
profile_name=profile_name,
|
|
81
|
+
profile=profile_cfg,
|
|
82
|
+
token=token,
|
|
83
|
+
output=selected_output,
|
|
84
|
+
debug=debug,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def run() -> None:
|
|
89
|
+
try:
|
|
90
|
+
app()
|
|
91
|
+
except ApiError as exc:
|
|
92
|
+
print_error(str(exc))
|
|
93
|
+
raise SystemExit(2) from None
|
|
94
|
+
except ValueError as exc:
|
|
95
|
+
print_error(str(exc))
|
|
96
|
+
raise SystemExit(2) from None
|
|
97
|
+
except typer.Exit:
|
|
98
|
+
raise
|
|
99
|
+
except SystemExit:
|
|
100
|
+
raise
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
run()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Pydantic models for API envelopes."""
|
caraer_cli/models/api.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CaraerValidationError(BaseModel):
|
|
9
|
+
field: str | None = None
|
|
10
|
+
message: str | None = None
|
|
11
|
+
correctionSuggestion: str | None = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ErrorEnvelope(BaseModel):
|
|
15
|
+
message: str = "Request failed."
|
|
16
|
+
status: int = 500
|
|
17
|
+
errors: list[CaraerValidationError] | None = None
|
|
18
|
+
roles: list[str] | None = None
|
|
19
|
+
scopes: list[str] | None = None
|
|
20
|
+
stackTrace: str | None = None
|
|
21
|
+
requestId: str | None = None
|
|
22
|
+
correlationId: str | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SuccessEnvelope(BaseModel):
|
|
26
|
+
message: str = "Success"
|
|
27
|
+
data: Any = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PaginationEnvelope(SuccessEnvelope):
|
|
31
|
+
total: int = 0
|
|
32
|
+
page: int = 1
|
|
33
|
+
perPage: int = 25
|
|
34
|
+
lastPage: int = 1
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AuthTokenModel(BaseModel):
|
|
38
|
+
token: str | None = None
|
|
39
|
+
expiresAt: int | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CompanyModel(BaseModel):
|
|
43
|
+
uuid: str
|
|
44
|
+
name: str | None = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class UserModel(BaseModel):
|
|
48
|
+
uuid: str
|
|
49
|
+
email: str | None = None
|
|
50
|
+
globalRole: str | None = None
|
|
51
|
+
token: AuthTokenModel | None = None
|
|
52
|
+
companies: list[CompanyModel] = Field(default_factory=list)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Local Caraer developer project helpers."""
|
|
2
|
+
|
|
3
|
+
from caraer_cli.project.schema import (
|
|
4
|
+
PLATFORM_VERSION,
|
|
5
|
+
FunctionManifest,
|
|
6
|
+
ProjectConfig,
|
|
7
|
+
load_project_config,
|
|
8
|
+
save_project_config,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"PLATFORM_VERSION",
|
|
13
|
+
"FunctionManifest",
|
|
14
|
+
"ProjectConfig",
|
|
15
|
+
"load_project_config",
|
|
16
|
+
"save_project_config",
|
|
17
|
+
]
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Discover / write modular appBars files under src/app/app-bars/."""
|
|
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 caraer_cli.project.paths import app_bars_dir
|
|
11
|
+
from caraer_cli.project.schema import ProjectConfig
|
|
12
|
+
|
|
13
|
+
LOCAL_APP_BAR_KEYS = (
|
|
14
|
+
"uuid",
|
|
15
|
+
"location",
|
|
16
|
+
"label",
|
|
17
|
+
"tooltipLabel",
|
|
18
|
+
"description",
|
|
19
|
+
"actionLabel",
|
|
20
|
+
"iframeUrl",
|
|
21
|
+
"icon",
|
|
22
|
+
"settingsSchema",
|
|
23
|
+
"webhook",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
ACTION_BASED_LOCATIONS = frozenset({"RECORD_PREVIEW", "RECORD_OVERVIEW"})
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _slug(value: str) -> str:
|
|
30
|
+
text = value.strip().lower()
|
|
31
|
+
text = re.sub(r"[^a-z0-9]+", "-", text)
|
|
32
|
+
return text.strip("-") or "app-bar"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def app_bar_filename(item: dict[str, Any]) -> str:
|
|
36
|
+
location = _slug(str(item.get("location") or "bar"))
|
|
37
|
+
label = _slug(str(item.get("label") or "app-bar"))
|
|
38
|
+
return f"{location}-{label}.json"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def app_bar_identity(item: dict[str, Any]) -> str:
|
|
42
|
+
location = str(item.get("location") or "").strip().upper()
|
|
43
|
+
label = str(item.get("label") or "").strip().lower()
|
|
44
|
+
return f"{location}|{label}"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _sanitize_webhook(webhook: Any) -> dict[str, Any] | None:
|
|
48
|
+
if not isinstance(webhook, dict):
|
|
49
|
+
return None
|
|
50
|
+
cleaned: dict[str, Any] = {}
|
|
51
|
+
for key in (
|
|
52
|
+
"uuid",
|
|
53
|
+
"topic",
|
|
54
|
+
"deliveryMode",
|
|
55
|
+
"url",
|
|
56
|
+
"description",
|
|
57
|
+
"enabled",
|
|
58
|
+
"webhookFormat",
|
|
59
|
+
"serverlessFunction",
|
|
60
|
+
):
|
|
61
|
+
if key in webhook and webhook[key] is not None:
|
|
62
|
+
cleaned[key] = webhook[key]
|
|
63
|
+
sf = cleaned.get("serverlessFunction")
|
|
64
|
+
if isinstance(sf, dict):
|
|
65
|
+
sf_out: dict[str, Any] = {}
|
|
66
|
+
if sf.get("name"):
|
|
67
|
+
sf_out["name"] = sf["name"]
|
|
68
|
+
elif sf.get("uuid"):
|
|
69
|
+
sf_out["uuid"] = sf["uuid"]
|
|
70
|
+
if sf_out:
|
|
71
|
+
cleaned["serverlessFunction"] = sf_out
|
|
72
|
+
else:
|
|
73
|
+
cleaned.pop("serverlessFunction", None)
|
|
74
|
+
return cleaned or None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def sanitize_app_bar(item: dict[str, Any]) -> dict[str, Any]:
|
|
78
|
+
payload: dict[str, Any] = {}
|
|
79
|
+
for key in LOCAL_APP_BAR_KEYS:
|
|
80
|
+
if key not in item or item[key] is None:
|
|
81
|
+
continue
|
|
82
|
+
if key == "webhook":
|
|
83
|
+
webhook = _sanitize_webhook(item[key])
|
|
84
|
+
if webhook:
|
|
85
|
+
payload["webhook"] = webhook
|
|
86
|
+
else:
|
|
87
|
+
payload[key] = item[key]
|
|
88
|
+
return payload
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def discover_local_app_bars(
|
|
92
|
+
root: Path, config: ProjectConfig
|
|
93
|
+
) -> list[tuple[Path, dict[str, Any]]]:
|
|
94
|
+
base = app_bars_dir(root, config.srcDir)
|
|
95
|
+
if not base.is_dir():
|
|
96
|
+
return []
|
|
97
|
+
found: list[tuple[Path, dict[str, Any]]] = []
|
|
98
|
+
for path in sorted(base.glob("*.json")):
|
|
99
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
100
|
+
if isinstance(data, dict) and data.get("label") and data.get("location"):
|
|
101
|
+
found.append((path, sanitize_app_bar(data)))
|
|
102
|
+
return found
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def write_app_bars_files(
|
|
106
|
+
root: Path, config: ProjectConfig, items: list[dict[str, Any]]
|
|
107
|
+
) -> int:
|
|
108
|
+
base = app_bars_dir(root, config.srcDir)
|
|
109
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
for existing in base.glob("*.json"):
|
|
111
|
+
existing.unlink()
|
|
112
|
+
count = 0
|
|
113
|
+
for item in items:
|
|
114
|
+
if not isinstance(item, dict):
|
|
115
|
+
continue
|
|
116
|
+
sanitized = sanitize_app_bar(item)
|
|
117
|
+
if not app_bar_identity(sanitized).strip("|"):
|
|
118
|
+
continue
|
|
119
|
+
path = base / app_bar_filename(sanitized)
|
|
120
|
+
path.write_text(json.dumps(sanitized, indent=2) + "\n", encoding="utf-8")
|
|
121
|
+
count += 1
|
|
122
|
+
return count
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def resolve_app_bar_functions(
|
|
126
|
+
item: dict[str, Any],
|
|
127
|
+
*,
|
|
128
|
+
fn_by_name: dict[str, str],
|
|
129
|
+
strict: bool = True,
|
|
130
|
+
) -> dict[str, Any] | None:
|
|
131
|
+
out = dict(item)
|
|
132
|
+
out.pop("uuid", None)
|
|
133
|
+
webhook = out.get("webhook")
|
|
134
|
+
if not isinstance(webhook, dict):
|
|
135
|
+
return out
|
|
136
|
+
webhook = dict(webhook)
|
|
137
|
+
webhook.pop("uuid", None)
|
|
138
|
+
sf = webhook.get("serverlessFunction")
|
|
139
|
+
if isinstance(sf, dict):
|
|
140
|
+
if sf.get("uuid"):
|
|
141
|
+
webhook["serverlessFunction"] = {"uuid": sf["uuid"]}
|
|
142
|
+
else:
|
|
143
|
+
name = sf.get("name")
|
|
144
|
+
if name and name in fn_by_name:
|
|
145
|
+
webhook["serverlessFunction"] = {"uuid": fn_by_name[name]}
|
|
146
|
+
elif name:
|
|
147
|
+
if strict:
|
|
148
|
+
raise ValueError(
|
|
149
|
+
f"App bar references function '{name}' but no UUID is known. "
|
|
150
|
+
"Push functions first."
|
|
151
|
+
)
|
|
152
|
+
return None
|
|
153
|
+
out["webhook"] = webhook
|
|
154
|
+
return out
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Render and locate local app manifests (YAML preferred)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
APP_MANIFEST_YAML = "app.caraer.yaml"
|
|
10
|
+
APP_MANIFEST_JSON = "app.caraer.json" # legacy; still loaded if present
|
|
11
|
+
APP_MANIFEST_NAMES = (APP_MANIFEST_YAML, APP_MANIFEST_JSON, "app.yaml", "app.yml", "app.json")
|
|
12
|
+
|
|
13
|
+
_EXAMPLE_REQUIRED_SCOPES = """\
|
|
14
|
+
# Example scopes (uncomment and edit):
|
|
15
|
+
# requiredScopes:
|
|
16
|
+
# - tools.forms.all
|
|
17
|
+
# - records.candidate.properties_all
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
_EXAMPLE_SETTINGS_SCHEMA = """\
|
|
21
|
+
# Edit settingsSchema above, or: caraer apps add-setting
|
|
22
|
+
# Optional modular files also work: src/app/settings/<name>.json (--modular)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
_EXAMPLE_PRICING_PLANS = """\
|
|
26
|
+
# Edit pricingPlans above, or: caraer apps add-pricing-plan
|
|
27
|
+
# Optional modular files: src/app/pricing/<slug>.json (--modular)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
_EXAMPLE_APP_BARS = """\
|
|
31
|
+
# Edit appBars above, or: caraer apps add-app-bar
|
|
32
|
+
# Optional modular files: src/app/app-bars/<slug>.json (--modular)
|
|
33
|
+
# Lifecycle hooks: src/app/lifecycle/ + functions/on-* (caraer apps add-lifecycle-hook)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def render_app_manifest(payload: dict[str, Any], *, include_examples: bool = True) -> str:
|
|
38
|
+
"""Serialize an app manifest as YAML, optionally with commented-out examples."""
|
|
39
|
+
from caraer_cli.project.json_schemas import YAML_LANGUAGE_SERVER_COMMENT
|
|
40
|
+
|
|
41
|
+
data = dict(payload)
|
|
42
|
+
for key in ("requiredScopes", "settingsSchema", "pricingPlans", "appBars"):
|
|
43
|
+
data.setdefault(key, [])
|
|
44
|
+
|
|
45
|
+
body = yaml.safe_dump(
|
|
46
|
+
data,
|
|
47
|
+
sort_keys=False,
|
|
48
|
+
allow_unicode=True,
|
|
49
|
+
default_flow_style=False,
|
|
50
|
+
).rstrip() + "\n"
|
|
51
|
+
header = YAML_LANGUAGE_SERVER_COMMENT + "\n"
|
|
52
|
+
if not include_examples:
|
|
53
|
+
return header + body
|
|
54
|
+
|
|
55
|
+
lines = body.splitlines()
|
|
56
|
+
out: list[str] = [YAML_LANGUAGE_SERVER_COMMENT]
|
|
57
|
+
for line in lines:
|
|
58
|
+
out.append(line)
|
|
59
|
+
stripped = line.strip()
|
|
60
|
+
if stripped == "requiredScopes: []":
|
|
61
|
+
out.append(_EXAMPLE_REQUIRED_SCOPES.rstrip("\n"))
|
|
62
|
+
elif stripped == "settingsSchema: []":
|
|
63
|
+
out.append(_EXAMPLE_SETTINGS_SCHEMA.rstrip("\n"))
|
|
64
|
+
elif stripped == "pricingPlans: []":
|
|
65
|
+
out.append(_EXAMPLE_PRICING_PLANS.rstrip("\n"))
|
|
66
|
+
elif stripped == "appBars: []":
|
|
67
|
+
out.append(_EXAMPLE_APP_BARS.rstrip("\n"))
|
|
68
|
+
return "\n".join(out) + "\n"
|