sovaria-cli 0.6.0__tar.gz → 0.7.0__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 (103) hide show
  1. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/PKG-INFO +1 -1
  2. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/__init__.py +1 -1
  3. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/db.py +1 -0
  4. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/import_cmd.py +8 -2
  5. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/keys.py +7 -6
  6. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/link_cmd.py +26 -2
  7. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/oidc_providers.py +2 -2
  8. sovaria_cli-0.7.0/apps/cli/src/commands/orgs_projects.py +167 -0
  9. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/vector.py +3 -2
  10. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/main.py +4 -2
  11. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/pyproject.toml +1 -1
  12. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/.gitignore +0 -0
  13. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/README.md +0 -0
  14. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/__init__.py +0 -0
  15. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/__init__.py +0 -0
  16. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/admin.py +0 -0
  17. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/ai_credits.py +0 -0
  18. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/ai_data.py +0 -0
  19. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/alerts.py +0 -0
  20. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/analytics.py +0 -0
  21. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/api.py +0 -0
  22. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/api_versions.py +0 -0
  23. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/audit.py +0 -0
  24. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/auth_enterprise.py +0 -0
  25. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/auth_product.py +0 -0
  26. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/auth_settings.py +0 -0
  27. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/billing.py +0 -0
  28. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/branches.py +0 -0
  29. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/builds.py +0 -0
  30. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/chrono.py +0 -0
  31. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/collections.py +0 -0
  32. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/compliance.py +0 -0
  33. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/connect.py +0 -0
  34. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/context.py +0 -0
  35. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/crashes.py +0 -0
  36. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/dashboard.py +0 -0
  37. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/data.py +0 -0
  38. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/deploy.py +0 -0
  39. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/distribute.py +0 -0
  40. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/env.py +0 -0
  41. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/experiments.py +0 -0
  42. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/fn_platform.py +0 -0
  43. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/frontends.py +0 -0
  44. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/gen.py +0 -0
  45. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/governance.py +0 -0
  46. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/hosting.py +0 -0
  47. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/iam.py +0 -0
  48. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/init_cmd.py +0 -0
  49. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/inspect.py +0 -0
  50. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/knowledge.py +0 -0
  51. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/local.py +0 -0
  52. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/login.py +0 -0
  53. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/logs.py +0 -0
  54. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/mcp.py +0 -0
  55. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/messaging.py +0 -0
  56. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/metrics.py +0 -0
  57. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/org.py +0 -0
  58. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/org_key.py +0 -0
  59. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/passkeys.py +0 -0
  60. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/perf.py +0 -0
  61. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/policy.py +0 -0
  62. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/project.py +0 -0
  63. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/push.py +0 -0
  64. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/queues.py +0 -0
  65. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/realtime.py +0 -0
  66. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/remote_config.py +0 -0
  67. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/rules.py +0 -0
  68. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/schema.py +0 -0
  69. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/scope.py +0 -0
  70. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/secrets.py +0 -0
  71. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/shell.py +0 -0
  72. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/sim.py +0 -0
  73. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/simulate.py +0 -0
  74. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/storage.py +0 -0
  75. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/templates.py +0 -0
  76. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/commands/test_cmd.py +0 -0
  77. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/local/docker-compose.yml +0 -0
  78. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/local/initdb/001_axis_tables.sql +0 -0
  79. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/local/initdb/002_axis_base_schema.sql +0 -0
  80. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/local/initdb/003_guard_policies.sql +0 -0
  81. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/local/initdb/010_local_stack.sql +0 -0
  82. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/__init__.py +0 -0
  83. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/api_client.py +0 -0
  84. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/api_helpers.py +0 -0
  85. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/auth_store.py +0 -0
  86. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/config.py +0 -0
  87. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/display.py +0 -0
  88. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/forge_local.py +0 -0
  89. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/importers.py +0 -0
  90. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/local_function_server.py +0 -0
  91. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/local_stack.py +0 -0
  92. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/log_tail.py +0 -0
  93. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/loopback_host.py +0 -0
  94. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/macho_symbols.py +0 -0
  95. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/migrations.py +0 -0
  96. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/mobile_types.py +0 -0
  97. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/output_mode.py +0 -0
  98. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/premium_display.py +0 -0
  99. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/project_context.py +0 -0
  100. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/schema_introspect.py +0 -0
  101. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/scope_picker.py +0 -0
  102. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/apps/cli/src/utils/sim_offline.py +0 -0
  103. {sovaria_cli-0.6.0 → sovaria_cli-0.7.0}/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.6.0
