@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/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