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
27 changes: 27 additions & 0 deletions .changeset/cli-runner-aware-help-banners.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
'@cipherstash/cli': patch
---

Make `--help` banners and the post-install "Next steps" panel show commands using the package manager the user actually invoked the CLI with, instead of always emitting `npx`.

A user who runs `bunx @cipherstash/cli --help` now sees:

```
Usage: bunx @cipherstash/cli <command> [options]
Examples:
bunx @cipherstash/cli init
bunx @cipherstash/cli auth login
bunx @cipherstash/cli db install
```
Comment thread
coderdan marked this conversation as resolved.

instead of `npx @cipherstash/cli …` regardless of how they invoked it. Same for `pnpm dlx`, `yarn dlx`, and the default `npx` path.

Concretely:

- `--help` (top-level) — usage line and all six examples in `bin/stash.ts`.
- `--help` (auth) — usage line and the two `auth login` examples in `commands/auth/index.ts`.
- `db install`'s "Next steps" note — the `wizard` invocation now matches the user's runner.
- The `@cipherstash/stack is required for this command` hint shown by `requireStack` (when `db push`/`validate`/`schema build` are run before the runtime SDK is installed) now suggests the package manager's install command and the user's runner for the follow-up `init` invocation.

No public-API change. Detection sources unchanged from #379: `npm_config_user_agent` first, then lockfile, then `npx` fallback.
44 changes: 28 additions & 16 deletions packages/cli/src/bin/stash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,31 +34,43 @@ function isModuleNotFound(err: unknown): boolean {
)
}

import {
detectPackageManager,
prodInstallCommand,
runnerCommand,
} from '../commands/init/utils.js'

const __dirname = dirname(fileURLToPath(import.meta.url))
const pkg = JSON.parse(
readFileSync(join(__dirname, '../../package.json'), 'utf-8'),
)

// Detect once, share across help rendering and the requireStack hint.
// Detection reads `npm_config_user_agent` (when the user invoked via
// `bunx`/`pnpm dlx`/`yarn dlx`) and falls back to the lockfile in cwd.
const PM = detectPackageManager()
const STASH = runnerCommand(PM, 'stash')

async function requireStack<T>(importFn: () => Promise<T>): Promise<T> {
try {
return await importFn()
} catch (err: unknown) {
if (isModuleNotFound(err)) {
p.log.error(
'@cipherstash/stack is required for this command.\n' +
' Install it with: npm install @cipherstash/stack\n' +
' Or run: npx stash init',
`@cipherstash/stack is required for this command.
Install it with: ${prodInstallCommand(PM, '@cipherstash/stack')}
Or run: ${STASH} init`,
)
process.exit(1) as never
}
throw err
}
}

const __dirname = dirname(fileURLToPath(import.meta.url))
const pkg = JSON.parse(
readFileSync(join(__dirname, '../../package.json'), 'utf-8'),
)

const HELP = `
${messages.cli.versionBannerPrefix}${pkg.version}

${messages.cli.usagePrefix} <command> [options]
${messages.cli.usagePrefix}${STASH} <command> [options]

Commands:
init Initialize CipherStash for your project
Expand Down Expand Up @@ -98,13 +110,13 @@ DB Flags:
--database-url <url> (all db / schema commands) Override DATABASE_URL for this run only — never written to disk

Examples:
npx stash init
npx stash init --supabase
npx stash auth login
npx stash wizard
npx stash db install
npx stash db push
npx stash schema build
${STASH} init
${STASH} init --supabase
${STASH} auth login
${STASH} wizard
${STASH} db install
${STASH} db push
${STASH} schema build
`.trim()

