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.
- package/README.md +5 -3
- package/dist/index.js +501 -243
- 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**:
|
|
35
|
-
- **Platform-
|
|
36
|
-
- **
|
|
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
|
|
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
|
-
|
|
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("
|
|
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 = [
|
|
63
|
-
|
|
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
|
-
|
|
171
|
+
spinner.succeed(chalk.bold(check.name));
|
|
67
172
|
} else {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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 (
|
|
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
|
-
"
|
|
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
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
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(
|
|
390
|
-
|
|
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:
|
|
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
|
|
409
|
-
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
433
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
"
|
|
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
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
|
1519
|
-
|
|
1520
|
-
|
|
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 (!
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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"
|