From 5245cd7789a22a479338439e29370e52cdf7affc Mon Sep 17 00:00:00 2001 From: CJ Brewer Date: Tue, 17 Mar 2026 14:31:30 -0600 Subject: [PATCH] chore: cli cleanup --- .changeset/nice-pugs-retire.md | 6 ++ packages/stack-forge/src/commands/init.ts | 31 +++--- .../bin/commands/init/steps/install-forge.ts | 96 +++++++++++-------- packages/stack/src/bin/commands/init/types.ts | 1 + packages/stack/src/bin/commands/init/utils.ts | 17 ++++ 5 files changed, 93 insertions(+), 58 deletions(-) create mode 100644 .changeset/nice-pugs-retire.md diff --git a/.changeset/nice-pugs-retire.md b/.changeset/nice-pugs-retire.md new file mode 100644 index 00000000..065bd381 --- /dev/null +++ b/.changeset/nice-pugs-retire.md @@ -0,0 +1,6 @@ +--- +"@cipherstash/stack-forge": minor +"@cipherstash/stack": minor +--- + +Improved CLI setup and initialization commands. diff --git a/packages/stack-forge/src/commands/init.ts b/packages/stack-forge/src/commands/init.ts index f8c7bdd5..1da21003 100644 --- a/packages/stack-forge/src/commands/init.ts +++ b/packages/stack-forge/src/commands/init.ts @@ -110,30 +110,21 @@ export async function setupCommand(options: SetupOptions = {}) { process.exit(0) } - // 3. Collect database URL - const databaseUrl = await p.text({ - message: 'What is your database URL?', - placeholder: 'postgresql://user:password@localhost:5432/mydb', - defaultValue: process.env.DATABASE_URL, - initialValue: process.env.DATABASE_URL, - validate(value) { - if (!value || value.trim().length === 0) { - return 'Database URL is required.' - } - }, - }) - - if (p.isCancel(databaseUrl)) { - p.cancel('Setup cancelled.') - process.exit(0) - } - - // 4. Generate stash.config.ts + // 3. Generate stash.config.ts const configContent = generateConfig(clientPath) writeFileSync(configPath, configContent, 'utf-8') p.log.success(`Created ${CONFIG_FILENAME}`) - // 5. Install EQL extensions + // 4. Install EQL extensions (only if DATABASE_URL is available) + if (!process.env.DATABASE_URL) { + p.note( + 'Set DATABASE_URL in your environment, then run:\n npx stash-forge install', + 'DATABASE_URL not set', + ) + p.outro('CipherStash Forge setup complete!') + return + } + const shouldInstall = await p.confirm({ message: 'Install EQL extensions in your database now?', initialValue: true, diff --git a/packages/stack/src/bin/commands/init/steps/install-forge.ts b/packages/stack/src/bin/commands/init/steps/install-forge.ts index 05646807..7197d34d 100644 --- a/packages/stack/src/bin/commands/init/steps/install-forge.ts +++ b/packages/stack/src/bin/commands/init/steps/install-forge.ts @@ -6,50 +6,70 @@ import { detectPackageManager, devInstallCommand, isPackageInstalled, + prodInstallCommand, } from '../utils.js' +const STACK_PACKAGE = '@cipherstash/stack' const FORGE_PACKAGE = '@cipherstash/stack-forge' +/** + * Installs a package if not already present. + * Returns true if installed (or already was), false if skipped or failed. + */ +async function installIfNeeded( + packageName: string, + buildCommand: (pm: ReturnType, pkg: string) => string, + depLabel: string, +): Promise { + if (isPackageInstalled(packageName)) { + p.log.success(`${packageName} is already installed.`) + return true + } + + const pm = detectPackageManager() + const cmd = buildCommand(pm, packageName) + + const install = await p.confirm({ + message: `Install ${packageName} as a ${depLabel} dependency? (${cmd})`, + }) + + if (p.isCancel(install)) throw new CancelledError() + + if (!install) { + p.log.info(`Skipping ${packageName} installation.`) + p.note( + `You can install it manually later:\n ${cmd}`, + 'Manual Installation', + ) + return false + } + + const s = p.spinner() + s.start(`Installing ${packageName}...`) + + try { + execSync(cmd, { cwd: process.cwd(), stdio: 'pipe' }) + s.stop(`${packageName} installed successfully`) + return true + } catch (err) { + const message = err instanceof Error ? err.message : String(err) + s.stop(`${packageName} installation failed`) + p.log.error(message) + p.note(`You can install it manually:\n ${cmd}`, 'Manual Installation') + return false + } +} + export const installForgeStep: InitStep = { id: 'install-forge', - name: 'Install stack-forge', + name: 'Install stack dependencies', async run(state: InitState, _provider: InitProvider): Promise { - if (isPackageInstalled(FORGE_PACKAGE)) { - p.log.success(`${FORGE_PACKAGE} is already installed.`) - return { ...state, forgeInstalled: true } - } - - const pm = detectPackageManager() - const cmd = devInstallCommand(pm, FORGE_PACKAGE) - - const install = await p.confirm({ - message: `Install ${FORGE_PACKAGE} as a dev dependency? (${cmd})`, - }) - - if (p.isCancel(install)) throw new CancelledError() - - if (!install) { - p.log.info(`Skipping ${FORGE_PACKAGE} installation.`) - p.note( - `You can install it manually later:\n ${cmd}`, - 'Manual Installation', - ) - return { ...state, forgeInstalled: false } - } - - const s = p.spinner() - s.start(`Installing ${FORGE_PACKAGE}...`) - - try { - execSync(cmd, { cwd: process.cwd(), stdio: 'pipe' }) - s.stop(`${FORGE_PACKAGE} installed successfully`) - return { ...state, forgeInstalled: true } - } catch (err) { - const message = err instanceof Error ? err.message : String(err) - s.stop(`${FORGE_PACKAGE} installation failed`) - p.log.error(message) - p.note(`You can install it manually:\n ${cmd}`, 'Manual Installation') - return { ...state, forgeInstalled: false } - } + // Install @cipherstash/stack as a production dependency + const stackInstalled = await installIfNeeded(STACK_PACKAGE, prodInstallCommand, 'production') + + // Install @cipherstash/stack-forge as a dev dependency + const forgeInstalled = await installIfNeeded(FORGE_PACKAGE, devInstallCommand, 'dev') + + return { ...state, forgeInstalled, stackInstalled } }, } diff --git a/packages/stack/src/bin/commands/init/types.ts b/packages/stack/src/bin/commands/init/types.ts index ff9b00d5..eba0d478 100644 --- a/packages/stack/src/bin/commands/init/types.ts +++ b/packages/stack/src/bin/commands/init/types.ts @@ -21,6 +21,7 @@ export interface InitState { connectionMethod?: ConnectionMethod clientFilePath?: string schemaGenerated?: boolean + stackInstalled?: boolean forgeInstalled?: boolean } diff --git a/packages/stack/src/bin/commands/init/utils.ts b/packages/stack/src/bin/commands/init/utils.ts index edcc885b..dd69500b 100644 --- a/packages/stack/src/bin/commands/init/utils.ts +++ b/packages/stack/src/bin/commands/init/utils.ts @@ -24,6 +24,23 @@ export function detectPackageManager(): 'npm' | 'pnpm' | 'yarn' | 'bun' { return 'npm' } +/** Returns the install command for adding a production dependency with the given package manager. */ +export function prodInstallCommand( + pm: ReturnType, + packageName: string, +): string { + switch (pm) { + case 'bun': + return `bun add ${packageName}` + case 'pnpm': + return `pnpm add ${packageName}` + case 'yarn': + return `yarn add ${packageName}` + case 'npm': + return `npm install ${packageName}` + } +} + /** Returns the install command for adding a dev dependency with the given package manager. */ export function devInstallCommand( pm: ReturnType,