interface ParsedArgs {
Expand Down
9 changes: 6 additions & 3 deletions packages/cli/src/commands/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { messages } from '../../messages.js'
import { detectPackageManager, runnerCommand } from '../init/utils.js'
import { bindDevice, login, selectRegion } from './login.js'

const STASH_AUTH = runnerCommand(detectPackageManager(), 'stash auth')

const HELP = `
${messages.auth.usagePrefix} <command> [options]
${messages.auth.usagePrefix}${STASH_AUTH} <command> [options]

Commands:
login Authenticate with CipherStash
Expand All @@ -12,8 +15,8 @@ Options:
--drizzle Track Drizzle as the referrer

Examples:
npx stash auth login
npx stash auth login --supabase
${STASH_AUTH} login
${STASH_AUTH} login --supabase
`.trim()

function referrerFromFlags(flags: Record<string, boolean>): string | undefined {
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/commands/db/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,12 +269,13 @@ function resolveProviderOptions(
}

function printNextSteps(): void {
const pm = detectPackageManager()
p.note(
[
'Next steps:',
'',
' 1. Wire up encrypt/decrypt with the wizard (AI-guided, automated):',
' stash wizard',
` ${runnerCommand(pm, 'stash')} wizard`,
'',
' 2. Or use the client directly from @cipherstash/stack:',
" import { Encryption } from '@cipherstash/stack'",
Expand Down
12 changes: 10 additions & 2 deletions packages/cli/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,19 @@
export const messages = {
cli: {
versionBannerPrefix: 'CipherStash CLI v',
usagePrefix: 'Usage: npx stash',
/**
* Stable leader of the usage line. The runner-and-package portion
* (e.g. `npx stash` or `bunx stash`) is appended at render time by
* the bin so the help text matches how the user invoked the CLI.
* Tests assert on this leader plus `'stash'` separately to stay
* runner-agnostic.
*/
usagePrefix: 'Usage: ',
unknownCommand: 'Unknown command',
},
auth: {
usagePrefix: 'Usage: npx stash auth',
/** Same shape as `cli.usagePrefix` — leader only. */
usagePrefix: 'Usage: ',
unknownSubcommand: 'Unknown auth command',
selectRegion: 'Select a region',
cancelled: 'Cancelled.',
Expand Down
55 changes: 55 additions & 0 deletions packages/cli/tests/e2e/runner-aware-help.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, expect, it } from 'vitest'
import { render } from '../helpers/pty.js'

/**
* E2E coverage for the runner-aware help rendering. The smoke suite
* already verifies the help is rendered; this file specifically asserts
* that the correct package-manager runner (`npx` / `bunx` / `pnpm dlx` /
* `yarn dlx`) flows from `npm_config_user_agent` through
* `detectPackageManager` → `runnerCommand` into the Usage line and
* Examples.
*
* Detection itself is unit-tested in
* `src/commands/init/__tests__/utils.test.ts`. These E2E tests close the
* gap between "the helper works in isolation" and "the rendered HELP
* actually surfaces what the helper produces".
*/

const cases = [
{ ua: '', label: 'npx' }, // empty UA — falls back to lockfile/npm
{ ua: 'bun/1.0.0', label: 'bunx' },
Comment thread
coderdan marked this conversation as resolved.
{ ua: 'pnpm/9.0.0', label: 'pnpm dlx' },
{ ua: 'yarn/4.0.0', label: 'yarn dlx' },
] as const

describe('--help — runner-aware Usage + Examples', () => {
it.each(cases)(
'with npm_config_user_agent=$ua, renders "$label stash"',
async ({ ua, label }) => {
const r = render(['--help'], { env: { npm_config_user_agent: ua } })
const { exitCode } = await r.exit
expect(exitCode).toBe(0)
// Usage line must use the right runner. The leader is stable
// (`messages.cli.usagePrefix === 'Usage: '`) so we assert on the
// suffix the renderer composes at runtime.
expect(r.output).toContain(`Usage: ${label} stash`)
// At least one of the Examples lines must surface the same runner.
expect(r.output).toContain(`${label} stash init`)
expect(r.output).toContain(`${label} stash db install`)
},
)
})

describe('auth — runner-aware Usage + Examples', () => {
it.each(cases)(
'with npm_config_user_agent=$ua, renders "$label stash auth"',
async ({ ua, label }) => {
// `auth` with no subcommand prints the auth HELP and exits 0.
const r = render(['auth'], { env: { npm_config_user_agent: ua } })
const { exitCode } = await r.exit
expect(exitCode).toBe(0)
expect(r.output).toContain(`Usage: ${label} stash auth`)
expect(r.output).toContain(`${label} stash auth login`)
},
)
})
Loading