buildai-cli 0.3.149__tar.gz → 0.3.151__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.151/AGENTS.md +24 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/Dockerfile.rescaling-migration-jobs +1 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/Dockerfile.rescaling-migration-jobs.dockerignore +3 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/PKG-INFO +1 -1
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/commands/egoexo.py +2 -0
- buildai_cli-0.3.151/cli/commands/egoexo_config.py +557 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/commands/ingest.py +30 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/pyproject.toml +1 -1
- buildai_cli-0.3.149/AGENTS.md +0 -50
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/.gitignore +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/CLAUDE.md +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/buildai_bootstrap.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/__init__.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/_has_core.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/auth_local.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/commands/__init__.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/commands/api_proxy.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/commands/auth.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/commands/db/__init__.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/commands/db/broker.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/commands/db/common.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/commands/db/migrate.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/commands/db/query.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/commands/db/rescaling.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/commands/db/schema.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/commands/db/status.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/commands/db/tunnel.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/commands/dev.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/commands/doctor.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/commands/ego_frame_search.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/commands/gigcamera.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/commands/ingest_docs.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/commands/intake.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/commands/label_replay_recovery.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/commands/labeling.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/commands/processing.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/commands/rescaling.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/commands/spec.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/commands/spec_pr.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/config.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/console.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/context.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/db_broker.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/guard.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/internal_api.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/labeling_onboarding.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/main.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/nl_query/__init__.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/nl_query/dataset_tools.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/ops_init.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/output.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/pagination.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/rescaling_migration/__init__.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/rescaling_migration/canonical.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/rescaling_migration/export.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/rescaling_migration/feed_proofs.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/rescaling_migration/fence.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/rescaling_migration/importer.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/rescaling_migration/jobs.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/rescaling_migration/management.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/rescaling_migration/orchestrator.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/rescaling_migration/probes.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/rescaling_migration/reconcile.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/rescaling_migration/run_record.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/rescaling_migration/runtime.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/rescaling_migration/sql/legacy_export_closeout.sql +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/rescaling_migration/sql/legacy_export_prepare.sql +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/rescaling_migration/sql/legacy_fence.sql +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/rescaling_migration/sql/legacy_unfreeze.sql +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/rescaling_migration/sql/v2_export_closeout.sql +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/rescaling_migration/sql/v2_export_prepare.sql +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/rescaling_migration/sql/v2_fence.sql +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/rescaling_migration/sql/v2_unfreeze.sql +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/rescaling_migration/storage.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/rescaling_migration/transform.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/rls_guard.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/rescaling_logical_backup/__init__.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/rescaling_logical_backup/__main__.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/rescaling_migration_job/__init__.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/rescaling_migration_job/__main__.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/rescaling_recovery_extractor/__init__.py +0 -0
- {buildai_cli-0.3.149 → buildai_cli-0.3.151}/rescaling_recovery_extractor/__main__.py +0 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Build AI CLI
|
|
2
|
+
|
|
3
|
+
The installed CLI calls the API. The workspace CLI can also inspect the database.
|
|
4
|
+
Keep those modes separate: installed clients must work without `core/` or database credentials.
|
|
5
|
+
|
|
6
|
+
## Database commands
|
|
7
|
+
|
|
8
|
+
- Database commands use a profile-specific `alloydb-auth-proxy` broker. Its state in
|
|
9
|
+
`~/.buildai/db-brokers` is shared across worktrees. Use `db broker status`,
|
|
10
|
+
`ensure`, or `stop` to manage it.
|
|
11
|
+
- Select the database with `buildai db --env production|staging|dev`.
|
|
12
|
+
App targeting comes from environment variables and the Makefile, not a saved CLI context.
|
|
13
|
+
- Staging needs an IAP, VPN, or SOCKS route. For the CLI tunnel, run
|
|
14
|
+
`buildai db --env staging tunnel`, then query with `--all-proxy socks5://127.0.0.1:1080`.
|
|
15
|
+
- Before counting frame labels by status or mode, read the
|
|
16
|
+
[labeling query rule](../../../core/dal/labeling/AGENTS.md).
|
|
17
|
+
A pushed-down filter can produce incorrect totals.
|
|
18
|
+
|
|
19
|
+
## Check your change
|
|
20
|
+
|
|
21
|
+
Run the relevant tests in `tests/cli/` from the repo root. For broker changes,
|
|
22
|
+
include `tests/cli/test_db_broker.py`; for query handling, include
|
|
23
|
+
`tests/cli/test_db_query_analysis.py`. Live queries are needed only when the
|
|
24
|
+
change depends on connection behavior.
|
|
@@ -20,6 +20,7 @@ WORKDIR /app
|
|
|
20
20
|
# uv must see every workspace member declaration to honor the checked-in lockfile.
|
|
21
21
|
COPY pyproject.toml uv.lock ./
|
|
22
22
|
COPY core/pyproject.toml core/pyproject.toml
|
|
23
|
+
COPY packages/egoexo/pyproject.toml packages/egoexo/pyproject.toml
|
|
23
24
|
COPY services/pyproject.toml services/pyproject.toml
|
|
24
25
|
COPY apps/buildai/buildai-api/pyproject.toml apps/buildai/buildai-api/pyproject.toml
|
|
25
26
|
COPY apps/buildai/buildai-cli/pyproject.toml apps/buildai/buildai-cli/pyproject.toml
|
|
@@ -10,10 +10,12 @@ import httpx
|
|
|
10
10
|
import typer
|
|
11
11
|
|
|
12
12
|
from cli.commands.api_proxy import _build_url
|
|
13
|
+
from cli.commands.egoexo_config import app as config_app
|
|
13
14
|
from cli.config import resolve_api_url, resolve_credential
|
|
14
15
|
from cli.console import error
|
|
15
16
|
|
|
16
17
|
app = typer.Typer(help="Inspect EgoExo verdict-plane outcomes.", no_args_is_help=True)
|
|
18
|
+
app.add_typer(config_app, name="config")
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
def _request(path: str, *, query_params: list[str] | None = None) -> None:
|
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
"""Operator commands for the EgoExo runtime configuration store.
|
|
2
|
+
|
|
3
|
+
The store is one JSON object in the capture bucket. The processing API reads it
|
|
4
|
+
every thirty seconds and lets each stored field override the matching environment
|
|
5
|
+
value. These commands read the object, show the effective configuration, and
|
|
6
|
+
write new revisions. Every write needs ``--write --reason``. Reads never change
|
|
7
|
+
anything.
|
|
8
|
+
|
|
9
|
+
The commands need the workspace ``egoexo`` package and the Google Cloud Storage
|
|
10
|
+
client, so run them from the repo with ``uv run buildai egoexo config ...``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
import tempfile
|
|
18
|
+
from dataclasses import replace
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
import typer
|
|
23
|
+
|
|
24
|
+
from cli.console import error, info
|
|
25
|
+
|
|
26
|
+
app = typer.Typer(
|
|
27
|
+
help="Show and change the EgoExo runtime configuration store.", no_args_is_help=True
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
DEPLOYMENT_RELATIVE = Path("infra/production/services/processing-api")
|
|
31
|
+
LOCAL_JSON_MAX_BYTES = 1024 * 1024
|
|
32
|
+
ASSET_PREFIX = "derived/runtime-assets/"
|
|
33
|
+
|
|
34
|
+
URI_OPTION = typer.Option(
|
|
35
|
+
None,
|
|
36
|
+
"--uri",
|
|
37
|
+
envvar="EGOEXO_RUNTIME_CONFIG_URI",
|
|
38
|
+
help="Runtime config object. Default: the production object in gs://buildai-ego-exo.",
|
|
39
|
+
)
|
|
40
|
+
DEPLOYMENT_OPTION = typer.Option(
|
|
41
|
+
None,
|
|
42
|
+
"--deployment",
|
|
43
|
+
help=(
|
|
44
|
+
"Checked-in deployment directory whose egoexo-recipe.json and "
|
|
45
|
+
"egoexo-trusted-assets.json are the environment fallback shown for env fields. "
|
|
46
|
+
"Default: infra/production/services/processing-api when run inside the repo."
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
WRITE_OPTION = typer.Option(False, "--write", help="Write the revision. Omit for a dry run.")
|
|
50
|
+
REASON_OPTION = typer.Option(
|
|
51
|
+
None, "--reason", help="Why this revision exists. Required with --write."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _runtime_config():
|
|
56
|
+
"""Import the store module lazily so installed clients without it get a clear message."""
|
|
57
|
+
try:
|
|
58
|
+
from egoexo.runtime.pipeline import runtime_config
|
|
59
|
+
except ImportError as exc:
|
|
60
|
+
error(
|
|
61
|
+
"'buildai egoexo config' needs the workspace egoexo package and the GCS client.\n"
|
|
62
|
+
" Run it from the repo: uv run buildai egoexo config ..."
|
|
63
|
+
)
|
|
64
|
+
raise typer.Exit(1) from exc
|
|
65
|
+
return runtime_config
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _store(uri: str | None):
|
|
69
|
+
"""Build a store on the real GCS adapter. Tests replace this function."""
|
|
70
|
+
rc = _runtime_config()
|
|
71
|
+
from egoexo.adapters.gcs.publication import GCSStorage
|
|
72
|
+
|
|
73
|
+
return rc.RuntimeConfigStore(uri or rc.DEFAULT_RUNTIME_CONFIG_URI, storage=GCSStorage())
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _author() -> str:
|
|
77
|
+
"""Return the active gcloud account. It is recorded as the revision author."""
|
|
78
|
+
from cli.auth_local import _gcloud_value
|
|
79
|
+
|
|
80
|
+
account = _gcloud_value("account")
|
|
81
|
+
if not account:
|
|
82
|
+
error(
|
|
83
|
+
"No active gcloud account. Run `gcloud auth login` first; the account is "
|
|
84
|
+
"recorded as the revision author."
|
|
85
|
+
)
|
|
86
|
+
raise typer.Exit(1)
|
|
87
|
+
return account
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _deployment_dir(explicit: Path | None) -> Path | None:
|
|
91
|
+
"""Return the checked-in deployment directory, or None when it is not available."""
|
|
92
|
+
if explicit is not None:
|
|
93
|
+
if not explicit.is_dir():
|
|
94
|
+
error(f"Deployment directory not found: {explicit}")
|
|
95
|
+
raise typer.Exit(1)
|
|
96
|
+
return explicit
|
|
97
|
+
for root in (Path.cwd(), *Path.cwd().parents):
|
|
98
|
+
candidate = root / DEPLOYMENT_RELATIVE
|
|
99
|
+
if (candidate / "egoexo-recipe.json").is_file():
|
|
100
|
+
return candidate
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _load_json_file(path: Path) -> Any:
|
|
105
|
+
"""Read a bounded local JSON file."""
|
|
106
|
+
if not path.is_file():
|
|
107
|
+
error(f"File not found: {path}")
|
|
108
|
+
raise typer.Exit(1)
|
|
109
|
+
data = path.read_bytes()
|
|
110
|
+
if len(data) > LOCAL_JSON_MAX_BYTES:
|
|
111
|
+
error(f"{path} exceeds {LOCAL_JSON_MAX_BYTES} bytes")
|
|
112
|
+
raise typer.Exit(1)
|
|
113
|
+
try:
|
|
114
|
+
return json.loads(data)
|
|
115
|
+
except ValueError as exc:
|
|
116
|
+
error(f"{path} is not valid JSON: {exc}")
|
|
117
|
+
raise typer.Exit(1) from exc
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _deployment_env(deployment: Path | None):
|
|
121
|
+
"""Build the environment fallback from checked-in files. Flags stay unknown."""
|
|
122
|
+
rc = _runtime_config()
|
|
123
|
+
if deployment is None:
|
|
124
|
+
return rc.EnvironmentConfig(), None
|
|
125
|
+
recipe = deployment / "egoexo-recipe.json"
|
|
126
|
+
assets = deployment / "egoexo-trusted-assets.json"
|
|
127
|
+
return (
|
|
128
|
+
rc.EnvironmentConfig(
|
|
129
|
+
recipe_json=recipe.read_text() if recipe.is_file() else None,
|
|
130
|
+
trusted_assets_json=assets.read_text() if assets.is_file() else "[]",
|
|
131
|
+
),
|
|
132
|
+
deployment,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _read_base(store):
|
|
137
|
+
"""Read the current document once. Returns (document, generation, problem)."""
|
|
138
|
+
try:
|
|
139
|
+
document, generation = store.read()
|
|
140
|
+
return document, generation, None
|
|
141
|
+
except (ValueError, TypeError, KeyError) as exc:
|
|
142
|
+
generation = str(store.storage.metadata(store.uri)["generation"])
|
|
143
|
+
return None, generation, f"{type(exc).__name__}: {exc}"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _emit(payload: dict[str, Any]) -> None:
|
|
147
|
+
sys.stdout.write(json.dumps(payload, indent=2, default=str) + "\n")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _recipe_summary(recipe: dict[str, Any]) -> dict[str, Any]:
|
|
151
|
+
stages = {}
|
|
152
|
+
for stage, spec in sorted(recipe.get("preset", {}).items()):
|
|
153
|
+
entry: dict[str, Any] = {"method": spec.get("method")}
|
|
154
|
+
if "config" in spec:
|
|
155
|
+
entry["config_uri"] = spec["config"].get("uri")
|
|
156
|
+
for key in ("frames_per_job", "selection", "selections"):
|
|
157
|
+
if key in spec:
|
|
158
|
+
entry[key] = spec[key]
|
|
159
|
+
stages[stage] = entry
|
|
160
|
+
return {
|
|
161
|
+
key: recipe.get(key)
|
|
162
|
+
for key in ("enabled", "gpu_enabled", "bucket", "max_pages", "max_plans")
|
|
163
|
+
if key in recipe
|
|
164
|
+
} | {"stages": stages}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _task_class_summary(classes: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
168
|
+
keys = (
|
|
169
|
+
"name",
|
|
170
|
+
"machine_type",
|
|
171
|
+
"provisioning_model",
|
|
172
|
+
"max_active_tasks",
|
|
173
|
+
"max_tasks_per_capture",
|
|
174
|
+
"runtime_seconds",
|
|
175
|
+
"image",
|
|
176
|
+
)
|
|
177
|
+
return [{key: item.get(key) for key in keys if key in item} for item in classes]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _asset_summary(assets: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
181
|
+
return [{"role": a.get("role"), "uri": a.get("uri"), "sha256": a.get("sha256")} for a in assets]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
ENV_NOTE = "environment value lives in the processing API deployment; not readable locally"
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@app.command("show")
|
|
188
|
+
def show(
|
|
189
|
+
uri: str | None = URI_OPTION,
|
|
190
|
+
deployment: Path | None = DEPLOYMENT_OPTION,
|
|
191
|
+
full: bool = typer.Option(False, "--full", help="Print full values instead of summaries."),
|
|
192
|
+
) -> None:
|
|
193
|
+
"""Show the effective configuration, the source of each field, and the store generation."""
|
|
194
|
+
rc = _runtime_config()
|
|
195
|
+
store = _store(uri)
|
|
196
|
+
document, generation, problem = _read_base(store)
|
|
197
|
+
env, deployment_dir = _deployment_env(_deployment_dir(deployment))
|
|
198
|
+
loaded = rc.LoadedDocument(document, generation) if document is not None else None
|
|
199
|
+
effective = rc.merge_effective(env, loaded)
|
|
200
|
+
fields: dict[str, Any] = {}
|
|
201
|
+
|
|
202
|
+
def value_or_none(name: str, raw: str | None, summarize):
|
|
203
|
+
if raw is None:
|
|
204
|
+
return None
|
|
205
|
+
parsed = json.loads(raw)
|
|
206
|
+
return parsed if full else summarize(parsed)
|
|
207
|
+
|
|
208
|
+
for name, raw, summarize, filename in (
|
|
209
|
+
("recipe", effective.recipe_json, _recipe_summary, "egoexo-recipe.json"),
|
|
210
|
+
(
|
|
211
|
+
"trusted_assets",
|
|
212
|
+
effective.trusted_assets_json,
|
|
213
|
+
_asset_summary,
|
|
214
|
+
"egoexo-trusted-assets.json",
|
|
215
|
+
),
|
|
216
|
+
):
|
|
217
|
+
source = effective.source(name)
|
|
218
|
+
entry: dict[str, Any] = {"source": source}
|
|
219
|
+
if source == "store" or deployment_dir is not None:
|
|
220
|
+
entry["value"] = value_or_none(name, raw, summarize)
|
|
221
|
+
if source == "env":
|
|
222
|
+
entry["note"] = (
|
|
223
|
+
f"environment value read from {deployment_dir / filename}"
|
|
224
|
+
if deployment_dir is not None
|
|
225
|
+
else ENV_NOTE
|
|
226
|
+
)
|
|
227
|
+
fields[name] = entry
|
|
228
|
+
source = effective.source("task_classes")
|
|
229
|
+
fields["task_classes"] = {"source": source}
|
|
230
|
+
if source == "store":
|
|
231
|
+
fields["task_classes"]["value"] = value_or_none(
|
|
232
|
+
"task_classes", effective.task_classes_json, _task_class_summary
|
|
233
|
+
)
|
|
234
|
+
else:
|
|
235
|
+
fields["task_classes"]["note"] = ENV_NOTE + " (rendered by terraform task_classes.tf)"
|
|
236
|
+
fields["flags"] = {
|
|
237
|
+
name: (
|
|
238
|
+
{"source": "store", "value": getattr(effective, name)}
|
|
239
|
+
if effective.source(name) == "store"
|
|
240
|
+
else {"source": "env", "note": ENV_NOTE}
|
|
241
|
+
)
|
|
242
|
+
for name in rc.FLAG_NAMES
|
|
243
|
+
}
|
|
244
|
+
_emit(
|
|
245
|
+
{
|
|
246
|
+
"uri": store.uri,
|
|
247
|
+
"generation": None if generation == "0" else generation,
|
|
248
|
+
"revision": (
|
|
249
|
+
{
|
|
250
|
+
"author": document.author,
|
|
251
|
+
"reason": document.reason,
|
|
252
|
+
"created_at": document.created_at,
|
|
253
|
+
"previous_generation": document.previous_generation,
|
|
254
|
+
}
|
|
255
|
+
if document is not None
|
|
256
|
+
else None
|
|
257
|
+
),
|
|
258
|
+
"problem": problem,
|
|
259
|
+
"fields": fields,
|
|
260
|
+
}
|
|
261
|
+
)
|
|
262
|
+
if problem:
|
|
263
|
+
raise typer.Exit(1)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _revise(store, *, write: bool, reason: str | None, change: dict[str, Any], mutate) -> None:
|
|
267
|
+
"""Validate a new revision, print the plan, and write it only with --write --reason."""
|
|
268
|
+
rc = _runtime_config()
|
|
269
|
+
document, generation, problem = _read_base(store)
|
|
270
|
+
base = document if document is not None else rc.RuntimeConfigDocument()
|
|
271
|
+
revised = mutate(base)
|
|
272
|
+
try:
|
|
273
|
+
revised.validate()
|
|
274
|
+
except (ValueError, TypeError, KeyError) as exc:
|
|
275
|
+
error(f"Rejected by the production parser: {type(exc).__name__}: {exc}")
|
|
276
|
+
raise typer.Exit(1) from exc
|
|
277
|
+
plan = {
|
|
278
|
+
"uri": store.uri,
|
|
279
|
+
"expected_generation": None if generation == "0" else generation,
|
|
280
|
+
"current_problem": problem,
|
|
281
|
+
"change": change,
|
|
282
|
+
"overrides_after": {
|
|
283
|
+
"recipe": revised.recipe is not None,
|
|
284
|
+
"trusted_assets": revised.trusted_assets is not None,
|
|
285
|
+
"task_classes": revised.task_classes is not None,
|
|
286
|
+
"flags": dict(revised.flags),
|
|
287
|
+
},
|
|
288
|
+
}
|
|
289
|
+
if not write:
|
|
290
|
+
_emit(plan | {"written": False})
|
|
291
|
+
info('Dry run. Nothing was written. Re-run with --write --reason "<text>".')
|
|
292
|
+
return
|
|
293
|
+
if not reason or not reason.strip():
|
|
294
|
+
error("--reason is required with --write.")
|
|
295
|
+
raise typer.Exit(1)
|
|
296
|
+
author = _author()
|
|
297
|
+
try:
|
|
298
|
+
new_generation = store.write(
|
|
299
|
+
revised, author=author, reason=reason, expected_generation=generation
|
|
300
|
+
)
|
|
301
|
+
except rc.GenerationMismatch as exc:
|
|
302
|
+
error("The object changed since it was read. Re-run the command to read it again.")
|
|
303
|
+
raise typer.Exit(1) from exc
|
|
304
|
+
_emit(
|
|
305
|
+
plan
|
|
306
|
+
| {
|
|
307
|
+
"written": True,
|
|
308
|
+
"generation": new_generation,
|
|
309
|
+
"history_uri": store.history_uri(new_generation),
|
|
310
|
+
"author": author,
|
|
311
|
+
"reason": reason,
|
|
312
|
+
}
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
@app.command("set-recipe")
|
|
317
|
+
def set_recipe(
|
|
318
|
+
file: Path = typer.Argument(..., help="Recipe JSON, the same document the API loads."),
|
|
319
|
+
uri: str | None = URI_OPTION,
|
|
320
|
+
write: bool = WRITE_OPTION,
|
|
321
|
+
reason: str | None = REASON_OPTION,
|
|
322
|
+
) -> None:
|
|
323
|
+
"""Store a recipe that overrides EGOEXO_CAPTURE_RECIPE_JSON."""
|
|
324
|
+
recipe = _load_json_file(file)
|
|
325
|
+
_revise(
|
|
326
|
+
_store(uri),
|
|
327
|
+
write=write,
|
|
328
|
+
reason=reason,
|
|
329
|
+
change={"field": "recipe", "file": str(file)},
|
|
330
|
+
mutate=lambda base: replace(base, recipe=recipe),
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@app.command("set-task-classes")
|
|
335
|
+
def set_task_classes(
|
|
336
|
+
file: Path = typer.Argument(..., help="Task class list JSON, one to three classes."),
|
|
337
|
+
uri: str | None = URI_OPTION,
|
|
338
|
+
write: bool = WRITE_OPTION,
|
|
339
|
+
reason: str | None = REASON_OPTION,
|
|
340
|
+
) -> None:
|
|
341
|
+
"""Store task classes that override BUILDAI_EGOEXO_ASSIGNED_TASK_CLASSES_JSON."""
|
|
342
|
+
classes = _load_json_file(file)
|
|
343
|
+
_revise(
|
|
344
|
+
_store(uri),
|
|
345
|
+
write=write,
|
|
346
|
+
reason=reason,
|
|
347
|
+
change={"field": "task_classes", "file": str(file)},
|
|
348
|
+
mutate=lambda base: replace(base, task_classes=classes),
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@app.command("set-trusted-assets")
|
|
353
|
+
def set_trusted_assets(
|
|
354
|
+
file: Path = typer.Argument(..., help="Trusted asset list JSON with pinned identities."),
|
|
355
|
+
uri: str | None = URI_OPTION,
|
|
356
|
+
write: bool = WRITE_OPTION,
|
|
357
|
+
reason: str | None = REASON_OPTION,
|
|
358
|
+
) -> None:
|
|
359
|
+
"""Store trusted assets that override BUILDAI_EGOEXO_CAPTURE_TRUSTED_ASSETS_JSON."""
|
|
360
|
+
assets = _load_json_file(file)
|
|
361
|
+
_revise(
|
|
362
|
+
_store(uri),
|
|
363
|
+
write=write,
|
|
364
|
+
reason=reason,
|
|
365
|
+
change={"field": "trusted_assets", "file": str(file)},
|
|
366
|
+
mutate=lambda base: replace(base, trusted_assets=assets),
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@app.command("set-flag")
|
|
371
|
+
def set_flag(
|
|
372
|
+
name: str = typer.Argument(
|
|
373
|
+
...,
|
|
374
|
+
help="assigned_dispatch_enabled, auto_advance_enabled or native_discovery_enabled.",
|
|
375
|
+
),
|
|
376
|
+
value: str = typer.Argument(..., help="true or false."),
|
|
377
|
+
uri: str | None = URI_OPTION,
|
|
378
|
+
write: bool = WRITE_OPTION,
|
|
379
|
+
reason: str | None = REASON_OPTION,
|
|
380
|
+
) -> None:
|
|
381
|
+
"""Store one lane flag that overrides its environment variable."""
|
|
382
|
+
rc = _runtime_config()
|
|
383
|
+
if name not in rc.FLAG_NAMES:
|
|
384
|
+
error(f"Unknown flag {name!r}. Choose one of: {', '.join(rc.FLAG_NAMES)}")
|
|
385
|
+
raise typer.Exit(1)
|
|
386
|
+
if value.lower() not in {"true", "false"}:
|
|
387
|
+
error("Flag value must be true or false.")
|
|
388
|
+
raise typer.Exit(1)
|
|
389
|
+
enabled = value.lower() == "true"
|
|
390
|
+
_revise(
|
|
391
|
+
_store(uri),
|
|
392
|
+
write=write,
|
|
393
|
+
reason=reason,
|
|
394
|
+
change={"field": f"flags.{name}", "value": enabled},
|
|
395
|
+
mutate=lambda base: replace(base, flags=dict(base.flags) | {name: enabled}),
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
@app.command("clear")
|
|
400
|
+
def clear(
|
|
401
|
+
field: str = typer.Argument(
|
|
402
|
+
..., help="recipe, trusted_assets, task_classes, or a flag name to hand back to env."
|
|
403
|
+
),
|
|
404
|
+
uri: str | None = URI_OPTION,
|
|
405
|
+
write: bool = WRITE_OPTION,
|
|
406
|
+
reason: str | None = REASON_OPTION,
|
|
407
|
+
) -> None:
|
|
408
|
+
"""Remove one stored override so the environment value applies again."""
|
|
409
|
+
rc = _runtime_config()
|
|
410
|
+
if field in rc.VALUE_FIELDS:
|
|
411
|
+
mutate = lambda base: replace(base, **{field: None}) # noqa: E731
|
|
412
|
+
elif field in rc.FLAG_NAMES:
|
|
413
|
+
mutate = lambda base: replace( # noqa: E731
|
|
414
|
+
base, flags={k: v for k, v in base.flags.items() if k != field}
|
|
415
|
+
)
|
|
416
|
+
else:
|
|
417
|
+
error(f"Unknown field {field!r}. Choose one of: {', '.join(rc.FIELD_NAMES)}")
|
|
418
|
+
raise typer.Exit(1)
|
|
419
|
+
_revise(
|
|
420
|
+
_store(uri),
|
|
421
|
+
write=write,
|
|
422
|
+
reason=reason,
|
|
423
|
+
change={"field": field, "value": "environment"},
|
|
424
|
+
mutate=mutate,
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _asset_group(uri: str) -> str | None:
|
|
429
|
+
"""Return the <group> segment of derived/runtime-assets/<group>/<sha256>/<name>."""
|
|
430
|
+
marker = uri.find(ASSET_PREFIX)
|
|
431
|
+
if marker < 0:
|
|
432
|
+
return None
|
|
433
|
+
parts = uri[marker + len(ASSET_PREFIX) :].split("/")
|
|
434
|
+
return parts[0] if len(parts) == 3 and parts[0] else None
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
@app.command("pin-config-asset")
|
|
438
|
+
def pin_config_asset(
|
|
439
|
+
stage: str = typer.Option(..., "--stage", help="Recipe stage whose config changes."),
|
|
440
|
+
file: Path = typer.Option(..., "--file", help="Local JSON config to upload."),
|
|
441
|
+
group: str | None = typer.Option(
|
|
442
|
+
None, "--group", help="Asset group. Default: the group of the stage's current config."
|
|
443
|
+
),
|
|
444
|
+
name: str | None = typer.Option(
|
|
445
|
+
None, "--name", help="Object name under the SHA256 folder. Default: the file name."
|
|
446
|
+
),
|
|
447
|
+
uri: str | None = URI_OPTION,
|
|
448
|
+
deployment: Path | None = DEPLOYMENT_OPTION,
|
|
449
|
+
write: bool = WRITE_OPTION,
|
|
450
|
+
reason: str | None = REASON_OPTION,
|
|
451
|
+
) -> None:
|
|
452
|
+
"""Upload a content-addressed stage config and pin it in the recipe and trusted assets.
|
|
453
|
+
|
|
454
|
+
The object lands at derived/runtime-assets/<group>/<sha256>/<name>. After the
|
|
455
|
+
upload the command reads it back and checks generation, size, CRC32C and
|
|
456
|
+
SHA256. The recipe's stage config, the method config_sha256, and the trusted
|
|
457
|
+
asset list change together in one new store revision.
|
|
458
|
+
"""
|
|
459
|
+
_runtime_config()
|
|
460
|
+
from egoexo.adapters.gcs import publication
|
|
461
|
+
|
|
462
|
+
_load_json_file(file)
|
|
463
|
+
store = _store(uri)
|
|
464
|
+
document, generation, problem = _read_base(store)
|
|
465
|
+
env, deployment_dir = _deployment_env(_deployment_dir(deployment))
|
|
466
|
+
if document is not None and document.recipe is not None:
|
|
467
|
+
recipe = json.loads(json.dumps(document.recipe))
|
|
468
|
+
recipe_source = "store"
|
|
469
|
+
elif env.recipe_json:
|
|
470
|
+
recipe = json.loads(env.recipe_json)
|
|
471
|
+
recipe_source = f"env ({deployment_dir / 'egoexo-recipe.json'})"
|
|
472
|
+
else:
|
|
473
|
+
error("No recipe in the store and no --deployment recipe to start from.")
|
|
474
|
+
raise typer.Exit(1)
|
|
475
|
+
if document is not None and document.trusted_assets is not None:
|
|
476
|
+
assets = json.loads(json.dumps(document.trusted_assets))
|
|
477
|
+
else:
|
|
478
|
+
assets = json.loads(env.trusted_assets_json)
|
|
479
|
+
spec = recipe.get("preset", {}).get(stage)
|
|
480
|
+
if not isinstance(spec, dict) or not isinstance(spec.get("method"), dict):
|
|
481
|
+
error(f"Stage {stage!r} is not in the recipe preset.")
|
|
482
|
+
raise typer.Exit(1)
|
|
483
|
+
old = spec.get("config")
|
|
484
|
+
group = group or (_asset_group(old["uri"]) if isinstance(old, dict) else None)
|
|
485
|
+
if not group or "/" in group:
|
|
486
|
+
error("Pass --group; the stage has no current config to take the group from.")
|
|
487
|
+
raise typer.Exit(1)
|
|
488
|
+
name = name or file.name
|
|
489
|
+
if "/" in name or name in {"", ".", ".."}:
|
|
490
|
+
error("--name must be a plain file name.")
|
|
491
|
+
raise typer.Exit(1)
|
|
492
|
+
size, sha, crc = publication.identity(file)
|
|
493
|
+
target = f"gs://{recipe['bucket']}/{ASSET_PREFIX}{group}/{sha}/{name}"
|
|
494
|
+
change = {
|
|
495
|
+
"stage": stage,
|
|
496
|
+
"recipe_source": recipe_source,
|
|
497
|
+
"file": str(file),
|
|
498
|
+
"target_uri": target,
|
|
499
|
+
"sha256": sha,
|
|
500
|
+
"size_bytes": size,
|
|
501
|
+
"crc32c_base64": crc,
|
|
502
|
+
"replaces_config_uri": old["uri"] if isinstance(old, dict) else None,
|
|
503
|
+
}
|
|
504
|
+
if not write:
|
|
505
|
+
_emit(
|
|
506
|
+
{
|
|
507
|
+
"uri": store.uri,
|
|
508
|
+
"expected_generation": None if generation == "0" else generation,
|
|
509
|
+
"current_problem": problem,
|
|
510
|
+
"change": change,
|
|
511
|
+
"written": False,
|
|
512
|
+
}
|
|
513
|
+
)
|
|
514
|
+
info('Dry run. Nothing was uploaded or written. Re-run with --write --reason "<text>".')
|
|
515
|
+
return
|
|
516
|
+
if not reason or not reason.strip():
|
|
517
|
+
error("--reason is required with --write.")
|
|
518
|
+
raise typer.Exit(1)
|
|
519
|
+
with tempfile.TemporaryDirectory() as scratch:
|
|
520
|
+
published = publication.publish(store.storage, target, file, Path(scratch))
|
|
521
|
+
check = Path(scratch) / "readback.json"
|
|
522
|
+
store.storage.download(target, published.generation, check, size)
|
|
523
|
+
if publication.identity(check) != (size, sha, crc):
|
|
524
|
+
error("Readback of the uploaded object did not match the local file.")
|
|
525
|
+
raise typer.Exit(1)
|
|
526
|
+
ref = {
|
|
527
|
+
"role": "producer_config",
|
|
528
|
+
"uri": target,
|
|
529
|
+
"generation": published.generation,
|
|
530
|
+
"size_bytes": size,
|
|
531
|
+
"sha256": sha,
|
|
532
|
+
"sha256_verification": "declared",
|
|
533
|
+
"crc32c_base64": crc,
|
|
534
|
+
}
|
|
535
|
+
spec["config"] = ref
|
|
536
|
+
spec["method"] = dict(spec["method"]) | {"config_sha256": sha}
|
|
537
|
+
replaced = False
|
|
538
|
+
for index, asset in enumerate(assets):
|
|
539
|
+
if (
|
|
540
|
+
isinstance(old, dict)
|
|
541
|
+
and asset.get("uri") == old["uri"]
|
|
542
|
+
and asset.get("role") == ref["role"]
|
|
543
|
+
):
|
|
544
|
+
assets[index] = ref
|
|
545
|
+
replaced = True
|
|
546
|
+
if not replaced and not any(
|
|
547
|
+
a.get("uri") == target and a.get("role") == ref["role"] for a in assets
|
|
548
|
+
):
|
|
549
|
+
assets.append(ref)
|
|
550
|
+
change |= {"uploaded_generation": published.generation, "trusted_asset_replaced": replaced}
|
|
551
|
+
_revise(
|
|
552
|
+
store,
|
|
553
|
+
write=True,
|
|
554
|
+
reason=reason,
|
|
555
|
+
change=change,
|
|
556
|
+
mutate=lambda base: replace(base, recipe=recipe, trusted_assets=assets),
|
|
557
|
+
)
|
|
@@ -6,6 +6,7 @@ import json
|
|
|
6
6
|
import sys
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Annotated
|
|
9
|
+
from uuid import UUID
|
|
9
10
|
|
|
10
11
|
import typer
|
|
11
12
|
|
|
@@ -285,6 +286,35 @@ def fleet_readiness(
|
|
|
285
286
|
info(f"next: {action}")
|
|
286
287
|
|
|
287
288
|
|
|
289
|
+
@fleet_app.command("retry-upload")
|
|
290
|
+
def fleet_retry_upload(
|
|
291
|
+
server: str = typer.Option(..., "--server", help="Inventory server number or exact name."),
|
|
292
|
+
handoff_id: UUID = typer.Option(..., "--handoff-id"),
|
|
293
|
+
transport: str = typer.Option("tailscale", "--transport", help=TRANSPORT_HELP),
|
|
294
|
+
write: bool = typer.Option(False, "--write", help="Retry only the reviewed source state."),
|
|
295
|
+
expected_revision: str | None = typer.Option(None, "--expected-revision"),
|
|
296
|
+
reason: str | None = typer.Option(None, "--reason"),
|
|
297
|
+
) -> None:
|
|
298
|
+
"""Preview a failed card, or retry its exact marker revision after repair."""
|
|
299
|
+
if write and (not expected_revision or not (reason or "").strip()):
|
|
300
|
+
raise typer.BadParameter("--write requires --expected-revision and --reason")
|
|
301
|
+
engine = _ingest_engine()
|
|
302
|
+
try:
|
|
303
|
+
payload = engine.retry_mono_upload(
|
|
304
|
+
server=server,
|
|
305
|
+
handoff_id=str(handoff_id),
|
|
306
|
+
transport=transport,
|
|
307
|
+
write=write,
|
|
308
|
+
expected_revision=expected_revision,
|
|
309
|
+
reason=reason,
|
|
310
|
+
)
|
|
311
|
+
except ValueError as exc:
|
|
312
|
+
payload = {"error": {"code": "invalid_request", "message": str(exc)}}
|
|
313
|
+
sys.stdout.write(json.dumps(payload, indent=2) + "\n")
|
|
314
|
+
if "error" in payload:
|
|
315
|
+
raise typer.Exit(1)
|
|
316
|
+
|
|
317
|
+
|
|
288
318
|
@fleet_app.command("preflight")
|
|
289
319
|
def fleet_preflight(
|
|
290
320
|
scope: str = typer.Option(
|
buildai_cli-0.3.149/AGENTS.md
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
# BuildAI CLI — Typer CLI with standalone (API-backed) and workspace (DB-direct) modes.
|
|
2
|
-
|
|
3
|
-
## Read first
|
|
4
|
-
|
|
5
|
-
- [../../../AGENTS.md](../../../AGENTS.md)
|
|
6
|
-
- Docs site Engineering section (`make docs`)
|
|
7
|
-
- Docs site Operations > Database roles (`make docs`)
|
|
8
|
-
|
|
9
|
-
## Rules
|
|
10
|
-
|
|
11
|
-
**Two planes.**
|
|
12
|
-
|
|
13
|
-
| Mode | Install | Auth | DB access |
|
|
14
|
-
|------|---------|------|-----------|
|
|
15
|
-
| Standalone (`buildai`) | `uv tool install buildai-cli` | API key / JWT | Through API |
|
|
16
|
-
| Workspace (`uv run buildai`) | Repo-local editable install | IAM / password | Direct via `db`, through profile-keyed local broker |
|
|
17
|
-
|
|
18
|
-
**DB guards.** `db` subcommands require workspace install + gcloud IAM. Inspection/migration uses a profile-keyed `alloydb-auth-proxy` broker with state in `~/.buildai/db-brokers` (shared across worktrees). Use `buildai db broker status|ensure|stop` for local transport.
|
|
19
|
-
|
|
20
|
-
**Writes.** Require `--write`. Production migrations prompt for confirmation.
|
|
21
|
-
|
|
22
|
-
**Frame-label totals.** A `buildai db query` that counts
|
|
23
|
-
`labeling.frame_labels` by `status` and `label_mode` must use
|
|
24
|
-
`MATERIALIZED` then filter, or `COUNT(*) FILTER`. Do not push those
|
|
25
|
-
predicates into the scan. See
|
|
26
|
-
[core/dal/labeling/AGENTS.md](../../../core/dal/labeling/AGENTS.md).
|
|
27
|
-
|
|
28
|
-
**Targeting.** Worktree app/runtime targeting comes from explicit env vars and the repo Makefile, not a saved CLI context layer. `buildai db --env` selects `production`, `staging`, or `dev`.
|
|
29
|
-
|
|
30
|
-
**Staging.** Private-IP only. Start `buildai db --env staging tunnel`, then pass `--all-proxy socks5://127.0.0.1:1080` for queries.
|
|
31
|
-
|
|
32
|
-
**Workers.** Call the API through the SDK; do not import `core/` or access the database directly from worker shells.
|
|
33
|
-
|
|
34
|
-
## Verify
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
buildai auth whoami
|
|
38
|
-
uv run buildai ingest deploy plan --scope active
|
|
39
|
-
buildai db broker status
|
|
40
|
-
buildai db schema tables
|
|
41
|
-
buildai db query "SELECT count(*) FROM core.clips"
|
|
42
|
-
buildai db --env staging tunnel # in one terminal; query with --all-proxy in another
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
## Don't
|
|
46
|
-
|
|
47
|
-
- Run `db --write` without `--write`.
|
|
48
|
-
- Query staging without an IAP/VPN/SOCKS route and `--all-proxy`.
|
|
49
|
-
- Assume a saved CLI context picks the worktree's DB lane — use env vars and Makefile.
|
|
50
|
-
- Deploy ingest fleet changes from an unclear branch or dirty checkout.
|
|
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
|
|
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
|
{buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/rescaling_migration/sql/legacy_export_closeout.sql
RENAMED
|
File without changes
|
{buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/rescaling_migration/sql/legacy_export_prepare.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/rescaling_migration/sql/v2_export_closeout.sql
RENAMED
|
File without changes
|
{buildai_cli-0.3.149 → buildai_cli-0.3.151}/cli/rescaling_migration/sql/v2_export_prepare.sql
RENAMED
|
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
|