create-blitzpack 0.1.18 → 0.1.20

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 +565 -259
  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
@@ -7,16 +7,85 @@ import { dirname, join } from "path";
7
7
  import { fileURLToPath } from "url";
8
8
 
9
9
  // src/commands/create.ts
10
+ import { confirm as confirm2, isCancel as isCancel2 } from "@clack/prompts";
10
11
  import chalk4 from "chalk";
11
12
  import { spawn } from "child_process";
12
13
  import fs3 from "fs-extra";
13
- import ora from "ora";
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
206
+ import {
207
+ cancel,
208
+ confirm,
209
+ isCancel,
210
+ multiselect,
211
+ note,
212
+ select,
213
+ text
214
+ } from "@clack/prompts";
146
215
  import chalk3 from "chalk";
147
- import prompts from "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,87 @@ 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(
538
+ chalk3.red(" \u2716"),
539
+ validation.problems?.[0] ?? "Invalid project name"
540
+ );
394
541
  return null;
395
542
  }
396
543
  const useCurrentDir = projectName === ".";
@@ -398,134 +545,215 @@ async function getProjectOptions(providedName, flags = {}) {
398
545
  return {
399
546
  projectName: actualProjectName,
400
547
  projectSlug: toSlug(actualProjectName),
401
- projectDescription: response.projectDescription || DEFAULT_DESCRIPTION,
548
+ projectDescription: state.projectDescription || DEFAULT_DESCRIPTION,
402
549
  skipGit: flags.skipGit || false,
403
550
  skipInstall: flags.skipInstall || false,
404
551
  useCurrentDir,
405
- features
552
+ features: buildFeatureOptions(state.selectedFeatures)
406
553
  };
407
554
  }
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"
555
+ async function getProjectOptions(providedName, flags = {}) {
556
+ const initialProjectName = providedName || "my-app";
557
+ const initialProfileKey = "recommended";
558
+ const state = {
559
+ projectNameInput: initialProjectName,
560
+ projectDescription: DEFAULT_DESCRIPTION,
561
+ profileKey: initialProfileKey,
562
+ selectedFeatures: deriveProfileFeatureSelection(initialProfileKey)
563
+ };
564
+ let stage = "details";
565
+ while (true) {
566
+ if (stage === "details") {
567
+ printWizardStep(1, getStepTotal(state.profileKey), "Project details");
568
+ if (!providedName) {
569
+ const projectNameInput = handleCancelledPrompt(
570
+ await text({
571
+ message: chalk3.cyan("Project name"),
572
+ initialValue: state.projectNameInput,
573
+ validate: (value) => {
574
+ if (typeof value !== "string") {
575
+ return "Project name is required";
576
+ }
577
+ const result = validateProjectName(value);
578
+ return result.valid ? void 0 : result.problems?.[0] ?? "Invalid project name";
579
+ }
580
+ })
581
+ );
582
+ if (projectNameInput === null) {
583
+ return null;
430
584
  }
431
- ],
432
- initial: 0,
433
- hint: "- Use arrow-keys, Enter to submit"
434
- },
435
- {
436
- onCancel: () => {
437
- cancelled = true;
585
+ state.projectNameInput = projectNameInput;
586
+ } else {
587
+ console.log(chalk3.dim(" Project name:"), chalk3.white(providedName));
588
+ }
589
+ const descriptionInput = handleCancelledPrompt(
590
+ await text({
591
+ message: chalk3.cyan("Project description"),
592
+ initialValue: state.projectDescription
593
+ })
594
+ );
595
+ if (descriptionInput === null) {
596
+ return null;
438
597
  }
598
+ state.projectDescription = descriptionInput;
599
+ stage = "profile";
600
+ continue;
439
601
  }
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;
602
+ if (stage === "profile") {
603
+ printWizardStep(
604
+ 2,
605
+ getStepTotal(state.profileKey),
606
+ "Choose a setup preset"
607
+ );
608
+ const profileAction = handleCancelledPrompt(
609
+ await select({
610
+ message: chalk3.cyan("Setup preset"),
611
+ options: [
612
+ ...PROJECT_PROFILES.map((profile2) => ({
613
+ value: profile2.key,
614
+ label: profile2.name,
615
+ hint: profile2.description
616
+ })),
617
+ {
618
+ value: "__back__",
619
+ label: "Back",
620
+ hint: "Return to project details"
621
+ }
622
+ ],
623
+ initialValue: state.profileKey
624
+ })
625
+ );
626
+ if (profileAction === null) {
627
+ return null;
628
+ }
629
+ if (profileAction === "__back__") {
630
+ stage = "details";
631
+ continue;
480
632
  }
633
+ state.profileKey = profileAction;
634
+ state.selectedFeatures = deriveProfileFeatureSelection(state.profileKey);
635
+ if (state.profileKey !== "modular") {
636
+ const result = finalizeOptions(state, providedName, flags);
637
+ if (!result) {
638
+ stage = "details";
639
+ continue;
640
+ }
641
+ return result;
642
+ }
643
+ stage = "features";
644
+ continue;
481
645
  }
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;
646
+ if (stage === "features") {
647
+ const isModular = state.profileKey === "modular";
648
+ printWizardStep(
649
+ 3,
650
+ 4,
651
+ isModular ? "Select features" : "Deployment options"
652
+ );
653
+ if (isModular) {
654
+ printMultiselectControls();
655
+ const selectedFeatures = handleCancelledPrompt(
656
+ await multiselect({
657
+ message: chalk3.cyan("Select features"),
658
+ options: [...APP_FEATURES, ...DEPLOYMENT_FEATURES].map(
659
+ (feature) => ({
660
+ value: feature.key,
661
+ label: FEATURE_LABELS[feature.key],
662
+ hint: FEATURE_HINTS[feature.key]
663
+ })
664
+ ),
665
+ initialValues: state.selectedFeatures,
666
+ required: false
667
+ })
668
+ );
669
+ if (selectedFeatures === null) {
670
+ return null;
671
+ }
672
+ state.selectedFeatures = resolveFeatureSelection(
673
+ selectedFeatures
674
+ );
675
+ if (state.selectedFeatures.includes("ciCd") && !selectedFeatures.includes("dockerDeploy")) {
676
+ console.log();
677
+ console.log(
678
+ chalk3.dim(
679
+ " \u2139 CD workflow requires Docker deployment assets, enabling both."
680
+ )
681
+ );
682
+ }
683
+ }
684
+ const nextAction = handleCancelledPrompt(
685
+ await select({
686
+ message: chalk3.cyan("Continue"),
687
+ options: [
688
+ {
689
+ value: "review",
690
+ label: "Review configuration"
691
+ },
692
+ {
693
+ value: "profile",
694
+ label: "Back",
695
+ hint: "Return to preset selection"
696
+ }
697
+ ],
698
+ initialValue: "review"
699
+ })
700
+ );
701
+ if (nextAction === null) {
702
+ return null;
504
703
  }
704
+ stage = nextAction;
705
+ continue;
505
706
  }
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
- )
707
+ const profile = PROJECT_PROFILES.find((item) => item.key === state.profileKey) ?? PROJECT_PROFILES[0];
708
+ const options = finalizeOptions(state, providedName, flags);
709
+ if (!options) {
710
+ stage = "details";
711
+ continue;
712
+ }
713
+ const features = options.features;
714
+ const reviewStep = state.profileKey === "modular" ? 4 : 3;
715
+ printWizardStep(
716
+ reviewStep,
717
+ getStepTotal(state.profileKey),
718
+ "Review and confirm"
719
+ );
720
+ printConfigurationSummary(profile.name, features);
721
+ const reviewOptions = [
722
+ {
723
+ value: "create",
724
+ label: "Create project"
725
+ },
726
+ {
727
+ value: "profile",
728
+ label: "Edit preset"
729
+ },
730
+ {
731
+ value: "details",
732
+ label: "Edit project details"
733
+ }
734
+ ];
735
+ if (state.profileKey === "modular") {
736
+ reviewOptions.splice(1, 0, {
737
+ value: "features",
738
+ label: "Edit features"
739
+ });
740
+ }
741
+ const reviewAction = handleCancelledPrompt(
742
+ await select({
743
+ message: chalk3.cyan("Ready to scaffold?"),
744
+ options: reviewOptions,
745
+ initialValue: "create"
746
+ })
520
747
  );
748
+ if (reviewAction === null) {
749
+ return null;
750
+ }
751
+ if (reviewAction !== "create") {
752
+ stage = reviewAction;
753
+ continue;
754
+ }
755
+ return options;
521
756
  }
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
757
  }
