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();