diff --git a/content/docs/reference/cli/auth.mdx b/content/docs/reference/cli/auth.mdx
new file mode 100644
index 0000000..4ae27d0
--- /dev/null
+++ b/content/docs/reference/cli/auth.mdx
@@ -0,0 +1,50 @@
+---
+title: stash auth
+description: "Authenticate with CipherStash"
+type: reference
+components: [cli]
+verifiedAgainst:
+ cli: "0.16.0"
+---
+
+{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash --help` (v0.16.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */}
+
+
+Generated from **`stash` v0.16.0**. Run `npx stash@0.16.0 --help` to see the live command surface.
+
+
+Authenticate with CipherStash
+
+```bash
+npx stash auth
+```
+
+## Examples
+
+```bash
+npx stash auth login
+```
+
+## 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.
+
+The `--supabase` / `--drizzle` flags only tag the referrer for onboarding
+analytics; they don't change the flow.
+
+> **Good to know**: 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..d5924d0
--- /dev/null
+++ b/content/docs/reference/cli/db.mdx
@@ -0,0 +1,163 @@
+---
+title: stash db
+description: "Reference for the `stash db` commands."
+type: reference
+components: [cli, eql]
+verifiedAgainst:
+ cli: "0.16.0"
+---
+
+{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash --help` (v0.16.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */}
+
+
+Generated from **`stash` v0.16.0**. Run `npx stash@0.16.0 --help` to see the live command surface.
+
+
+The `stash db` command group.
+
+### `db install`
+
+Scaffold stash.config.ts (if missing) and install EQL extensions
+
+```bash
+npx stash db 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` | (requires --supabase) Write a Supabase migration file instead of running SQL directly |
+| `--direct` | (requires --supabase) Run the SQL directly against the database (mutually exclusive with --migration) |
+| `--migrations-dir ` | (requires --supabase) Override the Supabase migrations directory (default: supabase/migrations) |
+| `--exclude-operator-family` | Skip operator family creation |
+| `--latest` | Fetch the latest EQL from GitHub |
+| `--database-url ` | Override DATABASE_URL for this run only — never written to disk |
+
+
+#### Examples
+
+```bash
+npx stash db install
+```
+
+### `db upgrade`
+
+Upgrade EQL extensions to the latest version
+
+```bash
+npx stash db upgrade [flags]
+```
+
+#### Flags
+
+| Flag | Description |
+| --- | --- |
+| `--dry-run` | Show what would happen without making changes |
+| `--supabase` | Use Supabase-compatible mode (auto-detected from DATABASE_URL) |
+| `--exclude-operator-family` | Skip operator family creation |
+| `--latest` | Fetch the latest EQL from GitHub |
+| `--database-url ` | Override DATABASE_URL for this run only — never written to disk |
+
+
+### `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 |
+
+
+#### Examples
+
+```bash
+npx stash db push
+```
+
+### `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 |
+
+
+### `db validate`
+
+Validate encryption schema
+
+```bash
+npx stash db validate [flags]
+```
+
+#### Flags
+
+| Flag | Description |
+| --- | --- |
+| `--supabase` | Use Supabase-compatible mode (auto-detected from DATABASE_URL) |
+| `--exclude-operator-family` | Skip operator family creation |
+| `--database-url ` | Override DATABASE_URL for this run only — never written to disk |
+
+
+### `db migrate`
+
+Run pending encrypt config migrations
+
+```bash
+npx stash db migrate [flags]
+```
+
+#### Flags
+
+| Flag | Description |
+| --- | --- |
+| `--database-url ` | Override DATABASE_URL for this run only — never written to disk |
+
+
+### `db status`
+
+Show EQL installation status
+
+```bash
+npx stash db status [flags]
+```
+
+#### Flags
+
+| Flag | Description |
+| --- | --- |
+| `--database-url ` | Override DATABASE_URL for this run only — never written to disk |
+
+
+### `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 |
diff --git a/content/docs/reference/cli/encrypt.mdx b/content/docs/reference/cli/encrypt.mdx
new file mode 100644
index 0000000..421feb5
--- /dev/null
+++ b/content/docs/reference/cli/encrypt.mdx
@@ -0,0 +1,56 @@
+---
+title: stash encrypt
+description: "Reference for the `stash encrypt` commands."
+type: reference
+components: [cli, eql]
+verifiedAgainst:
+ cli: "0.16.0"
+---
+
+{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash --help` (v0.16.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */}
+
+
+Generated from **`stash` v0.16.0**. Run `npx stash@0.16.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
+```
+
+### `encrypt cutover`
+
+Rename swap encrypted → primary column
+
+```bash
+npx stash encrypt cutover
+```
+
+### `encrypt drop`
+
+Generate a migration to drop the plaintext column
+
+```bash
+npx stash encrypt drop
+```
diff --git a/content/docs/reference/cli/env.mdx b/content/docs/reference/cli/env.mdx
new file mode 100644
index 0000000..bcf8961
--- /dev/null
+++ b/content/docs/reference/cli/env.mdx
@@ -0,0 +1,20 @@
+---
+title: stash env
+description: "(experimental) Print production env vars for deployment"
+type: reference
+components: [cli]
+verifiedAgainst:
+ cli: "0.16.0"
+---
+
+{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash --help` (v0.16.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */}
+
+
+Generated from **`stash` v0.16.0**. Run `npx stash@0.16.0 --help` to see the live command surface.
+
+
+(experimental) Print production env vars for deployment
+
+```bash
+npx stash env
+```
diff --git a/content/docs/reference/cli/impl.mdx b/content/docs/reference/cli/impl.mdx
new file mode 100644
index 0000000..9190888
--- /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.16.0"
+---
+
+{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash --help` (v0.16.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */}
+
+
+Generated from **`stash` v0.16.0**. Run `npx stash@0.16.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 to call from non-TTY contexts (CI, pipes). Without --target in non-TTY, the command prints a hint and exits cleanly instead of hanging. |
+
+
+## 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..8ee4efa 100644
--- a/content/docs/reference/cli/index.mdx
+++ b/content/docs/reference/cli/index.mdx
@@ -1,8 +1,67 @@
---
title: CLI
-description: "CLI documentation — being built as part of the docs V2 overhaul."
+description: "Command reference for the stash CLI, generated from v0.16.0."
+type: reference
+components: [cli]
+verifiedAgainst:
+ cli: "0.16.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 --help` (v0.16.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.16.0**. Run `npx stash@0.16.0 --help` to see the live command surface.
+
+
+The `stash` CLI. Install with `npx stash@0.16.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) |
+
+### Auth
+
+| Command | Description |
+| --- | --- |
+| [`auth`](/reference/cli/auth) | Authenticate with CipherStash |
+
+### Database
+
+| Command | Description |
+| --- | --- |
+| [`db install`](/reference/cli/db#db-install) | Scaffold stash.config.ts (if missing) and install EQL extensions |
+| [`db upgrade`](/reference/cli/db#db-upgrade) | Upgrade EQL extensions to the latest version |
+| [`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 |
+| [`db status`](/reference/cli/db#db-status) | Show EQL installation status |
+| [`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 |
+
+### Deployment
+
+| 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..57f4171
--- /dev/null
+++ b/content/docs/reference/cli/init.mdx
@@ -0,0 +1,39 @@
+---
+title: stash init
+description: "Initialize CipherStash for your project"
+type: reference
+components: [cli]
+verifiedAgainst:
+ cli: "0.16.0"
+---
+
+{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash --help` (v0.16.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */}
+
+
+Generated from **`stash` v0.16.0**. Run `npx stash@0.16.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) |
+
+
+## Examples
+
+```bash
+npx stash init
+npx stash init --supabase
+npx stash init --prisma-next
+```
diff --git a/content/docs/reference/cli/meta.json b/content/docs/reference/cli/meta.json
index 0a67892..5e139d7 100644
--- a/content/docs/reference/cli/meta.json
+++ b/content/docs/reference/cli/meta.json
@@ -1,4 +1,21 @@
{
"title": "CLI",
- "pages": ["..."]
+ "pages": [
+ "---Setup & workflow---",
+ "init",
+ "plan",
+ "impl",
+ "status",
+ "wizard",
+ "---Auth---",
+ "auth",
+ "---Database---",
+ "db",
+ "---Schema---",
+ "schema",
+ "---Encrypt---",
+ "encrypt",
+ "---Deployment---",
+ "env"
+ ]
}
diff --git a/content/docs/reference/cli/plan.mdx b/content/docs/reference/cli/plan.mdx
new file mode 100644
index 0000000..70cbc0a
--- /dev/null
+++ b/content/docs/reference/cli/plan.mdx
@@ -0,0 +1,34 @@
+---
+title: stash plan
+description: "Draft a reviewable encryption plan at .cipherstash/plan.md"
+type: reference
+components: [cli]
+verifiedAgainst:
+ cli: "0.16.0"
+---
+
+{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash --help` (v0.16.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */}
+
+
+Generated from **`stash` v0.16.0**. Run `npx stash@0.16.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 that normally separates rollout from cutover. Only safe when this database is not backing a deployed application (local dev, sandbox, freshly seeded test environment). |
+| `--target ` | Skip the agent-target picker and hand off directly to one of claude-code \| codex \| agents-md \| wizard. Safe to call from non-TTY contexts (CI, pipes). Without --target in non-TTY, the command prints a hint and exits cleanly instead of hanging. |
+
+
+## Examples
+
+```bash
+npx stash plan
+```
diff --git a/content/docs/reference/cli/schema.mdx b/content/docs/reference/cli/schema.mdx
new file mode 100644
index 0000000..11ae03c
--- /dev/null
+++ b/content/docs/reference/cli/schema.mdx
@@ -0,0 +1,30 @@
+---
+title: stash schema
+description: "Reference for the `stash schema` commands."
+type: reference
+components: [cli, eql]
+verifiedAgainst:
+ cli: "0.16.0"
+---
+
+{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash --help` (v0.16.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */}
+
+
+Generated from **`stash` v0.16.0**. Run `npx stash@0.16.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
+```
+
+#### Examples
+
+```bash
+npx stash schema build
+```
diff --git a/content/docs/reference/cli/status.mdx b/content/docs/reference/cli/status.mdx
new file mode 100644
index 0000000..74e2834
--- /dev/null
+++ b/content/docs/reference/cli/status.mdx
@@ -0,0 +1,35 @@
+---
+title: stash status
+description: "Displays implementation status"
+type: reference
+components: [cli]
+verifiedAgainst:
+ cli: "0.16.0"
+---
+
+{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash --help` (v0.16.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */}
+
+
+Generated from **`stash` v0.16.0**. Run `npx stash@0.16.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. Default is auto: fancy in a terminal, plain in CI / pipes / agents. |
+| `--plain` | Force the plain-text output even in TTY contexts. |
+| `--json` | Emit a structured JSON document instead. |
+
+
+## Examples
+
+```bash
+npx stash status
+```
diff --git a/content/docs/reference/cli/wizard.mdx b/content/docs/reference/cli/wizard.mdx
new file mode 100644
index 0000000..081dc9e
--- /dev/null
+++ b/content/docs/reference/cli/wizard.mdx
@@ -0,0 +1,26 @@
+---
+title: stash wizard
+description: "AI-guided encryption setup (reads your codebase)"
+type: reference
+components: [cli]
+verifiedAgainst:
+ cli: "0.16.0"
+---
+
+{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash --help` (v0.16.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */}
+
+
+Generated from **`stash` v0.16.0**. Run `npx stash@0.16.0 --help` to see the live command surface.
+
+
+AI-guided encryption setup (reads your codebase)
+
+```bash
+npx stash wizard
+```
+
+## Examples
+
+```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..4d13348
--- /dev/null
+++ b/scripts/cli-supplements/auth.md
@@ -0,0 +1,23 @@
+## 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.
+
+The `--supabase` / `--drizzle` flags only tag the referrer for onboarding
+analytics; they don't change the flow.
+
+> **Good to know**: 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-help.txt b/scripts/fixtures/stash-help.txt
new file mode 100644
index 0000000..fa27406
--- /dev/null
+++ b/scripts/fixtures/stash-help.txt
@@ -0,0 +1,98 @@
+◇ injected env (0) from .env.local // tip: ⌁ auth for agents [www.vestauth.com]
+◇ injected env (0) from .env.development.local // tip: ◈ secrets for agents [www.dotenvx.com]
+◇ injected env (0) from .env.development // tip: ◈ encrypted .env [www.dotenvx.com]
+◇ injected env (0) from .env // tip: ◈ encrypted .env [www.dotenvx.com]
+CipherStash CLI v0.16.0
+
+Usage: bunx stash [options]
+
+Commands:
+ init Initialize CipherStash for your project
+ plan Draft a reviewable encryption plan at .cipherstash/plan.md
+ impl Execute the plan with a local agent
+ status Displays implementation status
+ auth Authenticate with CipherStash
+ wizard AI-guided encryption setup (reads your codebase)
+
+ db install Scaffold stash.config.ts (if missing) and install EQL extensions
+ db upgrade Upgrade EQL extensions to the latest version
+ db push Push encryption schema (writes pending if active config already exists)
+ db activate Promote pending → active without renames (use after additive db push)
+ db validate Validate encryption schema
+ db migrate Run pending encrypt config migrations
+ db status Show EQL installation status
+ db test-connection Test database connectivity
+
+ schema build Build an encryption schema from your database
+
+ encrypt status Show per-column migration status (phase, progress, drift)
+ encrypt plan Diff intent (.cipherstash/migrations.json) vs observed state
+ encrypt backfill Resumably encrypt plaintext into the encrypted column
+ encrypt cutover Rename swap encrypted → primary column
+ encrypt drop Generate a migration to drop the plaintext column
+
+ env (experimental) Print production env vars for deployment
+
+Options:
+ --help, -h Show help
+ --version, -v Show version
+
+Init Flags:
+ --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)
+
+Plan Flags:
+ --complete-rollout Plan the entire encryption lifecycle (schema-add through drop)
+ in one document. Skips the production-deploy gate that
+ normally separates rollout from cutover. Only safe when this
+ database is not backing a deployed application (local dev,
+ sandbox, freshly seeded test environment).
+ --target Skip the agent-target picker and hand off directly to one of
+ claude-code | codex | agents-md | wizard. Safe to call from
+ non-TTY contexts (CI, pipes). Without --target in non-TTY,
+ the command prints a hint and exits cleanly instead of hanging.
+
+Status Flags:
+ --quest Force the quest-log output (emoji + progress bars)
+ even in non-TTY contexts. Default is auto: fancy
+ in a terminal, plain in CI / pipes / agents.
+ --plain Force the plain-text output even in TTY contexts.
+ --json Emit a structured JSON document instead.
+
+Impl Flags:
+ --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 to call from
+ non-TTY contexts (CI, pipes). Without --target in non-TTY,
+ the command prints a hint and exits cleanly instead of hanging.
+
+DB Flags:
+ --force (install) Reinstall / overwrite even if already installed
+ --dry-run (install, push, upgrade) Show what would happen without making changes
+ --supabase (install, upgrade, validate) Use Supabase-compatible mode (auto-detected from DATABASE_URL)
+ --drizzle (install) Generate a Drizzle migration instead of direct install (auto-detected from project)
+ --migration (install, requires --supabase) Write a Supabase migration file instead of running SQL directly
+ --direct (install, requires --supabase) Run the SQL directly against the database (mutually exclusive with --migration)
+ --migrations-dir (install, requires --supabase) Override the Supabase migrations directory (default: supabase/migrations)
+ --exclude-operator-family (install, upgrade, validate) Skip operator family creation
+ --latest (install, upgrade) Fetch the latest EQL from GitHub
+ --database-url (all db / schema commands) Override DATABASE_URL for this run only — never written to disk
+
+Examples:
+ bunx stash init
+ bunx stash init --supabase
+ bunx stash init --prisma-next
+ bunx stash plan
+ bunx stash impl
+ bunx stash impl --continue-without-plan
+ bunx stash impl --target claude-code
+ bunx stash status
+ bunx stash auth login
+ bunx stash wizard
+ bunx stash db install
+ bunx stash db push
+ bunx stash schema build
diff --git a/scripts/generate-cli-docs.ts b/scripts/generate-cli-docs.ts
new file mode 100644
index 0000000..30a0427
--- /dev/null
+++ b/scripts/generate-cli-docs.ts
@@ -0,0 +1,450 @@
+#!/usr/bin/env tsx
+/**
+ * CLI reference generator — PROTOTYPE (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 ───────────────────────────────────────────────────────────
+ * TODAY (bootstrap): we parse `stash --help`, captured to a fixture. `stash`
+ * is a hand-rolled TS CLI with no machine-readable output yet, and no
+ * per-command `--help` (every command prints the top-level help), so the
+ * single top-level help is the whole surface. It is thin — no args, no
+ * per-command examples, `auth`/`encrypt` subcommands undetailed.
+ *
+ * TARGET: add `stash manifest --json` to the CLI (it already has the command
+ * registry it prints `--help` from). Then replace `loadManifest()` with:
+ *
+ * JSON.parse(execSync(`npx stash@${CLI_VERSION} manifest --json`))
+ *
+ * and delete the parser below. The renderer and page format stay identical —
+ * that is the point of this prototype.
+ *
+ * ── 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-help.txt`.
+ */
+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-help.txt");
+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) stays drift-free; a supplement adds
+// rich narrative + curated examples the thin `--help` can't provide. Lives
+// outside content/ so it's never treated as a page or wiped by the clean step.
+// Long-term, migrate these into the CLI's own long-help/examples (see the PR).
+const SUPPLEMENTS_DIR = path.join(process.cwd(), "scripts/cli-supplements");
+
+// ── Types (this shape is the spec for `stash manifest --json`) ──────────────
+interface Flag {
+ name: string; // "--supabase"
+ value?: string; // ""
+ description: string;
+ appliesTo?: string[]; // db-flag applicability: ["install", "upgrade"] or ["all"]
+}
+interface Command {
+ path: string; // "db install"
+ base: string; // "db"
+ sub?: string; // "install"
+ summary: string;
+ flags: Flag[];
+ examples: string[];
+}
+interface Manifest {
+ name: string;
+ version: string;
+ usage: string;
+ globalFlags: Flag[];
+ commands: Command[];
+}
+
+// Which nav group each top-level command belongs to, and the group order.
+const GROUPS: Record = {
+ init: "Setup & workflow",
+ plan: "Setup & workflow",
+ impl: "Setup & workflow",
+ status: "Setup & workflow",
+ wizard: "Setup & workflow",
+ auth: "Auth",
+ db: "Database",
+ schema: "Schema",
+ encrypt: "Encrypt",
+ env: "Deployment",
+};
+const GROUP_ORDER = [
+ "Setup & workflow",
+ "Auth",
+ "Database",
+ "Schema",
+ "Encrypt",
+ "Deployment",
+];
+// Known db/schema subcommand names, used to resolve db-flag applicability.
+const DB_SUBCOMMANDS = new Set([
+ "install",
+ "upgrade",
+ "push",
+ "activate",
+ "validate",
+ "migrate",
+ "status",
+ "test-connection",
+]);
+
+// EQL/Postgres commands get the `eql` component facet too (content-model rule:
+// tag `eql` only for queryable-in-Postgres ciphertext).
+const componentsFor = (base: string): string[] =>
+ ["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)
+ ? fs.readFileSync(FIXTURE, "utf8").match(/CipherStash CLI v([0-9.]+)/)?.[1]
+ : 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 version and cache its help. (Target: `stash manifest --json`.)
+function loadHelp(version: string): string {
+ try {
+ const out = execSync(`npx --yes ${CLI_NAME}@${version} --help`, {
+ encoding: "utf8",
+ cwd: os.tmpdir(),
+ stdio: ["ignore", "pipe", "ignore"],
+ });
+ fs.mkdirSync(path.dirname(FIXTURE), { recursive: true });
+ fs.writeFileSync(FIXTURE, out);
+ return out;
+ } catch {
+ if (fs.existsSync(FIXTURE)) {
+ console.warn(`⚠ Could not run stash@${version}; using cached fixture.`);
+ return fs.readFileSync(FIXTURE, "utf8");
+ }
+ throw new Error(`Could not run stash@${version} and no cached fixture exists.`);
+ }
+}
+
+// Drop dotenvx's env-injection tips and blank leading noise.
+const stripNoise = (text: string): string[] =>
+ text
+ .split("\n")
+ .filter((l) => !/^\s*◇|injected env|dotenvx|www\.(dotenvx|vestauth)/.test(l));
+
+// ── Parser (delete once `stash manifest --json` exists) ─────────────────────
+function parseHelp(text: string): Manifest {
+ const lines = stripNoise(text);
+ const joined = lines.join("\n");
+
+ const version = joined.match(/CipherStash CLI v([0-9]+\.[0-9]+\.[0-9]+)/)?.[1] ?? CLI_VERSION;
+
+ // Section boundaries: a line like "Commands:", "Options:", "DB Flags:", "Examples:".
+ const sections: Record = {};
+ let current = "";
+ for (const line of lines) {
+ const header = line.match(/^([A-Za-z][A-Za-z ]*):\s*$/);
+ if (header && !line.startsWith(" ")) {
+ current = header[1].trim();
+ sections[current] = [];
+ } else if (current) {
+ sections[current].push(line);
+ }
+ }
+
+ // Commands: " db install Scaffold ..." (name is non-greedy up to 2+ spaces)
+ const commands: Command[] = [];
+ for (const line of sections.Commands ?? []) {
+ const m = line.match(/^ {2}(\S.*?) {2,}(.+)$/);
+ if (!m) continue;
+ const rawName = m[1].replace(/\s*\s*/, "").trim();
+ const [base, ...rest] = rawName.split(/\s+/);
+ commands.push({
+ path: rawName,
+ base,
+ sub: rest.length ? rest.join(" ") : undefined,
+ summary: m[2].trim(),
+ flags: [],
+ examples: [],
+ });
+ }
+
+ // Global options.
+ const globalFlags = parseFlagBlock(sections.Options ?? []);
+
+ // Per-command flag sections: "Init Flags", "Plan Flags", "DB Flags", …
+ for (const [name, body] of Object.entries(sections)) {
+ const fm = name.match(/^(.*) Flags$/);
+ if (!fm) continue;
+ const label = fm[1].toLowerCase(); // "init", "plan", "db"
+ const flags = parseFlagBlock(body);
+ if (label === "db") {
+ // DB flags carry applicability annotations; resolve onto each subcommand.
+ for (const cmd of commands.filter((c) => c.base === "db")) {
+ cmd.flags = flags.filter(
+ (f) =>
+ !f.appliesTo ||
+ f.appliesTo.includes("all") ||
+ (cmd.sub ? f.appliesTo.includes(cmd.sub) : false),
+ );
+ }
+ } else {
+ const cmd = commands.find((c) => c.path === label);
+ if (cmd) cmd.flags = flags;
+ }
+ }
+
+ // Examples: " npx stash db install" → attach to the longest matching command.
+ const byLength = [...commands].sort((a, b) => b.path.length - a.path.length);
+ for (const line of sections.Examples ?? []) {
+ const inv = line.trim();
+ const m = inv.match(/^(?:npx|bunx|pnpm dlx|stash)\s+(?:stash\s+)?(.+)$/);
+ const argPart = m ? m[1] : inv;
+ const cmd = byLength.find(
+ (c) => argPart === c.path || argPart.startsWith(`${c.path} `),
+ );
+ if (cmd) cmd.examples.push(`${RUNNER} ${CLI_NAME} ${argPart}`);
+ }
+
+ return {
+ name: CLI_NAME,
+ version,
+ usage: `${RUNNER} ${CLI_NAME} [options]`,
+ globalFlags,
+ commands,
+ };
+}
+
+// Parse an indented flag block, folding continuation lines into descriptions.
+function parseFlagBlock(body: string[]): Flag[] {
+ const flags: Flag[] = [];
+ for (const line of body) {
+ if (!line.trim()) continue;
+ const m = line.match(/^ {2}(--[\w-]+)(?:,\s*-\w)?(?: +(<[^>]+>))? {2,}(.+)$/);
+ if (m) {
+ let description = m[3].trim();
+ let appliesTo: string[] | undefined;
+ // Leading "(install, push, …)" on DB flags = applicability (+ conditions).
+ const paren = description.match(/^\(([^)]+)\)\s*(.*)$/);
+ if (paren) {
+ const inner = paren[1].trim();
+ if (/^all\b/.test(inner)) {
+ // "(all db / schema commands)" — applies everywhere; drop the note.
+ appliesTo = ["all"];
+ description = paren[2];
+ } else {
+ const tokens = inner.split(/[,/]/).map((t) => t.trim()).filter(Boolean);
+ const applic = tokens.filter((t) => DB_SUBCOMMANDS.has(t));
+ const conditions = tokens.filter((t) => !DB_SUBCOMMANDS.has(t));
+ if (applic.length) appliesTo = applic;
+ description =
+ (conditions.length ? `(${conditions.join(", ")}) ` : "") + paren[2];
+ }
+ }
+ flags.push({ name: m[1], value: m[2], description: description.trim(), appliesTo });
+ } else {
+ const cont = line.trim();
+ if (flags.length && cont) flags[flags.length - 1].description += ` ${cont}`;
+ }
+ }
+ return flags;
+}
+
+// ── Render ───────────────────────────────────────────────────────────────────
+const generatedMarker = (): string =>
+ `{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from \`${CLI_NAME} --help\` (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}**. Run \`${RUNNER} ${CLI_NAME}@${CLI_VERSION} --help\` to see the live command surface.
+`;
+}
+
+function flagsTable(flags: Flag[]): string {
+ if (!flags.length) return "";
+ const rows = flags
+ .map((f) => {
+ const opt = `\`${f.name}${f.value ? ` ${f.value}` : ""}\``;
+ return `| ${opt} | ${f.description.replace(/\|/g, "\\|")} |`;
+ })
+ .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}\``,
+ "",
+ 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(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 = GROUP_ORDER.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, "-")}` : "";
+ return `| [\`${c.path}\`](/reference/cli/${base}${anchor}) | ${c.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(groups: Map): string {
+ const pages: string[] = [];
+ for (const g of GROUP_ORDER) {
+ 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 {
+ // Swap point: return JSON.parse(execSync(`npx ${CLI_NAME}@${CLI_VERSION} manifest --json`)).
+ return parseHelp(loadHelp(CLI_VERSION));
+}
+
+function main() {
+ CLI_VERSION = latestVersion();
+ const manifest = loadManifest();
+
+ // 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);
+
+ const groups = new Map();
+ for (const base of bases) {
+ const g = GROUPS[base] ?? "Other";
+ if (!groups.has(g)) groups.set(g, []);
+ groups.get(g)!.push(base);
+ }
+
+ // 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(groups));
+
+ console.log(
+ `✓ Generated ${count} CLI reference page(s) for ${CLI_NAME} v${manifest.version} → ${path.relative(process.cwd(), OUT_DIR)}`,
+ );
+}
+
+main();