From a40e9fa1821be45f8a488a39f45563db21a55ed1 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Sat, 25 Jul 2026 19:10:16 +1000 Subject: [PATCH] feat(cli,wizard)!: remove the --proxy / --no-proxy choice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `--proxy` never configured a connection to CipherStash Proxy. It recorded a declaration of intent: the flag, or an interactive "Proxy or SDK?" prompt, set `usesProxy` in `.cipherstash/context.json`. Exactly one piece of code acted on the value — the wizard's post-agent step, which ran `stash db push` when true and logged a skip notice when false. `stash plan` and `stash impl` read the field into state and never branched on it. `stash db push` writes `eql_v2_configuration`, which only ever applied to EQL v2 with Proxy. With the v2 surface going away there is no condition under which that step should run, so the flag gating it goes too. Removed: the flags and the init prompt (init is now six steps, not seven); `usesProxy` from InitState, ContextFile, and the wizard's gathered context; the wizard post-agent `stash db push` step. A context.json from an older CLI may still carry the field — it is now an ignored extra property, so the read path tolerates it rather than migrating it. The argument parser is permissive, so a leftover `--proxy` in a scripted init is ignored rather than rejected. Called out in the changeset, since silently ignoring a flag someone believes is doing something is the sharper edge here. `stash db push` itself is untouched and retires separately with the rest of EQL v2. `--proxy-url` on `stash encrypt cutover` is a different flag — a real Proxy connection URL for `eql_v2.reload_config()` — and is also untouched. skills/stash-cli and both READMEs updated to match; the wizard's post-agent tests keep their runner-resolution coverage and gain a regression guard that no code path shells out to `stash db push`. --- .changeset/remove-proxy-choice-flags.md | 20 +++ packages/cli/README.md | 4 +- packages/cli/src/cli/registry.ts | 9 - packages/cli/src/commands/impl/index.ts | 1 - .../init/__tests__/init-command.test.ts | 3 - packages/cli/src/commands/init/index.ts | 9 - .../cli/src/commands/init/lib/read-context.ts | 11 +- .../src/commands/init/lib/write-context.ts | 3 - .../__tests__/resolve-proxy-choice.test.ts | 157 ------------------ .../init/steps/resolve-proxy-choice.ts | 63 ------- packages/cli/src/commands/init/types.ts | 2 - packages/cli/src/commands/plan/index.ts | 1 - packages/stack/README.md | 1 - .../wizard/src/__tests__/post-agent.test.ts | 53 +++--- packages/wizard/src/lib/gather.ts | 35 ---- packages/wizard/src/lib/post-agent.ts | 14 -- skills/stash-cli/SKILL.md | 16 +- 17 files changed, 59 insertions(+), 343 deletions(-) create mode 100644 .changeset/remove-proxy-choice-flags.md delete mode 100644 packages/cli/src/commands/init/steps/__tests__/resolve-proxy-choice.test.ts delete mode 100644 packages/cli/src/commands/init/steps/resolve-proxy-choice.ts diff --git a/.changeset/remove-proxy-choice-flags.md b/.changeset/remove-proxy-choice-flags.md new file mode 100644 index 000000000..2a5e5e064 --- /dev/null +++ b/.changeset/remove-proxy-choice-flags.md @@ -0,0 +1,20 @@ +--- +'stash': major +'@cipherstash/wizard': major +--- + +Remove the `--proxy` / `--no-proxy` choice from `stash init`, and the wizard step that depended on it. + +`--proxy` never configured a connection to CipherStash Proxy. It recorded a declaration of intent: the flag (or an interactive prompt) set `usesProxy` in `.cipherstash/context.json`, and exactly one piece of code acted on it — the wizard's post-agent step, which ran `stash db push` when it was true and logged a skip notice when it was false. `stash plan` and `stash impl` read the field into state and never branched on it. + +`stash db push` writes `eql_v2_configuration`, which only ever applied to EQL v2 with Proxy. With the v2 surface going away there is no condition under which that step should run, so the flag it gated goes with it. + +Removed: + +- `--proxy` / `--no-proxy` on `stash init`, and the interactive "Proxy or SDK?" prompt (`init` is now six steps, not seven) +- `usesProxy` from `.cipherstash/context.json`, from `InitState`, and from the wizard's gathered context +- The wizard post-agent `stash db push` step and its skip notice + +**Upgrading:** drop `--proxy` / `--no-proxy` from any scripted `stash init` invocation. The CLI's argument parser is permissive, so a leftover flag is ignored rather than rejected — it will not fail your pipeline, but it will not do anything either. A `context.json` written by an older CLI may still carry `usesProxy`; it is now an ignored extra property and needs no migration. + +`stash db push` itself is unchanged here and is being retired separately with the rest of the EQL v2 surface. `--proxy-url` on `stash encrypt cutover` is a different flag — a real Proxy connection URL used to call `eql_v2.reload_config()` — and is also untouched. diff --git a/packages/cli/README.md b/packages/cli/README.md index 853e1b21b..57786feb5 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -101,10 +101,10 @@ The full pipeline state — integration, columns, env-key names, paths, versions `CIPHERSTASH_WIZARD_URL` overrides the gateway endpoint for the rulebook fetch. Useful for local-dev against a wizard gateway running on `localhost`. -**Running `init` non-interactively** (CI, agents, pipes): every prompt has an escape hatch, so `init` never blocks waiting on a TTY. Provide the region up front (`--region` / `STASH_REGION`) if you aren't already logged in, the database URL (`--database-url` / `DATABASE_URL`), the proxy choice (`--proxy` / `--no-proxy`), and — for the closing agent handoff — nothing is required (init exits at a clean checkpoint and points you at `stash plan --target …`). When a required value is missing in a non-TTY context the command exits non-zero with an actionable message rather than hanging. +**Running `init` non-interactively** (CI, agents, pipes): every prompt has an escape hatch, so `init` never blocks waiting on a TTY. Provide the region up front (`--region` / `STASH_REGION`) if you aren't already logged in, the database URL (`--database-url` / `DATABASE_URL`), and — for the closing agent handoff — nothing is required (init exits at a clean checkpoint and points you at `stash plan --target …`). When a required value is missing in a non-TTY context the command exits non-zero with an actionable message rather than hanging. ```bash -STASH_REGION=us-east-1 DATABASE_URL=postgres://… npx stash init --no-proxy +STASH_REGION=us-east-1 DATABASE_URL=postgres://… npx stash init ``` --- diff --git a/packages/cli/src/cli/registry.ts b/packages/cli/src/cli/registry.ts index d287dffde..3e1233bf8 100644 --- a/packages/cli/src/cli/registry.ts +++ b/packages/cli/src/cli/registry.ts @@ -130,15 +130,6 @@ export const registry: CommandGroup[] = [ description: 'Use Prisma Next-specific setup flow (EQL bundle installed via prisma-next migrate).', }, - { - name: '--proxy', - description: 'Query encrypted data via CipherStash Proxy.', - }, - { - name: '--no-proxy', - description: 'Query encrypted data directly via the SDK.', - default: 'true', - }, { ...REGION_FLAG, description: diff --git a/packages/cli/src/commands/impl/index.ts b/packages/cli/src/commands/impl/index.ts index 044d82e6f..44fa65f28 100644 --- a/packages/cli/src/commands/impl/index.ts +++ b/packages/cli/src/commands/impl/index.ts @@ -40,7 +40,6 @@ function buildStateFromContext( eqlInstalled: true, agents, mode: 'implement', - usesProxy: ctx.usesProxy ?? false, } } diff --git a/packages/cli/src/commands/init/__tests__/init-command.test.ts b/packages/cli/src/commands/init/__tests__/init-command.test.ts index 44c5b3a65..cf9344eee 100644 --- a/packages/cli/src/commands/init/__tests__/init-command.test.ts +++ b/packages/cli/src/commands/init/__tests__/init-command.test.ts @@ -23,9 +23,6 @@ vi.mock('../steps/authenticate.js', () => ({ vi.mock('../steps/resolve-database.js', () => ({ resolveDatabaseStep: { id: 'resolve-database', ...passthrough }, })) -vi.mock('../steps/resolve-proxy-choice.js', () => ({ - resolveProxyChoiceStep: { id: 'resolve-proxy-choice', ...passthrough }, -})) vi.mock('../steps/build-schema.js', () => ({ buildSchemaStep: { id: 'build-schema', ...passthrough }, })) diff --git a/packages/cli/src/commands/init/index.ts b/packages/cli/src/commands/init/index.ts index 4fec87bfc..11c52960b 100644 --- a/packages/cli/src/commands/init/index.ts +++ b/packages/cli/src/commands/init/index.ts @@ -14,7 +14,6 @@ import { gatherContextStep } from './steps/gather-context.js' import { installDepsStep } from './steps/install-deps.js' import { installEqlStep } from './steps/install-eql.js' import { resolveDatabaseStep } from './steps/resolve-database.js' -import { resolveProxyChoiceStep } from './steps/resolve-proxy-choice.js' import type { InitProvider, InitState } from './types.js' import { CancelledError } from './types.js' import { detectPackageManager, runnerCommand } from './utils.js' @@ -39,7 +38,6 @@ const PROVIDER_MAP: Record InitProvider> = { const STEPS = [ authenticateStep, resolveDatabaseStep, - resolveProxyChoiceStep, buildSchemaStep, installDepsStep, installEqlStep, @@ -87,13 +85,6 @@ export async function initCommand( state.regionFlag = values.region } - // Parse --proxy and --no-proxy flags; --proxy wins if both are set - if (flags.proxy) { - state.usesProxy = true - } else if (flags['no-proxy']) { - state.usesProxy = false - } - try { for (const step of STEPS) { state = await step.run(state, provider) diff --git a/packages/cli/src/commands/init/lib/read-context.ts b/packages/cli/src/commands/init/lib/read-context.ts index 9b7550917..c1f8efea1 100644 --- a/packages/cli/src/commands/init/lib/read-context.ts +++ b/packages/cli/src/commands/init/lib/read-context.ts @@ -38,13 +38,10 @@ export function readContextFile(cwd: string): ContextFile | undefined { try { const parsed = JSON.parse(readFileSync(path, 'utf-8')) as unknown if (!isContextFile(parsed)) return undefined - // Normalize usesProxy to a strict boolean: older files lack it and a - // hand-edited file could hold any JSON value, so coerce to `true` only - // when it is exactly `true` and `false` otherwise. - return { - ...parsed, - usesProxy: parsed.usesProxy === true, - } + // A context.json written by an older CLI may still carry `usesProxy`. + // It is no longer part of `ContextFile` and nothing reads it, so it + // rides along as an ignored extra property rather than failing the read. + return parsed } catch { return undefined } diff --git a/packages/cli/src/commands/init/lib/write-context.ts b/packages/cli/src/commands/init/lib/write-context.ts index ba5bd3e95..25514ad2d 100644 --- a/packages/cli/src/commands/init/lib/write-context.ts +++ b/packages/cli/src/commands/init/lib/write-context.ts @@ -66,8 +66,6 @@ export interface ContextFile { * handoffs use. Absent when the file was written by `stash init` or * `stash impl` rather than `stash plan`. */ planStep?: PlanStep - /** Whether the user queries encrypted data via CipherStash Proxy. Captured in stash init. SDK users default to false. */ - usesProxy?: boolean generatedAt: string } @@ -134,7 +132,6 @@ export function buildContextFile(state: InitState): ContextFile { installedSkills: [], inlinedSkills: [], planStep: state.planStep, - usesProxy: state.usesProxy, generatedAt: new Date().toISOString(), } } diff --git a/packages/cli/src/commands/init/steps/__tests__/resolve-proxy-choice.test.ts b/packages/cli/src/commands/init/steps/__tests__/resolve-proxy-choice.test.ts deleted file mode 100644 index 9dd72eb2c..000000000 --- a/packages/cli/src/commands/init/steps/__tests__/resolve-proxy-choice.test.ts +++ /dev/null @@ -1,157 +0,0 @@ -import * as p from '@clack/prompts' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { InitProvider, InitState } from '../../types.js' -import { resolveProxyChoiceStep } from '../resolve-proxy-choice.js' - -vi.mock('@clack/prompts', () => ({ - select: vi.fn(), - isCancel: vi.fn(() => false), - log: { info: vi.fn(), success: vi.fn(), warn: vi.fn(), error: vi.fn() }, -})) - -const provider = {} as InitProvider -const baseState = {} as InitState - -const originalStdinIsTTY = process.stdin.isTTY -const originalStdoutIsTTY = process.stdout.isTTY - -/** - * Force BOTH streams. A CI runner that allocates a TTY has stdin *and* stdout - * as TTYs — that is the configuration this gate used to hang in, and setting - * stdin alone would let the old `process.stdout.isTTY` gate pass these tests - * for the wrong reason (vitest's own stdout is not a TTY). - */ -function setTty(value: boolean) { - Object.defineProperty(process.stdin, 'isTTY', { value, configurable: true }) - Object.defineProperty(process.stdout, 'isTTY', { value, configurable: true }) -} - -function restoreTty() { - Object.defineProperty(process.stdin, 'isTTY', { - value: originalStdinIsTTY, - configurable: true, - }) - Object.defineProperty(process.stdout, 'isTTY', { - value: originalStdoutIsTTY, - configurable: true, - }) -} - -beforeEach(() => { - vi.clearAllMocks() -}) - -afterEach(() => { - restoreTty() - vi.unstubAllEnvs() -}) - -describe('resolveProxyChoiceStep — flag short-circuit', () => { - it('returns state untouched when --proxy/--no-proxy already set it', async () => { - setTty(true) - vi.stubEnv('CI', '') - - const state = { ...baseState, usesProxy: true } - await expect(resolveProxyChoiceStep.run(state, provider)).resolves.toEqual( - state, - ) - expect(vi.mocked(p.select)).not.toHaveBeenCalled() - }) - - it('returns state untouched when --no-proxy set usesProxy to false', async () => { - // The guard is `state.usesProxy !== undefined`; `false` is the value that - // distinguishes it from a truthiness check. Rewriting it as - // `if (state.usesProxy)` would silently re-prompt for --no-proxy (and log - // the misleading "No --proxy flag set" notice non-interactively). - setTty(true) - vi.stubEnv('CI', '') - - const state = { ...baseState, usesProxy: false } - await expect(resolveProxyChoiceStep.run(state, provider)).resolves.toEqual( - state, - ) - expect(vi.mocked(p.select)).not.toHaveBeenCalled() - // An explicit --no-proxy is not "no flag set" — don't log the default notice. - expect(vi.mocked(p.log.info)).not.toHaveBeenCalled() - }) -}) - -describe('resolveProxyChoiceStep — CI detection with a TTY attached', () => { - // Regression: this gate was `process.stdout.isTTY`, which consulted CI not - // at all. On a CI runner with an allocated TTY the clack `select` rendered - // and blocked on /dev/tty forever — a hang, not an error. `isInteractive()` - // gates on stdin and treats CI=1/TRUE/true alike. - beforeEach(() => { - setTty(true) - }) - - for (const ciValue of ['1', 'TRUE', 'true']) { - it(`defaults to SDK-only under CI=${ciValue} without prompting`, async () => { - vi.stubEnv('CI', ciValue) - - const result = await resolveProxyChoiceStep.run(baseState, provider) - - // Non-interactive proceeds with the safe default rather than failing: - // SDK-only is what the flagless interactive prompt defaults to anyway. - expect(vi.mocked(p.select)).not.toHaveBeenCalled() - expect(result.usesProxy).toBe(false) - expect(vi.mocked(p.log.info)).toHaveBeenCalledWith( - expect.stringContaining('defaulting to SDK-only mode'), - ) - }) - } - - it('prompts when CI is unset and stdin is a TTY', async () => { - vi.stubEnv('CI', '') - vi.mocked(p.select).mockResolvedValueOnce(true) - - const result = await resolveProxyChoiceStep.run(baseState, provider) - - expect(vi.mocked(p.select)).toHaveBeenCalledTimes(1) - expect(result.usesProxy).toBe(true) - }) - - it('falls back to SDK-only when the interactive prompt is cancelled', async () => { - vi.stubEnv('CI', '') - vi.mocked(p.select).mockResolvedValueOnce(Symbol('cancel')) - vi.mocked(p.isCancel).mockReturnValueOnce(true) - - const result = await resolveProxyChoiceStep.run(baseState, provider) - - expect(result.usesProxy).toBe(false) - }) -}) - -describe('resolveProxyChoiceStep — no TTY', () => { - it('defaults to SDK-only when stdin is not a TTY, CI unset', async () => { - setTty(false) - vi.stubEnv('CI', '') - - const result = await resolveProxyChoiceStep.run(baseState, provider) - - expect(vi.mocked(p.select)).not.toHaveBeenCalled() - expect(result.usesProxy).toBe(false) - }) - - it('does not prompt when stdin is piped even though stdout is a TTY', async () => { - // setTty() can't express this — it moves both streams together. The fix - // gates on stdin, so a redirected stdin (`stash init < /dev/null` from a - // terminal) must not reach the prompt even while stdout is still a TTY. - // This is the case that verifies the changeset's "wrong stream" claim; - // only the CI axis distinguishes old from new anywhere else. - Object.defineProperty(process.stdin, 'isTTY', { - value: false, - configurable: true, - }) - Object.defineProperty(process.stdout, 'isTTY', { - value: true, - configurable: true, - }) - vi.stubEnv('CI', '') - - const result = await resolveProxyChoiceStep.run(baseState, provider) - - expect(vi.mocked(p.select)).not.toHaveBeenCalled() - expect(result.usesProxy).toBe(false) - }) -}) diff --git a/packages/cli/src/commands/init/steps/resolve-proxy-choice.ts b/packages/cli/src/commands/init/steps/resolve-proxy-choice.ts deleted file mode 100644 index 1aaef5537..000000000 --- a/packages/cli/src/commands/init/steps/resolve-proxy-choice.ts +++ /dev/null @@ -1,63 +0,0 @@ -import * as p from '@clack/prompts' -import { isInteractive } from '../../../config/tty.js' -import type { InitProvider, InitState, InitStep } from '../types.js' - -/** - * Resolve whether the user queries encrypted data via CipherStash Proxy or - * directly via the SDK. Captured as a flag (--proxy / --no-proxy) or an - * interactive prompt, and stored on state.usesProxy. - * - * The prompt is non-blocking: cancellation falls back to false (SDK-only, - * the default). In non-interactive contexts without a flag, defaults to false - * and logs an info message. - */ -export const resolveProxyChoiceStep: InitStep = { - id: 'resolve-proxy-choice', - name: 'Resolve proxy choice', - async run(state: InitState, _provider: InitProvider): Promise { - // If the flag was already set by the user, use it - if (state.usesProxy !== undefined) { - return state - } - - // Prompt only when it's safe to: stdin is a real TTY and we're not in CI. - // Via the shared `isInteractive()` (config/tty.ts) so this gate matches - // every other prompt gate. Keying off `process.stdout.isTTY` was wrong on - // both counts — it ignored CI entirely (a runner with an allocated TTY - // blocked here forever), and a redirected stdin still hangs clack `select`, - // which reads from /dev/tty. - if (isInteractive()) { - const choice = await p.select({ - message: - 'Are you planning to query encrypted data via CipherStash Proxy, or directly via the SDK?', - options: [ - { - value: false, - label: 'Directly via the SDK (most common)', - }, - { - value: true, - label: 'CipherStash Proxy', - }, - ], - initialValue: false, - }) - - // Non-blocking: if cancelled, default to false - if (p.isCancel(choice)) { - p.log.info( - 'Cancelled proxy choice; defaulting to SDK-only mode (no `stash db push` in default flows).', - ) - return { ...state, usesProxy: false } - } - - return { ...state, usesProxy: choice } - } - - // Non-interactive: default to false and log - p.log.info( - 'No --proxy flag set; defaulting to SDK-only mode (no `stash db push` in default flows).', - ) - return { ...state, usesProxy: false } - }, -} diff --git a/packages/cli/src/commands/init/types.ts b/packages/cli/src/commands/init/types.ts index d1317ef0b..738955b26 100644 --- a/packages/cli/src/commands/init/types.ts +++ b/packages/cli/src/commands/init/types.ts @@ -108,8 +108,6 @@ export interface InitState { * on-disk plan-summary block instead. Defaults to `'rollout'` when the * CLI has nothing else to go on (fresh project, no DB connectivity). */ planStep?: PlanStep - /** Whether the user queries encrypted data via CipherStash Proxy. Captured in stash init. SDK users default to false; setting true makes prompts/skills include `stash db push` steps. */ - usesProxy?: boolean } /** diff --git a/packages/cli/src/commands/plan/index.ts b/packages/cli/src/commands/plan/index.ts index 094afd685..18182d9a4 100644 --- a/packages/cli/src/commands/plan/index.ts +++ b/packages/cli/src/commands/plan/index.ts @@ -68,7 +68,6 @@ function buildStateFromContext( agents, mode: 'plan', planStep, - usesProxy: ctx.usesProxy ?? false, } } diff --git a/packages/stack/README.md b/packages/stack/README.md index ae45f1ea6..d121b1135 100644 --- a/packages/stack/README.md +++ b/packages/stack/README.md @@ -571,7 +571,6 @@ The wizard will: | Flag | Description | |------|-------------| | `--supabase` / `--drizzle` / `--prisma-next` | Target a specific integration's setup flow | -| `--proxy` / `--no-proxy` | Opt in/out of the CipherStash Proxy path | | `--region ` | Workspace region (env `STASH_REGION`); **required for non-interactive init when not already logged in** | ## Configuration diff --git a/packages/wizard/src/__tests__/post-agent.test.ts b/packages/wizard/src/__tests__/post-agent.test.ts index 8e7656d9f..88eb07834 100644 --- a/packages/wizard/src/__tests__/post-agent.test.ts +++ b/packages/wizard/src/__tests__/post-agent.test.ts @@ -41,7 +41,7 @@ describe('runPostAgentSteps execution commands', () => { vi.mocked(childProcess.execSync).mockImplementation(() => Buffer.from('')) }) - it('executes eql install/db push using the detected runner (bun → bunx) when usesProxy=true', async () => { + it('executes eql install using the detected runner (bun → bunx)', async () => { await runPostAgentSteps({ cwd: '/tmp/fake', integration: 'supabase', @@ -49,7 +49,6 @@ describe('runPostAgentSteps execution commands', () => { gathered: { installCommand: 'bun add @cipherstash/stack', hasStashConfig: false, - usesProxy: true, // Other GatheredContext fields aren't read in this code path; cast for the test. } as never, }) @@ -58,14 +57,13 @@ describe('runPostAgentSteps execution commands', () => { .mocked(childProcess.execSync) .mock.calls.map((c) => c[0] as string) 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) { expect(cmd).not.toMatch(/^npx @cipherstash/) } }) - it('skips eql install when hasStashConfig=true and still uses bunx for db push when usesProxy=true', async () => { + it('skips eql install when hasStashConfig=true', async () => { await runPostAgentSteps({ cwd: '/tmp/fake', integration: 'supabase', @@ -73,17 +71,15 @@ describe('runPostAgentSteps execution commands', () => { gathered: { installCommand: 'bun add @cipherstash/stack', hasStashConfig: true, - usesProxy: true, } as never, }) const commands = vi .mocked(childProcess.execSync) .mock.calls.map((c) => c[0] as string) - expect(commands).toContain('bunx stash db push') expect(commands).not.toContain('bunx stash eql install') }) - it('falls back to npx when packageManager is undefined and usesProxy=true', async () => { + it('falls back to npx when packageManager is undefined', async () => { await runPostAgentSteps({ cwd: '/tmp/fake', integration: 'supabase', @@ -91,33 +87,37 @@ describe('runPostAgentSteps execution commands', () => { gathered: { installCommand: 'npm install @cipherstash/stack', hasStashConfig: false, - usesProxy: true, } as never, }) const commands = vi .mocked(childProcess.execSync) .mock.calls.map((c) => c[0] as string) expect(commands).toContain('npx stash eql install') - expect(commands).toContain('npx stash db push') }) - it('skips db push when usesProxy=false', async () => { - await runPostAgentSteps({ - cwd: '/tmp/fake', - integration: 'supabase', - packageManager: bun, - gathered: { - installCommand: 'bun add @cipherstash/stack', - hasStashConfig: false, - usesProxy: false, - } as never, - }) - - const commands = vi - .mocked(childProcess.execSync) - .mock.calls.map((c) => c[0] as string) - expect(commands).not.toContain('bunx stash db push') - expect(commands).toContain('bunx stash eql install') + // `stash db push` writes `eql_v2_configuration`, which only ever applied to + // EQL v2 with CipherStash Proxy. The wizard used to run it whenever the + // removed `usesProxy` flag was set; with no v2 surface left there is no + // condition under which post-agent should shell out to it. + it('never runs `stash db push`', async () => { + for (const hasStashConfig of [false, true]) { + vi.mocked(childProcess.execSync).mockClear() + await runPostAgentSteps({ + cwd: '/tmp/fake', + integration: 'supabase', + packageManager: bun, + gathered: { + installCommand: 'bun add @cipherstash/stack', + hasStashConfig, + } as never, + }) + const commands = vi + .mocked(childProcess.execSync) + .mock.calls.map((c) => c[0] as string) + for (const cmd of commands) { + expect(cmd).not.toMatch(/stash db push/) + } + } }) }) @@ -135,7 +135,6 @@ describe('drizzle migrate prompt after a destructive rewrite', () => { gathered: { installCommand: 'bun add @cipherstash/stack', hasStashConfig: true, - usesProxy: false, } as never, }) diff --git a/packages/wizard/src/lib/gather.ts b/packages/wizard/src/lib/gather.ts index c190d16fb..ce67df683 100644 --- a/packages/wizard/src/lib/gather.ts +++ b/packages/wizard/src/lib/gather.ts @@ -46,8 +46,6 @@ export interface GatheredContext { installCommand: string /** Whether stash.config.ts already exists. */ hasStashConfig: boolean - /** Whether the user runs CipherStash Proxy. False = SDK-only (post-agent skips `stash db push`). Sourced from .cipherstash/context.json; defaults to false when the file is missing or the field is absent. */ - usesProxy: boolean } export interface GatherContextOptions { @@ -80,9 +78,6 @@ export async function gatherContext( existsSync(resolve(cwd, 'stash.config.ts')) || existsSync(resolve(cwd, 'stash.config.js')) - // Read usesProxy from .cipherstash/context.json, defaulting to false - const usesProxy = readUsesProxyFromContext(cwd) - let selectedColumns: ColumnSelection[] if (mode === 'plan') { selectedColumns = [] @@ -122,7 +117,6 @@ export async function gatherContext( outputPath, installCommand: installCmd, hasStashConfig, - usesProxy, } } @@ -422,32 +416,3 @@ function scanForPgTable( // Permission error or similar — skip } } - -// --- Context file reading --- - -/** - * Read usesProxy from .cipherstash/context.json. - * Silently defaults to false if the file is missing, invalid JSON, - * or the field is absent. - */ -function readUsesProxyFromContext(cwd: string): boolean { - const contextPath = resolve(cwd, '.cipherstash', 'context.json') - if (!existsSync(contextPath)) { - return false - } - try { - const content = readFileSync(contextPath, 'utf-8') - const parsed = JSON.parse(content) as unknown - if ( - parsed && - typeof parsed === 'object' && - 'usesProxy' in parsed && - typeof (parsed as Record).usesProxy === 'boolean' - ) { - return (parsed as Record).usesProxy as boolean - } - } catch { - // Silently ignore parse errors or other issues - } - return false -} diff --git a/packages/wizard/src/lib/post-agent.ts b/packages/wizard/src/lib/post-agent.ts index 25c37b7da..9cf08010e 100644 --- a/packages/wizard/src/lib/post-agent.ts +++ b/packages/wizard/src/lib/post-agent.ts @@ -53,20 +53,6 @@ export async function runPostAgentSteps(opts: PostAgentOptions): Promise { ) } - // Step 3: Push encryption config (only when using Proxy) - if (gathered.usesProxy) { - await runStep( - 'Pushing encryption config to database...', - 'Encryption config pushed', - `${runner} stash db push`, - cwd, - ) - } else { - p.log.info( - 'Skipping `stash db push` — not using CipherStash Proxy. Run it manually if you ever switch to Proxy.', - ) - } - // Step 4: Integration-specific migrations if (integration === 'drizzle') { await runStep( diff --git a/skills/stash-cli/SKILL.md b/skills/stash-cli/SKILL.md index aa1c03c27..60d5fa472 100644 --- a/skills/stash-cli/SKILL.md +++ b/skills/stash-cli/SKILL.md @@ -130,7 +130,6 @@ There is **no global `--non-interactive` or `--json` flag** (and no global `--ye | Region (`auth login`, `init`) | `--region ` or `STASH_REGION` | | Database URL (all `db` / `eql` / `schema` commands) | `--database-url ` or `DATABASE_URL` | | Agent target (`plan`, `impl`) | `--target ` | -| Proxy choice (`init`) | `--proxy` / `--no-proxy` (default) | | Dual-write confirmation (`encrypt backfill`) | `--confirm-dual-writes-deployed` | | Machine-readable output | `--json` on `status`, `manifest`, `auth login`, `auth regions` | @@ -216,22 +215,21 @@ Four explicit save-points. Each runs standalone; chain prompts make first-time s ### `init` — scaffold -Seven mechanical steps, no agent handoff. It prompts only when it can't pick a sensible default. +Six mechanical steps, no agent handoff. It prompts only when it can't pick a sensible default. 1. **Authenticate** — silent when a valid token exists. 2. **Resolve database** — per the resolution order above; verifies the connection. -3. **Resolve proxy choice** — CipherStash Proxy or direct SDK access (the default). Stored as `usesProxy` in `context.json`. Set by `--proxy` / `--no-proxy`; non-TTY without a flag defaults to SDK. -4. **Build schema** — auto-detects Drizzle (`drizzle.config.*`, `drizzle-orm`/`drizzle-kit`), Supabase (from the `DATABASE_URL` host), and Prisma Next. Writes a placeholder encryption client; prompts only if a file already exists there. -5. **Install dependencies** — one combined prompt for `@cipherstash/stack` and `stash`. Skipped when both are present. -6. **Install EQL** — always EQL v3. **Drizzle** projects generate a v3 install migration (the same output as `eql migration --drizzle`, including the `cs_migrations` tracking schema) so the install lands in your migration history — apply it with `drizzle-kit migrate`; requires `drizzle-kit` to be installed and configured. **Prisma Next** is skipped (it installs EQL via `prisma-next migrate`). Everything else runs `eql install` directly against the resolved database, and is skipped when EQL is already installed. -7. **Gather context** — detects available coding agents and writes `.cipherstash/context.json`. +3. **Build schema** — auto-detects Drizzle (`drizzle.config.*`, `drizzle-orm`/`drizzle-kit`), Supabase (from the `DATABASE_URL` host), and Prisma Next. Writes a placeholder encryption client; prompts only if a file already exists there. +4. **Install dependencies** — one combined prompt for `@cipherstash/stack` and `stash`. Skipped when both are present. +5. **Install EQL** — always EQL v3. **Drizzle** projects generate a v3 install migration (the same output as `eql migration --drizzle`, including the `cs_migrations` tracking schema) so the install lands in your migration history — apply it with `drizzle-kit migrate`; requires `drizzle-kit` to be installed and configured. **Prisma Next** is skipped (it installs EQL via `prisma-next migrate`). Everything else runs `eql install` directly against the resolved database, and is skipped when EQL is already installed. +6. **Gather context** — detects available coding agents and writes `.cipherstash/context.json`. -Flags: `--supabase`, `--drizzle`, `--prisma-next`, `--proxy` / `--no-proxy`, `--region `. +Flags: `--supabase`, `--drizzle`, `--prisma-next`, `--region `. | Generated file | Purpose | |---|---| | `./src/encryption/index.ts` | Placeholder encryption client — declare encrypted columns here, or let `plan`/`impl` do it | -| `.cipherstash/context.json` | Detected facts: integration, package manager, schemas, env key names, agents, `usesProxy`. CLI-owned; never hand-edit | +| `.cipherstash/context.json` | Detected facts: integration, package manager, schemas, env key names, agents. CLI-owned; never hand-edit | | `stash.config.ts` | Scaffolded if missing | ### `plan` — draft for review