buildai-cli 0.3.68__tar.gz → 0.3.69__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 (36) hide show
  1. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/CLAUDE.md +6 -1
  2. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/PKG-INFO +1 -1
  3. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/cli/commands/db/__init__.py +2 -0
  4. buildai_cli-0.3.69/cli/commands/db/tunnel.py +115 -0
  5. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/cli/commands/dev.py +22 -1
  6. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/cli/db_broker.py +29 -5
  7. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/cli/main.py +6 -0
  8. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/cli/ops_init.py +5 -0
  9. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/pyproject.toml +1 -1
  10. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/.gitignore +0 -0
  11. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/AGENTS.md +0 -0
  12. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/buildai_bootstrap.py +0 -0
  13. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/cli/__init__.py +0 -0
  14. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/cli/_has_core.py +0 -0
  15. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/cli/auth_local.py +0 -0
  16. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/cli/commands/__init__.py +0 -0
  17. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/cli/commands/api_proxy.py +0 -0
  18. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/cli/commands/auth.py +0 -0
  19. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/cli/commands/db/broker.py +0 -0
  20. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/cli/commands/db/common.py +0 -0
  21. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/cli/commands/db/migrate.py +0 -0
  22. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/cli/commands/db/query.py +0 -0
  23. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/cli/commands/db/schema.py +0 -0
  24. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/cli/commands/db/status.py +0 -0
  25. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/cli/commands/doctor.py +0 -0
  26. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/cli/commands/gigcamera.py +0 -0
  27. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/cli/commands/processing.py +0 -0
  28. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/cli/config.py +0 -0
  29. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/cli/console.py +0 -0
  30. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/cli/context.py +0 -0
  31. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/cli/guard.py +0 -0
  32. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/cli/internal_api.py +0 -0
  33. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/cli/nl_query/__init__.py +0 -0
  34. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/cli/nl_query/dataset_tools.py +0 -0
  35. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/cli/output.py +0 -0
  36. {buildai_cli-0.3.68 → buildai_cli-0.3.69}/cli/pagination.py +0 -0
