Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .changeset/auth-login-prisma-referrer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'stash': patch
---

`stash auth login` now accepts `--prisma`, bringing the integration referrer
flags to parity with `stash init`: `--supabase`, `--drizzle`, `--prisma`. A
multi-flag referrer is now ordered alphabetically, so it no longer depends on
argv order.

This closes a documentation/implementation gap: the bundled `stash-cli` skill
listed `--prisma` among `auth login`'s referrer flags, but the command did not
register it — and because the CLI's argument parser does not reject unknown
flags, `stash auth login --prisma` was silently dropped rather than erroring.

The `stash-cli` skill also now records that `init` writes no encryption-client
placeholder for Prisma Next, which derives its schemas from `contract.json` —
previously the scaffold step and the generated-file table both claimed the file
was always written.
14 changes: 14 additions & 0 deletions .changeset/cli-init-prisma-flag.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'stash': minor
---

`stash init` now takes `--prisma`, the Prisma Next setup flag, replacing
`--prisma-next`. This makes the integration flags consistent — `--supabase`,
`--drizzle`, `--prisma` — and matches how `--supabase` is used for referrer
tracking. `--prisma` selects the same Prisma Next flow (EQL bundle installed via
`prisma-next migrate`, no encryption-client scaffold) and records `prisma` as the
referrer.

**Breaking:** `stash init --prisma-next` is no longer recognized. Init errors with
guidance to re-run with `--prisma`. The bundled `stash-cli` skill is updated to
document the new flag.
5 changes: 3 additions & 2 deletions packages/cli/src/cli/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export const registry: CommandGroup[] = [
examples: [
'init',
'init --supabase',
'init --prisma-next',
'init --prisma',
'init --region us-east-1',
],
flags: [
Expand All @@ -126,7 +126,7 @@ export const registry: CommandGroup[] = [
description: 'Use Drizzle-specific setup flow.',
},
{
name: '--prisma-next',
name: '--prisma',
description:
'Use Prisma Next-specific setup flow (EQL bundle installed via prisma-next migrate).',
},
Expand Down Expand Up @@ -284,6 +284,7 @@ export const registry: CommandGroup[] = [
description: 'Track Supabase as the referrer.',
},
{ name: '--drizzle', description: 'Track Drizzle as the referrer.' },
{ name: '--prisma', description: 'Track Prisma as the referrer.' },
],
},
{
Expand Down
26 changes: 26 additions & 0 deletions packages/cli/src/commands/auth/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,32 @@ describe('authCommand login — option forwarding', () => {
)
})

it('derives the referrer from --prisma', async () => {
// `--prisma` is the third integration referrer flag, at parity with
// `--supabase` / `--drizzle` (`stash init --prisma` records the same name).
await authCommand(['login'], { prisma: true }, {})

expect(loginMock.login).toHaveBeenCalledWith(
'us-east-1.aws',
'prisma',
expect.objectContaining({ json: false }),
)
})

it('orders a multi-flag referrer alphabetically, not by argv order', async () => {
await authCommand(
['login'],
{ supabase: true, prisma: true, drizzle: true },
{},
)

expect(loginMock.login).toHaveBeenCalledWith(
'us-east-1.aws',
'drizzle-prisma-supabase',
expect.objectContaining({ json: false }),
)
})

