Skip to content

Commit 1ceecc8

Browse files
committed
Revamp create-blitzpack UX and flow
1 parent 7e8d441 commit 1ceecc8

8 files changed

Lines changed: 592 additions & 232 deletions

File tree

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,11 @@ The setup wizard will guide you through project creation and next steps. Make su
4545

4646
During setup, choose a profile:
4747

48-
- **Recommended**: Full app features + Docker deployment assets + CD workflow.
49-
- **Platform-First**: Full app features, no deployment assets.
50-
- **Custom**: Pick app features and deployment options independently.
48+
- **Recommended**: Core app with everything included.
49+
- **Platform-Agnostic**: Core app without dockerfiles.
50+
- **Modular**: Core app with features of your choice.
51+
52+
If you choose **Modular**, the wizard opens feature customization before scaffolding.
5153

5254
**What's running after setup:**
5355

create-blitzpack/README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@ pnpm create blitzpack [project-name] [options]
3131

3232
The scaffold wizard supports three profiles:
3333

34-
- **Recommended**: All app features plus Docker deployment assets and CD workflow.
35-
- **Platform-First**: All app features, without deployment assets.
36-
- **Custom**: Pick app features and deployment options independently.
34+
- **Recommended**: Core app with everything included.
35+
- **Platform-Agnostic**: Core app without dockerfiles.
36+
- **Modular**: Core app with features of your choice.
37+
38+
Each profile is a setup path. The **Modular** path opens full feature customization before files are created.
3739

3840
## Requirements
3941

create-blitzpack/package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "create-blitzpack",
3-
"version": "0.1.18",
3+
"version": "0.1.19",
44
"description": "Create a new Blitzpack project - full-stack TypeScript monorepo with Next.js and Fastify",
55
"type": "module",
66
"bin": {
@@ -16,18 +16,17 @@
1616
"clean": "rm -rf dist"
1717
},
1818
"dependencies": {
19+
"@clack/prompts": "^1.0.0",
1920
"chalk": "^5.6.2",
2021
"commander": "^13.1.0",
2122
"fs-extra": "^11.3.3",
2223
"giget": "^2.0.0",
2324
"ora": "^8.2.0",
24-
"prompts": "^2.4.2",
2525
"validate-npm-package-name": "^6.0.2"
2626
},
2727
"devDependencies": {
2828
"@types/fs-extra": "^11.0.4",
2929
"@types/node": "^22.19.3",
30-
"@types/prompts": "^2.4.9",
3130
"@types/validate-npm-package-name": "^4.0.2",
3231
"tsup": "^8.5.1",
3332
"typescript": "^5.9.3"

create-blitzpack/src/checks.ts

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import chalk from 'chalk';
22
import { execSync } from 'child_process';
3+
import ora from 'ora';
4+
5+
import { isDockerInstalled } from './docker.js';
6+
import { isGitInstalled } from './git.js';
37

48
interface CheckResult {
59
passed: boolean;
610
name: string;
7-
message?: string;
11+
required: boolean;
12+
message: string;
813
}
914

1015
function checkNodeVersion(): CheckResult {
@@ -16,67 +21,124 @@ function checkNodeVersion(): CheckResult {
1621
return {
1722
passed: true,
1823
name: 'Node.js',
24+
required: true,
25+
message: nodeVersion,
1926
};
2027
}
2128

2229
return {
2330
passed: false,
2431
name: 'Node.js',
32+
required: true,
2533
message: `Node.js >= 20.0.0 required (found ${nodeVersion})`,
2634
};
2735
} catch {
2836
return {
2937
passed: false,
3038
name: 'Node.js',
39+
required: true,
3140
message: 'Failed to check Node.js version',
3241
};
3342
}
3443
}
3544

3645
function checkPnpmInstalled(): CheckResult {
3746
try {
38-
execSync('pnpm --version', { stdio: 'ignore' });
47+
const version = execSync('pnpm --version', {
48+
encoding: 'utf-8',
49+
stdio: ['ignore', 'pipe', 'ignore'],
50+
}).trim();
3951
return {
4052
passed: true,
4153
name: 'pnpm',
54+
required: true,
55+
message: `v${version}`,
4256
};
4357
} catch {
4458
return {
4559
passed: false,
4660
name: 'pnpm',
61+
required: true,
4762
message: 'pnpm not found. Install: npm install -g pnpm',
4863
};
4964
}
5065
}
5166