3
+ Version: 0.7.0
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.6.0"
3
+ __version__ = "0.7.0"
@@ -972,6 +972,7 @@ def _dry_run_in_order(items: list[tuple[Path, dict]], scope: dict[str, str | Non
972
972
  print_success(f"All {len(items)} pending migration(s) apply cleanly in order. Nothing was changed.")
973
973
 
974
974
 
975
+ @app.command("push", help="Apply pending migrations (same as `sov db migrations push`, like `supabase db push`).")
975
976
  @migrations_app.command("push")
976
977
  def migrations_push(
977
978
  directory: str | None = typer.Option(
@@ -543,7 +543,7 @@ def import_firebase(
543
543
  if mapping and not dry_run:
544
544
  Path(_UID_MAP).write_text(json.dumps(mapping, indent=2))
545
545
  print_success(f"Users: {len(users)} read" + (f"; uid map in {_UID_MAP}" if mapping else ""))
546
- elif "auth" in parts and only:
546
+ elif "auth" in parts:
547
547
  print_warning("Auth skipped: pass --auth-export (firebase auth:export users.json --format=json)")
548
548
 
549
549
  if "firestore" in parts and sa:
@@ -562,8 +562,14 @@ def import_firebase(
562
562
  report["rules"] = suggestions
563
563
  Path("sovaria-rules-report.md").write_text(_rules_markdown(suggestions))
564
564
  print_success(f"Rules: {len(suggestions)} rule(s) mapped in sovaria-rules-report.md (review, then add in Policies)")
565
- if ("firestore" in parts or "storage" in parts) and not sa and only:
565
+ if ("firestore" in parts or "storage" in parts) and not sa:
566
566
  print_warning("Firestore/Storage skipped: pass --service-account")
567
+ if "rules" in parts and not rules and only:
568
+ print_warning("Rules skipped: pass --rules firestore.rules")
569
+ if not ({"auth", "firestore", "storage", "rules"} & set(report)):
570
+ typer.echo("Nothing was imported: pass --auth-export, --service-account and/or --rules "
571
+ "(see `sov import firebase --help`).", err=True)
572
+ raise typer.Exit(1)
567
573
  except CLIRequestError as exc:
568
574
  report["error"] = str(exc)
569
575
  _write_report(report)
@@ -6,7 +6,7 @@ from urllib.parse import quote
6
6
 
7
7
  import typer
8
8
 
9
- from apps.cli.src.utils.api_client import CLIRequestError, call_api, parse_json_arg
9
+ from apps.cli.src.utils.api_client import CLIRequestError, call_api
10
10
  from apps.cli.src.utils.display import handle_cli_error, print_json, print_success, print_warning
11
11
  from apps.cli.src.utils.premium_display import run_with_spinner
12
12
 
@@ -49,7 +49,8 @@ def create_key(
49
49
  name: str = typer.Option(..., "--name", help="Human-readable key name"),
50
50
  role: str = typer.Option("viewer", "--role", help="admin, editor or viewer (read-only)"),
51
51
  expires: str = typer.Option("never", "--expires", help="never, 30d, 90d or 1y"),
52
- metadata: str | None = typer.Option(None, "--metadata", help="JSON object"),
52
+ metadata: str | None = typer.Option(None, "--metadata", hidden=True,
53
+ help="Not stored by the server (kept so old scripts still run)"),
53
54
  profile: str | None = typer.Option(None, "--profile"),
54
55
  org_id: str | None = typer.Option(None, "--org"),
55
56
  project_id: str | None = typer.Option(None, "--project"),
@@ -72,9 +73,9 @@ def create_key(
72
73
  expires = expires.strip().lower()
73
74
  if expires not in _EXPIRIES:
74
75
  raise typer.BadParameter("choose never, 30d, 90d or 1y", param_hint="--expires")
75
- parsed = parse_json_arg(metadata, label="--metadata") if metadata else {}
76
- if parsed is not None and not isinstance(parsed, dict):
77
- raise typer.BadParameter("--metadata must be a JSON object")
76
+ if metadata:
77
+ # API keys have no metadata field: the server dropped it silently (p32). Say so instead.
78
+ print_warning("--metadata is ignored: API keys don't store metadata (use --name to label the key)")
78
79
  try:
79
80
  result = run_with_spinner(
80
81
  f"Creating API key '{name}'…",
@@ -86,7 +87,7 @@ def create_key(
86
87
  project_id=project_id,
87
88
  env_id=env_id,
88
89
  require_scope=True,
89
- body={"name": name, "kind": "custom", "role": role, "expires_in": expires, "metadata": parsed or {}},
90
+ body={"name": name, "kind": "custom", "role": role, "expires_in": expires},
90
91
  ),
91
92
  success="API key created",
92
93
  )
@@ -7,7 +7,8 @@ from pathlib import Path
7
7
  import typer
8
8
 
9
9
  from apps.cli.src.utils.api_client import CLIRequestError
10
- from apps.cli.src.utils.display import handle_cli_error, handle_command_error, print_success
10
+ from apps.cli.src.utils.auth_store import load_tokens
11
+ from apps.cli.src.utils.display import handle_cli_error, handle_command_error, print_success, print_warning
11
12
  from apps.cli.src.utils.premium_display import (
12
13
  print_brand_panel,
13
14
  print_next_steps,
@@ -15,7 +16,7 @@ from apps.cli.src.utils.premium_display import (
15
16
  run_with_spinner,
16
17
  )
17
18
  from apps.cli.src.utils.project_context import find_project_config, link_project_to_profile
18
- from apps.cli.src.utils.scope_picker import interactive_pick_scope
19
+ from apps.cli.src.utils.scope_picker import fetch_projects, interactive_pick_scope
19
20
 
20
21
  app = typer.Typer(help="Link local project to Sovaria cloud", invoke_without_command=True)
21
22
 
@@ -28,6 +29,27 @@ _LINK_NEXT_STEPS = [
28
29
  ]
29
30
 
30
31
 
32
+ def _verify_project(org_id: str, project_id: str, profile: str | None) -> None:
33
+ """Refuse to link a project the signed-in user can't reach (a typo used to be written as-is and every later
34
+ command failed). Logged out (CI with a key, offline), the IDs are linked unchecked, with a warning."""
35
+ if not load_tokens(profile=profile):
36
+ print_warning("Not logged in: the org/project IDs were not checked (run `sov login` to check them).")
37
+ return
38
+ try:
39
+ projects = fetch_projects(org_id, profile)
40
+ except CLIRequestError as exc:
41
+ if exc.status_code in (401, 403, 404, 422):
42
+ typer.echo(f"You can't access organisation {org_id} ({exc}). Check --org, or run `sov orgs`.", err=True)
43
+ raise typer.Exit(1) from None
44
+ print_warning(f"Couldn't check the project ({exc}); linking anyway.")
45
+ return
46
+ ids = {str(p.get("node_id") or p.get("id") or "").lower() for p in projects}
47
+ if project_id.strip().lower() not in ids:
48
+ typer.echo(f"Project {project_id} isn't in organisation {org_id}. Run `sov projects {org_id}` to list them.",
49
+ err=True)
50
+ raise typer.Exit(1)
51
+
52
+
31
53
  @app.callback()
32
54
  def link_project(
33
55
  org_id: str | None = typer.Option(None, "--org", help="Organisation UUID"),
@@ -87,6 +109,8 @@ def link_project(
87
109
  typer.echo("No sovaria.config.json found. Run: sov init", err=True)
88
110
  raise typer.Exit(1)
89
111
 
112
+ _verify_project(resolved_org, resolved_project, profile)
113
+
90
114
  try:
91
115
  path = link_project_to_profile(
92
116
  org_id=resolved_org,
@@ -175,13 +175,13 @@ def login_url(
175
175
  "Building OIDC login URL…",
176
176
  lambda: call_api(
177
177
  method="GET",
178
- path=f"/api/v1/auth/oidc/{quote(provider_id, safe='')}/login",
178
+ path=f"/api/v1/auth/sso/{quote(provider_id, safe='')}/authorize",
179
179
  profile=profile,
180
180
  org_id=resolved_org,
181
181
  project_id=resolved_project,
182
182
  env_id=env_id,
183
183
  require_scope=True,
184
- params={"project_id": resolved_project},
184
+ params={"project_id": resolved_project, "mode": "json"},
185
185
  ),
186
186
  success="Login URL ready",
187
187
  )
@@ -0,0 +1,167 @@
1
+ """`sov orgs` and `sov projects`: list (the old forms keep working), `sov orgs get`, `sov projects create`.
2
+
3
+ `sov projects create` does what the console's New project dialog does: the project node, its owner edge, the
4
+ primary environment, and the database. It sends a region only when you pass `--region`; otherwise the server uses
5
+ the organization's default region (Organization settings › General), and Sovaria's default when the org has none.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import sys
11
+ from typing import Any
12
+ from urllib.parse import quote
13
+
14
+ import typer
15
+ from typer.core import TyperGroup
16
+
17
+ from apps.cli.src.commands import context as context_mod
18
+ from apps.cli.src.utils.api_client import CLIRequestError, call_api
19
+ from apps.cli.src.utils.api_helpers import require_login
20
+ from apps.cli.src.utils.config import get_scope
21
+ from apps.cli.src.utils.display import handle_cli_error, print_json, print_success, print_warning
22
+ from apps.cli.src.utils.output_mode import wants_json
23
+
24
+
25
+ class _ListByDefault(TyperGroup):
26
+ """`sov projects` / `sov projects <org_id>` run `list`; named subcommands run as usual."""
27
+
28
+ def resolve_command(self, ctx: Any, args: list[str]) -> Any:
29
+ if args and args[0] not in self.commands and not args[0].startswith("-"):
30
+ return "list", self.commands["list"], args
31
+ return super().resolve_command(ctx, args)
32
+
33
+
34
+ orgs_app = typer.Typer(cls=_ListByDefault, invoke_without_command=True,
35
+ help="Your organisations: list (default) and get (with the default region)")
36
+ projects_app = typer.Typer(cls=_ListByDefault, invoke_without_command=True,
37
+ help="Projects: list (default) and create (in the org's default region unless --region)")
38
+
39
+
40
+ @orgs_app.callback()
41
+ def _orgs_default(ctx: typer.Context, profile: str | None = typer.Option(None, "--profile")) -> None:
42
+ if ctx.invoked_subcommand is None:
43
+ context_mod.list_orgs(profile=profile)
44
+
45
+
46
+ @projects_app.callback()
47
+ def _projects_default(ctx: typer.Context, profile: str | None = typer.Option(None, "--profile")) -> None:
48
+ if ctx.invoked_subcommand is None:
49
+ context_mod.list_projects(org_id=None, profile=profile)
50
+
51
+
52
+ orgs_app.command("list")(context_mod.list_orgs)
53
+ projects_app.command("list")(context_mod.list_projects)
54
+
55
+
56
+ def _settings(node: dict[str, Any]) -> dict[str, Any]:
57
+ for key in ("metadata_json", "metadata"):
58
+ meta = node.get(key)
59
+ if isinstance(meta, dict) and isinstance(meta.get("settings"), dict):
60
+ return meta["settings"]
61
+ return {}
62
+
63
+
64
+ def org_default_region(org_id: str, profile: str | None) -> str | None:
65
+ """The organization's default region (Organization settings › General), or None when it has none."""
66
+ node = call_api(method="GET", path=f"/api/v1/org/{quote(org_id, safe='')}", profile=profile)
67
+ if not isinstance(node, dict):
68
+ return None
69
+ return str(_settings(node).get("default_region") or "").strip() or None
70
+
71
+
72
+ @orgs_app.command("get")
73
+ def get_org(
74
+ org_id: str = typer.Argument(..., help="Organisation ID"),
75
+ profile: str | None = typer.Option(None, "--profile"),
76
+ ) -> None:
77
+ """An organisation, with its default region (where new projects go unless you pick one)."""
78
+ require_login(profile)
79
+ try:
80
+ node = call_api(method="GET", path=f"/api/v1/org/{quote(org_id, safe='')}", profile=profile)
81
+ except CLIRequestError as exc:
82
+ handle_cli_error(exc)
83
+ return
84
+ region = str(_settings(node if isinstance(node, dict) else {}).get("default_region") or "").strip()
85
+ if wants_json():
86
+ print_json({**(node if isinstance(node, dict) else {}), "default_region": region or None})
87
+ return
88
+ print_json(node, title=f"Organisation {org_id[:8]}…")
89
+ typer.echo(f"Default region: {region or 'not set (new projects use Sovaria’s default region)'}")
90
+
91
+
92
+ def _node_id(data: Any, step: str) -> str:
93
+ node_id = str((data or {}).get("node_id") or (data or {}).get("id") or "") if isinstance(data, dict) else ""
94
+ if not node_id:
95
+ raise CLIRequestError(f"{step}: the server returned no id")
96
+ return node_id
97
+
98
+
99
+ @projects_app.command("create")
100
+ def create_project(
101
+ name: str = typer.Argument(..., help="Project name"),
102
+ org_id: str | None = typer.Option(None, "--org", help="Organisation ID (default: your current scope)"),
103
+ region: str | None = typer.Option(
104
+ None, "--region", help="Region for the database; omit to use the organisation's default region"),
105
+ env_name: str = typer.Option("production", "--env-name", help="Name of the primary environment"),
106
+ db_password_stdin: bool = typer.Option(
107
+ False, "--db-password-stdin", help="Read the database password (12–72 characters) from stdin; random otherwise"),
108
+ profile: str | None = typer.Option(None, "--profile"),
109
+ ) -> None:
110
+ """Create a cloud project: the project, its primary environment and its database (like the console)."""
111
+ require_login(profile)
112
+ org = (org_id or get_scope(profile=profile).get("org_id") or "").strip()
113
+ if not org:
114
+ typer.echo("Pass --org, or pick a scope first (sov scope pick).", err=True)
115
+ raise typer.Exit(1)
116
+ password = sys.stdin.readline().rstrip("\n") if db_password_stdin else ""
117
+ if db_password_stdin and not 12 <= len(password) <= 72:
118
+ raise typer.BadParameter("the database password must be 12–72 characters", param_hint="--db-password-stdin")
119
+
120
+ try:
121
+ default_region = None if region else org_default_region(org, profile)
122
+ except CLIRequestError as exc:
123
+ handle_cli_error(exc)
124
+ return
125
+
126
+ created: dict[str, str] = {}
127
+ try:
128
+ project = call_api(method="POST", path="/api/v1/org", profile=profile, body={
129
+ "type": "PROJECT", "name": name.strip(), "metadata": {"org_id": org},
130
+ **({"database_password": password} if password else {}),
131
+ })
132
+ created["project_id"] = _node_id(project, "Creating the project")
133
+ call_api(method="POST", path="/api/v1/edge", profile=profile, body={
134
+ "source_id": org, "target_id": created["project_id"], "relation_type": "OWNS", "role": "OWNER"})
135
+ env = call_api(method="POST", path="/api/v1/org", profile=profile, body={
136
+ "type": "ENV", "name": env_name.strip() or "production", "metadata": {"db_type": "hybrid"}})
137
+ created["env_id"] = _node_id(env, "Creating the environment")
138
+ call_api(method="POST", path="/api/v1/edge", profile=profile, body={
139
+ "source_id": created["project_id"], "target_id": created["env_id"], "relation_type": "HAS_ENV",
140
+ "role": "PRIMARY"})
141
+ # No region unless asked: the server places the database in the org's default region.
142
+ database = call_api(
143
+ method="POST", path=f"/api/v1/governance/tenants/{quote(org, safe='')}/databases/provision",
144
+ profile=profile, timeout=120,
145
+ body={"project_id": created["project_id"], "tier": "dedicated", **({"region": region} if region else {})})
146
+ except CLIRequestError as exc:
147
+ if created.get("project_id"):
148
+ print_warning(f"Stopped part way: project {created['project_id']} exists"
149
+ + (f" with environment {created['env_id']}" if created.get("env_id") else "")
150
+ + ". Finish it in the console, or remove it: sov api call DELETE /api/v1/org/"
151
+ + created["project_id"])
152
+ handle_cli_error(exc)
153
+ return
154
+
155
+ placed = (database or {}).get("region") if isinstance(database, dict) else None
156
+ where = region or placed or default_region
157
+ source = ("--region" if region else "the organisation's default region" if default_region
158
+ else "Sovaria's default region (the organisation has none set)")
159
+ out = {"org_id": org, **created, "name": name.strip(), "env_name": env_name, "region": where,
160
+ "region_source": source, "database": database}
161
+ if wants_json():
162
+ print_json(out)
163
+ return
164
+ print_success(f"Project created: {created['project_id']}")
165
+ typer.echo(f"Environment: {env_name} ({created['env_id']})")
166
+ typer.echo(f"Region: {where or 'chosen by the server'} — from {source}")
167
+ typer.echo(f"Next: sov link --org {org} --project {created['project_id']}")
@@ -121,7 +121,8 @@ def app_search(
121
121
  @app.command("reembed")
122
122
  def reembed(
123
123
  collection: str = typer.Argument(..., help="Collection name"),
124
- force: bool = typer.Option(False, "--force", help="Also for collections above the automatic limit (uses quota)"),
124
+ force: bool = typer.Option(False, "--force", hidden=True,
125
+ help="Not needed: an explicit re-embed always covers the whole collection (uses quota)"),
125
126
  org_id: str | None = typer.Option(None, "--org"),
126
127
  project_id: str | None = typer.Option(None, "--project"),
127
128
  env_id: str | None = typer.Option(None, "--env"),
@@ -131,7 +132,7 @@ def reembed(
131
132
  result = run_with_spinner(
132
133
  f"Re-embedding '{collection}'…",
133
134
  lambda: _call("POST", f"/api/v1/collections/{quote(collection, safe='')}/reembed", org_id=org_id,
134
- project_id=project_id, env_id=env_id, profile=profile, params={"force": "true"} if force else None),
135
+ project_id=project_id, env_id=env_id, profile=profile),
135
136
  success="Re-embedded",
136
137
  )
137
138
  print_json(result)
@@ -221,8 +221,10 @@ app.add_typer(shell_mod.app, name="shell", help="Interactive Sovaria REPL")
221
221
  app.command("login")(login_mod.login)
222
222
  app.command("logout")(login_mod.logout)
223
223
  app.command("whoami")(context_mod.whoami)
224
- app.command("orgs")(context_mod.list_orgs)
225
- app.command("projects")(context_mod.list_projects)
224
+ from apps.cli.src.commands import orgs_projects as orgs_projects_mod # noqa: E402
225
+
226
+ app.add_typer(orgs_projects_mod.orgs_app, name="orgs")
227
+ app.add_typer(orgs_projects_mod.projects_app, name="projects")
226
228
  app.command("context")(context_mod.context)
227
229
  app.command("dashboard")(dashboard_mod.show_dashboard)
228
230
  app.command("connect")(connect_mod.show_connect)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sovaria-cli"
7
- version = "0.6.0"
7
+ version = "0.7.0"
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