Skip to content
Closed
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
43 changes: 17 additions & 26 deletions pkgs/cli/src/commands/install/copy-migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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
Expand All @@ -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
}
45 changes: 14 additions & 31 deletions pkgs/cli/src/commands/install/create-edge-function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
134 changes: 65 additions & 69 deletions pkgs/cli/src/commands/install/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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!')
);
}

Expand Down Expand Up @@ -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'));
});
};
73 changes: 35 additions & 38 deletions pkgs/cli/src/commands/install/update-config-toml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading