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,173 @@
1
+ from __future__ import annotations
2
+
3
+ import getpass
4
+ import time
5
+ import webbrowser
6
+
7
+ import typer
8
+
9
+ from caraer_cli.api import auth as auth_api
10
+ from caraer_cli.context import AppContext
11
+ from caraer_cli.errors import ApiError, AuthError
12
+ from caraer_cli.formatters.output import print_data, print_success, print_warning
13
+ from caraer_cli.state import session as session_store
14
+
15
+ app = typer.Typer(help="Authentication commands.", no_args_is_help=True)
16
+
17
+
18
+ def _store_tokens(profile: str, data: dict) -> str:
19
+ """Extract and persist access (+ optional refresh) token from an auth payload."""
20
+ token = None
21
+ refresh_token = data.get("refreshToken")
22
+ token_obj = data.get("token")
23
+ if isinstance(token_obj, dict):
24
+ token = token_obj.get("token")
25
+ if not token:
26
+ token = data.get("accessToken")
27
+ if not token:
28
+ raise typer.BadParameter("Login succeeded but no token was returned.")
29
+ session_store.set_session_token(
30
+ profile,
31
+ str(token),
32
+ refresh_token=str(refresh_token) if refresh_token else None,
33
+ )
34
+ return str(token)
35
+
36
+
37
+ @app.command("login")
38
+ def login(
39
+ ctx: typer.Context,
40
+ email: str | None = typer.Option(
41
+ None,
42
+ "--email",
43
+ help="Account email (password login). Omit for device-code browser login.",
44
+ ),
45
+ password: str = typer.Option(
46
+ None,
47
+ "--password",
48
+ prompt=False,
49
+ hide_input=True,
50
+ help="Password (prompted securely if --email is set and password omitted).",
51
+ ),
52
+ device: bool = typer.Option(
53
+ False,
54
+ "--device",
55
+ help="Force device-code login (default when --email is omitted).",
56
+ ),
57
+ ) -> None:
58
+ """Log in and store a session token for the active profile.
59
+
60
+ Defaults to device-code / browser login. Pass --email for password login (CI).
61
+ """
62
+ app_ctx: AppContext = ctx.obj
63
+ client = app_ctx.api_client()
64
+
65
+ use_device = device or not email
66
+ if use_device and email:
67
+ print_warning("Ignoring --email/--password; using device-code login.")
68
+
69
+ if use_device:
70
+ try:
71
+ start = auth_api.device_start(client).get("data") or {}
72
+ except ApiError as exc:
73
+ if email:
74
+ print_warning(
75
+ f"Device login unavailable ({exc}). Falling back to password login."
76
+ )
77
+ use_device = False
78
+ else:
79
+ raise typer.BadParameter(
80
+ "Device login is unavailable on this backend. "
81
+ "Use 'caraer auth login --email you@example.com'."
82
+ ) from exc
83
+ else:
84
+ device_code = str(start.get("deviceCode") or "")
85
+ user_code = str(start.get("userCode") or "")
86
+ verification_uri = str(
87
+ start.get("verificationUriComplete")
88
+ or start.get("verificationUri")
89
+ or ""
90
+ )
91
+ interval = float(start.get("interval") or 5)
92
+ expires_in = float(start.get("expiresIn") or 600)
93
+ if not device_code or not user_code:
94
+ raise typer.BadParameter("Device start response missing codes.")
95
+ print_success(f"Device code: {user_code}")
96
+ if verification_uri:
97
+ print_success(f"Open: {verification_uri}")
98
+ try:
99
+ webbrowser.open(verification_uri)
100
+ except Exception: # noqa: BLE001
101
+ pass
102
+ print_success("Waiting for approval in the browser…")
103
+ deadline = time.monotonic() + expires_in
104
+ while time.monotonic() < deadline:
105
+ time.sleep(max(1.0, interval))
106
+ poll = auth_api.device_poll(client, device_code).get("data") or {}
107
+ status = str(poll.get("status") or "").lower()
108
+ if status == "approved":
109
+ _store_tokens(app_ctx.profile_name, poll)
110
+ print_success(f"Logged in for profile '{app_ctx.profile_name}'.")
111
+ _print_next_steps(app_ctx)
112
+ return
113
+ if status in {"expired", "denied"}:
114
+ raise typer.BadParameter(f"Device login {status}.")
115
+ raise typer.BadParameter("Device login timed out.")
116
+
117
+ from caraer_cli.wizard.prompts import require_text
118
+
119
+ email = require_text(email, "Email", flag="--email")
120
+ resolved_password = password or getpass.getpass("Password: ")
121
+ response = auth_api.login(client, email, resolved_password)
122
+ data = response.get("data", {})
123
+ _store_tokens(app_ctx.profile_name, data if isinstance(data, dict) else {})
124
+ print_success(f"Logged in for profile '{app_ctx.profile_name}'.")
125
+ _print_next_steps(app_ctx)
126
+
127
+
128
+ def _print_next_steps(app_ctx: AppContext) -> None:
129
+ if app_ctx.profile.company_uuid:
130
+ print_success(
131
+ f"Profile company is '{app_ctx.profile.company_uuid}'. "
132
+ "If API calls fail with Company not found, run 'caraer company clear' "
133
+ "then 'caraer company list'."
134
+ )
135
+ else:
136
+ print_success("Next: run 'caraer company list' and 'caraer company select <uuid>'.")
137
+
138
+
139
+ @app.command("logout")
140
+ def logout(ctx: typer.Context) -> None:
141
+ """Log out and clear the stored session token for the active profile."""
142
+ app_ctx: AppContext = ctx.obj
143
+ client = app_ctx.api_client()
144
+ try:
145
+ auth_api.logout(client)
146
+ except AuthError:
147
+ pass
148
+ session_store.clear_session_token(app_ctx.profile_name)
149
+ print_success(f"Logged out profile '{app_ctx.profile_name}'.")
150
+
151
+
152
+ @app.command("me")
153
+ def me(ctx: typer.Context) -> None:
154
+ """Show the currently authenticated user."""
155
+ app_ctx: AppContext = ctx.obj
156
+ client = app_ctx.api_client()
157
+ response = auth_api.me(client)
158
+ print_data(response.get("data"), app_ctx.output)
159
+
160
+
161
+ @app.command("refresh")
162
+ def refresh_token(ctx: typer.Context) -> None:
163
+ """Refresh the access token using the stored refresh token."""
164
+ app_ctx: AppContext = ctx.obj
165
+ refresh = session_store.get_refresh_token(app_ctx.profile_name)
166
+ if not refresh:
167
+ raise typer.BadParameter(
168
+ "No refresh token stored. Run 'caraer auth login' (device flow) first."
169
+ )
170
+ client = app_ctx.api_client()
171
+ data = auth_api.refresh(client, refresh).get("data") or {}
172
+ _store_tokens(app_ctx.profile_name, data if isinstance(data, dict) else {})
173
+ print_success(f"Refreshed session for profile '{app_ctx.profile_name}'.")
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+
5
+ from caraer_cli.api import auth as auth_api
6
+ from caraer_cli.completion_callbacks import complete_company
7
+ from caraer_cli.context import AppContext
8
+ from caraer_cli.formatters.output import print_data, print_success
9
+ from caraer_cli.state.config import save_config
10
+
11
+ app = typer.Typer(help="Company context commands.", no_args_is_help=True)
12
+
13
+
14
+ @app.command("list")
15
+ def list_companies(ctx: typer.Context) -> None:
16
+ """List companies available to the authenticated user."""
17
+ app_ctx: AppContext = ctx.obj
18
+ client = app_ctx.api_client()
19
+ response = auth_api.companies(client)
20
+ companies = response.get("data")
21
+ if isinstance(companies, list):
22
+ minimal = []
23
+ for company in companies:
24
+ if not isinstance(company, dict):
25
+ continue
26
+ minimal.append(
27
+ {
28
+ "name": company.get("name"),
29
+ "uuid": company.get("uuid"),
30
+ }
31
+ )
32
+ print_data(minimal, app_ctx.output)
33
+ return
34
+ print_data([], app_ctx.output)
35
+
36
+
37
+ @app.command("select")
38
+ def select_company(
39
+ ctx: typer.Context,
40
+ company_uuid: str | None = typer.Argument(
41
+ None,
42
+ help="Company UUID to use for API calls. Prompted if omitted.",
43
+ autocompletion=complete_company,
44
+ ),
45
+ ) -> None:
46
+ """Select a company for the active profile."""
47
+ from caraer_cli.wizard.prompts import require_text
48
+
49
+ app_ctx: AppContext = ctx.obj
50
+ company_uuid = require_text(company_uuid, "Company UUID", flag="company-uuid")
51
+ cfg = app_ctx.config
52
+ profile = cfg.profiles[app_ctx.profile_name]
53
+ profile.company_uuid = company_uuid
54
+ profile.sandbox_uuid = None # sandbox is always scoped to its owner company
55
+ save_config(cfg)
56
+ print_success(f"Selected company '{company_uuid}' for profile '{app_ctx.profile_name}'.")
57
+
58
+
59
+ @app.command("clear")
60
+ def clear_company(ctx: typer.Context) -> None:
61
+ """Remove the selected company from the active profile."""
62
+ app_ctx: AppContext = ctx.obj
63
+ cfg = app_ctx.config
64
+ profile = cfg.profiles[app_ctx.profile_name]
65
+ profile.company_uuid = None
66
+ profile.sandbox_uuid = None
67
+ save_config(cfg)
68
+ print_success(f"Cleared company for profile '{app_ctx.profile_name}'.")
@@ -0,0 +1,182 @@
1
+ """Installation runtime commands: state, secrets, jobs, connections."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ import typer
9
+
10
+ from caraer_cli.context import AppContext
11
+ from caraer_cli.formatters.output import print_data, print_success
12
+
13
+ state_app = typer.Typer(help="Installation state (key/value store).", no_args_is_help=True)
14
+ secrets_app = typer.Typer(help="Installation secrets.", no_args_is_help=True)
15
+ jobs_app = typer.Typer(help="Installation background jobs.", no_args_is_help=True)
16
+ connections_app = typer.Typer(help="External OAuth connections.", no_args_is_help=True)
17
+
18
+
19
+ def _require_app_uuid(ctx: typer.Context) -> tuple[AppContext, str]:
20
+ from caraer_cli.app_sync import resolve_app_root
21
+ from caraer_cli.project.schema import load_workspace
22
+
23
+ app_ctx: AppContext = ctx.obj
24
+ root = resolve_app_root(app_file=app_ctx.profile.app_file)
25
+ config = load_workspace(root)
26
+ if not config.appUuid:
27
+ raise ValueError("App has no remote UUID. Run 'caraer apps select' or 'caraer apps push'.")
28
+ return app_ctx, config.appUuid
29
+
30
+
31
+ @state_app.command("get")
32
+ def state_get(
33
+ ctx: typer.Context,
34
+ key: str | None = typer.Argument(None, help="Optional state key (omit for full map)."),
35
+ ) -> None:
36
+ """Get installation state (all keys, or one key)."""
37
+ from caraer_cli.api import installation as installation_api
38
+
39
+ app_ctx, app_uuid = _require_app_uuid(ctx)
40
+ client = app_ctx.api_client()
41
+ if key:
42
+ response = installation_api.get_state_key(client, app_uuid, key)
43
+ else:
44
+ response = installation_api.get_state(client, app_uuid)
45
+ print_data(response.get("data"), app_ctx.output)
46
+
47
+
48
+ @state_app.command("set")
49
+ def state_set(
50
+ ctx: typer.Context,
51
+ key: str = typer.Argument(..., help="State key."),
52
+ value: str = typer.Argument(..., help="JSON or plain string value."),
53
+ ) -> None:
54
+ """Set an installation state key."""
55
+ from caraer_cli.api import installation as installation_api
56
+
57
+ app_ctx, app_uuid = _require_app_uuid(ctx)
58
+ try:
59
+ parsed: Any = json.loads(value)
60
+ except json.JSONDecodeError:
61
+ parsed = value
62
+ response = installation_api.put_state_key(app_ctx.api_client(), app_uuid, key, parsed)
63
+ print_success(f"Set state key '{key}'")
64
+ print_data(response.get("data"), app_ctx.output)
65
+
66
+
67
+ @state_app.command("delete")
68
+ def state_delete(
69
+ ctx: typer.Context,
70
+ key: str = typer.Argument(..., help="State key to delete."),
71
+ ) -> None:
72
+ """Delete an installation state key."""
73
+ from caraer_cli.api import installation as installation_api
74
+
75
+ app_ctx, app_uuid = _require_app_uuid(ctx)
76
+ response = installation_api.delete_state_key(app_ctx.api_client(), app_uuid, key)
77
+ print_success(f"Deleted state key '{key}'")
78
+ print_data(response.get("data"), app_ctx.output)
79
+
80
+
81
+ @secrets_app.command("list")
82
+ def secrets_list(ctx: typer.Context) -> None:
83
+ """List installation secret names (values are never returned)."""
84
+ from caraer_cli.api import installation as installation_api
85
+
86
+ app_ctx, app_uuid = _require_app_uuid(ctx)
87
+ response = installation_api.list_secrets(app_ctx.api_client(), app_uuid)
88
+ print_data(response.get("data"), app_ctx.output)
89
+
90
+
91
+ @secrets_app.command("set")
92
+ def secrets_set(
93
+ ctx: typer.Context,
94
+ name: str = typer.Argument(..., help="Secret name."),
95
+ value: str = typer.Argument(..., help="Secret value."),
96
+ ) -> None:
97
+ """Create or update an installation secret."""
98
+ from caraer_cli.api import installation as installation_api
99
+
100
+ app_ctx, app_uuid = _require_app_uuid(ctx)
101
+ response = installation_api.put_secret(app_ctx.api_client(), app_uuid, name, value)
102
+ print_success(f"Set secret '{name}'")
103
+ print_data(response.get("data"), app_ctx.output)
104
+
105
+
106
+ @secrets_app.command("delete")
107
+ def secrets_delete(
108
+ ctx: typer.Context,
109
+ name: str = typer.Argument(..., help="Secret name to delete."),
110
+ ) -> None:
111
+ """Delete an installation secret."""
112
+ from caraer_cli.api import installation as installation_api
113
+
114
+ app_ctx, app_uuid = _require_app_uuid(ctx)
115
+ response = installation_api.delete_secret(app_ctx.api_client(), app_uuid, name)
116
+ print_success(f"Deleted secret '{name}'")
117
+ print_data(response.get("data"), app_ctx.output)
118
+
119
+
120
+ @jobs_app.command("enqueue")
121
+ def jobs_enqueue(
122
+ ctx: typer.Context,
123
+ function_name: str = typer.Argument(..., help="Function name to invoke."),
124
+ payload: str = typer.Option("{}", "--payload", help="JSON payload object."),
125
+ delay_seconds: float = typer.Option(0, "--delay", help="Delay before invoke (seconds)."),
126
+ ) -> None:
127
+ """Enqueue a background job for a function."""
128
+ from caraer_cli.api import installation as installation_api
129
+
130
+ app_ctx, app_uuid = _require_app_uuid(ctx)
131
+ try:
132
+ body = json.loads(payload)
133
+ except json.JSONDecodeError as exc:
134
+ raise ValueError(f"Invalid --payload JSON: {exc}") from exc
135
+ if not isinstance(body, dict):
136
+ raise ValueError("--payload must be a JSON object")
137
+ response = installation_api.enqueue_job(
138
+ app_ctx.api_client(),
139
+ app_uuid,
140
+ function_name=function_name,
141
+ payload=body,
142
+ delay_seconds=delay_seconds,
143
+ )
144
+ print_success("Job enqueued")
145
+ print_data(response.get("data"), app_ctx.output)
146
+
147
+
148
+ @jobs_app.command("status")
149
+ def jobs_status(
150
+ ctx: typer.Context,
151
+ job_id: str = typer.Argument(..., help="Job id from enqueue."),
152
+ ) -> None:
153
+ """Get status for an installation job."""
154
+ from caraer_cli.api import installation as installation_api
155
+
156
+ app_ctx, app_uuid = _require_app_uuid(ctx)
157
+ response = installation_api.get_job(app_ctx.api_client(), app_uuid, job_id)
158
+ print_data(response.get("data"), app_ctx.output)
159
+
160
+
161
+ @connections_app.command("list")
162
+ def connections_list(ctx: typer.Context) -> None:
163
+ """List external OAuth connection status for this app installation."""
164
+ from caraer_cli.api import installation as installation_api
165
+
166
+ app_ctx, app_uuid = _require_app_uuid(ctx)
167
+ response = installation_api.list_connections(app_ctx.api_client(), app_uuid)
168
+ print_data(response.get("data"), app_ctx.output)
169
+
170
+
171
+ @connections_app.command("revoke")
172
+ def connections_revoke(
173
+ ctx: typer.Context,
174
+ provider: str = typer.Argument(..., help="Provider name to revoke."),
175
+ ) -> None:
176
+ """Revoke an external OAuth connection."""
177
+ from caraer_cli.api import installation as installation_api
178
+
179
+ app_ctx, app_uuid = _require_app_uuid(ctx)
180
+ response = installation_api.delete_connection(app_ctx.api_client(), app_uuid, provider)
181
+ print_success(f"Revoked connection '{provider}'")
182
+ print_data(response.get("data"), app_ctx.output)
@@ -0,0 +1,239 @@
1
+ from __future__ import annotations
2
+
3
+ from copy import deepcopy
4
+
5
+ import typer
6
+
7
+ from caraer_cli.completion_callbacks import complete_output_format, complete_profile
8
+ from caraer_cli.context import AppContext
9
+ from caraer_cli.formatters.output import print_data, print_success
10
+ from caraer_cli.state.config import ProfileConfig, profile_as_dict, save_config
11
+
12
+ app = typer.Typer(help="Manage CLI config profiles.", no_args_is_help=True)
13
+
14
+
15
+ def _require_profile(app_ctx: AppContext, name: str) -> ProfileConfig:
16
+ if name not in app_ctx.config.profiles:
17
+ raise typer.BadParameter(f"Unknown profile '{name}'.")
18
+ return app_ctx.config.profiles[name]
19
+
20
+
21
+ def _resolve_target(app_ctx: AppContext, name: str | None) -> tuple[str, ProfileConfig]:
22
+ target = (name or app_ctx.profile_name).strip()
23
+ return target, _require_profile(app_ctx, target)
24
+
25
+
26
+ @app.command("list")
27
+ def list_profiles(ctx: typer.Context) -> None:
28
+ """List all profiles (marks the active one)."""
29
+ app_ctx: AppContext = ctx.obj
30
+ rows = []
31
+ for name, profile in app_ctx.config.profiles.items():
32
+ row = profile_as_dict(profile)
33
+ row["name"] = name
34
+ row["active"] = name == app_ctx.config.active_profile
35
+ rows.append(row)
36
+ print_data(rows, app_ctx.output)
37
+
38
+
39
+ @app.command("show")
40
+ def show_profile(
41
+ ctx: typer.Context,
42
+ name: str | None = typer.Argument(
43
+ None,
44
+ help="Profile name (defaults to the active profile).",
45
+ autocompletion=complete_profile,
46
+ ),
47
+ ) -> None:
48
+ """Show one profile's settings."""
49
+ app_ctx: AppContext = ctx.obj
50
+ target, profile = _resolve_target(app_ctx, name)
51
+ output = profile_as_dict(profile)
52
+ output["name"] = target
53
+ output["active"] = target == app_ctx.config.active_profile
54
+ print_data(output, app_ctx.output)
55
+
56
+
57
+ @app.command("current")
58
+ def current_profile(ctx: typer.Context) -> None:
59
+ """Show the active profile name and settings."""
60
+ app_ctx: AppContext = ctx.obj
61
+ output = profile_as_dict(app_ctx.profile)
62
+ output["name"] = app_ctx.profile_name
63
+ output["active"] = True
64
+ print_data(output, app_ctx.output)
65
+
66
+
67
+ @app.command("use")
68
+ def use_profile(
69
+ ctx: typer.Context,
70
+ name: str | None = typer.Argument(
71
+ None,
72
+ help="Profile name to activate. Prompted if omitted.",
73
+ autocompletion=complete_profile,
74
+ ),
75
+ ) -> None:
76
+ """Switch the active profile."""
77
+ from caraer_cli.wizard.prompts import require_text
78
+
79
+ app_ctx: AppContext = ctx.obj
80
+ name = require_text(name, "Profile name", flag="name")
81
+ _require_profile(app_ctx, name)
82
+ app_ctx.config.active_profile = name
83
+ save_config(app_ctx.config)
84
+ print_success(f"Set active profile to '{name}'.")
85
+
86
+
87
+ @app.command("create")
88
+ def create_profile(
89
+ ctx: typer.Context,
90
+ name: str | None = typer.Argument(None, help="New profile name. Prompted if omitted."),
91
+ base_url: str | None = typer.Option(
92
+ None,
93
+ "--base-url",
94
+ help="API base URL (required unless --from is set; prompted if omitted).",
95
+ ),
96
+ from_profile: str | None = typer.Option(
97
+ None,
98
+ "--from",
99
+ help="Copy settings from an existing profile.",
100
+ autocompletion=complete_profile,
101
+ ),
102
+ use: bool = typer.Option(False, "--use", help="Make this the active profile."),
103
+ ) -> None:
104
+ """Create a new profile, optionally copied from an existing one."""
105
+ from caraer_cli.wizard.prompts import require_text
106
+
107
+ app_ctx: AppContext = ctx.obj
108
+ name = require_text(name, "Profile name", flag="name")
109
+ if name in app_ctx.config.profiles:
110
+ raise typer.BadParameter(f"Profile '{name}' already exists.")
111
+
112
+ if from_profile:
113
+ source = _require_profile(app_ctx, from_profile)
114
+ profile = deepcopy(source)
115
+ if base_url:
116
+ profile.base_url = base_url
117
+ else:
118
+ base_url = require_text(base_url, "API base URL", flag="--base-url")
119
+ profile = ProfileConfig(base_url=base_url)
120
+
121
+ app_ctx.config.profiles[name] = profile
122
+ if use:
123
+ app_ctx.config.active_profile = name
124
+ save_config(app_ctx.config)
125
+ message = f"Created profile '{name}'."
126
+ if use:
127
+ message += " Set as active."
128
+ print_success(message)
129
+
130
+
131
+ @app.command("delete")
132
+ def delete_profile(
133
+ ctx: typer.Context,
134
+ name: str | None = typer.Argument(
135
+ None,
136
+ help="Profile name to delete. Prompted if omitted.",
137
+ autocompletion=complete_profile,
138
+ ),
139
+ force: bool = typer.Option(False, "--force", help="Allow deleting the active profile."),
140
+ ) -> None:
141
+ """Delete a profile."""
142
+ from caraer_cli.wizard.prompts import require_text
143
+
144
+ app_ctx: AppContext = ctx.obj
145
+ name = require_text(name, "Profile name", flag="name")
146
+ _require_profile(app_ctx, name)
147
+ if len(app_ctx.config.profiles) <= 1:
148
+ raise typer.BadParameter("Cannot delete the last remaining profile.")
149
+ if name == app_ctx.config.active_profile and not force:
150
+ raise typer.BadParameter(
151
+ f"'{name}' is the active profile. Switch first, or pass --force."
152
+ )
153
+
154
+ del app_ctx.config.profiles[name]
155
+ if app_ctx.config.active_profile == name:
156
+ app_ctx.config.active_profile = next(iter(app_ctx.config.profiles.keys()))
157
+ save_config(app_ctx.config)
158
+ print_success(f"Deleted profile '{name}'.")
159
+
160
+
161
+ @app.command("set")
162
+ def set_profile(
163
+ ctx: typer.Context,
164
+ name: str | None = typer.Argument(
165
+ None,
166
+ help="Profile to update (defaults to the active profile).",
167
+ autocompletion=complete_profile,
168
+ ),
169
+ base_url: str | None = typer.Option(None, "--base-url", help="API base URL."),
170
+ company_uuid: str | None = typer.Option(None, "--company-uuid", help="Selected company UUID."),
171
+ app_uuid: str | None = typer.Option(None, "--app-uuid", help="Selected remote app UUID."),
172
+ app_file: str | None = typer.Option(None, "--app-file", help="Selected local app file path."),
173
+ output: str | None = typer.Option(
174
+ None,
175
+ "--output",
176
+ help="Default output format: table|json|yaml.",
177
+ autocompletion=complete_output_format,
178
+ ),
179
+ timeout_seconds: float | None = typer.Option(
180
+ None,
181
+ "--timeout-seconds",
182
+ help="HTTP timeout in seconds.",
183
+ ),
184
+ verify_ssl: bool | None = typer.Option(
185
+ None,
186
+ "--verify-ssl/--no-verify-ssl",
187
+ help="Verify TLS certificates.",
188
+ ),
189
+ clear_company: bool = typer.Option(False, "--clear-company", help="Clear company_uuid."),
190
+ clear_app: bool = typer.Option(False, "--clear-app", help="Clear app_uuid."),
191
+ clear_app_file: bool = typer.Option(False, "--clear-app-file", help="Clear app_file."),
192
+ ) -> None:
193
+ """Update fields on a profile."""
194
+ app_ctx: AppContext = ctx.obj
195
+ target, profile = _resolve_target(app_ctx, name)
196
+
197
+ updates: list[str] = []
198
+ if base_url is not None:
199
+ profile.base_url = base_url
200
+ updates.append("base_url")
201
+ if company_uuid is not None:
202
+ profile.company_uuid = company_uuid.strip() or None
203
+ updates.append("company_uuid")
204
+ if app_uuid is not None:
205
+ profile.app_uuid = app_uuid.strip() or None
206
+ updates.append("app_uuid")
207
+ if app_file is not None:
208
+ profile.app_file = app_file.strip() or None
209
+ updates.append("app_file")
210
+ if output is not None:
211
+ normalized = output.strip().lower()
212
+ if normalized not in {"table", "json", "yaml"}:
213
+ raise typer.BadParameter("output must be one of: table, json, yaml.")
214
+ profile.output = normalized
215
+ updates.append("output")
216
+ if timeout_seconds is not None:
217
+ if timeout_seconds <= 0:
218
+ raise typer.BadParameter("timeout-seconds must be greater than 0.")
219
+ profile.timeout_seconds = timeout_seconds
220
+ updates.append("timeout_seconds")
221
+ if verify_ssl is not None:
222
+ profile.verify_ssl = verify_ssl
223
+ updates.append("verify_ssl")
224
+ if clear_company:
225
+ profile.company_uuid = None
226
+ updates.append("company_uuid")
227
+ if clear_app:
228
+ profile.app_uuid = None
229
+ updates.append("app_uuid")
230
+ if clear_app_file:
231
+ profile.app_file = None
232
+ updates.append("app_file")
233
+
234
+ if not updates:
235
+ raise typer.BadParameter("No fields to update. Pass at least one option.")
236
+
237
+ save_config(app_ctx.config)
238
+ unique = ", ".join(dict.fromkeys(updates))
239
+ print_success(f"Updated {unique} on profile '{target}'.")