@alexmc2/create-express-api-starter 0.1.0
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/LICENSE +21 -0
- package/README.md +293 -0
- package/dist/cli.js +930 -0
- package/dist/cli.js.map +1 -0
- package/package.json +70 -0
- package/templates/js/mvc/.env.example.ejs +7 -0
- package/templates/js/mvc/.eslintrc.cjs.ejs +24 -0
- package/templates/js/mvc/.gitignore.ejs +6 -0
- package/templates/js/mvc/README.md.ejs +187 -0
- package/templates/js/mvc/__tests__/app.test.js.ejs +51 -0
- package/templates/js/mvc/compose.yaml.ejs +13 -0
- package/templates/js/mvc/db/schema.sql.ejs +8 -0
- package/templates/js/mvc/db/seed.sql.ejs +7 -0
- package/templates/js/mvc/jest.config.js.ejs +6 -0
- package/templates/js/mvc/package.json.ejs +40 -0
- package/templates/js/mvc/scripts/dbCreate.js.ejs +97 -0
- package/templates/js/mvc/scripts/dbReset.js.ejs +42 -0
- package/templates/js/mvc/scripts/dbSeed.js.ejs +69 -0
- package/templates/js/mvc/scripts/dbSetup.js.ejs +69 -0
- package/templates/js/mvc/src/app.js.ejs +57 -0
- package/templates/js/mvc/src/controllers/usersController.js.ejs +32 -0
- package/templates/js/mvc/src/db/pool.js.ejs +19 -0
- package/templates/js/mvc/src/errors/AppError.js.ejs +16 -0
- package/templates/js/mvc/src/middleware/errorHandler.js.ejs +39 -0
- package/templates/js/mvc/src/middleware/notFound.js.ejs +15 -0
- package/templates/js/mvc/src/repositories/usersRepository.js.ejs +69 -0
- package/templates/js/mvc/src/routes/health.js.ejs +19 -0
- package/templates/js/mvc/src/routes/users.js.ejs +22 -0
- package/templates/js/mvc/src/server.js.ejs +21 -0
- package/templates/js/mvc/src/services/usersService.js.ejs +34 -0
- package/templates/js/mvc/src/utils/getPort.js.ejs +18 -0
- package/templates/js/simple/.env.example.ejs +7 -0
- package/templates/js/simple/.eslintrc.cjs.ejs +24 -0
- package/templates/js/simple/.gitignore.ejs +6 -0
- package/templates/js/simple/README.md.ejs +182 -0
- package/templates/js/simple/__tests__/app.test.js.ejs +51 -0
- package/templates/js/simple/compose.yaml.ejs +13 -0
- package/templates/js/simple/db/schema.sql.ejs +8 -0
- package/templates/js/simple/db/seed.sql.ejs +7 -0
- package/templates/js/simple/jest.config.js.ejs +6 -0
- package/templates/js/simple/package.json.ejs +40 -0
- package/templates/js/simple/scripts/dbCreate.js.ejs +97 -0
- package/templates/js/simple/scripts/dbReset.js.ejs +42 -0
- package/templates/js/simple/scripts/dbSeed.js.ejs +69 -0
- package/templates/js/simple/scripts/dbSetup.js.ejs +69 -0
- package/templates/js/simple/src/app.js.ejs +57 -0
- package/templates/js/simple/src/db/pool.js.ejs +19 -0
- package/templates/js/simple/src/errors/AppError.js.ejs +16 -0
- package/templates/js/simple/src/middleware/errorHandler.js.ejs +39 -0
- package/templates/js/simple/src/middleware/notFound.js.ejs +15 -0
- package/templates/js/simple/src/repositories/usersRepository.js.ejs +69 -0
- package/templates/js/simple/src/routes/health.js.ejs +19 -0
- package/templates/js/simple/src/routes/users.js.ejs +52 -0
- package/templates/js/simple/src/server.js.ejs +21 -0
- package/templates/js/simple/src/utils/getPort.js.ejs +18 -0
- package/templates/ts/mvc/.env.example.ejs +7 -0
- package/templates/ts/mvc/.eslintrc.cjs.ejs +27 -0
- package/templates/ts/mvc/.gitignore.ejs +6 -0
- package/templates/ts/mvc/README.md.ejs +188 -0
- package/templates/ts/mvc/__tests__/app.test.ts.ejs +45 -0
- package/templates/ts/mvc/compose.yaml.ejs +13 -0
- package/templates/ts/mvc/db/schema.sql.ejs +8 -0
- package/templates/ts/mvc/db/seed.sql.ejs +7 -0
- package/templates/ts/mvc/jest.config.js.ejs +7 -0
- package/templates/ts/mvc/package.json.ejs +51 -0
- package/templates/ts/mvc/scripts/dbCreate.js.ejs +93 -0
- package/templates/ts/mvc/scripts/dbReset.js.ejs +40 -0
- package/templates/ts/mvc/scripts/dbSeed.js.ejs +62 -0
- package/templates/ts/mvc/scripts/dbSetup.js.ejs +62 -0
- package/templates/ts/mvc/src/app.ts.ejs +45 -0
- package/templates/ts/mvc/src/controllers/usersController.ts.ejs +31 -0
- package/templates/ts/mvc/src/db/pool.ts.ejs +17 -0
- package/templates/ts/mvc/src/errors/AppError.ts.ejs +14 -0
- package/templates/ts/mvc/src/middleware/errorHandler.ts.ejs +49 -0
- package/templates/ts/mvc/src/middleware/notFound.ts.ejs +13 -0
- package/templates/ts/mvc/src/repositories/usersRepository.ts.ejs +87 -0
- package/templates/ts/mvc/src/routes/health.ts.ejs +13 -0
- package/templates/ts/mvc/src/routes/users.ts.ejs +14 -0
- package/templates/ts/mvc/src/server.ts.ejs +15 -0
- package/templates/ts/mvc/src/services/usersService.ts.ejs +35 -0
- package/templates/ts/mvc/src/utils/getPort.ts.ejs +12 -0
- package/templates/ts/mvc/tsconfig.json.ejs +13 -0
- package/templates/ts/simple/.env.example.ejs +7 -0
- package/templates/ts/simple/.eslintrc.cjs.ejs +27 -0
- package/templates/ts/simple/.gitignore.ejs +6 -0
- package/templates/ts/simple/README.md.ejs +182 -0
- package/templates/ts/simple/__tests__/app.test.ts.ejs +45 -0
- package/templates/ts/simple/compose.yaml.ejs +13 -0
- package/templates/ts/simple/db/schema.sql.ejs +8 -0
- package/templates/ts/simple/db/seed.sql.ejs +7 -0
- package/templates/ts/simple/jest.config.js.ejs +7 -0
- package/templates/ts/simple/package.json.ejs +51 -0
- package/templates/ts/simple/scripts/dbCreate.js.ejs +93 -0
- package/templates/ts/simple/scripts/dbReset.js.ejs +40 -0
- package/templates/ts/simple/scripts/dbSeed.js.ejs +62 -0
- package/templates/ts/simple/scripts/dbSetup.js.ejs +62 -0
- package/templates/ts/simple/src/app.ts.ejs +45 -0
- package/templates/ts/simple/src/db/pool.ts.ejs +17 -0
- package/templates/ts/simple/src/errors/AppError.ts.ejs +14 -0
- package/templates/ts/simple/src/middleware/errorHandler.ts.ejs +49 -0
- package/templates/ts/simple/src/middleware/notFound.ts.ejs +13 -0
- package/templates/ts/simple/src/repositories/usersRepository.ts.ejs +87 -0
- package/templates/ts/simple/src/routes/health.ts.ejs +13 -0
- package/templates/ts/simple/src/routes/users.ts.ejs +43 -0
- package/templates/ts/simple/src/server.ts.ejs +15 -0
- package/templates/ts/simple/src/utils/getPort.ts.ejs +12 -0
- package/templates/ts/simple/tsconfig.json.ejs +13 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,930 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/index.ts
|
|
4
|
+
import pc4 from "picocolors";
|
|
5
|
+
import { pathToFileURL } from "url";
|
|
6
|
+
import fs4 from "fs";
|
|
7
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8
|
+
|
|
9
|
+
// src/cli/args.ts
|
|
10
|
+
var TRUE_VALUES = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
11
|
+
var FALSE_VALUES = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
|
|
12
|
+
function parseBooleanValue(value) {
|
|
13
|
+
if (value === void 0) {
|
|
14
|
+
return void 0;
|
|
15
|
+
}
|
|
16
|
+
const normalized = value.trim().toLowerCase();
|
|
17
|
+
if (TRUE_VALUES.has(normalized)) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
if (FALSE_VALUES.has(normalized)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return void 0;
|
|
24
|
+
}
|
|
25
|
+
function splitFlag(token) {
|
|
26
|
+
const withoutPrefix = token.slice(2);
|
|
27
|
+
const equalsIndex = withoutPrefix.indexOf("=");
|
|
28
|
+
if (equalsIndex === -1) {
|
|
29
|
+
return {
|
|
30
|
+
name: withoutPrefix,
|
|
31
|
+
value: void 0
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
name: withoutPrefix.slice(0, equalsIndex),
|
|
36
|
+
value: withoutPrefix.slice(equalsIndex + 1)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function parseArgs(argv) {
|
|
40
|
+
const flags = {
|
|
41
|
+
yes: false,
|
|
42
|
+
dryRun: false,
|
|
43
|
+
install: true,
|
|
44
|
+
git: true,
|
|
45
|
+
verbose: false
|
|
46
|
+
};
|
|
47
|
+
const provided = {
|
|
48
|
+
yes: false,
|
|
49
|
+
dryRun: false,
|
|
50
|
+
install: false,
|
|
51
|
+
git: false,
|
|
52
|
+
verbose: false
|
|
53
|
+
};
|
|
54
|
+
const unknownFlags = [];
|
|
55
|
+
const positionals = [];
|
|
56
|
+
let positionalOnly = false;
|
|
57
|
+
for (const token of argv) {
|
|
58
|
+
if (positionalOnly) {
|
|
59
|
+
positionals.push(token);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (token === "--") {
|
|
63
|
+
positionalOnly = true;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!token.startsWith("-") || token === "-") {
|
|
67
|
+
positionals.push(token);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (!token.startsWith("--")) {
|
|
71
|
+
unknownFlags.push(token);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const { name, value } = splitFlag(token);
|
|
75
|
+
if (name === "yes") {
|
|
76
|
+
const parsedValue = parseBooleanValue(value);
|
|
77
|
+
if (value !== void 0 && parsedValue === void 0) {
|
|
78
|
+
unknownFlags.push(token);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
flags.yes = parsedValue ?? true;
|
|
82
|
+
provided.yes = true;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (name === "dry-run") {
|
|
86
|
+
const parsedValue = parseBooleanValue(value);
|
|
87
|
+
if (value !== void 0 && parsedValue === void 0) {
|
|
88
|
+
unknownFlags.push(token);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
flags.dryRun = parsedValue ?? true;
|
|
92
|
+
provided.dryRun = true;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (name === "no-install") {
|
|
96
|
+
const parsedValue = parseBooleanValue(value);
|
|
97
|
+
if (value !== void 0 && parsedValue === void 0) {
|
|
98
|
+
unknownFlags.push(token);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const noInstall = parsedValue ?? true;
|
|
102
|
+
flags.install = !noInstall;
|
|
103
|
+
provided.install = true;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (name === "no-git") {
|
|
107
|
+
const parsedValue = parseBooleanValue(value);
|
|
108
|
+
if (value !== void 0 && parsedValue === void 0) {
|
|
109
|
+
unknownFlags.push(token);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const noGit = parsedValue ?? true;
|
|
113
|
+
flags.git = !noGit;
|
|
114
|
+
provided.git = true;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (name === "verbose") {
|
|
118
|
+
const parsedValue = parseBooleanValue(value);
|
|
119
|
+
if (value !== void 0 && parsedValue === void 0) {
|
|
120
|
+
unknownFlags.push(token);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
flags.verbose = parsedValue ?? true;
|
|
124
|
+
provided.verbose = true;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
unknownFlags.push(token);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
projectName: positionals[0],
|
|
131
|
+
positionals,
|
|
132
|
+
unknownFlags,
|
|
133
|
+
flags,
|
|
134
|
+
provided
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/cli/output.ts
|
|
139
|
+
import path from "path";
|
|
140
|
+
import pc2 from "picocolors";
|
|
141
|
+
|
|
142
|
+
// src/core/labels.ts
|
|
143
|
+
function languageLabel(language) {
|
|
144
|
+
return language === "ts" ? "TypeScript" : "JavaScript";
|
|
145
|
+
}
|
|
146
|
+
function moduleSystemLabel(moduleSystem) {
|
|
147
|
+
return moduleSystem === "esm" ? "ES Modules" : "CommonJS";
|
|
148
|
+
}
|
|
149
|
+
function jsDevWatcherLabel(jsDevWatcher) {
|
|
150
|
+
return jsDevWatcher === "nodemon" ? "nodemon" : "node --watch";
|
|
151
|
+
}
|
|
152
|
+
function architectureLabel(architecture) {
|
|
153
|
+
return architecture === "mvc" ? "MVC" : "Simple";
|
|
154
|
+
}
|
|
155
|
+
function databaseLabel(databaseMode) {
|
|
156
|
+
if (databaseMode === "postgres-psql") {
|
|
157
|
+
return "Postgres (psql)";
|
|
158
|
+
}
|
|
159
|
+
if (databaseMode === "postgres-docker") {
|
|
160
|
+
return "Postgres (Docker)";
|
|
161
|
+
}
|
|
162
|
+
return "In-memory";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/utils/terminalUi.ts
|
|
166
|
+
import pc from "picocolors";
|
|
167
|
+
var ANSI_PATTERN = /\u001b\[[0-9;]*m/g;
|
|
168
|
+
function stripAnsi(value) {
|
|
169
|
+
return value.replace(ANSI_PATTERN, "");
|
|
170
|
+
}
|
|
171
|
+
function displayLength(value) {
|
|
172
|
+
return stripAnsi(value).length;
|
|
173
|
+
}
|
|
174
|
+
function padDisplay(value, width) {
|
|
175
|
+
const padding = width - displayLength(value);
|
|
176
|
+
if (padding <= 0) {
|
|
177
|
+
return value;
|
|
178
|
+
}
|
|
179
|
+
return `${value}${" ".repeat(padding)}`;
|
|
180
|
+
}
|
|
181
|
+
function minimumContentWidth(lines) {
|
|
182
|
+
return lines.reduce((max, line) => {
|
|
183
|
+
return Math.max(max, displayLength(line));
|
|
184
|
+
}, 0);
|
|
185
|
+
}
|
|
186
|
+
function styleValue(value, tone) {
|
|
187
|
+
if (tone === "accent") {
|
|
188
|
+
return pc.cyan(value);
|
|
189
|
+
}
|
|
190
|
+
if (tone === "success") {
|
|
191
|
+
return pc.green(value);
|
|
192
|
+
}
|
|
193
|
+
if (tone === "warn") {
|
|
194
|
+
return pc.yellow(value);
|
|
195
|
+
}
|
|
196
|
+
if (tone === "muted") {
|
|
197
|
+
return pc.dim(value);
|
|
198
|
+
}
|
|
199
|
+
return value;
|
|
200
|
+
}
|
|
201
|
+
function statusTag(tone) {
|
|
202
|
+
if (tone === "success") {
|
|
203
|
+
return pc.bold(pc.green("[ok]"));
|
|
204
|
+
}
|
|
205
|
+
if (tone === "warn") {
|
|
206
|
+
return pc.bold(pc.yellow("[!!]"));
|
|
207
|
+
}
|
|
208
|
+
if (tone === "error") {
|
|
209
|
+
return pc.bold(pc.red("[x]"));
|
|
210
|
+
}
|
|
211
|
+
return pc.bold(pc.cyan("[..]"));
|
|
212
|
+
}
|
|
213
|
+
function formatKeyValueLines(rows) {
|
|
214
|
+
const keyWidth = rows.reduce((max, row) => Math.max(max, row.key.length), 0);
|
|
215
|
+
return rows.map((row) => {
|
|
216
|
+
const key = pc.bold(padDisplay(row.key, keyWidth));
|
|
217
|
+
const value = styleValue(row.value, row.tone ?? "default");
|
|
218
|
+
return `${key} ${value}`;
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function formatCommandLines(commands) {
|
|
222
|
+
return commands.map((command) => pc.bold(pc.cyan(command)));
|
|
223
|
+
}
|
|
224
|
+
function printCard(title, lines) {
|
|
225
|
+
const content = lines.length > 0 ? lines : [pc.dim("(none)")];
|
|
226
|
+
const width = Math.max(30, minimumContentWidth([title, ...content]));
|
|
227
|
+
const border = pc.dim(pc.cyan(`+${"-".repeat(width + 2)}+`));
|
|
228
|
+
const edge = pc.dim(pc.cyan("|"));
|
|
229
|
+
const divider = pc.dim("-".repeat(width));
|
|
230
|
+
console.log(border);
|
|
231
|
+
console.log(
|
|
232
|
+
`${edge} ${padDisplay(pc.bold(pc.cyan(title)), width)} ${edge}`
|
|
233
|
+
);
|
|
234
|
+
console.log(`${edge} ${divider} ${edge}`);
|
|
235
|
+
for (const line of content) {
|
|
236
|
+
console.log(`${edge} ${padDisplay(line, width)} ${edge}`);
|
|
237
|
+
}
|
|
238
|
+
console.log(border);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/cli/output.ts
|
|
242
|
+
function buildNextStepCommands(selection) {
|
|
243
|
+
const commands = [`cd ${selection.projectName}`];
|
|
244
|
+
if (!selection.installDeps) {
|
|
245
|
+
commands.push("npm install");
|
|
246
|
+
}
|
|
247
|
+
commands.push("cp .env.example .env");
|
|
248
|
+
if (selection.databaseMode === "postgres-psql") {
|
|
249
|
+
commands.push("npm run db:create");
|
|
250
|
+
commands.push("npm run db:setup");
|
|
251
|
+
commands.push("npm run db:seed");
|
|
252
|
+
}
|
|
253
|
+
if (selection.databaseMode === "postgres-docker") {
|
|
254
|
+
commands.push("npm run db:up");
|
|
255
|
+
commands.push("npm run db:setup");
|
|
256
|
+
commands.push("npm run db:seed");
|
|
257
|
+
}
|
|
258
|
+
commands.push("npm run dev");
|
|
259
|
+
if (selection.language === "ts") {
|
|
260
|
+
commands.push("npm run build");
|
|
261
|
+
}
|
|
262
|
+
commands.push("npm test");
|
|
263
|
+
return commands;
|
|
264
|
+
}
|
|
265
|
+
function printDryRunPlan(selection, plan) {
|
|
266
|
+
const languageValue = selection.language === "js" ? `${languageLabel(selection.language)} (${moduleSystemLabel(selection.moduleSystem)})` : languageLabel(selection.language);
|
|
267
|
+
const summaryEntries = [
|
|
268
|
+
{
|
|
269
|
+
key: "Target",
|
|
270
|
+
value: formatTargetPath(plan.targetDir),
|
|
271
|
+
tone: "accent"
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
key: "Language",
|
|
275
|
+
value: languageValue,
|
|
276
|
+
tone: "accent"
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
key: "Architecture",
|
|
280
|
+
value: architectureLabel(selection.architecture),
|
|
281
|
+
tone: "accent"
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
key: "Database",
|
|
285
|
+
value: databaseLabel(selection.databaseMode),
|
|
286
|
+
tone: "accent"
|
|
287
|
+
}
|
|
288
|
+
];
|
|
289
|
+
if (selection.language === "js") {
|
|
290
|
+
summaryEntries.push({
|
|
291
|
+
key: "Dev watcher",
|
|
292
|
+
value: jsDevWatcherLabel(selection.jsDevWatcher),
|
|
293
|
+
tone: "accent"
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
summaryEntries.push(
|
|
297
|
+
{
|
|
298
|
+
key: "Educational",
|
|
299
|
+
value: selection.educational ? "On" : "Off",
|
|
300
|
+
tone: selection.educational ? "success" : "muted"
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
key: "Install deps",
|
|
304
|
+
value: selection.installDeps ? "Yes" : "No",
|
|
305
|
+
tone: selection.installDeps ? "success" : "warn"
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
key: "Init git",
|
|
309
|
+
value: selection.initGit ? "Yes" : "No",
|
|
310
|
+
tone: selection.initGit ? "success" : "warn"
|
|
311
|
+
}
|
|
312
|
+
);
|
|
313
|
+
const summaryLines = formatKeyValueLines(summaryEntries);
|
|
314
|
+
const fileLines = plan.files.map((file) => `${pc2.dim("-")} ${file.outputRelativePath}`);
|
|
315
|
+
console.log("");
|
|
316
|
+
printCard("Dry Run: Configuration", summaryLines);
|
|
317
|
+
console.log("");
|
|
318
|
+
printCard(`Dry Run: Files (${plan.files.length})`, fileLines);
|
|
319
|
+
}
|
|
320
|
+
function printNextSteps(selection) {
|
|
321
|
+
const stackParts = [
|
|
322
|
+
selection.language === "js" ? `${languageLabel(selection.language)} (${moduleSystemLabel(selection.moduleSystem)})` : languageLabel(selection.language),
|
|
323
|
+
architectureLabel(selection.architecture),
|
|
324
|
+
databaseLabel(selection.databaseMode)
|
|
325
|
+
];
|
|
326
|
+
const summaryEntries = [
|
|
327
|
+
{
|
|
328
|
+
key: "Project",
|
|
329
|
+
value: selection.projectName,
|
|
330
|
+
tone: "accent"
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
key: "Stack",
|
|
334
|
+
value: stackParts.join(" | "),
|
|
335
|
+
tone: "accent"
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
key: "Educational",
|
|
339
|
+
value: selection.educational ? "On" : "Off",
|
|
340
|
+
tone: selection.educational ? "success" : "muted"
|
|
341
|
+
}
|
|
342
|
+
];
|
|
343
|
+
if (selection.language === "js") {
|
|
344
|
+
summaryEntries.push({
|
|
345
|
+
key: "Dev watcher",
|
|
346
|
+
value: jsDevWatcherLabel(selection.jsDevWatcher),
|
|
347
|
+
tone: "accent"
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
const summaryLines = formatKeyValueLines(summaryEntries);
|
|
351
|
+
const nextStepCommands = buildNextStepCommands(selection);
|
|
352
|
+
console.log("");
|
|
353
|
+
printCard("Project Ready", summaryLines);
|
|
354
|
+
console.log("");
|
|
355
|
+
printCard("Next Steps", formatCommandLines(nextStepCommands));
|
|
356
|
+
if (selection.databaseMode === "postgres-psql") {
|
|
357
|
+
const setupLines = [
|
|
358
|
+
pc2.yellow("Linux first-time setup (run once if needed):"),
|
|
359
|
+
pc2.dim("# Create a Postgres role matching your OS user"),
|
|
360
|
+
...formatCommandLines([
|
|
361
|
+
'sudo -u postgres createuser --createdb "$USER"',
|
|
362
|
+
`sudo -u postgres psql -c "ALTER USER \\"$USER\\" WITH PASSWORD 'postgres';"`
|
|
363
|
+
])
|
|
364
|
+
];
|
|
365
|
+
console.log("");
|
|
366
|
+
printCard("Postgres Setup", setupLines);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function formatTargetPath(targetDir) {
|
|
370
|
+
const relative = path.relative(process.cwd(), targetDir);
|
|
371
|
+
return relative || ".";
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// src/cli/prompts.ts
|
|
375
|
+
import {
|
|
376
|
+
confirm,
|
|
377
|
+
intro,
|
|
378
|
+
isCancel,
|
|
379
|
+
outro,
|
|
380
|
+
select,
|
|
381
|
+
text
|
|
382
|
+
} from "@clack/prompts";
|
|
383
|
+
import pc3 from "picocolors";
|
|
384
|
+
|
|
385
|
+
// src/core/defaults.ts
|
|
386
|
+
var DEFAULT_PROJECT_NAME = "my-api";
|
|
387
|
+
var DEFAULT_SELECTIONS = {
|
|
388
|
+
language: "js",
|
|
389
|
+
moduleSystem: "commonjs",
|
|
390
|
+
jsDevWatcher: "node-watch",
|
|
391
|
+
architecture: "simple",
|
|
392
|
+
databaseMode: "memory",
|
|
393
|
+
educational: true,
|
|
394
|
+
installDeps: true,
|
|
395
|
+
initGit: true
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
// src/cli/prompts.ts
|
|
399
|
+
var PromptCancelledError = class extends Error {
|
|
400
|
+
constructor() {
|
|
401
|
+
super("Prompt cancelled by user.");
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
function unwrapPrompt(value) {
|
|
405
|
+
if (isCancel(value)) {
|
|
406
|
+
throw new PromptCancelledError();
|
|
407
|
+
}
|
|
408
|
+
return value;
|
|
409
|
+
}
|
|
410
|
+
async function collectSelections(parsedArgs) {
|
|
411
|
+
if (parsedArgs.flags.yes || !process.stdin.isTTY) {
|
|
412
|
+
return {
|
|
413
|
+
projectName: parsedArgs.projectName ?? DEFAULT_PROJECT_NAME,
|
|
414
|
+
language: DEFAULT_SELECTIONS.language,
|
|
415
|
+
moduleSystem: DEFAULT_SELECTIONS.moduleSystem,
|
|
416
|
+
jsDevWatcher: DEFAULT_SELECTIONS.jsDevWatcher,
|
|
417
|
+
architecture: DEFAULT_SELECTIONS.architecture,
|
|
418
|
+
databaseMode: DEFAULT_SELECTIONS.databaseMode,
|
|
419
|
+
educational: DEFAULT_SELECTIONS.educational,
|
|
420
|
+
installDeps: parsedArgs.flags.install,
|
|
421
|
+
initGit: parsedArgs.flags.git,
|
|
422
|
+
dryRun: parsedArgs.flags.dryRun
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
intro(
|
|
426
|
+
[
|
|
427
|
+
pc3.bold(pc3.cyan("Create Express API Starter")),
|
|
428
|
+
pc3.dim("Scaffold an Express backend with practical defaults.")
|
|
429
|
+
].join("\n")
|
|
430
|
+
);
|
|
431
|
+
const projectName = parsedArgs.projectName ? parsedArgs.projectName : unwrapPrompt(
|
|
432
|
+
await text({
|
|
433
|
+
message: "Project name",
|
|
434
|
+
placeholder: DEFAULT_PROJECT_NAME,
|
|
435
|
+
defaultValue: DEFAULT_PROJECT_NAME,
|
|
436
|
+
validate(value) {
|
|
437
|
+
if (!value.trim()) {
|
|
438
|
+
return "Project name is required.";
|
|
439
|
+
}
|
|
440
|
+
return void 0;
|
|
441
|
+
}
|
|
442
|
+
})
|
|
443
|
+
);
|
|
444
|
+
const language = unwrapPrompt(
|
|
445
|
+
await select({
|
|
446
|
+
message: "Language",
|
|
447
|
+
initialValue: DEFAULT_SELECTIONS.language,
|
|
448
|
+
options: [
|
|
449
|
+
{
|
|
450
|
+
value: "js",
|
|
451
|
+
label: "JavaScript"
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
value: "ts",
|
|
455
|
+
label: "TypeScript"
|
|
456
|
+
}
|
|
457
|
+
]
|
|
458
|
+
})
|
|
459
|
+
);
|
|
460
|
+
const moduleSystem = language === "js" ? unwrapPrompt(
|
|
461
|
+
await select({
|
|
462
|
+
message: "Module system",
|
|
463
|
+
initialValue: DEFAULT_SELECTIONS.moduleSystem,
|
|
464
|
+
options: [
|
|
465
|
+
{
|
|
466
|
+
value: "commonjs",
|
|
467
|
+
label: "CommonJS"
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
value: "esm",
|
|
471
|
+
label: "ES Modules"
|
|
472
|
+
}
|
|
473
|
+
]
|
|
474
|
+
})
|
|
475
|
+
) : "commonjs";
|
|
476
|
+
const jsDevWatcher = language === "js" ? unwrapPrompt(
|
|
477
|
+
await select({
|
|
478
|
+
message: "Dev watcher (JavaScript)",
|
|
479
|
+
initialValue: DEFAULT_SELECTIONS.jsDevWatcher,
|
|
480
|
+
options: [
|
|
481
|
+
{
|
|
482
|
+
value: "node-watch",
|
|
483
|
+
label: "node --watch (built-in)"
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
value: "nodemon",
|
|
487
|
+
label: "nodemon"
|
|
488
|
+
}
|
|
489
|
+
]
|
|
490
|
+
})
|
|
491
|
+
) : DEFAULT_SELECTIONS.jsDevWatcher;
|
|
492
|
+
const architecture = unwrapPrompt(
|
|
493
|
+
await select({
|
|
494
|
+
message: "Architecture",
|
|
495
|
+
initialValue: DEFAULT_SELECTIONS.architecture,
|
|
496
|
+
options: [
|
|
497
|
+
{
|
|
498
|
+
value: "simple",
|
|
499
|
+
label: "Simple"
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
value: "mvc",
|
|
503
|
+
label: "MVC"
|
|
504
|
+
}
|
|
505
|
+
]
|
|
506
|
+
})
|
|
507
|
+
);
|
|
508
|
+
const databaseMode = unwrapPrompt(
|
|
509
|
+
await select({
|
|
510
|
+
message: "Database",
|
|
511
|
+
initialValue: DEFAULT_SELECTIONS.databaseMode,
|
|
512
|
+
options: [
|
|
513
|
+
{
|
|
514
|
+
value: "memory",
|
|
515
|
+
label: "In-memory"
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
value: "postgres-psql",
|
|
519
|
+
label: "Postgres (psql)"
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
value: "postgres-docker",
|
|
523
|
+
label: "Postgres (Docker)"
|
|
524
|
+
}
|
|
525
|
+
]
|
|
526
|
+
})
|
|
527
|
+
);
|
|
528
|
+
const educational = unwrapPrompt(
|
|
529
|
+
await confirm({
|
|
530
|
+
message: "Add educational comments",
|
|
531
|
+
initialValue: DEFAULT_SELECTIONS.educational
|
|
532
|
+
})
|
|
533
|
+
);
|
|
534
|
+
const installDeps = parsedArgs.provided.install ? parsedArgs.flags.install : unwrapPrompt(
|
|
535
|
+
await confirm({
|
|
536
|
+
message: "Install dependencies now",
|
|
537
|
+
initialValue: DEFAULT_SELECTIONS.installDeps
|
|
538
|
+
})
|
|
539
|
+
);
|
|
540
|
+
const initGit = parsedArgs.provided.git ? parsedArgs.flags.git : unwrapPrompt(
|
|
541
|
+
await confirm({
|
|
542
|
+
message: "Initialize git repository",
|
|
543
|
+
initialValue: DEFAULT_SELECTIONS.initGit
|
|
544
|
+
})
|
|
545
|
+
);
|
|
546
|
+
outro(pc3.cyan("Scaffolding project files..."));
|
|
547
|
+
return {
|
|
548
|
+
projectName,
|
|
549
|
+
language,
|
|
550
|
+
moduleSystem,
|
|
551
|
+
jsDevWatcher,
|
|
552
|
+
architecture,
|
|
553
|
+
databaseMode,
|
|
554
|
+
educational,
|
|
555
|
+
installDeps,
|
|
556
|
+
initGit,
|
|
557
|
+
dryRun: parsedArgs.flags.dryRun
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// src/core/validation.ts
|
|
562
|
+
import path2 from "path";
|
|
563
|
+
function validateProjectName(projectName) {
|
|
564
|
+
const trimmed = projectName.trim();
|
|
565
|
+
if (!trimmed) {
|
|
566
|
+
return "Project name is required.";
|
|
567
|
+
}
|
|
568
|
+
if (trimmed === "." || trimmed === "..") {
|
|
569
|
+
return 'Project name cannot be "." or "..".';
|
|
570
|
+
}
|
|
571
|
+
if (trimmed !== path2.basename(trimmed)) {
|
|
572
|
+
return "Project name must be a folder name, not a path.";
|
|
573
|
+
}
|
|
574
|
+
if (/[^a-zA-Z0-9._-]/.test(trimmed)) {
|
|
575
|
+
return 'Project name can only include letters, numbers, ".", "_", and "-".';
|
|
576
|
+
}
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// src/generator/index.ts
|
|
581
|
+
import path4 from "path";
|
|
582
|
+
import os from "os";
|
|
583
|
+
import fs2 from "fs-extra";
|
|
584
|
+
import ejs from "ejs";
|
|
585
|
+
|
|
586
|
+
// src/utils/paths.ts
|
|
587
|
+
import path3 from "path";
|
|
588
|
+
import { fileURLToPath } from "url";
|
|
589
|
+
import fs from "fs-extra";
|
|
590
|
+
function resolveTargetDir(baseDir, projectName) {
|
|
591
|
+
return path3.resolve(baseDir, projectName);
|
|
592
|
+
}
|
|
593
|
+
function resolveTemplatesDir() {
|
|
594
|
+
const moduleDir = path3.dirname(fileURLToPath(import.meta.url));
|
|
595
|
+
const candidates = [
|
|
596
|
+
path3.resolve(moduleDir, "../templates"),
|
|
597
|
+
path3.resolve(moduleDir, "../../templates"),
|
|
598
|
+
path3.resolve(process.cwd(), "templates")
|
|
599
|
+
];
|
|
600
|
+
for (const candidate of candidates) {
|
|
601
|
+
if (fs.existsSync(candidate)) {
|
|
602
|
+
return candidate;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
throw new Error("Unable to locate templates directory.");
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// src/generator/index.ts
|
|
609
|
+
function toPosixPath(value) {
|
|
610
|
+
return value.split(path4.sep).join("/");
|
|
611
|
+
}
|
|
612
|
+
function isEjsTemplate(relativePath) {
|
|
613
|
+
return relativePath.endsWith(".ejs");
|
|
614
|
+
}
|
|
615
|
+
function stripEjsSuffix(relativePath) {
|
|
616
|
+
return relativePath.endsWith(".ejs") ? relativePath.slice(0, -".ejs".length) : relativePath;
|
|
617
|
+
}
|
|
618
|
+
function resolveTemplateRoot(config) {
|
|
619
|
+
const templatesDir = resolveTemplatesDir();
|
|
620
|
+
return path4.join(templatesDir, config.language, config.architecture);
|
|
621
|
+
}
|
|
622
|
+
async function listFilesRecursive(directory, baseDir = directory) {
|
|
623
|
+
const entries = await fs2.readdir(directory, {
|
|
624
|
+
withFileTypes: true
|
|
625
|
+
});
|
|
626
|
+
const sortedEntries = entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
627
|
+
const results = [];
|
|
628
|
+
for (const entry of sortedEntries) {
|
|
629
|
+
const entryPath = path4.join(directory, entry.name);
|
|
630
|
+
if (entry.isDirectory()) {
|
|
631
|
+
const childEntries = await listFilesRecursive(entryPath, baseDir);
|
|
632
|
+
results.push(...childEntries);
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
results.push(path4.relative(baseDir, entryPath));
|
|
636
|
+
}
|
|
637
|
+
return results;
|
|
638
|
+
}
|
|
639
|
+
function shouldIncludeTemplate(relativePath, config) {
|
|
640
|
+
if (relativePath === "compose.yaml.ejs") {
|
|
641
|
+
return config.databaseMode === "postgres-docker";
|
|
642
|
+
}
|
|
643
|
+
if (relativePath === "scripts/dbCreate.js.ejs") {
|
|
644
|
+
return config.databaseMode === "postgres-psql";
|
|
645
|
+
}
|
|
646
|
+
if (relativePath.startsWith("scripts/")) {
|
|
647
|
+
return config.databaseMode !== "memory";
|
|
648
|
+
}
|
|
649
|
+
if (relativePath.startsWith("db/")) {
|
|
650
|
+
return config.databaseMode !== "memory";
|
|
651
|
+
}
|
|
652
|
+
if (relativePath.startsWith("src/db/")) {
|
|
653
|
+
return config.databaseMode !== "memory";
|
|
654
|
+
}
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
function toPlannedFile(relativeTemplatePath) {
|
|
658
|
+
return {
|
|
659
|
+
templateRelativePath: relativeTemplatePath,
|
|
660
|
+
outputRelativePath: stripEjsSuffix(relativeTemplatePath),
|
|
661
|
+
isTemplate: isEjsTemplate(relativeTemplatePath)
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
function toPackageName(projectName) {
|
|
665
|
+
const cleaned = projectName.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+/, "").replace(/-+$/, "");
|
|
666
|
+
return cleaned || "express-api";
|
|
667
|
+
}
|
|
668
|
+
function toDatabaseName(projectName) {
|
|
669
|
+
const cleaned = projectName.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+/, "").replace(/_+$/, "");
|
|
670
|
+
return (cleaned || "express_api") + "_dev";
|
|
671
|
+
}
|
|
672
|
+
function getOsUsername() {
|
|
673
|
+
try {
|
|
674
|
+
return os.userInfo().username;
|
|
675
|
+
} catch {
|
|
676
|
+
return process.env.USER ?? process.env.USERNAME ?? "postgres";
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
function templateData(config) {
|
|
680
|
+
const isTypeScript = config.language === "ts";
|
|
681
|
+
const isEsm = config.moduleSystem === "esm";
|
|
682
|
+
const isJavaScript = config.language === "js";
|
|
683
|
+
const useNodemon = isJavaScript && config.jsDevWatcher === "nodemon";
|
|
684
|
+
const isPostgres = config.databaseMode !== "memory";
|
|
685
|
+
const isDocker = config.databaseMode === "postgres-docker";
|
|
686
|
+
const isPsql = config.databaseMode === "postgres-psql";
|
|
687
|
+
const dbName = toDatabaseName(config.projectName);
|
|
688
|
+
const username = isPostgres ? getOsUsername() : "";
|
|
689
|
+
return {
|
|
690
|
+
...config,
|
|
691
|
+
isTypeScript,
|
|
692
|
+
isEsm,
|
|
693
|
+
isCommonJs: !isEsm,
|
|
694
|
+
isPostgres,
|
|
695
|
+
isDocker,
|
|
696
|
+
isPsql,
|
|
697
|
+
packageName: toPackageName(config.projectName),
|
|
698
|
+
databaseName: dbName,
|
|
699
|
+
educationalLabel: config.educational ? "On" : "Off",
|
|
700
|
+
languageLabel: languageLabel(config.language),
|
|
701
|
+
moduleSystemLabel: moduleSystemLabel(config.moduleSystem),
|
|
702
|
+
architectureLabel: architectureLabel(config.architecture),
|
|
703
|
+
databaseLabel: databaseLabel(config.databaseMode),
|
|
704
|
+
jsDevWatcherLabel: jsDevWatcherLabel(config.jsDevWatcher),
|
|
705
|
+
jsDevCommand: useNodemon ? "nodemon src/server.js" : "node --watch src/server.js",
|
|
706
|
+
useNodemon,
|
|
707
|
+
databaseUrl: config.databaseMode === "postgres-docker" ? `postgres://postgres:postgres@localhost:5433/${dbName}` : `postgres://${encodeURIComponent(username)}:postgres@localhost:5432/${dbName}`,
|
|
708
|
+
osUsername: username
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
function fromPosixPath(relativePath) {
|
|
712
|
+
return relativePath.split("/").join(path4.sep);
|
|
713
|
+
}
|
|
714
|
+
async function planProject(config, targetDir) {
|
|
715
|
+
const templateRoot = resolveTemplateRoot(config);
|
|
716
|
+
const templateRootExists = await fs2.pathExists(templateRoot);
|
|
717
|
+
if (!templateRootExists) {
|
|
718
|
+
throw new Error(`Template root not found: ${templateRoot}`);
|
|
719
|
+
}
|
|
720
|
+
const allFiles = await listFilesRecursive(templateRoot);
|
|
721
|
+
const files = allFiles.map(toPosixPath).filter((relativePath) => shouldIncludeTemplate(relativePath, config)).map(toPlannedFile);
|
|
722
|
+
return {
|
|
723
|
+
targetDir,
|
|
724
|
+
actions: [
|
|
725
|
+
`Create project directory: ${targetDir}`,
|
|
726
|
+
`Write ${files.length} files`
|
|
727
|
+
],
|
|
728
|
+
files
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
async function generateProject({
|
|
732
|
+
config,
|
|
733
|
+
targetDir,
|
|
734
|
+
dryRun = false
|
|
735
|
+
}) {
|
|
736
|
+
const templateRoot = resolveTemplateRoot(config);
|
|
737
|
+
const plan = await planProject(config, targetDir);
|
|
738
|
+
if (dryRun) {
|
|
739
|
+
return plan;
|
|
740
|
+
}
|
|
741
|
+
await fs2.ensureDir(targetDir);
|
|
742
|
+
const data = templateData(config);
|
|
743
|
+
for (const file of plan.files) {
|
|
744
|
+
const sourcePath = path4.join(
|
|
745
|
+
templateRoot,
|
|
746
|
+
fromPosixPath(file.templateRelativePath)
|
|
747
|
+
);
|
|
748
|
+
const destinationPath = path4.join(
|
|
749
|
+
targetDir,
|
|
750
|
+
fromPosixPath(file.outputRelativePath)
|
|
751
|
+
);
|
|
752
|
+
await fs2.ensureDir(path4.dirname(destinationPath));
|
|
753
|
+
if (file.isTemplate) {
|
|
754
|
+
const template = await fs2.readFile(sourcePath, "utf8");
|
|
755
|
+
const rendered = ejs.render(template, data);
|
|
756
|
+
await fs2.writeFile(destinationPath, rendered, "utf8");
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
await fs2.copy(sourcePath, destinationPath);
|
|
760
|
+
}
|
|
761
|
+
return plan;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// src/utils/exec.ts
|
|
765
|
+
import { execa } from "execa";
|
|
766
|
+
async function commandExists(command, args = ["--version"]) {
|
|
767
|
+
try {
|
|
768
|
+
await execa(command, args, {
|
|
769
|
+
stdio: "ignore"
|
|
770
|
+
});
|
|
771
|
+
return true;
|
|
772
|
+
} catch {
|
|
773
|
+
return false;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
async function runCommand(command, args, cwd) {
|
|
777
|
+
await execa(command, args, {
|
|
778
|
+
cwd,
|
|
779
|
+
stdio: "inherit"
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
async function installDependencies(cwd, verbose = false) {
|
|
783
|
+
const args = ["install", "--no-audit", "--no-fund"];
|
|
784
|
+
if (!verbose) {
|
|
785
|
+
args.push("--loglevel=error");
|
|
786
|
+
}
|
|
787
|
+
await runCommand("npm", args, cwd);
|
|
788
|
+
}
|
|
789
|
+
async function initGitRepo(cwd) {
|
|
790
|
+
await runCommand("git", ["init"], cwd);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// src/utils/files.ts
|
|
794
|
+
import path5 from "path";
|
|
795
|
+
import fs3 from "fs-extra";
|
|
796
|
+
async function assertSafeTargetDir(targetDir) {
|
|
797
|
+
const exists = await fs3.pathExists(targetDir);
|
|
798
|
+
if (!exists) {
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
const stats = await fs3.stat(targetDir);
|
|
802
|
+
if (!stats.isDirectory()) {
|
|
803
|
+
throw new Error(`Target path already exists and is not a directory: ${targetDir}`);
|
|
804
|
+
}
|
|
805
|
+
const entries = await fs3.readdir(targetDir);
|
|
806
|
+
if (entries.length > 0) {
|
|
807
|
+
throw new Error(
|
|
808
|
+
`Target directory "${path5.basename(targetDir)}" already exists and is not empty.`
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// src/utils/logger.ts
|
|
814
|
+
var logger = {
|
|
815
|
+
info(message) {
|
|
816
|
+
console.log(`${statusTag("info")} ${message}`);
|
|
817
|
+
},
|
|
818
|
+
success(message) {
|
|
819
|
+
console.log(`${statusTag("success")} ${message}`);
|
|
820
|
+
},
|
|
821
|
+
warn(message) {
|
|
822
|
+
console.warn(`${statusTag("warn")} ${message}`);
|
|
823
|
+
},
|
|
824
|
+
error(message) {
|
|
825
|
+
console.error(`${statusTag("error")} ${message}`);
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
// src/cli/index.ts
|
|
830
|
+
async function ensurePsqlAvailable() {
|
|
831
|
+
const hasPsql = await commandExists("psql", ["--version"]);
|
|
832
|
+
if (!hasPsql) {
|
|
833
|
+
throw new Error(
|
|
834
|
+
[
|
|
835
|
+
"Postgres (psql) mode requires the `psql` client tool, but it was not found.",
|
|
836
|
+
"Install Postgres client tools and make sure `psql --version` works, or rerun and choose Postgres (Docker)."
|
|
837
|
+
].join(" ")
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
function registerSigintHandler() {
|
|
842
|
+
process.on("SIGINT", () => {
|
|
843
|
+
logger.warn("Cancelled by user.");
|
|
844
|
+
process.exit(1);
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
async function runCli(argv) {
|
|
848
|
+
const parsedArgs = parseArgs(argv);
|
|
849
|
+
for (const unknownFlag of parsedArgs.unknownFlags) {
|
|
850
|
+
logger.warn(`Unknown flag "${unknownFlag}" was ignored.`);
|
|
851
|
+
}
|
|
852
|
+
const selections = await collectSelections(parsedArgs);
|
|
853
|
+
const projectNameError = validateProjectName(selections.projectName);
|
|
854
|
+
if (projectNameError) {
|
|
855
|
+
throw new Error(projectNameError);
|
|
856
|
+
}
|
|
857
|
+
const targetDir = resolveTargetDir(process.cwd(), selections.projectName);
|
|
858
|
+
await assertSafeTargetDir(targetDir);
|
|
859
|
+
if (selections.databaseMode === "postgres-psql") {
|
|
860
|
+
await ensurePsqlAvailable();
|
|
861
|
+
}
|
|
862
|
+
const templateConfig = {
|
|
863
|
+
projectName: selections.projectName,
|
|
864
|
+
language: selections.language,
|
|
865
|
+
moduleSystem: selections.moduleSystem,
|
|
866
|
+
jsDevWatcher: selections.jsDevWatcher,
|
|
867
|
+
architecture: selections.architecture,
|
|
868
|
+
educational: selections.educational,
|
|
869
|
+
databaseMode: selections.databaseMode
|
|
870
|
+
};
|
|
871
|
+
const plan = await planProject(templateConfig, targetDir);
|
|
872
|
+
if (selections.dryRun) {
|
|
873
|
+
printDryRunPlan(selections, plan);
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
await generateProject({
|
|
877
|
+
config: templateConfig,
|
|
878
|
+
targetDir
|
|
879
|
+
});
|
|
880
|
+
logger.success(`Project files generated at ${targetDir}.`);
|
|
881
|
+
if (selections.installDeps) {
|
|
882
|
+
const installCommand = parsedArgs.flags.verbose ? "npm install --no-audit --no-fund" : "npm install --no-audit --no-fund --loglevel=error";
|
|
883
|
+
logger.info(`Installing dependencies (${installCommand})...`);
|
|
884
|
+
await installDependencies(targetDir, parsedArgs.flags.verbose);
|
|
885
|
+
logger.success("Dependencies installed.");
|
|
886
|
+
} else {
|
|
887
|
+
logger.info("Skipped dependency installation.");
|
|
888
|
+
}
|
|
889
|
+
if (selections.initGit) {
|
|
890
|
+
logger.info("Initializing git repository...");
|
|
891
|
+
await initGitRepo(targetDir);
|
|
892
|
+
logger.success("Git repository initialized.");
|
|
893
|
+
} else {
|
|
894
|
+
logger.info("Skipped git initialization.");
|
|
895
|
+
}
|
|
896
|
+
logger.success("Scaffolding complete.");
|
|
897
|
+
printNextSteps(selections);
|
|
898
|
+
}
|
|
899
|
+
function isCliEntrypoint() {
|
|
900
|
+
if (typeof process.argv[1] !== "string") {
|
|
901
|
+
return false;
|
|
902
|
+
}
|
|
903
|
+
try {
|
|
904
|
+
const argvPath = fs4.realpathSync(process.argv[1]);
|
|
905
|
+
const modulePath = fs4.realpathSync(fileURLToPath2(import.meta.url));
|
|
906
|
+
return argvPath === modulePath;
|
|
907
|
+
} catch {
|
|
908
|
+
return pathToFileURL(process.argv[1]).href === import.meta.url;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
var isEntrypoint = isCliEntrypoint();
|
|
912
|
+
if (isEntrypoint) {
|
|
913
|
+
registerSigintHandler();
|
|
914
|
+
runCli(process.argv.slice(2)).catch((error) => {
|
|
915
|
+
if (error instanceof PromptCancelledError) {
|
|
916
|
+
logger.warn("Cancelled by user.");
|
|
917
|
+
process.exit(1);
|
|
918
|
+
}
|
|
919
|
+
const message = error instanceof Error ? error.message : "Unexpected error";
|
|
920
|
+
logger.error(message);
|
|
921
|
+
if (error instanceof Error && error.stack) {
|
|
922
|
+
console.error(pc4.gray(error.stack));
|
|
923
|
+
}
|
|
924
|
+
process.exit(1);
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
export {
|
|
928
|
+
runCli
|
|
929
|
+
};
|
|
930
|
+
//# sourceMappingURL=cli.js.map
|