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
12 changes: 12 additions & 0 deletions .changeset/init-friction-cuts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@cipherstash/cli': minor
---

Reduce friction in `stash init`.

- **No more "How will you connect to your database?" prompt.** Init now auto-detects Drizzle (from `drizzle.config.*` or `drizzle-orm`/`drizzle-kit` in `package.json`) and Supabase (from the host in `DATABASE_URL`), and silently picks the matching encryption client template. Falls back to a generic Postgres template otherwise.
- **No more "Where should we create your encryption client?" prompt.** Init writes to `./src/encryption/index.ts` by default. The "file already exists, what would you like to do?" prompt still appears so existing client files aren't silently overwritten.
- **Single combined dependency-install prompt.** Previously init asked twice (once for `@cipherstash/stack`, once for `@cipherstash/cli`). It now asks once, listing both, and runs the installs in sequence. When both packages are already in `node_modules`, no prompt appears at all.
- **Already-authenticated users skip the "Continue with workspace X?" prompt.** Init logs `Using workspace X` and proceeds. Run `stash auth login` directly to switch workspaces.

`stash db install` now also calls into the same encryption-client scaffolder as a safety net — users who run `db install` without `init` first still get a working client file generated at the path their `stash.config.ts` points to.
9 changes: 9 additions & 0 deletions .changeset/wizard-extracted-package.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@cipherstash/cli': minor
---

**Breaking:** the `stash wizard` command has been removed. The AI-guided encryption setup is now its own package — run it via `npx @cipherstash/wizard` (or `pnpm dlx`, `bunx`, `yarn dlx`).

The wizard was pulling `@anthropic-ai/claude-agent-sdk` (47MB unpacked) into every `npx @cipherstash/cli` invocation, even for fast commands like `init`, `auth`, and `db install`. Splitting it out keeps cli's dependency tree small and lets each package manager handle the wizard's install natively — no more shelling out to `npm` from inside the cli, no Yarn PnP / Bun-only failure modes.

The next-steps output from `init` and `db install` still recommends `npx @cipherstash/wizard` as the automated path. The `schema build` command no longer offers a wizard/builder selection prompt — it goes straight to the schema builder.
16 changes: 16 additions & 0 deletions .changeset/wizard-initial-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@cipherstash/wizard': minor
---

Initial release of `@cipherstash/wizard` — AI-powered encryption setup for CipherStash, extracted from `@cipherstash/cli`.

Run it once per project, after `stash init`:

```bash
npx @cipherstash/wizard
pnpm dlx @cipherstash/wizard
yarn dlx @cipherstash/wizard
bunx @cipherstash/wizard
```