@@ -14,6 +14,8 @@ Typer-based CLI with two modes: standalone (PyPI, API-backed) and workspace (rep
14
14
  ```bash
15
15
  buildai auth whoami # API auth inspection
16
16
  buildai db query "SELECT count(*) FROM core.clips" # DB-direct
17
+ buildai db --env staging tunnel # IAP/SOCKS route for private staging DB
18
+ buildai db --env staging --all-proxy socks5://127.0.0.1:1080 query "SELECT 1" # staging query
17
19
  buildai db broker status # Active local DB broker state
18
20
  buildai db schema tables # Schema introspection
19
21
  buildai db schema describe core.clips # Table details
@@ -31,7 +33,10 @@ buildai db status # Migration status
31
33
  - Writes require `--write` flag.
32
34
  - Production migrations prompt for confirmation.
33
35
  - Worktree app/runtime targeting comes from explicit env vars and the repo Makefile, not a saved CLI context layer.
34
- - `buildai db --env` selects the explicit DB lane: `production`, `dev`.
36
+ - `buildai db --env` selects the explicit DB lane: `production`, `staging`, `dev`.
37
+ - Staging is private-IP only. Start an IAP/VPN/SOCKS route first, then pass it
38
+ with `--all-proxy` so the local AlloyDB broker can reach the private address.
39
+ The standard route is `buildai db --env staging tunnel`.
35
40
 
36
41
  ## Reference
37
42
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: buildai-cli
3
- Version: 0.3.68
3
+ Version: 0.3.69
4
4
  Summary: Build AI CLI (Typer)
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: httpx>=0.27.0
@@ -9,6 +9,7 @@ from . import migrate as migrate_mod
9
9
  from . import query as query_mod
10
10
  from . import schema as schema_mod
11
11
  from . import status as status_mod
12
+ from . import tunnel as tunnel_mod
12
13
 
13
14
  app = typer.Typer(
14
15
  name="db",
@@ -19,5 +20,6 @@ app = typer.Typer(
19
20
  app.command("query")(query_mod.query)
20
21
  app.command("status")(status_mod.status)
21
22
  app.command("migrate")(migrate_mod.migrate)
23
+ app.command("tunnel")(tunnel_mod.tunnel)
22
24
  app.add_typer(broker_mod.app, name="broker")
23
25
  app.add_typer(schema_mod.app, name="schema")
@@ -0,0 +1,115 @@
1
+ """IAP tunnel helpers for private-only DB lanes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ import subprocess
7
+
8
+ import typer
9
+ from infra.settings import Settings
10
+ from typing_extensions import Annotated
11
+
12
+ from cli.console import error, info, warning
13
+
14
+
15
+ def _build_iap_socks_command(
16
+ *,
17
+ host: str,
18
+ project: str,
19
+ zone: str,
20
+ local_host: str,
21
+ local_port: int,
22
+ ) -> list[str]:
23
+ """Build the gcloud command that opens one local SOCKS proxy over IAP SSH."""
24
+
25
+ return [
26
+ "gcloud",
27
+ "compute",
28
+ "ssh",
29
+ host,
30
+ f"--project={project}",
31
+ f"--zone={zone}",
32
+ "--tunnel-through-iap",
33
+ "--",
34
+ "-N",
35
+ "-D",
36
+ f"{local_host}:{local_port}",
37
+ ]
38
+
39
+
40
+ def tunnel(
41
+ ctx: typer.Context,
42
+ local_port: Annotated[
43
+ int,
44
+ typer.Option(
45
+ "--local-port",
46
+ help="Local SOCKS port for the DB auth proxy to use.",
47
+ ),
48
+ ] = 1080,
49
+ local_host: Annotated[
50
+ str,
51
+ typer.Option(
52
+ "--local-host",
53
+ help="Local bind address for the SOCKS listener.",
54
+ ),
55
+ ] = "127.0.0.1",
56
+ host: Annotated[
57
+ str,
58
+ typer.Option(
59
+ "--host",
60
+ help="IAP-reachable VM that can route to the private AlloyDB address.",
61
+ ),
62
+ ] = "atlas-control-plane",
63
+ project: Annotated[
64
+ str | None,
65
+ typer.Option(
66
+ "--project",
67
+ help="GCP project for the tunnel host. Defaults to the active DB settings project.",
68
+ ),
69
+ ] = None,
70
+ zone: Annotated[
71
+ str,
72
+ typer.Option(
73
+ "--zone",
74
+ help="Compute zone for the tunnel host.",
75
+ ),
76
+ ] = "asia-south1-a",
77
+ ) -> None:
78
+ """Open an IAP-backed SOCKS tunnel for private-only DB access.
79
+
80
+ Keep this command running in one terminal, then pass
81
+ ``--all-proxy socks5://127.0.0.1:1080`` to ``buildai db`` commands in
82
+ another terminal.
83
+ """
84
+
85
+ settings: Settings = ctx.obj["settings"]
86
+ resolved_project = project or settings.gcp_project_id
87
+
88
+ if shutil.which("gcloud") is None:
89
+ error("gcloud is not installed or not on PATH.")
90
+ raise typer.Exit(1)
91
+
92
+ if not settings.use_private_ip:
93
+ warning(
94
+ "The selected DB lane is not configured for private-IP access. "
95
+ "This tunnel is normally needed only for staging/private lanes."
96
+ )
97
+
98
+ proxy_url = f"socks5://{local_host}:{local_port}"
99
+ info(f"Opening IAP SOCKS tunnel on {proxy_url}. Stop with Ctrl-C.")
100
+ info(
101
+ "In another terminal, run: "
102
+ f'uv run buildai db --env {settings.app_env.value} --all-proxy {proxy_url} query "SELECT 1"'
103
+ )
104
+
105
+ command = _build_iap_socks_command(
106
+ host=host,
107
+ project=resolved_project,
108
+ zone=zone,
109
+ local_host=local_host,
110
+ local_port=local_port,
111
+ )
112
+ try:
113
+ raise typer.Exit(subprocess.run(command, check=False).returncode)
114
+ except KeyboardInterrupt:
115
+ raise typer.Exit(130) from None
@@ -24,6 +24,11 @@ _DEFAULT_DEV_INSTANCE_URI = (
24
24
  "instances/buildai-india-dev-primary"
25
25
  )
26
26
  _DEFAULT_DEV_DATABASE = "buildai_dev"
27
+ _DEFAULT_STAGING_INSTANCE_URI = (
28
+ "projects/data-470400/locations/asia-south1/clusters/buildai-india-staging/"
29
+ "instances/buildai-india-staging-primary"
30
+ )
31
+ _DEFAULT_STAGING_DATABASE = "buildai_staging"
27
32
 
28
33
 
29
34
  def _build_proxy_command(
@@ -54,7 +59,9 @@ def _db_info_payload(
54
59
  try:
55
60
  resolved_env = Environment(env)
56
61
  except ValueError as exc:
57
- raise typer.BadParameter("Use one of: development, preview, production, test.") from exc
62
+ raise typer.BadParameter(
63
+ "Use one of: development, staging, preview, production, test."
64
+ ) from exc
58
65
 
59
66
  profile = resolve_sanctioned_profile(profile_name, service=service, environment=env)
60
67
  settings = Settings(app_env=resolved_env, execution_context=ExecutionContext.HUMAN)
@@ -69,6 +76,8 @@ def _db_info_payload(
69
76
  except RuntimeError:
70
77
  if resolved_env == Environment.DEVELOPMENT:
71
78
  instance_uri = _DEFAULT_DEV_INSTANCE_URI
79
+ elif resolved_env == Environment.STAGING:
80
+ instance_uri = _DEFAULT_STAGING_INSTANCE_URI
72
81
  else:
73
82
  raise
74
83
 
@@ -77,6 +86,8 @@ def _db_info_payload(
77
86
  except RuntimeError:
78
87
  if resolved_env == Environment.DEVELOPMENT:
79
88
  database_name = _DEFAULT_DEV_DATABASE
89
+ elif resolved_env == Environment.STAGING:
90
+ database_name = _DEFAULT_STAGING_DATABASE
80
91
  else:
81
92
  raise
82
93
 
@@ -87,6 +98,12 @@ def _db_info_payload(
87
98
  "If you are outside its VPC, run the Auth Proxy through an approved "
88
99
  "intermediary path such as ALL_PROXY / PSC / VPN."
89
100
  )
101
+ elif resolved_env == Environment.STAGING:
102
+ private_only_note = (
103
+ "The staging AlloyDB cluster is private-only. Run "
104
+ "`buildai db --env staging tunnel`, then pass "
105
+ "`--all-proxy socks5://127.0.0.1:1080` to staging DB commands."
106
+ )
90
107
 
