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,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from caraer_cli.api import apps as apps_api
|
|
6
|
+
from caraer_cli.context import AppContext
|
|
7
|
+
from caraer_cli.formatters.output import print_data, print_success
|
|
8
|
+
from caraer_cli.resolve import resolve_app_uuid
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(help="Publishing commands for app creators.", no_args_is_help=True)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.command("submit")
|
|
14
|
+
def submit(
|
|
15
|
+
ctx: typer.Context,
|
|
16
|
+
app_uuid: str | None = typer.Option(None, "--app", "--app-uuid", help="App UUID (defaults to selected app)."),
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Submit the selected app for marketplace review."""
|
|
19
|
+
app_ctx: AppContext = ctx.obj
|
|
20
|
+
resolved = resolve_app_uuid(app_ctx, app_uuid)
|
|
21
|
+
response = apps_api.submit_for_review(app_ctx.api_client(), resolved)
|
|
22
|
+
print_success("Submitted app for review.")
|
|
23
|
+
print_data(response.get("data"), app_ctx.output)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@app.command("status")
|
|
27
|
+
def status(
|
|
28
|
+
ctx: typer.Context,
|
|
29
|
+
app_uuid: str | None = typer.Option(None, "--app", "--app-uuid", help="App UUID (defaults to selected app)."),
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Show publish state and creator-visible feedback for the selected app."""
|
|
32
|
+
app_ctx: AppContext = ctx.obj
|
|
33
|
+
resolved = resolve_app_uuid(app_ctx, app_uuid)
|
|
34
|
+
response = apps_api.get_public_app(app_ctx.api_client(), resolved)
|
|
35
|
+
data = response.get("data", {})
|
|
36
|
+
publish_data = data.get("appPublish", {})
|
|
37
|
+
if isinstance(publish_data, dict):
|
|
38
|
+
# reviewerNotes are SUPER_ADMIN-only; drop defensively if ever present.
|
|
39
|
+
publish_data = {k: v for k, v in publish_data.items() if k != "reviewerNotes"}
|
|
40
|
+
print_data(publish_data, app_ctx.output)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from caraer_cli.api import sandboxes as sandboxes_api
|
|
6
|
+
from caraer_cli.context import AppContext
|
|
7
|
+
from caraer_cli.formatters.output import print_data, print_success
|
|
8
|
+
from caraer_cli.state.config import load_config, save_config
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(help="Developer sandbox commands.", no_args_is_help=True)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.command("create")
|
|
14
|
+
def create_sandbox(
|
|
15
|
+
ctx: typer.Context,
|
|
16
|
+
name: str | None = typer.Option(None, "--name", help="Sandbox name (slug). Prompted if omitted."),
|
|
17
|
+
label: str = typer.Option("", "--label", help="Display label (defaults to --name)."),
|
|
18
|
+
) -> None:
|
|
19
|
+
"""Clone the selected company's Neo4j database into a developer sandbox."""
|
|
20
|
+
from caraer_cli.wizard.prompts import require_text
|
|
21
|
+
|
|
22
|
+
app_ctx: AppContext = ctx.obj
|
|
23
|
+
if not app_ctx.profile.company_uuid:
|
|
24
|
+
raise ValueError("Select a company first with 'caraer company select <uuid>'.")
|
|
25
|
+
name = require_text(name, "Sandbox name", flag="--name")
|
|
26
|
+
response = sandboxes_api.create_sandbox(
|
|
27
|
+
app_ctx.api_client(),
|
|
28
|
+
{"name": name, "label": label or name},
|
|
29
|
+
)
|
|
30
|
+
data = response.get("data") or {}
|
|
31
|
+
print_success(
|
|
32
|
+
"Created sandbox DB clone for the selected company. "
|
|
33
|
+
"Activate with 'caraer sandbox use <sandbox-uuid>' "
|
|
34
|
+
"(sends X-Caraer-Sandbox-Uuid; company identity stays the owner)."
|
|
35
|
+
)
|
|
36
|
+
print_data(data, app_ctx.output)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@app.command("list")
|
|
40
|
+
def list_sandboxes(ctx: typer.Context) -> None:
|
|
41
|
+
"""List sandboxes for the owner company (ignores active sandbox override)."""
|
|
42
|
+
app_ctx: AppContext = ctx.obj
|
|
43
|
+
if not app_ctx.profile.company_uuid:
|
|
44
|
+
raise ValueError("Select a company first with 'caraer company select <uuid>'.")
|
|
45
|
+
response = sandboxes_api.list_sandboxes(app_ctx.api_client())
|
|
46
|
+
print_data(response.get("data"), app_ctx.output)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@app.command("use")
|
|
50
|
+
def use_sandbox(
|
|
51
|
+
ctx: typer.Context,
|
|
52
|
+
sandbox_uuid: str | None = typer.Argument(
|
|
53
|
+
None,
|
|
54
|
+
help="Sandbox UUID (DeveloperSandbox uuid). Prompted if omitted.",
|
|
55
|
+
),
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Activate a sandbox via X-Caraer-Sandbox-Uuid (overrides Neo4j databaseid only)."""
|
|
58
|
+
from caraer_cli.wizard.prompts import require_text
|
|
59
|
+
|
|
60
|
+
app_ctx: AppContext = ctx.obj
|
|
61
|
+
if not app_ctx.profile.company_uuid:
|
|
62
|
+
raise ValueError("Select the owner company first with 'caraer company select <uuid>'.")
|
|
63
|
+
sandbox_uuid = require_text(sandbox_uuid, "Sandbox UUID", flag="sandbox-uuid")
|
|
64
|
+
response = sandboxes_api.get_sandbox(app_ctx.api_client(), sandbox_uuid)
|
|
65
|
+
data = response.get("data") or {}
|
|
66
|
+
owner = data.get("ownerCompanyUuid")
|
|
67
|
+
if owner and str(owner) != str(app_ctx.profile.company_uuid):
|
|
68
|
+
raise ValueError(
|
|
69
|
+
f"Sandbox owner {owner} does not match selected company "
|
|
70
|
+
f"{app_ctx.profile.company_uuid}. Select the owner company first."
|
|
71
|
+
)
|
|
72
|
+
cfg = load_config()
|
|
73
|
+
profile = cfg.profiles[app_ctx.profile_name]
|
|
74
|
+
profile.sandbox_uuid = str(data.get("uuid") or sandbox_uuid)
|
|
75
|
+
save_config(cfg)
|
|
76
|
+
print_success(
|
|
77
|
+
f"Using sandbox {profile.sandbox_uuid} "
|
|
78
|
+
f"(company {profile.company_uuid}, db override). "
|
|
79
|
+
"API calls will send X-Caraer-Sandbox-Uuid."
|
|
80
|
+
)
|
|
81
|
+
print_data(data, app_ctx.output)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@app.command("clear")
|
|
85
|
+
def clear_sandbox(ctx: typer.Context) -> None:
|
|
86
|
+
"""Stop sending X-Caraer-Sandbox-Uuid (return to the owner company database)."""
|
|
87
|
+
app_ctx: AppContext = ctx.obj
|
|
88
|
+
cfg = load_config()
|
|
89
|
+
profile = cfg.profiles[app_ctx.profile_name]
|
|
90
|
+
profile.sandbox_uuid = None
|
|
91
|
+
save_config(cfg)
|
|
92
|
+
print_success("Cleared sandbox override; requests use the selected company database.")
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Install Cursor Agent skills shipped with caraer-cli."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from importlib import resources
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from caraer_cli.formatters.output import print_error, print_success, print_warning
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(help="Cursor / AI IDE skill helpers.", no_args_is_help=True)
|
|
14
|
+
|
|
15
|
+
SKILL_ID = "caraer-apps"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _repo_skill_dir() -> Path | None:
|
|
19
|
+
"""Dev checkout: <repo>/skills/caraer-apps next to the package."""
|
|
20
|
+
here = Path(__file__).resolve()
|
|
21
|
+
for parent in here.parents:
|
|
22
|
+
candidate = parent / "skills" / SKILL_ID
|
|
23
|
+
if candidate.is_dir() and (candidate / "SKILL.md").is_file():
|
|
24
|
+
return candidate
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _copy_packaged_skill(dest: Path) -> bool:
|
|
29
|
+
try:
|
|
30
|
+
root = resources.files("caraer_cli") / "skills" / SKILL_ID
|
|
31
|
+
except (TypeError, FileNotFoundError, ModuleNotFoundError):
|
|
32
|
+
return False
|
|
33
|
+
try:
|
|
34
|
+
if not root.is_dir():
|
|
35
|
+
return False
|
|
36
|
+
skill_md = root / "SKILL.md"
|
|
37
|
+
if not skill_md.is_file():
|
|
38
|
+
return False
|
|
39
|
+
with resources.as_file(root) as path:
|
|
40
|
+
shutil.copytree(path, dest)
|
|
41
|
+
return True
|
|
42
|
+
except (FileNotFoundError, OSError, NotADirectoryError):
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def resolve_skill_source() -> Path:
|
|
47
|
+
"""Return a real filesystem path for read-only inspection (path command)."""
|
|
48
|
+
repo = _repo_skill_dir()
|
|
49
|
+
if repo is not None:
|
|
50
|
+
return repo
|
|
51
|
+
try:
|
|
52
|
+
root = resources.files("caraer_cli") / "skills" / SKILL_ID
|
|
53
|
+
with resources.as_file(root) as path:
|
|
54
|
+
if path.is_dir() and (path / "SKILL.md").is_file():
|
|
55
|
+
# as_file may be temporary; prefer reporting packaged logical path
|
|
56
|
+
return Path(path)
|
|
57
|
+
except (TypeError, FileNotFoundError, ModuleNotFoundError, OSError):
|
|
58
|
+
pass
|
|
59
|
+
raise FileNotFoundError(
|
|
60
|
+
f"Skill '{SKILL_ID}' not found. Reinstall caraer-cli or clone "
|
|
61
|
+
"Caraer-HQ/caraer-cli."
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def default_user_skills_dir() -> Path:
|
|
66
|
+
return Path.home() / ".cursor" / "skills"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def project_skills_dir(cwd: Path | None = None) -> Path:
|
|
70
|
+
return (cwd or Path.cwd()) / ".cursor" / "skills"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def install_skill(
|
|
74
|
+
*,
|
|
75
|
+
project: bool = False,
|
|
76
|
+
target_root: Path | None = None,
|
|
77
|
+
force: bool = False,
|
|
78
|
+
) -> Path:
|
|
79
|
+
root = target_root or (
|
|
80
|
+
project_skills_dir() if project else default_user_skills_dir()
|
|
81
|
+
)
|
|
82
|
+
dest = root / SKILL_ID
|
|
83
|
+
if dest.exists():
|
|
84
|
+
if not force:
|
|
85
|
+
raise FileExistsError(
|
|
86
|
+
f"{dest} already exists. Pass --force to replace it."
|
|
87
|
+
)
|
|
88
|
+
if dest.is_dir():
|
|
89
|
+
shutil.rmtree(dest)
|
|
90
|
+
else:
|
|
91
|
+
dest.unlink()
|
|
92
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
|
|
94
|
+
if _copy_packaged_skill(dest):
|
|
95
|
+
return dest
|
|
96
|
+
|
|
97
|
+
repo = _repo_skill_dir()
|
|
98
|
+
if repo is not None:
|
|
99
|
+
shutil.copytree(repo, dest)
|
|
100
|
+
return dest
|
|
101
|
+
|
|
102
|
+
raise FileNotFoundError(
|
|
103
|
+
f"Skill '{SKILL_ID}' not found. Reinstall caraer-cli or clone "
|
|
104
|
+
"Caraer-HQ/caraer-cli."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@app.command("install")
|
|
109
|
+
def install_cmd(
|
|
110
|
+
project: bool = typer.Option(
|
|
111
|
+
False,
|
|
112
|
+
"--project",
|
|
113
|
+
help="Install into ./.cursor/skills (current directory) instead of ~/.cursor/skills.",
|
|
114
|
+
),
|
|
115
|
+
force: bool = typer.Option(
|
|
116
|
+
False,
|
|
117
|
+
"--force",
|
|
118
|
+
help="Replace an existing caraer-apps skill directory.",
|
|
119
|
+
),
|
|
120
|
+
path: Path | None = typer.Option(
|
|
121
|
+
None,
|
|
122
|
+
"--path",
|
|
123
|
+
help="Custom skills root directory (contains caraer-apps/).",
|
|
124
|
+
),
|
|
125
|
+
) -> None:
|
|
126
|
+
"""Install the Caraer Apps Cursor skill for AI IDEs."""
|
|
127
|
+
try:
|
|
128
|
+
dest = install_skill(project=project, target_root=path, force=force)
|
|
129
|
+
except FileExistsError as exc:
|
|
130
|
+
print_error(str(exc))
|
|
131
|
+
raise typer.Exit(1) from exc
|
|
132
|
+
except FileNotFoundError as exc:
|
|
133
|
+
print_error(str(exc))
|
|
134
|
+
raise typer.Exit(1) from exc
|
|
135
|
+
|
|
136
|
+
print_success(f"Installed skill → {dest}")
|
|
137
|
+
print_warning(
|
|
138
|
+
"Restart Cursor or start a new agent chat so the skill is discovered."
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@app.command("path")
|
|
143
|
+
def path_cmd() -> None:
|
|
144
|
+
"""Print the on-disk source path for the shipped caraer-apps skill."""
|
|
145
|
+
try:
|
|
146
|
+
source = resolve_skill_source()
|
|
147
|
+
except FileNotFoundError as exc:
|
|
148
|
+
print_error(str(exc))
|
|
149
|
+
raise typer.Exit(1) from exc
|
|
150
|
+
typer.echo(str(source))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@app.command("list")
|
|
154
|
+
def list_cmd() -> None:
|
|
155
|
+
"""Show install locations and whether caraer-apps is present."""
|
|
156
|
+
try:
|
|
157
|
+
source = resolve_skill_source()
|
|
158
|
+
typer.echo(f"source\t{source}")
|
|
159
|
+
except FileNotFoundError as exc:
|
|
160
|
+
typer.echo(f"source\tMISSING ({exc})")
|
|
161
|
+
|
|
162
|
+
user = default_user_skills_dir() / SKILL_ID
|
|
163
|
+
project = project_skills_dir() / SKILL_ID
|
|
164
|
+
typer.echo(f"user\t{user}\t{'yes' if user.is_dir() else 'no'}")
|
|
165
|
+
typer.echo(f"project\t{project}\t{'yes' if project.is_dir() else 'no'}")
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from caraer_cli.api import webhooks as webhook_api
|
|
6
|
+
from caraer_cli.context import AppContext
|
|
7
|
+
from caraer_cli.formatters.output import print_data
|
|
8
|
+
from caraer_cli.resolve import resolve_app_uuid
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(help="Webhook discovery and test helpers.", no_args_is_help=True)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.command("formats")
|
|
14
|
+
def formats(
|
|
15
|
+
ctx: typer.Context,
|
|
16
|
+
app_uuid: str | None = typer.Option(
|
|
17
|
+
None, "--app", "--app-uuid", help="App UUID (defaults to selected app)."
|
|
18
|
+
),
|
|
19
|
+
) -> None:
|
|
20
|
+
"""List available webhook payload formats."""
|
|
21
|
+
app_ctx: AppContext = ctx.obj
|
|
22
|
+
resolved = resolve_app_uuid(app_ctx, app_uuid)
|
|
23
|
+
response = webhook_api.get_formats(app_ctx.api_client(), resolved)
|
|
24
|
+
print_data(response.get("data", response), app_ctx.output)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.command("events")
|
|
28
|
+
def events(
|
|
29
|
+
ctx: typer.Context,
|
|
30
|
+
app_uuid: str | None = typer.Option(
|
|
31
|
+
None, "--app", "--app-uuid", help="App UUID (defaults to selected app)."
|
|
32
|
+
),
|
|
33
|
+
) -> None:
|
|
34
|
+
"""List available webhook record events."""
|
|
35
|
+
app_ctx: AppContext = ctx.obj
|
|
36
|
+
resolved = resolve_app_uuid(app_ctx, app_uuid)
|
|
37
|
+
response = webhook_api.get_events(app_ctx.api_client(), resolved)
|
|
38
|
+
print_data(response.get("data", response), app_ctx.output)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.command("test")
|
|
42
|
+
def test(
|
|
43
|
+
ctx: typer.Context,
|
|
44
|
+
webhook_uuid: str | None = typer.Argument(
|
|
45
|
+
None,
|
|
46
|
+
help="Remote webhook UUID. Prompted if omitted.",
|
|
47
|
+
),
|
|
48
|
+
record: str | None = typer.Option(
|
|
49
|
+
None,
|
|
50
|
+
"--record",
|
|
51
|
+
help="Record UUID to include in the test payload. Prompted if omitted.",
|
|
52
|
+
),
|
|
53
|
+
event: str = typer.Option("created", "--event", help="Record event type, e.g. created|updated."),
|
|
54
|
+
app_uuid: str | None = typer.Option(
|
|
55
|
+
None, "--app", "--app-uuid", help="App UUID (defaults to selected app)."
|
|
56
|
+
),
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Fire a test webhook delivery for a record event."""
|
|
59
|
+
from caraer_cli.wizard.prompts import require_text
|
|
60
|
+
|
|
61
|
+
app_ctx: AppContext = ctx.obj
|
|
62
|
+
webhook_uuid = require_text(webhook_uuid, "Webhook UUID", flag="webhook-uuid")
|
|
63
|
+
record = require_text(record, "Record UUID", flag="--record")
|
|
64
|
+
resolved = resolve_app_uuid(app_ctx, app_uuid)
|
|
65
|
+
response = webhook_api.test_webhook(
|
|
66
|
+
app_ctx.api_client(), resolved, webhook_uuid, record, event
|
|
67
|
+
)
|
|
68
|
+
print_data(response.get("data", response), app_ctx.output)
|
caraer_cli/completion.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Shell completion helpers and installers for the Caraer CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Iterable
|
|
8
|
+
|
|
9
|
+
from typer._completion_shared import get_completion_script, install as typer_install
|
|
10
|
+
|
|
11
|
+
SUPPORTED_SHELLS = ("zsh", "bash", "fish", "powershell", "pwsh")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def detect_shell(explicit: str | None = None) -> str:
|
|
15
|
+
"""Resolve a shell name, preferring an explicit value then common fallbacks."""
|
|
16
|
+
if explicit:
|
|
17
|
+
shell = explicit.strip().lower()
|
|
18
|
+
if shell not in SUPPORTED_SHELLS:
|
|
19
|
+
raise ValueError(
|
|
20
|
+
f"Unsupported shell '{explicit}'. Use one of: {', '.join(SUPPORTED_SHELLS)}"
|
|
21
|
+
)
|
|
22
|
+
return shell
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
import shellingham
|
|
26
|
+
|
|
27
|
+
name, _cmd = shellingham.detect_shell()
|
|
28
|
+
if name:
|
|
29
|
+
return str(name).lower()
|
|
30
|
+
except Exception:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
env_shell = (os.environ.get("SHELL") or "").strip()
|
|
34
|
+
if env_shell:
|
|
35
|
+
return Path(env_shell).name.lower()
|
|
36
|
+
|
|
37
|
+
raise ValueError(
|
|
38
|
+
"Could not detect your shell. Pass --shell zsh|bash|fish to the installer."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def completion_env_var(prog_name: str = "caraer") -> str:
|
|
43
|
+
return "_{}_COMPLETE".format(prog_name.replace("-", "_").upper())
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def show_completion_script(*, shell: str | None = None, prog_name: str = "caraer") -> str:
|
|
47
|
+
resolved = detect_shell(shell)
|
|
48
|
+
return get_completion_script(
|
|
49
|
+
prog_name=prog_name,
|
|
50
|
+
complete_var=completion_env_var(prog_name),
|
|
51
|
+
shell=resolved,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def install_completion(*, shell: str | None = None, prog_name: str = "caraer") -> tuple[str, Path]:
|
|
56
|
+
resolved = detect_shell(shell)
|
|
57
|
+
installed_shell, path = typer_install(
|
|
58
|
+
shell=resolved,
|
|
59
|
+
prog_name=prog_name,
|
|
60
|
+
complete_var=completion_env_var(prog_name),
|
|
61
|
+
)
|
|
62
|
+
return installed_shell, Path(path)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _safe_startswith(value: str, incomplete: str) -> bool:
|
|
66
|
+
return value.lower().startswith((incomplete or "").lower())
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def complete_from_pairs(
|
|
70
|
+
pairs: Iterable[tuple[str, str]],
|
|
71
|
+
incomplete: str,
|
|
72
|
+
) -> list[tuple[str, str]]:
|
|
73
|
+
return [
|
|
74
|
+
(value, help_text)
|
|
75
|
+
for value, help_text in pairs
|
|
76
|
+
if _safe_startswith(value, incomplete) or _safe_startswith(help_text, incomplete)
|
|
77
|
+
]
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Dynamic shell autocompletion callbacks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from caraer_cli.completion import complete_from_pairs
|
|
10
|
+
from caraer_cli.local_app import discover_local_app_files, local_app_summary
|
|
11
|
+
from caraer_cli.state.config import load_config
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def complete_profile(ctx: typer.Context, incomplete: str) -> list[tuple[str, str]]:
|
|
15
|
+
try:
|
|
16
|
+
cfg = load_config()
|
|
17
|
+
except Exception:
|
|
18
|
+
return []
|
|
19
|
+
pairs = [(name, profile.base_url) for name, profile in cfg.profiles.items()]
|
|
20
|
+
return complete_from_pairs(pairs, incomplete)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def complete_output_format(ctx: typer.Context, incomplete: str) -> list[tuple[str, str]]:
|
|
24
|
+
return complete_from_pairs(
|
|
25
|
+
[("table", "Human-readable table"), ("json", "JSON"), ("yaml", "YAML")],
|
|
26
|
+
incomplete,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def complete_local_app(ctx: typer.Context, incomplete: str) -> list[tuple[str, str]]:
|
|
31
|
+
pairs: list[tuple[str, str]] = []
|
|
32
|
+
try:
|
|
33
|
+
for path in discover_local_app_files():
|
|
34
|
+
try:
|
|
35
|
+
summary = local_app_summary(path)
|
|
36
|
+
label = summary.get("label") or summary.get("name") or path.name
|
|
37
|
+
pairs.append((str(path), str(label)))
|
|
38
|
+
# Also offer project/dir style selection when nested.
|
|
39
|
+
if path.parent.name == "app" and path.name.startswith("app.caraer."):
|
|
40
|
+
project_root = path.parents[2] if len(path.parents) >= 3 else path.parent
|
|
41
|
+
pairs.append((str(project_root), f"app: {label}"))
|
|
42
|
+
except Exception:
|
|
43
|
+
pairs.append((str(path), path.name))
|
|
44
|
+
except Exception:
|
|
45
|
+
return []
|
|
46
|
+
return complete_from_pairs(pairs, incomplete)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _completion_app_context() -> Any | None:
|
|
50
|
+
"""Build a lightweight AppContext for completion-time API lookups."""
|
|
51
|
+
try:
|
|
52
|
+
from caraer_cli.context import AppContext
|
|
53
|
+
from caraer_cli.state.config import active_profile, load_config
|
|
54
|
+
from caraer_cli.state.session import get_session_token
|
|
55
|
+
|
|
56
|
+
cfg = load_config()
|
|
57
|
+
profile_name, profile_cfg = active_profile(cfg)
|
|
58
|
+
token = get_session_token(profile_name)
|
|
59
|
+
if not token:
|
|
60
|
+
return None
|
|
61
|
+
return AppContext(
|
|
62
|
+
config=cfg,
|
|
63
|
+
profile_name=profile_name,
|
|
64
|
+
profile=profile_cfg,
|
|
65
|
+
token=token,
|
|
66
|
+
output="json",
|
|
67
|
+
debug=False,
|
|
68
|
+
)
|
|
69
|
+
except Exception:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def complete_app_ref(ctx: typer.Context, incomplete: str) -> list[tuple[str, str]]:
|
|
74
|
+
"""Complete remote app UUIDs and local app paths."""
|
|
75
|
+
results = complete_local_app(ctx, incomplete)
|
|
76
|
+
app_ctx = getattr(ctx, "obj", None) or _completion_app_context()
|
|
77
|
+
if app_ctx is None:
|
|
78
|
+
return results
|
|
79
|
+
try:
|
|
80
|
+
from caraer_cli.api import apps as apps_api
|
|
81
|
+
|
|
82
|
+
response = apps_api.list_apps(
|
|
83
|
+
app_ctx.api_client(),
|
|
84
|
+
app_type="my",
|
|
85
|
+
page=1,
|
|
86
|
+
limit=50,
|
|
87
|
+
)
|
|
88
|
+
rows = response.get("data") if isinstance(response.get("data"), list) else []
|
|
89
|
+
pairs: list[tuple[str, str]] = []
|
|
90
|
+
for row in rows:
|
|
91
|
+
if not isinstance(row, dict) or not row.get("uuid"):
|
|
92
|
+
continue
|
|
93
|
+
label = row.get("label") or row.get("name") or "app"
|
|
94
|
+
pairs.append((str(row["uuid"]), str(label)))
|
|
95
|
+
results.extend(complete_from_pairs(pairs, incomplete))
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
98
|
+
return results
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def complete_company(ctx: typer.Context, incomplete: str) -> list[tuple[str, str]]:
|
|
102
|
+
app_ctx = getattr(ctx, "obj", None) or _completion_app_context()
|
|
103
|
+
if app_ctx is None:
|
|
104
|
+
return []
|
|
105
|
+
try:
|
|
106
|
+
from caraer_cli.api import auth as auth_api
|
|
107
|
+
|
|
108
|
+
response = auth_api.companies(app_ctx.api_client())
|
|
109
|
+
companies = response.get("data") if isinstance(response.get("data"), list) else []
|
|
110
|
+
pairs: list[tuple[str, str]] = []
|
|
111
|
+
for company in companies:
|
|
112
|
+
if not isinstance(company, dict) or not company.get("uuid"):
|
|
113
|
+
continue
|
|
114
|
+
pairs.append((str(company["uuid"]), str(company.get("name") or "company")))
|
|
115
|
+
return complete_from_pairs(pairs, incomplete)
|
|
116
|
+
except Exception:
|
|
117
|
+
return []
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def complete_runtime(ctx: typer.Context, incomplete: str) -> list[tuple[str, str]]:
|
|
121
|
+
return complete_from_pairs(
|
|
122
|
+
[("nodejs22", "Node.js 22"), ("python312", "Python 3.12")],
|
|
123
|
+
incomplete,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def complete_auth_method(ctx: typer.Context, incomplete: str) -> list[tuple[str, str]]:
|
|
128
|
+
return complete_from_pairs(
|
|
129
|
+
[("OAUTH2", "OAuth 2.0"), ("API_KEY", "API key")],
|
|
130
|
+
incomplete,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def complete_local_function(ctx: typer.Context, incomplete: str) -> list[tuple[str, str]]:
|
|
135
|
+
try:
|
|
136
|
+
from caraer_cli.app_sync import resolve_app_root
|
|
137
|
+
from caraer_cli.project.schema import load_workspace
|
|
138
|
+
from caraer_cli.project.sync import list_local_function_names
|
|
139
|
+
from caraer_cli.state.config import active_profile, load_config
|
|
140
|
+
|
|
141
|
+
cfg = load_config()
|
|
142
|
+
_, profile = active_profile(cfg)
|
|
143
|
+
root = resolve_app_root(app_file=profile.app_file)
|
|
144
|
+
config = load_workspace(root)
|
|
145
|
+
pairs = [(name, "local function") for name in list_local_function_names(root, config)]
|
|
146
|
+
return complete_from_pairs(pairs, incomplete)
|
|
147
|
+
except Exception:
|
|
148
|
+
return []
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def complete_app_list_type(ctx: typer.Context, incomplete: str) -> list[tuple[str, str]]:
|
|
152
|
+
return complete_from_pairs(
|
|
153
|
+
[
|
|
154
|
+
("my", "Apps you created"),
|
|
155
|
+
("private", "Private apps"),
|
|
156
|
+
("public", "Public marketplace apps"),
|
|
157
|
+
("native", "Native apps"),
|
|
158
|
+
("local", "Local project files"),
|
|
159
|
+
],
|
|
160
|
+
incomplete,
|
|
161
|
+
)
|
|
162
|
+
|
caraer_cli/context.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from caraer_cli.api.client import CaraerApiClient, RequestContext
|
|
6
|
+
from caraer_cli.state import session as session_store
|
|
7
|
+
from caraer_cli.state.config import CliConfig, ProfileConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class AppContext:
|
|
12
|
+
config: CliConfig
|
|
13
|
+
profile_name: str
|
|
14
|
+
profile: ProfileConfig
|
|
15
|
+
token: str | None
|
|
16
|
+
output: str
|
|
17
|
+
debug: bool
|
|
18
|
+
|
|
19
|
+
def api_client(self) -> CaraerApiClient:
|
|
20
|
+
profile_name = self.profile_name
|
|
21
|
+
|
|
22
|
+
def _persist(access: str, refresh: str | None) -> None:
|
|
23
|
+
session_store.set_session_token(
|
|
24
|
+
profile_name, access, refresh_token=refresh
|
|
25
|
+
)
|
|
26
|
+
self.token = access
|
|
27
|
+
|
|
28
|
+
return CaraerApiClient(
|
|
29
|
+
RequestContext(
|
|
30
|
+
base_url=self.profile.base_url,
|
|
31
|
+
token=self.token,
|
|
32
|
+
company_uuid=self.profile.company_uuid,
|
|
33
|
+
sandbox_uuid=self.profile.sandbox_uuid,
|
|
34
|
+
timeout_seconds=self.profile.timeout_seconds,
|
|
35
|
+
verify_ssl=self.profile.verify_ssl,
|
|
36
|
+
debug=self.debug,
|
|
37
|
+
profile_name=self.profile_name,
|
|
38
|
+
refresh_token=session_store.get_refresh_token(self.profile_name),
|
|
39
|
+
on_tokens_refreshed=_persist,
|
|
40
|
+
)
|
|
41
|
+
)
|