diff --git a/.changeset/rename-db-install-to-eql-install.md b/.changeset/rename-db-install-to-eql-install.md new file mode 100644 index 00000000..d2138d18 --- /dev/null +++ b/.changeset/rename-db-install-to-eql-install.md @@ -0,0 +1,12 @@ +--- +"stash": minor +"@cipherstash/wizard": minor +--- + +Rename `stash db install`, `stash db upgrade`, and `stash db status` to +`stash eql install`, `stash eql upgrade`, and `stash eql status`. These +commands manage the EQL extension itself, so they now live under a dedicated +`eql` command group. The old `db` spellings keep working as deprecated +aliases that print a warning pointing at the new names. All help text, +hints, generated migration headers, and wizard steps now reference the +`eql` commands. diff --git a/e2e/tests/package-managers.e2e.test.ts b/e2e/tests/package-managers.e2e.test.ts index d6b788b2..85f65414 100644 --- a/e2e/tests/package-managers.e2e.test.ts +++ b/e2e/tests/package-managers.e2e.test.ts @@ -51,18 +51,18 @@ describe('CLI init providers — package-manager-aware Next Steps', () => { { label: 'base', create: createBaseProvider, - firstStep: (r) => `Set up your database: ${r} stash db install`, + firstStep: (r) => `Set up your database: ${r} stash eql install`, }, { label: 'drizzle', create: createDrizzleProvider, - firstStep: (r) => `Set up your database: ${r} stash db install --drizzle`, + firstStep: (r) => `Set up your database: ${r} stash eql install --drizzle`, }, { label: 'supabase', create: createSupabaseProvider, firstStep: (r) => - `Install EQL: ${r} stash db install --supabase (prompts for migration vs direct)`, + `Install EQL: ${r} stash eql install --supabase (prompts for migration vs direct)`, }, ] @@ -158,12 +158,12 @@ describe.skipIf(!authConfigured)( { pm: 'yarn' as const, lockfile: 'yarn.lock' }, ])('uses $pm runner when $lockfile is present', ({ pm, lockfile }) => { const out = runWizard({ lockfile }) - expect(out).toContain(`Run: ${RUNNER[pm]} stash db install`) + expect(out).toContain(`Run: ${RUNNER[pm]} stash eql install`) }) it('falls back to npx when no lockfile and no user agent', () => { const out = runWizard({}) - expect(out).toContain('Run: npx stash db install') + expect(out).toContain('Run: npx stash eql install') }) }) @@ -174,7 +174,7 @@ describe.skipIf(!authConfigured)( { pm: 'yarn' as const, userAgent: 'yarn/4.0.0 npm/? node/v20.0.0' }, ])('uses $pm runner when UA is $userAgent', ({ pm, userAgent }) => { const out = runWizard({ userAgent }) - expect(out).toContain(`Run: ${RUNNER[pm]} stash db install`) + expect(out).toContain(`Run: ${RUNNER[pm]} stash eql install`) }) }) @@ -184,7 +184,7 @@ describe.skipIf(!authConfigured)( lockfile: 'pnpm-lock.yaml', userAgent: 'bun/1.1.40 npm/? node/v22.3.0', }) - expect(out).toContain('Run: bunx stash db install') + expect(out).toContain('Run: bunx stash eql install') }) it('npm user agent is ignored in favour of a lockfile', () => { @@ -192,7 +192,7 @@ describe.skipIf(!authConfigured)( lockfile: 'bun.lock', userAgent: 'npm/10.2.4 node/v20.0.0', }) - expect(out).toContain('Run: bunx stash db install') + expect(out).toContain('Run: bunx stash eql install') }) }) }, diff --git a/packages/cli/AGENTS.md b/packages/cli/AGENTS.md index 5efd2a7b..c4dc360f 100644 --- a/packages/cli/AGENTS.md +++ b/packages/cli/AGENTS.md @@ -71,7 +71,7 @@ exercise the same code paths. tweak only needs to land in one place. Add to `messages.ts` only when a test actually asserts on the string — premature extraction is worse than copy-paste here. For literals tests don't touch (e.g. command - names like `init`, `db install`), keep them inline. + names like `init`, `eql install`), keep them inline. - **Telemetry.** The CLI source no longer imports `posthog-node` (analytics moved to `packages/wizard`). The dep is still listed in `package.json` and should be removed in a follow-up. If you re-introduce telemetry to diff --git a/packages/cli/README.md b/packages/cli/README.md index ba63754f..8e61e42c 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -61,7 +61,7 @@ export default defineConfig({ The CLI loads `.env` files automatically before reading the config, so `process.env` references work without extra setup. The config file is resolved by walking up from the current working directory. -Commands that consume `stash.config.ts`: `db install`, `db upgrade`, `db push`, `db validate`, `db status`, `db test-connection`, `schema build`. `db install` will scaffold `stash.config.ts` for you if it's missing. +Commands that consume `stash.config.ts`: `eql install`, `eql upgrade`, `db push`, `db validate`, `eql status`, `db test-connection`, `schema build`. `eql install` will scaffold `stash.config.ts` for you if it's missing. --- @@ -83,10 +83,10 @@ npx stash init [--supabase] [--drizzle] What `init` does, in order: 1. **Authenticate** — re-uses an existing token if found, otherwise opens the browser device-code flow. -2. **Resolve `DATABASE_URL`** — flag → env → `supabase status` → interactive prompt → hard-fail. The same resolver `db install` uses. +2. **Resolve `DATABASE_URL`** — flag → env → `supabase status` → interactive prompt → hard-fail. The same resolver `eql install` uses. 3. **Generate the encryption client** — connects to your database, lists tables, and prompts you to multi-select which columns to encrypt. Writes `./src/encryption/index.ts` with the right shape for the detected ORM (Drizzle / Supabase / plain Postgres). Falls back to a placeholder if the database has no tables yet. 4. **Install dependencies** — `@cipherstash/stack` (runtime) and `stash` (dev), with a confirmation prompt. -5. **Install EQL** — runs `stash db install` against the resolved URL after a y/N confirm. +5. **Install EQL** — runs `stash eql install` against the resolved URL after a y/N confirm. 6. **Hand off** — four-option menu (Claude Code / Codex / CipherStash Agent / write `AGENTS.md`). See the Quickstart section above for what each option writes and spawns. The full pipeline state — integration, columns, env-key names, paths, versions — is captured in `.cipherstash/context.json`. The action plan at `.cipherstash/setup-prompt.md` tells whichever agent picks up next what's already done and what's left. @@ -156,14 +156,14 @@ npx stash secrets delete -n DATABASE_URL -e production -y --- -### `npx stash db install` +### `npx stash eql install` -Configure your database and install CipherStash EQL extensions in a single command. Run this after `npx stash init`. +Configure your database and install CipherStash EQL extensions in a single command. Run this after `npx stash init`. (`npx stash db install` is a deprecated alias — it still works but prints a warning.) When `stash.config.ts` is missing, the command auto-detects your encryption client file (or asks for the path) and writes the config before installing. Supabase and Drizzle are detected from your `DATABASE_URL` and project files, so the matching flags default on. Install uses bundled SQL for offline, deterministic runs. ```bash -npx stash db install [options] +npx stash eql install [options] ``` | Flag | Description | @@ -183,12 +183,12 @@ The `--supabase` flag uses a Supabase-specific SQL variant and grants `USAGE`, t --- -### `npx stash db upgrade` +### `npx stash eql upgrade` Upgrade an existing EQL installation to the version bundled with the package (or the latest from GitHub). ```bash -npx stash db upgrade [options] +npx stash eql upgrade [options] ``` | Flag | Description | @@ -198,7 +198,7 @@ npx stash db upgrade [options] | `--exclude-operator-family` | Skip operator family creation | | `--latest` | Fetch the latest EQL from GitHub | -The install SQL is idempotent and safe to re-run. If EQL is not installed, the command suggests running `npx stash db install` instead. +The install SQL is idempotent and safe to re-run. If EQL is not installed, the command suggests running `npx stash eql install` instead. --- @@ -260,12 +260,12 @@ npx stash db migrate --- -### `npx stash db status` +### `npx stash eql status` Show the current state of EQL in your database. ```bash -npx stash db status +npx stash eql status ``` Reports EQL installation status and version, database permission status, and whether an active encrypt config exists in `eql_v2_configuration` (relevant only for CipherStash Proxy). @@ -300,10 +300,10 @@ Reads `databaseUrl` from `stash.config.ts`. ## Drizzle migration mode -Use `--drizzle` with `npx stash db install` to add EQL installation to your Drizzle migration history instead of applying it directly. `--drizzle` is auto-detected when your project has `drizzle-orm`, `drizzle-kit`, or a `drizzle.config.*` file, so you usually don't need to pass it explicitly. +Use `--drizzle` with `npx stash eql install` to add EQL installation to your Drizzle migration history instead of applying it directly. `--drizzle` is auto-detected when your project has `drizzle-orm`, `drizzle-kit`, or a `drizzle.config.*` file, so you usually don't need to pass it explicitly. ```bash -npx stash db install --drizzle +npx stash eql install --drizzle npx drizzle-kit migrate ``` @@ -315,7 +315,7 @@ How it works: With a custom name or output directory: ```bash -npx stash db install --drizzle --name setup-eql --out ./migrations +npx stash eql install --drizzle --name setup-eql --out ./migrations npx drizzle-kit migrate ``` diff --git a/packages/cli/src/__tests__/supabase-migration.test.ts b/packages/cli/src/__tests__/supabase-migration.test.ts index ea3b0955..f5b3b75a 100644 --- a/packages/cli/src/__tests__/supabase-migration.test.ts +++ b/packages/cli/src/__tests__/supabase-migration.test.ts @@ -18,7 +18,7 @@ import { SUPABASE_PERMISSIONS_SQL } from '../installer/index.js' * Mirrors the production function but imported for testing. */ function migrationHeader(runner: string): string { - return `-- CipherStash EQL — installed by \`${runner} stash db install --supabase --migration\`. + return `-- CipherStash EQL — installed by \`${runner} stash eql install --supabase --migration\`. -- -- This migration installs the CipherStash Encrypt Query Language (EQL) types, -- functions, and operators into the \`eql_v2\` schema, then grants Supabase's @@ -136,7 +136,7 @@ describe('writeSupabaseEqlMigration', () => { const contents = fs.readFileSync(result.path, 'utf-8') // Header comment block includes the detected runner instruction expect(contents).toMatch( - /-- CipherStash EQL — installed by `(npx|bunx|pnpm dlx|yarn dlx) stash db install --supabase --migration`/, + /-- CipherStash EQL — installed by `(npx|bunx|pnpm dlx|yarn dlx) stash eql install --supabase --migration`/, ) expect(contents).toContain('CipherStash') // EQL SQL body — the bundled supabase variant defines eql_v2. @@ -253,18 +253,18 @@ describe('migrationHeader', () => { it('renders the header with the provided runner for npx', () => { const header = migrationHeader('npx') expect(header).toContain( - '-- CipherStash EQL — installed by `npx stash db install --supabase --migration`.', + '-- CipherStash EQL — installed by `npx stash eql install --supabase --migration`.', ) }) it('renders the header with the provided runner for bunx', () => { const header = migrationHeader('bunx') - expect(header).toContain('bunx stash db install') + expect(header).toContain('bunx stash eql install') }) it('renders the header with the provided runner for pnpm dlx', () => { const header = migrationHeader('pnpm dlx') - expect(header).toContain('pnpm dlx stash db install') + expect(header).toContain('pnpm dlx stash eql install') }) it('includes all expected documentation lines', () => { diff --git a/packages/cli/src/bin/main.ts b/packages/cli/src/bin/main.ts index c9cbbc27..c6160178 100644 --- a/packages/cli/src/bin/main.ts +++ b/packages/cli/src/bin/main.ts @@ -90,13 +90,14 @@ Commands: wizard AI-guided encryption setup (reads your codebase) doctor Diagnose install problems (native binaries, runtime) - db install Scaffold stash.config.ts (if missing) and install EQL extensions - db upgrade Upgrade EQL extensions to the latest version + eql install Scaffold stash.config.ts (if missing) and install EQL extensions + eql upgrade Upgrade EQL extensions to the latest version + eql status Show EQL installation status + 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 @@ -146,17 +147,17 @@ Impl Flags: 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 +DB / EQL Flags: + --force (eql install) Reinstall / overwrite even if already installed + --dry-run (eql install, eql upgrade, db push) Show what would happen without making changes + --supabase (eql install, eql upgrade, db validate) Use Supabase-compatible mode (auto-detected from DATABASE_URL) + --drizzle (eql install) Generate a Drizzle migration instead of direct install (auto-detected from project) + --migration (eql install, requires --supabase) Write a Supabase migration file instead of running SQL directly + --direct (eql install, requires --supabase) Run the SQL directly against the database (mutually exclusive with --migration) + --migrations-dir (eql install, requires --supabase) Override the Supabase migrations directory (default: supabase/migrations) + --exclude-operator-family (eql install, eql upgrade, db validate) Skip operator family creation + --latest (eql install, eql upgrade) Fetch the latest EQL from GitHub + --database-url (all db / eql / schema commands) Override DATABASE_URL for this run only — never written to disk Examples: ${STASH} init @@ -169,7 +170,7 @@ Examples: ${STASH} status ${STASH} auth login ${STASH} wizard - ${STASH} db install + ${STASH} eql install ${STASH} db push ${STASH} schema build ${STASH} doctor @@ -212,6 +213,62 @@ function parseArgs(argv: string[]): ParsedArgs { return { command, subcommand, commandArgs, flags, values } } +async function runInstall( + flags: Record, + values: Record, +) { + await installCommand({ + force: flags.force, + dryRun: flags['dry-run'], + supabase: flags.supabase, + excludeOperatorFamily: flags['exclude-operator-family'], + drizzle: flags.drizzle, + latest: flags.latest, + name: values.name, + out: values.out, + migration: flags.migration, + direct: flags.direct, + migrationsDir: values['migrations-dir'], + databaseUrl: values['database-url'], + }) +} + +async function runUpgrade( + flags: Record, + values: Record, +) { + await upgradeCommand({ + dryRun: flags['dry-run'], + supabase: flags.supabase, + excludeOperatorFamily: flags['exclude-operator-family'], + latest: flags.latest, + databaseUrl: values['database-url'], + }) +} + +async function runEqlCommand( + sub: string | undefined, + flags: Record, + values: Record, +) { + switch (sub) { + case 'install': + await runInstall(flags, values) + break + case 'upgrade': + await runUpgrade(flags, values) + break + case 'status': + await dbStatusCommand({ databaseUrl: values['database-url'] }) + break + default: + p.log.error(`${messages.eql.unknownSubcommand}: ${sub ?? '(none)'}`) + console.log() + console.log(HELP) + process.exit(1) + } +} + async function runDbCommand( sub: string | undefined, flags: Record, @@ -222,30 +279,16 @@ async function runDbCommand( const databaseUrl = values['database-url'] switch (sub) { + // Deprecated aliases — these commands moved to the `eql` group. Keep the + // old spellings working so existing scripts and published docs don't + // break. case 'install': - await installCommand({ - force: flags.force, - dryRun: flags['dry-run'], - supabase: flags.supabase, - excludeOperatorFamily: flags['exclude-operator-family'], - drizzle: flags.drizzle, - latest: flags.latest, - name: values.name, - out: values.out, - migration: flags.migration, - direct: flags.direct, - migrationsDir: values['migrations-dir'], - databaseUrl, - }) + p.log.warn(messages.db.aliasDeprecated(STASH, 'install')) + await runInstall(flags, values) break case 'upgrade': - await upgradeCommand({ - dryRun: flags['dry-run'], - supabase: flags.supabase, - excludeOperatorFamily: flags['exclude-operator-family'], - latest: flags.latest, - databaseUrl, - }) + p.log.warn(messages.db.aliasDeprecated(STASH, 'upgrade')) + await runUpgrade(flags, values) break case 'push': { const { pushCommand } = await requireStack( @@ -273,6 +316,7 @@ async function runDbCommand( break } case 'status': + p.log.warn(messages.db.aliasDeprecated(STASH, 'status')) await dbStatusCommand({ databaseUrl }) break case 'test-connection': @@ -437,6 +481,9 @@ export async function run() { await authCommand(authArgs, flags) break } + case 'eql': + await runEqlCommand(subcommand, flags, values) + break case 'db': await runDbCommand(subcommand, flags, values) break diff --git a/packages/cli/src/commands/db/client-scaffold.ts b/packages/cli/src/commands/db/client-scaffold.ts index d04e26e2..5e438599 100644 --- a/packages/cli/src/commands/db/client-scaffold.ts +++ b/packages/cli/src/commands/db/client-scaffold.ts @@ -6,7 +6,7 @@ import { generatePlaceholderClient } from '../init/utils.js' import { detectDrizzle, detectSupabase } from './detect.js' /** - * Pick a placeholder template using the same signals `db install` already + * Pick a placeholder template using the same signals `eql install` already * detects. Drizzle wins over Supabase when both look present (a Drizzle-on- * Supabase project is more naturally scaffolded as Drizzle). */ @@ -25,7 +25,7 @@ function detectIntegration( * * `init`'s `buildSchemaStep` is the primary path that creates this file * (and handles the "file already exists" case interactively). This function - * exists as a safety net for users who run `db install` directly without + * exists as a safety net for users who run `eql install` directly without * `init` first — they still get a working client file rather than failing * later when the config tries to load a non-existent path. */ diff --git a/packages/cli/src/commands/db/config-scaffold.ts b/packages/cli/src/commands/db/config-scaffold.ts index b5e623c4..35947f32 100644 --- a/packages/cli/src/commands/db/config-scaffold.ts +++ b/packages/cli/src/commands/db/config-scaffold.ts @@ -87,7 +87,7 @@ export default defineConfig({ * Returns `true` if a config is present (either pre-existing or freshly * written), `false` if the user cancelled the prompt. * - * Invoked by `db install` when no `stash.config.ts` exists, so users don't + * Invoked by `eql install` when no `stash.config.ts` exists, so users don't * need to run a separate `setup` step before installing EQL. */ export async function ensureStashConfig( diff --git a/packages/cli/src/commands/db/detect.ts b/packages/cli/src/commands/db/detect.ts index 5900a0a8..93b681ee 100644 --- a/packages/cli/src/commands/db/detect.ts +++ b/packages/cli/src/commands/db/detect.ts @@ -34,7 +34,7 @@ export function detectSupabase(databaseUrl: string | undefined): boolean { export interface SupabaseProjectInfo { /** * Whether the migrations directory exists AND is a directory. Used to pick - * the migration-vs-direct default in the `db install --supabase` prompt. + * the migration-vs-direct default in the `eql install --supabase` prompt. */ hasMigrationsDir: boolean /** diff --git a/packages/cli/src/commands/db/install.ts b/packages/cli/src/commands/db/install.ts index 44bc70e8..2e0e82a2 100644 --- a/packages/cli/src/commands/db/install.ts +++ b/packages/cli/src/commands/db/install.ts @@ -71,7 +71,7 @@ export interface InstallOptions { export type SupabaseInstallMode = 'migration' | 'direct' export async function installCommand(options: InstallOptions) { - p.intro(runnerCommand(detectPackageManager(), 'stash db install')) + p.intro(runnerCommand(detectPackageManager(), 'stash eql install')) // Validate mutually-exclusive / supabase-required flags BEFORE doing any // I/O. `--migration` and `--direct` only make sense in the Supabase flow; @@ -84,7 +84,7 @@ export async function installCommand(options: InstallOptions) { process.exit(1) } - // Scaffold stash.config.ts if missing. `db install` is the single command + // Scaffold stash.config.ts if missing. `eql install` is the single command // that gets a project from zero to installed EQL — no separate setup step // (CIP-2986). const configReady = await ensureStashConfig() @@ -101,7 +101,7 @@ export async function installCommand(options: InstallOptions) { }) s.stop('Configuration loaded.') - // Safety net: if the user ran `db install` without first running `init`, + // Safety net: if the user ran `eql install` without first running `init`, // scaffold the encryption client file so config.client points somewhere // real. No-op when the file already exists. ensureEncryptionClient(config.client, process.cwd(), config.databaseUrl) @@ -139,7 +139,7 @@ export async function installCommand(options: InstallOptions) { // moving part we'd rather defer until someone needs it. if (options.latest) { p.log.error( - '`db install --supabase --migration --latest` is not yet supported. Please open an issue at https://github.com/cipherstash/stack/issues if you need this.', + '`eql install --supabase --migration --latest` is not yet supported. Please open an issue at https://github.com/cipherstash/stack/issues if you need this.', ) p.outro('Installation aborted.') process.exit(1) @@ -430,7 +430,7 @@ async function generateDrizzleMigration( // Step 4: Write the EQL SQL (and cs_migrations tracking schema) into // the migration file. Bundling both means `drizzle-kit migrate` rolls // everything needed for `stash encrypt ...` out to each environment - // in one go, rather than requiring an out-of-band `stash db install`. + // in one go, rather than requiring an out-of-band `stash eql install`. s.start('Writing EQL SQL into migration file...') const migrationContents = `${eqlSql}\n\n-- CipherStash encryption-migration tracking schema.\n-- Tracks per-column phase + backfill progress for \`stash encrypt\`.\n${MIGRATIONS_SCHEMA_SQL.trim()}\n` @@ -501,7 +501,7 @@ export function validateInstallFlags(options: InstallOptions): string | null { : null if (subFlag !== null && options.supabase !== true) { - return `\`${subFlag}\` requires \`--supabase\`. Re-run with \`db install --supabase ${subFlag}\`.` + return `\`${subFlag}\` requires \`--supabase\`. Re-run with \`eql install --supabase ${subFlag}\`.` } return null diff --git a/packages/cli/src/commands/db/status.ts b/packages/cli/src/commands/db/status.ts index 23979dee..d22abedc 100644 --- a/packages/cli/src/commands/db/status.ts +++ b/packages/cli/src/commands/db/status.ts @@ -6,7 +6,7 @@ import { EQLInstaller } from '@/installer/index.js' export async function statusCommand(options: { databaseUrl?: string } = {}) { const pm = detectPackageManager() - p.intro(runnerCommand(pm, 'stash db status')) + p.intro(runnerCommand(pm, 'stash eql status')) const s = p.spinner() @@ -44,7 +44,7 @@ export async function statusCommand(options: { databaseUrl?: string } = {}) { } else { s.stop('EQL is not installed.') p.log.warn( - `EQL is not installed. Run \`${runnerCommand(pm, 'stash db install')}\` to install it.`, + `EQL is not installed. Run \`${runnerCommand(pm, 'stash eql install')}\` to install it.`, ) p.outro('Status check complete.') return diff --git a/packages/cli/src/commands/db/supabase-migration.ts b/packages/cli/src/commands/db/supabase-migration.ts index 14d40aea..e1400b46 100644 --- a/packages/cli/src/commands/db/supabase-migration.ts +++ b/packages/cli/src/commands/db/supabase-migration.ts @@ -27,7 +27,7 @@ export const SUPABASE_EQL_MIGRATION_FILENAME = * package manager. */ function migrationHeader(runner: string): string { - return `-- CipherStash EQL — installed by \`${runner} stash db install --supabase --migration\`. + return `-- CipherStash EQL — installed by \`${runner} stash eql install --supabase --migration\`. -- -- This migration installs the CipherStash Encrypt Query Language (EQL) types, -- functions, and operators into the \`eql_v2\` schema, then grants Supabase's diff --git a/packages/cli/src/commands/db/upgrade.ts b/packages/cli/src/commands/db/upgrade.ts index 49fc2db7..492dddca 100644 --- a/packages/cli/src/commands/db/upgrade.ts +++ b/packages/cli/src/commands/db/upgrade.ts @@ -11,7 +11,7 @@ export async function upgradeCommand(options: { databaseUrl?: string }) { const pm = detectPackageManager() - p.intro(runnerCommand(pm, 'stash db upgrade')) + p.intro(runnerCommand(pm, 'stash eql upgrade')) const s = p.spinner() @@ -32,7 +32,7 @@ export async function upgradeCommand(options: { if (!installed) { s.stop('EQL is not installed.') p.log.warn( - `EQL is not currently installed. Run "${runnerCommand(pm, 'stash db install')}" first.`, + `EQL is not currently installed. Run "${runnerCommand(pm, 'stash eql install')}" first.`, ) p.outro('Upgrade aborted.') process.exit(1) diff --git a/packages/cli/src/commands/encrypt/drizzle-helper.ts b/packages/cli/src/commands/encrypt/drizzle-helper.ts index 019f39cf..fb775caf 100644 --- a/packages/cli/src/commands/encrypt/drizzle-helper.ts +++ b/packages/cli/src/commands/encrypt/drizzle-helper.ts @@ -6,7 +6,7 @@ import { detectPackageManager, runnerArgv } from '@/commands/init/utils.js' /** * Scaffold a custom Drizzle Kit migration file with a known name and write - * the supplied SQL into it. Mirrors the dance `db install --drizzle` already + * the supplied SQL into it. Mirrors the dance `eql install --drizzle` already * does — `drizzle-kit generate --custom` creates the file and records the * journal entry / snapshot, then we overwrite the empty body with our SQL. * diff --git a/packages/cli/src/commands/init/__tests__/utils.test.ts b/packages/cli/src/commands/init/__tests__/utils.test.ts index 9959843b..704dec34 100644 --- a/packages/cli/src/commands/init/__tests__/utils.test.ts +++ b/packages/cli/src/commands/init/__tests__/utils.test.ts @@ -146,8 +146,8 @@ describe('runnerCommand', () => { }) it('passes the package reference through verbatim (multi-word args allowed)', () => { - expect(runnerCommand('bun', 'stash db install')).toBe( - 'bunx stash db install', + expect(runnerCommand('bun', 'stash eql install')).toBe( + 'bunx stash eql install', ) }) }) diff --git a/packages/cli/src/commands/init/doctrine/AGENTS-doctrine.md b/packages/cli/src/commands/init/doctrine/AGENTS-doctrine.md index 7da721cb..6d6a464e 100644 --- a/packages/cli/src/commands/init/doctrine/AGENTS-doctrine.md +++ b/packages/cli/src/commands/init/doctrine/AGENTS-doctrine.md @@ -7,7 +7,7 @@ This document is the **durable rule book** for any agent working on this codebas ## What you are working with - **Encryption client** — a per-project module that defines which tables and columns are encrypted, what data type each column holds, and which search operations are enabled. The path is in `.cipherstash/context.json` under `encryptionClientPath`. -- **EQL extension** — a Postgres extension installed via `stash db install` that provides server-side functions for searchable encryption (`eql_v2.*`). Required before any migration that creates encrypted columns. +- **EQL extension** — a Postgres extension installed via `stash eql install` that provides server-side functions for searchable encryption (`eql_v2.*`). Required before any migration that creates encrypted columns. - **Project context** — `.cipherstash/context.json` records what `stash init` discovered: integration, package manager, env key names (never values), schemas, install command, CLI version. Treat it as authoritative. - **Action plan** — `.cipherstash/setup-prompt.md` is the project-specific to-do list for the current setup run. Read it first. @@ -18,7 +18,7 @@ This document is the **durable rule book** for any agent working on this codebas 3. **Never read or echo secrets.** Env key *names* (`CS_WORKSPACE_CRN`, `CS_CLIENT_ID`, `CS_CLIENT_KEY`, `CS_CLIENT_ACCESS_KEY`, `DATABASE_URL`) are fine to reference in code and docs. Their *values* are not. New env keys go in `.env.example` with placeholders; instruct the user to add the real value locally. 4. **Never invent CipherStash APIs.** If you don't know how a function is called, read the relevant skill (see below) — don't guess. The TypeScript types in `@cipherstash/stack` are the source of truth for what's callable. 5. **Never run database introspection yourself.** Don't run `psql`, `\d`, `pg_dump`, `supabase db dump`, or `drizzle-kit introspect`. The CLI already did this; the result is in `context.json`. If you need fresh introspection, ask the user to re-run `stash init`. -6. **Never modify these files.** `stash.config.ts` (generated by init — edits go in `.env`). `.cipherstash/` (CLI-owned). The `eql_v2` schema and `eql_v2_*` functions (CLI-managed; missing function ⇒ `stash db upgrade`, not a hand-edit). +6. **Never modify these files.** `stash.config.ts` (generated by init — edits go in `.env`). `.cipherstash/` (CLI-owned). The `eql_v2` schema and `eql_v2_*` functions (CLI-managed; missing function ⇒ `stash eql upgrade`, not a hand-edit). 7. **`@cipherstash/stack` must be excluded from any bundler.** The package wraps a native FFI module (`@cipherstash/protect-ffi`) that cannot be bundled. The moment you `npm install @cipherstash/stack` in a project with a bundler, configure the exclusion *before* writing any code that imports it. Concretely: Next.js needs `serverExternalPackages: ['@cipherstash/stack', '@cipherstash/protect-ffi']` in `next.config.{js,ts,mjs}`; webpack needs `externals` entries; esbuild needs `external`; Vite SSR needs `ssr.external`. Skipping this surfaces as `Cannot find module '@cipherstash/protect-ffi-*'` at runtime, often after the user has shipped to production. If you're declaring an encrypted column for the first time in a project, treat configuring this exclusion as part of the same change. ## Migrations — three phases, always reversible diff --git a/packages/cli/src/commands/init/lib/__tests__/setup-prompt.test.ts b/packages/cli/src/commands/init/lib/__tests__/setup-prompt.test.ts index 1db5a8a4..d44f760d 100644 --- a/packages/cli/src/commands/init/lib/__tests__/setup-prompt.test.ts +++ b/packages/cli/src/commands/init/lib/__tests__/setup-prompt.test.ts @@ -211,7 +211,7 @@ describe('renderSetupPrompt — plan mode (rollout, default)', () => { it('allows read-only inspection commands and points at `stash status`', () => { const out = renderSetupPrompt(planCtx) - expect(out).toMatch(/db status/) + expect(out).toMatch(/eql status/) expect(out).toContain('## Where am I?') expect(out).toMatch(/pnpm dlx stash status/) expect(out).toMatch(/Read-only/i) diff --git a/packages/cli/src/commands/init/lib/setup-prompt.ts b/packages/cli/src/commands/init/lib/setup-prompt.ts index 6419aeaf..98b40c58 100644 --- a/packages/cli/src/commands/init/lib/setup-prompt.ts +++ b/packages/cli/src/commands/init/lib/setup-prompt.ts @@ -129,7 +129,7 @@ const SKILL_PURPOSES: Record = { 'stash-dynamodb': 'DynamoDB encryption: per-item encrypt/decrypt, HMAC attribute keys, audit logging', 'stash-cli': - '`stash` command reference — `status`, `plan`, `impl`, `db install`, `encrypt {backfill,cutover,drop}`, etc.', + '`stash` command reference — `status`, `plan`, `impl`, `eql install`, `encrypt {backfill,cutover,drop}`, etc.', 'stash-secrets': 'storing and retrieving encrypted secrets (separate concern from column encryption)', 'stash-supply-chain-security': @@ -343,7 +343,7 @@ export function renderImplementPrompt(ctx: SetupPromptContext): string { * Plan-mode tells the agent its task is to produce a reviewable plan file * at `.cipherstash/plan.md` — no schema edits, no migrations, no `db push`, * no `encrypt *` mutations during this phase. Read-only inspection - * (`stash status`, `stash db status`, schema grep, file reads) is fine. + * (`stash status`, `stash eql status`, schema grep, file reads) is fine. * * Dispatches by `ctx.planStep`: * @@ -418,7 +418,7 @@ function planSharedNotDoBlock(ctx: SetupPromptContext): string[] { 'Modify the placeholder encryption client beyond what is required to read it.', ), '', - `Read-only commands (\`${cli} status\`, \`${cli} db status\`, file reads, greps, \`${cli} doctor\` if available) are fine and encouraged — the plan is more useful when grounded in the actual current state.`, + `Read-only commands (\`${cli} status\`, \`${cli} eql status\`, file reads, greps, \`${cli} doctor\` if available) are fine and encouraged — the plan is more useful when grounded in the actual current state.`, '', ] } @@ -513,7 +513,7 @@ function renderRolloutPlanPrompt(ctx: SetupPromptContext): string { : 'For migrate columns: what the rollout PR contains — schema-add and the exact dual-write code change. The dual-write definition matters: every persistence path that mutates the row writes both columns, in the same transaction, on every code branch.', ), bullet( - `Project-specific risks. Common ones: bundler exclusion not yet configured (Next.js / webpack / Vite), top-level-await in the placeholder encryption client breaks non-Next contexts, existing partial CipherStash state (run \`${cli} db status\` and note any pre-existing encrypted columns or pending configs).`, + `Project-specific risks. Common ones: bundler exclusion not yet configured (Next.js / webpack / Vite), top-level-await in the placeholder encryption client breaks non-Next contexts, existing partial CipherStash state (run \`${cli} eql status\` and note any pre-existing encrypted columns or pending configs).`, ), bullet( 'A "Deploy gate" section near the end of the plan that explicitly says: after the rollout PR is in production and serving real traffic, the user runs `' + @@ -626,7 +626,7 @@ function renderCutoverPlanPrompt(ctx: SetupPromptContext): string { ' encrypt drop --table --column `, plus the schema-migration apply step that follows.', ), bullet( - `Risks specific to cutover: row-count for the backfill (use \`${cli} db status\` to estimate if helpful), tables under heavy write load (cutover holds a brief lock on the rename), application code that constructs SQL by string (those reads won't transparently decrypt).`, + `Risks specific to cutover: row-count for the backfill (use \`${cli} eql status\` to estimate if helpful), tables under heavy write load (cutover holds a brief lock on the rename), application code that constructs SQL by string (those reads won't transparently decrypt).`, ), bullet( "Open questions for the user — anything you can't determine from the schema, context.json, or the skills.", @@ -700,7 +700,7 @@ function renderCompletePlanPrompt(ctx: SetupPromptContext): string { ), bullet('For new columns: the additive single-deploy walkthrough.'), bullet( - `Project-specific risks. Common ones: bundler exclusion not yet configured (Next.js / webpack / Vite), top-level-await in the placeholder encryption client breaks non-Next contexts, existing partial CipherStash state (run \`${cli} db status\` and note any pre-existing encrypted columns or pending configs).`, + `Project-specific risks. Common ones: bundler exclusion not yet configured (Next.js / webpack / Vite), top-level-await in the placeholder encryption client breaks non-Next contexts, existing partial CipherStash state (run \`${cli} eql status\` and note any pre-existing encrypted columns or pending configs).`, ), bullet( "Open questions for the user — anything you can't determine from the schema, context.json, or the skills.", diff --git a/packages/cli/src/commands/init/providers/__tests__/base.test.ts b/packages/cli/src/commands/init/providers/__tests__/base.test.ts index 773952b4..f5d58b29 100644 --- a/packages/cli/src/commands/init/providers/__tests__/base.test.ts +++ b/packages/cli/src/commands/init/providers/__tests__/base.test.ts @@ -6,13 +6,13 @@ describe('createBaseProvider getNextSteps', () => { it('uses npx when package manager is npm', () => { const steps = provider.getNextSteps({}, 'npm') - expect(steps[0]).toBe('Set up your database: npx stash db install') + expect(steps[0]).toBe('Set up your database: npx stash eql install') expect(steps[1]).toContain('npx stash wizard') }) it('uses bunx when package manager is bun', () => { const steps = provider.getNextSteps({}, 'bun') - expect(steps[0]).toBe('Set up your database: bunx stash db install') + expect(steps[0]).toBe('Set up your database: bunx stash eql install') expect(steps[1]).toContain('bunx stash wizard') // Sanity: the old hardcoded `npx` should be gone. for (const s of steps) expect(s).not.toMatch(/\bnpx\b/) @@ -20,13 +20,13 @@ describe('createBaseProvider getNextSteps', () => { it('uses pnpm dlx when package manager is pnpm', () => { const steps = provider.getNextSteps({}, 'pnpm') - expect(steps[0]).toBe('Set up your database: pnpm dlx stash db install') + expect(steps[0]).toBe('Set up your database: pnpm dlx stash eql install') expect(steps[1]).toContain('pnpm dlx stash wizard') }) it('uses yarn dlx when package manager is yarn', () => { const steps = provider.getNextSteps({}, 'yarn') - expect(steps[0]).toBe('Set up your database: yarn dlx stash db install') + expect(steps[0]).toBe('Set up your database: yarn dlx stash eql install') }) it('still includes the manual-edit suffix when clientFilePath is set', () => { diff --git a/packages/cli/src/commands/init/providers/__tests__/drizzle.test.ts b/packages/cli/src/commands/init/providers/__tests__/drizzle.test.ts index 43a417a6..ef6ccfe8 100644 --- a/packages/cli/src/commands/init/providers/__tests__/drizzle.test.ts +++ b/packages/cli/src/commands/init/providers/__tests__/drizzle.test.ts @@ -7,14 +7,14 @@ describe('createDrizzleProvider getNextSteps', () => { it('uses npx when package manager is npm', () => { const steps = provider.getNextSteps({}, 'npm') expect(steps[0]).toBe( - 'Set up your database: npx stash db install --drizzle', + 'Set up your database: npx stash eql install --drizzle', ) }) it('uses bunx when package manager is bun', () => { const steps = provider.getNextSteps({}, 'bun') expect(steps[0]).toBe( - 'Set up your database: bunx stash db install --drizzle', + 'Set up your database: bunx stash eql install --drizzle', ) expect(steps[1]).toContain('bunx stash wizard') for (const s of steps) expect(s).not.toMatch(/\bnpx\b/) @@ -23,14 +23,14 @@ describe('createDrizzleProvider getNextSteps', () => { it('uses pnpm dlx when package manager is pnpm', () => { const steps = provider.getNextSteps({}, 'pnpm') expect(steps[0]).toBe( - 'Set up your database: pnpm dlx stash db install --drizzle', + 'Set up your database: pnpm dlx stash eql install --drizzle', ) }) it('uses yarn dlx when package manager is yarn', () => { const steps = provider.getNextSteps({}, 'yarn') expect(steps[0]).toBe( - 'Set up your database: yarn dlx stash db install --drizzle', + 'Set up your database: yarn dlx stash eql install --drizzle', ) }) }) diff --git a/packages/cli/src/commands/init/providers/__tests__/prisma-next.test.ts b/packages/cli/src/commands/init/providers/__tests__/prisma-next.test.ts index 196e07e9..7e5abd61 100644 --- a/packages/cli/src/commands/init/providers/__tests__/prisma-next.test.ts +++ b/packages/cli/src/commands/init/providers/__tests__/prisma-next.test.ts @@ -4,12 +4,12 @@ import { createPrismaNextProvider } from '../prisma-next.js' describe('createPrismaNextProvider getNextSteps', () => { const provider = createPrismaNextProvider() - it('points at prisma-next migration plan + apply rather than stash db install', () => { + it('points at prisma-next migration plan + apply rather than stash eql install', () => { const steps = provider.getNextSteps({}, 'pnpm') // The whole story hinges on this: Prisma Next users never run - // `stash db install` — the framework handles the EQL bundle. + // `stash eql install` — the framework handles the EQL bundle. for (const step of steps) { - expect(step).not.toMatch(/stash db install/) + expect(step).not.toMatch(/stash eql install/) } const planApply = steps.find((s) => s.includes('migration plan')) expect(planApply).toBeDefined() diff --git a/packages/cli/src/commands/init/providers/__tests__/supabase.test.ts b/packages/cli/src/commands/init/providers/__tests__/supabase.test.ts index 0a9d2952..69304e52 100644 --- a/packages/cli/src/commands/init/providers/__tests__/supabase.test.ts +++ b/packages/cli/src/commands/init/providers/__tests__/supabase.test.ts @@ -7,14 +7,14 @@ describe('createSupabaseProvider getNextSteps', () => { it('uses npx when package manager is npm', () => { const steps = provider.getNextSteps({}, 'npm') expect(steps[0]).toBe( - 'Install EQL: npx stash db install --supabase (prompts for migration vs direct)', + 'Install EQL: npx stash eql install --supabase (prompts for migration vs direct)', ) }) it('uses bunx when package manager is bun', () => { const steps = provider.getNextSteps({}, 'bun') expect(steps[0]).toBe( - 'Install EQL: bunx stash db install --supabase (prompts for migration vs direct)', + 'Install EQL: bunx stash eql install --supabase (prompts for migration vs direct)', ) expect(steps[2]).toContain('bunx stash wizard') // wizard step is third for (const s of steps) expect(s).not.toMatch(/\bnpx\b/) @@ -22,13 +22,13 @@ describe('createSupabaseProvider getNextSteps', () => { it('uses pnpm dlx when package manager is pnpm', () => { const steps = provider.getNextSteps({}, 'pnpm') - expect(steps[0]).toContain('pnpm dlx stash db install --supabase') + expect(steps[0]).toContain('pnpm dlx stash eql install --supabase') }) it('uses yarn dlx when package manager is yarn', () => { const steps = provider.getNextSteps({}, 'yarn') expect(steps[0]).toBe( - 'Install EQL: yarn dlx stash db install --supabase (prompts for migration vs direct)', + 'Install EQL: yarn dlx stash eql install --supabase (prompts for migration vs direct)', ) expect(steps[2]).toContain('yarn dlx stash wizard') // Sanity: the supabase CLI commands stay untouched. diff --git a/packages/cli/src/commands/init/providers/base.ts b/packages/cli/src/commands/init/providers/base.ts index 1b74d117..47064d53 100644 --- a/packages/cli/src/commands/init/providers/base.ts +++ b/packages/cli/src/commands/init/providers/base.ts @@ -11,7 +11,7 @@ export function createBaseProvider(): InitProvider { ? `edit ${state.clientFilePath} directly` : 'edit your encryption schema directly' return [ - `Set up your database: ${cli} db install`, + `Set up your database: ${cli} eql install`, `Customize your schema: ${cli} wizard (AI-guided, automated) — or ${manualEdit}`, 'Quickstart: https://cipherstash.com/docs/stack/quickstart', 'Dashboard: https://dashboard.cipherstash.com/workspaces', diff --git a/packages/cli/src/commands/init/providers/drizzle.ts b/packages/cli/src/commands/init/providers/drizzle.ts index 16b5c73f..ac79167f 100644 --- a/packages/cli/src/commands/init/providers/drizzle.ts +++ b/packages/cli/src/commands/init/providers/drizzle.ts @@ -7,7 +7,7 @@ export function createDrizzleProvider(): InitProvider { introMessage: 'Setting up CipherStash for your Drizzle project...', getNextSteps(state: InitState, pm: PackageManager): string[] { const cli = runnerCommand(pm, 'stash') - const steps = [`Set up your database: ${cli} db install --drizzle`] + const steps = [`Set up your database: ${cli} eql install --drizzle`] const manualEdit = state.clientFilePath ? `edit ${state.clientFilePath} directly` diff --git a/packages/cli/src/commands/init/providers/prisma-next.ts b/packages/cli/src/commands/init/providers/prisma-next.ts index 74807757..ab70ab0d 100644 --- a/packages/cli/src/commands/init/providers/prisma-next.ts +++ b/packages/cli/src/commands/init/providers/prisma-next.ts @@ -8,7 +8,7 @@ export function createPrismaNextProvider(): InitProvider { // Note: Prisma Next absorbs the EQL bundle install and schema // scaffold steps via its migration framework. The next-steps list // below therefore points at `prisma-next migration plan|apply` - // instead of `stash db install`, and at `cipherstashFromStack` + // instead of `stash eql install`, and at `cipherstashFromStack` // instead of an `encryption/index.ts` placeholder. getNextSteps(_state, pm: PackageManager): string[] { const stash = runnerCommand(pm, 'stash') diff --git a/packages/cli/src/commands/init/providers/supabase.ts b/packages/cli/src/commands/init/providers/supabase.ts index 5c669fc5..d8b824d9 100644 --- a/packages/cli/src/commands/init/providers/supabase.ts +++ b/packages/cli/src/commands/init/providers/supabase.ts @@ -8,7 +8,7 @@ export function createSupabaseProvider(): InitProvider { getNextSteps(state: InitState, pm: PackageManager): string[] { const cli = runnerCommand(pm, 'stash') const steps = [ - `Install EQL: ${cli} db install --supabase (prompts for migration vs direct)`, + `Install EQL: ${cli} eql install --supabase (prompts for migration vs direct)`, 'Apply it: supabase db reset (local) or supabase migration up (remote)', ] diff --git a/packages/cli/src/commands/init/steps/build-schema.ts b/packages/cli/src/commands/init/steps/build-schema.ts index f23e4782..c6212a30 100644 --- a/packages/cli/src/commands/init/steps/build-schema.ts +++ b/packages/cli/src/commands/init/steps/build-schema.ts @@ -20,7 +20,7 @@ import { generatePlaceholderClient } from '../utils.js' const DEFAULT_CLIENT_PATH = './src/encryption/index.ts' /** - * Pick the integration template by reading the same signals `db install` + * Pick the integration template by reading the same signals `eql install` * uses — Drizzle config / dependency for `drizzle`, Supabase host in * `DATABASE_URL` for `supabase`, otherwise raw Postgres. Silent: never * prompts the user. diff --git a/packages/cli/src/commands/init/steps/install-deps.ts b/packages/cli/src/commands/init/steps/install-deps.ts index 1677dd03..7a77587e 100644 --- a/packages/cli/src/commands/init/steps/install-deps.ts +++ b/packages/cli/src/commands/init/steps/install-deps.ts @@ -17,7 +17,7 @@ const PRISMA_NEXT_PACKAGE = '@cipherstash/prisma-next' * * - `@cipherstash/stack` (prod) — the encryption client and per-integration * helpers (drizzle, supabase, schema). - * - `stash` (dev) — the CLI itself, so the user can run `stash db install`, + * - `stash` (dev) — the CLI itself, so the user can run `stash eql install`, * `stash wizard`, etc. as a project script without the global install. * * Skips silently when both are already present. Prompts before running the diff --git a/packages/cli/src/commands/init/steps/install-eql.ts b/packages/cli/src/commands/init/steps/install-eql.ts index 594fce0b..7dd48fb5 100644 --- a/packages/cli/src/commands/init/steps/install-eql.ts +++ b/packages/cli/src/commands/init/steps/install-eql.ts @@ -5,7 +5,7 @@ import { CancelledError } from '../types.js' import { isPackageInstalled } from '../utils.js' /** - * Run `stash db install` programmatically after a y/N confirm. + * Run `stash eql install` programmatically after a y/N confirm. * * EQL is the Postgres extension every CipherStash query relies on. Without * it, the encryption client can't read or write to encrypted columns. @@ -31,12 +31,12 @@ export const installEqlStep: InitStep = { // Prisma Next ships the EQL bundle as a baseline migration inside // `@cipherstash/prisma-next`. `prisma-next migration apply` runs // it in the same control-plane sweep as the user's application - // migrations — running `stash db install` here would be a + // migrations — running `stash eql install` here would be a // duplicate install and would race with the framework's // migration journal. Skip with guidance instead. if (integration === 'prisma-next' || provider.name === 'prisma-next') { p.log.success( - 'Skipping `stash db install` — Prisma Next installs the EQL bundle via `prisma-next migration apply` (runs alongside your app migrations).', + 'Skipping `stash eql install` — Prisma Next installs the EQL bundle via `prisma-next migration apply` (runs alongside your app migrations).', ) return { ...state, eqlInstalled: false } } @@ -55,7 +55,7 @@ export const installEqlStep: InitStep = { if (!proceed) { p.log.info('Skipping EQL installation.') p.note( - 'Run `stash db install` before applying any migration that references encrypted columns.', + 'Run `stash eql install` before applying any migration that references encrypted columns.', 'EQL not installed', ) return { ...state, eqlInstalled: false } @@ -91,7 +91,7 @@ export const installEqlStep: InitStep = { p.log.error( 'EQL install failed — check your database connection and try again.', ) - p.note('Re-run with: stash db install', 'You can retry manually') + p.note('Re-run with: stash eql install', 'You can retry manually') return { ...state, eqlInstalled: false } } diff --git a/packages/cli/src/commands/init/utils.ts b/packages/cli/src/commands/init/utils.ts index e3076fb7..7d8e3c4e 100644 --- a/packages/cli/src/commands/init/utils.ts +++ b/packages/cli/src/commands/init/utils.ts @@ -150,7 +150,7 @@ export function combinedInstallCommands( * yarn → `yarn dlx` * * `ref` is appended verbatim, so callers may pass `'stash'` or - * `'stash db install'`. + * `'stash eql install'`. */ export function runnerCommand(pm: PackageManager, ref: string): string { switch (pm) { diff --git a/packages/cli/src/messages.ts b/packages/cli/src/messages.ts index 822f87e2..9c21a2ea 100644 --- a/packages/cli/src/messages.ts +++ b/packages/cli/src/messages.ts @@ -35,8 +35,14 @@ export const messages = { selectRegion: 'Select a region', cancelled: 'Cancelled.', }, + eql: { + unknownSubcommand: 'Unknown eql subcommand', + }, db: { unknownSubcommand: 'Unknown db subcommand', + /** Warning shown when a deprecated `db ` alias for `eql ` is used. */ + aliasDeprecated: (stashRef: string, sub: string) => + `"${stashRef} db ${sub}" is deprecated — use "${stashRef} eql ${sub}" instead.`, migrateNotImplemented: (stashRef: string) => `"${stashRef} db migrate" is not yet implemented.`, /** Source labels surfaced after DATABASE_URL resolution. */ diff --git a/packages/cli/tests/e2e/runner-aware-help.e2e.test.ts b/packages/cli/tests/e2e/runner-aware-help.e2e.test.ts index 786f9cae..e8b1fa09 100644 --- a/packages/cli/tests/e2e/runner-aware-help.e2e.test.ts +++ b/packages/cli/tests/e2e/runner-aware-help.e2e.test.ts @@ -37,7 +37,7 @@ describe('--help — runner-aware Usage + Examples', () => { expect(r.output).toContain(`Usage: ${label} stash`) // At least one of the Examples lines must surface the same runner. expect(r.output).toContain(`${label} stash init`) - expect(r.output).toContain(`${label} stash db install`) + expect(r.output).toContain(`${label} stash eql install`) }) }) diff --git a/packages/cli/tests/e2e/smoke.e2e.test.ts b/packages/cli/tests/e2e/smoke.e2e.test.ts index 777f4e5a..f059e3c3 100644 --- a/packages/cli/tests/e2e/smoke.e2e.test.ts +++ b/packages/cli/tests/e2e/smoke.e2e.test.ts @@ -22,7 +22,9 @@ describe('stash CLI — non-interactive smoke', () => { // Command-list items — these are the literal command names users type, not // copy strings, so they stay inline. expect(r.output).toContain('init') - expect(r.output).toContain('db install') + expect(r.output).toContain('eql install') + expect(r.output).toContain('eql upgrade') + expect(r.output).toContain('eql status') // The dotenv "injected env" banner regression guard lives in the // dedicated test below — this cwd has no .env file, so a bare // `not.toContain('injected env')` here would pass vacuously. @@ -89,6 +91,36 @@ describe('stash CLI — non-interactive smoke', () => { expect(r.output).toContain('bogus-sub') }) + it('eql bogus-sub exits 1 with help', async () => { + const r = render(['eql', 'bogus-sub']) + const { exitCode } = await r.exit + expect(exitCode).toBe(1) + expect(r.output).toContain(messages.eql.unknownSubcommand) + expect(r.output).toContain('bogus-sub') + }) + + // `--migration` without `--supabase` fails flag validation before any I/O + // or prompt, so these two cases can observe the install entry path + // deterministically without a database. + it('db install still works as a deprecated alias and warns', async () => { + const r = render(['db', 'install', '--migration']) + const { exitCode } = await r.exit + expect(exitCode).toBe(1) + // Runner-aware factory — assert on the runner-agnostic suffix. + expect(r.output).toContain('stash db install" is deprecated') + expect(r.output).toContain('eql install" instead') + // The alias reaches the real install command (its flag validation ran). + expect(r.output).toContain('requires `--supabase`') + }) + + it('eql install routes to the install command without a deprecation warning', async () => { + const r = render(['eql', 'install', '--migration']) + const { exitCode } = await r.exit + expect(exitCode).toBe(1) + expect(r.output).not.toContain('is deprecated') + expect(r.output).toContain('requires `--supabase`') + }) + it('db migrate is a stub that exits 0 with a "not yet implemented" warning', async () => { const r = render(['db', 'migrate']) const { exitCode } = await r.exit diff --git a/packages/migrate/README.md b/packages/migrate/README.md index 2c7b38c4..37eb8a5c 100644 --- a/packages/migrate/README.md +++ b/packages/migrate/README.md @@ -12,7 +12,7 @@ Each column walks through these phases: schema-added → dual-writing → backfilling → backfilled → cut-over → dropped ``` -State is tracked in an append-only `cipherstash.cs_migrations` table installed by `stash db install`. The EQL intent (which indexes, which cast_as) continues to live in `eql_v2_configuration` so Proxy continues to work against the same database. +State is tracked in an append-only `cipherstash.cs_migrations` table installed by `stash eql install`. The EQL intent (which indexes, which cast_as) continues to live in `eql_v2_configuration` so Proxy continues to work against the same database. ## API @@ -32,7 +32,7 @@ import { ### `installMigrationsSchema(client)` -Creates `cipherstash.cs_migrations` idempotently. Normally called by `stash db install`. +Creates `cipherstash.cs_migrations` idempotently. Normally called by `stash eql install`. ### `runBackfill({ db, encryptionClient, tableSchema, tableName, plaintextColumn, encryptedColumn, pkColumn, schemaColumnKey, chunkSize?, signal?, onProgress? })` diff --git a/packages/migrate/src/eql.ts b/packages/migrate/src/eql.ts index 8c9b7089..f169442a 100644 --- a/packages/migrate/src/eql.ts +++ b/packages/migrate/src/eql.ts @@ -2,7 +2,7 @@ import type { ClientBase } from 'pg' /** * Thin, typed wrappers around the EQL (Encrypt Query Language) functions - * installed by `stash db install`. These mirror the canonical SQL API that + * installed by `stash eql install`. These mirror the canonical SQL API that * CipherStash Proxy also drives, so every action we take here stays * visible to Proxy using the same column-level config. * diff --git a/packages/migrate/src/install.ts b/packages/migrate/src/install.ts index 8bcd7945..2a3981a4 100644 --- a/packages/migrate/src/install.ts +++ b/packages/migrate/src/install.ts @@ -3,7 +3,7 @@ import type { ClientBase } from 'pg' /** * DDL for `cipherstash.cs_migrations` — the append-only per-column event * log that tracks encryption-migration runtime state (phase, backfill - * cursor, rows processed). Installed by `stash db install`. + * cursor, rows processed). Installed by `stash eql install`. * * All statements are `CREATE … IF NOT EXISTS` so running the installer * multiple times or alongside an existing deployment is safe. @@ -39,7 +39,7 @@ CREATE INDEX IF NOT EXISTS cs_migrations_column_id_desc /** * Create the `cipherstash` schema and `cs_migrations` table if they do not - * already exist. Safe to call on every `stash db install` invocation. + * already exist. Safe to call on every `stash eql install` invocation. * * Requires `CREATE SCHEMA` privileges on the database. If the caller lacks * them, the query will fail and the error bubbles up — the CLI currently diff --git a/packages/wizard/README.md b/packages/wizard/README.md index 5b9bd8b3..89965ce4 100644 --- a/packages/wizard/README.md +++ b/packages/wizard/README.md @@ -19,9 +19,9 @@ bunx @cipherstash/wizard # bun Before running the wizard, your project should have: -- `stash` available (the wizard shells out to `stash db install` / +- `stash` available (the wizard shells out to `stash eql install` / `db push` after the agent finishes editing) -- A `stash.config.ts` (or the wizard will run `stash db install` to scaffold one) +- A `stash.config.ts` (or the wizard will run `stash eql install` to scaffold one) - A reachable database via `DATABASE_URL` - An authenticated CipherStash session (`stash auth login`) @@ -32,7 +32,7 @@ Before running the wizard, your project should have: 3. Prompts you to pick the tables and columns to encrypt. 4. Hands a surgical prompt to the Claude Agent SDK, which edits your schema and call sites to use `@cipherstash/stack`'s encryption APIs. -5. Runs deterministic post-agent steps: package install, `db install`, +5. Runs deterministic post-agent steps: package install, `eql install`, `db push`, framework-specific migrations. 6. Reports remaining call sites that need `encryptModel` / `decryptModel` wiring. diff --git a/packages/wizard/src/__tests__/post-agent.test.ts b/packages/wizard/src/__tests__/post-agent.test.ts index a0408632..0ee7fc51 100644 --- a/packages/wizard/src/__tests__/post-agent.test.ts +++ b/packages/wizard/src/__tests__/post-agent.test.ts @@ -20,7 +20,7 @@ describe('runPostAgentSteps execution commands', () => { vi.mocked(childProcess.execSync).mockImplementation(() => Buffer.from('')) }) - it('executes db install/db push using the detected runner (bun → bunx) when usesProxy=true', async () => { + it('executes eql install/db push using the detected runner (bun → bunx) when usesProxy=true', async () => { await runPostAgentSteps({ cwd: '/tmp/fake', integration: 'supabase', @@ -36,7 +36,7 @@ describe('runPostAgentSteps execution commands', () => { const commands = vi .mocked(childProcess.execSync) .mock.calls.map((c) => c[0] as string) - expect(commands).toContain('bunx stash db install') + expect(commands).toContain('bunx stash eql install') expect(commands).toContain('bunx stash db push') // Sanity: no leftover npx forms for the cipherstash binaries. for (const cmd of commands) { @@ -44,7 +44,7 @@ describe('runPostAgentSteps execution commands', () => { } }) - it('skips db install when hasStashConfig=true and still uses bunx for db push when usesProxy=true', async () => { + it('skips eql install when hasStashConfig=true and still uses bunx for db push when usesProxy=true', async () => { await runPostAgentSteps({ cwd: '/tmp/fake', integration: 'supabase', @@ -59,7 +59,7 @@ describe('runPostAgentSteps execution commands', () => { .mocked(childProcess.execSync) .mock.calls.map((c) => c[0] as string) expect(commands).toContain('bunx stash db push') - expect(commands).not.toContain('bunx stash db install') + expect(commands).not.toContain('bunx stash eql install') }) it('falls back to npx when packageManager is undefined and usesProxy=true', async () => { @@ -76,7 +76,7 @@ describe('runPostAgentSteps execution commands', () => { const commands = vi .mocked(childProcess.execSync) .mock.calls.map((c) => c[0] as string) - expect(commands).toContain('npx stash db install') + expect(commands).toContain('npx stash eql install') expect(commands).toContain('npx stash db push') }) @@ -96,6 +96,6 @@ describe('runPostAgentSteps execution commands', () => { .mocked(childProcess.execSync) .mock.calls.map((c) => c[0] as string) expect(commands).not.toContain('bunx stash db push') - expect(commands).toContain('bunx stash db install') + expect(commands).toContain('bunx stash eql install') }) }) diff --git a/packages/wizard/src/__tests__/prerequisites.test.ts b/packages/wizard/src/__tests__/prerequisites.test.ts index 7f80cd6d..f57fddb3 100644 --- a/packages/wizard/src/__tests__/prerequisites.test.ts +++ b/packages/wizard/src/__tests__/prerequisites.test.ts @@ -40,7 +40,7 @@ describe('checkPrerequisites missing-list copy', () => { const r = await checkPrerequisites(tmp) expect(r.ok).toBe(false) expect(r.missing.join('\n')).toContain('Run: bunx stash auth login') - expect(r.missing.join('\n')).toContain('Run: bunx stash db install') + expect(r.missing.join('\n')).toContain('Run: bunx stash eql install') expect(r.missing.join('\n')).not.toMatch(/\bnpx\b/) }) diff --git a/packages/wizard/src/agent/__tests__/interface.test.ts b/packages/wizard/src/agent/__tests__/interface.test.ts index 755c463b..a14be3d6 100644 --- a/packages/wizard/src/agent/__tests__/interface.test.ts +++ b/packages/wizard/src/agent/__tests__/interface.test.ts @@ -21,10 +21,10 @@ describe('wizardCanUseTool — DLX command allowlist', () => { } }) - it('allows stash db with npx, bunx, pnpm dlx, yarn dlx', () => { + it('allows stash eql with npx, bunx, pnpm dlx, yarn dlx', () => { for (const runner of ['npx', 'bunx', 'pnpm dlx', 'yarn dlx']) { const result = wizardCanUseTool('Bash', { - command: `${runner} stash db install`, + command: `${runner} stash eql install`, }) expect(result).toBe(true) } @@ -75,8 +75,14 @@ describe('wizardCanUseTool — DLX command allowlist', () => { }) }) - describe('allows stash db commands', () => { - it('allows stash db install', () => { + describe('allows stash db / eql commands', () => { + it('allows stash eql install', () => { + expect(wizardCanUseTool('Bash', { command: 'stash eql install' })).toBe( + true, + ) + }) + + it('allows the deprecated stash db install alias', () => { expect(wizardCanUseTool('Bash', { command: 'stash db install' })).toBe( true, ) diff --git a/packages/wizard/src/agent/interface.ts b/packages/wizard/src/agent/interface.ts index d97df7be..2c1eceac 100644 --- a/packages/wizard/src/agent/interface.ts +++ b/packages/wizard/src/agent/interface.ts @@ -52,7 +52,12 @@ const RUNNER_PREFIXES = Object.values(PACKAGE_MANAGERS).map( ) /** Tools allowed to run via any DLX runner. */ -const ALLOWED_DLX_TOOLS = ['drizzle-kit', 'tsc', 'stash db'] as const +const ALLOWED_DLX_TOOLS = [ + 'drizzle-kit', + 'tsc', + 'stash db', + 'stash eql', +] as const /** Allowed Bash commands — whitelist approach. */ const ALLOWED_BASH_COMMANDS = [ @@ -74,6 +79,7 @@ const ALLOWED_BASH_COMMANDS = [ 'bun run', // Build & validation 'stash db', + 'stash eql', ] /** diff --git a/packages/wizard/src/lib/post-agent.ts b/packages/wizard/src/lib/post-agent.ts index e7290535..34c18f2a 100644 --- a/packages/wizard/src/lib/post-agent.ts +++ b/packages/wizard/src/lib/post-agent.ts @@ -41,14 +41,14 @@ export async function runPostAgentSteps(opts: PostAgentOptions): Promise { cwd, ) - // Step 2: Run runner stash db install if the project doesn't yet - // have a stash.config.ts. `db install` scaffolds the config and installs + // Step 2: Run runner stash eql install if the project doesn't yet + // have a stash.config.ts. `eql install` scaffolds the config and installs // EQL in a single step (CIP-2986). if (!gathered.hasStashConfig) { await runStep( - `Running ${runner} stash db install...`, - `${runner} stash db install complete`, - `${runner} stash db install`, + `Running ${runner} stash eql install...`, + `${runner} stash eql install complete`, + `${runner} stash eql install`, cwd, ) } diff --git a/packages/wizard/src/lib/prerequisites.ts b/packages/wizard/src/lib/prerequisites.ts index f92e7f60..fcf234ac 100644 --- a/packages/wizard/src/lib/prerequisites.ts +++ b/packages/wizard/src/lib/prerequisites.ts @@ -26,7 +26,7 @@ export async function checkPrerequisites( } if (!findStashConfig(cwd)) { - missing.push(`No stash.config.ts found. Run: ${runner} stash db install`) + missing.push(`No stash.config.ts found. Run: ${runner} stash eql install`) } return { ok: missing.length === 0, missing } diff --git a/packages/wizard/src/lib/rewrite-migrations.ts b/packages/wizard/src/lib/rewrite-migrations.ts index ad12558a..ea254486 100644 --- a/packages/wizard/src/lib/rewrite-migrations.ts +++ b/packages/wizard/src/lib/rewrite-migrations.ts @@ -19,7 +19,7 @@ import { join } from 'node:path' * - $2: column name (without quotes) * * Note: a copy of this lives in `stash` (`db/rewrite-migrations.ts`) - * because cli's `db install --drizzle` uses the same fix. Both copies are + * because cli's `eql install --drizzle` uses the same fix. Both copies are * tightly coupled to drizzle-kit's output format — if drizzle-kit changes, * both need to be updated together. */ diff --git a/packages/wizard/src/run.ts b/packages/wizard/src/run.ts index 636dc7df..87259688 100644 --- a/packages/wizard/src/run.ts +++ b/packages/wizard/src/run.ts @@ -240,7 +240,7 @@ export async function run(options: RunOptions) { }) changelog.phase( 'Post-agent steps complete', - 'Package install, `db install`, `db push`, and migrations finished.', + 'Package install, `eql install`, `db push`, and migrations finished.', ) const scanResult = await scanPromise diff --git a/skills/stash-cli/SKILL.md b/skills/stash-cli/SKILL.md index 7a07f467..ccf0509f 100644 --- a/skills/stash-cli/SKILL.md +++ b/skills/stash-cli/SKILL.md @@ -90,7 +90,7 @@ export default defineConfig({ }) ``` -`db install` will scaffold this file for you if it's missing. +`eql install` will scaffold this file for you if it's missing. ### Config options @@ -130,7 +130,7 @@ Init is the **scaffold** save-point. It does mechanical setup only — no agent 2. **Resolve database** — picks up `DATABASE_URL` from `.env`/`.env.local` or prompts for it. Verifies the connection. 3. **Build schema** — auto-detects your framework (Drizzle from `drizzle.config.*` / `drizzle-orm` / `drizzle-kit` in `package.json`; Supabase from the `DATABASE_URL` host) and silently writes a placeholder client to `./src/encryption/index.ts`. Only prompts you if a file already exists at that path. 4. **Install dependencies** — single combined prompt for `@cipherstash/stack` and `stash`. Skipped entirely when both are already in `node_modules`. -5. **Install EQL** — runs the equivalent of `stash db install` against the resolved database (Drizzle migration, Supabase migration, or direct, per detection). Skipped if EQL is already installed. +5. **Install EQL** — runs the equivalent of `stash eql install` against the resolved database (Drizzle migration, Supabase migration, or direct, per detection). Skipped if EQL is already installed. 6. **Gather context** — detects available coding agents (Claude Code, Codex, Cursor, Windsurf, Cline) and writes `.cipherstash/context.json` with integration, package manager, schemas, env keys, and detected agents. When init finishes, it prints a checkmark panel of completed phases and an interactive **chain prompt** (default-yes): *"Continue to `stash plan` now to draft your encryption plan?"* Yes auto-launches `stash plan`. No prints "Next: run `stash plan` to draft your encryption plan." Non-TTY (CI, pipes) skips the prompt and prints the hint. @@ -247,7 +247,7 @@ Use the JSON form for scripts; it has a stable shape (`active`, `completed`, per Run `status` after every transition during a rollout. It is the canonical "where am I?" surface; agents working through the rollout should re-read it as they go rather than tracking state mentally. -For the deeper, raw views that touch only the database, use `stash db status` (EQL installation state) and `stash encrypt status` (per-column migration phase, EQL state, backfill progress with drift detection). +For the deeper, raw views that touch only the database, use `stash eql status` (EQL installation state) and `stash encrypt status` (per-column migration phase, EQL state, backfill progress with drift detection). ### `auth login` — Authenticate with CipherStash @@ -257,20 +257,22 @@ stash auth login Opens a browser-based device code flow and saves a token to `~/.cipherstash/auth.json`. Database-touching commands check for this file before running. -### `db install` — Configure the database and install EQL extensions +### `eql install` — Configure the database and install EQL extensions ```bash -stash db install -stash db install --supabase -stash db install --supabase --migration -stash db install --supabase --direct -stash db install --drizzle -stash db install --force +stash eql install +stash eql install --supabase +stash eql install --supabase --migration +stash eql install --supabase --direct +stash eql install --drizzle +stash eql install --force ``` -`stash init` runs `db install` automatically as part of its EQL install phase. Run `db install` directly when you skipped init, when you need flags init doesn't expose (`--migration`, `--migrations-dir`, `--exclude-operator-family`), or when re-installing/upgrading EQL on its own. +`stash db install` is a deprecated alias for this command — it still works but prints a deprecation warning. Always use `eql install` in new instructions. -`db install` is the single command that gets a project from zero to installed EQL: +`stash init` runs `eql install` automatically as part of its EQL install phase. Run `eql install` directly when you skipped init, when you need flags init doesn't expose (`--migration`, `--migrations-dir`, `--exclude-operator-family`), or when re-installing/upgrading EQL on its own. + +`eql install` is the single command that gets a project from zero to installed EQL: 1. Scaffolds `stash.config.ts` if missing (auto-detects an existing client file at common locations, otherwise prompts). 2. Loads the config. @@ -298,7 +300,7 @@ stash db install --force `--migration`, `--direct`, and `--migrations-dir` only make sense in the Supabase flow and require `--supabase` to be passed explicitly. They never auto-enable `--supabase`. -#### `db install --drizzle` +#### `eql install --drizzle` When `--drizzle` is passed, the CLI: 1. Runs `drizzle-kit generate --custom --name=` to scaffold an empty migration. @@ -307,19 +309,19 @@ When `--drizzle` is passed, the CLI: You then run `npx drizzle-kit migrate` to apply it. Requires `drizzle-kit` as a dev dependency. -#### `db install --supabase --migration` +#### `eql install --supabase --migration` Writes the EQL SQL to `supabase/migrations/00000000000000_cipherstash_eql.sql`. The all-zero timestamp ensures this migration runs before any user migrations that reference `eql_v2_encrypted`. Run `supabase db reset` (local) or `supabase migration up` (remote) to apply it. Direct-push installs (`--supabase --direct`) do **not** survive `supabase db reset` — the reset drops the database and reruns only files in `supabase/migrations/`. Use `--migration` for projects that use `supabase db reset`. -### `db upgrade` — Upgrade EQL extensions +### `eql upgrade` — Upgrade EQL extensions ```bash -stash db upgrade -stash db upgrade --dry-run -stash db upgrade --supabase -stash db upgrade --latest +stash eql upgrade +stash eql upgrade --dry-run +stash eql upgrade --supabase +stash eql upgrade --latest ``` **Flags:** @@ -331,7 +333,7 @@ stash db upgrade --latest | `--exclude-operator-family` | Skip operator family creation | | `--latest` | Fetch latest EQL from GitHub instead of bundled | -The EQL install SQL is idempotent and safe to re-run. The command checks the current version, re-runs the install SQL, then reports the new version. If EQL is not installed, it suggests running `db install` instead. +The EQL install SQL is idempotent and safe to re-run. The command checks the current version, re-runs the install SQL, then reports the new version. If EQL is not installed, it suggests running `eql install` instead. ### `db validate` — Validate encryption schema @@ -420,10 +422,10 @@ Use after `stash db push` when the new config purely adds columns or changes ind Errors out with a clear message when there is no pending configuration to activate. -### `db status` — Show EQL installation status +### `eql status` — Show EQL installation status ```bash -stash db status +stash eql status ``` Reports: @@ -437,7 +439,7 @@ Reports: stash db test-connection ``` -Verifies the database URL in your config is valid and the database is reachable. Reports the database name, connected role, and PostgreSQL server version. Useful for debugging connection issues before running `db install`. +Verifies the database URL in your config is valid and the database is reachable. Reports the database name, connected role, and PostgreSQL server version. Useful for debugging connection issues before running `eql install`. ### `db migrate` — Run pending encrypt config migrations @@ -451,7 +453,7 @@ Not yet implemented — placeholder for future encrypt-config migration tooling. The `encrypt` group is the cutover-step toolset: it runs the database-side work that takes an existing plaintext column the rest of the way to encrypted, after the encryption-rollout PR is deployed and dual-writes are live in production. The internal event log uses `schema-added → dual-writing → backfilling → backfilled → cut-over → dropped` as machine-readable phase names; the user-facing story is the rollout/cutover model documented in the `stash-encryption` skill. -It drives the `@cipherstash/migrate` library, which records every transition in a `cipherstash.cs_migrations` table (installed by `stash db install`) and reads the user's intent from `.cipherstash/migrations.json`. This section documents the CLI surface. +It drives the `@cipherstash/migrate` library, which records every transition in a `cipherstash.cs_migrations` table (installed by `stash eql install`) and reads the user's intent from `.cipherstash/migrations.json`. This section documents the CLI surface. The examples below show the bare `stash` form, which works after `stash init` adds the CLI as a project dev dep. See the "CLI Usage" section above for how to invoke it through your package manager before that. @@ -639,7 +641,7 @@ if (await installer.isInstalled()) { - Node.js >= 22 - PostgreSQL database with sufficient permissions (see `checkPermissions()`) -- A `stash.config.ts` file with a valid `databaseUrl` (or run `stash init` / `stash db install` to scaffold it) +- A `stash.config.ts` file with a valid `databaseUrl` (or run `stash init` / `stash eql install` to scaffold it) - Peer dependency: `@cipherstash/stack` >= 0.6.0 ## Common issues @@ -650,7 +652,7 @@ The database role needs `CREATE` privileges on the database and public schema, o ### Config not found -`stash.config.ts` must be in the project root or a parent directory. The file must `export default defineConfig(...)`. The fastest fix is `stash init`, which scaffolds the config (and authenticates, installs deps, installs EQL, and writes `.cipherstash/context.json` in the same run). For a CLI-only setup, `stash db install` also scaffolds the config. +`stash.config.ts` must be in the project root or a parent directory. The file must `export default defineConfig(...)`. The fastest fix is `stash init`, which scaffolds the config (and authenticates, installs deps, installs EQL, and writes `.cipherstash/context.json` in the same run). For a CLI-only setup, `stash eql install` also scaffolds the config. ### Supabase environments