91
108
  proxy_command = _build_proxy_command(
92
109
  instance_uri=instance_uri,
@@ -102,6 +119,10 @@ def _db_info_payload(
102
119
  "ALLOYDB_AUTH_PROXY_HOST": proxy_host,
103
120
  f"ALLOYDB_AUTH_PROXY_PORT_{suffix}": str(proxy_port),
104
121
  }
122
+ if resolved_env == Environment.STAGING:
123
+ proxy_env["USE_PRIVATE_IP"] = "true"
124
+ if all_proxy:
125
+ proxy_env["ALLOYDB_AUTH_PROXY_ALL_PROXY"] = all_proxy
105
126
 
106
127
  return {
107
128
  "profile": profile.name,
@@ -56,6 +56,7 @@ class BrokerConfig:
56
56
  host: str
57
57
  port: int
58
58
  use_private_ip: bool
59
+ all_proxy: str | None = None
59
60
  password: str = ""
60
61
 
61
62
  @property
@@ -73,6 +74,7 @@ class BrokerConfig:
73
74
  "host": self.host,
74
75
  "port": self.port,
75
76
  "use_private_ip": self.use_private_ip,
77
+ "all_proxy": self.all_proxy,
76
78
  }
77
79
  raw = json.dumps(payload, sort_keys=True, separators=(",", ":"))
78
80
  return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:16]
@@ -502,6 +504,10 @@ def _is_matching_proxy_process(process: ListenerProcess, config: BrokerConfig) -
502
504
  """Return True when a listener command is the broker this profile needs."""
503
505
 
504
506
  command = process.command
507
+ if config.all_proxy:
508
+ # The proxy transport override is in the child environment, not argv.
509
+ # Only reuse env-proxied brokers through the machine-global state file.
510
+ return False
505
511
  if not command:
506
512
  return False
507
513
  executable_name = Path(command[0].strip('"')).name.lower()
@@ -569,6 +575,15 @@ def _proxy_popen_kwargs() -> dict[str, Any]:
569
575
  return {"creationflags": creationflags}
570
576
 
571
577
 
578
+ def _proxy_environment(config: BrokerConfig) -> dict[str, str]:
579
+ """Return the auth-proxy child environment for this broker config."""
580
+
581
+ env = os.environ.copy()
582
+ if config.all_proxy:
583
+ env["ALL_PROXY"] = config.all_proxy
584
+ return env
585
+
586
+
572
587
  def _recent_broker_log(config: BrokerConfig, *, max_bytes: int = 12000) -> str:
573
588
  """Return the recent proxy log text that explains connection resets."""
574
589
 
@@ -626,10 +641,16 @@ def _broker_failure_message(config: BrokerConfig, exc: BaseException) -> str:
626
641
  "Run `uv run buildai doctor auth` for the exact IAM/auth check."
627
642
  )
628
643
  elif "tls handshake timeout" in lower_detail or "context deadline exceeded" in lower_detail:
