Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0252803
feat(lint): add lint-no-hardcoded-runners with unit tests
auxesis May 4, 2026
45394f5
fix(lint): exclude test files from runner scan and add vitest to root…
auxesis May 4, 2026
55716a7
fix(lint): catch indented npx in multi-line template literals
auxesis May 4, 2026
c671560
fix(lint): unify npx detection into one token-aware regex
auxesis May 4, 2026
e8dad87
ci: enforce lint-no-hardcoded-runners and run script self-tests
auxesis May 4, 2026
a93cea9
fix(cli): render env command output with detected package manager
auxesis May 4, 2026
957d2f9
fix(cli): write supabase-migration header with detected runner
auxesis May 4, 2026
96ce1c3
fix(cli): use detected runner for supabase fallback exec
auxesis May 4, 2026
cead85e
fix(protect): render stash CLI help with detected package manager
auxesis May 4, 2026
571f2ab
fix(wizard): render usage and accept all dlx runners in agent allowlist
auxesis May 4, 2026
bcf33a4
refactor(wizard): derive agent runner prefixes from canonical detect …
auxesis May 4, 2026
9d259e6
fix(drizzle): use detected runner in generate-eql-migration
auxesis May 4, 2026
e820ee7
test(e2e): assert help text uses detected runner for every binary
auxesis May 4, 2026
a8dbb65
chore: changeset for package-manager-aware output coverage
auxesis May 4, 2026
6865884
fix(cli): pass runner through to printNextSteps in supabase migration…
auxesis May 4, 2026
6cc47d2
fix(cli): post-rebase cleanup against renamed `stash` package
auxesis May 4, 2026
b9c2c30
test(cli): assert on runner-agnostic suffix of migrateNotImplemented
auxesis May 4, 2026
17b63dd
fix(wizard): tighten dlx allowlist + cover yarn dlx + dedupe e2e import
auxesis May 4, 2026
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
8 changes: 8 additions & 0 deletions .changeset/package-manager-aware-output.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@cipherstash/cli": patch
"@cipherstash/wizard": patch
"@cipherstash/protect": patch
"@cipherstash/drizzle": patch
---

Render every user-facing CLI string and execute every shell-out under the detected package manager (`npx` / `bunx` / `pnpm dlx` / `yarn dlx`), completing the work started in #379. Affected surfaces: `@cipherstash/cli` top-level + `auth` + `env` help, `db install` Drizzle migration steps, `db migrate` not-implemented warning, the Supabase migration SQL header, the Supabase status fallback exec, the `@cipherstash/protect` `stash` Stricli help (set/get/list/delete), the `@cipherstash/wizard` usage line and agent command allowlist, and the `@cipherstash/drizzle` `generate-eql-migration` help + drizzle-kit invocation. A new `pnpm run lint:runners` lint runs in CI and fails on any reintroduction of a hardcoded runner literal.
6 changes: 6 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Lint — no hardcoded package-manager runners
run: pnpm run lint:runners

- name: Test — lint script self-tests
run: pnpm run test:scripts

- name: Create .env file in ./packages/protect/
run: |
touch ./packages/protect/.env
Expand Down
2 changes: 2 additions & 0 deletions e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
},
"dependencies": {
"stash": "workspace:*",
"@cipherstash/drizzle": "workspace:*",
"@cipherstash/protect": "workspace:*",
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.

Is this just for legacy testing?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correct

"@cipherstash/wizard": "workspace:*"
},
"devDependencies": {
Expand Down
36 changes: 35 additions & 1 deletion e2e/tests/package-managers.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { execFileSync } from 'node:child_process'
import { execFileSync, spawnSync } from 'node:child_process'
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
Expand All @@ -22,6 +22,20 @@ const RUNNER: Record<PackageManager, string> = {
yarn: 'yarn dlx',
}

const BIN = {
cli: resolve(REPO_ROOT, 'packages/cli/dist/bin/stash.js'),
wizard: resolve(REPO_ROOT, 'packages/wizard/dist/bin/wizard.js'),
protect: resolve(REPO_ROOT, 'packages/protect/dist/bin/stash.js'),
drizzleGen: resolve(REPO_ROOT, 'packages/drizzle/dist/bin/generate-eql-migration.js'),
} as const

const UA: Record<PackageManager, string> = {
npm: 'npm/10.0.0',
bun: 'bun/1.0.0',
pnpm: 'pnpm/10.0.0',
yarn: 'yarn/4.0.0',
}

// Suite A — pure-function rendering of "Next Steps" via the CLI's init
// providers. Imports source so we exercise the production code path
// without needing the binary to be built.
Expand Down Expand Up @@ -182,3 +196,23 @@ describe.skipIf(!authConfigured)(
})
},
)

