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.
Files changed (83) hide show
  1. caraer_cli/__init__.py +4 -0
  2. caraer_cli/api/__init__.py +1 -0
  3. caraer_cli/api/apps.py +78 -0
  4. caraer_cli/api/auth.py +53 -0
  5. caraer_cli/api/client.py +179 -0
  6. caraer_cli/api/functions.py +72 -0
  7. caraer_cli/api/installation.py +93 -0
  8. caraer_cli/api/integration_runtime.py +77 -0
  9. caraer_cli/api/projects.py +213 -0
  10. caraer_cli/api/sandboxes.py +29 -0
  11. caraer_cli/api/webhooks.py +59 -0
  12. caraer_cli/app_sync.py +697 -0
  13. caraer_cli/apps_local.py +274 -0
  14. caraer_cli/commands/__init__.py +1 -0
  15. caraer_cli/commands/apps.py +2025 -0
  16. caraer_cli/commands/auth.py +173 -0
  17. caraer_cli/commands/company.py +68 -0
  18. caraer_cli/commands/installation.py +182 -0
  19. caraer_cli/commands/profile.py +239 -0
  20. caraer_cli/commands/publish.py +40 -0
  21. caraer_cli/commands/sandbox.py +92 -0
  22. caraer_cli/commands/skill.py +165 -0
  23. caraer_cli/commands/webhooks.py +68 -0
  24. caraer_cli/completion.py +77 -0
  25. caraer_cli/completion_callbacks.py +162 -0
  26. caraer_cli/context.py +41 -0
  27. caraer_cli/errors.py +84 -0
  28. caraer_cli/formatters/__init__.py +1 -0
  29. caraer_cli/formatters/output.py +381 -0
  30. caraer_cli/formatters/timestamps.py +98 -0
  31. caraer_cli/install_completion.py +38 -0
  32. caraer_cli/local_app.py +135 -0
  33. caraer_cli/main.py +104 -0
  34. caraer_cli/models/__init__.py +1 -0
  35. caraer_cli/models/api.py +52 -0
  36. caraer_cli/project/__init__.py +17 -0
  37. caraer_cli/project/app_bars_sync.py +154 -0
  38. caraer_cli/project/app_manifest_template.py +68 -0
  39. caraer_cli/project/inbound_sync.py +192 -0
  40. caraer_cli/project/json_schemas.py +66 -0
  41. caraer_cli/project/lifecycle_sync.py +119 -0
  42. caraer_cli/project/local_dev.py +825 -0
  43. caraer_cli/project/manifest_edit.py +51 -0
  44. caraer_cli/project/marketplace_assemble.py +165 -0
  45. caraer_cli/project/oauth_providers_sync.py +180 -0
  46. caraer_cli/project/paths.py +102 -0
  47. caraer_cli/project/pricing_sync.py +81 -0
  48. caraer_cli/project/push_plan.py +590 -0
  49. caraer_cli/project/release.py +128 -0
  50. caraer_cli/project/scaffold.py +537 -0
  51. caraer_cli/project/schedules_sync.py +170 -0
  52. caraer_cli/project/schema.py +160 -0
  53. caraer_cli/project/settings_sync.py +77 -0
  54. caraer_cli/project/state.py +51 -0
  55. caraer_cli/project/sync.py +308 -0
  56. caraer_cli/project/typegen.py +278 -0
  57. caraer_cli/project/validate_app.py +1215 -0
  58. caraer_cli/project/webhooks_sync.py +233 -0
  59. caraer_cli/resolve.py +55 -0
  60. caraer_cli/schemas/app.caraer.schema.json +175 -0
  61. caraer_cli/schemas/function.caraer.schema.json +29 -0
  62. caraer_cli/schemas/inbound.caraer.schema.json +49 -0
  63. caraer_cli/schemas/lifecycle.caraer.schema.json +50 -0
  64. caraer_cli/schemas/schedule.caraer.schema.json +39 -0
  65. caraer_cli/schemas/webhook.caraer.schema.json +50 -0
  66. caraer_cli/skills/.gitkeep +2 -0
  67. caraer_cli/skills/caraer-apps/SKILL.md +172 -0
  68. caraer_cli/skills/caraer-apps/reference.md +163 -0
  69. caraer_cli/state/__init__.py +1 -0
  70. caraer_cli/state/config.py +148 -0
  71. caraer_cli/state/session.py +98 -0
  72. caraer_cli/utils.py +40 -0
  73. caraer_cli/wizard/__init__.py +9 -0
  74. caraer_cli/wizard/catalog.py +170 -0
  75. caraer_cli/wizard/marketplace.py +350 -0
  76. caraer_cli/wizard/prompts.py +99 -0
  77. caraer_cli/wizard/public_app.py +519 -0
  78. caraer_cli/wizard/scope_macros.py +214 -0
  79. caraer_cli-0.1.2.dist-info/METADATA +266 -0
  80. caraer_cli-0.1.2.dist-info/RECORD +83 -0
  81. caraer_cli-0.1.2.dist-info/WHEEL +4 -0
  82. caraer_cli-0.1.2.dist-info/entry_points.txt +2 -0
  83. 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)
@@ -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
+ )