530
758
  async function promptAutomaticSetup() {
531
759
  const dockerRunning = isDockerRunning();
@@ -542,13 +770,16 @@ async function promptAutomaticSetup() {
542
770
  return false;
543
771
  }
544
772
  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;
773
+ const runSetup = handleCancelledPrompt(
774
+ await confirm({
775
+ message: "Run local setup now? (Docker PostgreSQL + database migrations)",
776
+ initialValue: true
777
+ })
778
+ );
779
+ if (runSetup === null) {
780
+ return false;
781
+ }
782
+ return runSetup;
552
783
  }
553
784
 
554
785
  // src/template.ts
@@ -559,7 +790,9 @@ var GITHUB_REPO = "github:CarboxyDev/blitzpack";
559
790
  var POST_DOWNLOAD_EXCLUDES = [
560
791
  "create-blitzpack",
561
792
  "apps/marketing",
562
- "CONTRIBUTING.md"
793
+ "CONTRIBUTING.md",
794
+ "docs/create-blitzpack-scaffolding-maintenance-plan.md",
795
+ "pnpm-lock.yaml"
563
796
  ];
564
797
  function getFeatureExclusions(features) {
565
798
  const exclusions = [];
@@ -1146,7 +1379,8 @@ ${vars.projectDescription}
1146
1379
 
1147
1380
  \`\`\`bash
1148
1381
  pnpm install
1149
- pnpm init:project
1382
+ docker compose up -d
1383
+ pnpm db:migrate
1150
1384
  pnpm dev
1151
1385
  \`\`\`
1152
1386
 
@@ -1444,6 +1678,52 @@ function runInstall(cwd) {
1444
1678
  child.on("error", () => resolve(false));
1445
1679
  });
1446
1680
  }
1681
+ function installGitHooks(cwd) {
1682
+ return new Promise((resolve) => {
1683
+ const isWindows = process.platform === "win32";
1684
+ const child = spawn(
1685
+ isWindows ? "pnpm.cmd" : "pnpm",
1686
+ ["exec", "husky"],
1687
+ {
1688
+ cwd,
1689
+ stdio: "ignore"
1690
+ }
1691
+ );
1692
+ child.on("close", (code) => resolve(code === 0));
1693
+ child.on("error", () => resolve(false));
1694
+ });
1695
+ }
1696
+ function renderProgressBar(current, total) {
1697
+ const width = 28;
1698
+ const clampedTotal = Math.max(total, 1);
1699
+ const ratio = Math.min(Math.max(current / clampedTotal, 0), 1);
1700
+ const filled = Math.round(width * ratio);
1701
+ const empty = width - filled;
1702
+ const filledBar = chalk4.cyan("\u2588".repeat(filled));
1703
+ const emptyBar = chalk4.dim("\u2591".repeat(empty));
1704
+ const percentage = `${Math.round(ratio * 100)}`.padStart(3, " ");
1705
+ return `[${filledBar}${emptyBar}] ${percentage}%`;
1706
+ }
1707
+ function renderStepTrack(step, total) {
1708
+ const segments = [];
1709
+ for (let index = 1; index <= total; index++) {
1710
+ if (index < step) {
1711
+ segments.push(chalk4.green("\u25CF"));
1712
+ } else if (index === step) {
1713
+ segments.push(chalk4.cyan("\u25C6"));
1714
+ } else {
1715
+ segments.push(chalk4.dim("\u25C7"));
1716
+ }
1717
+ }
1718
+ return segments.join(chalk4.dim("\u2500\u2500"));
1719
+ }
1720
+ function printStepHeader(step, total, title) {
1721
+ const completed = step - 1;
1722
+ console.log();
1723
+ console.log(chalk4.cyan(` Step ${step}/${total}`), chalk4.bold(title));
1724
+ console.log(` ${renderProgressBar(completed, total)}`);
1725
+ console.log(` ${renderStepTrack(step, total)}`);
1726
+ }
1447
1727
  function printDryRun(options) {
1448
1728
  console.log(chalk4.yellow(" Dry run mode - no changes will be made"));
1449
1729
  console.log();
@@ -1480,12 +1760,12 @@ function printDryRun(options) {
1480
1760
  console.log(` ${chalk4.dim("\u2022")} Download template from GitHub`);
1481
1761
  console.log(` ${chalk4.dim("\u2022")} Transform package.json files`);
1482
1762
  console.log(` ${chalk4.dim("\u2022")} Create .env.local files`);
1483
- if (!options.skipGit) {
1484
- console.log(` ${chalk4.dim("\u2022")} Initialize git repository`);
1485
- }
1486
1763
  if (!options.skipInstall) {
1487
1764
  console.log(` ${chalk4.dim("\u2022")} Install dependencies (pnpm install)`);
1488
1765
  }
1766
+ if (!options.skipGit) {
1767
+ console.log(` ${chalk4.dim("\u2022")} Initialize git repository`);
1768
+ }
1489
1769
  console.log();
1490
1770
  }
1491
1771
  async function create(projectName, flags) {
@@ -1515,13 +1795,11 @@ async function create(projectName, flags) {
1515
1795
  const files = await fs3.readdir(targetDir);
1516
1796
  if (files.length > 0) {
1517
1797
  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
1798
+ const shouldContinue = await confirm2({
1799
+ message: "Current directory is not empty. Continue?",
1800
+ initialValue: false
1523
1801
  });
1524
- if (!confirm) {
1802
+ if (isCancel2(shouldContinue) || !shouldContinue) {
1525
1803
  return;
1526
1804
  }
1527
1805
  } else {
@@ -1530,11 +1808,19 @@ async function create(projectName, flags) {
1530
1808
  }
1531
1809
  }
1532
1810
  }
1533
- const spinner = ora();
1811
+ const shouldRunSetup = await promptAutomaticSetup();
1812
+ const totalSteps = 2 + (options.skipGit ? 0 : 1) + (options.skipInstall ? 0 : 1) + (shouldRunSetup ? 1 : 0);
1813
+ let currentStep = 0;
1814
+ let spinner;
1815
+ let installSucceeded = false;
1534
1816
  try {
1535
- spinner.start("Downloading template from GitHub...");
1817
+ currentStep += 1;
1818
+ printStepHeader(currentStep, totalSteps, "Scaffold template");
1819
+ spinner = ora2("Downloading template from GitHub...").start();
1536
1820
  await downloadAndPrepareTemplate(targetDir, spinner, options.features);
1537
- spinner.start("Configuring project...");
1821
+ currentStep += 1;
1822
+ printStepHeader(currentStep, totalSteps, "Configure project files");
1823
+ spinner.start("Applying template transforms...");
1538
1824
  await transformFiles(
1539
1825
  targetDir,
1540
1826
  {
@@ -1546,19 +1832,12 @@ async function create(projectName, flags) {
1546
1832
  );
1547
1833
  await copyEnvFiles(targetDir);
1548
1834
  spinner.succeed("Configured project");
1549
- if (!options.skipGit && isGitInstalled()) {
1550
- spinner.start("Initializing git repository...");
1551
- const gitSuccess = initGit(targetDir);
1552
- if (gitSuccess) {
1553
- spinner.succeed("Initialized git repository");
1554
- } else {
1555
- spinner.warn("Failed to initialize git repository");
1556
- }
1557
- }
1558
1835
  if (!options.skipInstall) {
1836
+ currentStep += 1;
1837
+ printStepHeader(currentStep, totalSteps, "Install dependencies");
1559
1838
  spinner.start("Installing dependencies...");
1560
- const success = await runInstall(targetDir);
1561
- if (success) {
1839
+ installSucceeded = await runInstall(targetDir);
1840
+ if (installSucceeded) {
1562
1841
  spinner.succeed("Installed dependencies");
1563
1842
  } else {
1564
1843
  spinner.warn(
@@ -1566,10 +1845,37 @@ async function create(projectName, flags) {
1566
1845
  );
1567
1846
  }
1568
1847
  }
1848
+ if (!options.skipGit && isGitInstalled()) {
1849
+ currentStep += 1;
1850
+ printStepHeader(currentStep, totalSteps, "Initialize git repository");
1851
+ spinner.start("Initializing git repository...");
1852
+ const gitSuccess = initGit(targetDir);
1853
+ if (gitSuccess) {
1854
+ if (installSucceeded) {
1855
+ spinner.start("Installing git hooks...");
1856
+ const hooksSuccess = await installGitHooks(targetDir);
1857
+ if (hooksSuccess) {
1858
+ spinner.succeed("Initialized git repository");
1859
+ } else {
1860
+ spinner.warn(
1861
+ "Initialized git repository, but failed to install git hooks"
1862
+ );
1863
+ }
1864
+ } else {
1865
+ spinner.succeed("Initialized git repository");
1866
+ }
1867
+ } else {
1868
+ spinner.warn("Failed to initialize git repository");
1869
+ }
1870
+ } else if (!options.skipGit) {
1871
+ currentStep += 1;
1872
+ printStepHeader(currentStep, totalSteps, "Initialize git repository");
1873
+ spinner.warn("Skipped git initialization (git not installed)");
1874
+ }
1569
1875
  let ranAutomaticSetup = false;
1570
- const shouldRunSetup = await promptAutomaticSetup();
1571
1876
  if (shouldRunSetup) {
1572
- console.log();
1877
+ currentStep += 1;
1878
+ printStepHeader(currentStep, totalSteps, "Run local database setup");
1573
1879
  spinner.start("Starting PostgreSQL database...");
1574
1880
  const dockerSuccess = runDockerCompose(targetDir);
1575
1881
  if (dockerSuccess) {
@@ -1596,7 +1902,7 @@ async function create(projectName, flags) {
1596
1902
  ranAutomaticSetup
1597
1903
  );
1598
1904
  } catch (error) {
1599
- spinner.fail();
1905
+ spinner?.fail();
1600
1906
  printError(
1601
1907
  error instanceof Error ? error.message : "Unknown error occurred"
1602
1908
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-blitzpack",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
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"