buildai-cli 0.3.82__tar.gz → 0.3.84__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.
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/.gitignore +1 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/CLAUDE.md +3 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/PKG-INFO +1 -1
- buildai_cli-0.3.84/cli/commands/ingest.py +363 -0
- buildai_cli-0.3.84/cli/commands/ingest_docs.py +311 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/main.py +15 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/pyproject.toml +1 -1
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/AGENTS.md +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/buildai_bootstrap.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/__init__.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/_has_core.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/auth_local.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/commands/__init__.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/commands/api_proxy.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/commands/auth.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/commands/db/__init__.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/commands/db/broker.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/commands/db/common.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/commands/db/migrate.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/commands/db/query.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/commands/db/schema.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/commands/db/status.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/commands/db/tunnel.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/commands/dev.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/commands/doctor.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/commands/egoexo.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/commands/gigcamera.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/commands/grid.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/commands/processing.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/config.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/console.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/context.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/db_broker.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/guard.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/internal_api.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/nl_query/__init__.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/nl_query/dataset_tools.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/ops_init.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/output.py +0 -0
- {buildai_cli-0.3.82 → buildai_cli-0.3.84}/cli/pagination.py +0 -0
|
@@ -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
|
|
@@ -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
|
+
"""Reconcile drifted minis to the managed Homebrew, rsync, and uv contract."""
|
|
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] — reconcile Homebrew/rsync/uv to the deploy contract (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 Reconcile minis to the managed Homebrew/rsync/uv contract
|
|
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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|