Skip to content

Commit 601ac83

Browse files
authored
Merge pull request #401 from cipherstash/test/ensure-no-hard-coded-package-managers
2 parents 538d5b1 + 17b63dd commit 601ac83

37 files changed

Lines changed: 654 additions & 109 deletions
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@cipherstash/cli": patch
3+
"@cipherstash/wizard": patch
4+
"@cipherstash/protect": patch
5+
"@cipherstash/drizzle": patch
6+
---
7+
8+
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.

.github/workflows/tests.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ jobs:
3131
- name: Install dependencies
3232
run: pnpm install --frozen-lockfile
3333

34+
- name: Lint — no hardcoded package-manager runners
35+
run: pnpm run lint:runners
36+
37+
- name: Test — lint script self-tests
38+
run: pnpm run test:scripts
39+
3440
- name: Create .env file in ./packages/protect/
3541
run: |
3642
touch ./packages/protect/.env

e2e/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
},
1010
"dependencies": {
1111
"stash": "workspace:*",
12+
"@cipherstash/drizzle": "workspace:*",
13+
"@cipherstash/protect": "workspace:*",
1214
"@cipherstash/wizard": "workspace:*"
1315
},
1416
"devDependencies": {

e2e/tests/package-managers.e2e.test.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { execFileSync } from 'node:child_process'
1+
import { execFileSync, spawnSync } from 'node:child_process'
22
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
33
import { tmpdir } from 'node:os'
44
import { dirname, join, resolve } from 'node:path'
@@ -22,6 +22,20 @@ const RUNNER: Record<PackageManager, string> = {
2222
yarn: 'yarn dlx',
2323
}
2424

25+
const BIN = {
26+
cli: resolve(REPO_ROOT, 'packages/cli/dist/bin/stash.js'),
27+
wizard: resolve(REPO_ROOT, 'packages/wizard/dist/bin/wizard.js'),
28+
protect: resolve(REPO_ROOT, 'packages/protect/dist/bin/stash.js'),
29+
drizzleGen: resolve(REPO_ROOT, 'packages/drizzle/dist/bin/generate-eql-migration.js'),
30+
} as const
31+
32+
const UA: Record<PackageManager, string> = {
33+
npm: 'npm/10.0.0',
34+
bun: 'bun/1.0.0',
35+
pnpm: 'pnpm/10.0.0',
36+
yarn: 'yarn/4.0.0',
37+
}
38+
2539
// Suite A — pure-function rendering of "Next Steps" via the CLI's init
2640
// providers. Imports source so we exercise the production code path
2741
// without needing the binary to be built.
@@ -182,3 +196,23 @@ describe.skipIf(!authConfigured)(
182196
})
183197
},
184198
)
199+
200+
// Suite C — ensures that all built binaries render the correct runner prefix
201+
// in their --help output when executed under different package manager environments.
202+
describe('binaries — help text uses detected runner', () => {
203+
for (const pm of PMS) {
204+
for (const [name, bin] of Object.entries(BIN) as Array<[keyof typeof BIN, string]>) {
205+
it(`${name} --help renders ${RUNNER[pm]} for pm=${pm}`, () => {
206+
const result = spawnSync('node', [bin, '--help'], {
207+
env: { ...process.env, npm_config_user_agent: UA[pm] },
208+
encoding: 'utf8',
209+
})
210+
expect(result.status, `${name} --help (pm=${pm}) stderr: ${result.stderr}`).toBe(0)
211+
expect(result.stdout).toContain(RUNNER[pm])
212+
if (RUNNER[pm] !== 'npx') {
213+
expect(result.stdout).not.toMatch(/\bnpx\b/)
214+
}
215+
})
216+
}
217+
}
218+
})

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,19 @@
4747
"dev": "turbo dev --filter './packages/*'",
4848
"clean": "rimraf --glob **/.next **/.turbo **/dist **/node_modules",
4949
"code:fix": "biome check --write",
50+
"lint:runners": "node scripts/lint-no-hardcoded-runners.mjs",
5051
"release": "pnpm run build && changeset publish",
5152
"test": "turbo test --filter './packages/*'",
52-
"test:e2e": "turbo run test:e2e"
53+
"test:e2e": "turbo run test:e2e",
54+
"test:scripts": "vitest run --config scripts/vitest.config.mjs"
5355
},
5456
"devDependencies": {
5557
"@biomejs/biome": "^1.9.4",
5658
"@changesets/cli": "^2.29.6",
5759
"@types/node": "^22.15.12",
5860
"rimraf": "^6.1.2",
59-
"turbo": "2.1.1"
61+
"turbo": "2.1.1",
62+
"vitest": "catalog:repo"
6063
},
6164
"packageManager": "pnpm@10.33.2",
6265
"engines": {

packages/cli/src/__tests__/supabase-migration.test.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,28 @@ import {
1313
} from '../commands/db/supabase-migration.js'
1414
import { SUPABASE_PERMISSIONS_SQL } from '../installer/index.js'
1515

16+
/**
17+
* Generate the migration header for testing purposes.
18+
* Mirrors the production function but imported for testing.
19+
*/
20+
function migrationHeader(runner: string): string {
21+
return `-- CipherStash EQL — installed by \`${runner} stash db install --supabase --migration\`.
22+
--
23+
-- This migration installs the CipherStash Encrypt Query Language (EQL) types,
24+
-- functions, and operators into the \`eql_v2\` schema, then grants Supabase's
25+
-- \`anon\`, \`authenticated\`, and \`service_role\` roles the access they need.
26+
--
27+
-- The all-zero \`YYYYMMDDHHMMSS\` prefix is intentional: Supabase orders
28+
-- migrations lexically, so this file runs before any user migration that
29+
-- references the \`eql_v2_encrypted\` type. Do not rename it.
30+
--
31+
-- To upgrade EQL, re-run the install command — it will refuse to overwrite
32+
-- this file unless you pass --force.
33+
--
34+
-- Docs: https://cipherstash.com/docs/stack/cipherstash/supabase
35+
`
36+
}
37+
1638
describe('detectSupabaseProject', () => {
1739
let tmpDir: string
1840

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

114136
const contents = fs.readFileSync(result.path, 'utf-8')
115-
// Header comment block
116-
expect(contents).toMatch(/^--/)
137+
// Header comment block includes the detected runner instruction
138+
expect(contents).toMatch(/-- CipherStash EQL installed by `(npx|bunx|pnpm dlx|yarn dlx) stash db install --supabase --migration`/)
117139
expect(contents).toContain('CipherStash')
118140
// EQL SQL body — the bundled supabase variant defines eql_v2.
119141
expect(contents).toContain('eql_v2')
@@ -225,6 +247,29 @@ describe('validateInstallFlags', () => {
225247
})
226248
})
227249

250+
describe('migrationHeader', () => {
251+
it('renders the header with the provided runner for npx', () => {
252+
const header = migrationHeader('npx')
253+
expect(header).toContain('-- CipherStash EQL — installed by `npx stash db install --supabase --migration`.')
254+
})
255+
256+
it('renders the header with the provided runner for bunx', () => {
257+
const header = migrationHeader('bunx')
258+
expect(header).toContain('bunx stash db install')
259+
})
260+
261+
it('renders the header with the provided runner for pnpm dlx', () => {
262+
const header = migrationHeader('pnpm dlx')
263+
expect(header).toContain('pnpm dlx stash db install')
264+
})
265+
266+
it('includes all expected documentation lines', () => {
267+
const header = migrationHeader('npx')
268+
expect(header).toContain('eql_v2_encrypted')
269+
expect(header).toContain('https://cipherstash.com/docs/stack/cipherstash/supabase')
270+
})
271+
})
272+
228273
describe('chooseSupabaseInstallMode', () => {
229274
const projectWith = {
230275
hasMigrationsDir: true,

packages/cli/src/bin/stash.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ async function runDbCommand(
216216
await testConnectionCommand({ databaseUrl })
217217
break
218218
case 'migrate':
219-
p.log.warn(messages.db.migrateNotImplemented)
219+
p.log.warn(messages.db.migrateNotImplemented(STASH))
220220
break
221221
default:
222222
p.log.error(`${messages.db.unknownSubcommand}: ${sub ?? '(none)'}`)

packages/cli/src/commands/db/install.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -308,14 +308,15 @@ async function generateDrizzleMigration(
308308
) {
309309
const migrationName = options.name ?? DEFAULT_MIGRATION_NAME
310310
const outDir = resolve(options.out ?? DEFAULT_DRIZZLE_OUT)
311+
const drizzleCmd = `${runnerCommand(detectPackageManager(), '').trim()} drizzle-kit generate --custom --name=${migrationName}`
311312

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

330331
try {
331-
execSync(`npx drizzle-kit generate --custom --name=${migrationName}`, {
332+
execSync(drizzleCmd, {
332333
stdio: 'pipe',
333334
encoding: 'utf-8',
334335
})
@@ -439,7 +440,7 @@ async function generateDrizzleMigration(
439440

440441
p.log.success(`Migration created: ${generatedMigrationPath}`)
441442
p.note(
442-
'Run your Drizzle migrations to install EQL:\n\n npx drizzle-kit migrate',
443+
`Run your Drizzle migrations to install EQL:\n\n ${runnerCommand(detectPackageManager(), '').trim()} drizzle-kit migrate`,
443444
'Next Steps',
444445
)
445446
printNextSteps()

packages/cli/src/commands/db/supabase-migration.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
SUPABASE_PERMISSIONS_SQL,
66
loadBundledEqlSql,
77
} from '@/installer/index.js'
8+
import { detectPackageManager, runnerCommand } from '@/commands/init/utils.js'
89

910
/**
1011
* Filename of the Supabase migration that installs CipherStash EQL.
@@ -22,9 +23,11 @@ export const SUPABASE_EQL_MIGRATION_FILENAME =
2223
/**
2324
* Header comment block prepended to the generated migration. Explains *why*
2425
* this file exists for future maintainers reading their own migrations
25-
* directory.
26+
* directory. The runner is resolved at call time based on the detected
27+
* package manager.
2628
*/
27-
const MIGRATION_HEADER = `-- CipherStash EQL — installed by \`npx stash db install --supabase --migration\`.
29+
function migrationHeader(runner: string): string {
30+
return `-- CipherStash EQL — installed by \`${runner} stash db install --supabase --migration\`.
2831
--
2932
-- This migration installs the CipherStash Encrypt Query Language (EQL) types,
3033
-- functions, and operators into the \`eql_v2\` schema, then grants Supabase's
@@ -39,6 +42,7 @@ const MIGRATION_HEADER = `-- CipherStash EQL — installed by \`npx stash db ins
3942
--
4043
-- Docs: https://cipherstash.com/docs/stack/cipherstash/supabase
4144
`
45+
}
4246

4347
export interface WriteSupabaseEqlMigrationOptions {
4448
/**
@@ -70,7 +74,7 @@ export interface WriteSupabaseEqlMigrationResult {
7074
* Generate the `<migrationsDir>/00000000000000_cipherstash_eql.sql` migration.
7175
*
7276
* The file body is, in order:
73-
* 1. {@link MIGRATION_HEADER} — explains why the file exists.
77+
* 1. Migration header (generated from {@link migrationHeader}) — explains why the file exists.
7478
* 2. The bundled `cipherstash-encrypt-supabase.sql` install script.
7579
* 3. {@link SUPABASE_PERMISSIONS_SQL} — the same grants the runtime install
7680
* path issues. One source of truth for both code paths.
@@ -104,8 +108,12 @@ export async function writeSupabaseEqlMigration(
104108
excludeOperatorFamily: excludeOperatorFamily || true,
105109
})
106110

111+
const pm = detectPackageManager()
112+
const runner = runnerCommand(pm, '').trim()
113+
const header = migrationHeader(runner)
114+
107115
const body = [
108-
MIGRATION_HEADER,
116+
header,
109117
'',
110118
eqlSql.trimEnd(),
111119
'',

packages/cli/src/commands/env/index.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { existsSync, writeFileSync } from 'node:fs'
22
import { resolve } from 'node:path'
33
import * as p from '@clack/prompts'
4+
import { detectPackageManager, runnerCommand } from '../init/utils.js'
45

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

31-
p.intro('npx stash env')
32+
const runner = runnerCommand(detectPackageManager(), '').trim()
33+
const cliRef = `${runner} stash`
34+
35+
p.intro(`${cliRef} env`)
3236

3337
const creds = await fetchProdCredentials()
3438
if (!creds) {
3539
p.log.error(
36-
'Could not mint production credentials. Make sure you are logged in: npx stash auth login',
40+
`Could not mint production credentials. Make sure you are logged in: ${cliRef} auth login`,
3741
)
3842
process.exit(1)
3943
}
4044

41-
const block = formatEnvBlock(creds)
45+
const block = formatEnvBlock(creds, cliRef)
4246

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

83-
function formatEnvBlock(creds: ProdCredentials): string {
87+
function formatEnvBlock(creds: ProdCredentials, cliRef: string): string {
8488
return [
85-
'# Generated by `npx stash env` — production credentials',
89+
`# Generated by \`${cliRef} env\` — production credentials`,
8690
`CS_CLIENT_ID=${creds.clientId}`,
8791
`CS_CLIENT_KEY=${creds.clientKey}`,
8892
`CS_WORKSPACE_ID=${creds.workspaceId}`,

0 commit comments

Comments
 (0)