// Suite C — ensures that all built binaries render the correct runner prefix
// in their --help output when executed under different package manager environments.
describe('binaries — help text uses detected runner', () => {
for (const pm of PMS) {
for (const [name, bin] of Object.entries(BIN) as Array<[keyof typeof BIN, string]>) {
it(`${name} --help renders ${RUNNER[pm]} for pm=${pm}`, () => {
const result = spawnSync('node', [bin, '--help'], {
env: { ...process.env, npm_config_user_agent: UA[pm] },
encoding: 'utf8',
})
expect(result.status, `${name} --help (pm=${pm}) stderr: ${result.stderr}`).toBe(0)
expect(result.stdout).toContain(RUNNER[pm])
if (RUNNER[pm] !== 'npx') {
expect(result.stdout).not.toMatch(/\bnpx\b/)
}
})
}
}
})
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,19 @@
"dev": "turbo dev --filter './packages/*'",
"clean": "rimraf --glob **/.next **/.turbo **/dist **/node_modules",
"code:fix": "biome check --write",
"lint:runners": "node scripts/lint-no-hardcoded-runners.mjs",
"release": "pnpm run build && changeset publish",
"test": "turbo test --filter './packages/*'",
"test:e2e": "turbo run test:e2e"
"test:e2e": "turbo run test:e2e",
"test:scripts": "vitest run --config scripts/vitest.config.mjs"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@changesets/cli": "^2.29.6",
"@types/node": "^22.15.12",
"rimraf": "^6.1.2",
"turbo": "2.1.1"
"turbo": "2.1.1",
"vitest": "catalog:repo"
},
"packageManager": "pnpm@10.33.2",
"engines": {
Expand Down
49 changes: 47 additions & 2 deletions packages/cli/src/__tests__/supabase-migration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,28 @@ import {
} from '../commands/db/supabase-migration.js'
import { SUPABASE_PERMISSIONS_SQL } from '../installer/index.js'

/**
* Generate the migration header for testing purposes.
* Mirrors the production function but imported for testing.
*/
function migrationHeader(runner: string): string {
return `-- CipherStash EQL — installed by \`${runner} stash db 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
-- \`anon\`, \`authenticated\`, and \`service_role\` roles the access they need.
--
-- The all-zero \`YYYYMMDDHHMMSS\` prefix is intentional: Supabase orders
-- migrations lexically, so this file runs before any user migration that
-- references the \`eql_v2_encrypted\` type. Do not rename it.
--
-- To upgrade EQL, re-run the install command — it will refuse to overwrite
-- this file unless you pass --force.
--
-- Docs: https://cipherstash.com/docs/stack/cipherstash/supabase
`
}

describe('detectSupabaseProject', () => {
let tmpDir: string

Expand Down Expand Up @@ -112,8 +134,8 @@ describe('writeSupabaseEqlMigration', () => {
const result = await writeSupabaseEqlMigration({ migrationsDir })

const contents = fs.readFileSync(result.path, 'utf-8')
// Header comment block
expect(contents).toMatch(/^--/)
// 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`/)
expect(contents).toContain('CipherStash')
// EQL SQL body — the bundled supabase variant defines eql_v2.
expect(contents).toContain('eql_v2')
Expand Down Expand Up @@ -225,6 +247,29 @@ describe('validateInstallFlags', () => {
})
})

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`.')
})

it('renders the header with the provided runner for bunx', () => {
const header = migrationHeader('bunx')
expect(header).toContain('bunx stash db 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')
})

it('includes all expected documentation lines', () => {
const header = migrationHeader('npx')
expect(header).toContain('eql_v2_encrypted')
expect(header).toContain('https://cipherstash.com/docs/stack/cipherstash/supabase')
})
})

describe('chooseSupabaseInstallMode', () => {
const projectWith = {
hasMigrationsDir: true,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/bin/stash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ async function runDbCommand(
await testConnectionCommand({ databaseUrl })
break
case 'migrate':
p.log.warn(messages.db.migrateNotImplemented)
p.log.warn(messages.db.migrateNotImplemented(STASH))
break
default:
p.log.error(`${messages.db.unknownSubcommand}: ${sub ?? '(none)'}`)
Expand Down
7 changes: 4 additions & 3 deletions packages/cli/src/commands/db/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,14 +308,15 @@ async function generateDrizzleMigration(
) {
const migrationName = options.name ?? DEFAULT_MIGRATION_NAME
const outDir = resolve(options.out ?? DEFAULT_DRIZZLE_OUT)
const drizzleCmd = `${runnerCommand(detectPackageManager(), '').trim()} drizzle-kit generate --custom --name=${migrationName}`
Comment on lines 309 to +311
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot May 4, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

migrationName is interpolated unquoted into a shell command — validate before use

options.name (a raw CLI flag value) flows directly into the drizzleCmd string without sanitization. execSync with a string argument spawns a shell, so it is important to never pass unsanitized user input to this function. A user who passes --name "foo; rm -rf ." (intentionally or accidentally) will have the injected command executed. Even a plain space in the name causes silent argument-splitting failures.

Add an allowlist check before constructing drizzleCmd:

🛡️ Proposed fix — validate `migrationName` early
  const migrationName = options.name ?? DEFAULT_MIGRATION_NAME
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(migrationName)) {
+   p.log.error(
+     `Invalid migration name "${migrationName}". Only alphanumerics, hyphens, and underscores are allowed.`,
+   )
+   p.outro('Migration aborted.')
+   process.exit(1)
+ }
  const outDir = resolve(options.out ?? DEFAULT_DRIZZLE_OUT)
  const drizzleCmd = `${runnerCommand(detectPackageManager(), '').trim()} drizzle-kit generate --custom --name=${migrationName}`

Also applies to: 332-335

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/commands/db/install.ts` around lines 309 - 311, The
migrationName (from options.name) is interpolated directly into drizzleCmd and
then executed via a shell, which allows command injection; validate and sanitize
migrationName before use by applying an allowlist regexp (e.g. only permit
[A-Za-z0-9._-] and reject or error on anything else) and refuse or normalize
invalid input, then construct drizzleCmd using the validated migrationName;
alternatively, avoid shell interpolation altogether by invoking the command with
child_process.spawn/execFile with an args array (use
runnerCommand/detectPackageManager to locate the binary and pass
["drizzle-kit","generate","--custom","--name", migrationName]) so user input is
never injected into a shell.

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.

I think this is valid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@coderdan, glad the finding resonates! Would you like me to go ahead and plan the code change? The fix involves two parts:

  1. Validate migrationName early (before constructing drizzleCmd) with an allowlist regex so invalid names are rejected with a clear error.
  2. Optionally, switch from execSync(string) to execSync with a pre-built array-style invocation or execFile to avoid shell interpolation entirely — though input validation alone is the minimal safe fix.

Just say the word and I'll kick off ``.

( ˆ_ˆ)⊃━ ✉


Failed to handle agent chat message. Please try again.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot May 4, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate and display the runnerCommand implementation
ast-grep --pattern 'function runnerCommand($$$) { $$$ }'
ast-grep --pattern 'const runnerCommand = ($$$) => $$$'
ast-grep --pattern 'export function runnerCommand($$$) { $$$ }'
rg -n "runnerCommand" --type ts -A 5 -B 2

Repository: cipherstash/stack

Length of output: 29999


Cache detectPackageManager() once and pass 'drizzle-kit' to runnerCommand for clarity

Two issues in generateDrizzleMigration:

  1. API misuse: runnerCommand expects a package name as its second argument. Passing '' and trimming the trailing space works today but is fragile — any internal change to runnerCommand (e.g., formatting differences) silently breaks the command. Pass 'drizzle-kit' directly instead.

  2. Redundant calls: detectPackageManager() is called separately on line 311 and again on line 443. Cache it once at the function level.

Suggested fix
  const migrationName = options.name ?? DEFAULT_MIGRATION_NAME
  const outDir = resolve(options.out ?? DEFAULT_DRIZZLE_OUT)
+ const pm = detectPackageManager()
- const drizzleCmd = `${runnerCommand(detectPackageManager(), '').trim()} drizzle-kit generate --custom --name=${migrationName}`
+ const drizzleCmd = `${runnerCommand(pm, 'drizzle-kit')} generate --custom --name=${migrationName}`
  p.note(
-   `Run your Drizzle migrations to install EQL:\n\n  ${runnerCommand(detectPackageManager(), '').trim()} drizzle-kit migrate`,
+   `Run your Drizzle migrations to install EQL:\n\n  ${runnerCommand(pm, 'drizzle-kit')} migrate`,
    'Next Steps',
  )

Also applies to: 443-445

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/commands/db/install.ts` at line 311, In
generateDrizzleMigration, avoid calling detectPackageManager() twice and pass
the real package name into runnerCommand: compute and store const pkgManager =
detectPackageManager() once at the top of the function, then build drizzleCmd
using runnerCommand(pkgManager, 'drizzle-kit') (instead of
runnerCommand(detectPackageManager(), '')). Update any other usages around the
later block (previously calling detectPackageManager() again on lines ~443-445)
to reuse pkgManager so the command uses a cached package manager and correctly
specifies 'drizzle-kit'; keep using migrationName for the --name flag and
preserve trimming/spacing as needed.

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.

Minor but valid.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@coderdan, glad it's useful! Here's the planned fix:

  • Cache detectPackageManager() once as pm at the top of generateDrizzleMigration
  • Replace both runnerCommand(detectPackageManager(), '').trim() usages with runnerCommand(pm, 'drizzle-kit')

Failed to handle agent chat message. Please try again.


if (options.dryRun) {
p.log.info('Dry run — no changes will be made.')
const source = options.latest
? 'Would download EQL install SQL from GitHub'
: 'Would use bundled EQL install SQL'
p.note(
`Would run: npx drizzle-kit generate --custom --name=${migrationName}\n${source}\nWould write SQL to migration file in ${outDir}`,
`Would run: ${drizzleCmd}\n${source}\nWould write SQL to migration file in ${outDir}`,
'Dry Run',
)
p.outro('Dry run complete.')
Expand All @@ -328,7 +329,7 @@ async function generateDrizzleMigration(
s.start('Generating custom Drizzle migration...')

try {
execSync(`npx drizzle-kit generate --custom --name=${migrationName}`, {
execSync(drizzleCmd, {
stdio: 'pipe',
encoding: 'utf-8',
})
Expand Down Expand Up @@ -439,7 +440,7 @@ async function generateDrizzleMigration(

p.log.success(`Migration created: ${generatedMigrationPath}`)
p.note(
'Run your Drizzle migrations to install EQL:\n\n npx drizzle-kit migrate',
`Run your Drizzle migrations to install EQL:\n\n ${runnerCommand(detectPackageManager(), '').trim()} drizzle-kit migrate`,
'Next Steps',
)
printNextSteps()
Expand Down
16 changes: 12 additions & 4 deletions packages/cli/src/commands/db/supabase-migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
SUPABASE_PERMISSIONS_SQL,
loadBundledEqlSql,
} from '@/installer/index.js'
import { detectPackageManager, runnerCommand } from '@/commands/init/utils.js'

/**
* Filename of the Supabase migration that installs CipherStash EQL.
Expand All @@ -22,9 +23,11 @@ export const SUPABASE_EQL_MIGRATION_FILENAME =
/**
* Header comment block prepended to the generated migration. Explains *why*
* this file exists for future maintainers reading their own migrations
* directory.
* directory. The runner is resolved at call time based on the detected
* package manager.
*/
const MIGRATION_HEADER = `-- CipherStash EQL — installed by \`npx stash db install --supabase --migration\`.
function migrationHeader(runner: string): string {
return `-- CipherStash EQL — installed by \`${runner} stash db 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
Expand All @@ -39,6 +42,7 @@ const MIGRATION_HEADER = `-- CipherStash EQL — installed by \`npx stash db ins
--
-- Docs: https://cipherstash.com/docs/stack/cipherstash/supabase
`
}

export interface WriteSupabaseEqlMigrationOptions {
/**
Expand Down Expand Up @@ -70,7 +74,7 @@ export interface WriteSupabaseEqlMigrationResult {
* Generate the `<migrationsDir>/00000000000000_cipherstash_eql.sql` migration.
*
* The file body is, in order:
* 1. {@link MIGRATION_HEADER} — explains why the file exists.
* 1. Migration header (generated from {@link migrationHeader}) — explains why the file exists.
* 2. The bundled `cipherstash-encrypt-supabase.sql` install script.
* 3. {@link SUPABASE_PERMISSIONS_SQL} — the same grants the runtime install
* path issues. One source of truth for both code paths.
Expand Down Expand Up @@ -104,8 +108,12 @@ export async function writeSupabaseEqlMigration(
excludeOperatorFamily: excludeOperatorFamily || true,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})

const pm = detectPackageManager()
const runner = runnerCommand(pm, '').trim()
const header = migrationHeader(runner)

const body = [
MIGRATION_HEADER,
header,
'',
eqlSql.trimEnd(),
'',
Expand Down
14 changes: 9 additions & 5 deletions packages/cli/src/commands/env/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { existsSync, writeFileSync } from 'node:fs'
import { resolve } from 'node:path'
import * as p from '@clack/prompts'
import { detectPackageManager, runnerCommand } from '../init/utils.js'

export interface EnvOptions {
/** Write the emitted block to `.env.production.local` instead of stdout. */
Expand Down Expand Up @@ -28,17 +29,20 @@ export async function envCommand(options: EnvOptions = {}): Promise<void> {
return
}

p.intro('npx stash env')
const runner = runnerCommand(detectPackageManager(), '').trim()
const cliRef = `${runner} stash`

p.intro(`${cliRef} env`)

const creds = await fetchProdCredentials()
if (!creds) {
p.log.error(
'Could not mint production credentials. Make sure you are logged in: npx stash auth login',
`Could not mint production credentials. Make sure you are logged in: ${cliRef} auth login`,
)
process.exit(1)
}

const block = formatEnvBlock(creds)
const block = formatEnvBlock(creds, cliRef)

if (options.write) {
const target = resolve(process.cwd(), '.env.production.local')
Expand Down Expand Up @@ -80,9 +84,9 @@ async function fetchProdCredentials(): Promise<ProdCredentials | undefined> {
return undefined
}

function formatEnvBlock(creds: ProdCredentials): string {
function formatEnvBlock(creds: ProdCredentials, cliRef: string): string {
return [
'# Generated by `npx stash env` — production credentials',
`# Generated by \`${cliRef} env\` — production credentials`,
`CS_CLIENT_ID=${creds.clientId}`,
`CS_CLIENT_KEY=${creds.clientKey}`,
`CS_WORKSPACE_ID=${creds.workspaceId}`,
Expand Down
14 changes: 11 additions & 3 deletions packages/cli/src/config/database-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { join } from 'node:path'
import * as p from '@clack/prompts'
import { detectSupabaseProject } from '../commands/db/detect.js'
import { messages } from '../messages.js'
import { detectPackageManager, runnerCommand } from '../commands/init/utils.js'

export interface ResolveDatabaseUrlOptions {
/** Value of `--database-url` if the user passed one. */
Expand Down Expand Up @@ -111,10 +112,17 @@ function isUrlParseable(value: string): boolean {

/** Try to extract a `DB_URL=...` value from `supabase status --output env`. */
function trySupabaseStatus(): string | undefined {
const candidates = [
const runner = runnerCommand(detectPackageManager(), '').trim()
// `runner` is one of 'npx' | 'bunx' | 'pnpm dlx' | 'yarn dlx'.
// Split on whitespace because pnpm/yarn dlx uses two tokens.
const dlxArgs = runner.split(/\s+/)
const candidates: Array<readonly [string, readonly string[]]> = [
['supabase', ['status', '--output', 'env']],
['npx', ['--no-install', 'supabase', 'status', '--output', 'env']],
] as const
[
dlxArgs[0],
[...dlxArgs.slice(1), 'supabase', 'status', '--output', 'env'],
],
]

for (const [cmd, args] of candidates) {
try {
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export const messages = {
},
db: {
unknownSubcommand: 'Unknown db subcommand',
migrateNotImplemented: '"npx stash db migrate" is not yet implemented.',
migrateNotImplemented: (stashRef: string) =>
`"${stashRef} db migrate" is not yet implemented.`,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
/** Source labels surfaced after DATABASE_URL resolution. */
urlResolvedFromFlag: 'Using DATABASE_URL from --database-url flag',
urlResolvedFromSupabase: 'Using DATABASE_URL from supabase status',
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/tests/e2e/smoke.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ describe('stash CLI — non-interactive smoke', () => {
const r = render(['db', 'migrate'])
const { exitCode } = await r.exit
expect(exitCode).toBe(0)
expect(r.output).toContain(messages.db.migrateNotImplemented)
// `migrateNotImplemented` is a runner-aware factory; the runner-agnostic
// suffix is the stable assertion target.
expect(r.output).toContain('stash db migrate" is not yet implemented.')
})
})
Loading
Loading