it('fails fast on a valueless --region without calling login', async () => {
// `--region` with no value booleanises into flags; guard must fire first.
await expect(authCommand(['login'], { region: true }, {})).rejects.toThrow(
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/commands/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,24 @@ Options:
--no-open Don't auto-open the verification URL in a browser.
--supabase Track Supabase as the referrer
--drizzle Track Drizzle as the referrer
--prisma Track Prisma as the referrer

Examples:
${STASH_AUTH} login
${STASH_AUTH} login --region us-east-1
${STASH_AUTH} login --supabase
${STASH_AUTH} login --prisma
${STASH_AUTH} regions # list available regions
${STASH_AUTH} regions --json # machine-readable [{slug,label}]
# Agent triggers auth; a human completes it in the browser:
${STASH_AUTH} login --region us-east-1 --json
`.trim()

function referrerFromFlags(flags: Record<string, boolean>): string | undefined {
// Alphabetical, so a multi-flag referrer is stable regardless of argv order.
const parts: string[] = []
if (flags.drizzle) parts.push('drizzle')
if (flags.prisma) parts.push('prisma')
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (flags.supabase) parts.push('supabase')
return parts.length > 0 ? parts.join('-') : undefined
}
Expand Down
21 changes: 21 additions & 0 deletions packages/cli/src/commands/init/__tests__/init-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,27 @@ describe('initCommand — region threading', () => {
})
})

describe('initCommand — integration flags', () => {
it('selects the Prisma provider (name `prisma`) for `--prisma`', async () => {
await initCommand({ prisma: true }, {})

expect(authRun).toHaveBeenCalledTimes(1)
// Steps receive the resolved provider as their second argument; `--prisma`
// must resolve to the Prisma Next provider whose referrer name is `prisma`.
const providerArg = authRun.mock.calls[0][1] as { name?: string }
expect(providerArg.name).toBe('prisma')
})

it('errors on the renamed `--prisma-next` flag before running any step', async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this calls initCommand({ 'prisma-next': true }, {}) directly, so it bypasses parseArgs and doesn't prove the retirement message is reachable through the real CLI — which is the interesting risk now that --prisma-next is gone from the registry.

It is reachable, for what it's worth: parseArgs is permissive and main.ts:531 passes flags/values through wholesale, so stash init --prisma-next does hit the friendly error rather than a generic failure. That chain just isn't pinned anywhere. packages/cli/tests/e2e/ would be the natural home if you think it's worth a guard.

// `--prisma-next` was renamed to `--prisma`; init must fail loudly with
// guidance rather than silently ignore a previously-documented flag.
await expect(
initCommand({ 'prisma-next': true }, {}),
).rejects.toBeInstanceOf(CliExit)
expect(authRun).not.toHaveBeenCalled()
})
})

describe('initCommand — honest summary', () => {
it('exits non-zero and reports "Setup incomplete" when EQL was not installed', async () => {
eqlRun.mockImplementationOnce(async (s: InitState) => ({
Expand Down
14 changes: 12 additions & 2 deletions packages/cli/src/commands/init/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { HANDOFF_CHOICES } from '../impl/steps/how-to-proceed.js'
import { planCommand } from '../plan/index.js'
import { createBaseProvider } from './providers/base.js'
import { createDrizzleProvider } from './providers/drizzle.js'
import { createPrismaNextProvider } from './providers/prisma-next.js'
import { createPrismaProvider } from './providers/prisma.js'
import { createSupabaseProvider } from './providers/supabase.js'
import { authenticateStep } from './steps/authenticate.js'
import { buildSchemaStep } from './steps/build-schema.js'
Expand All @@ -21,7 +21,7 @@ import { detectPackageManager, runnerCommand } from './utils.js'
const PROVIDER_MAP: Record<string, () => InitProvider> = {
supabase: createSupabaseProvider,
drizzle: createDrizzleProvider,
'prisma-next': createPrismaNextProvider,
prisma: createPrismaProvider,
}

/**
Expand Down Expand Up @@ -81,6 +81,16 @@ export async function initCommand(
throw new CliExit(1)
}

// `--prisma-next` was renamed to `--prisma` for consistency with `--supabase`
// and `--drizzle`. It selects the same Prisma Next setup flow; error rather
// than silently ignore a previously-documented flag.
if (flags['prisma-next'] === true || Object.hasOwn(values, 'prisma-next')) {
p.log.error(
'`--prisma-next` has been renamed to `--prisma`. Re-run `stash init --prisma` — it selects the same Prisma Next setup flow.',
)
throw new CliExit(1)
}

const provider = resolveProvider(flags)

p.intro('CipherStash Stack Setup')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { describe, expect, it } from 'vitest'
import { createPrismaNextProvider } from '../prisma-next.js'
import { createPrismaProvider } from '../prisma.js'

describe('createPrismaNextProvider getNextSteps', () => {
const provider = createPrismaNextProvider()
describe('createPrismaProvider', () => {
const provider = createPrismaProvider()

it('reports the `prisma` referrer name (consistent with --supabase/--drizzle)', () => {
// The flag is `--prisma`; `provider.name` is what init records as the
// referrer and what build-schema/install-eql branch on to force the
// Prisma Next integration.
expect(provider.name).toBe('prisma')
})

it('points at prisma-next migration plan + apply rather than stash eql install', () => {
const steps = provider.getNextSteps({}, 'pnpm')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import type { InitProvider } from '../types.js'
import { type PackageManager, runnerCommand } from '../utils.js'

export function createPrismaNextProvider(): InitProvider {
/**
* The `--prisma` provider. It backs the Prisma Next framework (the only
* Prisma integration Stack ships), so `provider.name` is the short,
* `--supabase`/`--drizzle`-consistent `'prisma'` used for referrer tracking,
* while the internal `Integration` value it resolves to stays `'prisma-next'`
* (see build-schema.ts) — that keeps skill/dependency/prompt wiring on the
* existing Prisma Next path.
*/
export function createPrismaProvider(): InitProvider {
return {
name: 'prisma-next',
name: 'prisma',
Comment thread
coderabbitai[bot] marked this conversation as resolved.
introMessage: 'Setting up CipherStash for your Prisma Next project...',
// Note: Prisma Next absorbs the EQL bundle install and schema
// scaffold steps via its migration framework. The next-steps list
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,18 @@ describe('buildSchemaStep', () => {
expect(writeFileSyncMock).not.toHaveBeenCalled()
expect(result.schemaGenerated).toBe(false)
})

it('forces the prisma-next integration and skips the scaffold when the `--prisma` provider is selected', async () => {
// `--prisma` sets provider.name === 'prisma'; the internal integration
// value stays 'prisma-next' so skill/dep/prompt wiring is unchanged.
// Prisma Next derives its schema from contract.json, so there is no
// placeholder client to write.
const prismaProvider = { name: 'prisma' } as unknown as InitProvider
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const result = await buildSchemaStep.run(baseState, prismaProvider)

expect(result.integration).toBe('prisma-next')
expect(result.schemaGenerated).toBe(false)
expect(writeFileSyncMock).not.toHaveBeenCalled()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,25 @@ describe('installEqlStep', () => {
)
})

describe('Prisma Next (`--prisma`)', () => {
it('skips `stash eql install` when the provider is `prisma` (framework installs the bundle)', async () => {
// `--prisma` sets provider.name === 'prisma'. Prisma Next installs the EQL
// bundle via `prisma-next migrate`, so init must NOT run its own install —
// that would duplicate the install and race the framework's journal.
const prismaProvider = { name: 'prisma' } as unknown as InitProvider

const result = await installEqlStep.run(
{ integration: 'prisma-next' } as unknown as InitState,
prismaProvider,
)

expect(p.confirm).not.toHaveBeenCalled()
expect(installCommand).not.toHaveBeenCalled()
expect(eqlMigrationCommand).not.toHaveBeenCalled()
expect(result.eqlInstalled).toBe(false)
})
})

describe('Drizzle', () => {
it('generates an EQL v3 migration instead of running `eql install` (the v2 pin is gone)', async () => {
// Regression guard for the defect: init used to pass `eqlVersion: '2'` to
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/init/steps/build-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export const buildSchemaStep: InitStep = {
async run(state: InitState, provider: InitProvider): Promise<InitState> {
const cwd = process.cwd()
const integration =
provider.name === 'prisma-next'
provider.name === 'prisma'
? 'prisma-next'
: detectIntegration(cwd, state.databaseUrl)
const clientFilePath = DEFAULT_CLIENT_PATH
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/init/steps/install-eql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const installEqlStep: InitStep = {
// 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') {
if (integration === 'prisma-next' || provider.name === 'prisma') {
p.log.success(
'Skipping `stash eql install` — Prisma Next installs the EQL bundle via `prisma-next migrate` (runs alongside your app migrations).',
)
Expand Down
11 changes: 6 additions & 5 deletions skills/stash-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ Do **not** trigger when:
The entry point, for humans and agents alike:

```bash
npx stash init # PostgreSQL / Drizzle / Prisma
npx stash init # PostgreSQL / Drizzle / Prisma (auto-detected)
npx stash init --supabase # Supabase
npx stash init --prisma # Prisma Next
```

`stash init` installs the CLI as a project dev dependency, so subsequent commands can drop the `npx`. The CLI is package-manager aware — before init, use whichever one-shot runner your project uses (`npx`, `pnpm dlx`, `bunx`, `yarn dlx`). Installs are **pinned to the exact `@cipherstash/*` versions this CLI release shipped with** (never bare dist-tags, which can lag behind a release), and init flags any already-installed `@cipherstash/*` package whose resolved version differs from the release's. The fix depends on direction, and init says which applies: an **older** install should be aligned to the release (init offers the exact command); a **newer** install must NOT be downgraded — update the `stash` CLI to the matching release instead (init prints that command too). **Non-interactively, an older ("behind") skew is fatal** — init refuses with a non-zero exit and the align command rather than scaffolding against mismatched packages and reporting a false success. Interactively it offers to align. Likewise, if the EQL extension isn't installed at the end, init reports **"Setup incomplete"** and exits non-zero — it never claims a setup is complete when encryption would fail at query time. Integrations that install EQL through a migration are the exception and exit 0: **Prisma Next** installs it via the top-level `prisma-next migrate`, and the **Drizzle** flow *generates* an EQL migration, which init reports honestly as "EQL migration generated — apply it with `drizzle-kit migrate`" rather than claiming the extension is already installed.
Expand Down Expand Up @@ -100,7 +101,7 @@ The working loop is:

**Authenticate before `stash init`.** Init's authenticate step uses the interactive path, so an agent running `init` unauthenticated makes the CLI try to open a browser on the agent's machine — and in a non-TTY it exits with `region_required` unless `--region` or `STASH_REGION` is set. Once a valid token exists, init logs `Using workspace X (region)` and moves on silently.

Flags: `--region <slug>` (env `STASH_REGION`), `--json`, `--no-open`, `--supabase` / `--drizzle` (referrer tracking only).
Flags: `--region <slug>` (env `STASH_REGION`), `--json`, `--no-open`, `--supabase` / `--drizzle` / `--prisma` (referrer tracking only).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove "(referrer tracking only)" from this text.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Flags: `--region <slug>` (env `STASH_REGION`), `--json`, `--no-open`, `--supabase` / `--drizzle` / `--prisma` (referrer tracking only).
Flags: `--region <slug>` (env `STASH_REGION`), `--json`, `--no-open`, `--supabase` / `--drizzle` / `--prisma`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This documents --prisma on auth login, which doesn't accept it.

This line sits under the auth login section, not init — the flags listed beside it (--region, --json, --no-open) are all auth login flags.

Against the shipped manifest:

init:       --supabase, --drizzle, --prisma, --region
auth login: --region, --json, --no-open, --supabase, --drizzle     <- no --prisma

registry.ts:284-286 confirms auth login has only --supabase and --drizzle referrer flags.

It fails silently, which is the worst part: parseArgs (main.ts:162-189) does no unknown-flag validation, so stash auth login --prisma won't error — the flag is just dropped and the referrer is never tracked. Since skills/ ships in the stash tarball and gets copied into customer repos, this lands as wrong guidance in someone else's project.

Two ways to fix, depending on intent:

  • revert this line to --supabase / --drizzle, or
  • add --prisma to auth login's registry flags if referrer parity across the three integrations is actually wanted (that'd match the spirit of the PR).

This is what the stash manifest --json check in AGENTS.md is for — worth running before merge.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed on this branch in f90389c — pushed here rather than as a follow-up PR, at Dan's request.

Took the second option: --prisma is now registered on auth login and threaded through referrerFromFlags, at exact parity with --supabase / --drizzle. This line needed no edit — registering the flag is what made it true, so your wording stands.

Manifest now agrees:

init       : --supabase, --drizzle, --prisma, --region
auth login : --region, --json, --no-open, --supabase, --drizzle, --prisma

Also made the referrer parts alphabetical, so a multi-flag referrer no longer depends on argv order. Two tests added in auth/__tests__/index.test.ts; suite is 967 passed / 10 skipped, code:check exit 0. Changeset: stash patch.

One caveat worth knowing: referrer tracking on auth login is inert today. auth/login.ts:30 names the parameter _referrer and never reads it — the comment at line 41 explains why (client_id must be cli, anything else gives INVALID_CLIENT). So --supabase and --drizzle were already accepted, threaded, and discarded; --prisma now matches them exactly. That makes the flag surface honest, but the "(referrer tracking only)" parenthetical overpromises for all three. Reviving it needs the CTS side to take a referrer separate from client_id, so I left it out of scope.


## Never read these

Expand Down Expand Up @@ -219,16 +220,16 @@ Six mechanical steps, no agent handoff. It prompts only when it can't pick a sen

1. **Authenticate** — silent when a valid token exists.
2. **Resolve database** — per the resolution order above; verifies the connection.
3. **Build schema** — auto-detects Drizzle, Supabase, and Prisma Next and writes the placeholder encryption client.
3. **Build schema** — auto-detects Drizzle, Supabase, and Prisma Next and writes the placeholder encryption client. **Prisma Next is the exception:** it derives schemas from `contract.json`, so no encryption-client file is written and none is needed.
4. **Install dependencies** — one combined prompt for `@cipherstash/stack` and `stash`.
5. **Install EQL** — always EQL v3. Drizzle generates `eql migration --drizzle`; Prisma Next installs through `prisma-next migrate`; other integrations install directly.
6. **Gather context** — detects available coding agents and writes `.cipherstash/context.json`.

Flags: `--supabase`, `--drizzle`, `--prisma-next`, `--region <slug>`.
Flags: `--supabase`, `--drizzle`, `--prisma`, `--region <slug>`.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

| Generated file | Purpose |
|---|---|
| `./src/encryption/index.ts` | Placeholder encryption client — declare encrypted columns here, or let `plan`/`impl` do it |
| `./src/encryption/index.ts` | Placeholder encryption client — declare encrypted columns here, or let `plan`/`impl` do it. **Not written for Prisma Next** (`--prisma`), which derives schemas from `contract.json` |
| `.cipherstash/context.json` | Detected facts: integration, package manager, schemas, env key names, and agents. CLI-owned; never hand-edit |
| `stash.config.ts` | Scaffolded if missing |

Expand Down
4 changes: 2 additions & 2 deletions skills/stash-prisma/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ capability semantics; this skill covers the Prisma-Next-specific surface.
npm install @cipherstash/stack @cipherstash/stack-prisma
```

Or run `npx stash init --prisma-next`, which detects Prisma Next, installs both
Or run `npx stash init --prisma`, which detects Prisma Next, installs both
packages pinned to the CLI release, and authenticates. It does **not** scaffold
the wiring files — Prisma Next derives its schema from `contract.json`, so there
is no encryption-client file to generate; init prints the next steps (declare
Expand Down Expand Up @@ -164,7 +164,7 @@ The apply command is the top-level `prisma-next migrate` (add `--yes` to skip th
confirmation prompt in CI). There is no `prisma-next migration apply` subcommand.

Do **not** run `stash eql install` for a Prisma Next project — `prisma-next
migrate` owns EQL installation, and `stash init --prisma-next` skips the
migrate` owns EQL installation, and `stash init --prisma` skips the
standalone installer for exactly this reason. The CLI enforces this: `stash eql
install` detects a Prisma Next project and refuses (pointing you at `prisma-next
migrate`) unless you pass `--force`.
Expand Down
Loading