sovaria-cli 0.8.3__tar.gz → 0.8.4__tar.gz

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 (105) hide show
  1. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/PKG-INFO +1 -1
  2. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/__init__.py +1 -1
  3. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/ai_credits.py +5 -3
  4. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/ai_data.py +1 -1
  5. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/api_versions.py +8 -2
  6. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/auth_product.py +5 -1
  7. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/billing.py +5 -1
  8. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/builds.py +9 -2
  9. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/chrono.py +5 -1
  10. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/crashes.py +8 -1
  11. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/data.py +5 -1
  12. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/db.py +6 -2
  13. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/distribute.py +8 -2
  14. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/hosting.py +12 -4
  15. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/iam.py +11 -1
  16. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/keys.py +9 -1
  17. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/knowledge.py +3 -1
  18. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/login.py +12 -1
  19. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/messaging.py +7 -4
  20. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/policy.py +3 -1
  21. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/push.py +3 -2
  22. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/queues.py +8 -2
  23. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/realtime.py +3 -1
  24. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/scope.py +3 -3
  25. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/storage.py +8 -1
  26. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/vector.py +5 -1
  27. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/utils/display.py +11 -0
  28. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/utils/premium_display.py +15 -0
  29. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/pyproject.toml +1 -1
  30. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/.gitignore +0 -0
  31. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/README.md +0 -0
  32. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/__init__.py +0 -0
  33. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/__init__.py +0 -0
  34. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/admin.py +0 -0
  35. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/alerts.py +0 -0
  36. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/analytics.py +0 -0
  37. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/api.py +0 -0
  38. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/audit.py +0 -0
  39. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/auth_enterprise.py +0 -0
  40. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/auth_settings.py +0 -0
  41. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/branches.py +0 -0
  42. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/collections.py +0 -0
  43. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/compliance.py +0 -0
  44. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/connect.py +0 -0
  45. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/context.py +0 -0
  46. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/dashboard.py +0 -0
  47. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/deploy.py +0 -0
  48. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/env.py +0 -0
  49. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/experiments.py +0 -0
  50. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/fn_platform.py +0 -0
  51. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/frontends.py +0 -0
  52. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/gen.py +0 -0
  53. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/governance.py +0 -0
  54. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/import_cmd.py +0 -0
  55. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/init_cmd.py +0 -0
  56. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/inspect.py +0 -0
  57. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/integrations.py +0 -0
  58. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/link_cmd.py +0 -0
  59. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/local.py +0 -0
  60. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/logs.py +0 -0
  61. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/mcp.py +0 -0
  62. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/metrics.py +0 -0
  63. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/oidc_providers.py +0 -0
  64. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/org.py +0 -0
  65. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/org_key.py +0 -0
  66. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/orgs_projects.py +0 -0
  67. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/passkeys.py +0 -0
  68. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/perf.py +0 -0
  69. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/project.py +0 -0
  70. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/remote_config.py +0 -0
  71. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/rules.py +0 -0
  72. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/schema.py +0 -0
  73. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/secrets.py +0 -0
  74. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/shell.py +0 -0
  75. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/sim.py +0 -0
  76. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/simulate.py +0 -0
  77. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/templates.py +0 -0
  78. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/test_cmd.py +0 -0
  79. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/commands/wallet.py +0 -0
  80. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/local/docker-compose.yml +0 -0
  81. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/local/initdb/001_axis_tables.sql +0 -0
  82. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/local/initdb/002_axis_base_schema.sql +0 -0
  83. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/local/initdb/003_guard_policies.sql +0 -0
  84. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/local/initdb/010_local_stack.sql +0 -0
  85. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/main.py +0 -0
  86. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/utils/__init__.py +0 -0
  87. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/utils/api_client.py +0 -0
  88. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/utils/api_helpers.py +0 -0
  89. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/utils/auth_store.py +0 -0
  90. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/utils/config.py +0 -0
  91. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/utils/forge_local.py +0 -0
  92. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/utils/importers.py +0 -0
  93. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/utils/local_function_server.py +0 -0
  94. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/utils/local_stack.py +0 -0
  95. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/utils/log_tail.py +0 -0
  96. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/utils/loopback_host.py +0 -0
  97. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/utils/macho_symbols.py +0 -0
  98. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/utils/migrations.py +0 -0
  99. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/utils/mobile_types.py +0 -0
  100. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/utils/output_mode.py +0 -0
  101. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/utils/project_context.py +0 -0
  102. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/utils/schema_introspect.py +0 -0
  103. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/utils/scope_picker.py +0 -0
  104. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/utils/sim_offline.py +0 -0
  105. {sovaria_cli-0.8.3 → sovaria_cli-0.8.4}/apps/cli/src/utils/theme.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sovaria-cli
3
- Version: 0.8.3
3
+ Version: 0.8.4
4
4
  Summary: Sovaria BaaS command-line interface (sov)
5
5
  Project-URL: Homepage, https://sovaria.in
6
6
  Project-URL: Documentation, https://docs.sovaria.in/cli
@@ -1,3 +1,3 @@
1
1
  """Sovaria CLI — sov command."""
2
2
 
3
- __version__ = "0.8.3"
3
+ __version__ = "0.8.4"
@@ -15,7 +15,7 @@ from urllib.parse import quote
15
15
  import typer
16
16
 
17
17
  from apps.cli.src.utils.api_client import CLIRequestError, call_api
18
- from apps.cli.src.utils.display import handle_cli_error, print_json
18
+ from apps.cli.src.utils.display import confirm_destructive, handle_cli_error, print_json
19
19
 
20
20
  app = typer.Typer(help="Sovariq credits and limits (credits, never dollars)", no_args_is_help=True)