The wizard reads your codebase, asks which columns to encrypt, hands a surgical prompt to the Claude Agent SDK against the CipherStash-hosted LLM gateway, and runs deterministic post-agent steps (package install, `db install`, `db push`, framework migrations). Same behavior as the previous `stash wizard` command — just shipped as its own package so it doesn't bloat the cli's dependency tree.
45 changes: 14 additions & 31 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![npm version](https://img.shields.io/npm/v/@cipherstash/cli.svg?style=for-the-badge&labelColor=000000)](https://www.npmjs.com/package/@cipherstash/cli)
[![License: MIT](https://img.shields.io/npm/l/@cipherstash/cli.svg?style=for-the-badge&labelColor=000000)](https://github.com/cipherstash/protectjs/blob/main/LICENSE.md)

The single CLI for CipherStash. It handles authentication, project initialization, AI-guided encryption setup, EQL database lifecycle (install, upgrade, validate, push, migrate), schema building, and encrypted secrets management. Install it as a devDependency alongside the runtime SDK `@cipherstash/stack`.
The single CLI for CipherStash. It handles authentication, project initialization, EQL database lifecycle (install, upgrade, validate, push, migrate), schema building, and encrypted secrets management. Install it as a devDependency alongside the runtime SDK `@cipherstash/stack`.

---

Expand All @@ -14,29 +14,30 @@ npm install -D @cipherstash/cli
npx @cipherstash/cli auth login # authenticate with CipherStash
npx @cipherstash/cli init # scaffold encryption schema and install dependencies
npx @cipherstash/cli db install # scaffold stash.config.ts (if missing) and install EQL
npx @cipherstash/cli wizard # AI agent wires encryption into your codebase
```

What each step does:

- `auth login` — opens a browser-based device code flow and saves a token to `~/.cipherstash/auth.json`.
- `init` — generates your encryption client file and installs `@cipherstash/cli` as a dev dependency. Pass `--supabase` or `--drizzle` for provider-specific setup.
- `db install` — detects your encryption client, writes `stash.config.ts` if it's missing, and installs EQL extensions in a single step.
- `wizard` — reads your codebase with an AI agent (uses the CipherStash-hosted LLM gateway, no Anthropic API key required) and modifies your schema files in place.

After `db install`, declare which columns to encrypt — either run [`@cipherstash/wizard`](https://www.npmjs.com/package/@cipherstash/wizard) to do it automatically, or edit your encryption client file (default `./src/encryption/index.ts`) by hand.

---

## Recommended flow

```
npx @cipherstash/cli init
└── npx @cipherstash/cli db install
└── npx @cipherstash/cli wizard ← fast path: AI edits your files
OR
Edit schema files by hand ← escape hatch
npx @cipherstash/cli auth login
└── npx @cipherstash/cli init
└── npx @cipherstash/cli db install
└── npx @cipherstash/wizard ← fast path: AI edits your files
OR
Edit schema files by hand ← escape hatch
```
Comment on lines 31 to 38
Copy link
Copy Markdown

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

Add a language to the flow-diagram fence.

The unlabeled fence on Line 31 triggers MD040. Mark it as text so docs lint stays clean.

Suggested fix
-```
+```text
 npx `@cipherstash/cli` auth login
     └── npx `@cipherstash/cli` init
             └── npx `@cipherstash/cli` db install
                     └── npx `@cipherstash/wizard`       ← fast path: AI edits your files
                             OR
                         Edit schema files by hand     ← escape hatch
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 31-31: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

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

In `@packages/cli/README.md` around lines 31 - 38, The fenced code block in the
README's flow-diagram is unlabeled and triggers MD040; update the opening fence
for that diagram (the triple-backtick block containing the npx flow) to include
the language label "text" so it reads ```text, leaving the block contents
unchanged, to satisfy the markdown linter (look for the flow-diagram block in
packages/cli/README.md).


`npx @cipherstash/cli wizard` is the recommended path after `db install`. It detects your framework (Drizzle, Supabase, Prisma, raw SQL), introspects your database, and integrates encryption directly into your existing schema definitions. If you prefer to write the schema by hand, skip the wizard and edit your encryption client file directly.
`@cipherstash/cli` covers authentication, initialization, EQL install/upgrade/validate/push/migrate, and schema introspection. The wizard ([`@cipherstash/wizard`](https://www.npmjs.com/package/@cipherstash/wizard)) is a separate package that calls back into these cli commands after its AI agent finishes editing your schema files.

---

Expand Down Expand Up @@ -79,7 +80,7 @@ npx @cipherstash/cli init [--supabase] [--drizzle]
| `--supabase` | Use the Supabase-specific setup flow |
| `--drizzle` | Use the Drizzle-specific setup flow |

After `init` completes, the Next Steps output tells you to run `npx @cipherstash/cli db install`, then either `npx @cipherstash/cli wizard` or edit the schema manually.
After `init` completes, the Next Steps output tells you to run `npx @cipherstash/cli db install`, then edit your encryption client file directly.

---

Expand All @@ -91,25 +92,7 @@ Authenticate with CipherStash using a browser-based device code flow.
npx @cipherstash/cli auth login
```

Saves the token to `~/.cipherstash/auth.json`. The wizard checks for this file as a prerequisite before running.

---

### `npx @cipherstash/cli wizard`

AI-powered encryption setup. The wizard reads your codebase, detects your framework, introspects your database schema, and edits your existing schema files to add encrypted column definitions.

```bash
npx @cipherstash/cli wizard
```

Prerequisites:
- Authenticated (`npx @cipherstash/cli auth login` completed).
- `stash.config.ts` present (run `npx @cipherstash/cli db install` first; it will scaffold the config if missing).

Supported integrations: Drizzle ORM, Supabase JS Client, Prisma (experimental), raw SQL / other.

The wizard uses the CipherStash-hosted LLM gateway. No Anthropic API key is required.
Saves the token to `~/.cipherstash/auth.json`. Database-touching commands check for this file before running.

---

Expand Down Expand Up @@ -286,7 +269,7 @@ Build an encryption client file from your database schema using DB introspection
npx @cipherstash/cli schema build [--supabase]
```

The first prompt offers `npx @cipherstash/cli wizard` as the recommended path. If you choose the manual builder, the command connects to your database, lets you select tables and columns to encrypt, asks about searchable indexes, and generates a typed encryption client file.
Connects to your database, lets you select tables and columns to encrypt, asks about searchable indexes, and generates a typed encryption client file.

Reads `databaseUrl` from `stash.config.ts`.

Expand Down Expand Up @@ -422,7 +405,7 @@ const sql = await downloadEqlSql(true) // no operator family variant

## Relationship to `@cipherstash/stack`

`@cipherstash/stack` is the runtime SDK. It stays lean with no heavy dependencies like `pg` and ships in your production bundle. `@cipherstash/cli` is a devDependency: it handles database tooling, AI-guided setup, and schema lifecycle at development time. Think of it like Drizzle Kit — a companion tool that prepares the database while the runtime SDK handles queries.
`@cipherstash/stack` is the runtime SDK. It stays lean with no heavy dependencies like `pg` and ships in your production bundle. `@cipherstash/cli` is a devDependency: it handles database tooling and schema lifecycle at development time. Think of it like Drizzle Kit — a companion tool that prepares the database while the runtime SDK handles queries.

---

Expand Down
3 changes: 1 addition & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@cipherstash/cli",
"version": "0.8.0",
"description": "CipherStash CLI — the one stash command for auth, init, encryption schema, database setup, secrets, and the AI wizard.",
"description": "CipherStash CLI — the one stash command for auth, init, encryption schema, database setup, and secrets.",
"license": "MIT",
"author": "CipherStash <hello@cipherstash.com>",
"files": [
Expand Down Expand Up @@ -40,7 +40,6 @@
"lint": "biome check ."
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.87",
"@cipherstash/auth": "catalog:repo",
"@clack/prompts": "0.10.1",
"dotenv": "16.4.7",
Expand Down
12 changes: 0 additions & 12 deletions packages/cli/src/bin/stash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ Usage: npx @cipherstash/cli <command> [options]
Commands:
init Initialize CipherStash for your project
auth <subcommand> Authenticate with CipherStash
wizard AI-powered encryption setup (reads your codebase)

db install Scaffold stash.config.ts (if missing) and install EQL extensions
db upgrade Upgrade EQL extensions to the latest version
Expand Down Expand Up @@ -98,7 +97,6 @@ Examples:
npx @cipherstash/cli init
npx @cipherstash/cli init --supabase
npx @cipherstash/cli auth login
npx @cipherstash/cli wizard
npx @cipherstash/cli db install
npx @cipherstash/cli db push
npx @cipherstash/cli schema build
Expand Down Expand Up @@ -248,16 +246,6 @@ async function main() {
await authCommand(authArgs, flags)
break
}
case 'wizard': {
// Lazy-load the wizard so the agent SDK is only imported when needed.
const { run } = await import('../commands/wizard/run.js')
await run({
cwd: process.cwd(),
debug: flags.debug,
cliVersion: pkg.version,
})
break
}
case 'db':
await runDbCommand(subcommand, flags, values)
break
Expand Down
52 changes: 52 additions & 0 deletions packages/cli/src/commands/db/client-scaffold.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import * as p from '@clack/prompts'
import type { Integration } from '../init/types.js'
import { generatePlaceholderClient } from '../init/utils.js'
import { detectDrizzle, detectSupabase } from './detect.js'

/**
* Pick a placeholder template using the same signals `db install` already
* detects. Drizzle wins over Supabase when both look present (a Drizzle-on-
* Supabase project is more naturally scaffolded as Drizzle).
*/
function detectIntegration(
cwd: string,
databaseUrl: string | undefined,
): Integration {
if (detectDrizzle(cwd)) return 'drizzle'
if (detectSupabase(databaseUrl)) return 'supabase'
return 'postgresql'
}

/**
* Scaffold an encryption client file at `clientPath` if one doesn't exist.
* No-op when the file is already present. Silent — never prompts.
*
* `init`'s `buildSchemaStep` is the primary path that creates this file
* (and handles the "file already exists" case interactively). This function
* exists as a safety net for users who run `db install` directly without
* `init` first — they still get a working client file rather than failing
* later when the config tries to load a non-existent path.
*/
export function ensureEncryptionClient(
clientPath: string,
cwd: string = process.cwd(),
databaseUrl: string | undefined = process.env.DATABASE_URL,
): void {
const resolved = resolve(cwd, clientPath)
if (existsSync(resolved)) return

const integration = detectIntegration(cwd, databaseUrl)
const contents = generatePlaceholderClient(integration)

const dir = dirname(resolved)
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true })
}
writeFileSync(resolved, contents, 'utf-8')

p.log.success(
`Scaffolded encryption client at ${clientPath} (${integration} template)`,
)
}
10 changes: 8 additions & 2 deletions packages/cli/src/commands/db/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
loadBundledEqlSql,
} from '@/installer/index.js'
import * as p from '@clack/prompts'
import { ensureEncryptionClient } from './client-scaffold.js'
import { ensureStashConfig } from './config-scaffold.js'
import {
type SupabaseProjectInfo,
Expand Down Expand Up @@ -86,6 +87,11 @@ export async function installCommand(options: InstallOptions) {
const config = await loadStashConfig()
s.stop('Configuration loaded.')

// Safety net: if the user ran `db install` without first running `init`,
// scaffold the encryption client file so config.client points somewhere
// real. No-op when the file already exists.
ensureEncryptionClient(config.client, process.cwd(), config.databaseUrl)
Comment on lines +90 to +93
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

--dry-run path still performs file scaffolding.

Line 93 runs ensureEncryptionClient(...) unconditionally, so db install --dry-run can still create/update the client file before dry-run exits. That breaks dry-run semantics.

Proposed fix
-  ensureEncryptionClient(config.client, process.cwd(), config.databaseUrl)
+  if (!options.dryRun) {
+    ensureEncryptionClient(config.client, process.cwd(), config.databaseUrl)
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Safety net: if the user ran `db install` without first running `init`,
// scaffold the encryption client file so config.client points somewhere
// real. No-op when the file already exists.
ensureEncryptionClient(config.client, process.cwd(), config.databaseUrl)
// Safety net: if the user ran `db install` without first running `init`,
// scaffold the encryption client file so config.client points somewhere
// real. No-op when the file already exists.
if (!options.dryRun) {
ensureEncryptionClient(config.client, process.cwd(), config.databaseUrl)
}
🤖 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 90 - 93, The install
command currently calls ensureEncryptionClient(config.client, process.cwd(),
config.databaseUrl) unconditionally which causes file scaffolding even during a
dry run; update the db install handler to detect the dry-run flag (e.g.,
options.dryRun or similar CLI flag) and skip calling ensureEncryptionClient when
dry-run is true (or alternatively pass a dryRun parameter into
ensureEncryptionClient and make it a no-op in that mode) so that no files are
created/modified during --dry-run.


// Auto-detect provider hints when the user didn't explicitly pass flags.
// CIP-2985.
const resolved = resolveProviderOptions(options, config.databaseUrl)
Expand Down Expand Up @@ -258,8 +264,8 @@ function printNextSteps(): void {
[
'Next steps:',
'',
' 1. Wire up encrypt/decrypt with the wizard:',
' npx @cipherstash/cli wizard',
' 1. Wire up encrypt/decrypt with the wizard (AI-guided, automated):',
' npx @cipherstash/wizard',
'',
' 2. Or use the client directly from @cipherstash/stack:',
" import { Encryption } from '@cipherstash/stack'",
Expand Down
2 changes: 0 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,6 @@ import { authenticateStep } from './steps/authenticate.js'
import { buildSchemaStep } from './steps/build-schema.js'
import { installForgeStep } from './steps/install-forge.js'
import { nextStepsStep } from './steps/next-steps.js'
import { selectConnectionStep } from './steps/select-connection.js'
import type { InitProvider, InitState } from './types.js'
import { CancelledError } from './types.js'

Expand All @@ -17,7 +16,6 @@ const PROVIDER_MAP: Record<string, () => InitProvider> = {

const STEPS = [
authenticateStep,
selectConnectionStep,
buildSchemaStep,
installForgeStep,
nextStepsStep,
Expand Down
22 changes: 6 additions & 16 deletions packages/cli/src/commands/init/providers/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,16 @@ export function createBaseProvider(): InitProvider {
return {
name: 'base',
introMessage: 'Setting up CipherStash for your project...',
connectionOptions: [
{ value: 'drizzle', label: 'Drizzle ORM' },
{ value: 'supabase-js', label: 'Supabase JS Client' },
{ value: 'prisma', label: 'Prisma' },
{ value: 'raw-sql', label: 'Raw SQL / pg' },
],
getNextSteps(state: InitState): string[] {
const steps = ['Set up your database: npx @cipherstash/cli db install']

const manualEdit = state.clientFilePath
? `edit ${state.clientFilePath} directly`
: 'edit your encryption schema directly'
steps.push(
`Customize your schema: npx @cipherstash/cli wizard (AI-guided, automated) — or ${manualEdit}`,
)

steps.push('Quickstart: https://cipherstash.com/docs/stack/quickstart')
steps.push('Dashboard: https://dashboard.cipherstash.com/workspaces')

return steps
return [
'Set up your database: npx @cipherstash/cli db install',
`Customize your schema: npx @cipherstash/wizard (AI-guided, automated) — or ${manualEdit}`,
'Quickstart: https://cipherstash.com/docs/stack/quickstart',
'Dashboard: https://dashboard.cipherstash.com/workspaces',
]
},
}
}
8 changes: 1 addition & 7 deletions packages/cli/src/commands/init/providers/drizzle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,14 @@ export function createDrizzleProvider(): InitProvider {
return {
name: 'drizzle',
introMessage: 'Setting up CipherStash for your Drizzle project...',
connectionOptions: [
{ value: 'drizzle', label: 'Drizzle ORM', hint: 'recommended' },
{ value: 'supabase-js', label: 'Supabase JS Client' },
{ value: 'prisma', label: 'Prisma' },
{ value: 'raw-sql', label: 'Raw SQL / pg' },
],
getNextSteps(state: InitState): string[] {
const steps = ['Set up your database: npx @cipherstash/cli db install --drizzle']

const manualEdit = state.clientFilePath
? `edit ${state.clientFilePath} directly`
: 'edit your encryption schema directly'
steps.push(
`Customize your schema: npx @cipherstash/cli wizard (AI-guided, automated) — or ${manualEdit}`,
`Customize your schema: npx @cipherstash/wizard (AI-guided, automated) — or ${manualEdit}`,
)

steps.push(
Expand Down
12 changes: 1 addition & 11 deletions packages/cli/src/commands/init/providers/supabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,6 @@ export function createSupabaseProvider(): InitProvider {
return {
name: 'supabase',
introMessage: 'Setting up CipherStash for your Supabase project...',
connectionOptions: [
{
value: 'supabase-js',
label: 'Supabase JS Client',
hint: 'recommended',
},
{ value: 'drizzle', label: 'Drizzle ORM' },
{ value: 'prisma', label: 'Prisma' },
{ value: 'raw-sql', label: 'Raw SQL / pg' },
],
getNextSteps(state: InitState): string[] {
const steps = [
'Install EQL: npx @cipherstash/cli db install --supabase (prompts for migration vs direct)',
Expand All @@ -24,7 +14,7 @@ export function createSupabaseProvider(): InitProvider {
? `edit ${state.clientFilePath} directly`
: 'edit your encryption schema directly'
steps.push(
`Customize your schema: npx @cipherstash/cli wizard (AI-guided, automated) — or ${manualEdit}`,
`Customize your schema: npx @cipherstash/wizard (AI-guided, automated) — or ${manualEdit}`,
)

steps.push(
Expand Down
Loading
Loading