buildai-cli 0.3.82__tar.gz → 0.3.83__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 (40) hide show
  1. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/.gitignore +1 -0
  2. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/CLAUDE.md +3 -0
  3. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/PKG-INFO +1 -1
  4. buildai_cli-0.3.83/cli/commands/ingest.py +363 -0
  5. buildai_cli-0.3.83/cli/commands/ingest_docs.py +311 -0
  6. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/main.py +15 -0
  7. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/pyproject.toml +1 -1
  8. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/AGENTS.md +0 -0
  9. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/buildai_bootstrap.py +0 -0
  10. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/__init__.py +0 -0
  11. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/_has_core.py +0 -0
  12. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/auth_local.py +0 -0
  13. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/commands/__init__.py +0 -0
  14. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/commands/api_proxy.py +0 -0
  15. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/commands/auth.py +0 -0
  16. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/commands/db/__init__.py +0 -0
  17. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/commands/db/broker.py +0 -0
  18. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/commands/db/common.py +0 -0
  19. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/commands/db/migrate.py +0 -0
  20. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/commands/db/query.py +0 -0
  21. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/commands/db/schema.py +0 -0
  22. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/commands/db/status.py +0 -0
  23. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/commands/db/tunnel.py +0 -0
  24. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/commands/dev.py +0 -0
  25. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/commands/doctor.py +0 -0
  26. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/commands/egoexo.py +0 -0
  27. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/commands/gigcamera.py +0 -0
  28. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/commands/grid.py +0 -0
  29. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/commands/processing.py +0 -0
  30. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/config.py +0 -0
  31. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/console.py +0 -0
  32. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/context.py +0 -0
  33. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/db_broker.py +0 -0
  34. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/guard.py +0 -0
  35. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/internal_api.py +0 -0
  36. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/nl_query/__init__.py +0 -0
  37. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/nl_query/dataset_tools.py +0 -0
  38. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/ops_init.py +0 -0
  39. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/output.py +0 -0
  40. {buildai_cli-0.3.82 → buildai_cli-0.3.83}/cli/pagination.py +0 -0
@@ -59,6 +59,7 @@ Thumbs.db
59
59
 
60
60
  # Local tool state
61
61
  .superpowers/
62
+ .grid-builder-work/
62
63
 
63
64
  # EgoExo sync trial script spill (default output is under experiments/egoexo-pipeline/out/)
