create-blitzpack 0.1.18 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +5 -3
  2. package/dist/index.js +501 -243
  3. package/package.json +2 -3
package/README.md CHANGED
@@ -31,9 +31,11 @@ pnpm create blitzpack [project-name] [options]
31
31
 
32
32
  The scaffold wizard supports three profiles:
33
33
 
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.
37
39
 
38
40
  ## Requirements
39
41
 
package/dist/index.js CHANGED
@@ -10,13 +10,82 @@ import { fileURLToPath } from "url";
10
10
  import chalk4 from "chalk";
11
11
  import { spawn } from "child_process";
12
12
  import fs3 from "fs-extra";
13
- import ora from "ora";
13
+ import { confirm as confirm2, isCancel as isCancel2 } from "@clack/prompts";
14
+ import ora2 from "ora";
14
15
  import path4 from "path";
15
- import prompts2 from "prompts";
16
16
 
17
17
  // src/checks.ts
18
18
  import chalk from "chalk";
19
+ import { execSync as execSync3 } from "child_process";
20
+ import ora from "ora";
21
+
22
+ // src/docker.ts
19
23
  import { execSync } from "child_process";
24
+ function isDockerInstalled() {
25
+ try {
26
+ execSync("docker --version", { stdio: "ignore" });
27
+ return true;
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+ function isDockerRunning() {
33
+ try {
34
+ execSync("docker info", { stdio: "ignore", timeout: 3e3 });
35
+ return true;
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+ function runDockerCompose(targetDir) {
41
+ try {
42
+ execSync("docker compose up -d", {
43
+ cwd: targetDir,
44
+ stdio: "inherit"
45
+ });
46
+ return true;
47
+ } catch {
48
+ return false;
49
+ }
50
+ }
51
+ function runDatabaseMigrations(targetDir) {
52
+ try {
53
+ const isWindows = process.platform === "win32";
54
+ execSync(isWindows ? "pnpm.cmd db:migrate" : "pnpm db:migrate", {
55
+ cwd: targetDir,
56
+ stdio: "inherit"
57
+ });
58
+ return true;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ // src/git.ts
65
+ import { execSync as execSync2 } from "child_process";
66
+ function isGitInstalled() {
67
+ try {
68
+ execSync2("git --version", { stdio: "ignore" });
69
+ return true;
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+ function initGit(targetDir) {
75
+ try {
76
+ execSync2("git init", { cwd: targetDir, stdio: "ignore" });
77
+ execSync2("git add -A", { cwd: targetDir, stdio: "ignore" });
78
+ execSync2('git commit -m "Initial commit from create-blitzpack"', {
79
+ cwd: targetDir,
80
+ stdio: "ignore"
81
+ });
82
+ return true;
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
87
+
88
+ // src/checks.ts
20
89
  function checkNodeVersion() {
21
90
  try {
22
91
  const nodeVersion = process.version;
@@ -24,127 +93,126 @@ function checkNodeVersion() {
24
93
  if (majorVersion >= 20) {
25
94
  return {
26
95
  passed: true,
27
- name: "Node.js"
96
+ name: "Node.js",
97
+ required: true,
98
+ message: nodeVersion
28
99
  };
29
100
  }
30
101
  return {
31
102
  passed: false,
32
103
  name: "Node.js",
104
+ required: true,
33
105
  message: `Node.js >= 20.0.0 required (found ${nodeVersion})`
34
106
  };
35
107
  } catch {
36
108
  return {
37
109
  passed: false,
38
110
  name: "Node.js",
111
+ required: true,
39
112
  message: "Failed to check Node.js version"
40
113
  };
41
114
  }
42
115
  }
43
116
  function checkPnpmInstalled() {
44
117
  try {
45
- execSync("pnpm --version", { stdio: "ignore" });
118
+ const version = execSync3("pnpm --version", {
119
+ encoding: "utf-8",
120
+ stdio: ["ignore", "pipe", "ignore"]
121
+ }).trim();
46
122
  return {
47
123
  passed: true,
48
- name: "pnpm"
124
+ name: "pnpm",
125
+ required: true,
126
+ message: `v${version}`
49
127
  };
50
128
  } catch {
51
129
  return {
52
130
  passed: false,
53
131
  name: "pnpm",
132
+ required: true,
54
133
  message: "pnpm not found. Install: npm install -g pnpm"
55
134
  };
56
135
  }
57
136
  }
137
+ function checkGit() {
138
+ const installed = isGitInstalled();
139
+ return {
140
+ passed: installed,
141
+ name: "git",
142
+ required: false,
143
+ message: installed ? "available (repository initialization supported)" : "not found (git init step will be skipped)"
144
+ };
145
+ }
146
+ function checkDocker() {
147
+ const installed = isDockerInstalled();
148
+ return {
149
+ passed: installed,
150
+ name: "Docker",
151
+ required: false,
152
+ message: installed ? "available (automatic local DB setup supported)" : "not found (start PostgreSQL separately)"
153
+ };
154
+ }
58
155
  async function runPreflightChecks() {
59
156
  console.log();
60
- console.log(chalk.bold(" Checking requirements..."));
157
+ console.log(chalk.bold(" System readiness"));
158
+ console.log(chalk.dim(" Validating required and optional local tooling..."));
61
159
  console.log();
62
- const checks = [checkNodeVersion(), checkPnpmInstalled()];
63
- let hasErrors = false;
160
+ const checks = [
161
+ checkNodeVersion(),
162
+ checkPnpmInstalled(),
163
+ checkGit(),
164
+ checkDocker()
165
+ ];
166
+ const requiredFailures = [];
167
+ const optionalWarnings = [];
64
168
  for (const check of checks) {
169
+ const spinner = ora(`Checking ${check.name}...`).start();
65
170
  if (check.passed) {
66
- console.log(chalk.green(" \u2714"), check.name);
171
+ spinner.succeed(chalk.bold(check.name));
67
172
  } else {
68
- hasErrors = true;
69
- console.log(chalk.red(" \u2716"), check.name);
70
- if (check.message) {
71
- console.log(chalk.dim(` ${check.message}`));
173
+ if (check.required) {
174
+ requiredFailures.push(check);
175
+ spinner.fail(chalk.bold(check.name));
176
+ } else {
177
+ optionalWarnings.push(check);
178
+ spinner.warn(chalk.bold(check.name));
72
179
  }
180
+ console.log(chalk.dim(` ${check.message}`));
73
181
  }
74
182
  }
75
183
  console.log();
76
- if (hasErrors) {
184
+ if (optionalWarnings.length > 0) {
185
+ console.log(chalk.yellow(" Optional tools missing:"));
186
+ for (const warning of optionalWarnings) {
187
+ console.log(chalk.dim(` \u2022 ${warning.name}: ${warning.message}`));
188
+ }
189
+ console.log();
190
+ }
191
+ if (requiredFailures.length > 0) {
77
192
  console.log(
78
193
  chalk.red(" \u2716"),
79
- "Requirements not met. Please fix the errors above."
194
+ "Required dependencies are missing. Fix the items below and try again:"
80
195
  );
196
+ for (const failure of requiredFailures) {
197
+ console.log(chalk.dim(` \u2022 ${failure.name}: ${failure.message}`));
198
+ }
81
199
  console.log();
82
200
  return false;
83
201
  }
84
202
  return true;
85
203
  }
86
204
 
87
- // src/docker.ts
88
- import { execSync as execSync2 } from "child_process";
89
- function isDockerRunning() {
90
- try {
91
- execSync2("docker info", { stdio: "ignore", timeout: 3e3 });
92
- return true;
93
- } catch {
94
- return false;
95
- }
96
- }
97
- function runDockerCompose(targetDir) {
98
- try {
99
- execSync2("docker compose up -d", {
100
- cwd: targetDir,
101
- stdio: "inherit"
102
- });
103
- return true;
104
- } catch {
105
- return false;
106
- }
107
- }
108
- function runDatabaseMigrations(targetDir) {
109
- try {
110
- const isWindows = process.platform === "win32";
111
- execSync2(isWindows ? "pnpm.cmd db:migrate" : "pnpm db:migrate", {
112
- cwd: targetDir,
113
- stdio: "inherit"
114
- });
115
- return true;
116
- } catch {
117
- return false;
118
- }
119
- }
120
-
121
- // src/git.ts
122
- import { execSync as execSync3 } from "child_process";
123
- function isGitInstalled() {
124
- try {
125
- execSync3("git --version", { stdio: "ignore" });
126
- return true;
127
- } catch {
128
- return false;
129
- }
130
- }
131
- function initGit(targetDir) {
132
- try {
133
- execSync3("git init", { cwd: targetDir, stdio: "ignore" });
134
- execSync3("git add -A", { cwd: targetDir, stdio: "ignore" });
135
- execSync3('git commit -m "Initial commit from create-blitzpack"', {
136
- cwd: targetDir,
137
- stdio: "ignore"
138
- });
139
- return true;
140
- } catch {
141
- return false;
142
- }
143
- }
144
-
145
205
  // src/prompts.ts
146
206
  import chalk3 from "chalk";
147
- import prompts from "prompts";
207
+ import {
208
+ cancel,
209
+ confirm,
210
+ isCancel,
211
+ multiselect,
212
+ note,
213
+ select,
214
+ text
215
+ } from "@clack/prompts";
148
216
 
149
217
  // src/constants.ts
150
218
  var REPLACEABLE_FILES = [
@@ -234,6 +302,44 @@ var FEATURE_EXCLUSIONS = {
234
302
  ],
235
303
  ciCd: [".github/workflows/cd.yml"]
236
304
  };
305
+ var PROJECT_PROFILES = [
306
+ {
307
+ key: "recommended",
308
+ name: "Recommended",
309
+ description: "Core app with everything included",
310
+ defaultFeatures: {
311
+ testing: true,
312
+ admin: true,
313
+ uploads: true,
314
+ dockerDeploy: true,
315
+ ciCd: true
316
+ }
317
+ },
318
+ {
319
+ key: "platformAgnostic",
320
+ name: "Platform-Agnostic",
321
+ description: "Core app without dockerfiles",
322
+ defaultFeatures: {
323
+ testing: true,
324
+ admin: true,
325
+ uploads: true,
326
+ dockerDeploy: false,
327
+ ciCd: false
328
+ }
329
+ },
330
+ {
331
+ key: "modular",
332
+ name: "Modular",
333
+ description: "Core app with features of your choice",
334
+ defaultFeatures: {
335
+ testing: false,
336
+ admin: false,
337
+ uploads: false,
338
+ dockerDeploy: false,
339
+ ciCd: false
340
+ }
341
+ }
342
+ ];
237
343
 
238
344
  // src/utils.ts
239
345
  import chalk2 from "chalk";
@@ -351,46 +457,84 @@ function printError(message) {
351
457
  }
352
458
 
353
459
  // src/prompts.ts
354
- async function getProjectOptions(providedName, flags = {}) {
355
- const questions = [];
356
- if (!providedName) {
357
- questions.push({
358
- type: "text",
359
- name: "projectName",
360
- message: "Project name:",
361
- initial: "my-app",
362
- validate: (value) => {
363
- const result = validateProjectName(value);
364
- if (!result.valid) {
365
- return result.problems?.[0] || "Invalid project name";
366
- }
367
- return true;
368
- }
369
- });
370
- }
371
- questions.push({
372
- type: "text",
373
- name: "projectDescription",
374
- message: "Project description:",
375
- initial: DEFAULT_DESCRIPTION
376
- });
377
- let cancelled = false;
378
- const response = await prompts(questions, {
379
- onCancel: () => {
380
- cancelled = true;
381
- }
382
- });
383
- if (cancelled) {
460
+ var FEATURE_LABELS = {
461
+ testing: "Testing",
462
+ admin: "Admin Dashboard",
463
+ uploads: "File Uploads",
464
+ dockerDeploy: "Docker deploy assets",
465
+ ciCd: "CD workflow"
466
+ };
467
+ var FEATURE_HINTS = {
468
+ testing: "Vitest, integration tests, and test helpers",
469
+ admin: "Admin routes, dashboard views, and management hooks",
470
+ uploads: "Upload APIs, storage service, and UI upload components",
471
+ dockerDeploy: "API/Web Dockerfiles and production Docker Compose",
472
+ ciCd: "GitHub Actions workflow for image build and publish"
473
+ };
474
+ function handleCancelledPrompt(value) {
475
+ if (isCancel(value)) {
476
+ cancel("Setup cancelled.");
384
477
  return null;
385
478
  }
386
- const projectName = providedName || response.projectName;
479
+ return value;
480
+ }
481
+ function getEnabledFeatureKeys(features) {
482
+ return Object.keys(features).filter((key) => features[key]);
483
+ }
484
+ function deriveProfileFeatureSelection(profileKey) {
485
+ const profile = PROJECT_PROFILES.find((item) => item.key === profileKey) ?? PROJECT_PROFILES[0];
486
+ return getEnabledFeatureKeys(profile.defaultFeatures);
487
+ }
488
+ function resolveFeatureSelection(selected) {
489
+ const unique = Array.from(new Set(selected));
490
+ const hasCiCd = unique.includes("ciCd");
491
+ const hasDockerDeploy = unique.includes("dockerDeploy");
492
+ if (hasCiCd && !hasDockerDeploy) {
493
+ return [...unique.filter((key) => key !== "dockerDeploy"), "dockerDeploy"];
494
+ }
495
+ return unique;
496
+ }
497
+ function buildFeatureOptions(selectedFeatures) {
498
+ const normalized = resolveFeatureSelection(selectedFeatures);
499
+ return {
500
+ testing: normalized.includes("testing"),
501
+ admin: normalized.includes("admin"),
502
+ uploads: normalized.includes("uploads"),
503
+ dockerDeploy: normalized.includes("dockerDeploy"),
504
+ ciCd: normalized.includes("ciCd")
505
+ };
506
+ }
507
+ function printWizardStep(step, total, title) {
508
+ console.log();
509
+ console.log(chalk3.bold(` Step ${step}/${total}`), chalk3.dim(title));
510
+ console.log();
511
+ }
512
+ function getStepTotal(profileKey) {
513
+ return profileKey === "modular" ? 4 : 2;
514
+ }
515
+ function printMultiselectControls() {
516
+ console.log(
517
+ chalk3.dim(" Controls: \u2191/\u2193 navigate \u2022 Space toggle \u2022 Enter confirm")
518
+ );
519
+ console.log();
520
+ }
521
+ function printConfigurationSummary(profileName, features) {
522
+ const lines = [
523
+ `${chalk3.dim("Profile")}: ${profileName}`,
524
+ `${chalk3.dim("Testing")}: ${features.testing ? "yes" : "no"}`,
525
+ `${chalk3.dim("Admin dashboard")}: ${features.admin ? "yes" : "no"}`,
526
+ `${chalk3.dim("File uploads")}: ${features.uploads ? "yes" : "no"}`,
527
+ `${chalk3.dim("Docker deploy assets")}: ${features.dockerDeploy ? "yes" : "no"}`,
528
+ `${chalk3.dim("CD workflow")}: ${features.ciCd ? "yes" : "no"}`
529
+ ];
530
+ note(lines.join("\n"), chalk3.cyan("Configuration summary"));
531
+ }
532
+ function finalizeOptions(state, providedName, flags) {
533
+ const projectName = providedName || state.projectNameInput;
387
534
  const validation = validateProjectName(projectName);
388
535
  if (!validation.valid) {
389
- console.log(`Invalid project name: ${validation.problems?.[0]}`);
390
- return null;
391
- }
392
- const features = await promptFeatureSelection();
393
- if (!features) {
536
+ console.log();
537
+ console.log(chalk3.red(" \u2716"), validation.problems?.[0] ?? "Invalid project name");
394
538
  return null;
395
539
  }
396
540
  const useCurrentDir = projectName === ".";
@@ -398,134 +542,201 @@ async function getProjectOptions(providedName, flags = {}) {
398
542
  return {
399
543
  projectName: actualProjectName,
400
544
  projectSlug: toSlug(actualProjectName),
401
- projectDescription: response.projectDescription || DEFAULT_DESCRIPTION,
545
+ projectDescription: state.projectDescription || DEFAULT_DESCRIPTION,
402
546
  skipGit: flags.skipGit || false,
403
547
  skipInstall: flags.skipInstall || false,
404
548
  useCurrentDir,
405
- features
549
+ features: buildFeatureOptions(state.selectedFeatures)
406
550
  };
407
551
  }
408
- async function promptFeatureSelection() {
409
- let cancelled = false;
410
- const { setupType } = await prompts(
411
- {
412
- type: "select",
413
- name: "setupType",
414
- message: "Project profile:",
415
- choices: [
416
- {
417
- title: "Recommended",
418
- description: "all app features + Docker deploy assets + CD workflow",
419
- value: "recommended"
420
- },
421
- {
422
- title: "Platform-First",
423
- description: "all app features, no deployment assets",
424
- value: "platform"
425
- },
426
- {
427
- title: "Custom",
428
- description: "choose app and deployment features",
429
- value: "customize"
552
+ async function getProjectOptions(providedName, flags = {}) {
553
+ const initialProjectName = providedName || "my-app";
554
+ const initialProfileKey = "recommended";
555
+ const state = {
556
+ projectNameInput: initialProjectName,
557
+ projectDescription: DEFAULT_DESCRIPTION,
558
+ profileKey: initialProfileKey,
559
+ selectedFeatures: deriveProfileFeatureSelection(initialProfileKey)
560
+ };
561
+ let stage = "details";
562
+ while (true) {
563
+ if (stage === "details") {
564
+ printWizardStep(1, getStepTotal(state.profileKey), "Project details");
565
+ if (!providedName) {
566
+ const projectNameInput = handleCancelledPrompt(
567
+ await text({
568
+ message: chalk3.cyan("Project name"),
569
+ initialValue: state.projectNameInput,
570
+ validate: (value) => {
571
+ if (typeof value !== "string") {
572
+ return "Project name is required";
573
+ }
574
+ const result = validateProjectName(value);
575
+ return result.valid ? void 0 : result.problems?.[0] ?? "Invalid project name";
576
+ }
577
+ })
578
+ );
579
+ if (projectNameInput === null) {
580
+ return null;
430
581
  }
431
- ],
432
- initial: 0,
433
- hint: "- Use arrow-keys, Enter to submit"
434
- },
435
- {
436
- onCancel: () => {
437
- cancelled = true;
582
+ state.projectNameInput = projectNameInput;
583
+ } else {
584
+ console.log(chalk3.dim(" Project name:"), chalk3.white(providedName));
438
585
  }
586
+ const descriptionInput = handleCancelledPrompt(
587
+ await text({
588
+ message: chalk3.cyan("Project description"),
589
+ initialValue: state.projectDescription
590
+ })
591
+ );
592
+ if (descriptionInput === null) {
593
+ return null;
594
+ }
595
+ state.projectDescription = descriptionInput;
596
+ stage = "profile";
597
+ continue;
439
598
  }
440
- );
441
- if (cancelled) {
442
- return null;
443
- }
444
- if (setupType === "recommended") {
445
- return {
446
- testing: true,
447
- admin: true,
448
- uploads: true,
449
- dockerDeploy: true,
450
- ciCd: true
451
- };
452
- }
453
- if (setupType === "platform") {
454
- return {
455
- testing: true,
456
- admin: true,
457
- uploads: true,
458
- dockerDeploy: false,
459
- ciCd: false
460
- };
461
- }
462
- const appFeatureChoices = APP_FEATURES.map((feature) => ({
463
- title: feature.name,
464
- description: feature.description,
465
- value: feature.key,
466
- selected: true
467
- }));
468
- const { selectedAppFeatures } = await prompts(
469
- {
470
- type: "multiselect",
471
- name: "selectedAppFeatures",
472
- message: "Select app features:",
473
- choices: appFeatureChoices,
474
- hint: "- Space to toggle, Enter to confirm",
475
- instructions: false
476
- },
477
- {
478
- onCancel: () => {
479
- cancelled = true;
599
+ if (stage === "profile") {
600
+ printWizardStep(2, getStepTotal(state.profileKey), "Choose a setup preset");
601
+ const profileAction = handleCancelledPrompt(
602
+ await select({
603
+ message: chalk3.cyan("Setup preset"),
604
+ options: [
605
+ ...PROJECT_PROFILES.map((profile2) => ({
606
+ value: profile2.key,
607
+ label: profile2.name,
608
+ hint: profile2.description
609
+ })),
610
+ {
611
+ value: "__back__",
612
+ label: "Back",
613
+ hint: "Return to project details"
614
+ }
615
+ ],
616
+ initialValue: state.profileKey
617
+ })
618
+ );
619
+ if (profileAction === null) {
620
+ return null;
621
+ }
622
+ if (profileAction === "__back__") {
623
+ stage = "details";
624
+ continue;
625
+ }
626
+ state.profileKey = profileAction;
627
+ state.selectedFeatures = deriveProfileFeatureSelection(state.profileKey);
628
+ if (state.profileKey !== "modular") {
629
+ const result = finalizeOptions(state, providedName, flags);
630
+ if (!result) {
631
+ stage = "details";
632
+ continue;
633
+ }
634
+ return result;
480
635
  }
636
+ stage = "features";
637
+ continue;
481
638
  }
482
- );
483
- if (cancelled) {
484
- return null;
485
- }
486
- const deploymentFeatureChoices = DEPLOYMENT_FEATURES.map((feature) => ({
487
- title: feature.name,
488
- description: feature.description,
489
- value: feature.key,
490
- selected: false
491
- }));
492
- const { selectedDeploymentFeatures } = await prompts(
493
- {
494
- type: "multiselect",
495
- name: "selectedDeploymentFeatures",
496
- message: "Select deployment options (optional):",
497
- choices: deploymentFeatureChoices,
498
- hint: "- Space to toggle, Enter to confirm",
499
- instructions: false
500
- },
501
- {
502
- onCancel: () => {
503
- cancelled = true;
639
+ if (stage === "features") {
640
+ const isModular = state.profileKey === "modular";
641
+ printWizardStep(3, 4, isModular ? "Select features" : "Deployment options");
642
+ if (isModular) {
643
+ printMultiselectControls();
644
+ const selectedFeatures = handleCancelledPrompt(
645
+ await multiselect({
646
+ message: chalk3.cyan("Select features"),
647
+ options: [...APP_FEATURES, ...DEPLOYMENT_FEATURES].map((feature) => ({
648
+ value: feature.key,
649
+ label: FEATURE_LABELS[feature.key],
650
+ hint: FEATURE_HINTS[feature.key]
651
+ })),
652
+ initialValues: state.selectedFeatures,
653
+ required: false
654
+ })
655
+ );
656
+ if (selectedFeatures === null) {
657
+ return null;
658
+ }
659
+ state.selectedFeatures = resolveFeatureSelection(
660
+ selectedFeatures
661
+ );
662
+ if (state.selectedFeatures.includes("ciCd") && !selectedFeatures.includes("dockerDeploy")) {
663
+ console.log();
664
+ console.log(
665
+ chalk3.dim(
666
+ " \u2139 CD workflow requires Docker deployment assets, enabling both."
667
+ )
668
+ );
669
+ }
504
670
  }
671
+ const nextAction = handleCancelledPrompt(
672
+ await select({
673
+ message: chalk3.cyan("Continue"),
674
+ options: [
675
+ {
676
+ value: "review",
677
+ label: "Review configuration"
678
+ },
679
+ {
680
+ value: "profile",
681
+ label: "Back",
682
+ hint: "Return to preset selection"
683
+ }
684
+ ],
685
+ initialValue: "review"
686
+ })
687
+ );
688
+ if (nextAction === null) {
689
+ return null;
690
+ }
691
+ stage = nextAction;
692
+ continue;
505
693
  }
506
- );
507
- if (cancelled) {
508
- return null;
509
- }
510
- const selectedApp = selectedAppFeatures || [];
511
- const selectedDeployment = selectedDeploymentFeatures || [];
512
- const includesCiCd = selectedDeployment.includes("ciCd");
513
- const includesDockerDeploy = selectedDeployment.includes("dockerDeploy") || includesCiCd;
514
- if (includesCiCd && !selectedDeployment.includes("dockerDeploy")) {
515
- console.log();
516
- console.log(
517
- chalk3.dim(
518
- " \u2139 CD workflow requires Docker deployment assets, enabling both."
519
- )
694
+ const profile = PROJECT_PROFILES.find((item) => item.key === state.profileKey) ?? PROJECT_PROFILES[0];
695
+ const options = finalizeOptions(state, providedName, flags);
696
+ if (!options) {
697
+ stage = "details";
698
+ continue;
699
+ }
700
+ const features = options.features;
701
+ const reviewStep = state.profileKey === "modular" ? 4 : 3;
702
+ printWizardStep(reviewStep, getStepTotal(state.profileKey), "Review and confirm");
703
+ printConfigurationSummary(profile.name, features);
704
+ const reviewOptions = [
705
+ {
706
+ value: "create",
707
+ label: "Create project"
708
+ },
709
+ {
710
+ value: "profile",
711
+ label: "Edit preset"
712
+ },
713
+ {
714
+ value: "details",
715
+ label: "Edit project details"
716
+ }
717
+ ];
718
+ if (state.profileKey === "modular") {
719
+ reviewOptions.splice(1, 0, {
720
+ value: "features",
721
+ label: "Edit features"
722
+ });
723
+ }
724
+ const reviewAction = handleCancelledPrompt(
725
+ await select({
726
+ message: chalk3.cyan("Ready to scaffold?"),
727
+ options: reviewOptions,
728
+ initialValue: "create"
729
+ })
520
730
  );
731
+ if (reviewAction === null) {
732
+ return null;
733
+ }
734
+ if (reviewAction !== "create") {
735
+ stage = reviewAction;
736
+ continue;
737
+ }
738
+ return options;
521
739
  }
522
- return {
523
- testing: selectedApp.includes("testing"),
524
- admin: selectedApp.includes("admin"),
525
- uploads: selectedApp.includes("uploads"),
526
- dockerDeploy: includesDockerDeploy,
527
- ciCd: includesCiCd
528
- };
529
740
  }
530
741
  async function promptAutomaticSetup() {
531
742
  const dockerRunning = isDockerRunning();
@@ -542,13 +753,16 @@ async function promptAutomaticSetup() {
542
753
  return false;
543
754
  }
544
755
  console.log();
545
- const { runSetup } = await prompts({
546
- type: "confirm",
547
- name: "runSetup",
548
- message: "Run local setup now? (start PostgreSQL with Docker + run migrations)",
549
- initial: true
550
- });
551
- return runSetup || false;
756
+ const runSetup = handleCancelledPrompt(
757
+ await confirm({
758
+ message: "Run local setup now? (Docker PostgreSQL + database migrations)",
759
+ initialValue: true
760
+ })
761
+ );
762
+ if (runSetup === null) {
763
+ return false;
764
+ }
765
+ return runSetup;
552
766
  }
553
767
 
554
768
  // src/template.ts
@@ -1444,6 +1658,37 @@ function runInstall(cwd) {
1444
1658
  child.on("error", () => resolve(false));
1445
1659
  });
1446
1660
  }
1661
+ function renderProgressBar(current, total) {
1662
+ const width = 28;
1663
+ const clampedTotal = Math.max(total, 1);
1664
+ const ratio = Math.min(Math.max(current / clampedTotal, 0), 1);
1665
+ const filled = Math.round(width * ratio);
1666
+ const empty = width - filled;
1667
+ const filledBar = chalk4.cyan("\u2588".repeat(filled));
1668
+ const emptyBar = chalk4.dim("\u2591".repeat(empty));
1669
+ const percentage = `${Math.round(ratio * 100)}`.padStart(3, " ");
1670
+ return `[${filledBar}${emptyBar}] ${percentage}%`;
1671
+ }
1672
+ function renderStepTrack(step, total) {
1673
+ const segments = [];
1674
+ for (let index = 1; index <= total; index++) {
1675
+ if (index < step) {
1676
+ segments.push(chalk4.green("\u25CF"));
1677
+ } else if (index === step) {
1678
+ segments.push(chalk4.cyan("\u25C6"));
1679
+ } else {
1680
+ segments.push(chalk4.dim("\u25C7"));
1681
+ }
1682
+ }
1683
+ return segments.join(chalk4.dim("\u2500\u2500"));
1684
+ }
1685
+ function printStepHeader(step, total, title) {
1686
+ const completed = step - 1;
1687
+ console.log();
1688
+ console.log(chalk4.cyan(` Step ${step}/${total}`), chalk4.bold(title));
1689
+ console.log(` ${renderProgressBar(completed, total)}`);
1690
+ console.log(` ${renderStepTrack(step, total)}`);
1691
+ }
1447
1692
  function printDryRun(options) {
1448
1693
  console.log(chalk4.yellow(" Dry run mode - no changes will be made"));
1449
1694
  console.log();
@@ -1515,13 +1760,11 @@ async function create(projectName, flags) {
1515
1760
  const files = await fs3.readdir(targetDir);
1516
1761
  if (files.length > 0) {
1517
1762
  if (options.useCurrentDir) {
1518
- const { confirm } = await prompts2({
1519
- type: "confirm",
1520
- name: "confirm",
1521
- message: `Current directory is not empty. Continue?`,
1522
- initial: false
1763
+ const shouldContinue = await confirm2({
1764
+ message: "Current directory is not empty. Continue?",
1765
+ initialValue: false
1523
1766
  });
1524
- if (!confirm) {
1767
+ if (isCancel2(shouldContinue) || !shouldContinue) {
1525
1768
  return;
1526
1769
  }
1527
1770
  } else {
@@ -1530,11 +1773,18 @@ async function create(projectName, flags) {
1530
1773
  }
1531
1774
  }
1532
1775
  }
1533
- const spinner = ora();
1776
+ const shouldRunSetup = await promptAutomaticSetup();
1777
+ const totalSteps = 2 + (options.skipGit ? 0 : 1) + (options.skipInstall ? 0 : 1) + (shouldRunSetup ? 1 : 0);
1778
+ let currentStep = 0;
1779
+ let spinner;
1534
1780
  try {
1535
- spinner.start("Downloading template from GitHub...");
1781
+ currentStep += 1;
1782
+ printStepHeader(currentStep, totalSteps, "Scaffold template");
1783
+ spinner = ora2("Downloading template from GitHub...").start();
1536
1784
  await downloadAndPrepareTemplate(targetDir, spinner, options.features);
1537
- spinner.start("Configuring project...");
1785
+ currentStep += 1;
1786
+ printStepHeader(currentStep, totalSteps, "Configure project files");
1787
+ spinner.start("Applying template transforms...");
1538
1788
  await transformFiles(
1539
1789
  targetDir,
1540
1790
  {
@@ -1547,6 +1797,8 @@ async function create(projectName, flags) {
1547
1797
  await copyEnvFiles(targetDir);
1548
1798
  spinner.succeed("Configured project");
1549
1799
  if (!options.skipGit && isGitInstalled()) {
1800
+ currentStep += 1;
1801
+ printStepHeader(currentStep, totalSteps, "Initialize git repository");
1550
1802
  spinner.start("Initializing git repository...");
1551
1803
  const gitSuccess = initGit(targetDir);
1552
1804
  if (gitSuccess) {
@@ -1554,8 +1806,14 @@ async function create(projectName, flags) {
1554
1806
  } else {
1555
1807
  spinner.warn("Failed to initialize git repository");
1556
1808
  }
1809
+ } else if (!options.skipGit) {
1810
+ currentStep += 1;
1811
+ printStepHeader(currentStep, totalSteps, "Initialize git repository");
1812
+ spinner.warn("Skipped git initialization (git not installed)");
1557
1813
  }
1558
1814
  if (!options.skipInstall) {
1815
+ currentStep += 1;
1816
+ printStepHeader(currentStep, totalSteps, "Install dependencies");
1559
1817
  spinner.start("Installing dependencies...");
1560
1818
  const success = await runInstall(targetDir);
1561
1819
  if (success) {
@@ -1567,9 +1825,9 @@ async function create(projectName, flags) {
1567
1825
  }
1568
1826
  }
1569
1827
  let ranAutomaticSetup = false;
1570
- const shouldRunSetup = await promptAutomaticSetup();
1571
1828
  if (shouldRunSetup) {
1572
- console.log();
1829
+ currentStep += 1;
1830
+ printStepHeader(currentStep, totalSteps, "Run local database setup");
1573
1831
  spinner.start("Starting PostgreSQL database...");
1574
1832
  const dockerSuccess = runDockerCompose(targetDir);
1575
1833
  if (dockerSuccess) {
@@ -1596,7 +1854,7 @@ async function create(projectName, flags) {
1596
1854
  ranAutomaticSetup
1597
1855
  );
1598
1856
  } catch (error) {
1599
- spinner.fail();
1857
+ spinner?.fail();
1600
1858
  printError(
1601
1859
  error instanceof Error ? error.message : "Unknown error occurred"
1602
1860
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-blitzpack",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "Create a new Blitzpack project - full-stack TypeScript monorepo with Next.js and Fastify",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,18 +16,17 @@
16
16
  "clean": "rm -rf dist"
17
17
  },
18
18
  "dependencies": {
19
+ "@clack/prompts": "^1.0.0",
19
20
  "chalk": "^5.6.2",
20
21
  "commander": "^13.1.0",
21
22
  "fs-extra": "^11.3.3",
22
23
  "giget": "^2.0.0",
23
24
  "ora": "^8.2.0",
24
- "prompts": "^2.4.2",
25
25
  "validate-npm-package-name": "^6.0.2"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/fs-extra": "^11.0.4",
29
29
  "@types/node": "^22.19.3",
30
- "@types/prompts": "^2.4.9",
31
30
  "@types/validate-npm-package-name": "^4.0.2",
32
31
  "tsup": "^8.5.1",
33
32
  "typescript": "^5.9.3"