67+
function checkGit(): CheckResult {
68+
const installed = isGitInstalled();
69+
return {
70+
passed: installed,
71+
name: 'git',
72+
required: false,
73+
message: installed
74+
? 'available (repository initialization supported)'
75+
: 'not found (git init step will be skipped)',
76+
};
77+
}
78+
79+
function checkDocker(): CheckResult {
80+
const installed = isDockerInstalled();
81+
return {
82+
passed: installed,
83+
name: 'Docker',
84+
required: false,
85+
message: installed
86+
? 'available (automatic local DB setup supported)'
87+
: 'not found (start PostgreSQL separately)',
88+
};
89+
}
90+
5291
export async function runPreflightChecks(): Promise<boolean> {
5392
console.log();
54-
console.log(chalk.bold(' Checking requirements...'));
93+
console.log(chalk.bold(' System readiness'));
94+
console.log(chalk.dim(' Validating required and optional local tooling...'));
5595
console.log();
5696

57-
const checks: CheckResult[] = [checkNodeVersion(), checkPnpmInstalled()];
97+
const checks: CheckResult[] = [
98+
checkNodeVersion(),
99+
checkPnpmInstalled(),
100+
checkGit(),
101+
checkDocker(),
102+
];
58103

59-
let hasErrors = false;
104+
const requiredFailures: CheckResult[] = [];
105+
const optionalWarnings: CheckResult[] = [];
60106

61107
for (const check of checks) {
108+
const spinner = ora(`Checking ${check.name}...`).start();
109+
62110
if (check.passed) {
63-
console.log(chalk.green(' ✔'), check.name);
111+
spinner.succeed(chalk.bold(check.name));
64112
} else {
65-
hasErrors = true;
66-
console.log(chalk.red(' ✖'), check.name);
67-
if (check.message) {
68-
console.log(chalk.dim(` ${check.message}`));
113+
if (check.required) {
114+
requiredFailures.push(check);
115+
spinner.fail(chalk.bold(check.name));
116+
} else {
117+
optionalWarnings.push(check);
118+
spinner.warn(chalk.bold(check.name));
69119
}
120+
console.log(chalk.dim(` ${check.message}`));
70121
}
71122
}
72123

73124
console.log();
74125

75-
if (hasErrors) {
126+
if (optionalWarnings.length > 0) {
127+
console.log(chalk.yellow(' Optional tools missing:'));
128+
for (const warning of optionalWarnings) {
129+
console.log(chalk.dim(` • ${warning.name}: ${warning.message}`));
130+
}
131+
console.log();
132+
}
133+
134+
if (requiredFailures.length > 0) {
76135
console.log(
77136
chalk.red(' ✖'),
78-
'Requirements not met. Please fix the errors above.'
137+
'Required dependencies are missing. Fix the items below and try again:'
79138
);
139+
for (const failure of requiredFailures) {
140+
console.log(chalk.dim(` • ${failure.name}: ${failure.message}`));
141+
}
80142
console.log();
81143
return false;
82144
}

create-blitzpack/src/commands/create.ts

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1+
import { confirm, isCancel } from '@clack/prompts';
12
import chalk from 'chalk';
23
import { spawn } from 'child_process';
34
import fs from 'fs-extra';
4-
import ora from 'ora';
5+
import ora, { type Ora } from 'ora';
56
import path from 'path';
6-
import prompts from 'prompts';
77

88
import { runPreflightChecks } from '../checks.js';
99
import type { FeatureOptions } from '../constants.js';
@@ -47,6 +47,42 @@ interface CreateFlags {
4747
dryRun?: boolean;
4848
}
4949

50+
function renderProgressBar(current: number, total: number): string {
51+
const width = 28;
52+
const clampedTotal = Math.max(total, 1);
53+
const ratio = Math.min(Math.max(current / clampedTotal, 0), 1);
54+
const filled = Math.round(width * ratio);
55+
const empty = width - filled;
56+
const filledBar = chalk.cyan('█'.repeat(filled));
57+
const emptyBar = chalk.dim('░'.repeat(empty));
58+
const percentage = `${Math.round(ratio * 100)}`.padStart(3, ' ');
59+
return `[${filledBar}${emptyBar}] ${percentage}%`;
60+
}
61+
62+
function renderStepTrack(step: number, total: number): string {
63+
const segments: string[] = [];
64+
65+
for (let index = 1; index <= total; index++) {
66+
if (index < step) {
67+
segments.push(chalk.green('●'));
68+
} else if (index === step) {
69+
segments.push(chalk.cyan('◆'));
70+
} else {
71+
segments.push(chalk.dim('◇'));
72+
}
73+
}
74+
75+
return segments.join(chalk.dim('──'));
76+
}
77+
78+
function printStepHeader(step: number, total: number, title: string): void {
79+
const completed = step - 1;
80+
console.log();
81+
console.log(chalk.cyan(` Step ${step}/${total}`), chalk.bold(title));
82+
console.log(` ${renderProgressBar(completed, total)}`);
83+
console.log(` ${renderStepTrack(step, total)}`);
84+
}
85+
5086
function printDryRun(options: {
5187
projectName: string;
5288
projectSlug: string;
@@ -139,14 +175,12 @@ export async function create(
139175
const files = await fs.readdir(targetDir);
140176
if (files.length > 0) {
141177
if (options.useCurrentDir) {
142-
const { confirm } = await prompts({
143-
type: 'confirm',
144-
name: 'confirm',
145-
message: `Current directory is not empty. Continue?`,
146-
initial: false,
178+
const shouldContinue = await confirm({
179+
message: 'Current directory is not empty. Continue?',
180+
initialValue: false,
147181
});
148182

149-
if (!confirm) {
183+
if (isCancel(shouldContinue) || !shouldContinue) {
150184
return;
151185
}
152186
} else {
@@ -156,13 +190,24 @@ export async function create(
156190
}
157191
}
158192

159-
const spinner = ora();
193+
const shouldRunSetup = await promptAutomaticSetup();
194+
const totalSteps =
195+
2 +
196+
(options.skipGit ? 0 : 1) +
197+
(options.skipInstall ? 0 : 1) +
198+
(shouldRunSetup ? 1 : 0);
199+
let currentStep = 0;
200+
let spinner: Ora | undefined;
160201

161202
try {
162-
spinner.start('Downloading template from GitHub...');
203+
currentStep += 1;
204+
printStepHeader(currentStep, totalSteps, 'Scaffold template');
205+
spinner = ora('Downloading template from GitHub...').start();
163206
await downloadAndPrepareTemplate(targetDir, spinner, options.features);
164207

165-
spinner.start('Configuring project...');
208+
currentStep += 1;
209+
printStepHeader(currentStep, totalSteps, 'Configure project files');
210+
spinner.start('Applying template transforms...');
166211
await transformFiles(
167212
targetDir,
168213
{
@@ -176,16 +221,24 @@ export async function create(
176221
spinner.succeed('Configured project');
177222

178223
if (!options.skipGit && isGitInstalled()) {
224+
currentStep += 1;
225+
printStepHeader(currentStep, totalSteps, 'Initialize git repository');
179226
spinner.start('Initializing git repository...');
180227
const gitSuccess = initGit(targetDir);
181228
if (gitSuccess) {
182229
spinner.succeed('Initialized git repository');
183230
} else {
184231
spinner.warn('Failed to initialize git repository');
185232
}
233+
} else if (!options.skipGit) {
234+
currentStep += 1;
235+
printStepHeader(currentStep, totalSteps, 'Initialize git repository');
236+
spinner.warn('Skipped git initialization (git not installed)');
186237
}
187238

188239
if (!options.skipInstall) {
240+
currentStep += 1;
241+
printStepHeader(currentStep, totalSteps, 'Install dependencies');
189242
spinner.start('Installing dependencies...');
190243
const success = await runInstall(targetDir);
191244
if (success) {
@@ -197,12 +250,11 @@ export async function create(
197250
}
198251
}
199252

200-
// Prompt for automatic setup
201253
let ranAutomaticSetup = false;
202-
const shouldRunSetup = await promptAutomaticSetup();
203254

204255
if (shouldRunSetup) {
205-
console.log();
256+
currentStep += 1;
257+
printStepHeader(currentStep, totalSteps, 'Run local database setup');
206258
spinner.start('Starting PostgreSQL database...');
207259
const dockerSuccess = runDockerCompose(targetDir);
208260
if (dockerSuccess) {
@@ -231,7 +283,7 @@ export async function create(
231283
ranAutomaticSetup
232284
);
233285
} catch (error) {
234-
spinner.fail();
286+
spinner?.fail();
235287
printError(
236288
error instanceof Error ? error.message : 'Unknown error occurred'
237289
);

0 commit comments

Comments
 (0)