# VibeCoder CLI — Design Document **Sprint:** 7a (design + skeleton + first recipe) **Date:** 2026-09-16 **Lane:** id 9 (vibecoder-standalone-mavis) **Status:** Phase 2 — design only; skeleton + provider-setup recipe land in the same commit. --- ## TL;DR — what this CLI is A single `vibecoder` binary, installable via `npm link`, that takes a fresh Linux box and walks it through the 10 bootstrap steps from the gap memo. It is meta-circular: VibeCoder installs itself. The full bootstrap CLI is 2–3 weeks of work across multiple sub-slices (7a–7d). **Sprint 7a ships:** the command surface, the entry-point + dispatch, the `status` sub-command (read-only, real), and the `recipe provider-setup` recipe (first end-to-end recipe). The remaining recipes (`pb-init`, `proxy-install`, `service-install`, `watchdog-install`, `db-migrate`, `doctrine-bootstrap`, `gallery-fetch`, `coordination-bundle`) land in subsequent slices. The CLI is a **tool, not a service**. It is invoked from a shell by the operator (or a self-bootstrap script). It does NOT daemonize, does NOT bind a port, does NOT run on the bridge. The bridge remains the long-running surface; the CLI is the one-shot setup surface. --- ## 1. Command surface ``` vibecoder [--version] [--help] vibecoder init # full bootstrap (NOT IMPLEMENTED in 7a — prints "not yet") vibecoder provision # run a single provision recipe by name vibecoder doctor # health check of current instance (NOT IMPLEMENTED in 7a) vibecoder status # print current state: env, providers, DB, service (REAL in 7a) vibecoder recipe # alias for `vibecoder provision ` ``` ### 1a. Global flags | Flag | Effect | |---|---| | `--help`, `-h` | Print help and exit 0 | | `--version`, `-v` | Print version (from package.json) and exit 0 | | `--dry-run` | Print plan, skip writes. Recipe-specific — see §4. | | `--non-interactive` | Don't prompt. Read from env vars or fail. | | `--target ` | Override default target dir (default: `/opt/vibecoder-bridge`). | | `--json` | Emit machine-readable JSON to stdout (status, doctor only). | | `--verbose` | Verbose logging to stderr. | ### 1b. Per-command spec #### `vibecoder status` (REAL in 7a) Prints current instance state. Read-only, no writes. Calls: - `cat /opt/vibecoder-bridge/.env | grep -E "^[A-Z_]+="` → env vars (masked: show key name, last 4 chars only) - `curl -s http://127.0.0.1:3003/api/cascade/active-accounts -H "Authorization: Bearer $BRIDGE_TOKEN"` → cascade state - `systemctl is-active vibecoder-bridge` (if systemd unit exists) - `node -e "require('/opt/vibecoder-bridge/bridge/lib/env.cjs')"` for sanity - `which sqlite3 && sqlite3 /opt/operator/data/panel.db ".tables"` for DB schema presence Exit codes: - `0` — instance appears healthy (all probes succeed) - `1` — one or more probes failed - `2` — bridge not running - `3` — panel.db missing or unreadable JSON output (`--json`): ```json { "version": "0.1.0", "bridge_dir": "/opt/vibecoder-bridge", "bridge_running": true, "bridge_pid": 1234, "cascade_accounts": ["1"], "env_keys_present": ["GROQ_API_KEY", "NVIDIA_API_KEY", "BRIDGE_TOKEN", "PB_URL"], "env_keys_missing": ["GLM_API_KEY"], "panel_db_present": true, "panel_db_tables": ["vibechat_model_usage", "vibechat_prompt_archive", "hq_threads", "hq_bulletins"], "service_unit": "/etc/systemd/system/vibecoder-bridge.service", "service_unit_present": true, "service_unit_active": "active" } ``` #### `vibecoder recipe provider-setup` (REAL in 7a — first recipe) See §4. Collects provider keys, validates each with a 1-request smoke test, writes to `/opt/vibecoder-bridge/.env` and `~/.mavis/env`. #### `vibecoder init` (NOT YET) Orchestrates all 10 steps in order: provider-setup → pb-init → proxy-install → service-install → watchdog-install → db-migrate → doctrine-bootstrap → gallery-fetch → coordination-bundle → verify. Each step is a recipe. `init` is the runner that invokes them in sequence, stops on first failure (unless `--keep-going`), and reports a summary. Implementation lives in `recipes/init-runner.cjs` (7c scope). #### `vibecoder provision ` (PARTIAL in 7a — dispatches but only provider-setup implemented) ``` vibecoder provision provider-setup # REAL in 7a vibecoder provision pb-init # 7b scope vibecoder provision proxy-install # 7c scope vibecoder provision service-install # 7c scope vibecoder provision watchdog-install # 7c scope vibecoder provision db-migrate # 7d scope vibecoder provision doctrine-bootstrap # 7d scope vibecoder provision gallery-fetch # 7d scope vibecoder provision coordination-bundle # future ``` Unknown recipe name → exit 1 with message "Recipe not implemented. See docs/vibecoder-cli-design.md for the full list." --- ## 2. The 10 bootstrap steps → CLI sub-commands From the gap memo (`docs/audits/whats-missing-for-vibecoder-to-build-vibecoder.md`, Gap 1, table on lines 30–40): | # | Memo step | CLI recipe | Sprint | Needs root? | Operator required? | |---|---|---|---|---|---| | 1 | Acquire seed | (none — `npm link` covers this) | 7a | no | no | | 2 | Provision accounts | `provider-setup` | **7a (this slice)** | reads only | yes (pastes keys) | | 3 | Provision PocketBase | `pb-init` | 7b | yes | yes (PB admin token) | | 4 | Provision nginx + SSL | `proxy-install` | 7c | yes | yes (domain + certbot email) | | 5 | Provision systemd | `service-install` | 7c | yes | no | | 6 | Provision watchdog | `watchdog-install` | 7c | yes (writes cron) | no | | 7 | Provision panel.db schema | `db-migrate` | 7d | yes (writes DB) | no | | 8 | Bootstrap doctrine | `doctrine-bootstrap` | 7d | no (git tree writes) | no | | 9 | Bootstrap gallery | `gallery-fetch` | 7d | no | yes (which modules to vendor) | | 10 | Bootstrap coordination | `coordination-bundle` | future | no | yes (id 44 / id 59 endpoints) | **Why split by sprint?** Each recipe has different "needs root?" / "needs operator?" semantics. Recipes that write to `/opt/`, `/etc/`, or systemd need `sudo`. Recipes that write to the project tree or `.env` don't. Recipes that ask the operator for secrets can't run in `--non-interactive` mode without pre-loaded env vars. The CLI is **explicit** about which step needs which: - Recipes needing root check `process.geteuid() === 0` at start, exit 1 with a clear message if not. - Recipes needing operator input check for `--non-interactive` and the corresponding env var. - Recipes needing neither run silently in `--non-interactive` mode. --- ## 3. Idempotency Every recipe MUST be safely re-runnable. The pattern: ``` 1. Check current state (does the file / env / unit exist?) 2. If yes: ask "keep or overwrite?" (interactive) or fail (--non-interactive unless --force) 3. If no: create it 4. Verify the write (smoke test, file parse, unit reload) ``` `provider-setup` exemplifies this: - Reads existing `.env`. If a key is already present, asks "keep or overwrite?" — never silently overwrites. - Smoke-tests each key with a 1-request call. If the smoke test fails, the write is rolled back (env file restored to pre-run state). - On re-run: only writes the keys that the operator chose to overwrite. All recipes follow the same shape. Idempotency tests live alongside each recipe in `recipes/test-.sh`. --- ## 4. Dry-run mode `--dry-run` prints the recipe's plan without writing anything. Used by: - The operator to preview what `vibecoder init` would do before committing. - Tests to assert the write sequence without touching the filesystem. - Self-bootstrap scripts that want to log the plan and then invoke the real run. For `provider-setup`: ``` $ vibecoder recipe provider-setup --dry-run PLAN: GROQ_API_KEY not present, would prompt interactively (or read $PROMPT_GROQ_API_KEY) GROQ_API_KEY_2 not present, would prompt NVIDIA_API_KEY not present, would prompt NVIDIA_API_KEY_2 not present, would prompt GEMINI_API_KEY not present, would prompt WRITE PLAN: /opt/vibecoder-bridge/.env (chmod 600, append/update GROQ_API_KEY + NVIDIA_API_KEY + GEMINI_API_KEY) ~/.mavis/env (chmod 600, append/update same keys for operator user) VERIFY PLAN: smoke GROQ with model=openai/gpt-oss-20b (expects 200 + non-empty content) smoke NVIDIA with model=nvidia/nemotron-3-nano-omni-30b-a3b-reasoning (expects 200) smoke GEMINI with prompt 'OK' (expects 200 + non-empty content) NO WRITES PERFORMED (dry-run). ``` --- ## 5. Invoking user vs root vs operator The CLI runs as the **invoking user**. Recipes that need root escalate via `sudo` (the script itself doesn't bundle sudo — the operator invokes `sudo vibecoder ...`). Recipes that need operator input stay in the terminal. The pattern is explicit per-recipe: ``` // At the top of recipes/.js: const RECIPE_META = { needs_root: true | false, needs_operator_input: true | false, // ... }; ``` `bin/vibecoder.js` checks these flags before running the recipe and prints a clear error if the environment doesn't match. --- ## 6. What the CLI can do alone vs needs the operator **Alone (no operator):** - Print status (`vibecoder status`) - Detect already-present state (every recipe's idempotency check) - Re-write idempotent state without prompts (`--non-interactive --force`) - Smoke-test keys (calls provider APIs with the supplied keys) - Verify writes (reload systemd, test nginx config, etc.) **Needs operator:** - Paste keys (interactive prompts) - Confirm overwrite of existing state (interactive confirm, unless --force) - Provide PB admin token (env var or interactive prompt) - Provide domain name + certbot email (for nginx + SSL) - Choose which gallery modules to vendor The split is documented per-recipe in `RECIPE_META`. Recipes that can run alone print `[OK] no operator input needed` at the start of their interactive flow. --- ## 7. Files created in Sprint 7a | Path | Purpose | |---|---| | `bin/vibecoder.js` | CLI entry point — arg parsing, command dispatch | | `recipes/provider-setup.js` | First recipe — provider keys + smoke tests | | `recipes/_meta.js` | RECIPE_META helpers + shared smoke-test utilities (optional, may inline in 7a) | | `docs/vibecoder-cli-design.md` | This document | | `package.json` | Added `bin: { vibecoder: './bin/vibecoder.js' }` | Files created in subsequent slices (NOT in 7a): - `bin/vibecoder-init-runner.js` (7c — orchestrates `init`) - `recipes/pb-init.js`, `recipes/proxy-install.js`, `recipes/service-install.js`, `recipes/watchdog-install.js` (7b/7c) - `recipes/db-migrate.js`, `recipes/doctrine-bootstrap.js`, `recipes/gallery-fetch.js` (7d) - `recipes/test-*.sh` (alongside each recipe) --- ## 8. Sprint 7a commit The first commit adds `bin/vibecoder.js` + `recipes/provider-setup.js` + this design doc + the package.json `bin` entry. Single commit, single push, NO deploy (the CLI is a tool, not a service). The VPS does not need to be touched. The CLI lives in the repo; `npm link` (or `npm install -g` after publish) makes `vibecoder` available on the operator's machine. --- ## 9. Open questions for the operator 1. **Recipe naming convention** — `provider-setup` (kebab-case) or `providerSetup` (camelCase)? I'm using kebab-case to match CLI argument style. Confirm. 2. **Where does `vibecoder` live once installed?** Global via `npm link` from the repo, or published to a private npm registry? If global, the repo path matters (`/workspace/vibecoder-standalone` vs a different checkout). For 7a I'll assume `npm link` from the repo. 3. **Does `vibecoder init` need `--non-interactive` from day one?** Yes — self-bootstrap scripts will need it. Sprint 7a's `provider-setup` recipe supports it; subsequent recipes must too. 4. **Do we ever ship the CLI as a binary (pkg / ncc)?** No for 7a. Ship as source. Revisit if `npm link` proves fragile. --- ## 10. Sub-slice plan for 7b / 7c / 7d (Sprint 7a proposal) The remaining 9 recipes split across three sprints. Each sub-slice is bounded (1–3 days each), independently shippable, and follows the same template as `provider-setup`. ### Sprint 7b — PB + bridge core (1–2 days) | Recipe | What it does | Needs root? | Needs operator? | Effort | Deps | |---|---|---|---|---|---| | `pb-init` | Stands up a local PocketBase at `127.0.0.1:8090`. Creates `vibechat_*` tables (`model_usage`, `prompt_archive`, `reviewer_passes`, `agent_activity`, `fallback_config`, `audit_trail`, `workspace_context`) and `hq_*` tables (`threads`, `heartbeats`, `bulletins`, `reports`, `prompts`, `repos`). Imports schema from `bridge/lib/db.cjs`. Smoke-tests with a `curl http://127.0.0.1:8090/api/health`. | yes (writes `/opt/vibecoder-bridge/pb/`) | yes (PB admin email/password) | 1–2 d | none | ### Sprint 7c — Service layer (2–3 days) | Recipe | What it does | Needs root? | Needs operator? | Effort | Deps | |---|---|---|---|---|---| | `proxy-install` | Writes `/etc/nginx/sites-available/vibecoder` + symlinks to `sites-enabled`. Uses Let's Encrypt via certbot (assumes Caddy is the TLS terminator upstream; nginx serves plain HTTP on `127.0.0.1:8081`). Idempotent: re-running reuses existing certs. | yes (writes `/etc/nginx/`) | yes (domain + certbot email) | 1 d | none | | `service-install` | Writes `/etc/systemd/system/vibecoder-bridge.service` (Type=simple, ExecStart=`node /opt/vibecoder-bridge/server.cjs`, Restart=on-failure, EnvironmentFile=`/opt/vibecoder-bridge/.env`). Runs `systemctl daemon-reload` + `systemctl enable --now`. Smoke-tests with `systemctl is-active`. | yes (writes `/etc/systemd/`) | no | 0.5 d | none | | `watchdog-install` | Writes `/opt/vibecoder-bridge/watchdog.sh` (checks bridge on port 3003, auto-restart if down) + `/etc/cron.d/vibecoder-watchdog` (`*/5 * * * *`). Idempotent: re-running overwrites with the canonical version. | yes (writes cron) | no | 0.5 d | `service-install` (so the watchdog knows what to restart) | | `init-runner` (for `vibecoder init`) | Orchestrates all recipes in order. Stops on first failure unless `--keep-going`. Prints a summary table at the end. | no | no | 1 d | all of the above | ### Sprint 7d — Data + doctrine + bundle (2–3 days) | Recipe | What it does | Needs root? | Needs operator? | Effort | Deps | |---|---|---|---|---|---| | `db-migrate` | Runs the SQLite migrations in `bridge/lib/db.cjs` against `/opt/operator/data/panel.db`. Idempotent: each migration checks `IF NOT EXISTS`. Writes a `migration_log` row per run. | yes (writes DB) | no | 0.5 d | `pb-init` (so the DB file exists) | | `doctrine-bootstrap` | Creates `gallery-pact/fvw/.pact/fvw-amendments/` skeleton + writes a `current-ratification.json` listing the latest FvW version (v8.6.0). Idempotent: never overwrites an existing ratification. | no | no | 0.5 d | none | | `gallery-fetch` | Reads `app-recipe/manifest.json`'s `fv-module-gallery` block. Fetches the upstream repo at the pinned ref. Vends the listed modules into `app-vgm/`. Idempotent: `git pull` is safe, vendor copies are deterministic. | no | yes (which modules to vendor) | 1 d | none | | `coordination-bundle` | Sets `HQ_URL` env var + writes a `coordination.json` file mapping the bundle's id 44 + id 59 endpoints. Smoke-tests with `curl $HQ_URL/api/mavis/me`. | no | yes (id 44 / id 59 endpoints) | 1 d | `db-migrate` (HQ writes to panel.db) | ### Recommended sequence 1. **7b → `pb-init`** (1–2 days). PB is the foundation for `db-migrate` + `coordination-bundle`. Without it, those recipes can't run. 2. **7c → `service-install` + `watchdog-install` + `proxy-install` + `init-runner`** (2–3 days). The service layer is what makes the bridge persistent. `init-runner` unblocks end-to-end testing of `vibecoder init`. 3. **7d → `db-migrate` + `doctrine-bootstrap` + `gallery-fetch` + `coordination-bundle`** (2–3 days). The data + content + coordination layer. **Total remaining:** ~5–8 days for the remaining 8 recipes (excluding `init-runner`, which is the unblocker for testing). **Estimated calendar:** 7b in 1–2 days, 7c in 2–3 days, 7d in 2–3 days. Each dispatch ships 1–2 recipes. Total ~3–4 follow-up dispatches. ### What ships AFTER Sprint 7 Sprint 8 (FVRE invocation + self-recipe) is independent of the CLI — the CLI is a tool, FVRE invocation is a bridge-side feature. Sprint 9 (operator-mavis + reasoning-mavis bundling) is the meta-circular concern: once `vibecoder init` is complete, the bundle can host its own coordination. Sprint 10 (Phase B bridge refactor) is independent of the CLI — the modular bridge work continues. Sprint 7a + 7b + 7c + 7d = a complete `vibecoder init` that turns a fresh Linux box into a running, watchdogged, nginx-fronted VibeCoder instance with provider keys + PB + doctrine + gallery + coordination. Self-build unlocked.