diff --git a/.github/workflows/cli-manifest.yml b/.github/workflows/cli-manifest.yml new file mode 100644 index 0000000..3e87404 --- /dev/null +++ b/.github/workflows/cli-manifest.yml @@ -0,0 +1,186 @@ +# Watches for new `stash` CLI releases and opens a PR refreshing the CLI +# reference. +# +# The reference is generated from the CLI's own `manifest --json`, cached in +# scripts/fixtures/stash-manifest.json and committed alongside the pages it +# produces. The version is pinned in that script, and rendering asserts the +# fixture matches the pin, so a bumped pin with a stale fixture fails the build +# rather than shipping. The build reads the fixture and never the network: what +# deploys is exactly what is in the repo. +# +# That is the fix this workflow exists to complete. The generator used to +# resolve and invoke the published CLI on every build, falling back to the +# fixture if it could not. On Vercel it has never once succeeded — every +# production build log going back as far as they are retained carries the +# fallback, including builds where the fixture already matched the latest +# release and nothing looked wrong. +# +# Nothing failed, because a fallback that always works cannot fail. The first +# visible symptom was eleven days of a version-old reference after 1.1.1 +# shipped. Why the `npx` invocation fails on Vercel is still unknown: stderr +# was discarded on every one of those runs. +# +# Nothing runs `npx` any more. The refresh installs the pinned CLI into a temp +# directory and runs it from there, so the question stops mattering — and it +# inherits stderr, so if the equivalent ever fails it says why. +# +# A temp directory rather than a devDependency of this app, which was tried: +# `stash` needs zod 3, fumadocs needs zod 4, and adding it hoists zod 3 to the +# root, at which point the frontmatter schema stops inferring and the docs stop +# type-checking. A docs build should not be breakable by the CLI's dep tree. +# +# The refresh is not auto-merged. A CLI release can remove commands as well as +# add them — 1.1.1 dropped `db validate` and added `eql preflight` and +# `eql verify` — and hand-written pages link to command anchors, so the diff +# gets read. +name: CLI manifest + +on: + schedule: + # Daily. NOTE: GitHub only runs scheduled workflows from the DEFAULT + # branch, so this file must stay on the default branch to fire at all. + - cron: "30 6 * * *" + workflow_dispatch: + +env: + DOCS_BRANCH: main + +permissions: + contents: write + pull-requests: write + +concurrency: + group: cli-manifest + cancel-in-progress: false + +jobs: + refresh: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ env.DOCS_BRANCH }} + + - uses: oven-sh/setup-bun@v2 + with: + # Pinned rather than `latest`, matching docs.yml: a bun release + # should not change what this workflow commits on its own. + bun-version: 1.3.13 + + - run: bun install --frozen-lockfile + + - name: Compare the published CLI against the pinned one + id: check + run: | + set -euo pipefail + latest=$(npm view stash version) + pinned=$(node -p "/STASH_VERSION \?\? \"([^\"]+)\"/.exec(require('fs').readFileSync('scripts/generate-cli-docs.ts','utf8'))[1]") + echo "latest=$latest" >> "$GITHUB_OUTPUT" + echo "pinned=$pinned" >> "$GITHUB_OUTPUT" + if [ "$latest" = "$pinned" ]; then + echo "refresh=false" >> "$GITHUB_OUTPUT" + echo "CLI reference is current at $pinned." + else + echo "refresh=true" >> "$GITHUB_OUTPUT" + echo "Published stash is $latest; the pin is $pinned." + fi + + # Bumping the pin and regenerating are one action, not two: the generator + # refuses to render a fixture whose version disagrees with the pin, so a + # PR that did only the first would be red on arrival. + - name: Bump the pin and regenerate + if: steps.check.outputs.refresh == 'true' + env: + FROM: ${{ steps.check.outputs.pinned }} + TO: ${{ steps.check.outputs.latest }} + run: | + set -euo pipefail + node -e ' + const fs = require("fs"); + const file = "scripts/generate-cli-docs.ts"; + const before = fs.readFileSync(file, "utf8"); + const after = before.replace( + `STASH_VERSION ?? "${process.env.FROM}"`, + `STASH_VERSION ?? "${process.env.TO}"`, + ); + if (after === before) throw new Error("Pin not found in " + file); + fs.writeFileSync(file, after); + ' + bun run generate-docs:cli:refresh + + # Named-anchor and cross-link breakage from removed commands shows up + # here. Reported, never fatal — a broken link is what the PR is for. + - name: Check links against the regenerated pages + if: steps.check.outputs.refresh == 'true' + id: links + run: | + set +e + out=$(bun run validate-links 2>&1) + code=$? + echo "$out" + { + echo "status=$([ $code -eq 0 ] && echo clean || echo failing)" + echo "log<> "$GITHUB_OUTPUT" + + - name: Open or update the refresh PR + if: steps.check.outputs.refresh == 'true' + env: + GH_TOKEN: ${{ github.token }} + FROM: ${{ steps.check.outputs.pinned }} + TO: ${{ steps.check.outputs.latest }} + LINKS_STATUS: ${{ steps.links.outputs.status }} + LINKS_LOG: ${{ steps.links.outputs.log }} + run: | + set -euo pipefail + branch="chore/cli-manifest-${TO}" + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git checkout -b "$branch" + git add scripts/generate-cli-docs.ts scripts/fixtures/stash-manifest.json content/docs/reference/cli + git commit -m "docs: refresh the CLI reference for stash ${TO}" + git push --force-with-lease -u origin "$branch" + + if [ "$LINKS_STATUS" = "clean" ]; then + verdict="**Links check passes.** Every internal link and anchor still resolves, so this is a read of the command diff rather than a repair job." + else + verdict="**Links check fails.** Something links to a command or anchor that \`${TO}\` no longer has; the failures are below." + fi + + body=$(cat <Links check output + + \`\`\` + ${LINKS_LOG} + \`\`\` + + + + --- + + Opened automatically by \`.github/workflows/cli-manifest.yml\`. + EOF + ) + + open=$(gh pr list --head "$branch" --state open --json number --jq 'length') + if [ "$open" != "0" ]; then + gh pr edit "$branch" --body "$body" + echo "Updated the existing PR for $branch" + else + gh pr create \ + --base "${DOCS_BRANCH}" \ + --head "$branch" \ + --title "docs: refresh the CLI reference for stash ${TO}" \ + --body "$body" + fi diff --git a/content/docs/reference/cli/auth.mdx b/content/docs/reference/cli/auth.mdx index c0963c0..4d4c52a 100644 --- a/content/docs/reference/cli/auth.mdx +++ b/content/docs/reference/cli/auth.mdx @@ -4,13 +4,13 @@ description: "Reference for the `stash auth` commands." type: reference components: [cli] verifiedAgainst: - cli: "1.0.0" + cli: "1.1.1" --- -{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash manifest --json` (v1.0.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */} +{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from the committed `stash manifest --json` fixture (v1.1.1). To describe a newer release: bump CLI_VERSION_PIN in that script, run `bun run generate-docs:cli:refresh`, and commit the result. */} -Generated from **`stash` v1.0.0** via `npx stash@1.0.0 manifest --json`. Run `npx stash@1.0.0 --help` to see the live command surface. +Generated from **`stash` v1.1.1**, via that release's own `manifest --json`. Run `npx stash@1.1.1 --help` to see the live command surface. The `stash auth` command group. @@ -28,7 +28,7 @@ Runs the OAuth 2.0 device authorization flow: npx stash auth login [flags] ``` -#### Flags +#### stash auth login flags | Flag | Description | | --- | --- | @@ -40,7 +40,7 @@ npx stash auth login [flags] | `--prisma` | Track Prisma as the referrer. | -#### Examples +#### stash auth login examples ```bash npx stash auth login @@ -57,14 +57,14 @@ List the regions you can authenticate against npx stash auth regions [flags] ``` -#### Flags +#### stash auth regions flags | Flag | Description | | --- | --- | | `--json` | Emit machine-readable [\{ slug, label \}] instead of a text list. | -#### Examples +#### stash auth regions examples ```bash npx stash auth regions diff --git a/content/docs/reference/cli/db.mdx b/content/docs/reference/cli/db.mdx index 0d0e6ca..a1e312b 100644 --- a/content/docs/reference/cli/db.mdx +++ b/content/docs/reference/cli/db.mdx @@ -4,34 +4,17 @@ description: "Reference for the `stash db` commands." type: reference components: [cli, eql] verifiedAgainst: - cli: "1.0.0" + cli: "1.1.1" --- -{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash manifest --json` (v1.0.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */} +{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from the committed `stash manifest --json` fixture (v1.1.1). To describe a newer release: bump CLI_VERSION_PIN in that script, run `bun run generate-docs:cli:refresh`, and commit the result. */} -Generated from **`stash` v1.0.0** via `npx stash@1.0.0 manifest --json`. Run `npx stash@1.0.0 --help` to see the live command surface. +Generated from **`stash` v1.1.1**, via that release's own `manifest --json`. Run `npx stash@1.1.1 --help` to see the live command surface. The `stash db` command group. -### `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 ` | Database URL for this run only — never written to disk. Highest precedence in the resolution order: --database-url flag → DATABASE_URL env → supabase status → interactive prompt. A stash.config.ts is not a separate tier (its default databaseUrl re-runs this same chain); a hand-set literal databaseUrl in the config bypasses the resolver and wins over all of these. (env: `DATABASE_URL`) | - - ### `db migrate` Run pending encrypt config migrations (not yet implemented) @@ -48,7 +31,7 @@ Test database connectivity npx stash db test-connection [flags] ``` -#### Flags +#### stash db test-connection flags | Flag | Description | | --- | --- | diff --git a/content/docs/reference/cli/doctor.mdx b/content/docs/reference/cli/doctor.mdx index 176e425..4ab7b54 100644 --- a/content/docs/reference/cli/doctor.mdx +++ b/content/docs/reference/cli/doctor.mdx @@ -4,13 +4,13 @@ description: "Diagnose install problems (native binaries, runtime)" type: reference components: [cli] verifiedAgainst: - cli: "1.0.0" + cli: "1.1.1" --- -{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash manifest --json` (v1.0.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */} +{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from the committed `stash manifest --json` fixture (v1.1.1). To describe a newer release: bump CLI_VERSION_PIN in that script, run `bun run generate-docs:cli:refresh`, and commit the result. */} -Generated from **`stash` v1.0.0** via `npx stash@1.0.0 manifest --json`. Run `npx stash@1.0.0 --help` to see the live command surface. +Generated from **`stash` v1.1.1**, via that release's own `manifest --json`. Run `npx stash@1.1.1 --help` to see the live command surface. Diagnose install problems (native binaries, runtime) diff --git a/content/docs/reference/cli/encrypt.mdx b/content/docs/reference/cli/encrypt.mdx index 126a6c8..d63d353 100644 --- a/content/docs/reference/cli/encrypt.mdx +++ b/content/docs/reference/cli/encrypt.mdx @@ -4,13 +4,13 @@ description: "Reference for the `stash encrypt` commands." type: reference components: [cli, eql] verifiedAgainst: - cli: "1.0.0" + cli: "1.1.1" --- -{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash manifest --json` (v1.0.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */} +{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from the committed `stash manifest --json` fixture (v1.1.1). To describe a newer release: bump CLI_VERSION_PIN in that script, run `bun run generate-docs:cli:refresh`, and commit the result. */} -Generated from **`stash` v1.0.0** via `npx stash@1.0.0 manifest --json`. Run `npx stash@1.0.0 --help` to see the live command surface. +Generated from **`stash` v1.1.1**, via that release's own `manifest --json`. Run `npx stash@1.1.1 --help` to see the live command surface. The `stash encrypt` command group. @@ -39,7 +39,7 @@ Resumably encrypt plaintext into the encrypted column npx stash encrypt backfill [flags] ``` -#### Flags +#### stash encrypt backfill flags | Flag | Description | | --- | --- | @@ -61,7 +61,7 @@ Generate a migration to drop the plaintext column npx stash encrypt drop [flags] ``` -#### Flags +#### stash encrypt drop flags | Flag | Description | | --- | --- | diff --git a/content/docs/reference/cli/env.mdx b/content/docs/reference/cli/env.mdx index e489e33..f3d37c9 100644 --- a/content/docs/reference/cli/env.mdx +++ b/content/docs/reference/cli/env.mdx @@ -4,13 +4,13 @@ description: "Mint deployment credentials and print them as env vars" type: reference components: [cli] verifiedAgainst: - cli: "1.0.0" + cli: "1.1.1" --- -{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash manifest --json` (v1.0.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */} +{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from the committed `stash manifest --json` fixture (v1.1.1). To describe a newer release: bump CLI_VERSION_PIN in that script, run `bun run generate-docs:cli:refresh`, and commit the result. */} -Generated from **`stash` v1.0.0** via `npx stash@1.0.0 manifest --json`. Run `npx stash@1.0.0 --help` to see the live command surface. +Generated from **`stash` v1.1.1**, via that release's own `manifest --json`. Run `npx stash@1.1.1 --help` to see the live command surface. Mints a fresh ZeroKMS client and a CipherStash access key from your diff --git a/content/docs/reference/cli/eql.mdx b/content/docs/reference/cli/eql.mdx index 813e1bb..b9a2c22 100644 --- a/content/docs/reference/cli/eql.mdx +++ b/content/docs/reference/cli/eql.mdx @@ -4,17 +4,49 @@ description: "Reference for the `stash eql` commands." type: reference components: [cli, eql] verifiedAgainst: - cli: "1.0.0" + cli: "1.1.1" --- -{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash manifest --json` (v1.0.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */} +{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from the committed `stash manifest --json` fixture (v1.1.1). To describe a newer release: bump CLI_VERSION_PIN in that script, run `bun run generate-docs:cli:refresh`, and commit the result. */} -Generated from **`stash` v1.0.0** via `npx stash@1.0.0 manifest --json`. Run `npx stash@1.0.0 --help` to see the live command surface. +Generated from **`stash` v1.1.1**, via that release's own `manifest --json`. Run `npx stash@1.1.1 --help` to see the live command surface. The `stash eql` command group. +### `eql preflight` + +Read-only: probes the connected role — superuser, membership of +`postgres`, CREATE on the database and on `public`, pgcrypto, and +whether the EQL v3 schemas already exist — and names the statement +each gap blocks. Exits 1 when a gap would abort `eql install`. + +Membership of `postgres` is reported but never blocks: on managed +platforms whose role is not a member (e.g. Lovable), `eql install` +skips the optional owner-scoped ALTER DEFAULT PRIVILEGES statements +— the install is complete without them, since stash re-grants every +object on each install/upgrade. + +```bash +npx stash eql preflight [flags] +``` + +#### stash eql preflight flags + +| Flag | Description | +| --- | --- | +| `--json` | Emit the machine-readable preflight result instead of the table. | +| `--database-url ` | Database URL for this run only — never written to disk. Highest precedence in the resolution order: --database-url flag → DATABASE_URL env → supabase status → interactive prompt. A stash.config.ts is not a separate tier (its default databaseUrl re-runs this same chain); a hand-set literal databaseUrl in the config bypasses the resolver and wins over all of these. (env: `DATABASE_URL`) | + + +#### stash eql preflight examples + +```bash +npx stash eql preflight +npx stash eql preflight --json +``` + ### `eql install` Scaffold stash.config.ts (if missing) and install EQL extensions @@ -23,7 +55,7 @@ Scaffold stash.config.ts (if missing) and install EQL extensions npx stash eql install [flags] ``` -#### Flags +#### stash eql install flags | Flag | Description | | --- | --- | @@ -33,31 +65,85 @@ npx stash eql install [flags] | `--database-url ` | Database URL for this run only — never written to disk. Highest precedence in the resolution order: --database-url flag → DATABASE_URL env → supabase status → interactive prompt. A stash.config.ts is not a separate tier (its default databaseUrl re-runs this same chain); a hand-set literal databaseUrl in the config bypasses the resolver and wins over all of these. (env: `DATABASE_URL`) | +### `eql verify` + +Compare what the database actually has against everything the pinned +EQL v3 bundle installs — every domain, function overload, operator, +cast, and the ORE operator class — via read-only catalog queries. A +partial install (domains present, some comparison functions or +operators absent) reports success at install time and fails at query +time on a specific predicate; this is the check that catches it. + +Expected absences read as such: on managed Postgres the bundle +legitimately skips the ORE operator class (creating it requires +superuser) and poisons the `_ord_ore` domains to fail loudly — that +is a supported configuration, reported as info. + +Exit 0 means exactly one thing: the surface was checked and found +complete. Damage, EQL not installed, and a version mismatch with +the pinned bundle all exit 1 — on a mismatch the object-level diff +is skipped (the pinned bundle is the wrong manifest to compare +against) and the command suggests `eql upgrade` (or a one-shot +`eql install --force --database-url ...` where no stash.config.ts +exists — `eql upgrade` requires one). + +Runs automatically at the end of `stash eql install`, on the +fresh-install path and the already-installed early exit alike. +There, only damage fails the install — a version mismatch warns +and continues, keeping a no-op re-run over an older EQL exit 0 +for idempotent provisioning scripts. + +```bash +npx stash eql verify [flags] +``` + +#### stash eql verify flags + +| Flag | Description | +| --- | --- | +| `--json` | Emit the machine-readable verification report instead of the table. | +| `--database-url ` | One-shot, like `eql install`'s: bypasses config loading entirely, so the database you name is the database that gets judged. Also settable via DATABASE_URL. (env: `DATABASE_URL`) | + + +#### stash eql verify examples + +```bash +npx stash eql verify +npx stash eql verify --json +``` + ### `eql migration` -Generate an EQL v3 install migration for your ORM (Drizzle; Prisma Next installs EQL through its own migrations) +Migration-first is the preferred way to install EQL: it lands in your +migration history and ships to every environment through the same +migrate step as the rest of your schema. On Supabase it is the only +durable way — `supabase db reset` replays the migrations directory, so +a direct `eql install` is wiped by the next reset. ```bash npx stash eql migration [flags] ``` -#### Flags +#### stash eql migration flags | Flag | Description | | --- | --- | | `--drizzle` | Emit a Drizzle custom migration containing the EQL v3 install SQL. | | `--prisma` | Not needed: Prisma Next installs EQL through its own migration framework — run `prisma-next migrate` instead. | -| `--supabase` | Append the Supabase role grants (eql_v3 + eql_v3_internal for anon/authenticated/service_role). | +| `--supabase` | On its own, write the install into supabase/migrations/ so it survives `supabase db reset`. With --drizzle, instead append the Supabase role grants (eql_v3 + eql_v3_internal for anon/authenticated/service_role) to the Drizzle migration. | | `--name ` | Name for the generated migration (Drizzle). Letters, numbers, dashes, underscores only. Defaults to `install-eql`. | -| `--out ` | Directory drizzle-kit writes the migration into (passed to `drizzle-kit generate --out`). Defaults to `drizzle`; set it to match your drizzle.config.ts. | +| `--out ` | Where the migration is written. Drizzle: your drizzle.config.ts `out` decides that, and stash follows the path drizzle-kit reports — this is only the fallback directory to look in (defaults to `drizzle`) if it reports none. Supabase: leave it alone. The Supabase CLI replays `supabase/migrations` and has no setting to move it, so pointing elsewhere means `supabase db reset` / `db push` never apply the install; the command warns when you do. | +| `--force` | Write a Supabase install migration even though one already exists. Not needed for --drizzle (drizzle-kit numbers each generated migration). | | `--dry-run` | Show what would happen without making changes. | -#### Examples +#### stash eql migration examples ```bash npx stash eql migration --drizzle npx stash eql migration --drizzle --supabase +npx stash eql migration --supabase +npx stash eql migration --supabase --force ``` ### `eql repair` @@ -83,7 +169,7 @@ relation, finds nothing, and reports applied state as unverified. npx stash eql repair [flags] ``` -#### Flags +#### stash eql repair flags | Flag | Description | | --- | --- | @@ -94,7 +180,7 @@ npx stash eql repair [flags] | `--database-url ` | Database URL for this run only — never written to disk. Highest precedence in the resolution order: --database-url flag → DATABASE_URL env → supabase status → interactive prompt. A stash.config.ts is not a separate tier (its default databaseUrl re-runs this same chain); a hand-set literal databaseUrl in the config bypasses the resolver and wins over all of these. (env: `DATABASE_URL`) | -#### Examples +#### stash eql repair examples ```bash npx stash eql repair --drizzle @@ -110,7 +196,7 @@ Upgrade EQL extensions to the latest version npx stash eql upgrade [flags] ``` -#### Flags +#### stash eql upgrade flags | Flag | Description | | --- | --- | @@ -127,8 +213,48 @@ Show EQL installation status npx stash eql status [flags] ``` -#### Flags +#### stash eql status flags + +| Flag | Description | +| --- | --- | +| `--database-url ` | Database URL for this run only — never written to disk. Highest precedence in the resolution order: --database-url flag → DATABASE_URL env → supabase status → interactive prompt. A stash.config.ts is not a separate tier (its default databaseUrl re-runs this same chain); a hand-set literal databaseUrl in the config bypasses the resolver and wins over all of these. (env: `DATABASE_URL`) | + + +### `eql validate` + +Read the tables passed to `Encryption({ schemas })` and check each encrypted +column against the EQL v3 domain vocabulary — then, if a database is +reachable, against what that database actually has. + +Schema checks (no database needed): an `_ord_ore` domain, whose ORE operator +class only a superuser can create; storage-only columns, reported so an +unsearchable column is a decision rather than a surprise; and hand-authored +configs that ask for free-text match on a non-text domain, encrypted-JSONB +search without `types.Json`, or a searchable boolean. + +Database checks (skipped with a notice when no database is reachable): +declared columns missing from the database, a column whose domain has drifted +from the schema, an `_ord_ore` column on a database whose EQL install could +not create the ORE operator class, and queryable columns with no functional +index over their term extractor. + +Exits 1 on errors only — warnings and info do not fail the command. + +```bash +npx stash eql validate [flags] +``` + +#### stash eql validate flags | Flag | Description | | --- | --- | +| `--supabase` | Use Supabase-compatible mode. | | `--database-url ` | Database URL for this run only — never written to disk. Highest precedence in the resolution order: --database-url flag → DATABASE_URL env → supabase status → interactive prompt. A stash.config.ts is not a separate tier (its default databaseUrl re-runs this same chain); a hand-set literal databaseUrl in the config bypasses the resolver and wins over all of these. (env: `DATABASE_URL`) | + + +#### stash eql validate examples + +```bash +npx stash eql validate +npx stash eql validate --database-url postgres://… +``` diff --git a/content/docs/reference/cli/impl.mdx b/content/docs/reference/cli/impl.mdx index 6ca64d3..7a1b9c3 100644 --- a/content/docs/reference/cli/impl.mdx +++ b/content/docs/reference/cli/impl.mdx @@ -4,13 +4,13 @@ description: "Execute the plan with a local agent" type: reference components: [cli] verifiedAgainst: - cli: "1.0.0" + cli: "1.1.1" --- -{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash manifest --json` (v1.0.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */} +{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from the committed `stash manifest --json` fixture (v1.1.1). To describe a newer release: bump CLI_VERSION_PIN in that script, run `bun run generate-docs:cli:refresh`, and commit the result. */} -Generated from **`stash` v1.0.0** via `npx stash@1.0.0 manifest --json`. Run `npx stash@1.0.0 --help` to see the live command surface. +Generated from **`stash` v1.1.1**, via that release's own `manifest --json`. Run `npx stash@1.1.1 --help` to see the live command surface. Execute the plan with a local agent @@ -24,7 +24,7 @@ npx stash impl [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. | +| `--target ` | Skip the agent-target picker and hand off directly to one of claude-code \| codex \| agents-md \| lovable \| wizard. Safe in non-TTY contexts. | ## stash impl examples diff --git a/content/docs/reference/cli/index.mdx b/content/docs/reference/cli/index.mdx index eadd67e..e13f7d3 100644 --- a/content/docs/reference/cli/index.mdx +++ b/content/docs/reference/cli/index.mdx @@ -1,20 +1,20 @@ --- title: CLI navTitle: Overview -description: "Command reference for the stash CLI, generated from v1.0.0." +description: "Command reference for the stash CLI, generated from v1.1.1." type: reference components: [cli] verifiedAgainst: - cli: "1.0.0" + cli: "1.1.1" --- -{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash manifest --json` (v1.0.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */} +{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from the committed `stash manifest --json` fixture (v1.1.1). To describe a newer release: bump CLI_VERSION_PIN in that script, run `bun run generate-docs:cli:refresh`, and commit the result. */} -Generated from **`stash` v1.0.0** via `npx stash@1.0.0 manifest --json`. Run `npx stash@1.0.0 --help` to see the live command surface. +Generated from **`stash` v1.1.1**, via that release's own `manifest --json`. Run `npx stash@1.1.1 --help` to see the live command surface. -The `stash` CLI. Install with `npx stash@1.0.0`. Every command accepts `--help` and `--version`. +The `stash` CLI. Install with `npx stash@1.1.1`. Every command accepts `--help` and `--version`. ### Setup & workflow @@ -40,17 +40,19 @@ The `stash` CLI. Install with `npx stash@1.0.0`. Every command accepts `--help` | Command | Description | | --- | --- | +| [`eql preflight`](/reference/cli/eql#eql-preflight) | Report whether this database role can install EQL, before trying | | [`eql install`](/reference/cli/eql#eql-install) | Scaffold stash.config.ts (if missing) and install EQL extensions | -| [`eql migration`](/reference/cli/eql#eql-migration) | Generate an EQL v3 install migration for your ORM (Drizzle; Prisma Next installs EQL through its own migrations) | +| [`eql verify`](/reference/cli/eql#eql-verify) | Check the installed EQL surface is complete, not just present | +| [`eql migration`](/reference/cli/eql#eql-migration) | Generate an EQL v3 install migration (Drizzle, or supabase/migrations/; Prisma Next installs EQL through its own migrations) | | [`eql repair`](/reference/cli/eql#eql-repair) | Repair migrations drizzle-kit generated with an un-runnable ALTER COLUMN to an encrypted type | | [`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 | +| [`eql validate`](/reference/cli/eql#eql-validate) | Validate your encryption schema against EQL v3 | ### Database | Command | Description | | --- | --- | -| [`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 | diff --git a/content/docs/reference/cli/init.mdx b/content/docs/reference/cli/init.mdx index 472ac10..80297bb 100644 --- a/content/docs/reference/cli/init.mdx +++ b/content/docs/reference/cli/init.mdx @@ -4,13 +4,13 @@ description: "Initialize CipherStash for your project" type: reference components: [cli] verifiedAgainst: - cli: "1.0.0" + cli: "1.1.1" --- -{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash manifest --json` (v1.0.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */} +{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from the committed `stash manifest --json` fixture (v1.1.1). To describe a newer release: bump CLI_VERSION_PIN in that script, run `bun run generate-docs:cli:refresh`, and commit the result. */} -Generated from **`stash` v1.0.0** via `npx stash@1.0.0 manifest --json`. Run `npx stash@1.0.0 --help` to see the live command surface. +Generated from **`stash` v1.1.1**, via that release's own `manifest --json`. Run `npx stash@1.1.1 --help` to see the live command surface. Set up CipherStash end-to-end: authenticate, introspect your database, @@ -30,6 +30,7 @@ npx stash init [flags] | `--drizzle` | Use Drizzle-specific setup flow. | | `--prisma` | Use Prisma Next-specific setup flow (EQL bundle installed via prisma-next migrate). | | `--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`) | +| `--target ` | Which agent to install the bundled skills for: claude-code (.claude/skills) or codex (.codex/skills). Skips agent detection. Unlike `plan --target` and `impl --target`, this selects the skills destination only — init performs no handoff. agents-md, lovable and wizard install no skill directories (those handoffs inline the skills instead), so passing one here installs nothing. | ## stash init examples @@ -39,4 +40,5 @@ npx stash init npx stash init --supabase npx stash init --prisma npx stash init --region us-east-1 +npx stash init --target claude-code ``` diff --git a/content/docs/reference/cli/manifest.mdx b/content/docs/reference/cli/manifest.mdx index 767b098..aa005ce 100644 --- a/content/docs/reference/cli/manifest.mdx +++ b/content/docs/reference/cli/manifest.mdx @@ -4,13 +4,13 @@ description: "Print the structured, versioned command surface" type: reference components: [cli] verifiedAgainst: - cli: "1.0.0" + cli: "1.1.1" --- -{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash manifest --json` (v1.0.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */} +{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from the committed `stash manifest --json` fixture (v1.1.1). To describe a newer release: bump CLI_VERSION_PIN in that script, run `bun run generate-docs:cli:refresh`, and commit the result. */} -Generated from **`stash` v1.0.0** via `npx stash@1.0.0 manifest --json`. Run `npx stash@1.0.0 --help` to see the live command surface. +Generated from **`stash` v1.1.1**, via that release's own `manifest --json`. Run `npx stash@1.1.1 --help` to see the live command surface. Emit the CLI command surface as data. `--json` produces the machine- diff --git a/content/docs/reference/cli/plan.mdx b/content/docs/reference/cli/plan.mdx index e3dc7a3..6a5dac8 100644 --- a/content/docs/reference/cli/plan.mdx +++ b/content/docs/reference/cli/plan.mdx @@ -4,13 +4,13 @@ description: "Draft a reviewable encryption plan at .cipherstash/plan.md" type: reference components: [cli] verifiedAgainst: - cli: "1.0.0" + cli: "1.1.1" --- -{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash manifest --json` (v1.0.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */} +{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from the committed `stash manifest --json` fixture (v1.1.1). To describe a newer release: bump CLI_VERSION_PIN in that script, run `bun run generate-docs:cli:refresh`, and commit the result. */} -Generated from **`stash` v1.0.0** via `npx stash@1.0.0 manifest --json`. Run `npx stash@1.0.0 --help` to see the live command surface. +Generated from **`stash` v1.1.1**, via that release's own `manifest --json`. Run `npx stash@1.1.1 --help` to see the live command surface. Draft a reviewable encryption plan at .cipherstash/plan.md @@ -25,7 +25,7 @@ npx stash plan [flags] | --- | --- | | `--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. Needs confirmation — an interactive prompt, or --yes non-interactively (else it exits non-zero without drafting). | | `--yes` | Confirm --complete-rollout's gate-skip without a prompt (for automation / CI). No effect without --complete-rollout. | -| `--target ` | Skip the agent-target picker and hand off directly to one of claude-code \| codex \| agents-md \| wizard. Safe in non-TTY contexts. | +| `--target ` | Skip the agent-target picker and hand off directly to one of claude-code \| codex \| agents-md \| lovable \| wizard. Safe in non-TTY contexts. | ## stash plan examples diff --git a/content/docs/reference/cli/schema.mdx b/content/docs/reference/cli/schema.mdx index ada1471..fea644c 100644 --- a/content/docs/reference/cli/schema.mdx +++ b/content/docs/reference/cli/schema.mdx @@ -4,13 +4,13 @@ description: "Reference for the `stash schema` commands." type: reference components: [cli, eql] verifiedAgainst: - cli: "1.0.0" + cli: "1.1.1" --- -{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash manifest --json` (v1.0.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */} +{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from the committed `stash manifest --json` fixture (v1.1.1). To describe a newer release: bump CLI_VERSION_PIN in that script, run `bun run generate-docs:cli:refresh`, and commit the result. */} -Generated from **`stash` v1.0.0** via `npx stash@1.0.0 manifest --json`. Run `npx stash@1.0.0 --help` to see the live command surface. +Generated from **`stash` v1.1.1**, via that release's own `manifest --json`. Run `npx stash@1.1.1 --help` to see the live command surface. The `stash schema` command group. @@ -23,7 +23,7 @@ Build an encryption schema from your database npx stash schema build [flags] ``` -#### Flags +#### stash schema build flags | Flag | Description | | --- | --- | diff --git a/content/docs/reference/cli/status.mdx b/content/docs/reference/cli/status.mdx index 9fcd239..5d3c002 100644 --- a/content/docs/reference/cli/status.mdx +++ b/content/docs/reference/cli/status.mdx @@ -4,13 +4,13 @@ description: "Displays implementation status" type: reference components: [cli] verifiedAgainst: - cli: "1.0.0" + cli: "1.1.1" --- -{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash manifest --json` (v1.0.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */} +{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from the committed `stash manifest --json` fixture (v1.1.1). To describe a newer release: bump CLI_VERSION_PIN in that script, run `bun run generate-docs:cli:refresh`, and commit the result. */} -Generated from **`stash` v1.0.0** via `npx stash@1.0.0 manifest --json`. Run `npx stash@1.0.0 --help` to see the live command surface. +Generated from **`stash` v1.1.1**, via that release's own `manifest --json`. Run `npx stash@1.1.1 --help` to see the live command surface. Displays implementation status diff --git a/content/docs/reference/cli/telemetry.mdx b/content/docs/reference/cli/telemetry.mdx index afebed6..e06868c 100644 --- a/content/docs/reference/cli/telemetry.mdx +++ b/content/docs/reference/cli/telemetry.mdx @@ -4,13 +4,13 @@ description: "Manage anonymous usage analytics" type: reference components: [cli] verifiedAgainst: - cli: "1.0.0" + cli: "1.1.1" --- -{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash manifest --json` (v1.0.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */} +{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from the committed `stash manifest --json` fixture (v1.1.1). To describe a newer release: bump CLI_VERSION_PIN in that script, run `bun run generate-docs:cli:refresh`, and commit the result. */} -Generated from **`stash` v1.0.0** via `npx stash@1.0.0 manifest --json`. Run `npx stash@1.0.0 --help` to see the live command surface. +Generated from **`stash` v1.1.1**, via that release's own `manifest --json`. Run `npx stash@1.1.1 --help` to see the live command surface. Manage the anonymous, opt-out usage analytics the CLI collects to diff --git a/content/docs/reference/cli/wizard.mdx b/content/docs/reference/cli/wizard.mdx index f3e6956..1226c07 100644 --- a/content/docs/reference/cli/wizard.mdx +++ b/content/docs/reference/cli/wizard.mdx @@ -4,13 +4,13 @@ description: "AI-guided encryption setup (reads your codebase)" type: reference components: [cli] verifiedAgainst: - cli: "1.0.0" + cli: "1.1.1" --- -{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from `stash manifest --json` (v1.0.0). Re-run `bun run generate-docs:cli` to refresh from the latest published CLI. */} +{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from the committed `stash manifest --json` fixture (v1.1.1). To describe a newer release: bump CLI_VERSION_PIN in that script, run `bun run generate-docs:cli:refresh`, and commit the result. */} -Generated from **`stash` v1.0.0** via `npx stash@1.0.0 manifest --json`. Run `npx stash@1.0.0 --help` to see the live command surface. +Generated from **`stash` v1.1.1**, via that release's own `manifest --json`. Run `npx stash@1.1.1 --help` to see the live command surface. AI-guided encryption setup (reads your codebase) diff --git a/package.json b/package.json index f851fd1..28258d6 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "generate-docs:eql": "tsx scripts/generate-eql-docs.ts", "generate-docs:eql-api": "tsx scripts/generate-eql-api-docs.ts", "generate-docs:cli": "tsx scripts/generate-cli-docs.ts", + "generate-docs:cli:refresh": "tsx scripts/generate-cli-docs.ts --refresh", "check-eql-pin": "tsx scripts/check-eql-pin.ts", "validate-links": "tsx scripts/validate-links.ts", "validate-redirects": "tsx scripts/validate-v2-redirects.ts", diff --git a/scripts/fixtures/stash-manifest.json b/scripts/fixtures/stash-manifest.json index 9c54546..5dd077c 100644 --- a/scripts/fixtures/stash-manifest.json +++ b/scripts/fixtures/stash-manifest.json @@ -1,6 +1,6 @@ { "name": "stash", - "version": "1.0.0", + "version": "1.1.1", "groups": [ { "title": "Setup & workflow", @@ -13,7 +13,8 @@ "init", "init --supabase", "init --prisma", - "init --region us-east-1" + "init --region us-east-1", + "init --target claude-code" ], "flags": [ { @@ -33,6 +34,11 @@ "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": "--target", + "value": "", + "description": "Which agent to install the bundled skills for: claude-code (.claude/skills) or codex (.codex/skills). Skips agent detection. Unlike `plan --target` and `impl --target`, this selects the skills destination only — init performs no handoff. agents-md, lovable and wizard install no skill directories (those handoffs inline the skills instead), so passing one here installs nothing." } ] }, @@ -56,7 +62,7 @@ { "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." + "description": "Skip the agent-target picker and hand off directly to one of claude-code | codex | agents-md | lovable | wizard. Safe in non-TTY contexts." } ] }, @@ -76,7 +82,7 @@ { "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." + "description": "Skip the agent-target picker and hand off directly to one of claude-code | codex | agents-md | lovable | wizard. Safe in non-TTY contexts." } ] }, @@ -110,7 +116,10 @@ "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"], + "examples": [ + "manifest --json", + "manifest" + ], "flags": [ { "name": "--json", @@ -175,7 +184,10 @@ { "name": "auth regions", "summary": "List the regions you can authenticate against", - "examples": ["auth regions", "auth regions --json"], + "examples": [ + "auth regions", + "auth regions --json" + ], "flags": [ { "name": "--json", @@ -188,6 +200,27 @@ { "title": "EQL", "commands": [ + { + "name": "eql preflight", + "summary": "Report whether this database role can install EQL, before trying", + "long": "Read-only: probes the connected role — superuser, membership of\n`postgres`, CREATE on the database and on `public`, pgcrypto, and\nwhether the EQL v3 schemas already exist — and names the statement\neach gap blocks. Exits 1 when a gap would abort `eql install`.\n\nMembership of `postgres` is reported but never blocks: on managed\nplatforms whose role is not a member (e.g. Lovable), `eql install`\nskips the optional owner-scoped ALTER DEFAULT PRIVILEGES statements\n— the install is complete without them, since stash re-grants every\nobject on each install/upgrade.", + "examples": [ + "eql preflight", + "eql preflight --json" + ], + "flags": [ + { + "name": "--json", + "description": "Emit the machine-readable preflight result instead of the table." + }, + { + "name": "--database-url", + "value": "", + "description": "Database URL for this run only — never written to disk. Highest precedence in the resolution order: --database-url flag → DATABASE_URL env → supabase status → interactive prompt. A stash.config.ts is not a separate tier (its default databaseUrl re-runs this same chain); a hand-set literal databaseUrl in the config bypasses the resolver and wins over all of these.", + "env": "DATABASE_URL" + } + ] + }, { "name": "eql install", "summary": "Scaffold stash.config.ts (if missing) and install EQL extensions", @@ -212,12 +245,36 @@ } ] }, + { + "name": "eql verify", + "summary": "Check the installed EQL surface is complete, not just present", + "long": "Compare what the database actually has against everything the pinned\nEQL v3 bundle installs — every domain, function overload, operator,\ncast, and the ORE operator class — via read-only catalog queries. A\npartial install (domains present, some comparison functions or\noperators absent) reports success at install time and fails at query\ntime on a specific predicate; this is the check that catches it.\n\nExpected absences read as such: on managed Postgres the bundle\nlegitimately skips the ORE operator class (creating it requires\nsuperuser) and poisons the `_ord_ore` domains to fail loudly — that\nis a supported configuration, reported as info.\n\nExit 0 means exactly one thing: the surface was checked and found\ncomplete. Damage, EQL not installed, and a version mismatch with\nthe pinned bundle all exit 1 — on a mismatch the object-level diff\nis skipped (the pinned bundle is the wrong manifest to compare\nagainst) and the command suggests `eql upgrade` (or a one-shot\n`eql install --force --database-url ...` where no stash.config.ts\nexists — `eql upgrade` requires one).\n\nRuns automatically at the end of `stash eql install`, on the\nfresh-install path and the already-installed early exit alike.\nThere, only damage fails the install — a version mismatch warns\nand continues, keeping a no-op re-run over an older EQL exit 0\nfor idempotent provisioning scripts.", + "examples": [ + "eql verify", + "eql verify --json" + ], + "flags": [ + { + "name": "--json", + "description": "Emit the machine-readable verification report instead of the table." + }, + { + "name": "--database-url", + "value": "", + "description": "One-shot, like `eql install`'s: bypasses config loading entirely, so the database you name is the database that gets judged. Also settable via DATABASE_URL.", + "env": "DATABASE_URL" + } + ] + }, { "name": "eql migration", - "summary": "Generate an EQL v3 install migration for your ORM (Drizzle; Prisma Next installs EQL through its own migrations)", + "summary": "Generate an EQL v3 install migration (Drizzle, or supabase/migrations/; Prisma Next installs EQL through its own migrations)", + "long": "Migration-first is the preferred way to install EQL: it lands in your\nmigration history and ships to every environment through the same\nmigrate step as the rest of your schema. On Supabase it is the only\ndurable way — `supabase db reset` replays the migrations directory, so\na direct `eql install` is wiped by the next reset.", "examples": [ "eql migration --drizzle", - "eql migration --drizzle --supabase" + "eql migration --drizzle --supabase", + "eql migration --supabase", + "eql migration --supabase --force" ], "flags": [ { @@ -230,7 +287,7 @@ }, { "name": "--supabase", - "description": "Append the Supabase role grants (eql_v3 + eql_v3_internal for anon/authenticated/service_role)." + "description": "On its own, write the install into supabase/migrations/ so it survives `supabase db reset`. With --drizzle, instead append the Supabase role grants (eql_v3 + eql_v3_internal for anon/authenticated/service_role) to the Drizzle migration." }, { "name": "--name", @@ -240,7 +297,11 @@ { "name": "--out", "value": "", - "description": "Directory drizzle-kit writes the migration into (passed to `drizzle-kit generate --out`). Defaults to `drizzle`; set it to match your drizzle.config.ts." + "description": "Where the migration is written. Drizzle: your drizzle.config.ts `out` decides that, and stash follows the path drizzle-kit reports — this is only the fallback directory to look in (defaults to `drizzle`) if it reports none. Supabase: leave it alone. The Supabase CLI replays `supabase/migrations` and has no setting to move it, so pointing elsewhere means `supabase db reset` / `db push` never apply the install; the command warns when you do." + }, + { + "name": "--force", + "description": "Write a Supabase install migration even though one already exists. Not needed for --drizzle (drizzle-kit numbers each generated migration)." }, { "name": "--dry-run", @@ -315,24 +376,20 @@ "env": "DATABASE_URL" } ] - } - ] - }, - { - "title": "Database", - "commands": [ + }, { - "name": "db validate", - "summary": "Validate encryption schema", + "name": "eql validate", + "summary": "Validate your encryption schema against EQL v3", + "long": "Read the tables passed to `Encryption({ schemas })` and check each encrypted\ncolumn against the EQL v3 domain vocabulary — then, if a database is\nreachable, against what that database actually has.\n\nSchema checks (no database needed): an `_ord_ore` domain, whose ORE operator\nclass only a superuser can create; storage-only columns, reported so an\nunsearchable column is a decision rather than a surprise; and hand-authored\nconfigs that ask for free-text match on a non-text domain, encrypted-JSONB\nsearch without `types.Json`, or a searchable boolean.\n\nDatabase checks (skipped with a notice when no database is reachable):\ndeclared columns missing from the database, a column whose domain has drifted\nfrom the schema, an `_ord_ore` column on a database whose EQL install could\nnot create the ORE operator class, and queryable columns with no functional\nindex over their term extractor.\n\nExits 1 on errors only — warnings and info do not fail the command.", + "examples": [ + "eql validate", + "eql validate --database-url postgres://…" + ], "flags": [ { "name": "--supabase", "description": "Use Supabase-compatible mode." }, - { - "name": "--exclude-operator-family", - "description": "Skip operator family creation." - }, { "name": "--database-url", "value": "", @@ -340,7 +397,12 @@ "env": "DATABASE_URL" } ] - }, + } + ] + }, + { + "title": "Database", + "commands": [ { "name": "db migrate", "summary": "Run pending encrypt config migrations (not yet implemented)" diff --git a/scripts/generate-cli-docs.ts b/scripts/generate-cli-docs.ts index 84be754..4300037 100644 --- a/scripts/generate-cli-docs.ts +++ b/scripts/generate-cli-docs.ts @@ -14,12 +14,45 @@ * 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`. + * The manifest is read from the committed `scripts/fixtures/stash-manifest.json`, + * NOT from the network. Every page carries `verifiedAgainst.cli` and a visible + * banner, so readers and agents always know which version the docs describe. + * + * The version is PINNED here, the same way `EQL_RELEASE_TAG` pins EQL in + * generate-eql-docs.ts, rather than being whatever npm serves that morning. + * Rendering asserts the fixture matches the pin, so bumping one without + * regenerating the other fails the build — which is exactly the drift this + * file exists to prevent, caught without touching the network. + * + * `--refresh` installs the pinned CLI into a temp directory and runs it from + * there. A deliberate, separate step, run by .github/workflows/cli-manifest.yml + * on a schedule and by hand via `bun run generate-docs:cli:refresh`. + * + * It installs into a temp directory rather than depending on `stash` here, and + * that was tried first: `stash` needs zod 3, fumadocs needs zod 4, and adding + * it hoists zod 3 to the root, at which point the frontmatter schema in + * source.config.ts stops inferring and `page.data.navTitle` types as `{}`. A + * docs build should not be able to be broken by the CLI's dependency tree. + * + * This used to resolve and invoke the CLI on every build, falling back to the + * fixture when that failed. It has never once succeeded on Vercel. Every + * production build log carries the fallback, going back as far as the logs are + * retained: + * + * 2026-08-16 ⚠ Could not run stash@1.0.0; using cached fixture. + * 2026-08-18 ⚠ Could not run stash@1.1.0; using cached fixture. + * 2026-08-31 ⚠ Could not run stash@1.1.1; using cached fixture. + * + * The docs looked right for as long as the committed fixture happened to be + * the latest published version. When 1.1.x shipped, a failure that was already + * there simply became visible — as eleven days of a version-old reference. + * + * WHY the `npx` invocation failed there was never captured: it ran with + * `stdio: [..., "ignore"]`, so stderr was discarded on every one of those + * runs. It failed in a consistent ~8 seconds while `npm view` in the same + * script succeeded, so the registry was reachable. Nothing here runs `npx` any + * more — the CLI is installed, so the question stops mattering — but the + * refresh step inherits stderr, so if the equivalent ever fails it says why. */ import { execSync } from "node:child_process"; import fs from "node:fs"; @@ -27,8 +60,18 @@ 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 +// The CLI release the reference describes. To upgrade: bump this, run +// `bun run generate-docs:cli:refresh`, and read the command diff — a release +// removes commands as well as adding them. .github/workflows/cli-manifest.yml +// does all three and opens a PR. +const CLI_VERSION_PIN = process.env.STASH_VERSION ?? "1.1.1"; +// Refresh the fixture from npm instead of reading it. CI and humans only. +const REFRESH = process.argv.includes("--refresh"); +// Read back off the manifest in main(), not assumed from CLI_VERSION_PIN: the +// manifest is what the pages are actually built from, and readFixture() +// guarantees the two agree. +let CLI_VERSION = ""; +const RUNNER = "npx"; // how a READER runs the CLI, shown in docs. Not how this script obtains it. const FIXTURE = path.join( process.cwd(), "scripts/fixtures", @@ -99,55 +142,65 @@ 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; - } +function readFixture(): CliManifest { + if (!fs.existsSync(FIXTURE)) { throw new Error( - "Cannot resolve latest stash version (offline, no fixture).", + `No cached manifest at ${path.relative(process.cwd(), FIXTURE)}. Run \`bun run generate-docs:cli:refresh\` to create it.`, ); } + const manifest = JSON.parse(fs.readFileSync(FIXTURE, "utf8")) as CliManifest; + + // The gate. Bumping the pin without regenerating would ship a reference + // describing a version nobody is running. Costs no network. + if (manifest.version !== CLI_VERSION_PIN) { + throw new Error( + `Cached manifest is ${CLI_NAME} v${manifest.version} but the pin is v${CLI_VERSION_PIN}. Run \`bun run generate-docs:cli:refresh\` and commit the result.`, + ); + } + return manifest; } -// 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 { +// Install the pinned CLI into a throwaway directory and read its +// `manifest --json` from there, rewriting the fixture. +// +// Installed rather than run through `npx --yes stash@`, which is what +// this used to do and what never once worked on Vercel. dotenvx (the CLI's +// launcher) may print tips before the JSON, so slice from the first `{` to the +// last `}` defensively. stderr is inherited rather than discarded: when this +// fails, the reason is the only useful output. +function refreshFixture(): CliManifest { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "stash-manifest-")); try { - const out = execSync(`npx --yes ${CLI_NAME}@${version} manifest --json`, { - encoding: "utf8", - cwd: os.tmpdir(), - stdio: ["ignore", "pipe", "ignore"], + fs.writeFileSync( + path.join(dir, "package.json"), + `${JSON.stringify({ name: "stash-manifest-probe", private: true })}\n`, + ); + execSync(`bun add ${CLI_NAME}@${CLI_VERSION_PIN}`, { + cwd: dir, + stdio: ["ignore", "ignore", "inherit"], }); + const out = execSync( + `${path.join(dir, "node_modules", ".bin", CLI_NAME)} manifest --json`, + { encoding: "utf8", cwd: dir, stdio: ["ignore", "pipe", "inherit"] }, + ); 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)}…)`, + `\`${CLI_NAME} 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; + if (manifest.version !== CLI_VERSION_PIN) { + throw new Error( + `Installed ${CLI_NAME}@${CLI_VERSION_PIN} reported v${manifest.version} in its manifest.`, + ); + } 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.`, - ); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); } } @@ -189,11 +242,11 @@ function toManifest(m: CliManifest): Manifest { // ── 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. */}`; + `{/* GENERATED — do not edit. Produced by scripts/generate-cli-docs.ts from the committed \`${CLI_NAME} manifest --json\` fixture (v${CLI_VERSION}). To describe a newer release: bump CLI_VERSION_PIN in that script, run \`bun run generate-docs:cli:refresh\`, and commit the result. */}`; 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. +Generated from **\`${CLI_NAME}\` v${CLI_VERSION}**, via that release's own \`manifest --json\`. Run \`${RUNNER} ${CLI_NAME}@${CLI_VERSION} --help\` to see the live command surface. `; } @@ -238,11 +291,21 @@ function commandSection(cmd: Command, level: "##" | "###"): string { synopsis, "```", ]; + // Named after the command, for the same reason the single-command page names + // its examples section: a chunk lifted out of the page has to carry its own + // subject. On a group page it also stops the headings colliding — eql.mdx + // carried five identical "Examples" and eight identical "Flags", which is + // five and eight duplicate anchors as well as eight indistinguishable chunks. if (cmd.flags.length) - parts.push(flagsTable(cmd.flags).replace("### Flags", `${level}# Flags`)); + parts.push( + flagsTable(cmd.flags).replace( + "### Flags", + `${level}# ${CLI_NAME} ${cmd.path} flags`, + ), + ); if (cmd.examples.length) { parts.push( - `\n${level}# Examples\n`, + `\n${level}# ${CLI_NAME} ${cmd.path} examples\n`, "```bash", cmd.examples.join("\n"), "```", @@ -374,15 +437,12 @@ function renderMeta(manifest: Manifest, groups: Map): string { // ── Main ───────────────────────────────────────────────────────────────────── function loadManifest(): Manifest { - return toManifest(loadRawManifest(CLI_VERSION)); + // The manifest is authoritative for what to stamp, so the version is read + // back off it rather than assumed. + return toManifest(REFRESH ? refreshFixture() : readFixture()); } 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;