64
65
  /*.sync_trial.json
@@ -13,6 +13,9 @@ Typer-based CLI with two modes: standalone (PyPI, API-backed) and workspace (rep
13
13
 
14
14
  ```bash
15
15
  buildai auth whoami # API auth inspection
16
+ uv run buildai ingest deploy plan --scope active # Dry-run ingest fleet plan
17
+ uv run buildai ingest deploy run --scope station:2 # Staged ingest rollout
18
+ uv run buildai ingest fleet readiness --scope active # Host readiness gate
16
19
  buildai db query "SELECT count(*) FROM core.clips" # DB-direct
17
20
  buildai db --env staging tunnel # IAP/SOCKS route for private staging DB
18
21
  buildai db --env staging --all-proxy socks5://127.0.0.1:1080 query "SELECT 1" # staging query
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: buildai-cli
3
- Version: 0.3.82
3
+ Version: 0.3.83
4
4
  Summary: Build AI CLI (Typer)
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: httpx>=0.27.0
@@ -0,0 +1,363 @@
1
+ """Workspace commands for BuildAI ingest fleet deploy control plane."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Annotated
9
+
10
+ import typer
11
+
12
+ from cli.commands.ingest_docs import (
13
+ DEPLOY_GROUP_HELP,
14
+ FLEET_GROUP_HELP,
15
+ HOST_GROUP_HELP,
16
+ INGEST_ROOT_HELP,
17
+ TOPIC_NAMES,
18
+ render_guide,
19
+ )
20
+ from cli.console import error, info, success, warning
21
+
22
+ app = typer.Typer(
23
+ help=INGEST_ROOT_HELP,
24
+ no_args_is_help=True,
25
+ rich_markup_mode="rich",
26
+ )
27
+ deploy_app = typer.Typer(
28
+ help=DEPLOY_GROUP_HELP,
29
+ no_args_is_help=True,
30
+ rich_markup_mode="rich",
31
+ )
32
+ fleet_app = typer.Typer(
33
+ help=FLEET_GROUP_HELP,
34
+ no_args_is_help=True,
35
+ rich_markup_mode="rich",
36
+ )
37
+ host_app = typer.Typer(
38
+ help=HOST_GROUP_HELP,
39
+ no_args_is_help=True,
40
+ rich_markup_mode="rich",
41
+ )
42
+
43
+ app.add_typer(deploy_app, name="deploy")
44
+ app.add_typer(fleet_app, name="fleet")
45
+ app.add_typer(host_app, name="host")
46
+
47
+ SCOPE_HELP = (
48
+ "Deploy target set. Examples: active, all, station:2, mini:1, server:1, aggregator. "
49
+ "See: buildai ingest docs --topic scopes"
50
+ )
51
+ TRANSPORT_HELP = (
52
+ "Remote path: tailscale (default, requires Tailscale CLI + tailnet) or "
53
+ "lan (on-site 10G static IPs). See: buildai ingest docs --topic transport"
54
+ )
55
+
56
+
57
+ def _emit_deploy_run_result(record, *, json_output: bool, engine, run_id: str) -> None:
58
+ """Print human-readable deploy/resume outcome from the terminal run state."""
59
+
60
+ payload = engine.deploy_status(run_id)
61
+ if json_output:
62
+ sys.stdout.write(engine.format_json(payload) + "\n")
63
+ return
64
+ state = record.state.value
65
+ if state == "converged":
66
+ success(f"deploy converged: {record.run_id} ({record.release_id})")
67
+ elif state == "activated":
68
+ success(f"deploy activated: {record.run_id} ({record.release_id})")
69
+ elif state == "deployed_not_converged":
70
+ warning(f"deploy incomplete: {record.failure_message or state}")
71
+ elif state == "blocked_by_readiness":
72
+ warning(f"deploy blocked by readiness: {record.failure_message}")
73
+ elif state in {"aborted", "preflight_failed"}:
74
+ error(f"deploy failed ({state}): {record.failure_message}")
75
+ else:
76
+ error(f"deploy finished as {state}: {record.failure_message}")
77
+
78
+
79
+ def _ingest_engine():
80
+ """Import the ingest deploy engine from the workspace checkout."""
81
+
82
+ current = Path(__file__).resolve()
83
+ repo_root = None
84
+ for parent in current.parents:
85
+ if (parent / ".git").exists() and (parent / "apps" / "buildai-ingest" / "fleet").exists():
86
+ repo_root = parent
87
+ break
88
+ if repo_root is None:
89
+ error(
90
+ "Ingest deploy requires a workspace checkout with apps/buildai-ingest.\n"
91
+ "Run from the monorepo via: uv run buildai ingest ...\n"
92
+ "See: uv run buildai ingest docs --topic prerequisites"
93
+ )
94
+ raise typer.Exit(1)
95
+
96
+ ingest_dir = repo_root / "apps" / "buildai-ingest"
97
+ ingest_str = str(ingest_dir)
98
+ if ingest_str not in sys.path:
99
+ sys.path.insert(0, ingest_str)
100
+
101
+ import fleet.engine as engine
102
+
103
+ return engine
104
+
105
+
106
+ @app.command("docs")
107
+ def ingest_docs(
108
+ topic: Annotated[
109
+ str | None,
110
+ typer.Option(
111
+ "--topic",
112
+ "-t",
113
+ help=f"One section: {', '.join(TOPIC_NAMES)}",
114
+ ),
115
+ ] = None,
116
+ ) -> None:
117
+ """Print the ingest fleet operator guide (prerequisites, workflow, transport, credentials)."""
118
+
119
+ try:
120
+ sys.stdout.write(render_guide(topic=topic))
121
+ except ValueError as exc:
122
+ sys.stdout.write(f"{exc}\n")
123
+ raise typer.Exit(2) from exc
124
+
125
+
126
+ @deploy_app.command("plan")
127
+ def deploy_plan(
128
+ scope: str = typer.Option("active", "--scope", "-s", help=SCOPE_HELP),
129
+ transport: str = typer.Option("tailscale", "--transport", help=TRANSPORT_HELP),
130
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON plan."),
131
+ ) -> None:
132
+ """Dry-run deploy plan: resolved targets, rollout order, and git warnings (no host mutation)."""
133
+
134
+ engine = _ingest_engine()
135
+ plan = engine.plan_deploy(scope=scope, transport=transport)
136
+ if json_output:
137
+ payload = {
138
+ "scope": plan.scope,
139
+ "transport": plan.transport,
140
+ "commit_sha": plan.commit_sha,
141
+ "targets": [target.name for target in plan.targets],
142
+ "excluded": [
143
+ {"name": target.name, "reason": target.skip_reason} for target in plan.excluded
144
+ ],
145
+ "rollout_order": plan.rollout_order,
146
+ "warnings": plan.warnings,
147
+ }
148
+ sys.stdout.write(engine.format_json(payload) + "\n")
149
+ return
150
+ info(engine.format_plan(plan))
151
+
152
+
153
+ @deploy_app.command("run")
154
+ def deploy_run(
155
+ scope: str = typer.Option("active", "--scope", "-s", help=SCOPE_HELP),
156
+ reason: str = typer.Option(
157
+ "", "--reason", help="Operator reason recorded in the deploy ledger."
158
+ ),
159
+ transport: str = typer.Option("tailscale", "--transport", help=TRANSPORT_HELP),
160
+ allow_dirty: bool = typer.Option(
161
+ False,
162
+ "--allow-dirty",
163
+ help="Allow deploy with a dirty git working tree (emergency only).",
164
+ ),
165
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON result."),
166
+ ) -> None:
167
+ """Build an immutable release artifact and execute a canaried staged fleet rollout."""
168
+
169
+ engine = _ingest_engine()
170
+ record, exit_code = engine.run_deploy(
171
+ scope=scope,
172
+ reason=reason,
173
+ transport=transport,
174
+ allow_dirty=allow_dirty,
175
+ )
176
+ _emit_deploy_run_result(record, json_output=json_output, engine=engine, run_id=record.run_id)
177
+ raise typer.Exit(exit_code)
178
+
179
+
180
+ @deploy_app.command("status")
181
+ def deploy_status(
182
+ run_id: str | None = typer.Argument(
183
+ None,
184
+ help="Deploy run id (default: latest run in ~/.buildai/ingest-deploy-runs/).",
185
+ ),
186
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON result."),
187
+ ) -> None:
188
+ """Show deploy run state, release id, and failure classification from the ledger."""
189
+
190
+ engine = _ingest_engine()
191
+ payload = engine.deploy_status(run_id)
192
+ if json_output:
193
+ sys.stdout.write(engine.format_json(payload) + "\n")
194
+ return
195
+ if not payload.get("run_id"):
196
+ info("No deploy runs recorded yet.")
197
+ return
198
+ info(f"{payload['run_id']}: state={payload.get('state')} release={payload.get('release_id')}")
199
+
200
+
201
+ @deploy_app.command("resume")
202
+ def deploy_resume(
203
+ run_id: str = typer.Argument(..., help="Non-terminal run id to continue."),
204
+ transport: str = typer.Option("tailscale", "--transport", help=TRANSPORT_HELP),
205
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON result."),
206
+ ) -> None:
207
+ """Resume a partially completed deploy from its saved artifact and ledger state."""
208
+
209
+ engine = _ingest_engine()
210
+ record, exit_code = engine.resume_deploy(run_id, transport=transport)
211
+ _emit_deploy_run_result(record, json_output=json_output, engine=engine, run_id=record.run_id)
212
+ raise typer.Exit(exit_code)
213
+
214
+
215
+ @deploy_app.command("rollback")
216
+ def deploy_rollback(
217
+ scope: str = typer.Option(..., "--scope", "-s", help=SCOPE_HELP),
218
+ to_release: str | None = typer.Option(
219
+ None,
220
+ "--to",
221
+ help="Explicit prior release id (default: ledger previous_release_id).",
222
+ ),
223
+ transport: str = typer.Option("tailscale", "--transport", help=TRANSPORT_HELP),
224
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON result."),
225
+ ) -> None:
226
+ """Flip current symlink to a prior release; re-stage only when that release was pruned."""
227
+
228
+ engine = _ingest_engine()
229
+ rollback_plan, exit_code = engine.rollback_deploy(
230
+ scope=scope,
231
+ to_release=to_release,
232
+ transport=transport,
233
+ )
234
+ payload = {
235
+ "scope": rollback_plan.scope,
236
+ "from_release_id": rollback_plan.from_release_id,
237
+ "to_release_id": rollback_plan.to_release_id,
238
+ "targets": list(rollback_plan.targets),
239
+ "action": rollback_plan.action,
240
+ }
241
+ if json_output:
242
+ sys.stdout.write(engine.format_json(payload) + "\n")
243
+ elif exit_code == 0:
244
+ success(
245
+ f"rollback complete: {rollback_plan.from_release_id} -> {rollback_plan.to_release_id}"
246
+ )
247
+ else:
248
+ error(f"rollback failed: {rollback_plan.from_release_id} -> {rollback_plan.to_release_id}")
249
+ raise typer.Exit(exit_code)
250
+
251
+
252
+ @fleet_app.command("readiness")
253
+ def fleet_readiness(
254
+ scope: str = typer.Option("active", "--scope", "-s", help=SCOPE_HELP),
255
+ transport: str = typer.Option("tailscale", "--transport", help=TRANSPORT_HELP),
256
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON result."),
257
+ ) -> None:
258
+ """Report host drift and maintenance blocks before deploy run (deployable vs blocked)."""
259
+
260
+ engine = _ingest_engine()
261
+ payload = engine.readiness_report(scope=scope, transport=transport)
262
+ if json_output:
263
+ sys.stdout.write(engine.format_json(payload) + "\n")
264
+ return
265
+ if payload["state"] == "ready":
266
+ success(f"fleet readiness: {payload['state']}")
267
+ else:
268
+ warning(f"fleet readiness: {payload['state']}")
269
+ for action in payload.get("next_actions", []):
270
+ if action:
271
+ info(f"next: {action}")
272
+
273
+
274
+ @fleet_app.command("preflight")
275
+ def fleet_preflight(
276
+ scope: str = typer.Option(
277
+ "all",
278
+ "--scope",
279
+ "-s",
280
+ help="Preflight scope: all, minis, servers, aggregator, or mini/server filters.",
281
+ ),
282
+ targets: list[str] = typer.Argument(
283
+ None,
284
+ help="Optional filters after scope (e.g. minis 1,2,3).",
285
+ ),
286
+ transport: str = typer.Option("tailscale", "--transport", help=TRANSPORT_HELP),
287
+ ) -> None:
288
+ """Read-only gate: inventory consistency, reachability, and Tailscale CLI check."""
289
+
290
+ engine = _ingest_engine()
291
+ try:
292
+ engine.run_preflight_check(
293
+ scope=scope,
294
+ targets=list(targets or []),
295
+ transport=transport,
296
+ )
297
+ except Exception as exc:
298
+ error(str(exc))
299
+ raise typer.Exit(1) from exc
300
+ success(f"fleet preflight passed for scope={scope}")
301
+
302
+
303
+ @fleet_app.command("auth")
304
+ def fleet_auth(
305
+ scope: str = typer.Option("all", "--scope", "-s", help="Auth validation scope."),
306
+ targets: list[str] = typer.Argument(None, help="Optional target filters."),
307
+ transport: str = typer.Option("tailscale", "--transport", help=TRANSPORT_HELP),
308
+ ) -> None:
309
+ """Validate worker, uploader, and aggregator API keys for the selected scope."""
310
+
311
+ engine = _ingest_engine()
312
+ try:
313
+ engine.validate_runtime_auth(
314
+ scope=scope,
315
+ targets=list(targets or []),
316
+ transport=transport,
317
+ )
318
+ except Exception as exc:
319
+ error(str(exc))
320
+ raise typer.Exit(1) from exc
321
+ success(f"ingest runtime auth validated for scope={scope}")
322
+
323
+
324
+ @host_app.command("bootstrap")
325
+ def host_bootstrap(
326
+ targets: list[str] = typer.Argument(
327
+ ...,
328
+ help="Mini numbers or names: 5, 5,6,7 or ingest-mini-11.",
329
+ ),
330
+ transport: str = typer.Option("tailscale", "--transport", help=TRANSPORT_HELP),
331
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON result."),
332
+ ) -> None:
333
+ """Pin drifted minis to the ingest-mini-1 Homebrew, rsync, and uv baseline."""
334
+
335
+ engine = _ingest_engine()
336
+ try:
337
+ payload = engine.bootstrap_hosts(targets=targets, transport=transport)
338
+ except Exception as exc:
339
+ error(str(exc))
340
+ raise typer.Exit(1) from exc
341
+ if json_output:
342
+ sys.stdout.write(json.dumps(payload, indent=2, sort_keys=True) + "\n")
343
+ return
344
+ names = [item.get("name", "") for item in payload.get("targets", [])]
345
+ success(f"host bootstrap complete for {', '.join(names)}")
346
+
347
+
348
+ @host_app.command("repair")
349
+ def host_repair(
350
+ target: str = typer.Argument(..., help="Mini target: ingest-mini-5 or shorthand 5."),
351
+ transport: str = typer.Option("tailscale", "--transport", help=TRANSPORT_HELP),
352
+ json_output: bool = typer.Option(False, "--json", help="Emit JSON result."),
353
+ ) -> None:
354
+ """Repair one mini host surface when readiness reports drift (outside app deploy)."""
355
+
356
+ _ingest_engine()
357
+ from fleet.host_repair import repair_mini_host
358
+
359
+ summary = repair_mini_host(target, transport_mode=transport)
360
+ if json_output:
361
+ sys.stdout.write(json.dumps(summary, indent=2, sort_keys=True) + "\n")
362
+ return
363
+ success(f"host repair complete for {summary.get('name', target)}")
@@ -0,0 +1,311 @@
1
+ """Operator documentation rendered by ``buildai ingest docs``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ INGEST_ROOT_HELP = """
6
+ [b]Ingest fleet control plane[/b] — deploy and operate Mac minis, upload servers, and the aggregator.
7
+
8
+ [b]Quick start (remote laptop, Tailscale):[/b]
9
+ uv run buildai ingest docs
10
+ uv run buildai ingest fleet preflight --scope all
11
+ uv run buildai ingest fleet auth --scope all
12
+ uv run buildai ingest deploy plan --scope active
13
+ uv run buildai ingest deploy run --scope active --reason "ship fix"
14
+
15
+ [b]Read the full guide:[/b] [cyan]buildai ingest docs[/cyan] or [cyan]buildai ingest docs --topic workflow[/cyan]
16
+
17
+ [b]Requires:[/b] monorepo checkout ([cyan]apps/buildai-ingest[/cyan]), Tailscale or on-site LAN, local secrets file.
18
+ Standalone PyPI [cyan]buildai[/cyan] without a workspace cannot run ingest deploy.
19
+ """
20
+
21
+ DEPLOY_GROUP_HELP = """
22
+ Immutable artifact deploy: build release bundle, stage per-host [cyan]releases/<id>/[/cyan], flip [cyan]current[/cyan], audit convergence.
23
+
24
+ [b]Typical flow:[/b] plan → run → status. Use [cyan]resume[/cyan] after partial failure; [cyan]rollback[/cyan] to flip [cyan]current[/cyan] back.
25
+
26
+ [b]Scopes:[/b] active | all | station:N | mini:N | server:N | aggregator
27
+ [b]Transport:[/b] tailscale (default, remote) | lan (on-site 10G static IPs)
28
+ """
29
+
30
+ FLEET_GROUP_HELP = """
31
+ Read-only fleet gates and drift checks before mutating hosts.
32
+
33
+ [b]Order:[/b] preflight → auth → readiness → deploy run
34
+ """
35
+
36
+ HOST_GROUP_HELP = """
37
+ Host-level repair outside the app deploy path.
38
+
39
+ [b]bootstrap[/b] — pin Homebrew/rsync/uv to the ingest-mini-1 baseline (new or drifted minis).
40
+ [b]repair[/b] — reconcile one mini host surface when readiness reports drift.
41
+ """
42
+
43
+ TOPICS: dict[str, str] = {
44
+ "overview": """
45
+ INGEST FLEET — OPERATOR OVERVIEW
46
+ ================================
47
+
48
+ The ingest fleet runs outside Cloud Run:
49
+ • Mac minis run ingest-agent (SD card → rsync → erase)
50
+ • Upload servers run upload-daemon (GCS uploads)
51
+ • One aggregator coordinates fleet state and operator UI
52
+
53
+ All operator workflows go through the BuildAI CLI from a monorepo checkout:
54
+
55
+ uv run buildai ingest <group> <command> [options]
56
+
57
+ Command groups:
58
+ deploy — artifact build, staged rollout, status, resume, rollback
59
+ fleet — preflight, auth, readiness (read-only gates)
60
+ host — bootstrap and repair (host toolchain, not app code)
61
+ docs — this guide
62
+
63
+ Canonical runbook (more detail): apps/buildai-ingest/docs/deploy.md
64
+ Semantic contract: apps/buildai-ingest/docs/deploy-contract.md
65
+ Topology source of truth: apps/buildai-ingest/fleet/inventory.json
66
+ """,
67
+ "prerequisites": """
68
+ PREREQUISITES
69
+ =============
70
+
71
+ Workspace
72
+ • Clone build-ai monorepo and run setup (uv sync --locked, credentials hydration).
73
+ • Commands must run from the repo: uv run buildai ingest ...
74
+ • Standalone pip-installed buildai without apps/buildai-ingest will fail.
75
+
76
+ Network — pick one transport (see: buildai ingest docs --topic transport)
77
+ • tailscale (default) — deploy from any laptop on the tailnet
78
+ • lan — on-site only; uses 10G static IPs from inventory (192.168.10.x)
79
+
80
+ Tailscale (when --transport tailscale)
81
+ • Tailscale app installed and logged in on the operator laptop
82
+ • tailscale CLI on PATH (deploy preflight runs: tailscale status --json)
83
+ • Operator can reach mini/server tailnet hostnames in fleet/inventory.json
84
+
85
+ On-site LAN (when --transport lan)
86
+ • Laptop on the ingest VLAN with route to 192.168.10.x
87
+ • No Tailscale required; minis/servers reached via ingest_ip / lan_host
88
+
89
+ Local secrets (required)
90
+ • apps/buildai-ingest/docs/credentials.private.json (gitignored)
91
+ • Hydrated by scripts/setup from GCP Secret Manager on a configured workstation
92
+ • Or set equivalent env vars (see: buildai ingest docs --topic credentials)
93
+
94
+ Git discipline (before deploy run)
95
+ • git branch --show-current — use an intentional feature/fix branch
96
+ • git status --short — prefer clean tree; pass --allow-dirty only for emergencies
97
+ • Commit and push the snapshot you intend to ship; minis do not git pull
98
+ """,
99
+ "credentials": """
100
+ CREDENTIALS & SECRETS
101
+ =====================
102
+
103
+ Primary file (gitignored):
104
+ apps/buildai-ingest/docs/credentials.private.json
105
+
106
+ Deploy reads passwords and API keys from that file, with env-var overrides:
107
+
108
+ BUILD_AI_INGEST_MINI_PASSWORD — SSH password for all minis
109
+ BUILD_AI_INGEST_SERVER_PASSWORD — SSH/sudo password for servers + aggregator
110
+ BUILD_AI_INGEST_WORKER_API_KEY — Mini agent → Build.ai API
111
+ BUILD_AI_INGEST_AGGREGATOR_API_KEY — Aggregator → Build.ai API
112
+ BUILD_AI_INGEST_UPLOADER_API_KEY — Upload daemon → Build.ai API
113
+
114
+ Optional observability (artifact frontend build / runtime):
115
+ BUILD_AI_INGEST_AGENT_SENTRY_DSN
116
+ BUILD_AI_INGEST_DAEMON_SENTRY_DSN
117
+ BUILD_AI_INGEST_AGGREGATOR_SENTRY_DSN
118
+ BUILD_AI_INGEST_DASHBOARD_SENTRY_DSN (Vite frontend)
119
+ GRAFANA_CLOUD_LOKI_URL / USERNAME / PASSWORD — Alloy log shipping on minis
120
+
121
+ Validate before deploy:
122
+ uv run buildai ingest fleet auth --scope all
123
+
124
+ Never commit live secret values to tracked docs or code.
125
+ """,
126
+ "transport": """
127
+ TRANSPORT MODES
128
+ ===============
129
+
130
+ --transport tailscale (default, recommended remote)
131
+ • Uses tailnet hostnames from fleet/inventory.json
132
+ • Requires: Tailscale CLI, active tailnet session, tailscale status --json OK
133
+ • Password SSH to minis/servers (LegacyPasswordTransport)
134
+ • Future: set BUILDAI_INGEST_TAILSCALE_SSH=1 for Tailscale SSH key transport
135
+
136
+ --transport lan (on-site)
137
+ • Minis: ingest_ip (10G static, e.g. 192.168.10.x) — never mDNS
138
+ • Servers/aggregator: lan_host from inventory
139
+ • Use when physically on the ingest network without Tailscale
140
+
141
+ Preflight verifies reachability for the selected scope and transport:
142
+ uv run buildai ingest fleet preflight --scope all --transport tailscale
143
+ """,
144
+ "workflow": """
145
+ STANDARD DEPLOY WORKFLOW
146
+ ========================
147
+
148
+ 1. Preflight (read-only connectivity + inventory)
149
+ uv run buildai ingest fleet preflight --scope all
150
+
151
+ 2. Auth (API keys for worker/uploader/aggregator principals)
152
+ uv run buildai ingest fleet auth --scope all
153
+
154
+ 3. Readiness (host drift / maintenance blocks)
155
+ uv run buildai ingest fleet readiness --scope active
156
+
157
+ 4. Plan (dry run — targets, rollout order, git warnings)
158
+ uv run buildai ingest deploy plan --scope active
159
+
160
+ 5. Deploy (build artifact + staged rollout)
161
+ uv run buildai ingest deploy run --scope active --reason "describe change"
162
+
163
+ 6. Verify
164
+ uv run buildai ingest deploy status
165
+
166
+ Rollout order for multi-target scopes:
167
+ • Canary: ingest-mini-1 first
168
+ • Then station batches (minis per station, then servers, aggregator)
169
+ • Convergence audit at the end when in rollout plan
170
+
171
+ Partial failure:
172
+ uv run buildai ingest deploy status
173
+ uv run buildai ingest deploy resume <run-id>
174
+
175
+ Rollback (pointer flip when prior release dir still on host):
176
+ uv run buildai ingest deploy rollback --scope active
177
+ uv run buildai ingest deploy rollback --scope station:1 --to <release-id>
178
+ """,
179
+ "scopes": """
180
+ DEPLOY SCOPES (--scope)
181
+ =======================
182
+
183
+ active All inventory targets with desired_state=active (excludes maintenance minis)
184
+ all All minis, servers, aggregator (respects maintenance skips)
185
+ station:N Minis + server for station N; aggregator when any mini in scope
186
+ mini:N Single mini (e.g. mini:1 → ingest-mini-1)
187
+ Alias: ingest-mini-N
188
+ server:N Single upload server (e.g. server:2 → ingest-server-02)
189
+ aggregator Aggregator only
190
+
191
+ Comma lists: station:1,station:2
192
+
193
+ Notes:
194
+ • ingest-mini-11 is maintenance by default — target explicitly if needed
195
+ • Targeted deploys do not satisfy full-fleet convergence alone; follow with active scope
196
+ • First production cutover to release layout: prefer --scope mini:1 as canary
197
+ """,
198
+ "host-layout": """
199
+ ON-HOST RELEASE LAYOUT
200
+ ======================
201
+
202
+ Each host keeps a stable install root:
203
+
204
+ Minis: /Users/<user>/ingest-agent/
205
+ Servers: /home/buildai/upload-daemon/
206
+ Aggregator: /home/buildai/aggregator/
207
+
208
+ Under that root:
209
+
210
+ releases/<release-id>/ Immutable code + per-release .venv (uv sync --locked at stage)
211
+ current → releases/<id>/ Symlink — active release (rollback = flip this)
212
+ shared/
213
+ logs/ Agent, Alloy, launchd logs (persistent)
214
+ state/ Port telemetry, card state, recovery markers
215
+ config/ Fleet-rendered config.yaml (minis)
216
+ alloy-data/ Alloy storage path
217
+
218
+ Deploy stages into releases/<id>/, flips current, restarts services, prunes to keep last 3.
219
+
220
+ Agent plist/systemd units reference .../current for code and .../shared for data.
221
+ """,
222
+ "commands": """
223
+ COMMAND REFERENCE
224
+ =================
225
+
226
+ deploy plan Dry plan: targets, rollout order, git warnings
227
+ deploy run Build artifact + execute staged rollout
228
+ deploy status Latest or specific run state (ledger under ~/.buildai/ingest-deploy-runs/)
229
+ deploy resume Continue a non-terminal run from saved artifact
230
+ deploy rollback Flip current to previous release (re-stage only if pruned)
231
+
232
+ fleet preflight Read-only: inventory, reachability, tailscale check
233
+ fleet auth Validate runtime API credentials for scope
234
+ fleet readiness Host drift / maintenance deployability report
235
+
236
+ host bootstrap Pin minis to ingest-mini-1 Homebrew/rsync/uv baseline
237
+ host repair Repair one mini host outside app deploy
238
+
239
+ docs Print this guide (--topic <name> for one section)
240
+
241
+ Common options:
242
+ --scope, -s Deploy/preflight scope (default varies by command)
243
+ --transport tailscale | lan (default: tailscale)
244
+ --json Machine-readable output where supported
245
+ --allow-dirty deploy run only — allow dirty git tree
246
+ --reason deploy run only — audit trail string
247
+ --to deploy rollback only — explicit prior release id
248
+ """,
249
+ "troubleshooting": """
250
+ TROUBLESHOOTING
251
+ ===============
252
+
253
+ "requires a workspace checkout"
254
+ → Run from monorepo root: uv run buildai ingest ...
255
+ → apps/buildai-ingest/fleet must exist
256
+
257
+ "Tailscale CLI is required"
258
+ → Install Tailscale; ensure tailscale status works
259
+ → Or use --transport lan when on-site
260
+
261
+ "deploy blocked by readiness"
262
+ → uv run buildai ingest fleet readiness --scope <scope>
263
+ → Follow next_action hints (often: buildai ingest host repair <mini>)
264
+
265
+ "deploy lock held"
266
+ → Another run in progress; buildai ingest deploy status
267
+ → Wait or resolve stale lock in ~/.buildai/ingest-deploy-runs/
268
+
269
+ exit 6 / state staged
270
+ → Rollout stopped mid-batch; deploy resume <run-id>
271
+
272
+ exit 4 / deployed_not_converged
273
+ → Hosts updated but audit failed; check aggregator visibility / health
274
+
275
+ Host drift (brew/rsync/uv wrong version)
276
+ → uv run buildai ingest host bootstrap <mini-numbers>
277
+
278
+ Canary failure
279
+ → Rollout aborts; fix ingest-mini-1 before widening scope
280
+
281
+ Artifacts:
282
+ ~/.buildai/ingest-releases/<release-id>/
283
+ Run ledger:
284
+ ~/.buildai/ingest-deploy-runs/
285
+ """,
286
+ }
287
+
288
+ TOPIC_NAMES = sorted(TOPICS.keys())
289
+
290
+
291
+ def render_guide(*, topic: str | None = None) -> str:
292
+ """Return full or single-topic operator documentation."""
293
+
294
+ if topic is None:
295
+ parts = [TOPICS["overview"].strip()]
296
+ for name in TOPIC_NAMES:
297
+ if name == "overview":
298
+ continue
299
+ parts.append(TOPICS[name].strip())
300
+ parts.append(
301
+ "Topics: "
302
+ + ", ".join(name for name in TOPIC_NAMES if name != "overview")
303
+ + "\n\nRun: buildai ingest docs --topic <name>"
304
+ )
305
+ return "\n\n".join(parts) + "\n"
306
+
307
+ key = topic.strip().lower().replace("_", "-")
308
+ if key not in TOPICS:
309
+ available = ", ".join(TOPIC_NAMES)
310
+ raise ValueError(f"Unknown topic {topic!r}. Available: {available}")
311
+ return TOPICS[key].strip() + "\n"
@@ -317,6 +317,16 @@ def db_callback(
317
317
  init_ops_context(ctx)
318
318
 
319
319
 
320
+ def _has_ingest_workspace() -> bool:
321
+ """Return whether the current checkout exposes the ingest deploy engine."""
322
+
323
+ current = Path(__file__).resolve()
324
+ for parent in current.parents:
325
+ if (parent / ".git").exists() and (parent / "apps" / "buildai-ingest" / "fleet").exists():
326
+ return True
327
+ return False
328
+
329
+
320
330
  if has_core():
321
331
  from cli.commands.db import app as db_app
322
332
  from cli.commands.gigcamera import app as gigcamera_app
@@ -327,6 +337,11 @@ if has_core():
327
337
  app.add_typer(gigcamera_app, name="gigcamera")
328
338
  app.add_typer(grid_app, name="grid")
329
339
 
340
+ if _has_ingest_workspace():
341
+ from cli.commands.ingest import app as ingest_app
342
+
343
+ app.add_typer(ingest_app, name="ingest")
344
+
330
345
 
331
346
  def main() -> None:
332
347
  app()
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "buildai-cli"
7
- version = "0.3.82"
7
+ version = "0.3.83"
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