629
- lines.append(
630
- "Cause: the proxy could not reach Google or AlloyDB before its deadline. "
631
- "Check network/VPN connectivity, then retry."
632
- )
644
+ if config.use_private_ip and not config.all_proxy:
645
+ lines.append(
646
+ "Cause: this database lane is private-only. Start a VPN/IAP/SOCKS route "
647
+ "or pass `--all-proxy socks5://HOST:PORT`, then retry."
648
+ )
649
+ else:
650
+ lines.append(
651
+ "Cause: the proxy could not reach Google or AlloyDB before its deadline. "
652
+ "Check network/VPN connectivity, then retry."
653
+ )
633
654
  else:
634
655
  lines.append(
635
656
  "Cause: the local proxy accepted TCP but closed the Postgres startup handshake."
@@ -699,7 +720,7 @@ def ensure_broker(config: BrokerConfig) -> BrokerState:
699
720
  command,
700
721
  stdout=log_file,
701
722
  stderr=log_file,
702
- env=os.environ.copy(),
723
+ env=_proxy_environment(config),
703
724
  **_proxy_popen_kwargs(),
704
725
  )
705
726
 
@@ -790,6 +811,8 @@ def broker_status(config: BrokerConfig) -> dict[str, Any]:
790
811
  "instance_uri": config.instance_uri,
791
812
  "host": config.host,
792
813
  "port": config.port,
814
+ "use_private_ip": config.use_private_ip,
815
+ "all_proxy": config.all_proxy,
793
816
  "state_path": str(config.state_path),
794
817
  "log_path": state.log_path if state else str(config.log_path),
795
818
  "pid": pid,
@@ -831,6 +854,7 @@ def broker_config_for_identity(
831
854
  host=settings.alloydb_auth_proxy_host,
832
855
  port=_profile_port(settings, profile, db_user=db_user),
833
856
  use_private_ip=settings.use_private_ip,
857
+ all_proxy=(os.getenv("ALLOYDB_AUTH_PROXY_ALL_PROXY") or os.getenv("ALL_PROXY") or None),
834
858
  password=password,
835
859
  )
836
860
 
@@ -289,6 +289,11 @@ def db_callback(
289
289
  "-p",
290
290
  help="Auth workflow profile.",
291
291
  ),
292
+ all_proxy: str | None = typer.Option(
293
+ None,
294
+ "--all-proxy",
295
+ help="Proxy URL passed to alloydb-auth-proxy for private-only DB lanes.",
296
+ ),
292
297
  write: bool = typer.Option(False, "--write", help="Allow write operations."),
293
298
  verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose logging."),
294
299
  ) -> None:
@@ -300,6 +305,7 @@ def db_callback(
300
305
  ctx.obj["_env"] = env
301
306
  ctx.obj["_auth"] = auth
302
307
  ctx.obj["_user"] = user
308
+ ctx.obj["_all_proxy"] = all_proxy
303
309
  ctx.obj["_verbose"] = verbose
304
310
  ctx.obj["_ops_ready"] = False
305
311
 
@@ -60,6 +60,7 @@ def _apply_default_db_target(*, env_prefix: str) -> None:
60
60
  os.environ.setdefault("ALLOYDB_CLUSTER", "buildai-india-staging")
61
61
  os.environ.setdefault("ALLOYDB_INSTANCE", "buildai-india-staging-primary")
62
62
  os.environ.setdefault("DB_NAME", "buildai_staging")
63
+ os.environ.setdefault("USE_PRIVATE_IP", "true")
63
64
  else:
64
65
  os.environ.setdefault("ALLOYDB_CLUSTER", "buildai-india")
65
66
  os.environ.setdefault("ALLOYDB_INSTANCE", "buildai-india-primary")
@@ -104,8 +105,12 @@ def init_ops_context(ctx: typer.Context):
104
105
  env_flag = ctx.obj.get("_env")
105
106
  auth_flag = ctx.obj.get("_auth")
106
107
  user_flag = ctx.obj.get("_user")
108
+ all_proxy_flag = ctx.obj.get("_all_proxy")
107
109
  profile = ctx.obj.get("cli_profile") or os.getenv("BUILDAI_CLI_PROFILE") or "engineers-dev"
108
110
 
111
+ if all_proxy_flag:
112
+ os.environ["ALLOYDB_AUTH_PROXY_ALL_PROXY"] = str(all_proxy_flag)
113
+
109
114
  # --- resolve environment ------------------------------------------------
110
115
  if env_flag is not None:
111
116
  try:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "buildai-cli"
7
- version = "0.3.68"
7
+ version = "0.3.69"
8
8
  description = "Build AI CLI (Typer)"
9
9
  requires-python = ">=3.11"
10
10
  dependencies = [
File without changes
File without changes
File without changes
File without changes
File without changes