@quyentran93/servercn-cli 1.1.10
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 +74 -0
- package/dist/cli.js +2778 -0
- package/dist/cli.js.map +1 -0
- package/package.json +64 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2778 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/add/index.ts
|
|
7
|
+
import fs9 from "fs-extra";
|
|
8
|
+
import path10 from "path";
|
|
9
|
+
|
|
10
|
+
// src/lib/copy.ts
|
|
11
|
+
import fs from "fs-extra";
|
|
12
|
+
import path from "path";
|
|
13
|
+
|
|
14
|
+
// src/utils/highlighter.ts
|
|
15
|
+
import kleur from "kleur";
|
|
16
|
+
var highlighter = {
|
|
17
|
+
error: kleur.red,
|
|
18
|
+
warn: kleur.yellow,
|
|
19
|
+
info: kleur.cyan,
|
|
20
|
+
success: kleur.green,
|
|
21
|
+
create: kleur.blue,
|
|
22
|
+
mute: kleur.dim,
|
|
23
|
+
magenta: kleur.magenta
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// src/utils/logger.ts
|
|
27
|
+
var logger = {
|
|
28
|
+
error(...args) {
|
|
29
|
+
console.log(highlighter.error(args.join(" ")));
|
|
30
|
+
},
|
|
31
|
+
warn(...args) {
|
|
32
|
+
console.log(highlighter.warn(args.join(" ")));
|
|
33
|
+
},
|
|
34
|
+
info(...args) {
|
|
35
|
+
console.log(highlighter.info(args.join(" ")));
|
|
36
|
+
},
|
|
37
|
+
success(...args) {
|
|
38
|
+
console.log(highlighter.success(args.join(" ")));
|
|
39
|
+
},
|
|
40
|
+
log(...args) {
|
|
41
|
+
console.log(args.join(" "));
|
|
42
|
+
},
|
|
43
|
+
break() {
|
|
44
|
+
console.log("");
|
|
45
|
+
},
|
|
46
|
+
section: (title) => {
|
|
47
|
+
console.log("\n" + title);
|
|
48
|
+
},
|
|
49
|
+
muted: (msg) => console.log(highlighter.mute(msg)),
|
|
50
|
+
create: (msg) => console.log(`${highlighter.create("CREATE:")} ${msg}`),
|
|
51
|
+
skip: (msg) => console.log(`${highlighter.warn("SKIP:")} ${msg} (exists)`),
|
|
52
|
+
overwrite: (msg) => console.log(`${highlighter.info("OVERWRITE:")} ${msg}`)
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// src/utils/file.ts
|
|
56
|
+
function findFilesByPath(component, templatePath, selectedProvider) {
|
|
57
|
+
const parts = templatePath.split("/");
|
|
58
|
+
const [type] = parts;
|
|
59
|
+
if (type === "tooling" && "templates" in component) {
|
|
60
|
+
const templates = component.templates;
|
|
61
|
+
for (const tmpl of Object.values(templates || {})) {
|
|
62
|
+
if (tmpl.files) return tmpl.files;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
} else {
|
|
66
|
+
const [runtime, framework, type2] = parts;
|
|
67
|
+
const archKey = parts[parts.length - 1];
|
|
68
|
+
if (!("runtimes" in component)) return null;
|
|
69
|
+
const runtimes = component.runtimes;
|
|
70
|
+
const fw = runtimes[runtime]?.frameworks?.[framework];
|
|
71
|
+
if (!fw) return null;
|
|
72
|
+
if (fw.architectures && fw.architectures[archKey]) {
|
|
73
|
+
return fw.architectures[archKey].files;
|
|
74
|
+
}
|
|
75
|
+
if (fw.variants && selectedProvider !== void 0) {
|
|
76
|
+
return fw?.variants[selectedProvider]?.architectures[archKey].files;
|
|
77
|
+
}
|
|
78
|
+
if (fw.databases) {
|
|
79
|
+
if (type2 === "blueprint") {
|
|
80
|
+
const [, , , db, orm, arch] = parts;
|
|
81
|
+
const database = fw.databases?.[db];
|
|
82
|
+
if (!database) return null;
|
|
83
|
+
const ormConfig = database.orms?.[orm];
|
|
84
|
+
if (!ormConfig || !ormConfig.architectures) return null;
|
|
85
|
+
const architecture = ormConfig.architectures?.[arch];
|
|
86
|
+
if (!architecture) return null;
|
|
87
|
+
return architecture.files ?? null;
|
|
88
|
+
} else if (type2 === "schema") {
|
|
89
|
+
const [, , , db, orm, variant, arch] = parts;
|
|
90
|
+
const database = fw.databases?.[db];
|
|
91
|
+
if (!database) return null;
|
|
92
|
+
const ormConfig = database.orms?.[orm];
|
|
93
|
+
if (!ormConfig || !ormConfig.templates) return null;
|
|
94
|
+
const template = ormConfig.templates?.[variant];
|
|
95
|
+
if (!template) return null;
|
|
96
|
+
const architecture = template.architectures?.[arch];
|
|
97
|
+
if (!architecture) return null;
|
|
98
|
+
return architecture.files ?? null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/utils/normalize-eol.ts
|
|
106
|
+
function normalizeEol(content) {
|
|
107
|
+
return content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/lib/merge-marker.ts
|
|
111
|
+
function escapeRegExp(s) {
|
|
112
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
113
|
+
}
|
|
114
|
+
function markerBeginLine(slug) {
|
|
115
|
+
return `// @servercn:begin ${slug}`;
|
|
116
|
+
}
|
|
117
|
+
function markerEndLine(slug) {
|
|
118
|
+
return `// @servercn:end ${slug}`;
|
|
119
|
+
}
|
|
120
|
+
function isMergeOnlyFragment(content, slug) {
|
|
121
|
+
const n = normalizeEol(content).trim();
|
|
122
|
+
const begin = markerBeginLine(slug);
|
|
123
|
+
const end = markerEndLine(slug);
|
|
124
|
+
if (!n.startsWith(`${begin}
|
|
125
|
+
`) || !n.endsWith(end)) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
const inner = n.slice(begin.length + 1, n.length - end.length).trimEnd();
|
|
129
|
+
return !inner.includes("\n// @servercn:begin ");
|
|
130
|
+
}
|
|
131
|
+
function extractMarkerInner(content, slug) {
|
|
132
|
+
const n = normalizeEol(content).trim();
|
|
133
|
+
const begin = markerBeginLine(slug);
|
|
134
|
+
const end = markerEndLine(slug);
|
|
135
|
+
const re = new RegExp(
|
|
136
|
+
`^${escapeRegExp(begin)}\\s*\\n([\\s\\S]*?)\\n${escapeRegExp(end)}\\s*$`
|
|
137
|
+
);
|
|
138
|
+
const m = n.match(re);
|
|
139
|
+
return m ? m[1] : null;
|
|
140
|
+
}
|
|
141
|
+
function applyMarkerMerge(dest, template, slug) {
|
|
142
|
+
const inner = extractMarkerInner(template, slug);
|
|
143
|
+
if (inner === null) {
|
|
144
|
+
return { ok: false, reason: "missing_marker_in_template" };
|
|
145
|
+
}
|
|
146
|
+
const normalizedDest = normalizeEol(dest);
|
|
147
|
+
const begin = markerBeginLine(slug);
|
|
148
|
+
const end = markerEndLine(slug);
|
|
149
|
+
const blockRe = new RegExp(
|
|
150
|
+
`${escapeRegExp(begin)}\\s*\\n([\\s\\S]*?)\\n${escapeRegExp(end)}`,
|
|
151
|
+
"m"
|
|
152
|
+
);
|
|
153
|
+
if (!blockRe.test(normalizedDest)) {
|
|
154
|
+
return { ok: false, reason: "missing_marker_in_dest" };
|
|
155
|
+
}
|
|
156
|
+
const next = normalizedDest.replace(
|
|
157
|
+
blockRe,
|
|
158
|
+
`${begin}
|
|
159
|
+
${inner}
|
|
160
|
+
${end}`
|
|
161
|
+
);
|
|
162
|
+
return { ok: true, content: next };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/lib/copy.ts
|
|
166
|
+
async function copyTemplate({
|
|
167
|
+
templateDir,
|
|
168
|
+
targetDir,
|
|
169
|
+
registryItemName,
|
|
170
|
+
conflict = "skip",
|
|
171
|
+
dryRun = false,
|
|
172
|
+
merge = false
|
|
173
|
+
}) {
|
|
174
|
+
await fs.ensureDir(targetDir);
|
|
175
|
+
const entries = await fs.readdir(templateDir, { withFileTypes: true });
|
|
176
|
+
for (const entry of entries) {
|
|
177
|
+
const srcPath = path.join(templateDir, entry.name);
|
|
178
|
+
const rawName = entry.name === "_gitignore" ? ".gitignore" : entry.name;
|
|
179
|
+
const finalName = rawName;
|
|
180
|
+
const destPath = path.join(targetDir, finalName);
|
|
181
|
+
const relativeDestPath = path.relative(process.cwd(), destPath);
|
|
182
|
+
if (entry.isDirectory()) {
|
|
183
|
+
await copyTemplate({
|
|
184
|
+
templateDir: srcPath,
|
|
185
|
+
targetDir: destPath,
|
|
186
|
+
registryItemName,
|
|
187
|
+
conflict,
|
|
188
|
+
dryRun,
|
|
189
|
+
merge
|
|
190
|
+
});
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const exists = await fs.pathExists(destPath);
|
|
194
|
+
if (exists) {
|
|
195
|
+
if (conflict === "skip") {
|
|
196
|
+
if (merge && registryItemName) {
|
|
197
|
+
const peek = await fs.readFile(srcPath);
|
|
198
|
+
if (!peek.includes(0)) {
|
|
199
|
+
const srcText = normalizeEol(peek.toString("utf8"));
|
|
200
|
+
if (isMergeOnlyFragment(srcText, registryItemName)) {
|
|
201
|
+
const destText = normalizeEol(
|
|
202
|
+
await fs.readFile(destPath, "utf8")
|
|
203
|
+
);
|
|
204
|
+
const merged = applyMarkerMerge(
|
|
205
|
+
destText,
|
|
206
|
+
srcText,
|
|
207
|
+
registryItemName
|
|
208
|
+
);
|
|
209
|
+
if (!merged.ok) {
|
|
210
|
+
logger.error(
|
|
211
|
+
`Merge failed for ${relativeDestPath}: destination is missing // @servercn:begin/end ${registryItemName} markers. Add them or use --force.`
|
|
212
|
+
);
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
if (!dryRun) {
|
|
216
|
+
await fs.writeFile(destPath, merged.content, "utf8");
|
|
217
|
+
logger.info(`MERGE: ${relativeDestPath}`);
|
|
218
|
+
} else {
|
|
219
|
+
logger.info(`[dry-run] merge: ${relativeDestPath}`);
|
|
220
|
+
}
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
logger.skip(relativeDestPath);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (conflict === "error") {
|
|
229
|
+
throw new Error(`File already exists: ${relativeDestPath}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (dryRun) {
|
|
233
|
+
logger.info(
|
|
234
|
+
`[dry-run] ${exists ? "overwrite" : "create"}: ${relativeDestPath}`
|
|
235
|
+
);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const buffer = await fs.readFile(srcPath);
|
|
239
|
+
const isBinary = buffer.includes(0);
|
|
240
|
+
if (!exists && !isBinary && merge && registryItemName && isMergeOnlyFragment(normalizeEol(buffer.toString("utf8")), registryItemName)) {
|
|
241
|
+
logger.muted(
|
|
242
|
+
`SKIP (merge-only fragment, target missing): ${relativeDestPath}`
|
|
243
|
+
);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
await fs.ensureDir(path.dirname(destPath));
|
|
247
|
+
if (isBinary) {
|
|
248
|
+
await fs.copyFile(srcPath, destPath);
|
|
249
|
+
} else {
|
|
250
|
+
const content = normalizeEol(buffer.toString("utf8"));
|
|
251
|
+
await fs.writeFile(destPath, content, "utf8");
|
|
252
|
+
}
|
|
253
|
+
if (exists) {
|
|
254
|
+
logger.overwrite(relativeDestPath);
|
|
255
|
+
} else {
|
|
256
|
+
logger.create(relativeDestPath);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async function cloneServercnRegistry({
|
|
261
|
+
component,
|
|
262
|
+
templatePath,
|
|
263
|
+
targetDir,
|
|
264
|
+
selectedProvider,
|
|
265
|
+
options
|
|
266
|
+
}) {
|
|
267
|
+
logger.break();
|
|
268
|
+
try {
|
|
269
|
+
const files = findFilesByPath(component, templatePath, selectedProvider);
|
|
270
|
+
if (!files || files.length === 0) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
const slug = "slug" in component && typeof component.slug === "string" ? component.slug : "";
|
|
274
|
+
const useMerge = Boolean(options.merge && !options.force && slug);
|
|
275
|
+
for (const file of files) {
|
|
276
|
+
const destPath = path.join(targetDir, file.path);
|
|
277
|
+
const exists = await fs.pathExists(destPath);
|
|
278
|
+
const templateContent = normalizeEol(file.content);
|
|
279
|
+
if (options.force) {
|
|
280
|
+
await fs.ensureDir(path.dirname(destPath));
|
|
281
|
+
await fs.writeFile(destPath, templateContent, "utf8");
|
|
282
|
+
if (exists) {
|
|
283
|
+
logger.overwrite(file.path);
|
|
284
|
+
} else {
|
|
285
|
+
logger.create(file.path);
|
|
286
|
+
}
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (useMerge && isMergeOnlyFragment(templateContent, slug)) {
|
|
290
|
+
if (!exists) {
|
|
291
|
+
logger.muted(
|
|
292
|
+
`SKIP (merge-only fragment, target missing): ${file.path}`
|
|
293
|
+
);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
const destText = normalizeEol(await fs.readFile(destPath, "utf8"));
|
|
297
|
+
const merged = applyMarkerMerge(destText, templateContent, slug);
|
|
298
|
+
if (!merged.ok) {
|
|
299
|
+
logger.error(
|
|
300
|
+
`Merge failed for ${file.path}: destination is missing // @servercn:begin/end ${slug} markers. Add them or use --force.`
|
|
301
|
+
);
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
await fs.ensureDir(path.dirname(destPath));
|
|
305
|
+
await fs.writeFile(destPath, merged.content, "utf8");
|
|
306
|
+
logger.info(`MERGE: ${file.path}`);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (exists) {
|
|
310
|
+
logger.skip(file.path);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
await fs.ensureDir(path.dirname(destPath));
|
|
314
|
+
await fs.writeFile(destPath, templateContent, "utf8");
|
|
315
|
+
logger.create(file.path);
|
|
316
|
+
}
|
|
317
|
+
return true;
|
|
318
|
+
} catch {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/lib/registry.ts
|
|
324
|
+
import fs4 from "fs-extra";
|
|
325
|
+
import path4 from "path";
|
|
326
|
+
|
|
327
|
+
// src/lib/paths.ts
|
|
328
|
+
import path2 from "path";
|
|
329
|
+
import { fileURLToPath } from "url";
|
|
330
|
+
import fs2 from "fs";
|
|
331
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
332
|
+
var __dirname = path2.dirname(__filename);
|
|
333
|
+
function getMonorepoRoot() {
|
|
334
|
+
let current = __dirname;
|
|
335
|
+
while (current !== path2.parse(current).root) {
|
|
336
|
+
if (fs2.existsSync(path2.join(current, "packages")) && fs2.existsSync(path2.join(current, "apps"))) {
|
|
337
|
+
return current;
|
|
338
|
+
}
|
|
339
|
+
current = path2.join(current, "..");
|
|
340
|
+
}
|
|
341
|
+
return path2.resolve(
|
|
342
|
+
__dirname,
|
|
343
|
+
__dirname.includes("dist") ? "../../" : "../../../../"
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
function resolveTargetDir(folderName) {
|
|
347
|
+
const cwd = process.cwd();
|
|
348
|
+
return path2.join(cwd, folderName);
|
|
349
|
+
}
|
|
350
|
+
var paths = {
|
|
351
|
+
root: getMonorepoRoot(),
|
|
352
|
+
// Registry-build related paths
|
|
353
|
+
registryBase: path2.join(getMonorepoRoot(), "packages/registry"),
|
|
354
|
+
templateBase: path2.join(getMonorepoRoot(), "packages/templates"),
|
|
355
|
+
outputBase: path2.join(getMonorepoRoot(), "apps/web/public/sr"),
|
|
356
|
+
localRegistry: (f) => path2.join(getMonorepoRoot(), "packages/registry", f ? `${f}` : ""),
|
|
357
|
+
remoteRegistry: path2.join(
|
|
358
|
+
getMonorepoRoot(),
|
|
359
|
+
"apps/web/public/sr",
|
|
360
|
+
"index.json"
|
|
361
|
+
),
|
|
362
|
+
templates: () => path2.join(getMonorepoRoot(), "packages/templates"),
|
|
363
|
+
targets: (folderName) => resolveTargetDir(folderName)
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// src/utils/capitalize.ts
|
|
367
|
+
function capitalize(name = "") {
|
|
368
|
+
return name?.split("")[0]?.toUpperCase() + name.split("")?.slice(1)?.join("")?.toLowerCase();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// package.json
|
|
372
|
+
var package_default = {
|
|
373
|
+
name: "@quyentran93/servercn-cli",
|
|
374
|
+
version: "1.1.10",
|
|
375
|
+
description: "Backend components CLI for Node.js & Typescript",
|
|
376
|
+
main: "dist/cli.js",
|
|
377
|
+
readme: "README.md",
|
|
378
|
+
bin: {
|
|
379
|
+
servercn: "dist/cli.js"
|
|
380
|
+
},
|
|
381
|
+
scripts: {
|
|
382
|
+
dev: "tsup --watch",
|
|
383
|
+
build: "tsup",
|
|
384
|
+
typecheck: "tsc --noEmit",
|
|
385
|
+
"test:merge-marker": "tsx src/lib/merge-marker.selftest.ts",
|
|
386
|
+
prepublishOnly: "npm run build",
|
|
387
|
+
pub: "npm publish"
|
|
388
|
+
},
|
|
389
|
+
keywords: [
|
|
390
|
+
"servercn",
|
|
391
|
+
"cli",
|
|
392
|
+
"backend",
|
|
393
|
+
"typescript",
|
|
394
|
+
"node.js",
|
|
395
|
+
"express",
|
|
396
|
+
"nodejs",
|
|
397
|
+
"scaffold",
|
|
398
|
+
"boilerplate",
|
|
399
|
+
"component"
|
|
400
|
+
],
|
|
401
|
+
author: {
|
|
402
|
+
name: "Akkal Dhami",
|
|
403
|
+
github: "https://github.com/QuyenTran93",
|
|
404
|
+
url: "https://x.com/AavashDhami2127"
|
|
405
|
+
},
|
|
406
|
+
license: "MIT",
|
|
407
|
+
files: [
|
|
408
|
+
"dist",
|
|
409
|
+
"README.md"
|
|
410
|
+
],
|
|
411
|
+
repository: {
|
|
412
|
+
type: "git",
|
|
413
|
+
url: "git+https://github.com/QuyenTran93/servercn.git",
|
|
414
|
+
directory: "packages/cli"
|
|
415
|
+
},
|
|
416
|
+
type: "module",
|
|
417
|
+
dependencies: {
|
|
418
|
+
"cli-table3": "^0.6.5",
|
|
419
|
+
commander: "^14.0.2",
|
|
420
|
+
execa: "^9.6.1",
|
|
421
|
+
"fs-extra": "^11.3.3",
|
|
422
|
+
glob: "^10.5.0",
|
|
423
|
+
kleur: "^3.0.3",
|
|
424
|
+
ora: "^9.3.0",
|
|
425
|
+
prompts: "^2.4.2"
|
|
426
|
+
},
|
|
427
|
+
devDependencies: {
|
|
428
|
+
"@types/fs-extra": "^11.0.4",
|
|
429
|
+
"@types/node": "^25.0.3",
|
|
430
|
+
"@types/prompts": "^2.4.9",
|
|
431
|
+
tsup: "^8.5.1",
|
|
432
|
+
tsx: "^4.21.0",
|
|
433
|
+
typescript: "^5.9.3"
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
// src/constants/app.constants.ts
|
|
438
|
+
var SERVERCN_URL = "https://servercn.vercel.app";
|
|
439
|
+
var SERVERCN_CONFIG_FILE = "servercn.config.json";
|
|
440
|
+
var APP_NAME = "servercn";
|
|
441
|
+
var LATEST_VERSION = package_default.version || "1.0.0";
|
|
442
|
+
var RegistryTypeList = [
|
|
443
|
+
"component",
|
|
444
|
+
"blueprint",
|
|
445
|
+
"schema",
|
|
446
|
+
"foundation",
|
|
447
|
+
"tooling"
|
|
448
|
+
];
|
|
449
|
+
|
|
450
|
+
// src/lib/registry-list.ts
|
|
451
|
+
import fs3 from "fs-extra";
|
|
452
|
+
import path3 from "path";
|
|
453
|
+
async function loadRegistryItems(type, local = false) {
|
|
454
|
+
if (local) {
|
|
455
|
+
const registryDir = paths.localRegistry(type);
|
|
456
|
+
const files = await fs3.readdir(registryDir);
|
|
457
|
+
const items = [];
|
|
458
|
+
for (const file of files) {
|
|
459
|
+
let nestedFiles = [];
|
|
460
|
+
if (!file.endsWith(".json")) {
|
|
461
|
+
nestedFiles = await fs3.readdir(path3.join(registryDir, file));
|
|
462
|
+
for (const nestedFile of nestedFiles) {
|
|
463
|
+
if (!nestedFile.endsWith(".json")) continue;
|
|
464
|
+
const fullPath = path3.join(registryDir, file, nestedFile);
|
|
465
|
+
const data = await fs3.readJSON(fullPath);
|
|
466
|
+
items.push(data);
|
|
467
|
+
}
|
|
468
|
+
} else {
|
|
469
|
+
const fullPath = path3.join(registryDir, file);
|
|
470
|
+
const data = await fs3.readJSON(fullPath);
|
|
471
|
+
items.push(data);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const mappedItems = items.map((item) => {
|
|
475
|
+
return {
|
|
476
|
+
slug: item.slug,
|
|
477
|
+
type
|
|
478
|
+
};
|
|
479
|
+
});
|
|
480
|
+
return mappedItems;
|
|
481
|
+
} else {
|
|
482
|
+
const url = `${SERVERCN_URL}/sr/index.json`;
|
|
483
|
+
try {
|
|
484
|
+
const response = await fetch(url);
|
|
485
|
+
if (!response.ok) {
|
|
486
|
+
if (response.status === 404) {
|
|
487
|
+
logger.error(`
|
|
488
|
+
${capitalize(type)} not found in registry.
|
|
489
|
+
`);
|
|
490
|
+
} else {
|
|
491
|
+
logger.error(
|
|
492
|
+
`
|
|
493
|
+
Failed to fetch registry item: ${response.statusText}`
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
process.exit(1);
|
|
497
|
+
}
|
|
498
|
+
const data = await response.json();
|
|
499
|
+
const mappedItems = data.items.filter((item) => item.type === type);
|
|
500
|
+
return mappedItems;
|
|
501
|
+
} catch {
|
|
502
|
+
logger.error(`
|
|
503
|
+
Failed to fetch registry item
|
|
504
|
+
`);
|
|
505
|
+
process.exit(1);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// src/commands/list/list.handlers.ts
|
|
511
|
+
import Table from "cli-table3";
|
|
512
|
+
async function listOverview(options) {
|
|
513
|
+
const components = await loadRegistryItems("component", options.local);
|
|
514
|
+
const blueprints = await loadRegistryItems("blueprint", options.local);
|
|
515
|
+
const foundations = await loadRegistryItems("foundation", options.local);
|
|
516
|
+
const toolings = await loadRegistryItems("tooling", options.local);
|
|
517
|
+
const schemas = await loadRegistryItems("schema", options.local);
|
|
518
|
+
if (options.all) {
|
|
519
|
+
return await getRegistryLists("blueprint", options);
|
|
520
|
+
}
|
|
521
|
+
const data = {
|
|
522
|
+
command: "npx servercn-cli list <type>",
|
|
523
|
+
types: [
|
|
524
|
+
{
|
|
525
|
+
type: "component",
|
|
526
|
+
alias: "cp",
|
|
527
|
+
total: components.length,
|
|
528
|
+
command: "npx servercn-cli list cp"
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
type: "blueprint",
|
|
532
|
+
alias: "bp",
|
|
533
|
+
total: blueprints.length,
|
|
534
|
+
command: "npx servercn-cli list bp"
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
type: "foundation",
|
|
538
|
+
alias: "fd",
|
|
539
|
+
total: foundations.length,
|
|
540
|
+
command: "npx servercn-cli list fd"
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
type: "tooling",
|
|
544
|
+
alias: "tl",
|
|
545
|
+
total: toolings.length,
|
|
546
|
+
command: "npx servercn-cli list tl"
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
type: "schema",
|
|
550
|
+
alias: "sc",
|
|
551
|
+
total: schemas.length,
|
|
552
|
+
command: "npx servercn-cli list sc"
|
|
553
|
+
}
|
|
554
|
+
]
|
|
555
|
+
};
|
|
556
|
+
const table = new Table({
|
|
557
|
+
head: [
|
|
558
|
+
highlighter.error("type"),
|
|
559
|
+
highlighter.error("total"),
|
|
560
|
+
highlighter.error("alias"),
|
|
561
|
+
highlighter.error("command")
|
|
562
|
+
],
|
|
563
|
+
colWidths: [12, 8, 8, 28]
|
|
564
|
+
});
|
|
565
|
+
data.types.forEach((type) => {
|
|
566
|
+
table.push([
|
|
567
|
+
highlighter.create(type.type),
|
|
568
|
+
type.total,
|
|
569
|
+
highlighter.warn(type.alias),
|
|
570
|
+
highlighter.info(type.command)
|
|
571
|
+
]);
|
|
572
|
+
});
|
|
573
|
+
if (options?.json) {
|
|
574
|
+
logger.break();
|
|
575
|
+
process.stdout.write(JSON.stringify(data, null, 2));
|
|
576
|
+
logger.break();
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
logger.break();
|
|
580
|
+
logger.log(table.toString());
|
|
581
|
+
logger.log(`
|
|
582
|
+
${highlighter.create("Explore:")}
|
|
583
|
+
npx servercn-cli ls <type | alias>
|
|
584
|
+
npx servercn-cli ls <type | alias> --json
|
|
585
|
+
|
|
586
|
+
${highlighter.create("Examples:")}
|
|
587
|
+
npx servercn-cli ls component
|
|
588
|
+
npx servercn-cli ls cp
|
|
589
|
+
npx servercn-cli ls foundation
|
|
590
|
+
npx servercn-cli ls fd --json
|
|
591
|
+
npx servercn-cli ls schema
|
|
592
|
+
npx servercn-cli ls sc --json
|
|
593
|
+
`);
|
|
594
|
+
logger.break();
|
|
595
|
+
}
|
|
596
|
+
async function listComponents(options) {
|
|
597
|
+
const components = await loadRegistryItems(
|
|
598
|
+
"component",
|
|
599
|
+
options.local
|
|
600
|
+
);
|
|
601
|
+
const data = {
|
|
602
|
+
type: "component",
|
|
603
|
+
command: `npx servercn-cli add <component-name>`,
|
|
604
|
+
total: components.length,
|
|
605
|
+
items: components.map((c) => ({
|
|
606
|
+
name: c.slug,
|
|
607
|
+
command: `npx servercn-cli add ${c.slug}`,
|
|
608
|
+
...c?.frameworks && c.frameworks.length > 0 && { framework: c.frameworks }
|
|
609
|
+
}))
|
|
610
|
+
};
|
|
611
|
+
if (options?.json) {
|
|
612
|
+
process.stdout.write(JSON.stringify(data, null, 2));
|
|
613
|
+
logger.break();
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
const table = new Table({
|
|
617
|
+
head: [
|
|
618
|
+
highlighter.create("s.no"),
|
|
619
|
+
highlighter.create("name"),
|
|
620
|
+
highlighter.create("command"),
|
|
621
|
+
highlighter.create("frameworks")
|
|
622
|
+
],
|
|
623
|
+
colWidths: [6, 26, 46, 26]
|
|
624
|
+
});
|
|
625
|
+
logger.break();
|
|
626
|
+
logger.log(highlighter.create("Available Component"));
|
|
627
|
+
components.map((c, i) => {
|
|
628
|
+
table.push([
|
|
629
|
+
i + 1,
|
|
630
|
+
c.slug,
|
|
631
|
+
`npx servercn-cli add ${c.slug}`,
|
|
632
|
+
c?.frameworks && c.frameworks.join(", ") || ""
|
|
633
|
+
]);
|
|
634
|
+
});
|
|
635
|
+
logger.log(table.toString());
|
|
636
|
+
logger.info(` Learn more: ${SERVERCN_URL}/components`);
|
|
637
|
+
logger.break();
|
|
638
|
+
}
|
|
639
|
+
async function listFoundations(options) {
|
|
640
|
+
const foundations = await loadRegistryItems("foundation", options.local);
|
|
641
|
+
const data = {
|
|
642
|
+
type: "foundation",
|
|
643
|
+
command: `npx servercn-cli init <foundation-name>`,
|
|
644
|
+
total: foundations.length,
|
|
645
|
+
items: foundations.sort((a, b) => a.slug.localeCompare(b.slug)).map((c) => ({
|
|
646
|
+
name: c.slug,
|
|
647
|
+
command: `npx servercn-cli init ${c.slug}`,
|
|
648
|
+
...c?.frameworks && c.frameworks.length > 0 && { frameworks: c.frameworks }
|
|
649
|
+
}))
|
|
650
|
+
};
|
|
651
|
+
if (options?.json) {
|
|
652
|
+
process.stdout.write(JSON.stringify(data, null, 2));
|
|
653
|
+
logger.break();
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const table = new Table({
|
|
657
|
+
head: [
|
|
658
|
+
highlighter.create("s.no"),
|
|
659
|
+
highlighter.create("name"),
|
|
660
|
+
highlighter.create("command"),
|
|
661
|
+
highlighter.create("frameworks")
|
|
662
|
+
],
|
|
663
|
+
colWidths: [6, 26, 46, 26]
|
|
664
|
+
});
|
|
665
|
+
logger.break();
|
|
666
|
+
logger.log(highlighter.create("Available Foundation"));
|
|
667
|
+
foundations.map((c, i) => {
|
|
668
|
+
table.push([
|
|
669
|
+
i + 1,
|
|
670
|
+
c.slug,
|
|
671
|
+
`npx servercn-cli init ${c.slug}`,
|
|
672
|
+
c?.frameworks && c.frameworks.join(", ") || ""
|
|
673
|
+
]);
|
|
674
|
+
});
|
|
675
|
+
logger.log(table.toString());
|
|
676
|
+
logger.info(`Learn more: ${SERVERCN_URL}/foundations`);
|
|
677
|
+
logger.break();
|
|
678
|
+
}
|
|
679
|
+
async function listTooling(options) {
|
|
680
|
+
const toolings = await loadRegistryItems("tooling", options.local);
|
|
681
|
+
const data = {
|
|
682
|
+
type: "tooling",
|
|
683
|
+
alias: "tl",
|
|
684
|
+
command: `npx servercn-cli add tooling <tooling-name>`,
|
|
685
|
+
total: toolings.length,
|
|
686
|
+
items: toolings.map((c) => ({
|
|
687
|
+
name: c.slug,
|
|
688
|
+
command: `npx servercn-cli add tl ${c.slug}`
|
|
689
|
+
}))
|
|
690
|
+
};
|
|
691
|
+
if (options?.json) {
|
|
692
|
+
process.stdout.write(JSON.stringify(data, null, 2));
|
|
693
|
+
logger.break();
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
const table = new Table({
|
|
697
|
+
head: [
|
|
698
|
+
highlighter.create("s.no"),
|
|
699
|
+
highlighter.create("name"),
|
|
700
|
+
highlighter.create("command")
|
|
701
|
+
],
|
|
702
|
+
colWidths: [6, 26, 46]
|
|
703
|
+
});
|
|
704
|
+
logger.break();
|
|
705
|
+
logger.log(highlighter.create("Available Tooling"));
|
|
706
|
+
toolings.map((c, i) => {
|
|
707
|
+
table.push([i + 1, c.slug, `npx servercn-cli add tl ${c.slug}`]);
|
|
708
|
+
});
|
|
709
|
+
logger.log(table.toString());
|
|
710
|
+
logger.info(`Learn more: ${SERVERCN_URL}/docs`);
|
|
711
|
+
logger.break();
|
|
712
|
+
}
|
|
713
|
+
async function listSchemas(options) {
|
|
714
|
+
const schemas = await loadRegistryItems("schema", options.local);
|
|
715
|
+
const data = {
|
|
716
|
+
type: "schema",
|
|
717
|
+
alias: "sc",
|
|
718
|
+
command: `npx servercn-cli add schema <schema-name>`,
|
|
719
|
+
total: schemas.length,
|
|
720
|
+
items: schemas.sort((a, b) => a.slug.localeCompare(b.slug)).map((c) => ({
|
|
721
|
+
name: c.slug,
|
|
722
|
+
command: `npx servercn-cli add sc ${c.slug}`,
|
|
723
|
+
...c?.frameworks && c.frameworks.length > 0 && { frameworks: c.frameworks }
|
|
724
|
+
}))
|
|
725
|
+
};
|
|
726
|
+
if (options?.json) {
|
|
727
|
+
process.stdout.write(JSON.stringify(data, null, 2));
|
|
728
|
+
logger.break();
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const table = new Table({
|
|
732
|
+
head: [
|
|
733
|
+
highlighter.create("s.no"),
|
|
734
|
+
highlighter.create("name"),
|
|
735
|
+
highlighter.create("command"),
|
|
736
|
+
highlighter.create("frameworks")
|
|
737
|
+
],
|
|
738
|
+
colWidths: [6, 26, 46, 26]
|
|
739
|
+
});
|
|
740
|
+
logger.break();
|
|
741
|
+
logger.log(highlighter.create("Available Schemas"));
|
|
742
|
+
schemas.map((c, i) => {
|
|
743
|
+
table.push([
|
|
744
|
+
i + 1,
|
|
745
|
+
c.slug,
|
|
746
|
+
`npx servercn-cli add sc ${c.slug}`,
|
|
747
|
+
c?.frameworks && c.frameworks.join(", ") || ""
|
|
748
|
+
]);
|
|
749
|
+
});
|
|
750
|
+
logger.log(table.toString());
|
|
751
|
+
logger.info(`Learn more: ${SERVERCN_URL}/schemas`);
|
|
752
|
+
logger.break();
|
|
753
|
+
}
|
|
754
|
+
async function listBlueprints(options) {
|
|
755
|
+
const blueprints = await loadRegistryItems("blueprint", options.local);
|
|
756
|
+
const data = {
|
|
757
|
+
type: "blueprint",
|
|
758
|
+
alias: "bp",
|
|
759
|
+
command: `npx servercn-cli add blueprint <blueprint-name>`,
|
|
760
|
+
total: blueprints.length,
|
|
761
|
+
items: blueprints.map((c) => ({
|
|
762
|
+
name: c.slug,
|
|
763
|
+
command: `npx servercn-cli add bp ${c.slug}`,
|
|
764
|
+
...c?.frameworks && c.frameworks.length > 0 && { frameworks: c.frameworks }
|
|
765
|
+
}))
|
|
766
|
+
};
|
|
767
|
+
if (options?.json) {
|
|
768
|
+
process.stdout.write(JSON.stringify(data, null, 2));
|
|
769
|
+
logger.break();
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
const table = new Table({
|
|
773
|
+
head: [
|
|
774
|
+
highlighter.create("s.no"),
|
|
775
|
+
highlighter.create("name"),
|
|
776
|
+
highlighter.create("command"),
|
|
777
|
+
highlighter.create("frameworks")
|
|
778
|
+
],
|
|
779
|
+
colWidths: [6, 26, 46, 26]
|
|
780
|
+
});
|
|
781
|
+
logger.break();
|
|
782
|
+
logger.log(highlighter.create("Available Blueprints"));
|
|
783
|
+
blueprints.map((c, i) => {
|
|
784
|
+
table.push([
|
|
785
|
+
i + 1,
|
|
786
|
+
c.slug,
|
|
787
|
+
`npx servercn-cli add bp ${c.slug}`,
|
|
788
|
+
c?.frameworks && c.frameworks.join(", ") || ""
|
|
789
|
+
]);
|
|
790
|
+
});
|
|
791
|
+
logger.log(table.toString());
|
|
792
|
+
logger.info(`Learn more: ${SERVERCN_URL}/blueprints`);
|
|
793
|
+
logger.break();
|
|
794
|
+
}
|
|
795
|
+
async function getRegistryLists(type, options) {
|
|
796
|
+
if (options?.all && options.json) {
|
|
797
|
+
await listComponents({ json: true, local: options.local });
|
|
798
|
+
await listSchemas({ json: true, local: options.local });
|
|
799
|
+
await listBlueprints({ json: true, local: options.local });
|
|
800
|
+
await listTooling({ json: true, local: options.local });
|
|
801
|
+
await listFoundations({ json: true, local: options.local });
|
|
802
|
+
} else if (options?.all) {
|
|
803
|
+
await listComponents({ json: false, local: options.local });
|
|
804
|
+
await listSchemas({ json: false, local: options.local });
|
|
805
|
+
await listBlueprints({ json: false, local: options.local });
|
|
806
|
+
await listTooling({ json: false, local: options.local });
|
|
807
|
+
await listFoundations({ json: false, local: options.local });
|
|
808
|
+
} else {
|
|
809
|
+
switch (type) {
|
|
810
|
+
case "component":
|
|
811
|
+
return await listComponents(options ?? { json: false });
|
|
812
|
+
case "blueprint":
|
|
813
|
+
return await listBlueprints(options ?? { json: false });
|
|
814
|
+
case "schema":
|
|
815
|
+
return await listSchemas(options ?? { json: false });
|
|
816
|
+
case "tooling":
|
|
817
|
+
return listTooling(options ?? { json: false });
|
|
818
|
+
case "foundation":
|
|
819
|
+
return await listFoundations(options ?? { json: false });
|
|
820
|
+
default:
|
|
821
|
+
return await listComponents(options ?? { json: false });
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// src/lib/registry.ts
|
|
827
|
+
async function getRegistry(name, type, local) {
|
|
828
|
+
const registryItemName = name.includes("/") ? name.split("/").shift() || name : name;
|
|
829
|
+
if (local) {
|
|
830
|
+
const registryPath = paths.localRegistry(type);
|
|
831
|
+
if (!await fs4.pathExists(registryPath)) {
|
|
832
|
+
logger.break();
|
|
833
|
+
logger.error(
|
|
834
|
+
"Something went wrong. Please check the error below for more details."
|
|
835
|
+
);
|
|
836
|
+
logger.error(`
|
|
837
|
+
Registry path not found`);
|
|
838
|
+
logger.error("\nCheck if the item name is correct.");
|
|
839
|
+
logger.break();
|
|
840
|
+
process.exit(1);
|
|
841
|
+
}
|
|
842
|
+
const filePath = path4.join(registryPath, `${registryItemName}.json`);
|
|
843
|
+
if (!await fs4.pathExists(filePath)) {
|
|
844
|
+
logger.break();
|
|
845
|
+
logger.error(
|
|
846
|
+
"Something went wrong. Please check the error below for more details."
|
|
847
|
+
);
|
|
848
|
+
logger.error(`
|
|
849
|
+
${capitalize(type)} '${name}' not found!`);
|
|
850
|
+
logger.break();
|
|
851
|
+
await getRegistryLists(type);
|
|
852
|
+
process.exit(1);
|
|
853
|
+
}
|
|
854
|
+
return fs4.readJSON(filePath);
|
|
855
|
+
} else {
|
|
856
|
+
const url = `${SERVERCN_URL}/sr/${type}/${registryItemName}.json`;
|
|
857
|
+
try {
|
|
858
|
+
const response = await fetch(url);
|
|
859
|
+
if (!response.ok) {
|
|
860
|
+
if (response.status === 404) {
|
|
861
|
+
logger.error(
|
|
862
|
+
`
|
|
863
|
+
${capitalize(type)} '${name}' not found in registry.
|
|
864
|
+
`
|
|
865
|
+
);
|
|
866
|
+
} else {
|
|
867
|
+
logger.error(
|
|
868
|
+
`
|
|
869
|
+
Failed to fetch registry item: ${response.statusText}`
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
process.exit(1);
|
|
873
|
+
}
|
|
874
|
+
return await response.json();
|
|
875
|
+
} catch {
|
|
876
|
+
logger.error(`
|
|
877
|
+
Failed to fetch registry item
|
|
878
|
+
`);
|
|
879
|
+
process.exit(1);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// src/lib/install-deps.ts
|
|
885
|
+
import { execa } from "execa";
|
|
886
|
+
|
|
887
|
+
// src/lib/detect.ts
|
|
888
|
+
import fs5 from "fs";
|
|
889
|
+
import path5 from "path";
|
|
890
|
+
function detectPackageManager(cwd = process.cwd()) {
|
|
891
|
+
if (fs5.existsSync(path5.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
892
|
+
if (fs5.existsSync(path5.join(cwd, "yarn.lock"))) return "yarn";
|
|
893
|
+
if (fs5.existsSync(path5.join(cwd, "bun.lock"))) return "bun";
|
|
894
|
+
return "npm";
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// src/utils/spinner.ts
|
|
898
|
+
import ora from "ora";
|
|
899
|
+
function spinner(text, options) {
|
|
900
|
+
return ora({
|
|
901
|
+
text,
|
|
902
|
+
isSilent: options?.silent
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// src/lib/install-deps.ts
|
|
907
|
+
var clean = (arr) => arr.filter((dep) => typeof dep === "string" && dep.trim().length > 0);
|
|
908
|
+
async function installDependencies({
|
|
909
|
+
runtime = [],
|
|
910
|
+
dev = [],
|
|
911
|
+
cwd,
|
|
912
|
+
packageManager
|
|
913
|
+
}) {
|
|
914
|
+
const runtimeDeps = clean(runtime);
|
|
915
|
+
const devDeps = clean(dev);
|
|
916
|
+
if (runtimeDeps.length === 0 && devDeps.length === 0) return;
|
|
917
|
+
if (runtimeDeps.length > 0) {
|
|
918
|
+
logger.log("\nInstalling dependencies:");
|
|
919
|
+
runtimeDeps.forEach((dep) => logger.info(`- ${dep}`));
|
|
920
|
+
}
|
|
921
|
+
if (devDeps.length > 0) {
|
|
922
|
+
logger.log("\nInstalling devDependencies:");
|
|
923
|
+
devDeps.forEach((dep) => logger.info(`- ${dep}`));
|
|
924
|
+
}
|
|
925
|
+
if (runtimeDeps.length > 0 || devDeps.length > 0) {
|
|
926
|
+
logger.break();
|
|
927
|
+
}
|
|
928
|
+
const pm = packageManager ?? detectPackageManager();
|
|
929
|
+
const run = async (packages, isDev) => {
|
|
930
|
+
const label = isDev ? "devDependencies" : "dependencies";
|
|
931
|
+
if (packages.length === 0) return;
|
|
932
|
+
const spin = spinner(`Installing ${label} with ${pm}`)?.start();
|
|
933
|
+
try {
|
|
934
|
+
await execa(pm, getInstallArgs(pm, packages, isDev), {
|
|
935
|
+
cwd,
|
|
936
|
+
stdio: "pipe"
|
|
937
|
+
});
|
|
938
|
+
spin?.succeed(`Successfully installed ${packages.length} ${label}`);
|
|
939
|
+
} catch (error) {
|
|
940
|
+
spin?.fail(`Failed to install ${label}`);
|
|
941
|
+
logger.error(error.stderr || error.message);
|
|
942
|
+
throw error;
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
await run(runtimeDeps, false);
|
|
946
|
+
logger.break();
|
|
947
|
+
await run(devDeps, true);
|
|
948
|
+
}
|
|
949
|
+
function getInstallArgs(pm, packages, isDev) {
|
|
950
|
+
switch (pm) {
|
|
951
|
+
case "pnpm":
|
|
952
|
+
return ["add", ...isDev ? ["-D"] : [], ...packages];
|
|
953
|
+
case "yarn":
|
|
954
|
+
return ["add", ...isDev ? ["-D"] : [], ...packages];
|
|
955
|
+
case "bun":
|
|
956
|
+
return ["add", ...isDev ? ["-d"] : [], ...packages];
|
|
957
|
+
case "npm":
|
|
958
|
+
default:
|
|
959
|
+
return ["install", ...isDev ? ["--save-dev"] : [], ...packages];
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// src/lib/package.ts
|
|
964
|
+
import fs6 from "fs";
|
|
965
|
+
import path6 from "path";
|
|
966
|
+
import { execSync } from "child_process";
|
|
967
|
+
function ensurePackageJson(dir) {
|
|
968
|
+
const pkgPath = path6.join(dir, "package.json");
|
|
969
|
+
if (fs6.existsSync(pkgPath)) return;
|
|
970
|
+
logger.info("Initializing package.json");
|
|
971
|
+
execSync("npm init -y", {
|
|
972
|
+
cwd: dir,
|
|
973
|
+
stdio: "ignore"
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
function ensureTsConfig(dir) {
|
|
977
|
+
const tsconfigPath = path6.join(dir, "tsconfig.json");
|
|
978
|
+
if (fs6.existsSync(tsconfigPath)) return;
|
|
979
|
+
const tsConfig2 = {
|
|
980
|
+
compilerOptions: {
|
|
981
|
+
target: "ES2021",
|
|
982
|
+
module: "es2022",
|
|
983
|
+
moduleResolution: "bundler",
|
|
984
|
+
strict: true,
|
|
985
|
+
esModuleInterop: true,
|
|
986
|
+
skipLibCheck: true,
|
|
987
|
+
outDir: "dist",
|
|
988
|
+
rootDir: "src",
|
|
989
|
+
sourceMap: true,
|
|
990
|
+
alwaysStrict: true,
|
|
991
|
+
useUnknownInCatchVariables: true,
|
|
992
|
+
forceConsistentCasingInFileNames: true,
|
|
993
|
+
paths: {
|
|
994
|
+
"@/*": ["./src/*"]
|
|
995
|
+
}
|
|
996
|
+
},
|
|
997
|
+
include: ["src/**/*"],
|
|
998
|
+
exclude: ["node_modules"]
|
|
999
|
+
};
|
|
1000
|
+
fs6.writeFileSync(tsconfigPath, JSON.stringify(tsConfig2, null, 2));
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// src/lib/assert-initialized.ts
|
|
1004
|
+
import fs7 from "fs-extra";
|
|
1005
|
+
import path7 from "path";
|
|
1006
|
+
async function assertInitialized() {
|
|
1007
|
+
const configPath = path7.resolve(process.cwd(), SERVERCN_CONFIG_FILE);
|
|
1008
|
+
if (!await fs7.pathExists(configPath)) {
|
|
1009
|
+
logger.break();
|
|
1010
|
+
logger.error(`${APP_NAME} is not initialized in this project.`);
|
|
1011
|
+
logger.break();
|
|
1012
|
+
logger.log("Run the following command first:");
|
|
1013
|
+
logger.log(`> ${highlighter.create("npx servercn-cli init")}`);
|
|
1014
|
+
logger.break();
|
|
1015
|
+
logger.log(
|
|
1016
|
+
`For express starter:
|
|
1017
|
+
> ${highlighter.create("npx servercn-cli init express-server")}`
|
|
1018
|
+
);
|
|
1019
|
+
logger.break();
|
|
1020
|
+
logger.log(
|
|
1021
|
+
`For (express + mongoose) starter:
|
|
1022
|
+
> ${highlighter.create("npx servercn-cli init mongoose-starter")}`
|
|
1023
|
+
);
|
|
1024
|
+
logger.break();
|
|
1025
|
+
logger.log(
|
|
1026
|
+
`For (drizzle + mysql) starter:
|
|
1027
|
+
> ${highlighter.create("npx servercn-cli init drizzle-mysql-starter")}`
|
|
1028
|
+
);
|
|
1029
|
+
logger.break();
|
|
1030
|
+
logger.log(
|
|
1031
|
+
`For (drizzle + postgresql) starter:
|
|
1032
|
+
> ${highlighter.create("npx servercn-cli init drizzle-pg-starter")}`
|
|
1033
|
+
);
|
|
1034
|
+
logger.break();
|
|
1035
|
+
logger.info(`Visit ${SERVERCN_URL}/docs/installation for more information`);
|
|
1036
|
+
logger.break();
|
|
1037
|
+
process.exit(1);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// src/lib/config.ts
|
|
1042
|
+
import fs8 from "fs-extra";
|
|
1043
|
+
import path8 from "path";
|
|
1044
|
+
async function getServerCNConfig() {
|
|
1045
|
+
const cwd = process.cwd();
|
|
1046
|
+
const configPath = path8.resolve(cwd, SERVERCN_CONFIG_FILE);
|
|
1047
|
+
if (!await fs8.pathExists(configPath)) {
|
|
1048
|
+
logger.warn(
|
|
1049
|
+
"\nServerCN is not initialized. Run `npx servercn-cli init` first.\n"
|
|
1050
|
+
);
|
|
1051
|
+
process.exit(1);
|
|
1052
|
+
}
|
|
1053
|
+
return fs8.readJSON(configPath);
|
|
1054
|
+
}
|
|
1055
|
+
function getDatabaseConfig(foundation) {
|
|
1056
|
+
switch (foundation) {
|
|
1057
|
+
case "mongoose-starter":
|
|
1058
|
+
return {
|
|
1059
|
+
engine: "mongodb",
|
|
1060
|
+
adapter: "mongoose"
|
|
1061
|
+
};
|
|
1062
|
+
case "drizzle-mysql-starter":
|
|
1063
|
+
return {
|
|
1064
|
+
engine: "mysql",
|
|
1065
|
+
adapter: "drizzle"
|
|
1066
|
+
};
|
|
1067
|
+
case "drizzle-pg-starter":
|
|
1068
|
+
return {
|
|
1069
|
+
engine: "postgresql",
|
|
1070
|
+
adapter: "drizzle"
|
|
1071
|
+
};
|
|
1072
|
+
case "prisma-mongodb-starter":
|
|
1073
|
+
return {
|
|
1074
|
+
engine: "mongodb",
|
|
1075
|
+
adapter: "prisma"
|
|
1076
|
+
};
|
|
1077
|
+
default:
|
|
1078
|
+
return null;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// src/commands/add/add.handlers.ts
|
|
1083
|
+
import prompts from "prompts";
|
|
1084
|
+
async function resolveTemplateResolution({
|
|
1085
|
+
registryItemName,
|
|
1086
|
+
component,
|
|
1087
|
+
config,
|
|
1088
|
+
options
|
|
1089
|
+
}) {
|
|
1090
|
+
const type = options.type || "component";
|
|
1091
|
+
const framework = config.stack.framework;
|
|
1092
|
+
const architecture = config.stack.architecture;
|
|
1093
|
+
const runtime = config.stack.runtime;
|
|
1094
|
+
const isBuilt = !options.local;
|
|
1095
|
+
if (type === "tooling") {
|
|
1096
|
+
const selectedPath = registryItemName;
|
|
1097
|
+
return {
|
|
1098
|
+
templatePath: `tooling/${selectedPath}/base`,
|
|
1099
|
+
additionalRuntimeDeps: [],
|
|
1100
|
+
additionalDevDeps: []
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
const templateConfig = component.runtimes?.[runtime]?.frameworks?.[framework];
|
|
1104
|
+
if (!templateConfig) {
|
|
1105
|
+
logger.break();
|
|
1106
|
+
logger.error(
|
|
1107
|
+
`Unsupported framework '${framework}' for ${type}: '${component.slug}'.`
|
|
1108
|
+
);
|
|
1109
|
+
logger.error(
|
|
1110
|
+
`This ${type} does not provide templates for the selected framework.`
|
|
1111
|
+
);
|
|
1112
|
+
logger.break();
|
|
1113
|
+
process.exit(1);
|
|
1114
|
+
}
|
|
1115
|
+
if (templateConfig.variants) {
|
|
1116
|
+
return resolvePromptVariants({
|
|
1117
|
+
component,
|
|
1118
|
+
runtime,
|
|
1119
|
+
architecture,
|
|
1120
|
+
framework,
|
|
1121
|
+
type,
|
|
1122
|
+
isBuilt
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
let selectedSubPath;
|
|
1126
|
+
switch (type) {
|
|
1127
|
+
case "component":
|
|
1128
|
+
case "foundation":
|
|
1129
|
+
if (isBuilt) {
|
|
1130
|
+
if (templateConfig.architectures?.[architecture]) {
|
|
1131
|
+
selectedSubPath = architecture;
|
|
1132
|
+
}
|
|
1133
|
+
} else {
|
|
1134
|
+
const haveTemplates = templateConfig.templates;
|
|
1135
|
+
selectedSubPath = typeof templateConfig === "string" ? templateConfig : haveTemplates?.[architecture];
|
|
1136
|
+
}
|
|
1137
|
+
break;
|
|
1138
|
+
case "schema":
|
|
1139
|
+
case "blueprint":
|
|
1140
|
+
selectedSubPath = resolveDatabaseTemplate({
|
|
1141
|
+
templateConfig,
|
|
1142
|
+
config,
|
|
1143
|
+
architecture,
|
|
1144
|
+
options,
|
|
1145
|
+
registryItemName: type === "blueprint" ? component.slug : registryItemName
|
|
1146
|
+
});
|
|
1147
|
+
break;
|
|
1148
|
+
default:
|
|
1149
|
+
if (!isBuilt) {
|
|
1150
|
+
const haveTemplates = templateConfig.templates;
|
|
1151
|
+
selectedSubPath = typeof templateConfig === "string" ? templateConfig : haveTemplates && templateConfig.templates[architecture];
|
|
1152
|
+
}
|
|
1153
|
+
break;
|
|
1154
|
+
}
|
|
1155
|
+
if (!selectedSubPath) {
|
|
1156
|
+
logger.break();
|
|
1157
|
+
logger.error(
|
|
1158
|
+
`Architecture '${architecture}' is not supported for '${type}:${component.slug}'.`
|
|
1159
|
+
);
|
|
1160
|
+
logger.break();
|
|
1161
|
+
process.exit(1);
|
|
1162
|
+
}
|
|
1163
|
+
let runtimeDeps = [];
|
|
1164
|
+
let devDeps = [];
|
|
1165
|
+
if (type === "schema" || type === "blueprint") {
|
|
1166
|
+
const db = config.database?.engine;
|
|
1167
|
+
const orm = config.database?.adapter;
|
|
1168
|
+
const deps = resolveDependencies({
|
|
1169
|
+
component,
|
|
1170
|
+
framework,
|
|
1171
|
+
db,
|
|
1172
|
+
orm,
|
|
1173
|
+
runtime
|
|
1174
|
+
});
|
|
1175
|
+
runtimeDeps = deps.runtime || [];
|
|
1176
|
+
devDeps = deps.dev || [];
|
|
1177
|
+
}
|
|
1178
|
+
return {
|
|
1179
|
+
templatePath: `${runtime}/${framework}/${type}/${selectedSubPath}`,
|
|
1180
|
+
additionalRuntimeDeps: runtimeDeps,
|
|
1181
|
+
additionalDevDeps: devDeps
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
function resolveDatabaseTemplate({
|
|
1185
|
+
templateConfig,
|
|
1186
|
+
config,
|
|
1187
|
+
architecture,
|
|
1188
|
+
options,
|
|
1189
|
+
registryItemName
|
|
1190
|
+
}) {
|
|
1191
|
+
const formattedRegistryItemName = registryItemName.includes("/") ? registryItemName.split("/").pop() || "index" : options.type == "schema" ? "index" : registryItemName;
|
|
1192
|
+
const dbType = config?.database?.engine;
|
|
1193
|
+
const orm = config?.database?.adapter;
|
|
1194
|
+
if (!dbType || !orm) {
|
|
1195
|
+
logger.break();
|
|
1196
|
+
logger.error(
|
|
1197
|
+
"Database or ORM not configured.\nPlease add database:type or database:orm in `servercn.config.json` file"
|
|
1198
|
+
);
|
|
1199
|
+
logger.break();
|
|
1200
|
+
process.exit(1);
|
|
1201
|
+
}
|
|
1202
|
+
const dbConfig = templateConfig?.databases[dbType];
|
|
1203
|
+
const dbOrm = dbConfig?.orms[orm];
|
|
1204
|
+
if (!dbConfig || !dbOrm) {
|
|
1205
|
+
logger.break();
|
|
1206
|
+
logger.error(
|
|
1207
|
+
`Database stack '${dbType}:${orm}' is not supported by ${options.type}:'${formattedRegistryItemName}'.`
|
|
1208
|
+
);
|
|
1209
|
+
logger.break();
|
|
1210
|
+
process.exit(1);
|
|
1211
|
+
}
|
|
1212
|
+
const archOptions = dbOrm?.templates;
|
|
1213
|
+
if (options.type === "blueprint") {
|
|
1214
|
+
const path15 = options?.local ? archOptions[architecture] : `${config.database?.engine}/${config.database?.adapter}/${config.stack.architecture}`;
|
|
1215
|
+
return path15;
|
|
1216
|
+
}
|
|
1217
|
+
if (options.type == "schema") {
|
|
1218
|
+
const path15 = options?.local ? archOptions[formattedRegistryItemName][architecture] : `${config.database?.engine}/${config.database?.adapter}/${formattedRegistryItemName}/${config.stack.architecture}`;
|
|
1219
|
+
return path15;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
async function resolvePromptVariants({
|
|
1223
|
+
component,
|
|
1224
|
+
runtime,
|
|
1225
|
+
architecture,
|
|
1226
|
+
framework,
|
|
1227
|
+
type,
|
|
1228
|
+
isBuilt
|
|
1229
|
+
}) {
|
|
1230
|
+
const variantConfig = component.runtimes[runtime].frameworks[framework];
|
|
1231
|
+
const choices = Object.entries(variantConfig?.variants || {}).map(
|
|
1232
|
+
([key, value]) => {
|
|
1233
|
+
return {
|
|
1234
|
+
title: value.label,
|
|
1235
|
+
value: key
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
);
|
|
1239
|
+
const { variant } = await prompts({
|
|
1240
|
+
type: "select",
|
|
1241
|
+
name: "variant",
|
|
1242
|
+
message: variantConfig?.prompt || "Select",
|
|
1243
|
+
choices
|
|
1244
|
+
});
|
|
1245
|
+
if (!variant) {
|
|
1246
|
+
logger.break();
|
|
1247
|
+
logger.warn("Operation cancelled.");
|
|
1248
|
+
logger.break();
|
|
1249
|
+
process.exit(0);
|
|
1250
|
+
}
|
|
1251
|
+
const selectedTemplate = isBuilt ? variantConfig?.variants?.[variant]?.architectures?.[architecture] ? architecture : "" : variantConfig?.variants?.[variant]?.templates[architecture] || "";
|
|
1252
|
+
if (!selectedTemplate) {
|
|
1253
|
+
logger.break();
|
|
1254
|
+
logger.error(
|
|
1255
|
+
`Architecture '${architecture}' is not supported for variant "${variant}".`
|
|
1256
|
+
);
|
|
1257
|
+
logger.break();
|
|
1258
|
+
process.exit(1);
|
|
1259
|
+
}
|
|
1260
|
+
const subPath = isBuilt ? `${variant}/${selectedTemplate}` : selectedTemplate;
|
|
1261
|
+
return {
|
|
1262
|
+
templatePath: `${runtime}/${framework}/${type}/${subPath}`,
|
|
1263
|
+
additionalRuntimeDeps: variantConfig?.variants?.[variant]?.dependencies?.runtime ?? [],
|
|
1264
|
+
additionalDevDeps: variantConfig?.variants?.[variant]?.dependencies?.dev ?? [],
|
|
1265
|
+
selectedProvider: variant
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
function resolveDependencies({
|
|
1269
|
+
component,
|
|
1270
|
+
framework,
|
|
1271
|
+
db,
|
|
1272
|
+
orm,
|
|
1273
|
+
runtime
|
|
1274
|
+
}) {
|
|
1275
|
+
const sets = component.runtimes[runtime].frameworks[framework].databases[db].orms[orm].dependencies;
|
|
1276
|
+
return sets;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// src/commands/add/index.ts
|
|
1280
|
+
import { execa as execa2 } from "execa";
|
|
1281
|
+
|
|
1282
|
+
// src/utils/update-env.ts
|
|
1283
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
1284
|
+
import path9 from "path";
|
|
1285
|
+
function updateEnvKeys({
|
|
1286
|
+
envFile,
|
|
1287
|
+
envKeys,
|
|
1288
|
+
cwd = process.cwd(),
|
|
1289
|
+
label
|
|
1290
|
+
}) {
|
|
1291
|
+
if (envKeys.length < 1) return;
|
|
1292
|
+
const envFilePath = path9.join(cwd, envFile);
|
|
1293
|
+
const existing = normalizeEol(
|
|
1294
|
+
existsSync(envFilePath) ? readFileSync(envFilePath, "utf8") : ""
|
|
1295
|
+
);
|
|
1296
|
+
const existingKeys = new Set(
|
|
1297
|
+
existing.split(/\r?\n/).map(
|
|
1298
|
+
(line) => line.replace(/^export\s+/, "").split("=")[0]?.trim()
|
|
1299
|
+
).filter((key) => key && !key.startsWith("#"))
|
|
1300
|
+
);
|
|
1301
|
+
const newEnvVars = envKeys.filter((key) => !existingKeys.has(key)).map((key) => `
|
|
1302
|
+
${key}='${key.split("_").join("_").toLowerCase()}'`);
|
|
1303
|
+
logger.break();
|
|
1304
|
+
if (!newEnvVars.length) {
|
|
1305
|
+
logger.log(
|
|
1306
|
+
`All env keys already exist in ${highlighter.info(envFile)} \u2014 nothing to add`
|
|
1307
|
+
);
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
const envSpinner = spinner(
|
|
1311
|
+
`Adding ${newEnvVars.length} environment key(s) to ${highlighter.info(envFile)}`
|
|
1312
|
+
);
|
|
1313
|
+
envSpinner?.start();
|
|
1314
|
+
const header = `# ${label} environment variables`;
|
|
1315
|
+
const block = `${header}
|
|
1316
|
+
` + newEnvVars.join("\n") + "\n";
|
|
1317
|
+
const content = normalizeEol(
|
|
1318
|
+
existing.trim().length > 0 ? `${existing.trim()}
|
|
1319
|
+
|
|
1320
|
+
${block}` : block
|
|
1321
|
+
);
|
|
1322
|
+
writeFileSync(envFilePath, content, "utf8");
|
|
1323
|
+
envSpinner?.succeed(`Env keys added to ${highlighter.info(envFile)}`);
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// src/utils/tooling.ts
|
|
1327
|
+
import prompts2 from "prompts";
|
|
1328
|
+
var TOOLING_MAP = {
|
|
1329
|
+
prettier: ["prettier"],
|
|
1330
|
+
eslint: [
|
|
1331
|
+
"eslint",
|
|
1332
|
+
"@typescript-eslint/parser",
|
|
1333
|
+
"@typescript-eslint/eslint-plugin",
|
|
1334
|
+
"eslint-plugin-prettier"
|
|
1335
|
+
],
|
|
1336
|
+
typescript: ["typescript", "tsx", "tsc-alias", "@types/node"],
|
|
1337
|
+
husky: ["husky"],
|
|
1338
|
+
"lint-staged": ["lint-staged"],
|
|
1339
|
+
commitlint: ["@commitlint/cli", "@commitlint/config-conventional"]
|
|
1340
|
+
};
|
|
1341
|
+
var TOOLING_CHOICES = [
|
|
1342
|
+
{ title: "Prettier", value: "prettier" },
|
|
1343
|
+
{ title: "ESLint", value: "eslint" },
|
|
1344
|
+
{ title: "TypeScript", value: "typescript" },
|
|
1345
|
+
{ title: "Husky", value: "husky" },
|
|
1346
|
+
{ title: "Lint Staged", value: "lint-staged" },
|
|
1347
|
+
{ title: "Commitlint", value: "commitlint" }
|
|
1348
|
+
];
|
|
1349
|
+
var RECOMMENDED_TOOLING = ["prettier", "eslint", "typescript"];
|
|
1350
|
+
async function getToolingChoices() {
|
|
1351
|
+
const { enable } = await prompts2({
|
|
1352
|
+
type: "toggle",
|
|
1353
|
+
name: "enable",
|
|
1354
|
+
message: "Would you like to set up development tooling?",
|
|
1355
|
+
initial: true,
|
|
1356
|
+
active: "yes",
|
|
1357
|
+
inactive: "no"
|
|
1358
|
+
});
|
|
1359
|
+
if (!enable) return [];
|
|
1360
|
+
const { mode } = await prompts2({
|
|
1361
|
+
type: "select",
|
|
1362
|
+
name: "mode",
|
|
1363
|
+
message: "Choose tooling setup:",
|
|
1364
|
+
choices: [
|
|
1365
|
+
{
|
|
1366
|
+
title: "Prettier + ESLint + TypeScript",
|
|
1367
|
+
value: "recommended"
|
|
1368
|
+
},
|
|
1369
|
+
{
|
|
1370
|
+
title: "All (Prettier + ESLint + TypeScript + Husky + Lint Staged + Commitlint)",
|
|
1371
|
+
value: "all"
|
|
1372
|
+
},
|
|
1373
|
+
{ title: "Custom", value: "custom" }
|
|
1374
|
+
]
|
|
1375
|
+
});
|
|
1376
|
+
if (mode === "recommended") {
|
|
1377
|
+
return RECOMMENDED_TOOLING;
|
|
1378
|
+
}
|
|
1379
|
+
if (mode === "all") {
|
|
1380
|
+
return Object.keys(TOOLING_MAP);
|
|
1381
|
+
}
|
|
1382
|
+
const { tooling } = await prompts2({
|
|
1383
|
+
type: "multiselect",
|
|
1384
|
+
name: "tooling",
|
|
1385
|
+
message: "Select the tooling you want to use:",
|
|
1386
|
+
choices: TOOLING_CHOICES,
|
|
1387
|
+
hint: "- Space to select. Return to submit"
|
|
1388
|
+
});
|
|
1389
|
+
return tooling ?? [];
|
|
1390
|
+
}
|
|
1391
|
+
function getToolingDepsFromChoices(choices = []) {
|
|
1392
|
+
const deps = /* @__PURE__ */ new Set();
|
|
1393
|
+
choices.forEach((tool) => {
|
|
1394
|
+
TOOLING_MAP[tool]?.forEach((dep) => deps.add(dep));
|
|
1395
|
+
});
|
|
1396
|
+
return Array.from(deps);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// src/commands/add/index.ts
|
|
1400
|
+
async function add(registryItemName, options = {}) {
|
|
1401
|
+
await assertInitialized();
|
|
1402
|
+
validateInput(registryItemName);
|
|
1403
|
+
if (options.merge && options.force) {
|
|
1404
|
+
logger.warn("--merge is ignored when --force is set.");
|
|
1405
|
+
}
|
|
1406
|
+
const effectiveMerge = Boolean(options.merge && !options.force);
|
|
1407
|
+
const config = await getServerCNConfig();
|
|
1408
|
+
validateStack(config);
|
|
1409
|
+
let toolingDeps;
|
|
1410
|
+
if (["blueprint"].includes(options?.type || "")) {
|
|
1411
|
+
const toolingChoices = await getToolingChoices();
|
|
1412
|
+
toolingDeps = getToolingDepsFromChoices(toolingChoices);
|
|
1413
|
+
}
|
|
1414
|
+
const type = options.type ?? "component";
|
|
1415
|
+
const component = await getRegistry(registryItemName, type, options.local);
|
|
1416
|
+
validateCompatibility(component, config);
|
|
1417
|
+
const resolution = await resolveTemplateResolution({
|
|
1418
|
+
component,
|
|
1419
|
+
config,
|
|
1420
|
+
options,
|
|
1421
|
+
registryItemName
|
|
1422
|
+
});
|
|
1423
|
+
await scaffoldFiles({
|
|
1424
|
+
registryItemName,
|
|
1425
|
+
templatePath: resolution.templatePath,
|
|
1426
|
+
options: { ...options, merge: effectiveMerge },
|
|
1427
|
+
component,
|
|
1428
|
+
selectedProvider: resolution.selectedProvider
|
|
1429
|
+
});
|
|
1430
|
+
ensureProjectFiles();
|
|
1431
|
+
const { runtimeDeps, devDeps } = resolveDependencies2({
|
|
1432
|
+
component,
|
|
1433
|
+
config,
|
|
1434
|
+
additionalRuntimeDeps: resolution.additionalRuntimeDeps,
|
|
1435
|
+
additionalDevDeps: resolution.additionalDevDeps
|
|
1436
|
+
});
|
|
1437
|
+
if (runtimeDeps.length > 0 || devDeps.length > 0) {
|
|
1438
|
+
await installDependencies({
|
|
1439
|
+
runtime: runtimeDeps,
|
|
1440
|
+
dev: [...toolingDeps || [], ...devDeps],
|
|
1441
|
+
cwd: process.cwd(),
|
|
1442
|
+
packageManager: config.project.packageManager
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
await runPostInstallHooks({
|
|
1446
|
+
registryItemName,
|
|
1447
|
+
type,
|
|
1448
|
+
component,
|
|
1449
|
+
framework: config.stack.framework,
|
|
1450
|
+
runtime: config.stack.runtime,
|
|
1451
|
+
selectedProvider: resolution.selectedProvider ?? "",
|
|
1452
|
+
dbEngine: config.database?.engine,
|
|
1453
|
+
dbAdapter: config.database?.adapter
|
|
1454
|
+
});
|
|
1455
|
+
logger.break();
|
|
1456
|
+
logger.success(`${capitalize(type)}: ${component.slug} added successfully`);
|
|
1457
|
+
logger.break();
|
|
1458
|
+
}
|
|
1459
|
+
function validateInput(name) {
|
|
1460
|
+
if (!name) {
|
|
1461
|
+
logger.error("Component name is required.");
|
|
1462
|
+
process.exit(1);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
function validateStack(config) {
|
|
1466
|
+
if (!config.stack.runtime || !config.stack.framework) {
|
|
1467
|
+
logger.error(
|
|
1468
|
+
"Stack configuration is missing. Run `npx servercn-cli init` first."
|
|
1469
|
+
);
|
|
1470
|
+
process.exit(1);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
function validateCompatibility(component, config) {
|
|
1474
|
+
if ("runtimes" in component) {
|
|
1475
|
+
const runtime = component.runtimes[config.stack.runtime];
|
|
1476
|
+
if (!runtime) {
|
|
1477
|
+
logger.error(
|
|
1478
|
+
`Runtime ${config.stack.runtime} is not supported by ${component.slug}`
|
|
1479
|
+
);
|
|
1480
|
+
process.exit(1);
|
|
1481
|
+
}
|
|
1482
|
+
const framework = runtime.frameworks[config.stack.framework];
|
|
1483
|
+
if (!framework) {
|
|
1484
|
+
logger.break();
|
|
1485
|
+
logger.error(
|
|
1486
|
+
`Unsupported framework '${config.stack.framework}' for component '${component.slug}'.`
|
|
1487
|
+
);
|
|
1488
|
+
logger.error(
|
|
1489
|
+
`This '${component.slug}' does not provide templates for the selected framework.`
|
|
1490
|
+
);
|
|
1491
|
+
logger.error(
|
|
1492
|
+
`Please choose one of the supported frameworks and try again.`
|
|
1493
|
+
);
|
|
1494
|
+
logger.break();
|
|
1495
|
+
process.exit(1);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
async function scaffoldFiles({
|
|
1500
|
+
registryItemName,
|
|
1501
|
+
templatePath,
|
|
1502
|
+
options,
|
|
1503
|
+
component,
|
|
1504
|
+
selectedProvider
|
|
1505
|
+
}) {
|
|
1506
|
+
const IS_LOCAL = options.local ?? false;
|
|
1507
|
+
const targetDir = paths.targets(".");
|
|
1508
|
+
const spin = spinner("Scaffolding files...")?.start();
|
|
1509
|
+
if (IS_LOCAL) {
|
|
1510
|
+
const templateDir = path10.resolve(paths.templates(), templatePath);
|
|
1511
|
+
if (!await fs9.pathExists(templateDir)) {
|
|
1512
|
+
logger.error(
|
|
1513
|
+
`
|
|
1514
|
+
Template not found: ${templateDir}
|
|
1515
|
+
Check your servercn configuration.
|
|
1516
|
+
`
|
|
1517
|
+
);
|
|
1518
|
+
process.exit(1);
|
|
1519
|
+
}
|
|
1520
|
+
logger.break();
|
|
1521
|
+
await copyTemplate({
|
|
1522
|
+
templateDir,
|
|
1523
|
+
targetDir,
|
|
1524
|
+
registryItemName,
|
|
1525
|
+
conflict: options.force ? "overwrite" : "skip",
|
|
1526
|
+
merge: options.merge
|
|
1527
|
+
});
|
|
1528
|
+
} else {
|
|
1529
|
+
const ok = await cloneServercnRegistry({
|
|
1530
|
+
component,
|
|
1531
|
+
templatePath,
|
|
1532
|
+
targetDir,
|
|
1533
|
+
options,
|
|
1534
|
+
selectedProvider
|
|
1535
|
+
});
|
|
1536
|
+
if (!ok) {
|
|
1537
|
+
logger.error("\nSomething went wrong. Failed to scaffold template\n");
|
|
1538
|
+
process.exit(1);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
logger.break();
|
|
1542
|
+
spin?.succeed("Scaffolding files successfully!");
|
|
1543
|
+
}
|
|
1544
|
+
function ensureProjectFiles() {
|
|
1545
|
+
ensurePackageJson(process.cwd());
|
|
1546
|
+
ensureTsConfig(process.cwd());
|
|
1547
|
+
}
|
|
1548
|
+
function resolveDependencies2({
|
|
1549
|
+
component,
|
|
1550
|
+
config,
|
|
1551
|
+
additionalDevDeps,
|
|
1552
|
+
additionalRuntimeDeps
|
|
1553
|
+
}) {
|
|
1554
|
+
if (!("runtimes" in component)) {
|
|
1555
|
+
return {
|
|
1556
|
+
runtimeDeps: [
|
|
1557
|
+
...component.dependencies?.runtime ?? [],
|
|
1558
|
+
...additionalRuntimeDeps
|
|
1559
|
+
],
|
|
1560
|
+
devDeps: [...component.dependencies?.dev ?? [], ...additionalDevDeps]
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
const framework = component.runtimes[config.stack.runtime].frameworks[config.stack.framework];
|
|
1564
|
+
return {
|
|
1565
|
+
runtimeDeps: [
|
|
1566
|
+
...framework && "dependencies" in framework ? framework.dependencies?.runtime ?? [] : [],
|
|
1567
|
+
...additionalRuntimeDeps
|
|
1568
|
+
],
|
|
1569
|
+
devDeps: [
|
|
1570
|
+
...framework && "dependencies" in framework ? framework?.dependencies?.dev ?? [] : [],
|
|
1571
|
+
...additionalDevDeps
|
|
1572
|
+
]
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
async function runPostInstallHooks({
|
|
1576
|
+
component,
|
|
1577
|
+
registryItemName,
|
|
1578
|
+
type,
|
|
1579
|
+
runtime,
|
|
1580
|
+
framework,
|
|
1581
|
+
selectedProvider,
|
|
1582
|
+
dbEngine,
|
|
1583
|
+
dbAdapter
|
|
1584
|
+
}) {
|
|
1585
|
+
if (type === "tooling" && registryItemName === "husky") {
|
|
1586
|
+
try {
|
|
1587
|
+
await execa2("npx", ["husky", "init"], { stdio: "inherit" });
|
|
1588
|
+
} catch {
|
|
1589
|
+
logger.warn(
|
|
1590
|
+
"Could not initialize husky automatically. Please run 'npx husky init' manually."
|
|
1591
|
+
);
|
|
1592
|
+
}
|
|
1593
|
+
} else {
|
|
1594
|
+
let filterEnvs = [];
|
|
1595
|
+
switch (type) {
|
|
1596
|
+
case "component":
|
|
1597
|
+
const registry = component?.runtimes[runtime]?.frameworks[framework];
|
|
1598
|
+
if (registry?.prompt) {
|
|
1599
|
+
filterEnvs = registry?.variants[selectedProvider]?.env?.filter(
|
|
1600
|
+
(env) => env !== ""
|
|
1601
|
+
);
|
|
1602
|
+
} else {
|
|
1603
|
+
filterEnvs = registry?.env?.filter((env) => env !== "");
|
|
1604
|
+
}
|
|
1605
|
+
break;
|
|
1606
|
+
case "blueprint":
|
|
1607
|
+
const registryBlueprint = component?.runtimes[runtime]?.frameworks[framework]?.databases[dbEngine].orms[dbAdapter]?.env ?? [];
|
|
1608
|
+
filterEnvs = registryBlueprint?.filter((env) => env !== "");
|
|
1609
|
+
break;
|
|
1610
|
+
default:
|
|
1611
|
+
break;
|
|
1612
|
+
}
|
|
1613
|
+
if (filterEnvs?.length > 0) {
|
|
1614
|
+
updateEnvKeys({
|
|
1615
|
+
envFile: ".env.example",
|
|
1616
|
+
envKeys: filterEnvs,
|
|
1617
|
+
label: registryItemName
|
|
1618
|
+
});
|
|
1619
|
+
updateEnvKeys({
|
|
1620
|
+
envFile: ".env",
|
|
1621
|
+
envKeys: filterEnvs,
|
|
1622
|
+
label: registryItemName
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
// src/commands/init.ts
|
|
1629
|
+
import fs10 from "fs-extra";
|
|
1630
|
+
import path11 from "path";
|
|
1631
|
+
import prompts3 from "prompts";
|
|
1632
|
+
import { execa as execa3 } from "execa";
|
|
1633
|
+
|
|
1634
|
+
// src/configs/ts.config.ts
|
|
1635
|
+
var tsConfig = {
|
|
1636
|
+
compilerOptions: {
|
|
1637
|
+
target: "ES2021",
|
|
1638
|
+
module: "es2022",
|
|
1639
|
+
moduleResolution: "bundler",
|
|
1640
|
+
strict: true,
|
|
1641
|
+
esModuleInterop: true,
|
|
1642
|
+
skipLibCheck: true,
|
|
1643
|
+
outDir: "dist",
|
|
1644
|
+
rootDir: "src",
|
|
1645
|
+
sourceMap: true,
|
|
1646
|
+
alwaysStrict: true,
|
|
1647
|
+
useUnknownInCatchVariables: true,
|
|
1648
|
+
forceConsistentCasingInFileNames: true,
|
|
1649
|
+
paths: {
|
|
1650
|
+
"@/*": ["./src/*"],
|
|
1651
|
+
"@/shared/*": ["../../shared/*"]
|
|
1652
|
+
}
|
|
1653
|
+
},
|
|
1654
|
+
"tsc-alias": {
|
|
1655
|
+
resolveFullPaths: true,
|
|
1656
|
+
verbose: false
|
|
1657
|
+
},
|
|
1658
|
+
include: ["src/**/*"],
|
|
1659
|
+
exclude: ["node_modules"]
|
|
1660
|
+
};
|
|
1661
|
+
|
|
1662
|
+
// src/configs/commitlint.config.ts
|
|
1663
|
+
var commitlintConfig = {
|
|
1664
|
+
extends: ["@commitlint/config-conventional"],
|
|
1665
|
+
rules: {
|
|
1666
|
+
"type-enum": [
|
|
1667
|
+
2,
|
|
1668
|
+
"always",
|
|
1669
|
+
[
|
|
1670
|
+
"feat",
|
|
1671
|
+
"fix",
|
|
1672
|
+
"docs",
|
|
1673
|
+
"style",
|
|
1674
|
+
"refactor",
|
|
1675
|
+
"test",
|
|
1676
|
+
"chore",
|
|
1677
|
+
"ci",
|
|
1678
|
+
"perf",
|
|
1679
|
+
"build",
|
|
1680
|
+
"release",
|
|
1681
|
+
"workflow",
|
|
1682
|
+
"security"
|
|
1683
|
+
]
|
|
1684
|
+
],
|
|
1685
|
+
"subject-case": [2, "always", ["lower-case"]]
|
|
1686
|
+
}
|
|
1687
|
+
};
|
|
1688
|
+
|
|
1689
|
+
// src/configs/prettier.config.ts
|
|
1690
|
+
var prettierConfig = {
|
|
1691
|
+
singleQuote: false,
|
|
1692
|
+
semi: true,
|
|
1693
|
+
tabWidth: 2,
|
|
1694
|
+
trailingComma: "none",
|
|
1695
|
+
bracketSameLine: false,
|
|
1696
|
+
arrowParens: "avoid",
|
|
1697
|
+
endOfLine: "lf"
|
|
1698
|
+
};
|
|
1699
|
+
var prettierIgnore = `# dependencies
|
|
1700
|
+
node_modules
|
|
1701
|
+
|
|
1702
|
+
# build outputs
|
|
1703
|
+
dist
|
|
1704
|
+
build
|
|
1705
|
+
coverage
|
|
1706
|
+
|
|
1707
|
+
# lock files
|
|
1708
|
+
package-lock.json
|
|
1709
|
+
pnpm-lock.yaml
|
|
1710
|
+
yarn.lock
|
|
1711
|
+
|
|
1712
|
+
# environment
|
|
1713
|
+
.env
|
|
1714
|
+
|
|
1715
|
+
# generated files
|
|
1716
|
+
*.min.js
|
|
1717
|
+
*.bundle.js
|
|
1718
|
+
|
|
1719
|
+
# logs
|
|
1720
|
+
*.log
|
|
1721
|
+
|
|
1722
|
+
# git
|
|
1723
|
+
.git
|
|
1724
|
+
.gitignore
|
|
1725
|
+
`;
|
|
1726
|
+
|
|
1727
|
+
// src/configs/servercn.config.ts
|
|
1728
|
+
var servercnConfig = (config) => {
|
|
1729
|
+
return {
|
|
1730
|
+
$schema: `${SERVERCN_URL}/schema/servercn.config.json`,
|
|
1731
|
+
version: LATEST_VERSION,
|
|
1732
|
+
project: {
|
|
1733
|
+
rootDir: config.project.rootDir,
|
|
1734
|
+
type: config.project.type,
|
|
1735
|
+
packageManager: config.project.packageManager
|
|
1736
|
+
},
|
|
1737
|
+
stack: config.stack,
|
|
1738
|
+
database: config.database,
|
|
1739
|
+
meta: {
|
|
1740
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1741
|
+
createdBy: `servercn@${LATEST_VERSION}`
|
|
1742
|
+
}
|
|
1743
|
+
};
|
|
1744
|
+
};
|
|
1745
|
+
|
|
1746
|
+
// src/configs/gitignore.config.ts
|
|
1747
|
+
var gitignore = `# dependencies
|
|
1748
|
+
node_modules
|
|
1749
|
+
.pnpm-store
|
|
1750
|
+
|
|
1751
|
+
# build output
|
|
1752
|
+
dist
|
|
1753
|
+
build
|
|
1754
|
+
coverage
|
|
1755
|
+
|
|
1756
|
+
# environment variables
|
|
1757
|
+
.env
|
|
1758
|
+
.env.local
|
|
1759
|
+
.env.*.local
|
|
1760
|
+
|
|
1761
|
+
# logs
|
|
1762
|
+
logs
|
|
1763
|
+
*.log
|
|
1764
|
+
npm-debug.log*
|
|
1765
|
+
pnpm-debug.log*
|
|
1766
|
+
yarn-debug.log*
|
|
1767
|
+
yarn-error.log*
|
|
1768
|
+
|
|
1769
|
+
# OS files
|
|
1770
|
+
.DS_Store
|
|
1771
|
+
Thumbs.db
|
|
1772
|
+
|
|
1773
|
+
# IDE
|
|
1774
|
+
.vscode
|
|
1775
|
+
.idea
|
|
1776
|
+
|
|
1777
|
+
# temp
|
|
1778
|
+
tmp
|
|
1779
|
+
temp
|
|
1780
|
+
.cache
|
|
1781
|
+
|
|
1782
|
+
# misc
|
|
1783
|
+
*.tsbuildinfo
|
|
1784
|
+
`;
|
|
1785
|
+
|
|
1786
|
+
// src/configs/eslint.config.ts
|
|
1787
|
+
var eslintConfig = `
|
|
1788
|
+
import tseslint from "@typescript-eslint/eslint-plugin";
|
|
1789
|
+
import tsparser from "@typescript-eslint/parser";
|
|
1790
|
+
import prettierPlugin from "eslint-plugin-prettier";
|
|
1791
|
+
|
|
1792
|
+
export default [
|
|
1793
|
+
{
|
|
1794
|
+
files: ["**/*.ts"],
|
|
1795
|
+
|
|
1796
|
+
languageOptions: {
|
|
1797
|
+
parser: tsparser,
|
|
1798
|
+
sourceType: "module"
|
|
1799
|
+
},
|
|
1800
|
+
|
|
1801
|
+
plugins: {
|
|
1802
|
+
"@typescript-eslint": tseslint,
|
|
1803
|
+
prettier: prettierPlugin
|
|
1804
|
+
},
|
|
1805
|
+
|
|
1806
|
+
rules: {
|
|
1807
|
+
...tseslint.configs.recommended.rules,
|
|
1808
|
+
"@typescript-eslint/no-unused-vars": "off",
|
|
1809
|
+
"@typescript-eslint/no-unused-expressions": "off",
|
|
1810
|
+
"no-console": "warn",
|
|
1811
|
+
semi: ["error", "always"],
|
|
1812
|
+
quotes: ["error", "double"],
|
|
1813
|
+
"prettier/prettier": "error"
|
|
1814
|
+
}
|
|
1815
|
+
},
|
|
1816
|
+
];
|
|
1817
|
+
`;
|
|
1818
|
+
|
|
1819
|
+
// src/commands/init.ts
|
|
1820
|
+
async function init(foundation, options = {}) {
|
|
1821
|
+
const cwd = process.cwd();
|
|
1822
|
+
const configPath = path11.join(cwd, SERVERCN_CONFIG_FILE);
|
|
1823
|
+
if (!foundation) {
|
|
1824
|
+
const fd = await prompts3({
|
|
1825
|
+
type: "select",
|
|
1826
|
+
name: "foundation",
|
|
1827
|
+
message: "Select a project foundation: ",
|
|
1828
|
+
choices: [
|
|
1829
|
+
{
|
|
1830
|
+
title: "Express Starter",
|
|
1831
|
+
description: "Minimal Express server setup",
|
|
1832
|
+
value: "express-starter"
|
|
1833
|
+
},
|
|
1834
|
+
{
|
|
1835
|
+
title: "Express + Mongoose",
|
|
1836
|
+
description: "MongoDB with Mongoose ODM",
|
|
1837
|
+
value: "mongoose-starter"
|
|
1838
|
+
},
|
|
1839
|
+
{
|
|
1840
|
+
title: "Express + MongoDB (Prisma)",
|
|
1841
|
+
description: "MongoDB database with Prisma ORM",
|
|
1842
|
+
value: "prisma-mongodb-starter"
|
|
1843
|
+
},
|
|
1844
|
+
{
|
|
1845
|
+
title: "Express + MySQL (Drizzle)",
|
|
1846
|
+
description: "MySQL database with Drizzle ORM",
|
|
1847
|
+
value: "drizzle-mysql-starter"
|
|
1848
|
+
},
|
|
1849
|
+
{
|
|
1850
|
+
title: "Express + PostgreSQL (Drizzle)",
|
|
1851
|
+
description: "PostgreSQL database with Drizzle ORM",
|
|
1852
|
+
value: "drizzle-pg-starter"
|
|
1853
|
+
},
|
|
1854
|
+
{
|
|
1855
|
+
title: "Existing Project",
|
|
1856
|
+
description: `Generate ${SERVERCN_CONFIG_FILE} for an existing project`,
|
|
1857
|
+
value: null
|
|
1858
|
+
}
|
|
1859
|
+
]
|
|
1860
|
+
});
|
|
1861
|
+
foundation = fd.foundation;
|
|
1862
|
+
}
|
|
1863
|
+
if (await fs10.pathExists(configPath) && !foundation) {
|
|
1864
|
+
logger.break();
|
|
1865
|
+
logger.break();
|
|
1866
|
+
logger.warn(`${APP_NAME} is already initialized in this project.`);
|
|
1867
|
+
logger.info(
|
|
1868
|
+
"You can now add components: npx servercn-cli add <component-name>"
|
|
1869
|
+
);
|
|
1870
|
+
logger.break();
|
|
1871
|
+
process.exit(1);
|
|
1872
|
+
}
|
|
1873
|
+
if (foundation) {
|
|
1874
|
+
try {
|
|
1875
|
+
logger.break();
|
|
1876
|
+
const response2 = await prompts3([
|
|
1877
|
+
{
|
|
1878
|
+
type: "text",
|
|
1879
|
+
name: "root",
|
|
1880
|
+
message: "Project root directory",
|
|
1881
|
+
initial: ".",
|
|
1882
|
+
format: (val) => val.trim() || "."
|
|
1883
|
+
},
|
|
1884
|
+
{
|
|
1885
|
+
type: "select",
|
|
1886
|
+
name: "architecture",
|
|
1887
|
+
message: "Select architecture",
|
|
1888
|
+
choices: [
|
|
1889
|
+
{ title: "MVC (controllers, services, models)", value: "mvc" },
|
|
1890
|
+
{ title: "Feature (modules, shared)", value: "feature" },
|
|
1891
|
+
{ title: "Modular Architecture (NestJS)", value: "modular" }
|
|
1892
|
+
]
|
|
1893
|
+
},
|
|
1894
|
+
{
|
|
1895
|
+
type: "select",
|
|
1896
|
+
name: "packageManager",
|
|
1897
|
+
message: "Select package manager",
|
|
1898
|
+
choices: [
|
|
1899
|
+
{ title: "npm", value: "npm" },
|
|
1900
|
+
{ title: "pnpm", value: "pnpm" },
|
|
1901
|
+
{ title: "yarn", value: "yarn" },
|
|
1902
|
+
{ title: "bun", value: "bun" }
|
|
1903
|
+
],
|
|
1904
|
+
initial: Math.max(
|
|
1905
|
+
0,
|
|
1906
|
+
["npm", "pnpm", "yarn", "bun"].indexOf(detectPackageManager())
|
|
1907
|
+
)
|
|
1908
|
+
},
|
|
1909
|
+
{
|
|
1910
|
+
type: "confirm",
|
|
1911
|
+
name: "initGit",
|
|
1912
|
+
message: "Initialize git repository?",
|
|
1913
|
+
initial: false
|
|
1914
|
+
}
|
|
1915
|
+
]);
|
|
1916
|
+
logger.break();
|
|
1917
|
+
const toolingChoices = await getToolingChoices();
|
|
1918
|
+
const devDeps = getToolingDepsFromChoices(toolingChoices);
|
|
1919
|
+
const rootPath2 = path11.resolve(cwd, response2.root);
|
|
1920
|
+
if (response2.root !== "." && fs10.pathExistsSync(rootPath2)) {
|
|
1921
|
+
logger.break();
|
|
1922
|
+
logger.error(`Cannot create '${response2.root}' \u2014 file already exists!`);
|
|
1923
|
+
logger.break();
|
|
1924
|
+
process.exit(1);
|
|
1925
|
+
}
|
|
1926
|
+
await fs10.ensureDir(rootPath2);
|
|
1927
|
+
if (!fs10.pathExistsSync(rootPath2)) {
|
|
1928
|
+
logger.error(`Failed to create project directory: ${rootPath2}`);
|
|
1929
|
+
process.exit(1);
|
|
1930
|
+
}
|
|
1931
|
+
if (response2.initGit) {
|
|
1932
|
+
try {
|
|
1933
|
+
await execa3("git", ["init"], { cwd: rootPath2 });
|
|
1934
|
+
logger.info("Initialized git repository.");
|
|
1935
|
+
} catch {
|
|
1936
|
+
logger.warn("Failed to initialize git repository. is git installed?");
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
try {
|
|
1940
|
+
const component = await getRegistry(
|
|
1941
|
+
foundation,
|
|
1942
|
+
"foundation",
|
|
1943
|
+
options.local
|
|
1944
|
+
);
|
|
1945
|
+
const baseConfig = component.runtimes["node"].frameworks[getFramework(options.fw ?? "express")];
|
|
1946
|
+
if (options.local) {
|
|
1947
|
+
const targetDir = paths.targets(response2.root ?? ".");
|
|
1948
|
+
const localTemplatePath = `node/${getFramework(options.fw ?? "express")}/foundation/${baseConfig?.templates[response2.architecture]}` || "";
|
|
1949
|
+
const templateDir = path11.resolve(
|
|
1950
|
+
paths.templates(),
|
|
1951
|
+
localTemplatePath
|
|
1952
|
+
);
|
|
1953
|
+
if (!await fs10.pathExists(templateDir)) {
|
|
1954
|
+
logger.error(
|
|
1955
|
+
`
|
|
1956
|
+
Template not found: ${templateDir}
|
|
1957
|
+
Check your servercn configuration.
|
|
1958
|
+
`
|
|
1959
|
+
);
|
|
1960
|
+
process.exit(1);
|
|
1961
|
+
}
|
|
1962
|
+
logger.break();
|
|
1963
|
+
await copyTemplate({
|
|
1964
|
+
templateDir,
|
|
1965
|
+
targetDir,
|
|
1966
|
+
registryItemName: foundation,
|
|
1967
|
+
conflict: options.force ? "overwrite" : "skip"
|
|
1968
|
+
});
|
|
1969
|
+
} else {
|
|
1970
|
+
const templatePath = `node/${getFramework(options.fw ?? "express")}/${response2.architecture}`;
|
|
1971
|
+
if (!templatePath) {
|
|
1972
|
+
logger.error(
|
|
1973
|
+
`Template not found for ${foundation?.toLowerCase()} (${response2.architecture})`
|
|
1974
|
+
);
|
|
1975
|
+
fs10.removeSync(rootPath2);
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
const ok = await cloneServercnRegistry({
|
|
1979
|
+
templatePath,
|
|
1980
|
+
targetDir: response2.root,
|
|
1981
|
+
component,
|
|
1982
|
+
options
|
|
1983
|
+
});
|
|
1984
|
+
if (!ok) {
|
|
1985
|
+
logger.error(`Failed to initialize foundation:${foundation}.
|
|
1986
|
+
`);
|
|
1987
|
+
fs10.removeSync(rootPath2);
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
await fs10.writeJson(
|
|
1992
|
+
path11.join(rootPath2, SERVERCN_CONFIG_FILE),
|
|
1993
|
+
servercnConfig({
|
|
1994
|
+
project: {
|
|
1995
|
+
rootDir: response2.root,
|
|
1996
|
+
type: "backend",
|
|
1997
|
+
packageManager: response2.packageManager
|
|
1998
|
+
},
|
|
1999
|
+
stack: {
|
|
2000
|
+
runtime: "node",
|
|
2001
|
+
language: "typescript",
|
|
2002
|
+
framework: getFramework(options.fw ?? "express"),
|
|
2003
|
+
architecture: response2.architecture
|
|
2004
|
+
},
|
|
2005
|
+
database: getDatabaseConfig(foundation)
|
|
2006
|
+
}),
|
|
2007
|
+
{
|
|
2008
|
+
spaces: 2
|
|
2009
|
+
}
|
|
2010
|
+
);
|
|
2011
|
+
await fs10.writeJson(path11.join(rootPath2, ".prettierrc"), prettierConfig, {
|
|
2012
|
+
spaces: 2
|
|
2013
|
+
});
|
|
2014
|
+
await fs10.writeFile(
|
|
2015
|
+
path11.join(rootPath2, ".prettierignore"),
|
|
2016
|
+
prettierIgnore
|
|
2017
|
+
);
|
|
2018
|
+
await fs10.writeFile(path11.join(rootPath2, ".gitignore"), gitignore);
|
|
2019
|
+
await fs10.writeJson(path11.join(rootPath2, "tsconfig.json"), tsConfig, {
|
|
2020
|
+
spaces: 2
|
|
2021
|
+
});
|
|
2022
|
+
await fs10.writeFile(
|
|
2023
|
+
path11.join(rootPath2, "commitlint.config.ts"),
|
|
2024
|
+
`export default ${JSON.stringify(commitlintConfig, null, 2)}`
|
|
2025
|
+
);
|
|
2026
|
+
await fs10.writeFile(
|
|
2027
|
+
path11.join(rootPath2, "eslint.config.mjs"),
|
|
2028
|
+
eslintConfig
|
|
2029
|
+
);
|
|
2030
|
+
const filterEnvs = baseConfig?.env?.filter((env) => env !== "") || [];
|
|
2031
|
+
if (filterEnvs?.length > 0) {
|
|
2032
|
+
updateEnvKeys({
|
|
2033
|
+
envFile: ".env.example",
|
|
2034
|
+
envKeys: filterEnvs,
|
|
2035
|
+
label: foundation,
|
|
2036
|
+
cwd: rootPath2
|
|
2037
|
+
});
|
|
2038
|
+
updateEnvKeys({
|
|
2039
|
+
envFile: ".env",
|
|
2040
|
+
envKeys: filterEnvs,
|
|
2041
|
+
label: foundation,
|
|
2042
|
+
cwd: rootPath2
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
2045
|
+
await installDependencies({
|
|
2046
|
+
runtime: baseConfig?.dependencies?.runtime || [],
|
|
2047
|
+
dev: [...devDeps, baseConfig?.dependencies?.dev || []].flat(),
|
|
2048
|
+
cwd: rootPath2,
|
|
2049
|
+
packageManager: response2.packageManager
|
|
2050
|
+
});
|
|
2051
|
+
logger.break();
|
|
2052
|
+
logger.success(
|
|
2053
|
+
`${APP_NAME} initialized with 'foundation:${foundation}'.`
|
|
2054
|
+
);
|
|
2055
|
+
logger.break();
|
|
2056
|
+
logger.info("Configure environment variables in .env file.");
|
|
2057
|
+
logger.break();
|
|
2058
|
+
logger.log("Run the following commands:");
|
|
2059
|
+
if (response2.root === ".") {
|
|
2060
|
+
logger.muted(`1. ${response2.packageManager || "npm"} run dev
|
|
2061
|
+
`);
|
|
2062
|
+
} else {
|
|
2063
|
+
logger.muted(`1. cd ${response2.root}`);
|
|
2064
|
+
logger.muted(`2. ${response2.packageManager || "npm"} run dev
|
|
2065
|
+
`);
|
|
2066
|
+
}
|
|
2067
|
+
process.exit(1);
|
|
2068
|
+
} catch (e) {
|
|
2069
|
+
fs10.removeSync(rootPath2);
|
|
2070
|
+
logger.error(`Failed to initialize foundation: ${e}`);
|
|
2071
|
+
process.exit(1);
|
|
2072
|
+
}
|
|
2073
|
+
} catch {
|
|
2074
|
+
logger.error(`
|
|
2075
|
+
Failed to initialize foundation
|
|
2076
|
+
`);
|
|
2077
|
+
process.exit(1);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
logger.break();
|
|
2081
|
+
const response = await prompts3([
|
|
2082
|
+
{
|
|
2083
|
+
type: "text",
|
|
2084
|
+
name: "root",
|
|
2085
|
+
message: "Project root directory",
|
|
2086
|
+
initial: ".",
|
|
2087
|
+
format: (val) => val.trim() || "."
|
|
2088
|
+
},
|
|
2089
|
+
{
|
|
2090
|
+
type: "select",
|
|
2091
|
+
name: "language",
|
|
2092
|
+
message: "Programming language",
|
|
2093
|
+
choices: [
|
|
2094
|
+
{
|
|
2095
|
+
title: "Typescript (recommended)",
|
|
2096
|
+
value: "typescript"
|
|
2097
|
+
}
|
|
2098
|
+
]
|
|
2099
|
+
},
|
|
2100
|
+
{
|
|
2101
|
+
type: "select",
|
|
2102
|
+
name: "framework",
|
|
2103
|
+
message: "Backend framework",
|
|
2104
|
+
choices: [
|
|
2105
|
+
{
|
|
2106
|
+
title: "Express.js",
|
|
2107
|
+
value: "express"
|
|
2108
|
+
},
|
|
2109
|
+
{
|
|
2110
|
+
title: "NestJS",
|
|
2111
|
+
value: "nestjs"
|
|
2112
|
+
}
|
|
2113
|
+
],
|
|
2114
|
+
initial: 0
|
|
2115
|
+
},
|
|
2116
|
+
{
|
|
2117
|
+
type: "select",
|
|
2118
|
+
name: "architecture",
|
|
2119
|
+
message: "Select architecture",
|
|
2120
|
+
choices: [
|
|
2121
|
+
{ title: "MVC (controllers, services, models)", value: "mvc" },
|
|
2122
|
+
{ title: "Feature-based (modules, shared)", value: "feature" },
|
|
2123
|
+
{ title: "Modular Architecture (NestJS)", value: "modular" }
|
|
2124
|
+
]
|
|
2125
|
+
},
|
|
2126
|
+
{
|
|
2127
|
+
type: "select",
|
|
2128
|
+
name: "databaseType",
|
|
2129
|
+
message: "Select database",
|
|
2130
|
+
choices: [
|
|
2131
|
+
{
|
|
2132
|
+
title: "Mongodb",
|
|
2133
|
+
value: "mongodb"
|
|
2134
|
+
},
|
|
2135
|
+
{
|
|
2136
|
+
title: "PostgreSQL",
|
|
2137
|
+
value: "postgresql"
|
|
2138
|
+
},
|
|
2139
|
+
{
|
|
2140
|
+
title: "MySQL",
|
|
2141
|
+
value: "mysql"
|
|
2142
|
+
}
|
|
2143
|
+
]
|
|
2144
|
+
},
|
|
2145
|
+
{
|
|
2146
|
+
type: (prev) => prev === "mongodb" ? "select" : null,
|
|
2147
|
+
name: "orm",
|
|
2148
|
+
message: "Mongodb library",
|
|
2149
|
+
choices: [
|
|
2150
|
+
{ title: "Mongoose", value: "mongoose" },
|
|
2151
|
+
{ title: "Prisma", value: "prisma" }
|
|
2152
|
+
]
|
|
2153
|
+
},
|
|
2154
|
+
{
|
|
2155
|
+
type: (_prev, values) => ["postgresql", "mysql"].includes(values.databaseType) ? "select" : null,
|
|
2156
|
+
name: "orm",
|
|
2157
|
+
message: "Orm / query builder",
|
|
2158
|
+
choices: [
|
|
2159
|
+
{ title: "Drizzle", value: "drizzle" },
|
|
2160
|
+
{ title: "Prisma", value: "prisma" }
|
|
2161
|
+
]
|
|
2162
|
+
},
|
|
2163
|
+
{
|
|
2164
|
+
type: "select",
|
|
2165
|
+
name: "packageManager",
|
|
2166
|
+
message: "Select package manager",
|
|
2167
|
+
choices: [
|
|
2168
|
+
{ title: "npm", value: "npm" },
|
|
2169
|
+
{ title: "pnpm", value: "pnpm" },
|
|
2170
|
+
{ title: "yarn", value: "yarn" },
|
|
2171
|
+
{ title: "bun", value: "bun" }
|
|
2172
|
+
],
|
|
2173
|
+
initial: Math.max(
|
|
2174
|
+
0,
|
|
2175
|
+
["npm", "pnpm", "yarn", "bun"].indexOf(detectPackageManager())
|
|
2176
|
+
)
|
|
2177
|
+
}
|
|
2178
|
+
]);
|
|
2179
|
+
if (!response.architecture || !response.databaseType || !response.framework || !response.language || !response.orm || !response.root || !response.packageManager) {
|
|
2180
|
+
logger.break();
|
|
2181
|
+
logger.warn("Initialization cancelled.");
|
|
2182
|
+
logger.break();
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
const rootPath = path11.resolve(cwd, response.root);
|
|
2186
|
+
if (response.root !== "." && fs10.pathExistsSync(rootPath)) {
|
|
2187
|
+
logger.break();
|
|
2188
|
+
logger.error(`Cannot create '${response.root}' \u2014 file already exists!`);
|
|
2189
|
+
logger.break();
|
|
2190
|
+
process.exit(1);
|
|
2191
|
+
}
|
|
2192
|
+
await fs10.ensureDir(rootPath);
|
|
2193
|
+
await fs10.writeJson(
|
|
2194
|
+
path11.join(rootPath, SERVERCN_CONFIG_FILE),
|
|
2195
|
+
servercnConfig({
|
|
2196
|
+
project: {
|
|
2197
|
+
rootDir: response.root,
|
|
2198
|
+
type: "backend",
|
|
2199
|
+
packageManager: response.packageManager
|
|
2200
|
+
},
|
|
2201
|
+
stack: {
|
|
2202
|
+
runtime: "node",
|
|
2203
|
+
language: response.language,
|
|
2204
|
+
framework: response.framework,
|
|
2205
|
+
architecture: response.architecture
|
|
2206
|
+
},
|
|
2207
|
+
database: {
|
|
2208
|
+
engine: response.databaseType,
|
|
2209
|
+
adapter: response.orm
|
|
2210
|
+
}
|
|
2211
|
+
}),
|
|
2212
|
+
{
|
|
2213
|
+
spaces: 2
|
|
2214
|
+
}
|
|
2215
|
+
);
|
|
2216
|
+
logger.success(`
|
|
2217
|
+
${APP_NAME} initialized successfully.`);
|
|
2218
|
+
logger.break();
|
|
2219
|
+
logger.log("You may now add components by running:");
|
|
2220
|
+
if (response.root === ".") {
|
|
2221
|
+
logger.muted("1. npx servercn-cli add <component>");
|
|
2222
|
+
} else {
|
|
2223
|
+
logger.muted(`1. cd ${response.root}`);
|
|
2224
|
+
logger.muted("2. npx servercn-cli add <component>");
|
|
2225
|
+
}
|
|
2226
|
+
logger.muted(
|
|
2227
|
+
"ex: npx servercn-cli add jwt-utils error-handler http-status-codes"
|
|
2228
|
+
);
|
|
2229
|
+
logger.break();
|
|
2230
|
+
}
|
|
2231
|
+
function getFramework(fw) {
|
|
2232
|
+
switch (fw) {
|
|
2233
|
+
case "express":
|
|
2234
|
+
case "expressjs":
|
|
2235
|
+
return "express";
|
|
2236
|
+
case "nestjs":
|
|
2237
|
+
case "nest":
|
|
2238
|
+
return "nestjs";
|
|
2239
|
+
default:
|
|
2240
|
+
return "express";
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
// src/commands/list/index.ts
|
|
2245
|
+
function registryListCommands(program2) {
|
|
2246
|
+
const list = program2.command("list").alias("ls").description("List available ServerCN resources").option("--json", "Output resources as JSON").option("--all", "Display all available registries").option("--local", "Display only local registries").enablePositionalOptions().action((options) => {
|
|
2247
|
+
listOverview(options);
|
|
2248
|
+
});
|
|
2249
|
+
function resolveOptions(cmd) {
|
|
2250
|
+
return cmd.parent?.opts();
|
|
2251
|
+
}
|
|
2252
|
+
list.command("component").alias("cp").description("List available components").action((_, cmd) => {
|
|
2253
|
+
listComponents(resolveOptions(cmd));
|
|
2254
|
+
});
|
|
2255
|
+
list.command("foundation").alias("fd").description("List available foundations").action((_, cmd) => {
|
|
2256
|
+
listFoundations(resolveOptions(cmd));
|
|
2257
|
+
});
|
|
2258
|
+
list.command("tooling").alias("tl").description("List available tooling").action((_, cmd) => {
|
|
2259
|
+
listTooling(resolveOptions(cmd));
|
|
2260
|
+
});
|
|
2261
|
+
list.command("schema").alias("sc").description("List available schemas").action((_, cmd) => {
|
|
2262
|
+
listSchemas(resolveOptions(cmd));
|
|
2263
|
+
});
|
|
2264
|
+
list.command("blueprint").alias("bp").description("List available blueprints").action((_, cmd) => {
|
|
2265
|
+
listBlueprints(resolveOptions(cmd));
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
// src/commands/_build/index.ts
|
|
2270
|
+
import path13 from "path";
|
|
2271
|
+
import fs12 from "fs-extra";
|
|
2272
|
+
import { glob as glob2 } from "glob";
|
|
2273
|
+
|
|
2274
|
+
// src/commands/_build/build.handlers.ts
|
|
2275
|
+
import path12 from "path";
|
|
2276
|
+
import fs11 from "fs-extra";
|
|
2277
|
+
import { glob } from "glob";
|
|
2278
|
+
async function processRegistryItem(item, type) {
|
|
2279
|
+
switch (type) {
|
|
2280
|
+
case "component":
|
|
2281
|
+
return await buildComponent(item);
|
|
2282
|
+
case "blueprint":
|
|
2283
|
+
return await buildBlueprint(item);
|
|
2284
|
+
case "foundation":
|
|
2285
|
+
return await buildFoundation(item);
|
|
2286
|
+
case "schema":
|
|
2287
|
+
return await buildSchema(item);
|
|
2288
|
+
case "tooling":
|
|
2289
|
+
return await buildTooling(item);
|
|
2290
|
+
default:
|
|
2291
|
+
throw new Error(`Unsupported registry type: ${type}`);
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
async function buildComponent(component) {
|
|
2295
|
+
const built = { ...component, runtimes: {} };
|
|
2296
|
+
delete built["$schema"];
|
|
2297
|
+
for (const [runtimeKey, runtime] of Object.entries(component.runtimes)) {
|
|
2298
|
+
const rt = runtime;
|
|
2299
|
+
built.runtimes[runtimeKey] = { frameworks: {} };
|
|
2300
|
+
for (const [frameworkKey, framework] of Object.entries(rt.frameworks)) {
|
|
2301
|
+
const fw = framework;
|
|
2302
|
+
if ("variants" in fw && fw.variants) {
|
|
2303
|
+
const builtVariants = {};
|
|
2304
|
+
for (const [variantKey, variant] of Object.entries(fw.variants)) {
|
|
2305
|
+
const v = variant;
|
|
2306
|
+
builtVariants[variantKey] = {
|
|
2307
|
+
...v,
|
|
2308
|
+
architectures: await processArchitectureSet(
|
|
2309
|
+
v.templates,
|
|
2310
|
+
path12.join(runtimeKey, frameworkKey, "component")
|
|
2311
|
+
)
|
|
2312
|
+
};
|
|
2313
|
+
delete builtVariants[variantKey].templates;
|
|
2314
|
+
}
|
|
2315
|
+
built.runtimes[runtimeKey].frameworks[frameworkKey] = {
|
|
2316
|
+
prompt: fw.prompt,
|
|
2317
|
+
variants: builtVariants
|
|
2318
|
+
};
|
|
2319
|
+
} else if ("templates" in fw && fw.templates) {
|
|
2320
|
+
built.runtimes[runtimeKey].frameworks[frameworkKey] = {
|
|
2321
|
+
...fw,
|
|
2322
|
+
architectures: await processArchitectureSet(
|
|
2323
|
+
fw.templates,
|
|
2324
|
+
path12.join(runtimeKey, frameworkKey, "component")
|
|
2325
|
+
)
|
|
2326
|
+
};
|
|
2327
|
+
delete built.runtimes[runtimeKey].frameworks[frameworkKey].templates;
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
return built;
|
|
2332
|
+
}
|
|
2333
|
+
async function buildFoundation(foundation) {
|
|
2334
|
+
const built = { ...foundation, runtimes: {} };
|
|
2335
|
+
delete built["$schema"];
|
|
2336
|
+
for (const [runtimeKey, runtime] of Object.entries(foundation.runtimes)) {
|
|
2337
|
+
built.runtimes[runtimeKey] = { frameworks: {} };
|
|
2338
|
+
for (const [frameworkKey, framework] of Object.entries(
|
|
2339
|
+
runtime.frameworks
|
|
2340
|
+
)) {
|
|
2341
|
+
if (framework) {
|
|
2342
|
+
built.runtimes[runtimeKey].frameworks[frameworkKey] = {
|
|
2343
|
+
...framework,
|
|
2344
|
+
architectures: await processArchitectureSet(
|
|
2345
|
+
framework?.templates,
|
|
2346
|
+
path12.join(runtimeKey, frameworkKey, "foundation")
|
|
2347
|
+
)
|
|
2348
|
+
};
|
|
2349
|
+
delete built.runtimes[runtimeKey].frameworks[frameworkKey].templates;
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
return built;
|
|
2354
|
+
}
|
|
2355
|
+
async function buildBlueprint(blueprint) {
|
|
2356
|
+
const built = { ...blueprint, runtimes: {} };
|
|
2357
|
+
delete built["$schema"];
|
|
2358
|
+
for (const [runtimeKey, runtime] of Object.entries(blueprint.runtimes)) {
|
|
2359
|
+
built.runtimes[runtimeKey] = { frameworks: {} };
|
|
2360
|
+
for (const [frameworkKey, framework] of Object.entries(
|
|
2361
|
+
runtime.frameworks
|
|
2362
|
+
)) {
|
|
2363
|
+
const builtDatabases = {};
|
|
2364
|
+
for (const [dbKey, db] of Object.entries(framework.databases)) {
|
|
2365
|
+
const builtOrms = {};
|
|
2366
|
+
for (const [ormKey, orm] of Object.entries(db.orms)) {
|
|
2367
|
+
builtOrms[ormKey] = {
|
|
2368
|
+
...orm,
|
|
2369
|
+
architectures: await processArchitectureSet(
|
|
2370
|
+
orm.templates,
|
|
2371
|
+
path12.join(runtimeKey, frameworkKey, "blueprint")
|
|
2372
|
+
)
|
|
2373
|
+
};
|
|
2374
|
+
delete builtOrms[ormKey].templates;
|
|
2375
|
+
}
|
|
2376
|
+
builtDatabases[dbKey] = { orms: builtOrms };
|
|
2377
|
+
}
|
|
2378
|
+
built.runtimes[runtimeKey].frameworks[frameworkKey] = {
|
|
2379
|
+
databases: builtDatabases
|
|
2380
|
+
};
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
return built;
|
|
2384
|
+
}
|
|
2385
|
+
async function buildSchema(schema) {
|
|
2386
|
+
const built = { ...schema, runtimes: {} };
|
|
2387
|
+
delete built["$schema"];
|
|
2388
|
+
for (const [runtimeKey, runtime] of Object.entries(schema.runtimes)) {
|
|
2389
|
+
built.runtimes[runtimeKey] = { frameworks: {} };
|
|
2390
|
+
for (const [frameworkKey, framework] of Object.entries(
|
|
2391
|
+
runtime.frameworks
|
|
2392
|
+
)) {
|
|
2393
|
+
const builtDatabases = {};
|
|
2394
|
+
for (const [dbKey, db] of Object.entries(framework.databases)) {
|
|
2395
|
+
const builtOrms = {};
|
|
2396
|
+
for (const [ormKey, orm] of Object.entries(db.orms)) {
|
|
2397
|
+
const builtMultiTemplates = {};
|
|
2398
|
+
for (const [tmplKey, archSet] of Object.entries(orm.templates)) {
|
|
2399
|
+
builtMultiTemplates[tmplKey] = {
|
|
2400
|
+
architectures: await processArchitectureSet(
|
|
2401
|
+
archSet,
|
|
2402
|
+
path12.join(runtimeKey, frameworkKey, "schema")
|
|
2403
|
+
)
|
|
2404
|
+
};
|
|
2405
|
+
}
|
|
2406
|
+
builtOrms[ormKey] = {
|
|
2407
|
+
...orm,
|
|
2408
|
+
templates: builtMultiTemplates
|
|
2409
|
+
};
|
|
2410
|
+
}
|
|
2411
|
+
builtDatabases[dbKey] = { orms: builtOrms };
|
|
2412
|
+
}
|
|
2413
|
+
built.runtimes[runtimeKey].frameworks[frameworkKey] = {
|
|
2414
|
+
databases: builtDatabases
|
|
2415
|
+
};
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
return built;
|
|
2419
|
+
}
|
|
2420
|
+
async function buildTooling(tooling) {
|
|
2421
|
+
const built = { ...tooling };
|
|
2422
|
+
delete built["$schema"];
|
|
2423
|
+
const builtTemplates = {};
|
|
2424
|
+
for (const [tmplKey, tmplPath] of Object.entries(tooling.templates)) {
|
|
2425
|
+
const absolutePath = path12.join(paths.templateBase, "tooling", tmplPath);
|
|
2426
|
+
builtTemplates[tmplKey] = {
|
|
2427
|
+
files: await extractFiles(absolutePath, "tooling")
|
|
2428
|
+
};
|
|
2429
|
+
}
|
|
2430
|
+
built.templates = builtTemplates;
|
|
2431
|
+
return built;
|
|
2432
|
+
}
|
|
2433
|
+
async function processArchitectureSet(archSet, baseRelPath) {
|
|
2434
|
+
const architectures = {};
|
|
2435
|
+
for (const [archKey, relTemplatePath] of Object.entries(archSet)) {
|
|
2436
|
+
if (!relTemplatePath) continue;
|
|
2437
|
+
const absoluteTemplatePath = path12.join(
|
|
2438
|
+
paths.templateBase,
|
|
2439
|
+
baseRelPath,
|
|
2440
|
+
relTemplatePath
|
|
2441
|
+
);
|
|
2442
|
+
architectures[archKey] = {
|
|
2443
|
+
files: await extractFiles(absoluteTemplatePath, "file")
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2446
|
+
return architectures;
|
|
2447
|
+
}
|
|
2448
|
+
async function extractFiles(templateDir, type) {
|
|
2449
|
+
if (!await fs11.pathExists(templateDir)) {
|
|
2450
|
+
const msg = `Template directory not found: ${templateDir}`;
|
|
2451
|
+
logger.error(msg);
|
|
2452
|
+
throw new Error(msg);
|
|
2453
|
+
}
|
|
2454
|
+
const pattern = "**/*";
|
|
2455
|
+
const filePaths = await glob(pattern, {
|
|
2456
|
+
cwd: templateDir,
|
|
2457
|
+
nodir: true,
|
|
2458
|
+
dot: true
|
|
2459
|
+
});
|
|
2460
|
+
const files = [];
|
|
2461
|
+
for (const relativePath of filePaths) {
|
|
2462
|
+
const absolutePath = path12.join(templateDir, relativePath);
|
|
2463
|
+
const content = normalizeEol(await fs11.readFile(absolutePath, "utf8"));
|
|
2464
|
+
files.push({
|
|
2465
|
+
type,
|
|
2466
|
+
path: normalizePath(relativePath),
|
|
2467
|
+
content
|
|
2468
|
+
});
|
|
2469
|
+
}
|
|
2470
|
+
return files;
|
|
2471
|
+
}
|
|
2472
|
+
function normalizePath(p) {
|
|
2473
|
+
return p.split(path12.sep).join("/");
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
// src/commands/_build/index.ts
|
|
2477
|
+
async function build(options) {
|
|
2478
|
+
const buildSpin = spinner("Building ServerCN registry...").start();
|
|
2479
|
+
const index = {
|
|
2480
|
+
name: options.name ?? APP_NAME.toLowerCase(),
|
|
2481
|
+
homepage: options.url ?? SERVERCN_URL,
|
|
2482
|
+
items: []
|
|
2483
|
+
};
|
|
2484
|
+
let totalItems = 0;
|
|
2485
|
+
let builtItems = 0;
|
|
2486
|
+
let updatedItems = 0;
|
|
2487
|
+
let skippedItems = 0;
|
|
2488
|
+
for (const type of RegistryTypeList) {
|
|
2489
|
+
const sourceDir = path13.join(paths.registryBase, type);
|
|
2490
|
+
const targetDir = path13.join(paths.outputBase, type);
|
|
2491
|
+
if (!await fs12.pathExists(sourceDir)) {
|
|
2492
|
+
continue;
|
|
2493
|
+
}
|
|
2494
|
+
await fs12.ensureDir(targetDir);
|
|
2495
|
+
const files = await glob2("*.json", { cwd: sourceDir });
|
|
2496
|
+
for (const file of files) {
|
|
2497
|
+
totalItems++;
|
|
2498
|
+
const sourcePath = path13.join(sourceDir, file);
|
|
2499
|
+
const item = await fs12.readJson(sourcePath);
|
|
2500
|
+
const outputPath = path13.join(targetDir, `${item.slug}.json`);
|
|
2501
|
+
const buildStatus = await getBuildStatus(
|
|
2502
|
+
sourcePath,
|
|
2503
|
+
outputPath,
|
|
2504
|
+
item,
|
|
2505
|
+
type
|
|
2506
|
+
);
|
|
2507
|
+
if (buildStatus === "skip") {
|
|
2508
|
+
skippedItems++;
|
|
2509
|
+
if (type === "tooling" || !item.runtimes?.["node"]?.frameworks) {
|
|
2510
|
+
index.items.push({
|
|
2511
|
+
type,
|
|
2512
|
+
slug: item.slug
|
|
2513
|
+
});
|
|
2514
|
+
} else {
|
|
2515
|
+
index.items.push({
|
|
2516
|
+
type,
|
|
2517
|
+
slug: item.slug,
|
|
2518
|
+
frameworks: Object.keys(
|
|
2519
|
+
item.runtimes["node"].frameworks
|
|
2520
|
+
)
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
continue;
|
|
2524
|
+
}
|
|
2525
|
+
buildSpin.text = `${buildStatus === "rebuild" ? "Building" : "Updating"} ${type}: ${item.slug}`;
|
|
2526
|
+
try {
|
|
2527
|
+
const builtItem = await processRegistryItem(item, type);
|
|
2528
|
+
await fs12.writeJson(outputPath, builtItem, { spaces: 2 });
|
|
2529
|
+
if (type === "tooling" || !builtItem.runtimes?.["node"]?.frameworks) {
|
|
2530
|
+
index.items.push({
|
|
2531
|
+
type,
|
|
2532
|
+
slug: item.slug
|
|
2533
|
+
});
|
|
2534
|
+
} else {
|
|
2535
|
+
index.items.push({
|
|
2536
|
+
type,
|
|
2537
|
+
slug: item.slug,
|
|
2538
|
+
frameworks: Object.keys(
|
|
2539
|
+
builtItem.runtimes["node"].frameworks
|
|
2540
|
+
)
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2543
|
+
if (buildStatus === "rebuild") {
|
|
2544
|
+
builtItems++;
|
|
2545
|
+
} else {
|
|
2546
|
+
updatedItems++;
|
|
2547
|
+
}
|
|
2548
|
+
} catch (error) {
|
|
2549
|
+
console.error(error);
|
|
2550
|
+
buildSpin.fail(`Failed to build ${item.slug}`);
|
|
2551
|
+
logger.error(error);
|
|
2552
|
+
buildSpin.start("Resuming build...");
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
await fs12.writeJson(path13.join(paths.outputBase, "index.json"), index, {
|
|
2557
|
+
spaces: 2
|
|
2558
|
+
});
|
|
2559
|
+
buildSpin.succeed(
|
|
2560
|
+
`${highlighter.success("Registry build completed")}
|
|
2561
|
+
Total: ${totalItems}, Built: ${builtItems}, Updated: ${updatedItems}, Skipped: ${skippedItems}`
|
|
2562
|
+
);
|
|
2563
|
+
}
|
|
2564
|
+
async function getBuildStatus(sourcePath, outputPath, item, type) {
|
|
2565
|
+
if (!await fs12.pathExists(outputPath)) return "rebuild";
|
|
2566
|
+
const sourceStat = await fs12.stat(sourcePath);
|
|
2567
|
+
const targetStat = await fs12.stat(outputPath);
|
|
2568
|
+
const templatePaths = getTemplatePathsForItem(item, type);
|
|
2569
|
+
for (const tp of templatePaths) {
|
|
2570
|
+
const absoluteTp = path13.join(paths.templateBase, tp);
|
|
2571
|
+
if (!await fs12.pathExists(absoluteTp)) continue;
|
|
2572
|
+
const tpStat = await fs12.stat(absoluteTp);
|
|
2573
|
+
if (tpStat.isDirectory()) {
|
|
2574
|
+
const files = await glob2("**/*", {
|
|
2575
|
+
cwd: absoluteTp,
|
|
2576
|
+
nodir: true,
|
|
2577
|
+
absolute: true
|
|
2578
|
+
});
|
|
2579
|
+
for (const f of files) {
|
|
2580
|
+
const fStat = await fs12.stat(f);
|
|
2581
|
+
if (fStat.mtime > targetStat.mtime) return "rebuild";
|
|
2582
|
+
}
|
|
2583
|
+
} else {
|
|
2584
|
+
if (tpStat.mtime > targetStat.mtime) return "rebuild";
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
if (sourceStat.mtime > targetStat.mtime) return "update";
|
|
2588
|
+
return "skip";
|
|
2589
|
+
}
|
|
2590
|
+
function getTemplatePathsForItem(item, type) {
|
|
2591
|
+
const templatePaths = [];
|
|
2592
|
+
const itemAny = item;
|
|
2593
|
+
if (type === "tooling") {
|
|
2594
|
+
if (itemAny.templates) {
|
|
2595
|
+
for (const tPath of Object.values(itemAny.templates)) {
|
|
2596
|
+
templatePaths.push(path13.join("tooling", tPath));
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
return templatePaths;
|
|
2600
|
+
}
|
|
2601
|
+
if (!itemAny.runtimes) return templatePaths;
|
|
2602
|
+
for (const [runtimeKey, runtime] of Object.entries(itemAny.runtimes)) {
|
|
2603
|
+
for (const [frameworkKey, framework] of Object.entries(
|
|
2604
|
+
runtime.frameworks
|
|
2605
|
+
)) {
|
|
2606
|
+
const baseRelPath = path13.join(runtimeKey, frameworkKey, type);
|
|
2607
|
+
if (framework.variants) {
|
|
2608
|
+
for (const variant of Object.values(framework.variants)) {
|
|
2609
|
+
if (variant.templates) {
|
|
2610
|
+
for (const tPath of Object.values(variant.templates)) {
|
|
2611
|
+
templatePaths.push(path13.join(baseRelPath, tPath));
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
} else if (framework.templates) {
|
|
2616
|
+
for (const tPath of Object.values(framework.templates)) {
|
|
2617
|
+
if (typeof tPath === "string") {
|
|
2618
|
+
templatePaths.push(path13.join(baseRelPath, tPath));
|
|
2619
|
+
} else if (typeof tPath === "object" && tPath !== null) {
|
|
2620
|
+
for (const archSet of Object.values(tPath)) {
|
|
2621
|
+
if (typeof archSet === "string") {
|
|
2622
|
+
templatePaths.push(path13.join(baseRelPath, archSet));
|
|
2623
|
+
} else if (typeof archSet === "object" && archSet !== null) {
|
|
2624
|
+
for (const p of Object.values(archSet)) {
|
|
2625
|
+
templatePaths.push(path13.join(baseRelPath, p));
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
} else if (framework.databases) {
|
|
2632
|
+
for (const db of Object.values(framework.databases)) {
|
|
2633
|
+
for (const orm of Object.values(db.orms)) {
|
|
2634
|
+
if (orm.templates) {
|
|
2635
|
+
for (const val of Object.values(orm.templates)) {
|
|
2636
|
+
if (typeof val === "string") {
|
|
2637
|
+
templatePaths.push(path13.join(baseRelPath, val));
|
|
2638
|
+
} else if (typeof val === "object" && val !== null) {
|
|
2639
|
+
for (const p of Object.values(val)) {
|
|
2640
|
+
templatePaths.push(path13.join(baseRelPath, p));
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
return templatePaths;
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
// src/commands/doctor.ts
|
|
2654
|
+
import fs13 from "fs-extra";
|
|
2655
|
+
import path14 from "path";
|
|
2656
|
+
var README_UPGRADE_ANCHOR = "https://github.com/AkkalDhami/servercn/blob/main/packages/cli/README.md#existing-projects-upgrades--template-drift";
|
|
2657
|
+
function printStaticGuide() {
|
|
2658
|
+
logger.section("Post-init projects & upstream changes");
|
|
2659
|
+
logger.log(
|
|
2660
|
+
"Your scaffold is a snapshot from when you ran `init`. The CLI and registry evolve separately."
|
|
2661
|
+
);
|
|
2662
|
+
logger.break();
|
|
2663
|
+
logger.log("Typical situations:");
|
|
2664
|
+
logger.log(
|
|
2665
|
+
" \u2022 CLI-only bump (bugfixes): usually no repo changes unless you hit a specific fix."
|
|
2666
|
+
);
|
|
2667
|
+
logger.log(
|
|
2668
|
+
" \u2022 Registry/template updates (new markers, `--merge`, paths): read release notes; then copy markers from docs, use `add --merge`, or `--force` / new project."
|
|
2669
|
+
);
|
|
2670
|
+
logger.log(
|
|
2671
|
+
" \u2022 Foundation changes: no automatic sync \u2014 diff manually or start a fresh project."
|
|
2672
|
+
);
|
|
2673
|
+
logger.break();
|
|
2674
|
+
logger.info(`Full matrix: ${README_UPGRADE_ANCHOR}`);
|
|
2675
|
+
logger.info(`Installation / upgrades: ${SERVERCN_URL}/docs/installation`);
|
|
2676
|
+
logger.break();
|
|
2677
|
+
}
|
|
2678
|
+
async function checkMergeMarkers(config, projectRoot) {
|
|
2679
|
+
const arch = config.stack?.architecture ?? "mvc";
|
|
2680
|
+
const appPath = path14.join(projectRoot, "src", "app.ts");
|
|
2681
|
+
const appSlugs = arch === "feature" ? ["rate-limiter", "security-header"] : ["rate-limiter", "security-header", "async-handler"];
|
|
2682
|
+
if (await fs13.pathExists(appPath)) {
|
|
2683
|
+
const text = await fs13.readFile(appPath, "utf8");
|
|
2684
|
+
for (const slug of appSlugs) {
|
|
2685
|
+
const line = markerBeginLine(slug);
|
|
2686
|
+
if (!text.includes(line)) {
|
|
2687
|
+
logger.warn(
|
|
2688
|
+
`Missing ${line} in src/app.ts \u2014 add it (see README) before using add ${slug} --merge.`
|
|
2689
|
+
);
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
} else {
|
|
2693
|
+
logger.muted("No src/app.ts found; skipping app marker checks.");
|
|
2694
|
+
}
|
|
2695
|
+
if (arch === "feature") {
|
|
2696
|
+
const routesPath = path14.join(projectRoot, "src", "routes", "index.ts");
|
|
2697
|
+
if (await fs13.pathExists(routesPath)) {
|
|
2698
|
+
const text = await fs13.readFile(routesPath, "utf8");
|
|
2699
|
+
const line = markerBeginLine("async-handler");
|
|
2700
|
+
if (!text.includes(line)) {
|
|
2701
|
+
logger.warn(
|
|
2702
|
+
`Missing ${line} in src/routes/index.ts \u2014 required for async-handler --merge (feature architecture).`
|
|
2703
|
+
);
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
async function doctor() {
|
|
2709
|
+
printStaticGuide();
|
|
2710
|
+
const configPath = path14.resolve(process.cwd(), SERVERCN_CONFIG_FILE);
|
|
2711
|
+
if (!await fs13.pathExists(configPath)) {
|
|
2712
|
+
logger.muted("No servercn.config.json in this directory \u2014 marker checks skipped.");
|
|
2713
|
+
logger.break();
|
|
2714
|
+
return;
|
|
2715
|
+
}
|
|
2716
|
+
const config = await fs13.readJSON(configPath);
|
|
2717
|
+
const rootDir = config.project?.rootDir ?? ".";
|
|
2718
|
+
const projectRoot = path14.resolve(process.cwd(), rootDir);
|
|
2719
|
+
logger.section("Merge marker sanity (optional)");
|
|
2720
|
+
await checkMergeMarkers(config, projectRoot);
|
|
2721
|
+
logger.break();
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
// src/cli.ts
|
|
2725
|
+
var program = new Command();
|
|
2726
|
+
process.on("SIGINT", () => process.exit(0));
|
|
2727
|
+
process.on("SIGTERM", () => process.exit(0));
|
|
2728
|
+
async function main() {
|
|
2729
|
+
program.name("servercn-cli").description("Scaffold and manage backend components for Node.js projects").version(LATEST_VERSION, "-v, --version", "output the current version");
|
|
2730
|
+
program.command("init [foundation]").description("Initialize ServerCN in the current project").option("-f, --force", "Overwrite existing files if they exist").option("--fw <framework>", "Framework type: express or nestjs", "express").option(
|
|
2731
|
+
"--local",
|
|
2732
|
+
"Add registry items from local environment(development runtime)"
|
|
2733
|
+
).action(init);
|
|
2734
|
+
registryListCommands(program);
|
|
2735
|
+
program.command("doctor").description(
|
|
2736
|
+
"Print upgrade guidance and optional merge-marker checks for this project"
|
|
2737
|
+
).action(doctor);
|
|
2738
|
+
program.command("build").description("Build the project").option("--name <name>", "App name, website name").option("--url <url>", "App URL, website URL").action(async (options) => await build(options));
|
|
2739
|
+
program.command("add <components...>").description("Add one or more backend components to your project").option("--arch <arch>", "Project architecture: mvc or feature", "mvc").option("-f, --force", "Force overwrite existing files").option(
|
|
2740
|
+
"--merge",
|
|
2741
|
+
"Merge merge-only fragments (// @servercn:begin/end <slug>) into existing files"
|
|
2742
|
+
).option(
|
|
2743
|
+
"--local",
|
|
2744
|
+
"Add registry items from local environment(development runtime)"
|
|
2745
|
+
).action(
|
|
2746
|
+
async (components, options) => {
|
|
2747
|
+
let type = "component";
|
|
2748
|
+
let items = components;
|
|
2749
|
+
if (["schema", "sc"].includes(components[0])) {
|
|
2750
|
+
type = "schema";
|
|
2751
|
+
items = components.slice(1).map((item) => {
|
|
2752
|
+
return item;
|
|
2753
|
+
});
|
|
2754
|
+
} else if (["blueprint", "bp"].includes(components[0])) {
|
|
2755
|
+
type = "blueprint";
|
|
2756
|
+
items = components.slice(1);
|
|
2757
|
+
} else if (["tooling", "tl"].includes(components[0])) {
|
|
2758
|
+
type = "tooling";
|
|
2759
|
+
items = components.slice(1);
|
|
2760
|
+
}
|
|
2761
|
+
for (const item of items) {
|
|
2762
|
+
await add(item, {
|
|
2763
|
+
arch: options.arch,
|
|
2764
|
+
type,
|
|
2765
|
+
force: options.force,
|
|
2766
|
+
merge: options.merge,
|
|
2767
|
+
local: options.local
|
|
2768
|
+
});
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
);
|
|
2772
|
+
program.parse(process.argv);
|
|
2773
|
+
}
|
|
2774
|
+
main().catch((err) => {
|
|
2775
|
+
console.error(err);
|
|
2776
|
+
process.exit(1);
|
|
2777
|
+
});
|
|
2778
|
+
//# sourceMappingURL=cli.js.map
|