21
21
  limits_app = typer.Typer(help="The organization's Sovariq limits: who can use it, caps per member and project",
@@ -182,8 +182,9 @@ def set_member(user_id: str = typer.Argument(..., help="The member's user id"),
182
182
 
183
183
 
184
184
  @limits_app.command("remove-member")
185
- def remove_member(user_id: str = typer.Argument(...), profile: str | None = _PROFILE, org_id: str | None = _ORG) -> None:
185
+ def remove_member(user_id: str = typer.Argument(...), yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),profile: str | None = _PROFILE, org_id: str | None = _ORG) -> None:
186
186
  """Remove a member's own cap (the default for members applies again)."""
187
+ confirm_destructive(yes, f"Remove {user_id} from Sovariq credit sharing?")
187
188
  org = _org(profile, org_id)
188
189
  _call("DELETE", f"/api/v1/billing/ai-limits/{_seg(org)}/members/{_seg(user_id)}", profile=profile, org_id=org)
189
190
  typer.echo(f"{user_id}: own cap removed.")
@@ -200,8 +201,9 @@ def set_project(project: str = typer.Argument(..., help="The project id"), day:
200
201
 
201
202
 
202
203
  @limits_app.command("remove-project")
203
- def remove_project(project: str = typer.Argument(...), profile: str | None = _PROFILE, org_id: str | None = _ORG) -> None:
204
+ def remove_project(project: str = typer.Argument(...), yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),profile: str | None = _PROFILE, org_id: str | None = _ORG) -> None:
204
205
  """Remove a project's Sovariq cap."""
206
+ confirm_destructive(yes, f"Remove project {project} from Sovariq credit sharing?")
205
207
  org = _org(profile, org_id)
206
208
  _call("DELETE", f"/api/v1/billing/ai-limits/{_seg(org)}/projects/{_seg(project)}", profile=profile, org_id=org)
207
209
  typer.echo(f"{project}: cap removed.")
@@ -322,7 +322,7 @@ def gateway_usage(days: int = typer.Option(7, "--days", min=1, max=90), as_json:
322
322
 
323
323
 
324
324
  @gateway_app.command("cache-clear")
325
- def gateway_cache_clear(profile: str | None = _PROFILE, org_id: str | None = _ORG, project_id: str | None = _PROJECT) -> None:
325
+ def gateway_cache_clear( yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),profile: str | None = _PROFILE, org_id: str | None = _ORG, project_id: str | None = _PROJECT) -> None:
326
326
  print_json(_call("POST", "/api/v1/sovariq/gateway/cache/clear", profile=profile, org_id=org_id, project_id=project_id))
327
327
 
328
328
 
@@ -19,7 +19,12 @@ from urllib.parse import quote
19
19
  import typer
20
20
 
21
21
  from apps.cli.src.utils.api_client import CLIRequestError, call_api
22
- from apps.cli.src.utils.display import handle_cli_error, print_json, print_success
22
+ from apps.cli.src.utils.display import (
23
+ confirm_destructive,
24
+ handle_cli_error,
25
+ print_json,
26
+ print_success,
27
+ )
23
28
  from apps.cli.src.utils.output_mode import wants_json
24
29
 
25
30
  app = typer.Typer(help="API versions: pin function code for app builds already shipped (X-Sovaria-Api-Version)")
@@ -171,8 +176,9 @@ def retire(
171
176
 
172
177
 
173
178
  @app.command("delete")
174
- def delete_version(name: str, profile: str | None = Profile, org_id: str | None = Org, project_id: str | None = Project,
179
+ def delete_version(name: str, yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),profile: str | None = Profile, org_id: str | None = Org, project_id: str | None = Project,
175
180
  env: str | None = Env) -> None:
176
181
  """Delete a retired version."""
182
+ confirm_destructive(yes, f"Delete API version {name}?")
177
183
  out = _call("DELETE", _path(name), profile=profile, org_id=org_id, project_id=project_id, env=env)
178
184
  _done(out, f"Deleted {name}")
@@ -11,7 +11,7 @@ import typer
11
11
 
12
12
  from apps.cli.src.utils.api_client import CLIRequestError, call_api
13
13
  from apps.cli.src.utils.config import get_scope
14
- from apps.cli.src.utils.display import handle_cli_error, print_json
14
+ from apps.cli.src.utils.display import confirm_destructive, handle_cli_error, print_json
15
15
  from apps.cli.src.utils.premium_display import print_error_panel, run_with_spinner
16
16
 
17
17
  app = typer.Typer(help="Manage product users within an org/project scope")
@@ -129,12 +129,14 @@ def ban_user(
129
129
  @app.command("delete")
130
130
  def delete_user(
131
131
  user_id: str = typer.Argument(..., help="Product user id to delete"),
132
+ yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),
132
133
  profile: str | None = typer.Option(None, "--profile"),
133
134
  org_id: str | None = typer.Option(None, "--org"),
134
135
  project_id: str | None = typer.Option(None, "--project"),
135
136
  env_id: str | None = typer.Option(None, "--env"),
136
137
  ) -> None:
137
138
  """Delete a product user credential in scope."""
139
+ confirm_destructive(yes, f"Delete app user {user_id} and their sessions?")
138
140
  safe_user_id = quote(user_id, safe="")
139
141
  try:
140
142
  result = call_api(
@@ -230,12 +232,14 @@ def add_roles(
230
232
  def remove_roles(
231
233
  user_id: str = typer.Argument(..., help="Product user id"),
232
234
  roles: list[str] = typer.Argument(..., help="Roles to take away"),
235
+ yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),
233
236
  profile: str | None = typer.Option(None, "--profile"),
234
237
  org_id: str | None = typer.Option(None, "--org"),
235
238
  project_id: str | None = typer.Option(None, "--project"),
236
239
  env_id: str | None = typer.Option(None, "--env"),
237
240
  ) -> None:
238
241
  """Take roles away from a product user. The change applies to their requests within 20 seconds (usually at once), even on the token they already have."""
242
+ confirm_destructive(yes, f"Remove roles from app user {user_id}?")
239
243
  try:
240
244
  result = _change_roles(user_id, [], roles, profile, org_id, project_id, env_id)
241
245
  except CLIRequestError as exc:
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import typer
6
6
 
7
7
  from apps.cli.src.utils.api_helpers import premium_api, premium_mutate
8
- from apps.cli.src.utils.display import handle_command_error
8
+ from apps.cli.src.utils.display import confirm_destructive, handle_command_error
9
9
  from apps.cli.src.utils.project_context import resolve_org_id
10
10
 
11
11
  app = typer.Typer(help="Billing and subscription commands (Ledger engine)")
@@ -106,9 +106,11 @@ def set_subscription(
106
106
  def cancel_subscription(
107
107
  org_id: str = typer.Argument(..., help="Organisation ID"),
108
108
  immediate: bool = typer.Option(False, "--immediate", help="Cancel immediately"),
109
+ yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),
109
110
  profile: str | None = typer.Option(None, "--profile"),
110
111
  ) -> None:
111
112
  """Cancel an organisation subscription."""
113
+ confirm_destructive(yes, f"Cancel the subscription of organization {org_id}?")
112
114
  from urllib.parse import quote
113
115
 
114
116
  params = {"immediate": "true"} if immediate else None
@@ -461,9 +463,11 @@ def account_get(
461
463
  @account_app.command("cancel")
462
464
  def account_cancel(
463
465
  account_id: str = typer.Argument(..., help="Billing account ID"),
466
+ yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),
464
467
  profile: str | None = typer.Option(None, "--profile"),
465
468
  ) -> None:
466
469
  """Cancel a billing account subscription."""
470
+ confirm_destructive(yes, f"Cancel billing account {account_id}?")
467
471
  from urllib.parse import quote
468
472
 
469
473
  premium_api(
@@ -28,7 +28,13 @@ import typer
28
28
  import yaml
29
29
 
30
30
  from apps.cli.src.utils.api_client import CLIRequestError, call_api
31
- from apps.cli.src.utils.display import handle_cli_error, print_json, print_success, print_warning
31
+ from apps.cli.src.utils.display import (
32
+ confirm_destructive,
33
+ handle_cli_error,
34
+ print_json,
35
+ print_success,
36
+ print_warning,
37
+ )
32
38
  from apps.cli.src.utils.premium_display import run_with_spinner
33
39
 
34
40
  app = typer.Typer(help="App builds: check schema, rules, functions and tests, then open them as a pull request")
@@ -120,9 +126,10 @@ def show(build_id: str = typer.Argument(...), profile: str | None = _PROFILE, or
120
126
 
121
127
 
122
128
  @app.command("delete")
123
- def delete(build_id: str = typer.Argument(...), profile: str | None = _PROFILE, org_id: str | None = _ORG,
129
+ def delete(build_id: str = typer.Argument(...), yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),profile: str | None = _PROFILE, org_id: str | None = _ORG,
124
130
  project_id: str | None = _PROJECT, env_id: str | None = _ENV) -> None:
125
131
  """Forget a build (an opened pull request stays on GitHub)."""
132
+ confirm_destructive(yes, f"Delete build {build_id}?")
126
133
  _call("DELETE", f"/api/v1/fn/builds/{quote(build_id, safe='')}", profile=profile, org_id=org_id,
127
134
  project_id=project_id, env_id=env_id)
128
135
  print_success(f"Build {build_id} deleted")
@@ -7,7 +7,7 @@ from urllib.parse import quote
7
7
  import typer
8
8
 
9
9
  from apps.cli.src.utils.api_helpers import premium_api, premium_mutate
10
- from apps.cli.src.utils.display import print_json
10
+ from apps.cli.src.utils.display import confirm_destructive, print_json
11
11
 
12
12
  app = typer.Typer(help="Chrono timeline and snapshot operations")
13
13
 
@@ -71,9 +71,11 @@ def timeline_spawn(
71
71
  @app.command("timeline-delete")
72
72
  def timeline_delete(
73
73
  name: str = typer.Argument(...),
74
+ yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),
74
75
  profile: str | None = typer.Option(None, "--profile"),
75
76
  ) -> None:
76
77
  """Delete a timeline."""
78
+ confirm_destructive(yes, f"Delete timeline {name} and its snapshots?")
77
79
  premium_mutate(
78
80
  f"Deleting timeline '{name}'…",
79
81
  success="Deleted",
@@ -267,9 +269,11 @@ def snapshot_restore(
267
269
  @app.command("snapshot-delete")
268
270
  def snapshot_delete(
269
271
  snapshot_id: int = typer.Argument(...),
272
+ yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),
270
273
  profile: str | None = typer.Option(None, "--profile"),
271
274
  ) -> None:
272
275
  """Forget a snapshot (its commit stays in the history)."""
276
+ confirm_destructive(yes, f"Delete snapshot {snapshot_id}?")
273
277
  premium_mutate("Deleting the snapshot…", success="Deleted", title="Snapshot", method="DELETE",
274
278
  path=f"/api/v1/chrono/snapshots/{snapshot_id}", profile=profile)
275
279
 
@@ -24,7 +24,12 @@ import typer
24
24
 
25
25
  from apps.cli.src.utils import macho_symbols
26
26
  from apps.cli.src.utils.api_client import CLIRequestError, call_api
27
- from apps.cli.src.utils.display import handle_cli_error, print_json, print_success
27
+ from apps.cli.src.utils.display import (
28
+ confirm_destructive,
29
+ handle_cli_error,
30
+ print_json,
31
+ print_success,
32
+ )
28
33
  from apps.cli.src.utils.output_mode import wants_json
29
34
 
30
35
  app = typer.Typer(help="App crash reports grouped into issues (from sov.crashes in your app)")
@@ -291,10 +296,12 @@ def debug_files(
291
296
  @app.command("debug-files-delete")
292
297
  def debug_files_delete(
293
298
  file_id: str,
299
+ yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),
294
300
  profile: str | None = typer.Option(None, "--profile"), org_id: str | None = typer.Option(None, "--org"),
295
301
  project_id: str | None = typer.Option(None, "--project"), env_id: str | None = typer.Option(None, "--env"),
296
302
  ) -> None:
297
303
  """Delete one debug file."""
304
+ confirm_destructive(yes, f"Delete debug file {file_id}?")
298
305
  try:
299
306
  call_api(method="DELETE", path=f"/api/v1/crashes/debug-files/{quote(file_id, safe='')}", **_opts(profile, org_id, project_id, env_id))
300
307
  except CLIRequestError as exc:
@@ -8,7 +8,7 @@ from urllib.parse import quote
8
8
  import typer
9
9
 
10
10
  from apps.cli.src.utils.api_client import CLIRequestError, call_api, parse_json_arg
11
- from apps.cli.src.utils.display import handle_cli_error, print_json
11
+ from apps.cli.src.utils.display import confirm_destructive, handle_cli_error, print_json
12
12
  from apps.cli.src.utils.premium_display import run_with_spinner
13
13
 
14
14
  app = typer.Typer(help="Atlas-Doc and Atlas-Rel operations")
@@ -82,12 +82,14 @@ def doc_get(
82
82
  def doc_delete(
83
83
  collection: str = typer.Argument(...),
84
84
  document_id: str = typer.Argument(...),
85
+ yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),
85
86
  profile: str | None = typer.Option(None, "--profile"),
86
87
  org_id: str | None = typer.Option(None, "--org"),
87
88
  project_id: str | None = typer.Option(None, "--project"),
88
89
  env_id: str | None = typer.Option(None, "--env"),
89
90
  ) -> None:
90
91
  """Delete a document."""
92
+ confirm_destructive(yes, f"Delete document {collection}/{document_id}?")
91
93
  try:
92
94
  path = _safe_doc_path(collection, document_id)
93
95
  result = call_api(
@@ -212,12 +214,14 @@ def table_create(
212
214
  @app.command("table-drop")
213
215
  def table_drop(
214
216
  table_name: str = typer.Argument(...),
217
+ yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),
215
218
  profile: str | None = typer.Option(None, "--profile"),
216
219
  org_id: str | None = typer.Option(None, "--org"),
217
220
  project_id: str | None = typer.Option(None, "--project"),
218
221
  env_id: str | None = typer.Option(None, "--env"),
219
222
  ) -> None:
220
223
  """Drop a relational table."""
224
+ confirm_destructive(yes, f"Drop table {table_name} and all its rows?")
221
225
  try:
222
226
  result = call_api(
223
227
  method="POST",
@@ -18,9 +18,8 @@ Commands:
18
18
 
19
19
  from __future__ import annotations
20
20
 
21
- import re
22
-
23
21
  import json
22
+ import re
24
23
  from datetime import UTC, datetime, timedelta
25
24
  from pathlib import Path
26
25
 
@@ -28,6 +27,7 @@ import typer
28
27
 
29
28
  from apps.cli.src.utils.api_client import CLIRequestError, call_api, parse_json_arg
30
29
  from apps.cli.src.utils.display import (
30
+ confirm_destructive,
31
31
  handle_cli_error,
32
32
  print_banner,
33
33
  print_error,
@@ -714,12 +714,14 @@ def column_add(
714
714
  def column_drop(
715
715
  table: str = typer.Argument(...),
716
716
  name: str = typer.Argument(..., help="Column name"),
717
+ yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),
717
718
  profile: str | None = typer.Option(None, "--profile"),
718
719
  org_id: str | None = typer.Option(None, "--org"),
719
720
  project_id: str | None = typer.Option(None, "--project"),
720
721
  env_id: str | None = typer.Option(None, "--env"),
721
722
  ) -> None:
722
723
  """Drop a column from a table."""
724
+ confirm_destructive(yes, f"Drop column {name} from {table} (its data goes too)?")
723
725
  try:
724
726
  result = call_api(
725
727
  method="POST",
@@ -773,12 +775,14 @@ def fk_drop(
773
775
  column: str = typer.Argument(...),
774
776
  ref_table: str = typer.Option(..., "--ref-table"),
775
777
  ref_column: str = typer.Option(..., "--ref-column"),
778
+ yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),
776
779
  profile: str | None = typer.Option(None, "--profile"),
777
780
  org_id: str | None = typer.Option(None, "--org"),
778
781
  project_id: str | None = typer.Option(None, "--project"),
779
782
  env_id: str | None = typer.Option(None, "--env"),
780
783
  ) -> None:
781
784
  """Drop a foreign key."""
785
+ confirm_destructive(yes, f"Drop the foreign key on {table}.{column}?")
782
786
  try:
783
787
  result = call_api(
784
788
  method="POST",
@@ -22,7 +22,12 @@ import typer
22
22
 
23
23
  from apps.cli.src.utils.api_client import CLIRequestError, call_api
24
24
  from apps.cli.src.utils.config import get_scope
25
- from apps.cli.src.utils.display import handle_cli_error, print_json, print_success
25
+ from apps.cli.src.utils.display import (
26
+ confirm_destructive,
27
+ handle_cli_error,
28
+ print_json,
29
+ print_success,
30
+ )
26
31
  from apps.cli.src.utils.output_mode import wants_json
27
32
 
28
33
  app = typer.Typer(help="Tester builds (APK, AAB, IPA): upload, share with tester groups, invite testers")
@@ -128,8 +133,9 @@ def testers_add(emails: list[str] = typer.Argument(...), groups: str = typer.Opt
128
133
 
129
134
 
130
135
  @testers_app.command("remove")
131
- def testers_remove(tester_id: str, profile: str | None = _PROFILE, org_id: str | None = _ORG,
136
+ def testers_remove(tester_id: str, yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),profile: str | None = _PROFILE, org_id: str | None = _ORG,
132
137
  project_id: str | None = _PROJECT) -> None:
138
+ confirm_destructive(yes, f"Remove tester {tester_id}?")
133
139
  _call("DELETE", f"{_base(profile, project_id)}/testers/{quote(tester_id, safe='')}", profile=profile, org_id=org_id,
134
140
  project_id=project_id)
135
141
  print_success("Tester removed; their install links no longer work")
@@ -28,7 +28,12 @@ import typer
28
28
 
29
29
  from apps.cli.src.utils.api_client import CLIRequestError, build_request_context, call_api
30
30
  from apps.cli.src.utils.config import get_api_url
31
- from apps.cli.src.utils.display import handle_cli_error, print_json, print_success
31
+ from apps.cli.src.utils.display import (
32
+ confirm_destructive,
33
+ handle_cli_error,
34
+ print_json,
35
+ print_success,
36
+ )
32
37
  from apps.cli.src.utils.output_mode import wants_json
33
38
 
34
39
  app = typer.Typer(help="Web hosting: deploy static sites (Vite, React, Next export…) to <site>.sovaria.app")
@@ -382,10 +387,11 @@ def channels_deploy(
382
387
 
383
388
 
384
389
  @channels_app.command("delete")
385
- def channels_delete(channel: str, site: str | None = typer.Option(None, "--site"), profile: str | None = typer.Option(None, "--profile"),
390
+ def channels_delete(channel: str, site: str | None = typer.Option(None, "--site"), yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),profile: str | None = typer.Option(None, "--profile"),
386
391
  org_id: str | None = typer.Option(None, "--org"), project_id: str | None = typer.Option(None, "--project"),
387
392
  env_id: str | None = typer.Option(None, "--env")) -> None:
388
393
  """Delete a preview channel now."""
394
+ confirm_destructive(yes, f"Delete channel {channel}?")
389
395
  name = _site_of(site, _load_config())
390
396
  try:
391
397
  call_api(method="DELETE", path=f"{_BASE}/sites/{quote(name, safe='')}/channels/{quote(channel, safe='')}",
@@ -453,9 +459,10 @@ def domains_verify(domain_id: str, profile: str | None = typer.Option(None, "--p
453
459
 
454
460
 
455
461
  @domains_app.command("remove")
456
- def domains_remove(domain_id: str, profile: str | None = typer.Option(None, "--profile"), org_id: str | None = typer.Option(None, "--org"),
462
+ def domains_remove(domain_id: str, yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),profile: str | None = typer.Option(None, "--profile"), org_id: str | None = typer.Option(None, "--org"),
457
463
  project_id: str | None = typer.Option(None, "--project"), env_id: str | None = typer.Option(None, "--env")) -> None:
458
464
  """Disconnect a domain."""
465
+ confirm_destructive(yes, f"Remove domain {domain_id}?")
459
466
  org, _ = _org_and_project(profile, org_id, project_id, env_id)
460
467
  try:
461
468
  call_api(method="DELETE", path=f"/api/v1/governance/tenants/{quote(org, safe='')}/domains/{quote(domain_id, safe='')}",
@@ -502,9 +509,10 @@ def tokens_create(name: str = typer.Option("CI", "--name"), site: str | None = t
502
509
 
503
510
 
504
511
  @tokens_app.command("revoke")
505
- def tokens_revoke(token_id: str, site: str | None = typer.Option(None, "--site"), profile: str | None = typer.Option(None, "--profile"),
512
+ def tokens_revoke(token_id: str, site: str | None = typer.Option(None, "--site"), yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),profile: str | None = typer.Option(None, "--profile"),
506
513
  org_id: str | None = typer.Option(None, "--org"), project_id: str | None = typer.Option(None, "--project"),
507
514
  env_id: str | None = typer.Option(None, "--env")) -> None:
515
+ confirm_destructive(yes, f"Revoke deploy token {token_id}?")
508
516
  name = _site_of(site, _load_config())
509
517
  try:
510
518
  call_api(method="DELETE", path=f"{_BASE}/sites/{quote(name, safe='')}/tokens/{quote(token_id, safe='')}",
@@ -8,7 +8,13 @@ import typer
8
8
 
9
9
  from apps.cli.src.utils.api_client import CLIRequestError, call_api, parse_json_arg
10
10
  from apps.cli.src.utils.api_helpers import premium_api, premium_mutate
11
- from apps.cli.src.utils.display import handle_cli_error, handle_command_error, print_json, print_success
11
+ from apps.cli.src.utils.display import (
12
+ confirm_destructive,
13
+ handle_cli_error,
14
+ handle_command_error,
15
+ print_json,
16
+ print_success,
17
+ )
12
18
  from apps.cli.src.utils.project_context import resolve_org_id
13
19
 
14
20
  app = typer.Typer(help="Organisations, members, and project listings")
@@ -239,9 +245,11 @@ def org_set_plan(
239
245
  def member_remove(
240
246
  org_id: str = typer.Argument(..., help="Organisation ID"),
241
247
  user_id: str = typer.Argument(..., help="User node ID"),
248
+ yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),
242
249
  profile: str | None = typer.Option(None, "--profile"),
243
250
  ) -> None:
244
251
  """Remove a member from an organisation."""
252
+ confirm_destructive(yes, f"Remove {user_id} from organization {org_id}?")
245
253
  premium_api(
246
254
  "Updating organisation…",
247
255
  success="Updated",
@@ -417,12 +425,14 @@ def roles_matrix_add(
417
425
  @roles_matrix_app.command("remove")
418
426
  def roles_matrix_remove(
419
427
  name: str = typer.Argument(..., help="Role to remove (users holding it lose it)"),
428
+ yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),
420
429
  profile: str | None = typer.Option(None, "--profile"),
421
430
  org_id: str | None = typer.Option(None, "--org"),
422
431
  project_id: str | None = typer.Option(None, "--project"),
423
432
  env_id: str | None = typer.Option(None, "--env"),
424
433
  ) -> None:
425
434
  """Remove one app role, keeping the others."""
435
+ confirm_destructive(yes, f"Remove role {name} from the role matrix?")
426
436
  scope = {"profile": profile, "org_id": org_id, "project_id": project_id, "env_id": env_id}
427
437
  try:
428
438
  roles = _matrix_lists(scope)
@@ -7,7 +7,13 @@ from urllib.parse import quote
7
7
  import typer
8
8
 
9
9
  from apps.cli.src.utils.api_client import CLIRequestError, call_api
10
- from apps.cli.src.utils.display import handle_cli_error, print_json, print_success, print_warning
10
+ from apps.cli.src.utils.display import (
11
+ confirm_destructive,
12
+ handle_cli_error,
13
+ print_json,
14
+ print_success,
15
+ print_warning,
16
+ )
11
17
  from apps.cli.src.utils.premium_display import run_with_spinner
12
18
 
13
19
  app = typer.Typer(help="Project API keys (anon / service_role / custom)")
@@ -125,12 +131,14 @@ def ensure_defaults(
125
131
  @app.command("revoke")
126
132
  def revoke_key(
127
133
  key_id: str = typer.Argument(..., help="API key ID"),
134
+ yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),
128
135
  profile: str | None = typer.Option(None, "--profile"),
129
136
  org_id: str | None = typer.Option(None, "--org"),
130
137
  project_id: str | None = typer.Option(None, "--project"),
131
138
  env_id: str | None = typer.Option(None, "--env"),
132
139
  ) -> None:
133
140
  """Revoke an API key."""
141
+ confirm_destructive(yes, f"Revoke API key {key_id}? Apps using it stop working.")
134
142
  try:
135
143
  result = call_api(
136
144
  method="DELETE",
@@ -11,7 +11,7 @@ from urllib.parse import quote
11
11
  import typer
12
12
 
13
13
  from apps.cli.src.utils.api_client import CLIRequestError, call_api, parse_json_arg, stream_events
14
- from apps.cli.src.utils.display import handle_cli_error, print_json
14
+ from apps.cli.src.utils.display import confirm_destructive, handle_cli_error, print_json
15
15
  from apps.cli.src.utils.premium_display import run_with_spinner
16
16
 
17
17
  app = typer.Typer(help="Knowledge bases (RAG): add documents, search them with citations")
@@ -122,8 +122,10 @@ def documents(name: str = typer.Argument(...), limit: int = typer.Option(50, "--
122
122
  @app.command("rm-doc")
123
123
  def remove_document(name: str = typer.Argument(...), document_id: str = typer.Argument(...),
124
124
  org_id: str | None = _ORG, project_id: str | None = _PROJECT, env_id: str | None = _ENV,
125
+ yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),
125
126
  profile: str | None = _PROFILE) -> None:
126
127
  """Remove one document (all its sections)."""
128
+ confirm_destructive(yes, f"Remove document {document_id} from knowledge base {name}?")
127
129
  print_json(_call("DELETE", f"{_KB}/{_seg(name)}/documents/{_seg(document_id)}", org_id=org_id,
128
130
  project_id=project_id, env_id=env_id, profile=profile))
129
131
 
@@ -48,7 +48,12 @@ from apps.cli.src.utils.config import (
48
48
  get_api_url,
49
49
  get_console_url,
50
50
  )
51
- from apps.cli.src.utils.display import handle_cli_error, print_success, print_warning
51
+ from apps.cli.src.utils.display import (
52
+ confirm_destructive,
53
+ handle_cli_error,
54
+ print_success,
55
+ print_warning,
56
+ )
52
57
  from apps.cli.src.utils.premium_display import (
53
58
  format_http_error,
54
59
  print_brand_panel,
@@ -701,9 +706,11 @@ def list_sessions(
701
706
  @app.command("sessions-revoke")
702
707
  def revoke_session(
703
708
  session_id: str = typer.Argument(..., help="Session id (refresh token JTI)"),
709
+ yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),
704
710
  profile: str | None = typer.Option(None, "--profile"),
705
711
  ) -> None:
706
712
  """Revoke one active session."""
713
+ confirm_destructive(yes, f"Sign out session {session_id}?")
707
714
  from urllib.parse import quote
708
715
 
709
716
  from apps.cli.src.utils.api_client import CLIRequestError, call_api
@@ -726,9 +733,11 @@ def revoke_session(
726
733
 
727
734
  @app.command("sessions-revoke-others")
728
735
  def revoke_other_sessions(
736
+ yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),
729
737
  profile: str | None = typer.Option(None, "--profile"),
730
738
  ) -> None:
731
739
  """Revoke all sessions except the current one."""
740
+ confirm_destructive(yes, "Sign out every other session of your account?")
732
741
  from apps.cli.src.utils.api_client import CLIRequestError, call_api
733
742
 
734
743
  try:
@@ -1098,12 +1107,14 @@ def event_hooks_create(
1098
1107
  @event_hooks_app.command("delete")
1099
1108
  def event_hooks_delete(
1100
1109
  hook_id: str = typer.Argument(..., help="Auth event hook ID"),
1110
+ yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),
1101
1111
  profile: str | None = typer.Option(None, "--profile"),
1102
1112
  org_id: str | None = typer.Option(None, "--org"),
1103
1113
  project_id: str | None = typer.Option(None, "--project"),
1104
1114
  env_id: str | None = typer.Option(None, "--env"),
1105
1115
  ) -> None:
1106
1116
  """Delete an auth event hook (owners and admins only)."""
1117
+ confirm_destructive(yes, f"Delete auth event hook {hook_id}?")
1107
1118
  from urllib.parse import quote
1108
1119
 
1109
1120
  from apps.cli.src.utils.api_client import CLIRequestError, call_api
@@ -9,7 +9,7 @@ import typer
9
9
 
10
10
  from apps.cli.src.utils.api_client import CLIRequestError, call_api
11
11
  from apps.cli.src.utils.config import get_scope
12
- from apps.cli.src.utils.display import handle_cli_error, print_json
12
+ from apps.cli.src.utils.display import confirm_destructive, handle_cli_error, print_json
13
13
  from apps.cli.src.utils.premium_display import run_with_spinner
14
14
 
15
15
  app = typer.Typer(help="Messages to app users over push and email: templates, audiences, sends, delivery log")
@@ -76,9 +76,10 @@ def template_save(
76
76
 
77
77
 
78
78
  @app.command("template-delete")
79
- def template_delete(name: str = typer.Argument(...), profile: str | None = _PROFILE, org_id: str | None = _ORG,
79
+ def template_delete(name: str = typer.Argument(...), yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),profile: str | None = _PROFILE, org_id: str | None = _ORG,
80
80
  project_id: str | None = _PROJECT, env_id: str | None = _ENV) -> None:
81
81
  """Delete a template."""
82
+ confirm_destructive(yes, f"Delete message template {name}?")
82
83
  _call("DELETE", f"{_base(profile, project_id)}/templates/{quote(name, safe='')}", profile=profile, org_id=org_id,
83
84
  project_id=project_id, env_id=env_id)
84
85
  print_json({"deleted": name})
@@ -178,9 +179,10 @@ def send(
178
179
 
179
180
 
180
181
  @app.command("cancel")
181
- def cancel(send_id: str = typer.Argument(...), profile: str | None = _PROFILE, org_id: str | None = _ORG,
182
+ def cancel(send_id: str = typer.Argument(...), yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),profile: str | None = _PROFILE, org_id: str | None = _ORG,
182
183
  project_id: str | None = _PROJECT, env_id: str | None = _ENV) -> None:
183
184
  """Cancel a scheduled or running send (recipients not reached yet get nothing)."""
185
+ confirm_destructive(yes, f"Cancel send {send_id}?")
184
186
  print_json(_call("POST", f"{_base(profile, project_id)}/sends/{quote(send_id, safe='')}/cancel", body={}, profile=profile,
185
187
  org_id=org_id, project_id=project_id, env_id=env_id))
186
188
 
@@ -261,9 +263,10 @@ def in_app_save(
261
263
 
262
264
 
263
265
  @app.command("in-app-delete")
264
- def in_app_delete(name: str = typer.Argument(...), profile: str | None = _PROFILE, org_id: str | None = _ORG,
266
+ def in_app_delete(name: str = typer.Argument(...), yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),profile: str | None = _PROFILE, org_id: str | None = _ORG,
265
267
  project_id: str | None = _PROJECT, env_id: str | None = _ENV) -> None:
266
268
  """Delete an in-app message."""
269
+ confirm_destructive(yes, f"Delete in-app message {name}?")
267
270
  _call("DELETE", f"{_base(profile, project_id)}/in-app/{quote(name, safe='')}", profile=profile, org_id=org_id,
268
271
  project_id=project_id, env_id=env_id)
269
272
  typer.echo(f"Deleted {name}")
@@ -9,7 +9,7 @@ import typer
9
9
 
10
10
  from apps.cli.src.utils.api_client import CLIRequestError, call_api, parse_json_arg
11
11
  from apps.cli.src.utils.api_helpers import premium_api, premium_mutate
12
- from apps.cli.src.utils.display import handle_cli_error, print_json
12
+ from apps.cli.src.utils.display import confirm_destructive, handle_cli_error, print_json
13
13
  from apps.cli.src.utils.premium_display import run_with_spinner
14
14
  from apps.cli.src.utils.project_context import resolve_scope
15
15
 
@@ -166,12 +166,14 @@ def upsert_policy(
166
166
  @app.command("delete")
167
167
  def delete_policy(
168
168
  policy_id: str = typer.Argument(...),
169
+ yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),
169
170
  profile: str | None = typer.Option(None, "--profile"),
170
171
  org_id: str | None = typer.Option(None, "--org"),
171
172
  project_id: str | None = typer.Option(None, "--project"),
172
173
  env_id: str | None = typer.Option(None, "--env"),
173
174
  ) -> None:
174
175
  """Delete (deactivate) a policy."""
176
+ confirm_destructive(yes, f"Delete policy {policy_id}? What it allows stops working at once.")
175
177
  safe_id = quote(policy_id, safe="")
176
178
  premium_mutate(
177
179
  "Deleting policy…",
@@ -15,7 +15,7 @@ import typer
15
15
 
16
16
  from apps.cli.src.utils.api_client import CLIRequestError, call_api
17
17
  from apps.cli.src.utils.config import get_scope
18
- from apps.cli.src.utils.display import handle_cli_error, print_json
18
+ from apps.cli.src.utils.display import confirm_destructive, handle_cli_error, print_json
19
19
 
20
20
  app = typer.Typer(help="Push notifications: providers, web push keys, sends")
21
21
  _ORG = typer.Option(None, "--org")
@@ -94,8 +94,9 @@ def sends(limit: int = typer.Option(20, "--limit", min=1, max=200), profile: str
94
94
 
95
95
 
96
96
  @app.command("cancel")
97
- def cancel(send_id: str = typer.Argument(...), profile: str | None = _PROFILE, org_id: str | None = _ORG,
97
+ def cancel(send_id: str = typer.Argument(...), yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),profile: str | None = _PROFILE, org_id: str | None = _ORG,
98
98
  project_id: str | None = _PROJECT, env_id: str | None = _ENV) -> None:
99
99
  """Cancel a queued or running large send."""
100
+ confirm_destructive(yes, f"Cancel push send {send_id}?")
100
101
  print_json(_call("POST", f"{_base(profile, project_id)}/sends/{quote(send_id, safe='')}/cancel", body={}, profile=profile,
101
102
  org_id=org_id, project_id=project_id, env_id=env_id))
@@ -19,7 +19,12 @@ from urllib.parse import quote
19
19
  import typer
20
20
 
21
21
  from apps.cli.src.utils.api_client import CLIRequestError, call_api
22
- from apps.cli.src.utils.display import handle_cli_error, print_json, print_success
22
+ from apps.cli.src.utils.display import (
23
+ confirm_destructive,
24
+ handle_cli_error,
25
+ print_json,
26
+ print_success,
27
+ )
23
28
  from apps.cli.src.utils.output_mode import wants_json
24
29
 
25
30
  app = typer.Typer(help="Message queues: send, read with a visibility timeout, archive, dead letters, metrics, consumers")
@@ -264,9 +269,10 @@ def archive(name: str, msg_ids: list[int] = typer.Argument(...), profile: str |
264
269
 
265
270
 
266
271
  @app.command("delete-messages")
267
- def delete_messages(name: str, msg_ids: list[int] = typer.Argument(...), profile: str | None = Profile,
272
+ def delete_messages(name: str, msg_ids: list[int] = typer.Argument(...), yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),profile: str | None = Profile,
268
273
  org_id: str | None = Org, project_id: str | None = Project, env: str | None = Env) -> None:
269
274
  """Delete messages for good."""
275
+ confirm_destructive(yes, f"Delete these messages from queue {name}?")
270
276
  out = _call("POST", _path(name, "delete"), body={"msg_ids": msg_ids}, profile=profile, org_id=org_id,
271
277
  project_id=project_id, env=env)
272
278
  print_success(f"Deleted {len(out.get('deleted') or [])} of {len(msg_ids)}")
@@ -10,7 +10,7 @@ import typer
10
10
  from apps.cli.src.utils.api_client import CLIRequestError, parse_json_arg
11
11
  from apps.cli.src.utils.api_helpers import premium_api
12
12
  from apps.cli.src.utils.config import get_api_url, get_scope
13
- from apps.cli.src.utils.display import handle_cli_error
13
+ from apps.cli.src.utils.display import confirm_destructive, handle_cli_error
14
14
  from apps.cli.src.utils.premium_display import print_brand_panel, print_error_panel
15
15
 
16
16
  app = typer.Typer(help="Realtime stream and E2EE operations")
@@ -302,12 +302,14 @@ def e2ee_revoke(
302
302
  channel: str = typer.Argument(...),
303
303
  key_id: str | None = typer.Option(None, "--key-id"),
304
304
  reason: str | None = typer.Option(None, "--reason"),
305
+ yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),
305
306
  profile: str | None = typer.Option(None, "--profile"),
306
307
  org_id: str | None = typer.Option(None, "--org"),
307
308
  project_id: str | None = typer.Option(None, "--project"),
308
309
  env_id: str | None = typer.Option(None, "--env"),
309
310
  ) -> None:
310
311
  """Revoke an E2EE key."""
312
+ confirm_destructive(yes, f"Revoke end-to-end encryption keys of channel {channel}?")
311
313
  premium_api(
312
314
  "Revoking E2EE key…",
313
315
  success="E2EE key revoked",
@@ -4,10 +4,10 @@ from __future__ import annotations
4
4
 
5
5
  import typer
6
6
 
7
- from apps.cli.src.utils.api_helpers import require_login
8
7
  from apps.cli.src.utils.api_client import CLIRequestError
9
- from apps.cli.src.utils.display import handle_cli_error, print_success
8
+ from apps.cli.src.utils.api_helpers import require_login
10
9
  from apps.cli.src.utils.config import clear_scope, get_scope, set_scope
10
+ from apps.cli.src.utils.display import handle_cli_error, print_success
11
11
  from apps.cli.src.utils.premium_display import print_next_steps, print_scope_card
12
12
  from apps.cli.src.utils.scope_picker import interactive_pick_scope, print_access_catalog
13
13
 
@@ -53,7 +53,7 @@ def set_scope_cmd(
53
53
 
54
54
 
55
55
  @app.command("clear")
56
- def clear_scope_cmd(profile: str | None = typer.Option(None, "--profile")) -> None:
56
+ def clear_scope_cmd( yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),profile: str | None = typer.Option(None, "--profile")) -> None:
57
57
  """Clear default scope for the active profile."""
58
58
  clear_scope(profile)
59
59
  print_success("Default scope cleared")
@@ -24,7 +24,12 @@ import httpx
24
24
  import typer
25
25
 
26
26
  from apps.cli.src.utils.api_client import CLIRequestError, build_request_context, call_api
27
- from apps.cli.src.utils.display import handle_cli_error, print_json, print_success
27
+ from apps.cli.src.utils.display import (
28
+ confirm_destructive,
29
+ handle_cli_error,
30
+ print_json,
31
+ print_success,
32
+ )
28
33
  from apps.cli.src.utils.premium_display import run_with_spinner
29
34
 
30
35
  app = typer.Typer(help="Object storage management")
@@ -323,9 +328,11 @@ def delete_object(
323
328
  project_id: str | None = typer.Option(None, "--project", help="Project ID"),
324
329
  org_id: str | None = typer.Option(None, "--org"),
325
330
  env_id: str | None = typer.Option(None, "--env"),
331
+ yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),
326
332
  profile: str | None = typer.Option(None, "--profile"),
327
333
  ) -> None:
328
334
  """Delete an object from a bucket."""
335
+ confirm_destructive(yes, f"Delete {bucket}/{object_path}?")
329
336
  try:
330
337
  call_api(
331
338
  method="DELETE",
@@ -8,7 +8,7 @@ from urllib.parse import quote
8
8
  import typer
9
9
 
10
10
  from apps.cli.src.utils.api_client import CLIRequestError, call_api, parse_json_arg
11
- from apps.cli.src.utils.display import handle_cli_error, print_json
11
+ from apps.cli.src.utils.display import confirm_destructive, handle_cli_error, print_json
12
12
  from apps.cli.src.utils.premium_display import run_with_spinner
13
13
 
14
14
  app = typer.Typer(help="Vector search and embeddings: collections and SQL tables")
@@ -371,9 +371,11 @@ def delete_record(
371
371
  org_id: str | None = typer.Option(None, "--org"),
372
372
  project_id: str | None = typer.Option(None, "--project"),
373
373
  env_id: str | None = typer.Option(None, "--env"),
374
+ yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),
374
375
  profile: str | None = typer.Option(None, "--profile"),
375
376
  ) -> None:
376
377
  """Delete one record from a vector collection."""
378
+ confirm_destructive(yes, f"Delete record {record_id} from {collection}?")
377
379
  try:
378
380
  result = call_api(
379
381
  method="DELETE",
@@ -398,9 +400,11 @@ def delete_collection(
398
400
  org_id: str | None = typer.Option(None, "--org"),
399
401
  project_id: str | None = typer.Option(None, "--project"),
400
402
  env_id: str | None = typer.Option(None, "--env"),
403
+ yes: bool = typer.Option(False, "--yes", "-y", help="Don't ask"),
401
404
  profile: str | None = typer.Option(None, "--profile"),
402
405
  ) -> None:
403
406
  """Delete an entire vector collection."""
407
+ confirm_destructive(yes, f"Delete vector collection {collection} and every record in it?")
404
408
  try:
405
409
  result = call_api(
406
410
  method="DELETE",
@@ -100,3 +100,14 @@ def handle_command_error(exc: Exception) -> None:
100
100
 
101
101
  print_error_panel(str(exc))
102
102
  raise typer.Exit(1) from exc
103
+
104
+
105
+ def confirm_destructive(yes: bool, question: str) -> None:
106
+ """Ask before a destructive command in an interactive terminal; ``--yes`` skips it. Scripts and CI (no terminal)
107
+ run as they always did, so adding the question to a command that never asked breaks nobody's automation."""
108
+ import sys
109
+
110
+ if yes or not (sys.stdin.isatty() and sys.stdout.isatty()):
111
+ return
112
+ if not typer.confirm(f"{question} This can't be undone.", default=False):
113
+ raise typer.Exit(1)
@@ -209,6 +209,20 @@ def _detail_text(detail: Any) -> str:
209
209
  # ── Smart response rendering ──────────────────────────────────────────────────
210
210
 
211
211
 
212
+ def readable_times(value: Any, key: str = "") -> Any:
213
+ """Unix times in ``*_at`` fields as UTC dates for people (``expires_at 1793962538.2`` read as nothing);
214
+ --json output keeps the numbers."""
215
+ if isinstance(value, dict):
216
+ return {k: readable_times(v, str(k)) for k, v in value.items()}
217
+ if isinstance(value, list):
218
+ return [readable_times(v, key) for v in value]
219
+ if key.endswith("_at") and isinstance(value, (int, float)) and not isinstance(value, bool) and 1e9 <= value < 1e11:
220
+ from datetime import UTC, datetime
221
+
222
+ return datetime.fromtimestamp(value, UTC).strftime("%Y-%m-%d %H:%M UTC")
223
+ return value
224
+
225
+
212
226
  def print_smart(
213
227
  payload: Any,
214
228
  *,
@@ -223,6 +237,7 @@ def print_smart(
223
237
  if payload is None:
224
238
  _console.print("[dim](empty response)[/]")
225
239
  return
240
+ payload = readable_times(payload)
226
241
 
227
242
  if isinstance(payload, list) and not payload:
228
243
  title_lower = (title or "").lower()
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sovaria-cli"
7
- version = "0.8.3"
7
+ version = "0.8.4"
8
8
  description = "Sovaria BaaS command-line interface (sov)"
9
9
  readme = "apps/cli/README.md"
10
10
  requires-python = ">=3.11"
File without changes