diff --git a/pkgs/cli/src/commands/install/copy-migrations.ts b/pkgs/cli/src/commands/install/copy-migrations.ts index 04e5e2685..298cbc1d0 100644 --- a/pkgs/cli/src/commands/install/copy-migrations.ts +++ b/pkgs/cli/src/commands/install/copy-migrations.ts @@ -233,7 +233,7 @@ export async function copyMigrations({ // If no files to copy, show message and return false (no changes made) if (filesToCopy.length === 0) { - log.success(`All ${skippedFiles.length} pgflow migrations are already in place`); + log.success('Migrations already up to date'); return false; } @@ -247,32 +247,28 @@ export async function copyMigrations({ file.destination = `${baseTimestamp}_${file.source}`; }); - // Build summary message with explanation - show all migrations - const migrationLines = filesToCopy.map((file) => { - return ` ${chalk.bold(file.source)}`; - }); - - const summaryMsg = [ - `Add to ${chalk.cyan('migrations/')} ${chalk.dim('(database schema for workflow engine)')}:`, - '', - ...migrationLines, - ].join('\n'); + // Show preview and ask for confirmation only when not auto-confirming + if (!autoConfirm) { + const migrationLines = filesToCopy.map((file) => { + return ` ${chalk.bold(file.source)}`; + }); - log.info(summaryMsg); + const summaryMsg = [ + `Add to ${chalk.cyan('migrations/')} ${chalk.dim('(database schema for workflow engine)')}:`, + '', + ...migrationLines, + ].join('\n'); - let shouldContinue = autoConfirm; + log.info(summaryMsg); - if (!autoConfirm) { const confirmResult = await confirm({ message: `Add ${filesToCopy.length} migration${filesToCopy.length !== 1 ? 's' : ''}?`, }); - shouldContinue = confirmResult === true; - } - - if (!shouldContinue) { - log.warn('Migration installation skipped'); - return false; + if (confirmResult !== true) { + log.warn('Migration installation skipped'); + return false; + } } // Install migrations with new filenames @@ -283,12 +279,7 @@ export async function copyMigrations({ fs.copyFileSync(sourcePath1, destinationPath); } - const successMsg = [ - `Installed ${filesToCopy.length} migration${filesToCopy.length !== 1 ? 's' : ''}`, - ` ${chalk.dim('Learn more:')} ${chalk.blue.underline('https://pgflow.dev/concepts/data-model/')}`, - ].join('\n'); - - log.success(successMsg); + log.success(`Installed ${filesToCopy.length} migration${filesToCopy.length !== 1 ? 's' : ''}`); return true; // Return true to indicate migrations were copied } diff --git a/pkgs/cli/src/commands/install/create-edge-function.ts b/pkgs/cli/src/commands/install/create-edge-function.ts index 11ddd6d47..f8d3f2c86 100644 --- a/pkgs/cli/src/commands/install/create-edge-function.ts +++ b/pkgs/cli/src/commands/install/create-edge-function.ts @@ -59,40 +59,28 @@ export async function createEdgeFunction({ // If all files exist, return success if (filesToCreate.length === 0) { - const detailedMsg = [ - 'ControlPlane edge function files are already in place:', - ` ${chalk.bold(relativeIndexPath)}`, - ` ${chalk.bold(relativeDenoJsonPath)}`, - ].join('\n'); - - log.success(detailedMsg); - + log.success('Control Plane already up to date'); return false; } - // Show what will be created with explanation - const summaryMsg = [ - `Create ${chalk.cyan('functions/pgflow/')} ${chalk.dim('(Control Plane for flow registration and compilation)')}:`, - '', - ...filesToCreate.map((file) => ` ${chalk.bold(path.basename(file.relativePath))}`), - ].join('\n'); - - log.info(summaryMsg); + // Show preview and ask for confirmation only when not auto-confirming + if (!autoConfirm) { + const summaryMsg = [ + `Create ${chalk.cyan('functions/pgflow/')} ${chalk.dim('(Control Plane for flow registration and compilation)')}:`, + '', + ...filesToCreate.map((file) => ` ${chalk.bold(path.basename(file.relativePath))}`), + ].join('\n'); - // Get confirmation - let shouldContinue = autoConfirm; + log.info(summaryMsg); - if (!autoConfirm) { const confirmResult = await confirm({ message: `Create functions/pgflow/?`, }); - shouldContinue = confirmResult === true; - } - - if (!shouldContinue) { - log.warn('Control Plane installation skipped'); - return false; + if (confirmResult !== true) { + log.warn('Control Plane installation skipped'); + return false; + } } // Create the directory if it doesn't exist @@ -109,12 +97,7 @@ export async function createEdgeFunction({ fs.writeFileSync(denoJsonPath, DENO_JSON_TEMPLATE(getVersion())); } - const successMsg = [ - `Control Plane installed`, - ` ${chalk.dim('Learn more:')} ${chalk.blue.underline('https://pgflow.dev/concepts/compilation/')}`, - ].join('\n'); - - log.success(successMsg); + log.success('Control Plane installed'); return true; } diff --git a/pkgs/cli/src/commands/install/index.ts b/pkgs/cli/src/commands/install/index.ts index 0b55288d9..be281ed2c 100644 --- a/pkgs/cli/src/commands/install/index.ts +++ b/pkgs/cli/src/commands/install/index.ts @@ -1,5 +1,5 @@ import { type Command } from 'commander'; -import { intro, group, cancel, outro } from '@clack/prompts'; +import { intro, log, confirm, cancel, outro } from '@clack/prompts'; import chalk from 'chalk'; import { copyMigrations } from './copy-migrations.js'; import { updateConfigToml } from './update-config-toml.js'; @@ -16,84 +16,81 @@ export default (program: Command) => { .action(async (options) => { intro('Installing pgflow in your Supabase project'); - // Use the group feature to organize installation steps - const results = await group( - { - // Step 1: Determine Supabase path - supabasePath: () => - supabasePathPrompt({ supabasePath: options.supabasePath }), - - // Step 2: Update config.toml - configUpdate: async ({ results: { supabasePath } }) => { - if (!supabasePath) return false; - - return await updateConfigToml({ - supabasePath, - autoConfirm: options.yes, - }); - }, - - // Step 3: Copy migrations - migrations: async ({ results: { supabasePath } }) => { - if (!supabasePath) return false; - - return await copyMigrations({ - supabasePath, - autoConfirm: options.yes, - }); - }, - - // Step 4: Create ControlPlane edge function - edgeFunction: async ({ results: { supabasePath } }) => { - if (!supabasePath) return false; - - return await createEdgeFunction({ - supabasePath, - autoConfirm: options.yes, - }); - }, - - // Step 5: Update environment variables - envFile: async ({ results: { supabasePath } }) => { - if (!supabasePath) return false; - - return await updateEnvFile({ - supabasePath, - autoConfirm: options.yes, - }); - }, - }, - { - // Handle cancellation - onCancel: () => { - cancel('Installation cancelled'); - process.exit(1); - }, + // Step 1: Get supabase path + const supabasePathResult = await supabasePathPrompt({ + supabasePath: options.supabasePath, + }); + + if (!supabasePathResult || typeof supabasePathResult === 'symbol') { + cancel('Installation cancelled - valid Supabase path is required'); + process.exit(1); + } + + const supabasePath = supabasePathResult; + + // Step 2: Show summary and get single confirmation + const summaryMsg = [ + 'This will:', + '', + ` • Update ${chalk.cyan('supabase/config.toml')} ${chalk.dim('(enable pooler, per_worker runtime)')}`, + ` • Add pgflow migrations to ${chalk.cyan('supabase/migrations/')}`, + ` • Create Control Plane in ${chalk.cyan('supabase/functions/pgflow/')}`, + ` • Configure ${chalk.cyan('supabase/functions/.env')}`, + '', + ` ${chalk.green('✓ Safe to re-run - completed steps will be skipped')}`, + ].join('\n'); + + log.info(summaryMsg); + + let shouldProceed = options.yes; + + if (!options.yes) { + const confirmResult = await confirm({ + message: 'Proceed?', + }); + + if (confirmResult !== true) { + cancel('Installation cancelled'); + process.exit(1); } - ); - // Extract the results from the group operation - const supabasePath = results.supabasePath; - const configUpdate = results.configUpdate; - const migrations = results.migrations; - const edgeFunction = results.edgeFunction; - const envFile = results.envFile; + shouldProceed = true; + } - // Exit if supabasePath is null (validation failed or user cancelled) - if (!supabasePath) { - cancel('Installation cancelled - valid Supabase path is required'); + if (!shouldProceed) { + cancel('Installation cancelled'); process.exit(1); } - // Show completion message + // Step 3: Run all installation steps with autoConfirm + const configUpdate = await updateConfigToml({ + supabasePath, + autoConfirm: true, + }); + + const migrations = await copyMigrations({ + supabasePath, + autoConfirm: true, + }); + + const edgeFunction = await createEdgeFunction({ + supabasePath, + autoConfirm: true, + }); + + const envFile = await updateEnvFile({ + supabasePath, + autoConfirm: true, + }); + + // Step 4: Show completion message const outroMessages: string[] = []; - // Always start with a bolded acknowledgement if (migrations || configUpdate || edgeFunction || envFile) { - outroMessages.push(chalk.bold('Installation complete!')); + outroMessages.push(chalk.green.bold('✓ Installation complete!')); } else { outroMessages.push( - chalk.bold('pgflow is already installed - no changes needed!') + chalk.green.bold('✓ pgflow is already installed - no changes needed!') ); } @@ -121,7 +118,6 @@ export default (program: Command) => { ` ${stepNumber}. Create your first flow: ${chalk.blue.underline('https://pgflow.dev/getting-started/create-first-flow/')}` ); - // Single outro for all paths outro(outroMessages.join('\n')); }); }; diff --git a/pkgs/cli/src/commands/install/update-config-toml.ts b/pkgs/cli/src/commands/install/update-config-toml.ts index 5f9b75aeb..7332fea92 100644 --- a/pkgs/cli/src/commands/install/update-config-toml.ts +++ b/pkgs/cli/src/commands/install/update-config-toml.ts @@ -73,52 +73,49 @@ export async function updateConfigToml({ currentSettings.edgeRuntimePolicy !== 'per_worker'; if (!needsChanges) { - log.success('Supabase configuration is already set up for pgflow'); + log.success('Configuration already up to date'); return false; } - const changes: string[] = []; - - // Connection pooler changes - const poolerChanges: string[] = []; - if (currentSettings.poolerEnabled !== true) { - poolerChanges.push(`enabled = ${currentSettings.poolerEnabled} ${chalk.dim('->')} ${chalk.green('true')}`); - } - if (currentSettings.poolMode !== 'transaction') { - poolerChanges.push(`pool_mode = "${currentSettings.poolMode}" ${chalk.dim('->')} ${chalk.green('"transaction"')}`); - } - if (poolerChanges.length > 0) { - changes.push(` ${chalk.bold('[db.pooler]')} ${chalk.dim('(required for pgflow worker)')}`); - poolerChanges.forEach(change => changes.push(` ${change}`)); - } - - // Edge runtime changes - if (currentSettings.edgeRuntimePolicy !== 'per_worker') { - changes.push(` ${chalk.bold('[edge_runtime]')} ${chalk.dim('(required for long-running tasks)')}`); - changes.push(` policy = "${currentSettings.edgeRuntimePolicy}" ${chalk.dim('->')} ${chalk.green('"per_worker"')}`); - } - - const summaryMsg = [ - `Update ${chalk.cyan('config.toml')}:`, - '', - ...changes, - ].join('\n'); - - log.info(summaryMsg); - - let shouldContinue = autoConfirm; - + // Show preview and ask for confirmation only when not auto-confirming if (!autoConfirm) { + const changes: string[] = []; + + // Connection pooler changes + const poolerChanges: string[] = []; + if (currentSettings.poolerEnabled !== true) { + poolerChanges.push(`enabled = ${currentSettings.poolerEnabled} ${chalk.dim('->')} ${chalk.green('true')}`); + } + if (currentSettings.poolMode !== 'transaction') { + poolerChanges.push(`pool_mode = "${currentSettings.poolMode}" ${chalk.dim('->')} ${chalk.green('"transaction"')}`); + } + if (poolerChanges.length > 0) { + changes.push(` ${chalk.bold('[db.pooler]')} ${chalk.dim('(required for pgflow worker)')}`); + poolerChanges.forEach(change => changes.push(` ${change}`)); + } + + // Edge runtime changes + if (currentSettings.edgeRuntimePolicy !== 'per_worker') { + changes.push(` ${chalk.bold('[edge_runtime]')} ${chalk.dim('(required for long-running tasks)')}`); + changes.push(` policy = "${currentSettings.edgeRuntimePolicy}" ${chalk.dim('->')} ${chalk.green('"per_worker"')}`); + } + + const summaryMsg = [ + `Update ${chalk.cyan('config.toml')}:`, + '', + ...changes, + ].join('\n'); + + log.info(summaryMsg); + const confirmResult = await confirm({ message: `Update config.toml? (backup will be created)`, }); - shouldContinue = confirmResult === true; - } - - if (!shouldContinue) { - log.warn('Configuration update skipped'); - return false; + if (confirmResult !== true) { + log.warn('Configuration update skipped'); + return false; + } } // Update Supabase configuration diff --git a/pkgs/cli/src/commands/install/update-env-file.ts b/pkgs/cli/src/commands/install/update-env-file.ts index 915e479f5..96bb07d14 100644 --- a/pkgs/cli/src/commands/install/update-env-file.ts +++ b/pkgs/cli/src/commands/install/update-env-file.ts @@ -58,35 +58,30 @@ export async function updateEnvFile({ // If no changes needed, return early if (missingVars.length === 0) { - log.success('Environment variables are already set'); + log.success('Environment variables already configured'); return false; } - // Build summary message with explanation - const summaryParts = [ - isNewFile - ? `Create ${chalk.cyan('functions/.env')} ${chalk.dim('(worker configuration)')}:` - : `Update ${chalk.cyan('functions/.env')} ${chalk.dim('(worker configuration)')}:`, - '', - ...missingVars.map(({ key, value }) => ` ${chalk.bold(key)}="${value}"`), - ]; - - log.info(summaryParts.join('\n')); + // Show preview and ask for confirmation only when not auto-confirming + if (!autoConfirm) { + const summaryParts = [ + isNewFile + ? `Create ${chalk.cyan('functions/.env')} ${chalk.dim('(worker configuration)')}:` + : `Update ${chalk.cyan('functions/.env')} ${chalk.dim('(worker configuration)')}:`, + '', + ...missingVars.map(({ key, value }) => ` ${chalk.bold(key)}="${value}"`), + ]; - // Ask for confirmation if not auto-confirming - let shouldContinue = autoConfirm; + log.info(summaryParts.join('\n')); - if (!autoConfirm) { const confirmResult = await confirm({ message: isNewFile ? `Create functions/.env?` : `Update functions/.env?`, }); - shouldContinue = confirmResult === true; - } - - if (!shouldContinue) { - log.warn('Environment variable update skipped'); - return false; + if (confirmResult !== true) { + log.warn('Environment variable update skipped'); + return false; + } } // Update environment variables