diff --git a/content/docs/reference/cli/auth.mdx b/content/docs/reference/cli/auth.mdx
new file mode 100644
index 0000000..2ba2a00
--- /dev/null
+++ b/content/docs/reference/cli/auth.mdx
@@ -0,0 +1,88 @@
+---
+title: stash auth
+description: "Reference for the `stash auth` commands."
+type: reference
+components: [cli]
+verifiedAgainst:
+ cli: "0.17.0"
+---
+
+{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash manifest --json` (v0.17.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */}
+
+
+Generated from **`stash` v0.17.0** via `npx stash@0.17.0 manifest --json`. Run `npx stash@0.17.0 --help` to see the live command surface.
+
+
+The `stash auth` command group.
+
+### `auth login`
+
+Authenticate with CipherStash
+
+```bash
+npx stash auth login [flags]
+```
+
+#### Flags
+
+| Flag | Description |
+| --- | --- |
+| `--region ` | Region to authenticate against (e.g. us-east-1). Skips the interactive region picker. (env: `STASH_REGION`) |
+| `--json` | Emit newline-delimited JSON events instead of prose. The first event (authorization_required) carries the device verification URL for a human to open. Implies no prompt and no browser auto-open. |
+| `--no-open` | Don't auto-open the verification URL in a browser (already implied by --json). |
+| `--supabase` | Track Supabase as the referrer. |
+| `--drizzle` | Track Drizzle as the referrer. |
+
+
+#### Examples
+
+```bash
+npx stash auth login
+npx stash auth login --region us-east-1
+npx stash auth login --supabase
+npx stash auth login --region us-east-1 --json
+```
+
+### `auth regions`
+
+List the regions you can authenticate against
+
+```bash
+npx stash auth regions [flags]
+```
+
+#### Flags
+
+| Flag | Description |
+| --- | --- |
+| `--json` | Emit machine-readable [\{ slug, label \}] instead of a text list. |
+
+
+#### Examples
+
+```bash
+npx stash auth regions
+npx stash auth regions --json
+```
+
+## How authentication works
+
+`stash auth login` runs the OAuth 2.0 **device authorization flow**:
+
+1. You pick a **region** for your CipherStash workspace.
+2. The CLI opens your browser to a verification URL (and prints it, so it also
+ works over SSH or in a headless/agent environment) where you approve the
+ request.
+3. Meanwhile the CLI polls CipherStash until you approve, then receives a
+ short-lived access token (it reports the token's expiry).
+4. Your device is **bound to the workspace's default keyset**, so later commands
+ (`stash eql install`, `stash db push`, …) authenticate without a fresh login.
+
+
+Login is device- and workspace-scoped. Authenticating from a new machine, or for a different workspace, re-runs the device flow.
+
+
+{/* TODO(verify with product): profiles, multiple workspaces, and switching
+between them (where they're stored and how they're selected) belong here, or
+in a linked CLI concept page. The CLI currently exposes only `auth login`;
+confirm the profile / workspace-switching surface before documenting it. */}
diff --git a/content/docs/reference/cli/db.mdx b/content/docs/reference/cli/db.mdx
new file mode 100644
index 0000000..e0c26b2
--- /dev/null
+++ b/content/docs/reference/cli/db.mdx
@@ -0,0 +1,86 @@
+---
+title: stash db
+description: "Reference for the `stash db` commands."
+type: reference
+components: [cli, eql]
+verifiedAgainst:
+ cli: "0.17.0"
+---
+
+{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash manifest --json` (v0.17.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */}
+
+
+Generated from **`stash` v0.17.0** via `npx stash@0.17.0 manifest --json`. Run `npx stash@0.17.0 --help` to see the live command surface.
+
+
+The `stash db` command group.
+
+### `db push`
+
+Push encryption schema (writes pending if active config already exists)
+
+```bash
+npx stash db push [flags]
+```
+
+#### Flags
+
+| Flag | Description |
+| --- | --- |
+| `--dry-run` | Show what would happen without making changes. |
+| `--database-url ` | Override DATABASE_URL for this run only — never written to disk. (env: `DATABASE_URL`) |
+
+
+### `db activate`
+
+Promote pending → active without renames (use after additive db push)
+
+```bash
+npx stash db activate [flags]
+```
+
+#### Flags
+
+| Flag | Description |
+| --- | --- |
+| `--database-url ` | Override DATABASE_URL for this run only — never written to disk. (env: `DATABASE_URL`) |
+
+
+### `db validate`
+
+Validate encryption schema
+
+```bash
+npx stash db validate [flags]
+```
+
+#### Flags
+
+| Flag | Description |
+| --- | --- |
+| `--supabase` | Use Supabase-compatible mode. |
+| `--exclude-operator-family` | Skip operator family creation. |
+| `--database-url ` | Override DATABASE_URL for this run only — never written to disk. (env: `DATABASE_URL`) |
+
+
+### `db migrate`
+
+Run pending encrypt config migrations (not yet implemented)
+
+```bash
+npx stash db migrate
+```
+
+### `db test-connection`
+
+Test database connectivity
+
+```bash
+npx stash db test-connection [flags]
+```
+
+#### Flags
+
+| Flag | Description |
+| --- | --- |
+| `--database-url ` | Override DATABASE_URL for this run only — never written to disk. (env: `DATABASE_URL`) |
diff --git a/content/docs/reference/cli/doctor.mdx b/content/docs/reference/cli/doctor.mdx
new file mode 100644
index 0000000..1c6efb8
--- /dev/null
+++ b/content/docs/reference/cli/doctor.mdx
@@ -0,0 +1,20 @@
+---
+title: stash doctor
+description: "Diagnose install problems (native binaries, runtime)"
+type: reference
+components: [cli]
+verifiedAgainst:
+ cli: "0.17.0"
+---
+
+{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash manifest --json` (v0.17.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */}
+
+
+Generated from **`stash` v0.17.0** via `npx stash@0.17.0 manifest --json`. Run `npx stash@0.17.0 --help` to see the live command surface.
+
+
+Diagnose install problems (native binaries, runtime)
+
+```bash
+npx stash doctor
+```
diff --git a/content/docs/reference/cli/encrypt.mdx b/content/docs/reference/cli/encrypt.mdx
new file mode 100644
index 0000000..9c03ae8
--- /dev/null
+++ b/content/docs/reference/cli/encrypt.mdx
@@ -0,0 +1,88 @@
+---
+title: stash encrypt
+description: "Reference for the `stash encrypt` commands."
+type: reference
+components: [cli, eql]
+verifiedAgainst:
+ cli: "0.17.0"
+---
+
+{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash manifest --json` (v0.17.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */}
+
+
+Generated from **`stash` v0.17.0** via `npx stash@0.17.0 manifest --json`. Run `npx stash@0.17.0 --help` to see the live command surface.
+
+
+The `stash encrypt` command group.
+
+### `encrypt status`
+
+Show per-column migration status (phase, progress, drift)
+
+```bash
+npx stash encrypt status
+```
+
+### `encrypt plan`
+
+Diff intent (.cipherstash/migrations.json) vs observed state
+
+```bash
+npx stash encrypt plan
+```
+
+### `encrypt backfill`
+
+Resumably encrypt plaintext into the encrypted column
+
+```bash
+npx stash encrypt backfill [flags]
+```
+
+#### Flags
+
+| Flag | Description |
+| --- | --- |
+| `--table ` | Target table. |
+| `--column ` | Target column. |
+| `--pk-column ` | Primary-key column used to page through rows. |
+| `--chunk-size ` | Rows encrypted per batch. |
+| `--encrypted-column ` | Destination encrypted column. |
+| `--schema-column-key ` | Schema key identifying the column config. |
+| `--confirm-dual-writes-deployed` | Assert the app is dual-writing before backfilling (safety gate). |
+| `--force` | Proceed past non-fatal safety checks. |
+
+
+### `encrypt cutover`
+
+Rename swap encrypted → primary column
+
+```bash
+npx stash encrypt cutover [flags]
+```
+
+#### Flags
+
+| Flag | Description |
+| --- | --- |
+| `--table ` | Target table. |
+| `--column ` | Target column. |
+| `--proxy-url ` | Proxy URL to verify against. |
+| `--migrations-dir ` | Directory to write the rename migration into. |
+
+
+### `encrypt drop`
+
+Generate a migration to drop the plaintext column
+
+```bash
+npx stash encrypt drop [flags]
+```
+
+#### Flags
+
+| Flag | Description |
+| --- | --- |
+| `--table ` | Target table. |
+| `--column ` | Target column. |
+| `--migrations-dir ` | Directory to write the drop migration into. |
diff --git a/content/docs/reference/cli/env.mdx b/content/docs/reference/cli/env.mdx
new file mode 100644
index 0000000..fe277a9
--- /dev/null
+++ b/content/docs/reference/cli/env.mdx
@@ -0,0 +1,26 @@
+---
+title: stash env
+description: "(experimental) Print production env vars for deployment"
+type: reference
+components: [cli]
+verifiedAgainst:
+ cli: "0.17.0"
+---
+
+{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash manifest --json` (v0.17.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */}
+
+
+Generated from **`stash` v0.17.0** via `npx stash@0.17.0 manifest --json`. Run `npx stash@0.17.0 --help` to see the live command surface.
+
+
+(experimental) Print production env vars for deployment
+
+```bash
+npx stash env [flags]
+```
+
+### Flags
+
+| Flag | Description |
+| --- | --- |
+| `--write` | Write the vars to a file instead of printing them. |
diff --git a/content/docs/reference/cli/eql.mdx b/content/docs/reference/cli/eql.mdx
new file mode 100644
index 0000000..63bf8be
--- /dev/null
+++ b/content/docs/reference/cli/eql.mdx
@@ -0,0 +1,77 @@
+---
+title: stash eql
+description: "Reference for the `stash eql` commands."
+type: reference
+components: [cli, eql]
+verifiedAgainst:
+ cli: "0.17.0"
+---
+
+{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash manifest --json` (v0.17.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */}
+
+
+Generated from **`stash` v0.17.0** via `npx stash@0.17.0 manifest --json`. Run `npx stash@0.17.0 --help` to see the live command surface.
+
+
+The `stash eql` command group.
+
+### `eql install`
+
+Scaffold stash.config.ts (if missing) and install EQL extensions
+
+```bash
+npx stash eql install [flags]
+```
+
+#### Flags
+
+| Flag | Description |
+| --- | --- |
+| `--force` | Reinstall / overwrite even if already installed. |
+| `--dry-run` | Show what would happen without making changes. |
+| `--supabase` | Use Supabase-compatible mode (auto-detected from DATABASE_URL). |
+| `--drizzle` | Generate a Drizzle migration instead of direct install (auto-detected from project). |
+| `--migration` | Write a Supabase migration file instead of running SQL directly (requires --supabase). |
+| `--direct` | Run the SQL directly against the database (requires --supabase; mutually exclusive with --migration). |
+| `--migrations-dir ` | Override the Supabase migrations directory (requires --supabase). (default: `supabase/migrations`) |
+| `--exclude-operator-family` | Skip operator family creation. |
+| `--eql-version <2\|3>` | EQL generation to target. v3 is the native eql_v3.* domain schema (direct install only for now). (default: `2`) |
+| `--latest` | Fetch the latest EQL from GitHub (v2 only). |
+| `--name ` | With --drizzle: name for the generated migration (defaults to a scaffold name). |
+| `--out ` | With --drizzle: directory to write the generated migration into. |
+| `--database-url ` | Override DATABASE_URL for this run only — never written to disk. (env: `DATABASE_URL`) |
+
+
+### `eql upgrade`
+
+Upgrade EQL extensions to the latest version
+
+```bash
+npx stash eql upgrade [flags]
+```
+
+#### Flags
+
+| Flag | Description |
+| --- | --- |
+| `--dry-run` | Show what would happen without making changes. |
+| `--supabase` | Use Supabase-compatible mode. |
+| `--exclude-operator-family` | Skip operator family creation. |
+| `--eql-version <2\|3>` | EQL generation to target. (default: `2`) |
+| `--latest` | Fetch the latest EQL from GitHub (v2 only). |
+| `--database-url ` | Override DATABASE_URL for this run only — never written to disk. (env: `DATABASE_URL`) |
+
+
+### `eql status`
+
+Show EQL installation status
+
+```bash
+npx stash eql status [flags]
+```
+
+#### Flags
+
+| Flag | Description |
+| --- | --- |
+| `--database-url ` | Override DATABASE_URL for this run only — never written to disk. (env: `DATABASE_URL`) |
diff --git a/content/docs/reference/cli/impl.mdx b/content/docs/reference/cli/impl.mdx
new file mode 100644
index 0000000..b3ce114
--- /dev/null
+++ b/content/docs/reference/cli/impl.mdx
@@ -0,0 +1,36 @@
+---
+title: stash impl
+description: "Execute the plan with a local agent"
+type: reference
+components: [cli]
+verifiedAgainst:
+ cli: "0.17.0"
+---
+
+{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash manifest --json` (v0.17.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */}
+
+
+Generated from **`stash` v0.17.0** via `npx stash@0.17.0 manifest --json`. Run `npx stash@0.17.0 --help` to see the live command surface.
+
+
+Execute the plan with a local agent
+
+```bash
+npx stash impl [flags]
+```
+
+### Flags
+
+| Flag | Description |
+| --- | --- |
+| `--continue-without-plan` | Skip planning and go straight to implementation (interactively confirms before proceeding). |
+| `--target ` | Skip the agent-target picker and hand off directly to one of claude-code \| codex \| agents-md \| wizard. Safe in non-TTY contexts. |
+
+
+## Examples
+
+```bash
+npx stash impl
+npx stash impl --continue-without-plan
+npx stash impl --target claude-code
+```
diff --git a/content/docs/reference/cli/index.mdx b/content/docs/reference/cli/index.mdx
index 8eeffd3..88764db 100644
--- a/content/docs/reference/cli/index.mdx
+++ b/content/docs/reference/cli/index.mdx
@@ -1,8 +1,75 @@
---
title: CLI
-description: "CLI documentation — being built as part of the docs V2 overhaul."
+description: "Command reference for the stash CLI, generated from v0.17.0."
+type: reference
+components: [cli]
+verifiedAgainst:
+ cli: "0.17.0"
---
-This section is being built as part of the docs V2 overhaul ([CIP-3307](https://linear.app/cipherstash/issue/CIP-3307)). Track progress in [IA.md](https://github.com/cipherstash/docs/blob/v2/IA.md).
+{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash manifest --json` (v0.17.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */}
-Until it lands, current documentation lives in the [existing docs](/stack).
+
+Generated from **`stash` v0.17.0** via `npx stash@0.17.0 manifest --json`. Run `npx stash@0.17.0 --help` to see the live command surface.
+
+
+The `stash` CLI. Install with `npx stash@0.17.0`. Every command accepts `--help` and `--version`.
+
+### Setup & workflow
+
+| Command | Description |
+| --- | --- |
+| [`init`](/reference/cli/init) | Initialize CipherStash for your project |
+| [`plan`](/reference/cli/plan) | Draft a reviewable encryption plan at .cipherstash/plan.md |
+| [`impl`](/reference/cli/impl) | Execute the plan with a local agent |
+| [`status`](/reference/cli/status) | Displays implementation status |
+| [`wizard`](/reference/cli/wizard) | AI-guided encryption setup (reads your codebase) |
+| [`doctor`](/reference/cli/doctor) | Diagnose install problems (native binaries, runtime) |
+| [`manifest`](/reference/cli/manifest) | Print the structured, versioned command surface |
+
+### Auth
+
+| Command | Description |
+| --- | --- |
+| [`auth login`](/reference/cli/auth#auth-login) | Authenticate with CipherStash |
+| [`auth regions`](/reference/cli/auth#auth-regions) | List the regions you can authenticate against |
+
+### EQL
+
+| Command | Description |
+| --- | --- |
+| [`eql install`](/reference/cli/eql#eql-install) | Scaffold stash.config.ts (if missing) and install EQL extensions |
+| [`eql upgrade`](/reference/cli/eql#eql-upgrade) | Upgrade EQL extensions to the latest version |
+| [`eql status`](/reference/cli/eql#eql-status) | Show EQL installation status |
+
+### Database
+
+| Command | Description |
+| --- | --- |
+| [`db push`](/reference/cli/db#db-push) | Push encryption schema (writes pending if active config already exists) |
+| [`db activate`](/reference/cli/db#db-activate) | Promote pending → active without renames (use after additive db push) |
+| [`db validate`](/reference/cli/db#db-validate) | Validate encryption schema |
+| [`db migrate`](/reference/cli/db#db-migrate) | Run pending encrypt config migrations (not yet implemented) |
+| [`db test-connection`](/reference/cli/db#db-test-connection) | Test database connectivity |
+
+### Schema
+
+| Command | Description |
+| --- | --- |
+| [`schema build`](/reference/cli/schema#schema-build) | Build an encryption schema from your database |
+
+### Encrypt
+
+| Command | Description |
+| --- | --- |
+| [`encrypt status`](/reference/cli/encrypt#encrypt-status) | Show per-column migration status (phase, progress, drift) |
+| [`encrypt plan`](/reference/cli/encrypt#encrypt-plan) | Diff intent (.cipherstash/migrations.json) vs observed state |
+| [`encrypt backfill`](/reference/cli/encrypt#encrypt-backfill) | Resumably encrypt plaintext into the encrypted column |
+| [`encrypt cutover`](/reference/cli/encrypt#encrypt-cutover) | Rename swap encrypted → primary column |
+| [`encrypt drop`](/reference/cli/encrypt#encrypt-drop) | Generate a migration to drop the plaintext column |
+
+### Experimental
+
+| Command | Description |
+| --- | --- |
+| [`env`](/reference/cli/env) | (experimental) Print production env vars for deployment |
diff --git a/content/docs/reference/cli/init.mdx b/content/docs/reference/cli/init.mdx
new file mode 100644
index 0000000..518544b
--- /dev/null
+++ b/content/docs/reference/cli/init.mdx
@@ -0,0 +1,41 @@
+---
+title: stash init
+description: "Initialize CipherStash for your project"
+type: reference
+components: [cli]
+verifiedAgainst:
+ cli: "0.17.0"
+---
+
+{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash manifest --json` (v0.17.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */}
+
+
+Generated from **`stash` v0.17.0** via `npx stash@0.17.0 manifest --json`. Run `npx stash@0.17.0 --help` to see the live command surface.
+
+
+Initialize CipherStash for your project
+
+```bash
+npx stash init [flags]
+```
+
+### Flags
+
+| Flag | Description |
+| --- | --- |
+| `--supabase` | Use Supabase-specific setup flow. |
+| `--drizzle` | Use Drizzle-specific setup flow. |
+| `--prisma-next` | Use Prisma Next-specific setup flow (EQL bundle installed via prisma-next migration apply). |
+| `--proxy` | Query encrypted data via CipherStash Proxy. |
+| `--no-proxy` | Query encrypted data directly via the SDK. (default: `true`) |
+| `--region ` | Region to authenticate against (e.g. us-east-1). Skips the interactive region picker. Required for non-interactive init when not already logged in. (env: `STASH_REGION`) |
+
+
+## Examples
+
+```bash
+npx stash init
+npx stash init --supabase
+npx stash init --prisma-next
+npx stash init --region us-east-1
+```
diff --git a/content/docs/reference/cli/manifest.mdx b/content/docs/reference/cli/manifest.mdx
new file mode 100644
index 0000000..4c7578d
--- /dev/null
+++ b/content/docs/reference/cli/manifest.mdx
@@ -0,0 +1,34 @@
+---
+title: stash manifest
+description: "Print the structured, versioned command surface"
+type: reference
+components: [cli]
+verifiedAgainst:
+ cli: "0.17.0"
+---
+
+{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash manifest --json` (v0.17.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */}
+
+
+Generated from **`stash` v0.17.0** via `npx stash@0.17.0 manifest --json`. Run `npx stash@0.17.0 --help` to see the live command surface.
+
+
+Print the structured, versioned command surface
+
+```bash
+npx stash manifest [flags]
+```
+
+### Flags
+
+| Flag | Description |
+| --- | --- |
+| `--json` | Emit the structured JSON manifest instead of a text list. |
+
+
+## Examples
+
+```bash
+npx stash manifest --json
+npx stash manifest
+```
diff --git a/content/docs/reference/cli/meta.json b/content/docs/reference/cli/meta.json
index 0a67892..6b90890 100644
--- a/content/docs/reference/cli/meta.json
+++ b/content/docs/reference/cli/meta.json
@@ -1,4 +1,25 @@
{
"title": "CLI",
- "pages": ["..."]
+ "pages": [
+ "---Setup & workflow---",
+ "init",
+ "plan",
+ "impl",
+ "status",
+ "wizard",
+ "doctor",
+ "manifest",
+ "---Auth---",
+ "auth",
+ "---EQL---",
+ "eql",
+ "---Database---",
+ "db",
+ "---Schema---",
+ "schema",
+ "---Encrypt---",
+ "encrypt",
+ "---Experimental---",
+ "env"
+ ]
}
diff --git a/content/docs/reference/cli/plan.mdx b/content/docs/reference/cli/plan.mdx
new file mode 100644
index 0000000..4904f7a
--- /dev/null
+++ b/content/docs/reference/cli/plan.mdx
@@ -0,0 +1,35 @@
+---
+title: stash plan
+description: "Draft a reviewable encryption plan at .cipherstash/plan.md"
+type: reference
+components: [cli]
+verifiedAgainst:
+ cli: "0.17.0"
+---
+
+{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash manifest --json` (v0.17.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */}
+
+
+Generated from **`stash` v0.17.0** via `npx stash@0.17.0 manifest --json`. Run `npx stash@0.17.0 --help` to see the live command surface.
+
+
+Draft a reviewable encryption plan at .cipherstash/plan.md
+
+```bash
+npx stash plan [flags]
+```
+
+### Flags
+
+| Flag | Description |
+| --- | --- |
+| `--complete-rollout` | Plan the entire encryption lifecycle (schema-add through drop) in one document. Skips the production-deploy gate; only safe when this database is not backing a deployed application. |
+| `--target ` | Skip the agent-target picker and hand off directly to one of claude-code \| codex \| agents-md \| wizard. Safe in non-TTY contexts. |
+
+
+## Examples
+
+```bash
+npx stash plan
+npx stash plan --target claude-code
+```
diff --git a/content/docs/reference/cli/schema.mdx b/content/docs/reference/cli/schema.mdx
new file mode 100644
index 0000000..9b5e360
--- /dev/null
+++ b/content/docs/reference/cli/schema.mdx
@@ -0,0 +1,31 @@
+---
+title: stash schema
+description: "Reference for the `stash schema` commands."
+type: reference
+components: [cli, eql]
+verifiedAgainst:
+ cli: "0.17.0"
+---
+
+{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash manifest --json` (v0.17.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */}
+
+
+Generated from **`stash` v0.17.0** via `npx stash@0.17.0 manifest --json`. Run `npx stash@0.17.0 --help` to see the live command surface.
+
+
+The `stash schema` command group.
+
+### `schema build`
+
+Build an encryption schema from your database
+
+```bash
+npx stash schema build [flags]
+```
+
+#### Flags
+
+| Flag | Description |
+| --- | --- |
+| `--supabase` | Use Supabase-compatible mode. |
+| `--database-url ` | Override DATABASE_URL for this run only — never written to disk. (env: `DATABASE_URL`) |
diff --git a/content/docs/reference/cli/status.mdx b/content/docs/reference/cli/status.mdx
new file mode 100644
index 0000000..ba033d7
--- /dev/null
+++ b/content/docs/reference/cli/status.mdx
@@ -0,0 +1,28 @@
+---
+title: stash status
+description: "Displays implementation status"
+type: reference
+components: [cli]
+verifiedAgainst:
+ cli: "0.17.0"
+---
+
+{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash manifest --json` (v0.17.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */}
+
+
+Generated from **`stash` v0.17.0** via `npx stash@0.17.0 manifest --json`. Run `npx stash@0.17.0 --help` to see the live command surface.
+
+
+Displays implementation status
+
+```bash
+npx stash status [flags]
+```
+
+### Flags
+
+| Flag | Description |
+| --- | --- |
+| `--quest` | Force the quest-log output (emoji + progress bars) even in non-TTY contexts. |
+| `--plain` | Force the plain-text output even in TTY contexts. |
+| `--json` | Emit a structured JSON document instead. |
diff --git a/content/docs/reference/cli/wizard.mdx b/content/docs/reference/cli/wizard.mdx
new file mode 100644
index 0000000..05678ed
--- /dev/null
+++ b/content/docs/reference/cli/wizard.mdx
@@ -0,0 +1,20 @@
+---
+title: stash wizard
+description: "AI-guided encryption setup (reads your codebase)"
+type: reference
+components: [cli]
+verifiedAgainst:
+ cli: "0.17.0"
+---
+
+{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash manifest --json` (v0.17.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */}
+
+
+Generated from **`stash` v0.17.0** via `npx stash@0.17.0 manifest --json`. Run `npx stash@0.17.0 --help` to see the live command surface.
+
+
+AI-guided encryption setup (reads your codebase)
+
+```bash
+npx stash wizard
+```
diff --git a/package.json b/package.json
index 3b4681a..d3d8965 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"version": "0.0.0",
"private": true,
"scripts": {
- "prebuild": "bun run generate-docs && bun run generate-docs:eql && bun run validate-links && bun run validate-redirects",
+ "prebuild": "bun run generate-docs && bun run generate-docs:eql && bun run generate-docs:cli && bun run validate-links && bun run validate-redirects",
"build": "next build",
"dev": "next dev -p 3001",
"start": "next start",
@@ -13,6 +13,7 @@
"format": "biome format --write",
"generate-docs": "tsx scripts/generate-docs.ts",
"generate-docs:eql": "tsx scripts/generate-eql-docs.ts",
+ "generate-docs:cli": "tsx scripts/generate-cli-docs.ts",
"validate-links": "tsx scripts/validate-links.ts",
"validate-redirects": "tsx scripts/validate-v2-redirects.ts"
},
diff --git a/scripts/cli-supplements/auth.md b/scripts/cli-supplements/auth.md
new file mode 100644
index 0000000..b17fc57
--- /dev/null
+++ b/scripts/cli-supplements/auth.md
@@ -0,0 +1,21 @@
+## How authentication works
+
+`stash auth login` runs the OAuth 2.0 **device authorization flow**:
+
+1. You pick a **region** for your CipherStash workspace.
+2. The CLI opens your browser to a verification URL (and prints it, so it also
+ works over SSH or in a headless/agent environment) where you approve the
+ request.
+3. Meanwhile the CLI polls CipherStash until you approve, then receives a
+ short-lived access token (it reports the token's expiry).
+4. Your device is **bound to the workspace's default keyset**, so later commands
+ (`stash eql install`, `stash db push`, …) authenticate without a fresh login.
+
+
+Login is device- and workspace-scoped. Authenticating from a new machine, or for a different workspace, re-runs the device flow.
+
+
+{/* TODO(verify with product): profiles, multiple workspaces, and switching
+between them (where they're stored and how they're selected) belong here, or
+in a linked CLI concept page. The CLI currently exposes only `auth login`;
+confirm the profile / workspace-switching surface before documenting it. */}
diff --git a/scripts/fixtures/stash-manifest.json b/scripts/fixtures/stash-manifest.json
new file mode 100644
index 0000000..0de5ec4
--- /dev/null
+++ b/scripts/fixtures/stash-manifest.json
@@ -0,0 +1,511 @@
+{
+ "name": "stash",
+ "version": "0.17.0",
+ "groups": [
+ {
+ "title": "Setup & workflow",
+ "commands": [
+ {
+ "name": "init",
+ "summary": "Initialize CipherStash for your project",
+ "long": "Set up CipherStash end-to-end: authenticate, introspect your database,\ninstall dependencies, install EQL, and hand off the rest to your local\ncoding agent. Every prompt has a non-interactive escape hatch, so init\nnever blocks waiting on a TTY (CI, agents, pipes).",
+ "examples": [
+ "init",
+ "init --supabase",
+ "init --prisma-next",
+ "init --region us-east-1"
+ ],
+ "flags": [
+ {
+ "name": "--supabase",
+ "description": "Use Supabase-specific setup flow."
+ },
+ {
+ "name": "--drizzle",
+ "description": "Use Drizzle-specific setup flow."
+ },
+ {
+ "name": "--prisma-next",
+ "description": "Use Prisma Next-specific setup flow (EQL bundle installed via prisma-next migration apply)."
+ },
+ {
+ "name": "--proxy",
+ "description": "Query encrypted data via CipherStash Proxy."
+ },
+ {
+ "name": "--no-proxy",
+ "description": "Query encrypted data directly via the SDK.",
+ "default": "true"
+ },
+ {
+ "name": "--region",
+ "value": "",
+ "description": "Region to authenticate against (e.g. us-east-1). Skips the interactive region picker. Required for non-interactive init when not already logged in.",
+ "env": "STASH_REGION"
+ }
+ ]
+ },
+ {
+ "name": "plan",
+ "summary": "Draft a reviewable encryption plan at .cipherstash/plan.md",
+ "examples": [
+ "plan",
+ "plan --target claude-code"
+ ],
+ "flags": [
+ {
+ "name": "--complete-rollout",
+ "description": "Plan the entire encryption lifecycle (schema-add through drop) in one document. Skips the production-deploy gate; only safe when this database is not backing a deployed application."
+ },
+ {
+ "name": "--target",
+ "value": "",
+ "description": "Skip the agent-target picker and hand off directly to one of claude-code | codex | agents-md | wizard. Safe in non-TTY contexts."
+ }
+ ]
+ },
+ {
+ "name": "impl",
+ "summary": "Execute the plan with a local agent",
+ "examples": [
+ "impl",
+ "impl --continue-without-plan",
+ "impl --target claude-code"
+ ],
+ "flags": [
+ {
+ "name": "--continue-without-plan",
+ "description": "Skip planning and go straight to implementation (interactively confirms before proceeding)."
+ },
+ {
+ "name": "--target",
+ "value": "",
+ "description": "Skip the agent-target picker and hand off directly to one of claude-code | codex | agents-md | wizard. Safe in non-TTY contexts."
+ }
+ ]
+ },
+ {
+ "name": "status",
+ "summary": "Displays implementation status",
+ "flags": [
+ {
+ "name": "--quest",
+ "description": "Force the quest-log output (emoji + progress bars) even in non-TTY contexts."
+ },
+ {
+ "name": "--plain",
+ "description": "Force the plain-text output even in TTY contexts."
+ },
+ {
+ "name": "--json",
+ "description": "Emit a structured JSON document instead."
+ }
+ ]
+ },
+ {
+ "name": "wizard",
+ "summary": "AI-guided encryption setup (reads your codebase)"
+ },
+ {
+ "name": "doctor",
+ "summary": "Diagnose install problems (native binaries, runtime)"
+ },
+ {
+ "name": "manifest",
+ "summary": "Print the structured, versioned command surface",
+ "long": "Emit the CLI command surface as data. `--json` produces the machine-\nreadable manifest the docs generator and agents consume; without it a\ngrouped command list is printed. The manifest is stamped with the CLI\nversion, so a page generated from it always names the version it describes.",
+ "examples": [
+ "manifest --json",
+ "manifest"
+ ],
+ "flags": [
+ {
+ "name": "--json",
+ "description": "Emit the structured JSON manifest instead of a text list."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "title": "Auth",
+ "commands": [
+ {
+ "name": "auth login",
+ "summary": "Authenticate with CipherStash",
+ "long": "Runs the OAuth 2.0 device authorization flow:\n1. Pick a region for your workspace.\n2. Approve in the browser — the URL is printed, so it works over SSH/headless.\n3. The CLI polls until you approve, then stores a short-lived token.\n4. Your device is bound to the workspace's default keyset, so later\n commands authenticate without a fresh login.",
+ "examples": [
+ "auth login",
+ "auth login --region us-east-1",
+ "auth login --supabase",
+ "auth login --region us-east-1 --json"
+ ],
+ "flags": [
+ {
+ "name": "--region",
+ "value": "",
+ "description": "Region to authenticate against (e.g. us-east-1). Skips the interactive region picker.",
+ "env": "STASH_REGION"
+ },
+ {
+ "name": "--json",
+ "description": "Emit newline-delimited JSON events instead of prose. The first event (authorization_required) carries the device verification URL for a human to open. Implies no prompt and no browser auto-open."
+ },
+ {
+ "name": "--no-open",
+ "description": "Don't auto-open the verification URL in a browser (already implied by --json)."
+ },
+ {
+ "name": "--supabase",
+ "description": "Track Supabase as the referrer."
+ },
+ {
+ "name": "--drizzle",
+ "description": "Track Drizzle as the referrer."
+ }
+ ]
+ },
+ {
+ "name": "auth regions",
+ "summary": "List the regions you can authenticate against",
+ "examples": [
+ "auth regions",
+ "auth regions --json"
+ ],
+ "flags": [
+ {
+ "name": "--json",
+ "description": "Emit machine-readable [{ slug, label }] instead of a text list."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "title": "EQL",
+ "commands": [
+ {
+ "name": "eql install",
+ "summary": "Scaffold stash.config.ts (if missing) and install EQL extensions",
+ "flags": [
+ {
+ "name": "--force",
+ "description": "Reinstall / overwrite even if already installed."
+ },
+ {
+ "name": "--dry-run",
+ "description": "Show what would happen without making changes."
+ },
+ {
+ "name": "--supabase",
+ "description": "Use Supabase-compatible mode (auto-detected from DATABASE_URL)."
+ },
+ {
+ "name": "--drizzle",
+ "description": "Generate a Drizzle migration instead of direct install (auto-detected from project)."
+ },
+ {
+ "name": "--migration",
+ "description": "Write a Supabase migration file instead of running SQL directly (requires --supabase)."
+ },
+ {
+ "name": "--direct",
+ "description": "Run the SQL directly against the database (requires --supabase; mutually exclusive with --migration)."
+ },
+ {
+ "name": "--migrations-dir",
+ "value": "",
+ "description": "Override the Supabase migrations directory (requires --supabase).",
+ "default": "supabase/migrations"
+ },
+ {
+ "name": "--exclude-operator-family",
+ "description": "Skip operator family creation."
+ },
+ {
+ "name": "--eql-version",
+ "value": "<2|3>",
+ "description": "EQL generation to target. v3 is the native eql_v3.* domain schema (direct install only for now).",
+ "default": "2"
+ },
+ {
+ "name": "--latest",
+ "description": "Fetch the latest EQL from GitHub (v2 only)."
+ },
+ {
+ "name": "--name",
+ "value": "",
+ "description": "With --drizzle: name for the generated migration (defaults to a scaffold name)."
+ },
+ {
+ "name": "--out",
+ "value": "",
+ "description": "With --drizzle: directory to write the generated migration into."
+ },
+ {
+ "name": "--database-url",
+ "value": "",
+ "description": "Override DATABASE_URL for this run only — never written to disk.",
+ "env": "DATABASE_URL"
+ }
+ ]
+ },
+ {
+ "name": "eql upgrade",
+ "summary": "Upgrade EQL extensions to the latest version",
+ "flags": [
+ {
+ "name": "--dry-run",
+ "description": "Show what would happen without making changes."
+ },
+ {
+ "name": "--supabase",
+ "description": "Use Supabase-compatible mode."
+ },
+ {
+ "name": "--exclude-operator-family",
+ "description": "Skip operator family creation."
+ },
+ {
+ "name": "--eql-version",
+ "value": "<2|3>",
+ "description": "EQL generation to target.",
+ "default": "2"
+ },
+ {
+ "name": "--latest",
+ "description": "Fetch the latest EQL from GitHub (v2 only)."
+ },
+ {
+ "name": "--database-url",
+ "value": "",
+ "description": "Override DATABASE_URL for this run only — never written to disk.",
+ "env": "DATABASE_URL"
+ }
+ ]
+ },
+ {
+ "name": "eql status",
+ "summary": "Show EQL installation status",
+ "flags": [
+ {
+ "name": "--database-url",
+ "value": "",
+ "description": "Override DATABASE_URL for this run only — never written to disk.",
+ "env": "DATABASE_URL"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "title": "Database",
+ "commands": [
+ {
+ "name": "db push",
+ "summary": "Push encryption schema (writes pending if active config already exists)",
+ "flags": [
+ {
+ "name": "--dry-run",
+ "description": "Show what would happen without making changes."
+ },
+ {
+ "name": "--database-url",
+ "value": "",
+ "description": "Override DATABASE_URL for this run only — never written to disk.",
+ "env": "DATABASE_URL"
+ }
+ ]
+ },
+ {
+ "name": "db activate",
+ "summary": "Promote pending → active without renames (use after additive db push)",
+ "flags": [
+ {
+ "name": "--database-url",
+ "value": "",
+ "description": "Override DATABASE_URL for this run only — never written to disk.",
+ "env": "DATABASE_URL"
+ }
+ ]
+ },
+ {
+ "name": "db validate",
+ "summary": "Validate encryption schema",
+ "flags": [
+ {
+ "name": "--supabase",
+ "description": "Use Supabase-compatible mode."
+ },
+ {
+ "name": "--exclude-operator-family",
+ "description": "Skip operator family creation."
+ },
+ {
+ "name": "--database-url",
+ "value": "",
+ "description": "Override DATABASE_URL for this run only — never written to disk.",
+ "env": "DATABASE_URL"
+ }
+ ]
+ },
+ {
+ "name": "db migrate",
+ "summary": "Run pending encrypt config migrations (not yet implemented)"
+ },
+ {
+ "name": "db test-connection",
+ "summary": "Test database connectivity",
+ "flags": [
+ {
+ "name": "--database-url",
+ "value": "",
+ "description": "Override DATABASE_URL for this run only — never written to disk.",
+ "env": "DATABASE_URL"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "title": "Schema",
+ "commands": [
+ {
+ "name": "schema build",
+ "summary": "Build an encryption schema from your database",
+ "flags": [
+ {
+ "name": "--supabase",
+ "description": "Use Supabase-compatible mode."
+ },
+ {
+ "name": "--database-url",
+ "value": "",
+ "description": "Override DATABASE_URL for this run only — never written to disk.",
+ "env": "DATABASE_URL"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "title": "Encrypt",
+ "commands": [
+ {
+ "name": "encrypt status",
+ "summary": "Show per-column migration status (phase, progress, drift)"
+ },
+ {
+ "name": "encrypt plan",
+ "summary": "Diff intent (.cipherstash/migrations.json) vs observed state"
+ },
+ {
+ "name": "encrypt backfill",
+ "summary": "Resumably encrypt plaintext into the encrypted column",
+ "flags": [
+ {
+ "name": "--table",
+ "value": "",
+ "description": "Target table."
+ },
+ {
+ "name": "--column",
+ "value": "",
+ "description": "Target column."
+ },
+ {
+ "name": "--pk-column",
+ "value": "",
+ "description": "Primary-key column used to page through rows."
+ },
+ {
+ "name": "--chunk-size",
+ "value": "",
+ "description": "Rows encrypted per batch."
+ },
+ {
+ "name": "--encrypted-column",
+ "value": "",
+ "description": "Destination encrypted column."
+ },
+ {
+ "name": "--schema-column-key",
+ "value": "",
+ "description": "Schema key identifying the column config."
+ },
+ {
+ "name": "--confirm-dual-writes-deployed",
+ "description": "Assert the app is dual-writing before backfilling (safety gate)."
+ },
+ {
+ "name": "--force",
+ "description": "Proceed past non-fatal safety checks."
+ }
+ ]
+ },
+ {
+ "name": "encrypt cutover",
+ "summary": "Rename swap encrypted → primary column",
+ "flags": [
+ {
+ "name": "--table",
+ "value": "",
+ "description": "Target table."
+ },
+ {
+ "name": "--column",
+ "value": "",
+ "description": "Target column."
+ },
+ {
+ "name": "--proxy-url",
+ "value": "",
+ "description": "Proxy URL to verify against."
+ },
+ {
+ "name": "--migrations-dir",
+ "value": "",
+ "description": "Directory to write the rename migration into."
+ }
+ ]
+ },
+ {
+ "name": "encrypt drop",
+ "summary": "Generate a migration to drop the plaintext column",
+ "flags": [
+ {
+ "name": "--table",
+ "value": "",
+ "description": "Target table."
+ },
+ {
+ "name": "--column",
+ "value": "",
+ "description": "Target column."
+ },
+ {
+ "name": "--migrations-dir",
+ "value": "",
+ "description": "Directory to write the drop migration into."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "title": "Experimental",
+ "commands": [
+ {
+ "name": "env",
+ "summary": "(experimental) Print production env vars for deployment",
+ "flags": [
+ {
+ "name": "--write",
+ "description": "Write the vars to a file instead of printing them."
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/scripts/generate-cli-docs.ts b/scripts/generate-cli-docs.ts
new file mode 100644
index 0000000..07effd9
--- /dev/null
+++ b/scripts/generate-cli-docs.ts
@@ -0,0 +1,415 @@
+#!/usr/bin/env tsx
+/**
+ * CLI reference generator (CIP-33xx).
+ *
+ * Generates the `/reference/cli` pages from the `stash` CLI itself, so the
+ * reference can never drift from the shipped command surface. Every page is
+ * stamped with the CLI version it was generated from.
+ *
+ * ── Data source ───────────────────────────────────────────────────────────
+ * We consume `stash manifest --json` (shipped in stash CLI 0.17) — the
+ * structured, versioned command surface the CLI builds from its own command
+ * registry. Groups, summaries, per-command flags (with defaults + env vars),
+ * and curated examples all come straight from the CLI, so the docs are a
+ * projection of the real command set rather than a scrape of `--help`.
+ *
+ * ── Versioning ────────────────────────────────────────────────────────────
+ * Always generated from the LATEST published `stash` on npm (resolved via
+ * `npm view stash version`), so a new release plus a run of this script — it
+ * runs in `prebuild` — refreshes the docs automatically. Every page carries
+ * `verifiedAgainst.cli` and a visible banner, so readers and agents always
+ * know which version the docs describe. Offline, it falls back to the cached
+ * `scripts/fixtures/stash-manifest.json`.
+ */
+import { execSync } from "node:child_process";
+import fs from "node:fs";
+import os from "node:os";
+import path from "node:path";
+
+const CLI_NAME = "stash";
+let CLI_VERSION = ""; // resolved to the latest published npm version at run time
+const RUNNER = "npx"; // normalized invocation shown in docs
+const FIXTURE = path.join(
+ process.cwd(),
+ "scripts/fixtures",
+ "stash-manifest.json",
+);
+const OUT_DIR = path.join(process.cwd(), "content/docs/reference/cli");
+// Hand-authored per-command prose merged into the generated page (hybrid model):
+// the generated skeleton (synopsis + flags + examples) stays drift-free; a
+// supplement adds rich narrative the manifest doesn't carry. Lives outside
+// content/ so it's never treated as a page or wiped by the clean step. Where
+// the CLI grows per-command long-help, that prose can migrate into the CLI and
+// this hook retires.
+const SUPPLEMENTS_DIR = path.join(process.cwd(), "scripts/cli-supplements");
+
+// ── The `stash manifest --json` contract ────────────────────────────────────
+// Mirrors packages/cli/src/cli/manifest.ts in the stack repo. Command `name`
+// is the full path ("eql install"); flags are already resolved per-command.
+interface CliFlag {
+ name: string; // "--supabase"
+ value?: string; // ""
+ description: string;
+ default?: string; // surfaced default, when worth showing
+ env?: string; // env var that also sets this, e.g. DATABASE_URL
+}
+interface CliCommand {
+ name: string;
+ summary: string;
+ long?: string;
+ examples?: string[];
+ flags?: CliFlag[];
+}
+interface CliGroup {
+ title: string;
+ commands: CliCommand[];
+}
+interface CliManifest {
+ name: string;
+ version: string;
+ groups: CliGroup[];
+}
+
+// ── Internal model (what the renderer consumes) ─────────────────────────────
+interface Flag {
+ name: string;
+ value?: string;
+ description: string;
+}
+interface Command {
+ path: string; // "eql install"
+ base: string; // "eql"
+ sub?: string; // "install"
+ group: string; // nav group title, from the manifest
+ summary: string;
+ flags: Flag[];
+ examples: string[];
+}
+interface Manifest {
+ name: string;
+ version: string;
+ commands: Command[];
+ groupOrder: string[]; // nav group order, as the CLI declares it
+}
+
+// EQL/Postgres command groups get the `eql` component facet too (content-model
+// rule: tag `eql` for queryable-in-Postgres ciphertext).
+const componentsFor = (base: string): string[] =>
+ ["eql", "db", "schema", "encrypt"].includes(base) ? ["cli", "eql"] : ["cli"];
+
+// ── Source ──────────────────────────────────────────────────────────────────
+// Resolve the latest published version so the docs track releases automatically.
+function latestVersion(): string {
+ try {
+ return execSync(`npm view ${CLI_NAME} version`, {
+ encoding: "utf8",
+ }).trim();
+ } catch {
+ const cached = fs.existsSync(FIXTURE)
+ ? (JSON.parse(fs.readFileSync(FIXTURE, "utf8")) as CliManifest).version
+ : undefined;
+ if (cached) {
+ console.warn(`⚠ npm unreachable; using cached stash v${cached}.`);
+ return cached;
+ }
+ throw new Error(
+ "Cannot resolve latest stash version (offline, no fixture).",
+ );
+ }
+}
+
+// Run the resolved CLI and read its `manifest --json`, caching to a fixture for
+// offline builds. dotenvx (the CLI's launcher) may print tips before the JSON,
+// so slice from the first `{` to the last `}` defensively.
+function loadRawManifest(version: string): CliManifest {
+ try {
+ const out = execSync(`npx --yes ${CLI_NAME}@${version} manifest --json`, {
+ encoding: "utf8",
+ cwd: os.tmpdir(),
+ stdio: ["ignore", "pipe", "ignore"],
+ });
+ const start = out.indexOf("{");
+ const end = out.lastIndexOf("}");
+ if (start === -1 || end < start) {
+ throw new Error(
+ `\`${CLI_NAME}@${version} manifest --json\` did not emit a JSON object (got: ${out.trim().slice(0, 120)}…)`,
+ );
+ }
+ const manifest = JSON.parse(out.slice(start, end + 1)) as CliManifest;
+ fs.mkdirSync(path.dirname(FIXTURE), { recursive: true });
+ fs.writeFileSync(FIXTURE, `${JSON.stringify(manifest, null, 2)}\n`);
+ return manifest;
+ } catch {
+ if (fs.existsSync(FIXTURE)) {
+ console.warn(`⚠ Could not run stash@${version}; using cached fixture.`);
+ return JSON.parse(fs.readFileSync(FIXTURE, "utf8")) as CliManifest;
+ }
+ throw new Error(
+ `Could not run stash@${version} manifest --json and no cached fixture exists.`,
+ );
+ }
+}
+
+// Fold the manifest's richer flag metadata (default + env) into the description
+// column so the page format (Flag | Description) stays a single table.
+function mapFlag(f: CliFlag): Flag {
+ const notes: string[] = [];
+ if (f.default !== undefined) notes.push(`default: \`${f.default}\``);
+ if (f.env) notes.push(`env: \`${f.env}\``);
+ const description = notes.length
+ ? `${f.description} (${notes.join("; ")})`
+ : f.description;
+ return { name: f.name, value: f.value, description };
+}
+
+// Project the CLI manifest onto the internal model the renderer consumes.
+function toManifest(m: CliManifest): Manifest {
+ const commands: Command[] = [];
+ const groupOrder: string[] = [];
+ for (const group of m.groups) {
+ if (!group.commands.length) continue;
+ if (!groupOrder.includes(group.title)) groupOrder.push(group.title);
+ for (const c of group.commands) {
+ const [base, ...rest] = c.name.split(/\s+/);
+ commands.push({
+ path: c.name,
+ base,
+ sub: rest.length ? rest.join(" ") : undefined,
+ group: group.title,
+ summary: c.summary,
+ flags: (c.flags ?? []).map(mapFlag),
+ examples: (c.examples ?? []).map((e) => `${RUNNER} ${CLI_NAME} ${e}`),
+ });
+ }
+ }
+ return { name: m.name, version: m.version, commands, groupOrder };
+}
+
+// ── Render ───────────────────────────────────────────────────────────────────
+const generatedMarker = (): string =>
+ `{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from \`${CLI_NAME} manifest --json\` (v${CLI_VERSION}). Re-run \`bun run generate-docs:cli\` to refresh from the latest published CLI. */}`;
+
+function banner(): string {
+ return `
+Generated from **\`${CLI_NAME}\` v${CLI_VERSION}** via \`${RUNNER} ${CLI_NAME}@${CLI_VERSION} manifest --json\`. Run \`${RUNNER} ${CLI_NAME}@${CLI_VERSION} --help\` to see the live command surface.
+`;
+}
+
+// Escape characters MDX parses as JSX inside prose: `{`/`}` (expression braces —
+// e.g. the `auth regions` flag description "[{ slug, label }]" would otherwise
+// evaluate `slug` and crash the prerender) and stray `<` (tags). Flag names and
+// values render inside code spans, which are literal, so this only applies to
+// manifest-derived prose (descriptions, summaries).
+const escapeMdxText = (s: string): string => s.replace(/([{}<])/g, "\\$1");
+
+function flagsTable(flags: Flag[]): string {
+ if (!flags.length) return "";
+ const rows = flags
+ .map((f) => {
+ // Escape pipes (e.g. the `<2|3>` in `--eql-version`) so they don't read
+ // as table-column separators, even inside the code span.
+ const opt = `\`${f.name}${f.value ? ` ${f.value}` : ""}\``.replace(
+ /\|/g,
+ "\\|",
+ );
+ const description = escapeMdxText(f.description).replace(/\|/g, "\\|");
+ return `| ${opt} | ${description} |`;
+ })
+ .join("\n");
+ return `\n### Flags\n\n| Flag | Description |\n| --- | --- |\n${rows}\n`;
+}
+
+function commandSection(cmd: Command, level: "##" | "###"): string {
+ const synopsis = `${RUNNER} ${CLI_NAME} ${cmd.path}${cmd.flags.length ? " [flags]" : ""}`;
+ const parts = [
+ `${level} \`${cmd.path}\``,
+ "",
+ escapeMdxText(cmd.summary),
+ "",
+ "```bash",
+ synopsis,
+ "```",
+ ];
+ if (cmd.flags.length)
+ parts.push(flagsTable(cmd.flags).replace("### Flags", `${level}# Flags`));
+ if (cmd.examples.length) {
+ parts.push(
+ `\n${level}# Examples\n`,
+ "```bash",
+ cmd.examples.join("\n"),
+ "```",
+ );
+ }
+ return parts.join("\n");
+}
+
+function renderPage(
+ base: string,
+ cmds: Command[],
+): { slug: string; body: string } {
+ const isGroup = cmds.some((c) => c.sub) || cmds.length > 1;
+ const title = base;
+ const components = componentsFor(base);
+ const description = isGroup
+ ? `Reference for the \`${CLI_NAME} ${base}\` commands.`
+ : cmds[0].summary;
+
+ const frontmatter = [
+ "---",
+ `title: ${CLI_NAME} ${title}`,
+ `description: ${JSON.stringify(description)}`,
+ "type: reference",
+ `components: [${components.join(", ")}]`,
+ "verifiedAgainst:",
+ ` cli: "${CLI_VERSION}"`,
+ "---",
+ ].join("\n");
+
+ const parts = [frontmatter, "", generatedMarker(), "", banner(), ""];
+
+ if (isGroup) {
+ parts.push(
+ `The \`${CLI_NAME} ${base}\` command group.`,
+ "",
+ cmds.map((c) => commandSection(c, "###")).join("\n\n"),
+ );
+ } else {
+ const c = cmds[0];
+ parts.push(
+ escapeMdxText(c.summary),
+ "",
+ "```bash",
+ `${RUNNER} ${CLI_NAME} ${c.path}${c.flags.length ? " [flags]" : ""}`,
+ "```",
+ );
+ if (c.flags.length) parts.push(flagsTable(c.flags));
+ if (c.examples.length)
+ parts.push("\n## Examples\n", "```bash", c.examples.join("\n"), "```");
+ }
+
+ const supplement = readSupplement(base);
+ const body = `${parts.join("\n").trimEnd()}${supplement ? `\n\n${supplement}` : ""}\n`;
+ return { slug: base, body };
+}
+
+// Optional hand-authored prose for a command, merged after its generated
+// reference. Returns "" when no supplement exists.
+function readSupplement(slug: string): string {
+ const file = path.join(SUPPLEMENTS_DIR, `${slug}.md`);
+ return fs.existsSync(file) ? fs.readFileSync(file, "utf8").trim() : "";
+}
+
+function renderIndex(
+ manifest: Manifest,
+ groups: Map,
+): string {
+ const frontmatter = [
+ "---",
+ "title: CLI",
+ `description: "Command reference for the ${CLI_NAME} CLI, generated from v${CLI_VERSION}."`,
+ "type: reference",
+ "components: [cli]",
+ "verifiedAgainst:",
+ ` cli: "${CLI_VERSION}"`,
+ "---",
+ ].join("\n");
+
+ const sections = manifest.groupOrder
+ .filter((g) => groups.has(g))
+ .map((g) => {
+ const rows = groups
+ .get(g)!
+ .flatMap((base) =>
+ manifest.commands
+ .filter((c) => c.base === base)
+ .map((c) => {
+ const anchor = c.sub ? `#${c.path.replace(/\s+/g, "-")}` : "";
+ const summary = escapeMdxText(c.summary).replace(/\|/g, "\\|");
+ return `| [\`${c.path}\`](/reference/cli/${base}${anchor}) | ${summary} |`;
+ }),
+ )
+ .join("\n");
+ return `### ${g}\n\n| Command | Description |\n| --- | --- |\n${rows}`;
+ })
+ .join("\n\n");
+
+ return `${frontmatter}
+
+${generatedMarker()}
+
+${banner()}
+
+The \`${CLI_NAME}\` CLI. Install with \`${RUNNER} ${CLI_NAME}@${CLI_VERSION}\`. Every command accepts \`--help\` and \`--version\`.
+
+${sections}
+`;
+}
+
+function renderMeta(manifest: Manifest, groups: Map): string {
+ const pages: string[] = [];
+ for (const g of manifest.groupOrder) {
+ if (!groups.has(g)) continue;
+ pages.push(`---${g}---`);
+ pages.push(...groups.get(g)!);
+ }
+ return `${JSON.stringify({ title: "CLI", pages }, null, 2)}\n`;
+}
+
+// ── Main ─────────────────────────────────────────────────────────────────────
+function loadManifest(): Manifest {
+ return toManifest(loadRawManifest(CLI_VERSION));
+}
+
+function main() {
+ // latestVersion() picks which published CLI to invoke; the manifest we get
+ // back is authoritative for what to stamp. Reconcile CLI_VERSION to it so
+ // pages never claim a version different from the data they were built from
+ // (e.g. when the live run fails and we fall back to an older cached fixture).
+ CLI_VERSION = latestVersion();
+ const manifest = loadManifest();
+ CLI_VERSION = manifest.version;
+
+ // Group top-level commands by base, preserving discovery order.
+ const bases: string[] = [];
+ for (const c of manifest.commands)
+ if (!bases.includes(c.base)) bases.push(c.base);
+
+ // Nav groups, in the order the CLI declares them; a base inherits the group
+ // of its commands.
+ const groups = new Map();
+ for (const g of manifest.groupOrder) groups.set(g, []);
+ for (const base of bases) {
+ const g = manifest.commands.find((c) => c.base === base)!.group;
+ groups.get(g)!.push(base);
+ }
+ for (const [g, list] of groups) if (!list.length) groups.delete(g);
+
+ // Clean previously generated pages, then write fresh.
+ fs.mkdirSync(OUT_DIR, { recursive: true });
+ for (const f of fs.readdirSync(OUT_DIR)) {
+ if (f.endsWith(".mdx") || f === "meta.json")
+ fs.rmSync(path.join(OUT_DIR, f));
+ }
+
+ let count = 0;
+ for (const base of bases) {
+ const cmds = manifest.commands.filter((c) => c.base === base);
+ const { slug, body } = renderPage(base, cmds);
+ fs.writeFileSync(path.join(OUT_DIR, `${slug}.mdx`), body);
+ count++;
+ }
+ fs.writeFileSync(
+ path.join(OUT_DIR, "index.mdx"),
+ renderIndex(manifest, groups),
+ );
+ fs.writeFileSync(
+ path.join(OUT_DIR, "meta.json"),
+ renderMeta(manifest, groups),
+ );
+
+ console.log(
+ `✓ Generated ${count} CLI reference page(s) for ${CLI_NAME} v${manifest.version} → ${path.relative(process.cwd(), OUT_DIR)}`,
+ );
+}
+
+main();