Skip to content

Commit 85ef323

Browse files
committed
refactor: simplify CLI install command with cleaner logs and single confirmation (#440)
# Streamline CLI installation experience with simplified UI This PR improves the installation command UX by: 1. Consolidating the installation process into a single confirmation step instead of multiple prompts 2. Simplifying success and status messages to be more concise 3. Restructuring the installation flow to show a clear preview of all changes upfront 4. Removing redundant documentation links from individual steps 5. Using consistent formatting and styling for status messages 6. Adding clearer visual indicators for completed steps The installation now presents all planned changes at once with a single confirmation prompt when not using the `--yes` flag, making the process more efficient while still providing transparency about what will be modified.
1 parent 45df433 commit 85ef323

5 files changed

Lines changed: 146 additions & 184 deletions

File tree

pkgs/cli/src/commands/install/copy-migrations.ts

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ export async function copyMigrations({
233233

234234
// If no files to copy, show message and return false (no changes made)
235235
if (filesToCopy.length === 0) {
236-
log.success(`All ${skippedFiles.length} pgflow migrations are already in place`);
236+
log.success('Migrations already up to date');
237237
return false;
238238
}
239239

@@ -247,32 +247,28 @@ export async function copyMigrations({
247247
file.destination = `${baseTimestamp}_${file.source}`;
248248
});
249249

250-
// Build summary message with explanation - show all migrations
251-
const migrationLines = filesToCopy.map((file) => {
252-
return ` ${chalk.bold(file.source)}`;
253-
});
254-
255-
const summaryMsg = [
256-
`Add to ${chalk.cyan('migrations/')} ${chalk.dim('(database schema for workflow engine)')}:`,
257-
'',
258-
...migrationLines,
259-
].join('\n');
250+
// Show preview and ask for confirmation only when not auto-confirming
251+
if (!autoConfirm) {
252+
const migrationLines = filesToCopy.map((file) => {
253+
return ` ${chalk.bold(file.source)}`;
254+
});
260255

261-
log.info(summaryMsg);
256+
const summaryMsg = [
257+
`Add to ${chalk.cyan('migrations/')} ${chalk.dim('(database schema for workflow engine)')}:`,
258+
'',
259+
...migrationLines,
260+
].join('\n');
262261

263-
let shouldContinue = autoConfirm;
262+
log.info(summaryMsg);
264263

265-
if (!autoConfirm) {
266264
const confirmResult = await confirm({
267265
message: `Add ${filesToCopy.length} migration${filesToCopy.length !== 1 ? 's' : ''}?`,
268266
});
269267

270-
shouldContinue = confirmResult === true;
271-
}
272-
273-
if (!shouldContinue) {
274-
log.warn('Migration installation skipped');
275-
return false;
268+
if (confirmResult !== true) {
269+
log.warn('Migration installation skipped');
270+
return false;
271+
}
276272
}
277273

278274
// Install migrations with new filenames
@@ -283,12 +279,7 @@ export async function copyMigrations({
283279
fs.copyFileSync(sourcePath1, destinationPath);
284280
}
285281

286-
const successMsg = [
287-
`Installed ${filesToCopy.length} migration${filesToCopy.length !== 1 ? 's' : ''}`,
288-
` ${chalk.dim('Learn more:')} ${chalk.blue.underline('https://pgflow.dev/concepts/data-model/')}`,
289-
].join('\n');
290-
291-
log.success(successMsg);
282+
log.success(`Installed ${filesToCopy.length} migration${filesToCopy.length !== 1 ? 's' : ''}`);
292283

293284
return true; // Return true to indicate migrations were copied
294285
}

pkgs/cli/src/commands/install/create-edge-function.ts

Lines changed: 14 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -59,40 +59,28 @@ export async function createEdgeFunction({
5959

6060
// If all files exist, return success
6161
if (filesToCreate.length === 0) {
62-
const detailedMsg = [
63-
'ControlPlane edge function files are already in place:',
64-
` ${chalk.bold(relativeIndexPath)}`,
65-
` ${chalk.bold(relativeDenoJsonPath)}`,
66-
].join('\n');
67-
68-
log.success(detailedMsg);
69-
62+
log.success('Control Plane already up to date');
7063
return false;
7164
}
7265

73-
// Show what will be created with explanation
74-
const summaryMsg = [
75-
`Create ${chalk.cyan('functions/pgflow/')} ${chalk.dim('(Control Plane for flow registration and compilation)')}:`,
76-
'',
77-
...filesToCreate.map((file) => ` ${chalk.bold(path.basename(file.relativePath))}`),
78-
].join('\n');
79-
80-
log.info(summaryMsg);
66+
// Show preview and ask for confirmation only when not auto-confirming
67+
if (!autoConfirm) {
68+
const summaryMsg = [
69+
`Create ${chalk.cyan('functions/pgflow/')} ${chalk.dim('(Control Plane for flow registration and compilation)')}:`,
70+
'',
71+
...filesToCreate.map((file) => ` ${chalk.bold(path.basename(file.relativePath))}`),
72+
].join('\n');
8173

82-
// Get confirmation
83-
let shouldContinue = autoConfirm;
74+
log.info(summaryMsg);
8475

85-
if (!autoConfirm) {
8676
const confirmResult = await confirm({
8777
message: `Create functions/pgflow/?`,
8878
});
8979

90-
shouldContinue = confirmResult === true;
91-
}
92-
93-
if (!shouldContinue) {
94-
log.warn('Control Plane installation skipped');
95-
return false;
80+
if (confirmResult !== true) {
81+
log.warn('Control Plane installation skipped');
82+
return false;
83+
}
9684
}
9785

9886
// Create the directory if it doesn't exist
@@ -109,12 +97,7 @@ export async function createEdgeFunction({
10997
fs.writeFileSync(denoJsonPath, DENO_JSON_TEMPLATE(getVersion()));
11098
}
11199

112-
const successMsg = [
113-
`Control Plane installed`,
114-
` ${chalk.dim('Learn more:')} ${chalk.blue.underline('https://pgflow.dev/concepts/compilation/')}`,
115-
].join('\n');
116-
117-
log.success(successMsg);
100+
log.success('Control Plane installed');
118101

119102
return true;
120103
}
Lines changed: 65 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { type Command } from 'commander';
2-
import { intro, group, cancel, outro } from '@clack/prompts';
2+
import { intro, log, confirm, cancel, outro } from '@clack/prompts';
33
import chalk from 'chalk';
44
import { copyMigrations } from './copy-migrations.js';
55
import { updateConfigToml } from './update-config-toml.js';
@@ -16,84 +16,81 @@ export default (program: Command) => {
1616
.action(async (options) => {
1717
intro('Installing pgflow in your Supabase project');
1818

19-
// Use the group feature to organize installation steps
20-
const results = await group(
21-
{
22-
// Step 1: Determine Supabase path
23-
supabasePath: () =>
24-
supabasePathPrompt({ supabasePath: options.supabasePath }),
25-
26-
// Step 2: Update config.toml
27-
configUpdate: async ({ results: { supabasePath } }) => {
28-
if (!supabasePath) return false;
29-
30-
return await updateConfigToml({
31-
supabasePath,
32-
autoConfirm: options.yes,
33-
});
34-
},
35-
36-
// Step 3: Copy migrations
37-
migrations: async ({ results: { supabasePath } }) => {
38-
if (!supabasePath) return false;
39-
40-
return await copyMigrations({
41-
supabasePath,
42-
autoConfirm: options.yes,
43-
});
44-
},
45-
46-
// Step 4: Create ControlPlane edge function
47-
edgeFunction: async ({ results: { supabasePath } }) => {
48-
if (!supabasePath) return false;
49-
50-
return await createEdgeFunction({
51-
supabasePath,
52-
autoConfirm: options.yes,
53-
});
54-
},
55-
56-
// Step 5: Update environment variables
57-
envFile: async ({ results: { supabasePath } }) => {
58-
if (!supabasePath) return false;
59-
60-
return await updateEnvFile({
61-
supabasePath,
62-
autoConfirm: options.yes,
63-
});
64-
},
65-
},
66-
{
67-
// Handle cancellation
68-
onCancel: () => {
69-
cancel('Installation cancelled');
70-
process.exit(1);
71-
},
19+
// Step 1: Get supabase path
20+
const supabasePathResult = await supabasePathPrompt({
21+
supabasePath: options.supabasePath,
22+
});
23+
24+
if (!supabasePathResult || typeof supabasePathResult === 'symbol') {
25+
cancel('Installation cancelled - valid Supabase path is required');
26+
process.exit(1);
27+
}
28+
29+
const supabasePath = supabasePathResult;
30+
31+
// Step 2: Show summary and get single confirmation
32+
const summaryMsg = [
33+
'This will:',
34+
'',
35+
` • Update ${chalk.cyan('supabase/config.toml')} ${chalk.dim('(enable pooler, per_worker runtime)')}`,
36+
` • Add pgflow migrations to ${chalk.cyan('supabase/migrations/')}`,
37+
` • Create Control Plane in ${chalk.cyan('supabase/functions/pgflow/')}`,
38+
` • Configure ${chalk.cyan('supabase/functions/.env')}`,
39+
'',
40+
` ${chalk.green('✓ Safe to re-run - completed steps will be skipped')}`,
41+
].join('\n');
42+
43+
log.info(summaryMsg);
44+
45+
let shouldProceed = options.yes;
46+
47+
if (!options.yes) {
48+
const confirmResult = await confirm({
49+
message: 'Proceed?',
50+
});
51+
52+
if (confirmResult !== true) {
53+
cancel('Installation cancelled');
54+
process.exit(1);
7255
}
73-
);
7456

75-
// Extract the results from the group operation
76-
const supabasePath = results.supabasePath;
77-
const configUpdate = results.configUpdate;
78-
const migrations = results.migrations;
79-
const edgeFunction = results.edgeFunction;
80-
const envFile = results.envFile;
57+
shouldProceed = true;
58+
}
8159

82-
// Exit if supabasePath is null (validation failed or user cancelled)
83-
if (!supabasePath) {
84-
cancel('Installation cancelled - valid Supabase path is required');
60+
if (!shouldProceed) {
61+
cancel('Installation cancelled');
8562
process.exit(1);
8663
}
8764

88-
// Show completion message
65+
// Step 3: Run all installation steps with autoConfirm
66+
const configUpdate = await updateConfigToml({
67+
supabasePath,
68+
autoConfirm: true,
69+
});
70+
71+
const migrations = await copyMigrations({
72+
supabasePath,
73+
autoConfirm: true,
74+
});
75+
76+
const edgeFunction = await createEdgeFunction({
77+
supabasePath,
78+
autoConfirm: true,
79+
});
80+
81+
const envFile = await updateEnvFile({
82+
supabasePath,
83+
autoConfirm: true,
84+
});
85+
86+
// Step 4: Show completion message
8987
const outroMessages: string[] = [];
9088

91-
// Always start with a bolded acknowledgement
9289
if (migrations || configUpdate || edgeFunction || envFile) {
93-
outroMessages.push(chalk.bold('Installation complete!'));
90+
outroMessages.push(chalk.green.bold('Installation complete!'));
9491
} else {
9592
outroMessages.push(
96-
chalk.bold('pgflow is already installed - no changes needed!')
93+
chalk.green.bold('pgflow is already installed - no changes needed!')
9794
);
9895
}
9996

@@ -121,7 +118,6 @@ export default (program: Command) => {
121118
` ${stepNumber}. Create your first flow: ${chalk.blue.underline('https://pgflow.dev/getting-started/create-first-flow/')}`
122119
);
123120

124-
// Single outro for all paths
125121
outro(outroMessages.join('\n'));
126122
});
127123
};

pkgs/cli/src/commands/install/update-config-toml.ts

Lines changed: 35 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -73,52 +73,49 @@ export async function updateConfigToml({
7373
currentSettings.edgeRuntimePolicy !== 'per_worker';
7474

7575
if (!needsChanges) {
76-
log.success('Supabase configuration is already set up for pgflow');
76+
log.success('Configuration already up to date');
7777
return false;
7878
}
7979

80-
const changes: string[] = [];
81-
82-
// Connection pooler changes
83-
const poolerChanges: string[] = [];
84-
if (currentSettings.poolerEnabled !== true) {
85-
poolerChanges.push(`enabled = ${currentSettings.poolerEnabled} ${chalk.dim('->')} ${chalk.green('true')}`);
86-
}
87-
if (currentSettings.poolMode !== 'transaction') {
88-
poolerChanges.push(`pool_mode = "${currentSettings.poolMode}" ${chalk.dim('->')} ${chalk.green('"transaction"')}`);
89-
}
90-
if (poolerChanges.length > 0) {
91-
changes.push(` ${chalk.bold('[db.pooler]')} ${chalk.dim('(required for pgflow worker)')}`);
92-
poolerChanges.forEach(change => changes.push(` ${change}`));
93-
}
94-
95-
// Edge runtime changes
96-
if (currentSettings.edgeRuntimePolicy !== 'per_worker') {
97-
changes.push(` ${chalk.bold('[edge_runtime]')} ${chalk.dim('(required for long-running tasks)')}`);
98-
changes.push(` policy = "${currentSettings.edgeRuntimePolicy}" ${chalk.dim('->')} ${chalk.green('"per_worker"')}`);
99-
}
100-
101-
const summaryMsg = [
102-
`Update ${chalk.cyan('config.toml')}:`,
103-
'',
104-
...changes,
105-
].join('\n');
106-
107-
log.info(summaryMsg);
108-
109-
let shouldContinue = autoConfirm;
110-
80+
// Show preview and ask for confirmation only when not auto-confirming
11181
if (!autoConfirm) {
82+
const changes: string[] = [];
83+
84+
// Connection pooler changes
85+
const poolerChanges: string[] = [];
86+
if (currentSettings.poolerEnabled !== true) {
87+
poolerChanges.push(`enabled = ${currentSettings.poolerEnabled} ${chalk.dim('->')} ${chalk.green('true')}`);
88+
}
89+
if (currentSettings.poolMode !== 'transaction') {
90+
poolerChanges.push(`pool_mode = "${currentSettings.poolMode}" ${chalk.dim('->')} ${chalk.green('"transaction"')}`);
91+
}
92+
if (poolerChanges.length > 0) {
93+
changes.push(` ${chalk.bold('[db.pooler]')} ${chalk.dim('(required for pgflow worker)')}`);
94+
poolerChanges.forEach(change => changes.push(` ${change}`));
95+
}
96+
97+
// Edge runtime changes
98+
if (currentSettings.edgeRuntimePolicy !== 'per_worker') {
99+
changes.push(` ${chalk.bold('[edge_runtime]')} ${chalk.dim('(required for long-running tasks)')}`);
100+
changes.push(` policy = "${currentSettings.edgeRuntimePolicy}" ${chalk.dim('->')} ${chalk.green('"per_worker"')}`);
101+
}
102+
103+
const summaryMsg = [
104+
`Update ${chalk.cyan('config.toml')}:`,
105+
'',
106+
...changes,
107+
].join('\n');
108+
109+
log.info(summaryMsg);
110+
112111
const confirmResult = await confirm({
113112
message: `Update config.toml? (backup will be created)`,
114113
});
115114

116-
shouldContinue = confirmResult === true;
117-
}
118-
119-
if (!shouldContinue) {
120-
log.warn('Configuration update skipped');
121-
return false;
115+
if (confirmResult !== true) {
116+
log.warn('Configuration update skipped');
117+
return false;
118+
}
122119
}
123120

124121
// Update Supabase configuration

0 commit comments

Comments
 (0)