@solid-stack/mason 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +238 -0
  3. package/dist/bin.cjs +3480 -0
  4. package/dist/bin.cjs.map +1 -0
  5. package/dist/bin.d.ts +3 -0
  6. package/dist/bin.d.ts.map +1 -0
  7. package/dist/bin.js +3451 -0
  8. package/dist/bin.js.map +1 -0
  9. package/dist/cli.d.ts +3 -0
  10. package/dist/cli.d.ts.map +1 -0
  11. package/dist/commands/create.d.ts +2 -0
  12. package/dist/commands/create.d.ts.map +1 -0
  13. package/dist/commands/domain.d.ts +2 -0
  14. package/dist/commands/domain.d.ts.map +1 -0
  15. package/dist/commands/feature.d.ts +2 -0
  16. package/dist/commands/feature.d.ts.map +1 -0
  17. package/dist/commands/httpHandler.d.ts +2 -0
  18. package/dist/commands/httpHandler.d.ts.map +1 -0
  19. package/dist/commands/import.d.ts +10 -0
  20. package/dist/commands/import.d.ts.map +1 -0
  21. package/dist/commands/infrastructure.d.ts +2 -0
  22. package/dist/commands/infrastructure.d.ts.map +1 -0
  23. package/dist/commands/injectable.d.ts +2 -0
  24. package/dist/commands/injectable.d.ts.map +1 -0
  25. package/dist/commands/interface.d.ts +2 -0
  26. package/dist/commands/interface.d.ts.map +1 -0
  27. package/dist/commands/menu.d.ts +2 -0
  28. package/dist/commands/menu.d.ts.map +1 -0
  29. package/dist/commands/usecase.d.ts +2 -0
  30. package/dist/commands/usecase.d.ts.map +1 -0
  31. package/dist/core/domainGenerator.d.ts +4 -0
  32. package/dist/core/domainGenerator.d.ts.map +1 -0
  33. package/dist/core/featureGenerator.d.ts +3 -0
  34. package/dist/core/featureGenerator.d.ts.map +1 -0
  35. package/dist/core/httpHandlerGenerator.d.ts +8 -0
  36. package/dist/core/httpHandlerGenerator.d.ts.map +1 -0
  37. package/dist/core/infraGenerator.d.ts +60 -0
  38. package/dist/core/infraGenerator.d.ts.map +1 -0
  39. package/dist/core/injectableGenerator.d.ts +4 -0
  40. package/dist/core/injectableGenerator.d.ts.map +1 -0
  41. package/dist/core/interfaceGenerator.d.ts +4 -0
  42. package/dist/core/interfaceGenerator.d.ts.map +1 -0
  43. package/dist/core/providerManager.d.ts +16 -0
  44. package/dist/core/providerManager.d.ts.map +1 -0
  45. package/dist/core/useCaseGenerator.d.ts +5 -0
  46. package/dist/core/useCaseGenerator.d.ts.map +1 -0
  47. package/dist/index.cjs +3545 -0
  48. package/dist/index.cjs.map +1 -0
  49. package/dist/index.d.ts +19 -0
  50. package/dist/index.d.ts.map +1 -0
  51. package/dist/index.js +3444 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/templates/domain/class.hbs +17 -0
  54. package/dist/templates/domain/interface.hbs +5 -0
  55. package/dist/templates/domain/valueObject.hbs +15 -0
  56. package/dist/templates/domain.template.d.ts +4 -0
  57. package/dist/templates/domain.template.d.ts.map +1 -0
  58. package/dist/templates/engine.d.ts +20 -0
  59. package/dist/templates/engine.d.ts.map +1 -0
  60. package/dist/templates/express/index.hbs +1 -0
  61. package/dist/templates/expressIndex.template.d.ts +2 -0
  62. package/dist/templates/expressIndex.template.d.ts.map +1 -0
  63. package/dist/templates/httpHandler/handler.hbs +71 -0
  64. package/dist/templates/httpHandler.template.d.ts +2 -0
  65. package/dist/templates/httpHandler.template.d.ts.map +1 -0
  66. package/dist/templates/index.d.ts +10 -0
  67. package/dist/templates/index.d.ts.map +1 -0
  68. package/dist/templates/infra/concrete.hbs +29 -0
  69. package/dist/templates/infra/stub.hbs +103 -0
  70. package/dist/templates/infra.template.d.ts +3 -0
  71. package/dist/templates/infra.template.d.ts.map +1 -0
  72. package/dist/templates/injectable/injectable.hbs +32 -0
  73. package/dist/templates/injectable/interface.hbs +3 -0
  74. package/dist/templates/injectable.template.d.ts +3 -0
  75. package/dist/templates/injectable.template.d.ts.map +1 -0
  76. package/dist/templates/interface/interface.hbs +19 -0
  77. package/dist/templates/interface.template.d.ts +2 -0
  78. package/dist/templates/interface.template.d.ts.map +1 -0
  79. package/dist/templates/provider/provider.hbs +13 -0
  80. package/dist/templates/provider.template.d.ts +2 -0
  81. package/dist/templates/provider.template.d.ts.map +1 -0
  82. package/dist/templates/useCase/useCase.hbs +57 -0
  83. package/dist/templates/useCase/useCaseTest.hbs +24 -0
  84. package/dist/templates/useCase.template.d.ts +3 -0
  85. package/dist/templates/useCase.template.d.ts.map +1 -0
  86. package/dist/types/index.d.ts +129 -0
  87. package/dist/types/index.d.ts.map +1 -0
  88. package/dist/utils/fileOperations.d.ts +8 -0
  89. package/dist/utils/fileOperations.d.ts.map +1 -0
  90. package/dist/utils/formatting.d.ts +43 -0
  91. package/dist/utils/formatting.d.ts.map +1 -0
  92. package/dist/utils/project.d.ts +37 -0
  93. package/dist/utils/project.d.ts.map +1 -0
  94. package/dist/utils/prompts.d.ts +14 -0
  95. package/dist/utils/prompts.d.ts.map +1 -0
  96. package/dist/utils/registry.d.ts +61 -0
  97. package/dist/utils/registry.d.ts.map +1 -0
  98. package/package.json +84 -0
  99. package/templates/domain/class.hbs +17 -0
  100. package/templates/domain/interface.hbs +5 -0
  101. package/templates/domain/valueObject.hbs +15 -0
  102. package/templates/express/index.hbs +1 -0
  103. package/templates/httpHandler/handler.hbs +71 -0
  104. package/templates/infra/concrete.hbs +29 -0
  105. package/templates/infra/stub.hbs +103 -0
  106. package/templates/injectable/injectable.hbs +32 -0
  107. package/templates/injectable/interface.hbs +3 -0
  108. package/templates/interface/interface.hbs +19 -0
  109. package/templates/provider/provider.hbs +13 -0
  110. package/templates/useCase/useCase.hbs +57 -0
  111. package/templates/useCase/useCaseTest.hbs +24 -0
package/dist/bin.js ADDED
@@ -0,0 +1,3451 @@
1
+ #!/usr/bin/env node
2
+ import { cac } from 'cac';
3
+ import * as p3 from '@clack/prompts';
4
+ import pc13 from 'picocolors';
5
+ import path6 from 'path';
6
+ import fs7 from 'fs';
7
+ import { fileURLToPath } from 'url';
8
+ import Handlebars from 'handlebars';
9
+ import { execSync } from 'child_process';
10
+
11
+ function writeFileSafely(absolutePath, content, options = {}) {
12
+ const projectRoot = options.projectRoot || process.cwd();
13
+ const relPath = path6.relative(projectRoot, absolutePath).replace(/\\/g, "/");
14
+ const existedBefore = fs7.existsSync(absolutePath);
15
+ if (options.dryRun) {
16
+ return {
17
+ relativePath: relPath,
18
+ absolutePath,
19
+ content,
20
+ action: "preview",
21
+ existedBefore
22
+ };
23
+ }
24
+ if (existedBefore && !options.overwrite) {
25
+ return {
26
+ relativePath: relPath,
27
+ absolutePath,
28
+ content,
29
+ action: "skipped",
30
+ existedBefore: true
31
+ };
32
+ }
33
+ const dir = path6.dirname(absolutePath);
34
+ if (!fs7.existsSync(dir)) {
35
+ fs7.mkdirSync(dir, { recursive: true });
36
+ }
37
+ fs7.writeFileSync(absolutePath, content, "utf-8");
38
+ return {
39
+ relativePath: relPath,
40
+ absolutePath,
41
+ content,
42
+ action: existedBefore ? "overwritten" : "created",
43
+ existedBefore
44
+ };
45
+ }
46
+ function printGeneratedFiles(files, isDryRun = false) {
47
+ console.log("");
48
+ if (isDryRun) {
49
+ p3.log.warn(pc13.yellow(pc13.bold("[DRY RUN PREVIEW] No files were written to disk.")));
50
+ }
51
+ for (const f of files) {
52
+ if (f.action === "created") {
53
+ p3.log.success(`${pc13.bold(f.relativePath)} ${pc13.dim("(created)")}`);
54
+ } else if (f.action === "overwritten") {
55
+ p3.log.warn(`${pc13.bold(f.relativePath)} ${pc13.dim("(overwritten)")}`);
56
+ } else if (f.action === "skipped") {
57
+ p3.log.info(`${pc13.dim(f.relativePath)} ${pc13.dim("(skipped - already exists, pass -o to overwrite)")}`);
58
+ } else {
59
+ p3.log.info(`${pc13.bold(f.relativePath)} ${pc13.dim("(preview)")}`);
60
+ }
61
+ }
62
+ }
63
+
64
+ // src/utils/formatting.ts
65
+ function toPascalCase(str) {
66
+ if (!str) return "";
67
+ return str.replace(/[^\w\s-]/g, "").replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^[a-z]/, (c) => c.toUpperCase());
68
+ }
69
+ function toCamelCase(str) {
70
+ if (!str) return "";
71
+ let s = str;
72
+ if (/^I[A-Z]/.test(s)) {
73
+ s = s.slice(1);
74
+ }
75
+ const pascal = toPascalCase(s);
76
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
77
+ }
78
+ function toKebabCase(str) {
79
+ if (!str) return "";
80
+ return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[-_\s]+/g, "-").toLowerCase();
81
+ }
82
+ function toSingular(str) {
83
+ if (!str) return "";
84
+ const lower = str.toLowerCase();
85
+ if (lower.endsWith("ies")) {
86
+ return str.slice(0, -3) + (str.endsWith("IES") ? "Y" : "y");
87
+ }
88
+ if (lower.endsWith("ses") && !lower.endsWith("sses")) {
89
+ return str.slice(0, -2);
90
+ }
91
+ if (lower.endsWith("s") && !lower.endsWith("ss")) {
92
+ return str.slice(0, -1);
93
+ }
94
+ return str;
95
+ }
96
+ function toPlural(str) {
97
+ if (!str) return "";
98
+ const lower = str.toLowerCase();
99
+ if (lower.endsWith("y") && !/[aeiou]y$/i.test(lower)) {
100
+ return str.slice(0, -1) + (str.endsWith("Y") ? "IES" : "ies");
101
+ }
102
+ if (lower.endsWith("s") || lower.endsWith("x") || lower.endsWith("z") || lower.endsWith("ch") || lower.endsWith("sh")) {
103
+ return str + (str === str.toUpperCase() ? "ES" : "es");
104
+ }
105
+ return str + (str === str.toUpperCase() ? "S" : "s");
106
+ }
107
+ function ensureInterfacePrefix(name) {
108
+ const pascal = toPascalCase(name);
109
+ if (/^I[A-Z]/.test(pascal)) {
110
+ return pascal;
111
+ }
112
+ return `I${pascal}`;
113
+ }
114
+ function removeInterfacePrefix(name) {
115
+ if (/^I[A-Z]/.test(name)) {
116
+ return name.slice(1);
117
+ }
118
+ return name;
119
+ }
120
+ function toStubName(ifaceName) {
121
+ const base = removeInterfacePrefix(ifaceName);
122
+ if (base.startsWith("Stub")) {
123
+ return base;
124
+ }
125
+ return `Stub${base}`;
126
+ }
127
+ function findProjectRoot(startDir = process.cwd()) {
128
+ let current = path6.resolve(startDir);
129
+ while (true) {
130
+ if (fs7.existsSync(path6.join(current, "package.json"))) {
131
+ return current;
132
+ }
133
+ const parent = path6.dirname(current);
134
+ if (parent === current) {
135
+ return path6.resolve(startDir);
136
+ }
137
+ current = parent;
138
+ }
139
+ }
140
+ function resolveFeaturesDir(projectRoot) {
141
+ return path6.join(projectRoot, "features");
142
+ }
143
+ function listExistingFeatures(projectRoot) {
144
+ const featuresDir = resolveFeaturesDir(projectRoot);
145
+ if (!fs7.existsSync(featuresDir)) return [];
146
+ try {
147
+ const entries = fs7.readdirSync(featuresDir, { withFileTypes: true });
148
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
149
+ } catch {
150
+ return [];
151
+ }
152
+ }
153
+ function listDomainInterfaces(projectRoot, featureName) {
154
+ const results = [];
155
+ const features = featureName ? [featureName] : listExistingFeatures(projectRoot);
156
+ for (const feat of features) {
157
+ const domainDir = path6.join(resolveFeaturesDir(projectRoot), feat, "domain");
158
+ if (!fs7.existsSync(domainDir)) continue;
159
+ try {
160
+ const files = fs7.readdirSync(domainDir);
161
+ for (const file of files) {
162
+ if (file.endsWith(".ts") && !file.endsWith(".test.ts") && !file.endsWith(".d.ts")) {
163
+ const name = file.replace(/\.ts$/, "");
164
+ const fullPath = path6.join(domainDir, file);
165
+ if (/^I[A-Z]/.test(name)) {
166
+ results.push({
167
+ name,
168
+ fullPath,
169
+ feature: feat
170
+ });
171
+ } else {
172
+ try {
173
+ const content = fs7.readFileSync(fullPath, "utf-8");
174
+ if (/export\s+(?:abstract\s+class|interface)\s+(\w+)/.test(content)) {
175
+ results.push({
176
+ name,
177
+ fullPath,
178
+ feature: feat
179
+ });
180
+ }
181
+ } catch {
182
+ }
183
+ }
184
+ }
185
+ }
186
+ } catch {
187
+ }
188
+ }
189
+ return results;
190
+ }
191
+ function listDomainEntities(projectRoot, featureName) {
192
+ const results = [];
193
+ const features = featureName ? [featureName] : listExistingFeatures(projectRoot);
194
+ for (const feat of features) {
195
+ const domainDir = path6.join(resolveFeaturesDir(projectRoot), feat, "domain");
196
+ if (!fs7.existsSync(domainDir)) continue;
197
+ try {
198
+ const files = fs7.readdirSync(domainDir);
199
+ for (const file of files) {
200
+ if (file.endsWith(".ts") && !file.endsWith(".test.ts") && !file.endsWith(".d.ts")) {
201
+ const name = file.replace(/\.ts$/, "");
202
+ if (!/^I[A-Z]/.test(name)) {
203
+ results.push({
204
+ name,
205
+ fullPath: path6.join(domainDir, file),
206
+ feature: feat
207
+ });
208
+ }
209
+ }
210
+ }
211
+ } catch {
212
+ }
213
+ }
214
+ return results;
215
+ }
216
+ function listUseCases(projectRoot, featureName) {
217
+ const results = [];
218
+ const features = featureName ? [featureName] : listExistingFeatures(projectRoot);
219
+ for (const feat of features) {
220
+ const useCasesDir = path6.join(resolveFeaturesDir(projectRoot), feat, "useCases");
221
+ if (!fs7.existsSync(useCasesDir)) continue;
222
+ try {
223
+ const files = fs7.readdirSync(useCasesDir);
224
+ for (const file of files) {
225
+ if (file.endsWith(".ts") && !file.endsWith(".test.ts") && !file.endsWith(".d.ts")) {
226
+ const name = file.replace(/\.ts$/, "");
227
+ results.push({
228
+ name,
229
+ fullPath: path6.join(useCasesDir, file),
230
+ feature: feat
231
+ });
232
+ }
233
+ }
234
+ } catch {
235
+ }
236
+ }
237
+ return results;
238
+ }
239
+ var hbs = Handlebars.create();
240
+ hbs.registerHelper("eq", (a, b) => a === b);
241
+ hbs.registerHelper("ne", (a, b) => a !== b);
242
+ hbs.registerHelper("not", (a) => !a);
243
+ hbs.registerHelper("and", (...args) => args.slice(0, -1).every(Boolean));
244
+ hbs.registerHelper("or", (...args) => args.slice(0, -1).some(Boolean));
245
+ hbs.registerHelper("pascalCase", (str) => toPascalCase(str));
246
+ hbs.registerHelper("camelCase", (str) => toCamelCase(str));
247
+ hbs.registerHelper("kebabCase", (str) => toKebabCase(str));
248
+ hbs.registerHelper("join", (arr, sep) => {
249
+ if (!Array.isArray(arr)) return "";
250
+ const delimiter = typeof sep === "string" ? sep : ", ";
251
+ return arr.join(delimiter);
252
+ });
253
+ var cachedTemplatesDir = null;
254
+ function getTemplatesDir() {
255
+ if (cachedTemplatesDir && fs7.existsSync(cachedTemplatesDir)) {
256
+ return cachedTemplatesDir;
257
+ }
258
+ const currentDir = typeof __dirname !== "undefined" ? __dirname : path6.dirname(fileURLToPath(import.meta.url));
259
+ const cand1 = path6.resolve(currentDir, "../../templates");
260
+ if (fs7.existsSync(cand1)) {
261
+ cachedTemplatesDir = cand1;
262
+ return cand1;
263
+ }
264
+ const cand2 = path6.resolve(currentDir, "../templates");
265
+ if (fs7.existsSync(cand2)) {
266
+ cachedTemplatesDir = cand2;
267
+ return cand2;
268
+ }
269
+ const cand3 = path6.resolve(currentDir, "templates");
270
+ if (fs7.existsSync(cand3)) {
271
+ cachedTemplatesDir = cand3;
272
+ return cand3;
273
+ }
274
+ let curr = currentDir;
275
+ while (true) {
276
+ const candTemplates = path6.join(curr, "templates");
277
+ if (fs7.existsSync(candTemplates)) {
278
+ cachedTemplatesDir = candTemplates;
279
+ return candTemplates;
280
+ }
281
+ const parent = path6.dirname(curr);
282
+ if (parent === curr) break;
283
+ curr = parent;
284
+ }
285
+ throw new Error("Could not locate templates directory for @solid-stack/create");
286
+ }
287
+ var templateCache = /* @__PURE__ */ new Map();
288
+ function loadTemplate(relativePath) {
289
+ const fullPath = path6.isAbsolute(relativePath) ? relativePath : path6.join(getTemplatesDir(), relativePath);
290
+ return fs7.readFileSync(fullPath, "utf-8");
291
+ }
292
+ function renderTemplateFile(relativePath, context) {
293
+ let compiled = templateCache.get(relativePath);
294
+ if (!compiled) {
295
+ const source = loadTemplate(relativePath);
296
+ compiled = hbs.compile(source.trim(), { noEscape: true });
297
+ templateCache.set(relativePath, compiled);
298
+ }
299
+ const result = compiled(context);
300
+ return result.endsWith("\n") ? result : result + "\n";
301
+ }
302
+ function renderTemplate(sourceOrPath, context) {
303
+ if (sourceOrPath.endsWith(".hbs")) {
304
+ return renderTemplateFile(sourceOrPath, context);
305
+ }
306
+ let compiled = templateCache.get(sourceOrPath);
307
+ if (!compiled) {
308
+ compiled = hbs.compile(sourceOrPath.trim(), { noEscape: true });
309
+ templateCache.set(sourceOrPath, compiled);
310
+ }
311
+ const result = compiled(context);
312
+ return result.endsWith("\n") ? result : result + "\n";
313
+ }
314
+
315
+ // src/templates/domain.template.ts
316
+ var domainInterfaceTemplate = loadTemplate("domain/interface.hbs");
317
+ var domainClassTemplate = loadTemplate("domain/class.hbs");
318
+ var domainValueObjectTemplate = loadTemplate("domain/valueObject.hbs");
319
+
320
+ // src/templates/interface.template.ts
321
+ var interfaceTemplate = loadTemplate("interface/interface.hbs");
322
+
323
+ // src/templates/useCase.template.ts
324
+ var useCaseTemplate = loadTemplate("useCase/useCase.hbs");
325
+ var useCaseTestTemplate = loadTemplate("useCase/useCaseTest.hbs");
326
+
327
+ // src/templates/infra.template.ts
328
+ var concreteInfraTemplate = loadTemplate("infra/concrete.hbs");
329
+ var stubInfraTemplate = loadTemplate("infra/stub.hbs");
330
+
331
+ // src/templates/httpHandler.template.ts
332
+ var httpHandlerTemplate = loadTemplate("httpHandler/handler.hbs");
333
+
334
+ // src/templates/injectable.template.ts
335
+ var injectableTemplate = loadTemplate("injectable/injectable.hbs");
336
+ var injectableInterfaceTemplate = loadTemplate("injectable/interface.hbs");
337
+
338
+ // src/templates/provider.template.ts
339
+ var diProviderTemplate = loadTemplate("provider/provider.hbs");
340
+
341
+ // src/templates/expressIndex.template.ts
342
+ var expressIndexTemplate = loadTemplate("express/index.hbs");
343
+
344
+ // src/core/domainGenerator.ts
345
+ function generateDomainEntityCode(name, kind = "interface", properties = []) {
346
+ const entityName = toPascalCase(name);
347
+ const defaultProps = [
348
+ { name: "id", type: "string" },
349
+ { name: "createdAt", type: "Date" },
350
+ { name: "updatedAt", type: "Date" }
351
+ ];
352
+ const effectiveProps = properties.length > 0 ? properties : defaultProps;
353
+ if (kind === "interface") {
354
+ return renderTemplate(domainInterfaceTemplate, {
355
+ name: entityName,
356
+ properties: effectiveProps
357
+ });
358
+ }
359
+ if (kind === "class") {
360
+ return renderTemplate(domainClassTemplate, {
361
+ name: entityName,
362
+ properties: effectiveProps
363
+ });
364
+ }
365
+ if (kind === "valueObject") {
366
+ const valueProp = effectiveProps[0] || { name: "value", type: "string" };
367
+ return renderTemplate(domainValueObjectTemplate, {
368
+ name: entityName,
369
+ valueProp
370
+ });
371
+ }
372
+ return "";
373
+ }
374
+ function generateDomain(options) {
375
+ const projectRoot = options.projectRoot || findProjectRoot();
376
+ const entityName = toPascalCase(options.name);
377
+ const targetDir = options.targetDir || path6.join(resolveFeaturesDir(projectRoot), options.feature, "domain");
378
+ const filePath = path6.join(targetDir, `${entityName}.ts`);
379
+ const code = generateDomainEntityCode(
380
+ entityName,
381
+ options.kind || "interface",
382
+ options.properties || []
383
+ );
384
+ return writeFileSafely(filePath, code, {
385
+ projectRoot,
386
+ dryRun: options.dryRun,
387
+ overwrite: options.overwrite
388
+ });
389
+ }
390
+ function generateInterfaceCode(ifaceName, kind = "abstractClass", entityName, methodsPreset = "crud", customMethods = []) {
391
+ const validIfaceName = ensureInterfacePrefix(ifaceName);
392
+ const baseEntity = entityName ? toPascalCase(entityName) : toPascalCase(removeInterfacePrefix(validIfaceName).replace(/Repository$|Service$|Client$/, ""));
393
+ const isCrud = methodsPreset === "crud" && Boolean(baseEntity);
394
+ const isCustom = methodsPreset === "custom" && customMethods.length > 0;
395
+ const isAbstractClass = kind === "abstractClass";
396
+ const methodPrefix = isAbstractClass ? "abstract " : "";
397
+ const domainImport = isCrud ? baseEntity : void 0;
398
+ const entityKey = baseEntity ? baseEntity.charAt(0).toLowerCase() + baseEntity.slice(1) : "";
399
+ const idKey = `${entityKey}Id`;
400
+ const listKey = `${entityKey}s`;
401
+ const formattedCustomMethods = customMethods.map((m) => {
402
+ const params = m.params && m.params.length > 0 ? m.params.map((p15) => `${p15.name}: ${p15.type}`).join("; ") : "";
403
+ const returnType = m.returnType ? `Promise<${m.returnType}>` : "Promise<void>";
404
+ return {
405
+ name: m.name,
406
+ params,
407
+ returnType
408
+ };
409
+ });
410
+ return renderTemplate(interfaceTemplate, {
411
+ ifaceName: validIfaceName,
412
+ isAbstractClass,
413
+ domainImport,
414
+ isCrud,
415
+ baseEntity,
416
+ idKey,
417
+ entityKey,
418
+ listKey,
419
+ methodPrefix,
420
+ isCustom,
421
+ customMethods: formattedCustomMethods
422
+ });
423
+ }
424
+ function generateInterface(options) {
425
+ const projectRoot = options.projectRoot || findProjectRoot();
426
+ const ifaceName = ensureInterfacePrefix(options.name);
427
+ const targetDir = options.targetDir || path6.join(resolveFeaturesDir(projectRoot), options.feature, "domain");
428
+ const filePath = path6.join(targetDir, `${ifaceName}.ts`);
429
+ const code = generateInterfaceCode(
430
+ ifaceName,
431
+ options.kind || "abstractClass",
432
+ options.entityName,
433
+ options.methodsPreset || "crud",
434
+ options.customMethods || []
435
+ );
436
+ return writeFileSafely(filePath, code, {
437
+ projectRoot,
438
+ dryRun: options.dryRun,
439
+ overwrite: options.overwrite
440
+ });
441
+ }
442
+ function parseInterfaceMethodsAndImports(content, targetIfaceName) {
443
+ const imports = [];
444
+ const methods = [];
445
+ let extendsClass;
446
+ const importRegex = /import\s+(type\s+)?(?:(\w+)|\{([^}]+)\}|\*\s+as\s+(\w+))\s+from\s+["']([^"']+)["'];?/g;
447
+ let impMatch;
448
+ while ((impMatch = importRegex.exec(content)) !== null) {
449
+ const isGlobalType = Boolean(impMatch[1]);
450
+ const defaultImport = impMatch[2];
451
+ const namedImports = impMatch[3];
452
+ const namespaceImport = impMatch[4];
453
+ const specifier = impMatch[5];
454
+ const items = [];
455
+ if (defaultImport) {
456
+ items.push({ name: defaultImport, isTypeOnly: isGlobalType ? true : void 0 });
457
+ } else if (namespaceImport) {
458
+ items.push({ name: namespaceImport, isTypeOnly: isGlobalType ? true : void 0 });
459
+ } else if (namedImports) {
460
+ const parts = namedImports.split(",");
461
+ for (const part of parts) {
462
+ const trimmed = part.trim();
463
+ if (!trimmed) continue;
464
+ const typeMatch = trimmed.match(/^type\s+(\w+)(?:\s+as\s+\w+)?$/);
465
+ const normalMatch = trimmed.match(/^(\w+)(?:\s+as\s+\w+)?$/);
466
+ if (typeMatch) {
467
+ items.push({ name: typeMatch[1], isTypeOnly: true });
468
+ } else if (normalMatch) {
469
+ items.push({ name: normalMatch[1], isTypeOnly: isGlobalType ? true : void 0 });
470
+ }
471
+ }
472
+ }
473
+ if (items.length > 0) {
474
+ imports.push({
475
+ specifier,
476
+ items,
477
+ isTypeOnly: isGlobalType
478
+ });
479
+ }
480
+ }
481
+ let classRegex;
482
+ if (targetIfaceName) {
483
+ classRegex = new RegExp(
484
+ `(?:export\\s+)?(?:abstract\\s+class|interface)\\s+(${targetIfaceName})\\b([^{]*)\\{`,
485
+ "g"
486
+ );
487
+ } else {
488
+ classRegex = /(?:export\\s+)?(?:abstract\\s+class|interface)\\s+([A-Za-z0-9_$]+)\b([^{]*)\{/g;
489
+ }
490
+ const declMatch = classRegex.exec(content);
491
+ if (!declMatch) {
492
+ return {
493
+ className: targetIfaceName || "",
494
+ methods: [],
495
+ imports
496
+ };
497
+ }
498
+ const className = declMatch[1];
499
+ const headerRemainder = declMatch[2] || "";
500
+ const headerEnd = declMatch.index + declMatch[0].length;
501
+ const extMatch = headerRemainder.match(/\bextends\s+([A-Za-z0-9_$]+)\b/);
502
+ if (extMatch) {
503
+ extendsClass = extMatch[1];
504
+ }
505
+ let depth = 1;
506
+ let bodyEnd = -1;
507
+ for (let i = headerEnd; i < content.length; i++) {
508
+ const ch = content[i];
509
+ if (ch === "/" && content[i + 1] === "/") {
510
+ const nl = content.indexOf("\n", i);
511
+ i = nl === -1 ? content.length : nl;
512
+ continue;
513
+ }
514
+ if (ch === "/" && content[i + 1] === "*") {
515
+ const closeComment = content.indexOf("*/", i + 2);
516
+ i = closeComment === -1 ? content.length : closeComment + 1;
517
+ continue;
518
+ }
519
+ if (ch === '"' || ch === "'" || ch === "`") {
520
+ const quote = ch;
521
+ i++;
522
+ while (i < content.length && content[i] !== quote) {
523
+ if (content[i] === "\\") i++;
524
+ i++;
525
+ }
526
+ continue;
527
+ }
528
+ if (ch === "{") {
529
+ depth++;
530
+ } else if (ch === "}") {
531
+ depth--;
532
+ if (depth === 0) {
533
+ bodyEnd = i;
534
+ break;
535
+ }
536
+ }
537
+ }
538
+ if (bodyEnd === -1) {
539
+ bodyEnd = content.length;
540
+ }
541
+ const body = content.slice(headerEnd, bodyEnd);
542
+ const statements = [];
543
+ let currentStmt = "";
544
+ let pDepth = 0;
545
+ let bDepth = 0;
546
+ let sDepth = 0;
547
+ let aDepth = 0;
548
+ for (let i = 0; i < body.length; i++) {
549
+ const ch = body[i];
550
+ if (ch === "/" && body[i + 1] === "/") {
551
+ const nl = body.indexOf("\n", i);
552
+ i = nl === -1 ? body.length : nl;
553
+ continue;
554
+ }
555
+ if (ch === "/" && body[i + 1] === "*") {
556
+ const closeComment = body.indexOf("*/", i + 2);
557
+ i = closeComment === -1 ? body.length : closeComment + 1;
558
+ continue;
559
+ }
560
+ if (ch === '"' || ch === "'" || ch === "`") {
561
+ const quote = ch;
562
+ currentStmt += ch;
563
+ i++;
564
+ while (i < body.length && body[i] !== quote) {
565
+ if (body[i] === "\\") {
566
+ currentStmt += body[i];
567
+ i++;
568
+ }
569
+ if (i < body.length) currentStmt += body[i];
570
+ i++;
571
+ }
572
+ if (i < body.length) currentStmt += body[i];
573
+ continue;
574
+ }
575
+ if (ch === "(") pDepth++;
576
+ else if (ch === ")") pDepth = Math.max(0, pDepth - 1);
577
+ else if (ch === "{") bDepth++;
578
+ else if (ch === "}") bDepth = Math.max(0, bDepth - 1);
579
+ else if (ch === "[") sDepth++;
580
+ else if (ch === "]") sDepth = Math.max(0, sDepth - 1);
581
+ else if (ch === "<") aDepth++;
582
+ else if (ch === ">") aDepth = Math.max(0, aDepth - 1);
583
+ if (ch === ";" && pDepth === 0 && bDepth === 0 && sDepth === 0 && aDepth === 0) {
584
+ if (currentStmt.trim()) {
585
+ statements.push(currentStmt.trim());
586
+ }
587
+ currentStmt = "";
588
+ } else {
589
+ currentStmt += ch;
590
+ }
591
+ }
592
+ if (currentStmt.trim()) {
593
+ statements.push(currentStmt.trim());
594
+ }
595
+ for (let stmt of statements) {
596
+ stmt = stmt.replace(/\/\*[\s\S]*?\*\//g, "").trim();
597
+ stmt = stmt.replace(/^(?:public\s+|protected\s+|private\s+)?(?:abstract\s+)?(?:readonly\s+)?/, "").trim();
598
+ const firstParen = stmt.indexOf("(");
599
+ if (firstParen === -1) continue;
600
+ let methodName = "";
601
+ let typeParams;
602
+ const firstAngle = stmt.indexOf("<");
603
+ if (firstAngle !== -1 && firstAngle < firstParen) {
604
+ methodName = stmt.slice(0, firstAngle).trim().replace(/^(?:async\s+)/, "");
605
+ let angDepth = 0;
606
+ let angClose = -1;
607
+ for (let j = firstAngle; j < firstParen; j++) {
608
+ if (stmt[j] === "<") angDepth++;
609
+ else if (stmt[j] === ">") {
610
+ angDepth--;
611
+ if (angDepth === 0) {
612
+ angClose = j;
613
+ break;
614
+ }
615
+ }
616
+ }
617
+ if (angClose !== -1) {
618
+ typeParams = stmt.slice(firstAngle, angClose + 1).trim();
619
+ }
620
+ } else {
621
+ methodName = stmt.slice(0, firstParen).trim().replace(/^(?:async\s+)/, "");
622
+ }
623
+ if (!methodName || !/^[A-Za-z0-9_$]+$/.test(methodName)) {
624
+ continue;
625
+ }
626
+ let parenDepth = 0;
627
+ let parenClose = -1;
628
+ for (let j = firstParen; j < stmt.length; j++) {
629
+ if (stmt[j] === "(") parenDepth++;
630
+ else if (stmt[j] === ")") {
631
+ parenDepth--;
632
+ if (parenDepth === 0) {
633
+ parenClose = j;
634
+ break;
635
+ }
636
+ }
637
+ }
638
+ if (parenClose === -1) continue;
639
+ const rawParams = stmt.slice(firstParen + 1, parenClose).trim();
640
+ const params = rawParams.replace(/\s+/g, " ");
641
+ const afterParen = stmt.slice(parenClose + 1).trim();
642
+ let returnType = "void";
643
+ if (afterParen.startsWith(":")) {
644
+ returnType = afterParen.slice(1).trim().replace(/\s+/g, " ");
645
+ }
646
+ const isAsync = returnType.startsWith("Promise<") || returnType === "Promise";
647
+ methods.push({
648
+ name: methodName,
649
+ typeParams,
650
+ params,
651
+ returnType,
652
+ isAsync
653
+ });
654
+ }
655
+ return {
656
+ className,
657
+ methods,
658
+ imports,
659
+ extendsClass
660
+ };
661
+ }
662
+ function findInterfaceFile(projectRoot, ifaceName, featureName, customPath) {
663
+ if (customPath && fs7.existsSync(customPath)) {
664
+ return { filePath: customPath, ifaceName };
665
+ }
666
+ const candidateNames = [
667
+ ifaceName,
668
+ ensureInterfacePrefix(ifaceName),
669
+ removeInterfacePrefix(ifaceName)
670
+ ];
671
+ const features = featureName ? [featureName, ...listExistingFeatures(projectRoot).filter((f) => f !== featureName)] : listExistingFeatures(projectRoot);
672
+ for (const feat of features) {
673
+ const domainDir = path6.join(resolveFeaturesDir(projectRoot), feat, "domain");
674
+ if (!fs7.existsSync(domainDir)) continue;
675
+ for (const cand of candidateNames) {
676
+ const p1 = path6.join(domainDir, `${cand}.ts`);
677
+ if (fs7.existsSync(p1)) {
678
+ return { filePath: p1, ifaceName: cand, feature: feat };
679
+ }
680
+ }
681
+ }
682
+ for (const cand of candidateNames) {
683
+ const p1 = path6.join(projectRoot, "domain", `${cand}.ts`);
684
+ if (fs7.existsSync(p1)) return { filePath: p1, ifaceName: cand };
685
+ const p22 = path6.join(projectRoot, "src", "domain", `${cand}.ts`);
686
+ if (fs7.existsSync(p22)) return { filePath: p22, ifaceName: cand };
687
+ const p32 = path6.join(projectRoot, "interfaces", `${cand}.ts`);
688
+ if (fs7.existsSync(p32)) return { filePath: p32, ifaceName: cand };
689
+ }
690
+ return null;
691
+ }
692
+ function loadInterfaceHierarchy(projectRoot, initialIfaceName, featureName, customPath) {
693
+ const found = findInterfaceFile(projectRoot, initialIfaceName, featureName, customPath);
694
+ if (!found) {
695
+ return {
696
+ resolvedIfaceName: initialIfaceName,
697
+ methods: [],
698
+ imports: []
699
+ };
700
+ }
701
+ const visited = /* @__PURE__ */ new Set();
702
+ const allMethods = [];
703
+ const allImports = [];
704
+ let currentFile = found.filePath;
705
+ let currentName = found.ifaceName;
706
+ const primaryResolvedName = found.ifaceName;
707
+ const primaryFilePath = found.filePath;
708
+ while (currentFile && !visited.has(currentFile)) {
709
+ visited.add(currentFile);
710
+ try {
711
+ const content = fs7.readFileSync(currentFile, "utf-8");
712
+ const parsed = parseInterfaceMethodsAndImports(content, currentName);
713
+ for (const m of parsed.methods) {
714
+ if (!allMethods.some((existing) => existing.name === m.name)) {
715
+ allMethods.push(m);
716
+ }
717
+ }
718
+ for (const imp of parsed.imports) {
719
+ allImports.push(imp);
720
+ }
721
+ if (parsed.extendsClass) {
722
+ const nextFound = findInterfaceFile(projectRoot, parsed.extendsClass, featureName);
723
+ if (nextFound) {
724
+ currentFile = nextFound.filePath;
725
+ currentName = nextFound.ifaceName;
726
+ } else {
727
+ currentFile = void 0;
728
+ }
729
+ } else {
730
+ currentFile = void 0;
731
+ }
732
+ } catch {
733
+ break;
734
+ }
735
+ }
736
+ return {
737
+ resolvedIfaceName: primaryResolvedName,
738
+ ifaceFilePath: primaryFilePath,
739
+ methods: allMethods,
740
+ imports: allImports
741
+ };
742
+ }
743
+ function synthesizeCrudMethods(entityName) {
744
+ const baseEntity = toPascalCase(entityName);
745
+ const entityVar = baseEntity.charAt(0).toLowerCase() + baseEntity.slice(1);
746
+ const entitiesVar = `${entityVar}s`;
747
+ const idKey = `${entityVar}Id`;
748
+ return [
749
+ {
750
+ name: `Get${baseEntity}ById`,
751
+ params: `input: { ${idKey}: string }`,
752
+ returnType: `Promise<{ ${entityVar}: ${baseEntity} }>`,
753
+ isAsync: true
754
+ },
755
+ {
756
+ name: `List${baseEntity}s`,
757
+ params: `input?: { page?: number | undefined; limit?: number | undefined }`,
758
+ returnType: `Promise<{ ${entitiesVar}: ${baseEntity}[]; total?: number | undefined; page?: number | undefined; limit?: number | undefined }>`,
759
+ isAsync: true
760
+ },
761
+ {
762
+ name: `Create${baseEntity}`,
763
+ params: `input: { ${entityVar}: ${baseEntity} }`,
764
+ returnType: `Promise<{ ${entityVar}: ${baseEntity} }>`,
765
+ isAsync: true
766
+ },
767
+ {
768
+ name: `Update${baseEntity}`,
769
+ params: `input: { ${entityVar}: ${baseEntity} }`,
770
+ returnType: `Promise<{ ${entityVar}: ${baseEntity} }>`,
771
+ isAsync: true
772
+ },
773
+ {
774
+ name: `Delete${baseEntity}`,
775
+ params: `input: { ${idKey}: string }`,
776
+ returnType: `Promise<{ success: boolean }>`,
777
+ isAsync: true
778
+ }
779
+ ];
780
+ }
781
+ function generateStubCode(stubClassName, ifaceName, entityName, featureName, methods = [], domainImports = [], ifaceImportPath) {
782
+ const baseEntity = entityName ? toPascalCase(entityName) : toPascalCase(removeInterfacePrefix(ifaceName).replace(/Repository$|Service$|Client$/, ""));
783
+ const entityVar = baseEntity ? baseEntity.charAt(0).toLowerCase() + baseEntity.slice(1) : "";
784
+ const entitiesVar = `${entityVar}s`;
785
+ const idKey = `${entityVar}Id`;
786
+ const impPath = ifaceImportPath || `../domain/${ifaceName}.js`;
787
+ const entityImportPath = ifaceImportPath ? `${path6.dirname(ifaceImportPath)}/${baseEntity}.js` : `../domain/${baseEntity}.js`;
788
+ const importsByFrom = /* @__PURE__ */ new Map();
789
+ for (const imp of domainImports) {
790
+ if (imp.name === ifaceName || imp.name === baseEntity) continue;
791
+ if (!importsByFrom.has(imp.from)) {
792
+ importsByFrom.set(imp.from, { typeOnly: imp.isTypeOnly ?? true, names: /* @__PURE__ */ new Set() });
793
+ }
794
+ const group = importsByFrom.get(imp.from);
795
+ group.names.add(imp.name);
796
+ if (imp.isTypeOnly === false) {
797
+ group.typeOnly = false;
798
+ }
799
+ }
800
+ const formattedDomainImports = Array.from(importsByFrom.entries()).map(([from, group]) => ({
801
+ from,
802
+ names: Array.from(group.names).sort().join(", "),
803
+ isTypeOnly: group.typeOnly
804
+ }));
805
+ const standardNames = /* @__PURE__ */ new Set([
806
+ `Get${baseEntity}ById`,
807
+ `List${baseEntity}s`,
808
+ `Create${baseEntity}`,
809
+ `Update${baseEntity}`,
810
+ `Delete${baseEntity}`
811
+ ]);
812
+ const extraMethods = methods.filter((m) => !standardNames.has(m.name));
813
+ return renderTemplate(stubInfraTemplate, {
814
+ stubClassName,
815
+ ifaceName,
816
+ ifaceImportPath: impPath,
817
+ baseEntity: baseEntity || void 0,
818
+ entityImportPath,
819
+ domainImports: formattedDomainImports,
820
+ entitiesVar,
821
+ entityVar,
822
+ idKey,
823
+ extraMethods,
824
+ hasMethods: methods.length > 0,
825
+ methods
826
+ });
827
+ }
828
+ function generateConcreteInfraCode(className, ifaceName, dependencies = [], methods = [], domainImports = [], ifaceImportPath) {
829
+ let finalMethods = [...methods];
830
+ const finalDomainImports = [...domainImports];
831
+ if (finalMethods.length === 0 && ifaceName) {
832
+ const baseEntity = toPascalCase(removeInterfacePrefix(ifaceName).replace(/Repository$|Service$|Client$/, ""));
833
+ if (baseEntity) {
834
+ finalMethods = synthesizeCrudMethods(baseEntity);
835
+ if (!finalDomainImports.some((imp) => imp.name === baseEntity)) {
836
+ const importPath = ifaceImportPath ? `${path6.dirname(ifaceImportPath)}/${baseEntity}.js` : `../domain/${baseEntity}.js`;
837
+ finalDomainImports.push({
838
+ name: baseEntity,
839
+ from: importPath,
840
+ isTypeOnly: true
841
+ });
842
+ }
843
+ }
844
+ }
845
+ const impPath = ifaceName ? ifaceImportPath || `../domain/${ifaceName}.js` : "";
846
+ const importsByFrom = /* @__PURE__ */ new Map();
847
+ for (const imp of finalDomainImports) {
848
+ if (imp.name === ifaceName) continue;
849
+ if (!importsByFrom.has(imp.from)) {
850
+ importsByFrom.set(imp.from, { typeOnly: imp.isTypeOnly ?? true, names: /* @__PURE__ */ new Set() });
851
+ }
852
+ const group = importsByFrom.get(imp.from);
853
+ group.names.add(imp.name);
854
+ if (imp.isTypeOnly === false) {
855
+ group.typeOnly = false;
856
+ }
857
+ }
858
+ const formattedDomainImports = Array.from(importsByFrom.entries()).map(([from, group]) => ({
859
+ from,
860
+ names: Array.from(group.names).sort().join(", "),
861
+ isTypeOnly: group.typeOnly
862
+ }));
863
+ const formattedDeps = dependencies.map((dep) => ({
864
+ key: dep.charAt(0).toLowerCase() + dep.slice(1),
865
+ name: dep
866
+ }));
867
+ return renderTemplate(concreteInfraTemplate, {
868
+ className,
869
+ ifaceName,
870
+ ifaceImportPath: impPath,
871
+ domainImports: formattedDomainImports,
872
+ dependencies: formattedDeps,
873
+ hasMethods: finalMethods.length > 0,
874
+ methods: finalMethods
875
+ });
876
+ }
877
+ function generateInfrastructure(options) {
878
+ const projectRoot = options.projectRoot || findProjectRoot();
879
+ const className = toPascalCase(options.name);
880
+ const isFeatureScope = options.scope !== "global" && Boolean(options.feature);
881
+ const targetDir = options.targetDir || (isFeatureScope ? path6.join(resolveFeaturesDir(projectRoot), options.feature, "infrastructure") : path6.join(projectRoot, "infrastructure"));
882
+ const filePath = path6.join(targetDir, `${className}.ts`);
883
+ let code;
884
+ if (options.implementsInterface) {
885
+ const rawIfaceName = options.implementsInterface;
886
+ const hierarchy = loadInterfaceHierarchy(
887
+ projectRoot,
888
+ rawIfaceName,
889
+ options.feature,
890
+ options.interfacePath
891
+ );
892
+ const ifaceName = hierarchy.resolvedIfaceName;
893
+ const ifaceFilePath = hierarchy.ifaceFilePath;
894
+ let ifaceImportPath;
895
+ if (ifaceFilePath) {
896
+ let rel = path6.relative(targetDir, ifaceFilePath).replace(/\\/g, "/").replace(/\.ts$/, ".js");
897
+ if (!rel.startsWith(".")) rel = "./" + rel;
898
+ ifaceImportPath = rel;
899
+ } else if (isFeatureScope) {
900
+ ifaceImportPath = `../domain/${ifaceName}.js`;
901
+ }
902
+ const allSignatures = hierarchy.methods.map((m) => `${m.params} ${m.returnType}`).join(" ");
903
+ const domainImports = [];
904
+ for (const imp of hierarchy.imports) {
905
+ for (const item of imp.items) {
906
+ if (new RegExp(`\\b${item.name}\\b`).test(allSignatures)) {
907
+ let fromPath;
908
+ if (imp.specifier.startsWith(".") && ifaceFilePath) {
909
+ const absTarget = path6.resolve(path6.dirname(ifaceFilePath), imp.specifier);
910
+ let rel = path6.relative(targetDir, absTarget).replace(/\\/g, "/");
911
+ if (!rel.startsWith(".")) rel = "./" + rel;
912
+ fromPath = rel;
913
+ } else {
914
+ fromPath = imp.specifier;
915
+ }
916
+ domainImports.push({
917
+ name: item.name,
918
+ from: fromPath,
919
+ isTypeOnly: item.isTypeOnly ?? imp.isTypeOnly ?? true
920
+ });
921
+ }
922
+ }
923
+ }
924
+ if (options.kind === "stub") {
925
+ code = generateStubCode(
926
+ className,
927
+ ifaceName,
928
+ options.entityName,
929
+ options.feature,
930
+ hierarchy.methods,
931
+ domainImports,
932
+ ifaceImportPath
933
+ );
934
+ } else {
935
+ code = generateConcreteInfraCode(
936
+ className,
937
+ ifaceName,
938
+ options.dependencies || [],
939
+ hierarchy.methods,
940
+ domainImports,
941
+ ifaceImportPath
942
+ );
943
+ }
944
+ } else {
945
+ code = generateConcreteInfraCode(
946
+ className,
947
+ void 0,
948
+ options.dependencies || []
949
+ );
950
+ }
951
+ return writeFileSafely(filePath, code, {
952
+ projectRoot,
953
+ dryRun: options.dryRun,
954
+ overwrite: options.overwrite
955
+ });
956
+ }
957
+ var primitiveTypes = /* @__PURE__ */ new Set([
958
+ "string",
959
+ "number",
960
+ "boolean",
961
+ "bigint",
962
+ "symbol",
963
+ "undefined",
964
+ "null",
965
+ "any",
966
+ "unknown",
967
+ "never",
968
+ "void",
969
+ "Date",
970
+ "Record<string, any>",
971
+ "Record<string, unknown>",
972
+ "Record<string, never>"
973
+ ]);
974
+ function generateUseCaseCode(useCaseName, dependencies = [], inputProps = [], outputProps = [], domainImports = []) {
975
+ const ucName = toPascalCase(useCaseName);
976
+ const inputTypeName = `${ucName}Input`;
977
+ const outputTypeName = `${ucName}Output`;
978
+ const dependencyImports = dependencies.map((dep) => ({
979
+ name: dep,
980
+ from: /^I[A-Z]/.test(dep) ? `../domain/${dep}.js` : `./${dep}.js`
981
+ }));
982
+ const domainTypesToImport = new Set(domainImports);
983
+ for (const p15 of [...inputProps, ...outputProps]) {
984
+ const rawType = p15.type.trim();
985
+ const baseType = rawType.replace(/\[\]$/, "").trim();
986
+ if (!primitiveTypes.has(baseType) && /^[A-Z][A-Za-z0-9_]*$/.test(baseType) && !dependencies.includes(baseType)) {
987
+ domainTypesToImport.add(baseType);
988
+ }
989
+ }
990
+ const formattedInputProps = inputProps.map((p15) => ({
991
+ name: p15.name,
992
+ optional: p15.optional,
993
+ type: p15.optional && !p15.type.includes("undefined") ? `${p15.type} | undefined` : p15.type
994
+ }));
995
+ const formattedOutputProps = outputProps.map((p15) => ({
996
+ name: p15.name,
997
+ optional: p15.optional,
998
+ type: p15.optional && !p15.type.includes("undefined") ? `${p15.type} | undefined` : p15.type
999
+ }));
1000
+ const formattedDeps = dependencies.map((dep) => ({
1001
+ key: toCamelCase(dep),
1002
+ name: dep
1003
+ }));
1004
+ const getMatch = ucName.match(/^Get([A-Z][A-Za-z0-9]*)$/);
1005
+ const listMatch = ucName.match(/^List([A-Z][A-Za-z0-9]*)$/);
1006
+ const createMatch = ucName.match(/^Create([A-Z][A-Za-z0-9]*)$/);
1007
+ const updateMatch = ucName.match(/^Update([A-Z][A-Za-z0-9]*)$/);
1008
+ const deleteMatch = ucName.match(/^Delete([A-Z][A-Za-z0-9]*)$/);
1009
+ let callWired = null;
1010
+ if (getMatch && getMatch[1]) {
1011
+ const entity = getMatch[1];
1012
+ const matchingRepo = dependencies.find((d) => d === `I${entity}Repository`);
1013
+ if (matchingRepo && inputProps.length === 1 && outputProps.length === 1) {
1014
+ const repoKey = toCamelCase(matchingRepo);
1015
+ const idKey = inputProps[0].name;
1016
+ callWired = ` return await this.deps.${repoKey}.Get${entity}ById({
1017
+ ${idKey}: props.${idKey},
1018
+ });`;
1019
+ }
1020
+ } else if (listMatch && listMatch[1]) {
1021
+ const pluralEntity = listMatch[1];
1022
+ const singularEntity = toSingular(pluralEntity);
1023
+ const matchingRepo = dependencies.find(
1024
+ (d) => d === `I${singularEntity}Repository` || d === `I${pluralEntity}Repository`
1025
+ );
1026
+ if (matchingRepo) {
1027
+ const repoKey = toCamelCase(matchingRepo);
1028
+ const passProps = inputProps.length > 0 ? "props" : "";
1029
+ callWired = ` return await this.deps.${repoKey}.List${pluralEntity}(${passProps});`;
1030
+ }
1031
+ } else if (createMatch && createMatch[1]) {
1032
+ const entity = createMatch[1];
1033
+ const matchingRepo = dependencies.find((d) => d === `I${entity}Repository`);
1034
+ if (matchingRepo && inputProps.length === 1 && outputProps.length === 1) {
1035
+ const repoKey = toCamelCase(matchingRepo);
1036
+ const entityKey = inputProps[0].name;
1037
+ callWired = ` return await this.deps.${repoKey}.Create${entity}({
1038
+ ${entityKey}: props.${entityKey},
1039
+ });`;
1040
+ }
1041
+ } else if (updateMatch && updateMatch[1]) {
1042
+ const entity = updateMatch[1];
1043
+ const matchingRepo = dependencies.find((d) => d === `I${entity}Repository`);
1044
+ if (matchingRepo && inputProps.length === 1 && outputProps.length === 1) {
1045
+ const repoKey = toCamelCase(matchingRepo);
1046
+ const entityKey = inputProps[0].name;
1047
+ callWired = ` return await this.deps.${repoKey}.Update${entity}({
1048
+ ${entityKey}: props.${entityKey},
1049
+ });`;
1050
+ }
1051
+ } else if (deleteMatch && deleteMatch[1]) {
1052
+ const entity = deleteMatch[1];
1053
+ const matchingRepo = dependencies.find((d) => d === `I${entity}Repository`);
1054
+ if (matchingRepo && inputProps.length === 1) {
1055
+ const repoKey = toCamelCase(matchingRepo);
1056
+ const idKey = inputProps[0].name;
1057
+ callWired = ` return await this.deps.${repoKey}.Delete${entity}({
1058
+ ${idKey}: props.${idKey},
1059
+ });`;
1060
+ }
1061
+ }
1062
+ const paramStr = inputProps.length > 0 ? `props: ${inputTypeName}` : `_props?: ${inputTypeName}`;
1063
+ return renderTemplate(useCaseTemplate, {
1064
+ ucName,
1065
+ inputTypeName,
1066
+ outputTypeName,
1067
+ dependencyImports,
1068
+ domainImports: Array.from(domainTypesToImport),
1069
+ hasInputProps: inputProps.length > 0,
1070
+ inputProperties: formattedInputProps,
1071
+ hasOutputProps: outputProps.length > 0,
1072
+ outputProperties: formattedOutputProps,
1073
+ dependencies: formattedDeps,
1074
+ hasDependencies: dependencies.length > 0,
1075
+ paramStr,
1076
+ callWired
1077
+ });
1078
+ }
1079
+ function generateUseCaseTestCode(useCaseName, dependencies = []) {
1080
+ const ucName = toPascalCase(useCaseName);
1081
+ const stubBindings = [];
1082
+ for (const dep of dependencies) {
1083
+ if (/^I[A-Z]/.test(dep)) {
1084
+ const stub = toStubName(dep);
1085
+ stubBindings.push({ iface: dep, stub });
1086
+ }
1087
+ }
1088
+ return renderTemplate(useCaseTestTemplate, {
1089
+ ucName,
1090
+ stubBindings
1091
+ });
1092
+ }
1093
+ function generateUseCase(options) {
1094
+ const files = [];
1095
+ const projectRoot = options.projectRoot || findProjectRoot();
1096
+ const ucName = toPascalCase(options.name);
1097
+ const targetDir = options.targetDir || path6.join(resolveFeaturesDir(projectRoot), options.feature, "useCases");
1098
+ const filePath = path6.join(targetDir, `${ucName}.ts`);
1099
+ const code = generateUseCaseCode(
1100
+ ucName,
1101
+ options.dependencies || [],
1102
+ options.inputProperties || [],
1103
+ options.outputProperties || [],
1104
+ options.domainImports || []
1105
+ );
1106
+ files.push(
1107
+ writeFileSafely(filePath, code, {
1108
+ projectRoot,
1109
+ dryRun: options.dryRun,
1110
+ overwrite: options.overwrite
1111
+ })
1112
+ );
1113
+ if (options.includeTest) {
1114
+ const testPath = path6.join(targetDir, `${ucName}.test.ts`);
1115
+ const testCode = generateUseCaseTestCode(ucName, options.dependencies || []);
1116
+ files.push(
1117
+ writeFileSafely(testPath, testCode, {
1118
+ projectRoot,
1119
+ dryRun: options.dryRun,
1120
+ overwrite: options.overwrite
1121
+ })
1122
+ );
1123
+ }
1124
+ return files;
1125
+ }
1126
+ function deriveSuccessMessage(handlerName, useCaseName, method = "get") {
1127
+ const baseName = useCaseName ? toPascalCase(useCaseName) : toPascalCase(handlerName).replace(/Http$/, "");
1128
+ const createMatch = baseName.match(/^Create([A-Z].*)$/);
1129
+ if (createMatch) return `${createMatch[1]} created successfully`;
1130
+ const getMatch = baseName.match(/^(?:Get|Find)([A-Z].*)$/);
1131
+ if (getMatch) return `${getMatch[1]} retrieved successfully`;
1132
+ const listMatch = baseName.match(/^List([A-Z].*)$/);
1133
+ if (listMatch) return `${listMatch[1]} retrieved successfully`;
1134
+ const updateMatch = baseName.match(/^(?:Update|Modify|Patch)([A-Z].*)$/);
1135
+ if (updateMatch) return `${updateMatch[1]} updated successfully`;
1136
+ const deleteMatch = baseName.match(/^(?:Delete|Remove)([A-Z].*)$/);
1137
+ if (deleteMatch) return `${deleteMatch[1]} deleted successfully`;
1138
+ if (method === "post") return `${baseName} created successfully`;
1139
+ if (method === "delete") return `${baseName} deleted successfully`;
1140
+ return `${baseName} executed successfully`;
1141
+ }
1142
+ function generateHttpHandlerCode(handlerName, method = "get", routeSubPath = "/", useCaseName, includeValidation = false, validationSource = "body", validationFields = [], customSuccessMessage) {
1143
+ const className = toPascalCase(handlerName);
1144
+ const ucName = useCaseName ? toPascalCase(useCaseName) : void 0;
1145
+ const ucDepKey = ucName ? toCamelCase(ucName) : void 0;
1146
+ const schemaName = `${toCamelCase(className)}Schema`;
1147
+ const formattedFields = validationFields.map((f) => {
1148
+ let zodType = "z.string()";
1149
+ if (f.type === "number") zodType = validationSource === "query" ? "z.coerce.number()" : "z.number()";
1150
+ else if (f.type === "boolean") zodType = validationSource === "query" ? "z.coerce.boolean()" : "z.boolean()";
1151
+ else if (f.type === "Date") zodType = "z.coerce.date()";
1152
+ else if (f.type === "any") zodType = "z.any()";
1153
+ else if (f.type === "unknown") zodType = "z.unknown()";
1154
+ else if (f.type.endsWith("[]")) zodType = "z.array(z.any())";
1155
+ else if (/^[A-Z]/.test(f.type)) zodType = "z.record(z.any()) as any";
1156
+ if (f.optional) zodType += ".optional()";
1157
+ return { name: f.name, zodType };
1158
+ });
1159
+ const callArg = includeValidation ? "parsed.data" : "{}";
1160
+ const statusCode = method === "post" ? 201 : 200;
1161
+ const successMessage = customSuccessMessage || deriveSuccessMessage(className, ucName, method);
1162
+ return renderTemplate(httpHandlerTemplate, {
1163
+ className,
1164
+ ucName,
1165
+ ucDepKey,
1166
+ includeValidation,
1167
+ schemaName,
1168
+ hasValidationFields: formattedFields.length > 0,
1169
+ validationFields: formattedFields,
1170
+ method,
1171
+ routeSubPath,
1172
+ validationSource,
1173
+ callArg,
1174
+ statusCode,
1175
+ successMessage
1176
+ });
1177
+ }
1178
+ function generateHttpHandler(options) {
1179
+ const files = [];
1180
+ const projectRoot = options.projectRoot || findProjectRoot();
1181
+ const className = toPascalCase(options.name);
1182
+ const pxDir = path6.join(resolveFeaturesDir(projectRoot), options.feature, "pxExpress");
1183
+ const handlersDir = path6.join(pxDir, "handlers");
1184
+ const pxIndexPath = path6.join(pxDir, "index.ts");
1185
+ if (!fs7.existsSync(pxIndexPath)) {
1186
+ const featurePath = options.routePrefix || `/${options.feature.toLowerCase()}`;
1187
+ const pxIndexContent = renderTemplate(expressIndexTemplate, {
1188
+ routePath: featurePath
1189
+ });
1190
+ files.push(
1191
+ writeFileSafely(pxIndexPath, pxIndexContent, {
1192
+ projectRoot,
1193
+ dryRun: options.dryRun,
1194
+ overwrite: false
1195
+ })
1196
+ );
1197
+ }
1198
+ const handlerFilePath = path6.join(handlersDir, `${className}.ts`);
1199
+ const code = generateHttpHandlerCode(
1200
+ className,
1201
+ options.method || "get",
1202
+ options.path || "/",
1203
+ options.useCaseName,
1204
+ Boolean(options.includeValidation),
1205
+ options.validationSource || "body",
1206
+ options.validationFields || [],
1207
+ options.successMessage
1208
+ );
1209
+ files.push(
1210
+ writeFileSafely(handlerFilePath, code, {
1211
+ projectRoot,
1212
+ dryRun: options.dryRun,
1213
+ overwrite: options.overwrite
1214
+ })
1215
+ );
1216
+ return files;
1217
+ }
1218
+ function updateOrGenerateDiProvider(featureDir, featureName, bindings = [], options = {}) {
1219
+ const providerPath = path6.join(featureDir, "diProvider.ts");
1220
+ const pascalFeature = toPascalCase(featureName);
1221
+ const providerModuleName = `${pascalFeature}Provider`;
1222
+ if (fs7.existsSync(providerPath) && !options.overwrite) {
1223
+ let content = fs7.readFileSync(providerPath, "utf-8");
1224
+ content = content.replace(/\n*export\s+const\s+ScaffoldProvider\s*=[^;]+;?/g, "");
1225
+ for (const b of bindings) {
1226
+ const ifaceImp = b.ifaceImportPath || `./domain/${b.ifaceName}.js`;
1227
+ const implImp = b.implImportPath || `./infrastructure/${b.implName}.js`;
1228
+ if (!content.includes(b.ifaceName)) {
1229
+ content = `import { ${b.ifaceName} } from "${ifaceImp}";
1230
+ ` + content;
1231
+ }
1232
+ if (!content.includes(b.implName)) {
1233
+ content = `import { ${b.implName} } from "${implImp}";
1234
+ ` + content;
1235
+ }
1236
+ const provideCall = `c.provide(${b.ifaceName}, ${b.implName});`;
1237
+ if (!content.includes(b.ifaceName) || !content.includes(b.implName) || !content.includes(`c.provide(${b.ifaceName}`)) {
1238
+ const match = content.match(/(const\s+\w+Provider\s*:\s*DIModule\s*=\s*\((?:c|container)[^)]*\)\s*=>\s*\{)([\s\S]*?)(\}\;?)/);
1239
+ if (match) {
1240
+ const prefix = match[1];
1241
+ const body = match[2];
1242
+ const suffix = match[3];
1243
+ const updatedBody = `${body}
1244
+ ${provideCall}
1245
+ `;
1246
+ content = content.replace(match[0], `${prefix}${updatedBody}${suffix}`);
1247
+ } else {
1248
+ content = content.replace(
1249
+ /export default/g,
1250
+ `// Register ${b.ifaceName}
1251
+ // c.provide(${b.ifaceName}, ${b.implName});
1252
+
1253
+ export default`
1254
+ );
1255
+ }
1256
+ }
1257
+ }
1258
+ return writeFileSafely(providerPath, content, {
1259
+ ...options,
1260
+ overwrite: true
1261
+ });
1262
+ }
1263
+ const formattedBindings = bindings.map((b) => ({
1264
+ ifaceName: b.ifaceName,
1265
+ implName: b.implName,
1266
+ ifaceImportPath: b.ifaceImportPath || `./domain/${b.ifaceName}.js`,
1267
+ implImportPath: b.implImportPath || `./infrastructure/${b.implName}.js`
1268
+ }));
1269
+ const code = renderTemplate(diProviderTemplate, {
1270
+ providerModuleName,
1271
+ bindings: formattedBindings
1272
+ });
1273
+ return writeFileSafely(providerPath, code, options);
1274
+ }
1275
+
1276
+ // src/core/featureGenerator.ts
1277
+ function generateFeature(options) {
1278
+ const files = [];
1279
+ const projectRoot = options.projectRoot || findProjectRoot();
1280
+ const rawName = options.name.trim();
1281
+ const featureName = toCamelCase(rawName);
1282
+ const targetDir = options.targetDir || path6.join(resolveFeaturesDir(projectRoot), featureName);
1283
+ const routePath = options.routePath || `/${featureName}`;
1284
+ const rawEntities = [];
1285
+ if (options.entityNames && options.entityNames.length > 0) {
1286
+ rawEntities.push(...options.entityNames);
1287
+ } else if (options.entityName) {
1288
+ rawEntities.push(options.entityName);
1289
+ } else {
1290
+ rawEntities.push(toSingular(rawName));
1291
+ }
1292
+ const entities = Array.from(
1293
+ new Set(rawEntities.map((e) => toPascalCase(e.trim())).filter(Boolean))
1294
+ );
1295
+ if (entities.length === 0) {
1296
+ entities.push(toPascalCase(toSingular(rawName)));
1297
+ }
1298
+ if (options.includeDomain !== false) {
1299
+ for (const ent of entities) {
1300
+ const domainFile = generateDomain({
1301
+ name: ent,
1302
+ feature: featureName,
1303
+ targetDir: path6.join(targetDir, "domain"),
1304
+ projectRoot,
1305
+ dryRun: options.dryRun,
1306
+ overwrite: options.overwrite,
1307
+ kind: "interface",
1308
+ properties: [
1309
+ { name: "id", type: "string" },
1310
+ { name: "createdAt", type: "Date" },
1311
+ { name: "updatedAt", type: "Date" }
1312
+ ]
1313
+ });
1314
+ files.push(domainFile);
1315
+ }
1316
+ }
1317
+ if (options.includeInterface !== false) {
1318
+ for (const ent of entities) {
1319
+ const ifaceName = options.interfaceName && entities.length === 1 ? toPascalCase(options.interfaceName) : `I${ent}Repository`;
1320
+ const ifaceFile = generateInterface({
1321
+ name: ifaceName,
1322
+ feature: featureName,
1323
+ targetDir: path6.join(targetDir, "domain"),
1324
+ entityName: ent,
1325
+ projectRoot,
1326
+ dryRun: options.dryRun,
1327
+ overwrite: options.overwrite,
1328
+ methodsPreset: "crud"
1329
+ });
1330
+ files.push(ifaceFile);
1331
+ }
1332
+ }
1333
+ if (options.includeInfra !== false && options.includeInterface !== false) {
1334
+ for (const ent of entities) {
1335
+ const ifaceName = options.interfaceName && entities.length === 1 ? toPascalCase(options.interfaceName) : `I${ent}Repository`;
1336
+ const stubName = options.stubName && entities.length === 1 ? toPascalCase(options.stubName) : `Stub${ent}Repository`;
1337
+ const stubFile = generateInfrastructure({
1338
+ name: stubName,
1339
+ feature: featureName,
1340
+ targetDir: path6.join(targetDir, "infrastructure"),
1341
+ kind: "stub",
1342
+ implementsInterface: ifaceName,
1343
+ entityName: ent,
1344
+ projectRoot,
1345
+ dryRun: options.dryRun,
1346
+ overwrite: options.overwrite
1347
+ });
1348
+ files.push(stubFile);
1349
+ }
1350
+ }
1351
+ if (options.includeDiProvider !== false && options.includeInterface !== false && options.includeInfra !== false) {
1352
+ const bindings = entities.map((ent) => {
1353
+ const ifaceName = options.interfaceName && entities.length === 1 ? toPascalCase(options.interfaceName) : `I${ent}Repository`;
1354
+ const stubName = options.stubName && entities.length === 1 ? toPascalCase(options.stubName) : `Stub${ent}Repository`;
1355
+ return { ifaceName, implName: stubName };
1356
+ });
1357
+ const providerFile = updateOrGenerateDiProvider(targetDir, featureName, bindings, {
1358
+ projectRoot,
1359
+ dryRun: options.dryRun,
1360
+ overwrite: options.overwrite
1361
+ });
1362
+ files.push(providerFile);
1363
+ }
1364
+ if (options.includeUseCase !== false) {
1365
+ let ops = [];
1366
+ if (options.crudOperations) {
1367
+ ops = options.crudOperations;
1368
+ } else if (options.useCaseName) {
1369
+ ops = [];
1370
+ } else {
1371
+ ops = ["create", "read", "list", "update", "delete"];
1372
+ }
1373
+ if (options.useCaseName && ops.length === 0) {
1374
+ const primaryEntity = entities[0];
1375
+ const entVar = primaryEntity.charAt(0).toLowerCase() + primaryEntity.slice(1);
1376
+ const idParam = `${entVar}Id`;
1377
+ const ifaceName = options.interfaceName && entities.length === 1 ? toPascalCase(options.interfaceName) : `I${primaryEntity}Repository`;
1378
+ const ucName = toPascalCase(options.useCaseName);
1379
+ const ucFiles = generateUseCase({
1380
+ name: ucName,
1381
+ feature: featureName,
1382
+ targetDir: path6.join(targetDir, "useCases"),
1383
+ dependencies: options.includeInterface !== false ? [ifaceName] : [],
1384
+ inputProperties: [{ name: idParam, type: "string" }],
1385
+ outputProperties: [{ name: entVar, type: primaryEntity }],
1386
+ domainImports: options.includeDomain !== false ? [primaryEntity] : [],
1387
+ includeTest: options.includeTests !== false,
1388
+ projectRoot,
1389
+ dryRun: options.dryRun,
1390
+ overwrite: options.overwrite
1391
+ });
1392
+ files.push(...ucFiles);
1393
+ if (options.includeExpress !== false) {
1394
+ const httpName = options.httpHandlerName ? toPascalCase(options.httpHandlerName) : `${ucName}Http`;
1395
+ const httpFiles = generateHttpHandler({
1396
+ name: httpName,
1397
+ feature: featureName,
1398
+ routePrefix: routePath,
1399
+ method: "get",
1400
+ path: `/:${idParam}`,
1401
+ useCaseName: ucName,
1402
+ includeValidation: true,
1403
+ validationSource: "params",
1404
+ validationFields: [{ name: idParam, type: "string" }],
1405
+ projectRoot,
1406
+ dryRun: options.dryRun,
1407
+ overwrite: options.overwrite
1408
+ });
1409
+ files.push(...httpFiles);
1410
+ }
1411
+ }
1412
+ for (let i = 0; i < entities.length; i++) {
1413
+ const ent = entities[i];
1414
+ const entVar = ent.charAt(0).toLowerCase() + ent.slice(1);
1415
+ const idParam = `${entVar}Id`;
1416
+ const listVar = toPlural(entVar);
1417
+ const pluralEntity = toPlural(ent);
1418
+ const ifaceName = options.interfaceName && entities.length === 1 ? toPascalCase(options.interfaceName) : `I${ent}Repository`;
1419
+ const deps = options.includeInterface !== false ? [ifaceName] : [];
1420
+ const entitySubRoute = i === 0 ? "" : `/${toKebabCase(listVar)}`;
1421
+ for (const op of ops) {
1422
+ if (op === "create") {
1423
+ const ucName = `Create${ent}`;
1424
+ const ucFiles = generateUseCase({
1425
+ name: ucName,
1426
+ feature: featureName,
1427
+ targetDir: path6.join(targetDir, "useCases"),
1428
+ dependencies: deps,
1429
+ inputProperties: [{ name: entVar, type: ent }],
1430
+ outputProperties: [{ name: entVar, type: ent }],
1431
+ domainImports: options.includeDomain !== false ? [ent] : [],
1432
+ includeTest: options.includeTests !== false,
1433
+ projectRoot,
1434
+ dryRun: options.dryRun,
1435
+ overwrite: options.overwrite
1436
+ });
1437
+ files.push(...ucFiles);
1438
+ if (options.includeExpress !== false) {
1439
+ const httpFiles = generateHttpHandler({
1440
+ name: `${ucName}Http`,
1441
+ feature: featureName,
1442
+ routePrefix: routePath,
1443
+ method: "post",
1444
+ path: entitySubRoute || "/",
1445
+ useCaseName: ucName,
1446
+ includeValidation: true,
1447
+ validationSource: "body",
1448
+ validationFields: [{ name: entVar, type: "any" }],
1449
+ projectRoot,
1450
+ dryRun: options.dryRun,
1451
+ overwrite: options.overwrite
1452
+ });
1453
+ files.push(...httpFiles);
1454
+ }
1455
+ } else if (op === "read") {
1456
+ const ucName = `Get${ent}`;
1457
+ const ucFiles = generateUseCase({
1458
+ name: ucName,
1459
+ feature: featureName,
1460
+ targetDir: path6.join(targetDir, "useCases"),
1461
+ dependencies: deps,
1462
+ inputProperties: [{ name: idParam, type: "string" }],
1463
+ outputProperties: [{ name: entVar, type: ent }],
1464
+ domainImports: options.includeDomain !== false ? [ent] : [],
1465
+ includeTest: options.includeTests !== false,
1466
+ projectRoot,
1467
+ dryRun: options.dryRun,
1468
+ overwrite: options.overwrite
1469
+ });
1470
+ files.push(...ucFiles);
1471
+ if (options.includeExpress !== false) {
1472
+ const httpFiles = generateHttpHandler({
1473
+ name: `${ucName}Http`,
1474
+ feature: featureName,
1475
+ routePrefix: routePath,
1476
+ method: "get",
1477
+ path: entitySubRoute ? `${entitySubRoute}/:${idParam}` : `/:${idParam}`,
1478
+ useCaseName: ucName,
1479
+ includeValidation: true,
1480
+ validationSource: "params",
1481
+ validationFields: [{ name: idParam, type: "string" }],
1482
+ projectRoot,
1483
+ dryRun: options.dryRun,
1484
+ overwrite: options.overwrite
1485
+ });
1486
+ files.push(...httpFiles);
1487
+ }
1488
+ } else if (op === "list") {
1489
+ const ucName = `List${pluralEntity}`;
1490
+ const ucFiles = generateUseCase({
1491
+ name: ucName,
1492
+ feature: featureName,
1493
+ targetDir: path6.join(targetDir, "useCases"),
1494
+ dependencies: deps,
1495
+ inputProperties: [
1496
+ { name: "page", type: "number", optional: true },
1497
+ { name: "limit", type: "number", optional: true }
1498
+ ],
1499
+ outputProperties: [
1500
+ { name: listVar, type: `${ent}[]` },
1501
+ { name: "total", type: "number", optional: true },
1502
+ { name: "page", type: "number", optional: true },
1503
+ { name: "limit", type: "number", optional: true }
1504
+ ],
1505
+ domainImports: options.includeDomain !== false ? [ent] : [],
1506
+ includeTest: options.includeTests !== false,
1507
+ projectRoot,
1508
+ dryRun: options.dryRun,
1509
+ overwrite: options.overwrite
1510
+ });
1511
+ files.push(...ucFiles);
1512
+ if (options.includeExpress !== false) {
1513
+ const httpFiles = generateHttpHandler({
1514
+ name: `${ucName}Http`,
1515
+ feature: featureName,
1516
+ routePrefix: routePath,
1517
+ method: "get",
1518
+ path: entitySubRoute || "/",
1519
+ useCaseName: ucName,
1520
+ includeValidation: true,
1521
+ validationSource: "query",
1522
+ validationFields: [
1523
+ { name: "page", type: "number", optional: true },
1524
+ { name: "limit", type: "number", optional: true }
1525
+ ],
1526
+ projectRoot,
1527
+ dryRun: options.dryRun,
1528
+ overwrite: options.overwrite
1529
+ });
1530
+ files.push(...httpFiles);
1531
+ }
1532
+ } else if (op === "update") {
1533
+ const ucName = `Update${ent}`;
1534
+ const ucFiles = generateUseCase({
1535
+ name: ucName,
1536
+ feature: featureName,
1537
+ targetDir: path6.join(targetDir, "useCases"),
1538
+ dependencies: deps,
1539
+ inputProperties: [{ name: entVar, type: ent }],
1540
+ outputProperties: [{ name: entVar, type: ent }],
1541
+ domainImports: options.includeDomain !== false ? [ent] : [],
1542
+ includeTest: options.includeTests !== false,
1543
+ projectRoot,
1544
+ dryRun: options.dryRun,
1545
+ overwrite: options.overwrite
1546
+ });
1547
+ files.push(...ucFiles);
1548
+ if (options.includeExpress !== false) {
1549
+ const httpFiles = generateHttpHandler({
1550
+ name: `${ucName}Http`,
1551
+ feature: featureName,
1552
+ routePrefix: routePath,
1553
+ method: "put",
1554
+ path: entitySubRoute ? `${entitySubRoute}/:${idParam}` : `/:${idParam}`,
1555
+ useCaseName: ucName,
1556
+ includeValidation: true,
1557
+ validationSource: "body",
1558
+ validationFields: [{ name: entVar, type: "any" }],
1559
+ projectRoot,
1560
+ dryRun: options.dryRun,
1561
+ overwrite: options.overwrite
1562
+ });
1563
+ files.push(...httpFiles);
1564
+ }
1565
+ } else if (op === "delete") {
1566
+ const ucName = `Delete${ent}`;
1567
+ const ucFiles = generateUseCase({
1568
+ name: ucName,
1569
+ feature: featureName,
1570
+ targetDir: path6.join(targetDir, "useCases"),
1571
+ dependencies: deps,
1572
+ inputProperties: [{ name: idParam, type: "string" }],
1573
+ outputProperties: [{ name: "success", type: "boolean" }],
1574
+ includeTest: options.includeTests !== false,
1575
+ projectRoot,
1576
+ dryRun: options.dryRun,
1577
+ overwrite: options.overwrite
1578
+ });
1579
+ files.push(...ucFiles);
1580
+ if (options.includeExpress !== false) {
1581
+ const httpFiles = generateHttpHandler({
1582
+ name: `${ucName}Http`,
1583
+ feature: featureName,
1584
+ routePrefix: routePath,
1585
+ method: "delete",
1586
+ path: entitySubRoute ? `${entitySubRoute}/:${idParam}` : `/:${idParam}`,
1587
+ useCaseName: ucName,
1588
+ includeValidation: true,
1589
+ validationSource: "params",
1590
+ validationFields: [{ name: idParam, type: "string" }],
1591
+ projectRoot,
1592
+ dryRun: options.dryRun,
1593
+ overwrite: options.overwrite
1594
+ });
1595
+ files.push(...httpFiles);
1596
+ }
1597
+ }
1598
+ }
1599
+ }
1600
+ }
1601
+ if (options.customUseCases && options.customUseCases.length > 0) {
1602
+ for (const customUc of options.customUseCases) {
1603
+ const ucName = toPascalCase(customUc.name);
1604
+ const ucFiles = generateUseCase({
1605
+ name: ucName,
1606
+ feature: featureName,
1607
+ targetDir: path6.join(targetDir, "useCases"),
1608
+ dependencies: customUc.dependencies || [],
1609
+ inputProperties: customUc.inputProperties || [],
1610
+ outputProperties: customUc.outputProperties || [],
1611
+ domainImports: customUc.domainImports || [],
1612
+ includeTest: customUc.includeTest !== false && options.includeTests !== false,
1613
+ projectRoot,
1614
+ dryRun: options.dryRun,
1615
+ overwrite: options.overwrite
1616
+ });
1617
+ files.push(...ucFiles);
1618
+ if (options.includeExpress !== false && customUc.createHttpHandler) {
1619
+ const handlerName = `${ucName}Http`;
1620
+ const httpFiles = generateHttpHandler({
1621
+ name: handlerName,
1622
+ feature: featureName,
1623
+ routePrefix: routePath,
1624
+ method: customUc.httpMethod || "post",
1625
+ path: customUc.httpPath || `/${toKebabCase(ucName)}`,
1626
+ useCaseName: ucName,
1627
+ includeValidation: customUc.inputProperties && customUc.inputProperties.length > 0 || false,
1628
+ validationSource: customUc.validationSource || (customUc.httpMethod === "get" ? "query" : "body"),
1629
+ validationFields: customUc.validationFields || customUc.inputProperties || [],
1630
+ projectRoot,
1631
+ dryRun: options.dryRun,
1632
+ overwrite: options.overwrite
1633
+ });
1634
+ files.push(...httpFiles);
1635
+ }
1636
+ }
1637
+ }
1638
+ if (options.includeExpress !== false) {
1639
+ const pxDir = path6.join(targetDir, "pxExpress");
1640
+ const pxIndexPath = path6.join(pxDir, "index.ts");
1641
+ if (!files.some((f) => f.absolutePath === pxIndexPath)) {
1642
+ const pxIndexContent = renderTemplate(expressIndexTemplate, { routePath });
1643
+ files.push(
1644
+ writeFileSafely(pxIndexPath, pxIndexContent, {
1645
+ projectRoot,
1646
+ dryRun: options.dryRun,
1647
+ overwrite: false
1648
+ })
1649
+ );
1650
+ }
1651
+ }
1652
+ const uniqueFiles = [];
1653
+ const seen = /* @__PURE__ */ new Set();
1654
+ for (const f of files) {
1655
+ if (!seen.has(f.absolutePath)) {
1656
+ seen.add(f.absolutePath);
1657
+ uniqueFiles.push(f);
1658
+ }
1659
+ }
1660
+ return uniqueFiles;
1661
+ }
1662
+ var PromptCancelledError = class extends Error {
1663
+ constructor(message = "Operation cancelled.") {
1664
+ super(message);
1665
+ this.name = "PromptCancelledError";
1666
+ }
1667
+ };
1668
+ function handleCancel(result) {
1669
+ if (p3.isCancel(result)) {
1670
+ p3.cancel("Operation cancelled.");
1671
+ throw new PromptCancelledError();
1672
+ }
1673
+ return result;
1674
+ }
1675
+ function printBanner() {
1676
+ console.log("");
1677
+ p3.intro(pc13.bgCyan(pc13.black(" @solid-stack/mason ")));
1678
+ }
1679
+ async function promptProperties(options = {}) {
1680
+ const properties = options.defaults ? [...options.defaults] : [];
1681
+ if (properties.length > 0) {
1682
+ const summary = properties.map((p15) => `${p15.name}: ${p15.type}${p15.optional ? "?" : ""}`).join(", ");
1683
+ p3.log.info(pc13.dim(`Pre-configured fields: ${summary}`));
1684
+ const addMore = handleCancel(
1685
+ await p3.confirm({
1686
+ message: "Would you like to add custom fields/properties?",
1687
+ initialValue: false
1688
+ })
1689
+ );
1690
+ if (!addMore) {
1691
+ return properties;
1692
+ }
1693
+ }
1694
+ p3.log.info(pc13.cyan("Enter properties (leave name blank when finished):"));
1695
+ while (true) {
1696
+ const name = handleCancel(
1697
+ await p3.text({
1698
+ message: "Property name (or press Enter to finish):",
1699
+ placeholder: "e.g. title, email, status"
1700
+ })
1701
+ );
1702
+ if (!name || !name.trim()) {
1703
+ break;
1704
+ }
1705
+ const type = handleCancel(
1706
+ await p3.select({
1707
+ message: `Type for property "${name.trim()}":`,
1708
+ options: [
1709
+ { value: "string", label: "string" },
1710
+ { value: "number", label: "number" },
1711
+ { value: "boolean", label: "boolean" },
1712
+ { value: "Date", label: "Date" },
1713
+ { value: "string[]", label: "string[]" },
1714
+ { value: "Record<string, unknown>", label: "Record<string, unknown>" },
1715
+ { value: "custom", label: "Custom type..." }
1716
+ ],
1717
+ initialValue: "string"
1718
+ })
1719
+ );
1720
+ let finalType = type;
1721
+ if (type === "custom") {
1722
+ finalType = handleCancel(
1723
+ await p3.text({
1724
+ message: `Enter custom type for "${name.trim()}":`,
1725
+ placeholder: "e.g. UserRole, string | null",
1726
+ validate: (val) => val && val.trim() ? void 0 : "Type cannot be empty"
1727
+ })
1728
+ );
1729
+ }
1730
+ const optional = handleCancel(
1731
+ await p3.confirm({
1732
+ message: `Is "${name.trim()}" optional?`,
1733
+ initialValue: false
1734
+ })
1735
+ );
1736
+ properties.push({
1737
+ name: name.trim(),
1738
+ type: finalType.trim(),
1739
+ optional
1740
+ });
1741
+ p3.log.success(
1742
+ pc13.green(`Added: ${name.trim()}${optional ? "?" : ""}: ${finalType.trim()}`)
1743
+ );
1744
+ }
1745
+ return properties;
1746
+ }
1747
+
1748
+ // src/commands/feature.ts
1749
+ async function runFeatureCommand(featureNameArg, flags = {}) {
1750
+ const projectRoot = findProjectRoot();
1751
+ let name = featureNameArg?.trim();
1752
+ if (!name && flags.yes) {
1753
+ name = "feature";
1754
+ }
1755
+ if (!name) {
1756
+ name = handleCancel(
1757
+ await p3.text({
1758
+ message: "What is the feature name?",
1759
+ placeholder: "e.g. users, auth, orders, billing",
1760
+ validate: (val) => {
1761
+ if (!val || !val.trim()) return "Feature name cannot be empty.";
1762
+ return void 0;
1763
+ }
1764
+ })
1765
+ ).trim();
1766
+ }
1767
+ const normalizedName = toCamelCase(name);
1768
+ const defaultDir = path6.relative(projectRoot, path6.join(resolveFeaturesDir(projectRoot), normalizedName));
1769
+ let targetDirRel = defaultDir;
1770
+ if (!flags.yes && !flags.target) {
1771
+ targetDirRel = handleCancel(
1772
+ await p3.text({
1773
+ message: "Target directory for the feature:",
1774
+ initialValue: defaultDir,
1775
+ defaultValue: defaultDir
1776
+ })
1777
+ ).trim();
1778
+ }
1779
+ const targetDir = flags.target ? path6.resolve(projectRoot, flags.target) : path6.resolve(projectRoot, targetDirRel);
1780
+ let includeExpress = flags.express !== void 0 ? Boolean(flags.express) : true;
1781
+ let routePath = `/${normalizedName}`;
1782
+ let includeDomain = true;
1783
+ let includeInterface = true;
1784
+ let includeInfra = true;
1785
+ let includeUseCase = true;
1786
+ let includeTests = true;
1787
+ let includeDiProvider = true;
1788
+ const defaultEntity = toPascalCase(toSingular(normalizedName));
1789
+ let entityNames = [defaultEntity];
1790
+ let crudOperations = ["create", "read", "list", "update", "delete"];
1791
+ const customUseCases = [];
1792
+ if (!flags.yes) {
1793
+ includeExpress = handleCancel(
1794
+ await p3.confirm({
1795
+ message: "Include Express Presentation layer (pxExpress routes & handlers)?",
1796
+ initialValue: true
1797
+ })
1798
+ );
1799
+ if (includeExpress) {
1800
+ routePath = handleCancel(
1801
+ await p3.text({
1802
+ message: "Express route prefix:",
1803
+ initialValue: `/${normalizedName}`,
1804
+ defaultValue: `/${normalizedName}`
1805
+ })
1806
+ ).trim();
1807
+ }
1808
+ includeDomain = handleCancel(
1809
+ await p3.confirm({
1810
+ message: "Include Domain Entity/Entities?",
1811
+ initialValue: true
1812
+ })
1813
+ );
1814
+ if (includeDomain) {
1815
+ const domainInput = handleCancel(
1816
+ await p3.text({
1817
+ message: "Domain entity name(s) (comma-separated):",
1818
+ initialValue: defaultEntity,
1819
+ defaultValue: defaultEntity,
1820
+ placeholder: `e.g. ${defaultEntity}, ${defaultEntity}Item`,
1821
+ validate: (val) => {
1822
+ if (!val || !val.trim()) return "Please enter at least one domain entity name.";
1823
+ return void 0;
1824
+ }
1825
+ })
1826
+ ).trim();
1827
+ entityNames = domainInput.split(",").map((s2) => toPascalCase(s2.trim())).filter(Boolean);
1828
+ if (entityNames.length === 0) {
1829
+ entityNames = [defaultEntity];
1830
+ }
1831
+ } else {
1832
+ entityNames = [defaultEntity];
1833
+ }
1834
+ includeInterface = handleCancel(
1835
+ await p3.confirm({
1836
+ message: `Include Domain Repository interface(s) for ${entityNames.join(", ")}?`,
1837
+ initialValue: true
1838
+ })
1839
+ );
1840
+ if (includeInterface) {
1841
+ includeInfra = handleCancel(
1842
+ await p3.confirm({
1843
+ message: `Include Infrastructure Stub implementation(s) for ${entityNames.join(", ")}?`,
1844
+ initialValue: true
1845
+ })
1846
+ );
1847
+ }
1848
+ includeUseCase = handleCancel(
1849
+ await p3.confirm({
1850
+ message: "Include Domain Use Cases?",
1851
+ initialValue: true
1852
+ })
1853
+ );
1854
+ if (includeUseCase) {
1855
+ const crudChoices = [
1856
+ { value: "create", label: "Create (Create<Entity>)", hint: "POST" },
1857
+ { value: "read", label: "Read One (Get<Entity>)", hint: "GET /:id" },
1858
+ { value: "list", label: "List with Pagination (List<Entity>s)", hint: "GET / (query: page, limit)" },
1859
+ { value: "update", label: "Update (Update<Entity>)", hint: "PUT /:id" },
1860
+ { value: "delete", label: "Delete (Delete<Entity>)", hint: "DELETE /:id" }
1861
+ ];
1862
+ crudOperations = handleCancel(
1863
+ await p3.multiselect({
1864
+ message: "Select standard CRUD use cases to generate:",
1865
+ options: crudChoices,
1866
+ initialValues: ["create", "read", "list", "update", "delete"],
1867
+ required: false
1868
+ })
1869
+ );
1870
+ includeTests = handleCancel(
1871
+ await p3.confirm({
1872
+ message: "Generate Vitest unit tests for use cases?",
1873
+ initialValue: true
1874
+ })
1875
+ );
1876
+ const wantCustom = handleCancel(
1877
+ await p3.confirm({
1878
+ message: "Do you want to add any custom use cases to this feature?",
1879
+ initialValue: false
1880
+ })
1881
+ );
1882
+ if (wantCustom) {
1883
+ let addMore = true;
1884
+ const availableIfaces = entityNames.map((e) => `I${e}Repository`);
1885
+ while (addMore) {
1886
+ const customName = handleCancel(
1887
+ await p3.text({
1888
+ message: "Custom Use Case name:",
1889
+ placeholder: "e.g. ResetPassword, VerifyEmail, AssignRole",
1890
+ validate: (v) => v && v.trim() ? void 0 : "Use Case name cannot be empty"
1891
+ })
1892
+ ).trim();
1893
+ const customUcName = toPascalCase(customName);
1894
+ let selectedDeps = [];
1895
+ if (availableIfaces.length > 0) {
1896
+ selectedDeps = handleCancel(
1897
+ await p3.multiselect({
1898
+ message: `Select domain interfaces to inject into ${customUcName}:`,
1899
+ options: availableIfaces.map((i) => ({ value: i, label: i })),
1900
+ required: false
1901
+ })
1902
+ );
1903
+ }
1904
+ let customInputProps = [];
1905
+ const hasInput = handleCancel(
1906
+ await p3.confirm({
1907
+ message: `Does ${customUcName} accept input arguments?`,
1908
+ initialValue: true
1909
+ })
1910
+ );
1911
+ if (hasInput) {
1912
+ customInputProps = await promptProperties({
1913
+ });
1914
+ }
1915
+ let customOutputProps = [];
1916
+ const hasCustomOutput = handleCancel(
1917
+ await p3.confirm({
1918
+ message: `Does ${customUcName} return custom output data?`,
1919
+ initialValue: false
1920
+ })
1921
+ );
1922
+ if (hasCustomOutput) {
1923
+ customOutputProps = await promptProperties({
1924
+ });
1925
+ }
1926
+ const customIncludeTest = handleCancel(
1927
+ await p3.confirm({
1928
+ message: `Generate Vitest unit test for ${customUcName}?`,
1929
+ initialValue: true
1930
+ })
1931
+ );
1932
+ let customCreateHttp = false;
1933
+ let customHttpMethod = "post";
1934
+ let customHttpPath = `/${toKebabCase(customUcName)}`;
1935
+ if (includeExpress) {
1936
+ customCreateHttp = handleCancel(
1937
+ await p3.confirm({
1938
+ message: `Generate a matching ExpressRoute HTTP handler for ${customUcName}?`,
1939
+ initialValue: true
1940
+ })
1941
+ );
1942
+ if (customCreateHttp) {
1943
+ customHttpMethod = handleCancel(
1944
+ await p3.select({
1945
+ message: "HTTP method for route:",
1946
+ options: [
1947
+ { value: "post", label: "POST" },
1948
+ { value: "get", label: "GET" },
1949
+ { value: "put", label: "PUT" },
1950
+ { value: "patch", label: "PATCH" },
1951
+ { value: "delete", label: "DELETE" }
1952
+ ],
1953
+ initialValue: "post"
1954
+ })
1955
+ );
1956
+ customHttpPath = handleCancel(
1957
+ await p3.text({
1958
+ message: "Route subpath (relative to feature prefix):",
1959
+ initialValue: `/${toKebabCase(customUcName)}`,
1960
+ defaultValue: `/${toKebabCase(customUcName)}`
1961
+ })
1962
+ ).trim();
1963
+ }
1964
+ }
1965
+ customUseCases.push({
1966
+ name: customUcName,
1967
+ dependencies: selectedDeps,
1968
+ inputProperties: customInputProps,
1969
+ outputProperties: customOutputProps,
1970
+ includeTest: customIncludeTest,
1971
+ createHttpHandler: customCreateHttp,
1972
+ httpMethod: customHttpMethod,
1973
+ httpPath: customHttpPath,
1974
+ validationSource: customHttpMethod === "get" ? "query" : "body",
1975
+ validationFields: customInputProps
1976
+ });
1977
+ addMore = handleCancel(
1978
+ await p3.confirm({
1979
+ message: "Add another custom use case?",
1980
+ initialValue: false
1981
+ })
1982
+ );
1983
+ }
1984
+ }
1985
+ }
1986
+ includeDiProvider = handleCancel(
1987
+ await p3.confirm({
1988
+ message: "Generate / update feature DI provider (diProvider.ts)?",
1989
+ initialValue: true
1990
+ })
1991
+ );
1992
+ } else {
1993
+ if (flags.domainNames) {
1994
+ entityNames = String(flags.domainNames).split(",").map((s2) => toPascalCase(s2.trim())).filter(Boolean);
1995
+ } else {
1996
+ entityNames = [defaultEntity];
1997
+ }
1998
+ }
1999
+ const s = p3.spinner();
2000
+ s.start(`Generating feature '${pc13.cyan(normalizedName)}'...`);
2001
+ const options = {
2002
+ name: normalizedName,
2003
+ projectRoot,
2004
+ targetDir,
2005
+ dryRun: Boolean(flags.dryRun),
2006
+ overwrite: Boolean(flags.overwrite),
2007
+ includeExpress,
2008
+ routePath,
2009
+ includeDomain,
2010
+ entityNames,
2011
+ includeInterface,
2012
+ includeInfra,
2013
+ includeUseCase,
2014
+ crudOperations,
2015
+ customUseCases,
2016
+ includeTests,
2017
+ includeDiProvider
2018
+ };
2019
+ const files = generateFeature(options);
2020
+ s.stop("Feature scaffolded successfully!");
2021
+ printGeneratedFiles(files, options.dryRun);
2022
+ p3.outro(pc13.green(`Feature '${normalizedName}' ready to use!`));
2023
+ }
2024
+ async function runUseCaseCommand(useCaseNameArg, flags = {}) {
2025
+ const projectRoot = findProjectRoot();
2026
+ const existingFeatures = listExistingFeatures(projectRoot);
2027
+ let feature = flags.feature?.trim();
2028
+ if (!feature) {
2029
+ if (existingFeatures.length > 0) {
2030
+ const featureOptions = existingFeatures.map((f) => ({ value: f, label: f }));
2031
+ featureOptions.push({ value: "__custom__", label: "\u270F\uFE0F Enter custom feature name..." });
2032
+ const selected = handleCancel(
2033
+ await p3.select({
2034
+ message: "Select target feature:",
2035
+ options: featureOptions
2036
+ })
2037
+ );
2038
+ if (selected === "__custom__") {
2039
+ feature = handleCancel(
2040
+ await p3.text({
2041
+ message: "Enter feature name:",
2042
+ validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
2043
+ })
2044
+ ).trim();
2045
+ } else {
2046
+ feature = selected;
2047
+ }
2048
+ } else {
2049
+ feature = handleCancel(
2050
+ await p3.text({
2051
+ message: "Enter target feature name:",
2052
+ validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
2053
+ })
2054
+ ).trim();
2055
+ }
2056
+ }
2057
+ feature = toCamelCase(feature);
2058
+ let name = useCaseNameArg?.trim();
2059
+ if (!name) {
2060
+ name = handleCancel(
2061
+ await p3.text({
2062
+ message: "Use Case name:",
2063
+ placeholder: "e.g. CreateUser, GetUser, CancelOrder",
2064
+ validate: (v) => v && v.trim() ? void 0 : "Use Case name cannot be empty"
2065
+ })
2066
+ ).trim();
2067
+ }
2068
+ const ucName = toPascalCase(name);
2069
+ const availableInterfaces = listDomainInterfaces(projectRoot, feature);
2070
+ let selectedDeps = [];
2071
+ if (!flags.yes && availableInterfaces.length > 0) {
2072
+ const multiOptions = availableInterfaces.map((i) => ({
2073
+ value: i.name,
2074
+ label: i.name
2075
+ }));
2076
+ selectedDeps = handleCancel(
2077
+ await p3.multiselect({
2078
+ message: "Select domain interfaces to inject into use case:",
2079
+ options: multiOptions,
2080
+ required: false
2081
+ })
2082
+ );
2083
+ }
2084
+ let inputProps = [];
2085
+ let outputProps = [];
2086
+ let includeTest = true;
2087
+ let createHttp = false;
2088
+ let httpMethod = "post";
2089
+ let httpPath = "/";
2090
+ if (!flags.yes) {
2091
+ const hasInput = handleCancel(
2092
+ await p3.confirm({
2093
+ message: "Does this use case accept input arguments?",
2094
+ initialValue: true
2095
+ })
2096
+ );
2097
+ if (hasInput) {
2098
+ inputProps = await promptProperties({
2099
+ });
2100
+ }
2101
+ const hasCustomOutput = handleCancel(
2102
+ await p3.confirm({
2103
+ message: "Does this use case return custom output data?",
2104
+ initialValue: false
2105
+ })
2106
+ );
2107
+ if (hasCustomOutput) {
2108
+ outputProps = await promptProperties({
2109
+ });
2110
+ }
2111
+ includeTest = handleCancel(
2112
+ await p3.confirm({
2113
+ message: "Generate Vitest unit test skeleton (.test.ts)?",
2114
+ initialValue: true
2115
+ })
2116
+ );
2117
+ createHttp = handleCancel(
2118
+ await p3.confirm({
2119
+ message: "Generate a matching ExpressRoute HTTP handler?",
2120
+ initialValue: false
2121
+ })
2122
+ );
2123
+ if (createHttp) {
2124
+ httpMethod = handleCancel(
2125
+ await p3.select({
2126
+ message: "HTTP method for route:",
2127
+ options: [
2128
+ { value: "post", label: "POST" },
2129
+ { value: "get", label: "GET" },
2130
+ { value: "put", label: "PUT" },
2131
+ { value: "patch", label: "PATCH" },
2132
+ { value: "delete", label: "DELETE" }
2133
+ ],
2134
+ initialValue: "post"
2135
+ })
2136
+ );
2137
+ httpPath = handleCancel(
2138
+ await p3.text({
2139
+ message: "Route subpath (relative to feature prefix):",
2140
+ initialValue: "/",
2141
+ defaultValue: "/"
2142
+ })
2143
+ ).trim();
2144
+ }
2145
+ }
2146
+ const s = p3.spinner();
2147
+ s.start(`Generating use case '${pc13.cyan(ucName)}'...`);
2148
+ const options = {
2149
+ name: ucName,
2150
+ feature,
2151
+ dependencies: selectedDeps,
2152
+ inputProperties: inputProps,
2153
+ outputProperties: outputProps,
2154
+ includeTest,
2155
+ projectRoot,
2156
+ dryRun: Boolean(flags.dryRun),
2157
+ overwrite: Boolean(flags.overwrite)
2158
+ };
2159
+ const files = generateUseCase(options);
2160
+ if (createHttp) {
2161
+ const handlerName = `${ucName}Http`;
2162
+ const httpFiles = generateHttpHandler({
2163
+ name: handlerName,
2164
+ feature,
2165
+ method: httpMethod,
2166
+ path: httpPath,
2167
+ useCaseName: ucName,
2168
+ includeValidation: inputProps.length > 0,
2169
+ validationSource: httpMethod === "get" ? "query" : "body",
2170
+ validationFields: inputProps,
2171
+ projectRoot,
2172
+ dryRun: Boolean(flags.dryRun),
2173
+ overwrite: Boolean(flags.overwrite)
2174
+ });
2175
+ files.push(...httpFiles);
2176
+ }
2177
+ s.stop("Use case generated successfully!");
2178
+ printGeneratedFiles(files, options.dryRun);
2179
+ p3.outro(pc13.green(`Use case '${ucName}' created in feature '${feature}'!`));
2180
+ }
2181
+ async function runDomainCommand(domainNameArg, flags = {}) {
2182
+ const projectRoot = findProjectRoot();
2183
+ const existingFeatures = listExistingFeatures(projectRoot);
2184
+ let feature = flags.feature?.trim();
2185
+ if (!feature) {
2186
+ if (existingFeatures.length > 0) {
2187
+ const featureOptions = existingFeatures.map((f) => ({ value: f, label: f }));
2188
+ featureOptions.push({ value: "__custom__", label: "\u270F\uFE0F Enter custom feature name..." });
2189
+ const selected = handleCancel(
2190
+ await p3.select({
2191
+ message: "Select target feature:",
2192
+ options: featureOptions
2193
+ })
2194
+ );
2195
+ if (selected === "__custom__") {
2196
+ feature = handleCancel(
2197
+ await p3.text({
2198
+ message: "Enter feature name:",
2199
+ validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
2200
+ })
2201
+ ).trim();
2202
+ } else {
2203
+ feature = selected;
2204
+ }
2205
+ } else {
2206
+ feature = handleCancel(
2207
+ await p3.text({
2208
+ message: "Enter target feature name:",
2209
+ validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
2210
+ })
2211
+ ).trim();
2212
+ }
2213
+ }
2214
+ feature = toCamelCase(feature);
2215
+ let name = domainNameArg?.trim();
2216
+ if (!name) {
2217
+ name = handleCancel(
2218
+ await p3.text({
2219
+ message: "Domain model / entity name:",
2220
+ placeholder: "e.g. User, Order, Product, Address",
2221
+ validate: (v) => v && v.trim() ? void 0 : "Domain name cannot be empty"
2222
+ })
2223
+ ).trim();
2224
+ }
2225
+ const entityName = toPascalCase(name);
2226
+ let kind = "interface";
2227
+ let properties = [];
2228
+ if (!flags.yes) {
2229
+ kind = handleCancel(
2230
+ await p3.select({
2231
+ message: "Select domain model type:",
2232
+ options: [
2233
+ { value: "interface", label: "TypeScript Interface (pure data structure)" },
2234
+ { value: "class", label: "Entity Class (with constructor & properties)" },
2235
+ { value: "valueObject", label: "Value Object (immutable domain object with equals)" }
2236
+ ],
2237
+ initialValue: "interface"
2238
+ })
2239
+ );
2240
+ const defaultProps = [
2241
+ { name: "id", type: "string" },
2242
+ { name: "createdAt", type: "Date" },
2243
+ { name: "updatedAt", type: "Date" }
2244
+ ];
2245
+ properties = await promptProperties({
2246
+ defaults: kind === "valueObject" ? [{ name: "value", type: "string" }] : defaultProps
2247
+ });
2248
+ }
2249
+ const s = p3.spinner();
2250
+ s.start(`Generating domain model '${pc13.cyan(entityName)}'...`);
2251
+ const options = {
2252
+ name: entityName,
2253
+ feature,
2254
+ kind,
2255
+ properties,
2256
+ projectRoot,
2257
+ dryRun: Boolean(flags.dryRun),
2258
+ overwrite: Boolean(flags.overwrite)
2259
+ };
2260
+ const file = generateDomain(options);
2261
+ s.stop("Domain model generated successfully!");
2262
+ printGeneratedFiles([file], options.dryRun);
2263
+ p3.outro(pc13.green(`Domain model '${entityName}' created in feature '${feature}'!`));
2264
+ }
2265
+ async function runInfrastructureCommand(classNameArg, flags = {}) {
2266
+ const projectRoot = findProjectRoot();
2267
+ const existingFeatures = listExistingFeatures(projectRoot);
2268
+ let scope = flags.global ? "global" : "feature";
2269
+ let feature = flags.feature?.trim();
2270
+ if (!flags.yes && !flags.global && !flags.feature) {
2271
+ scope = handleCancel(
2272
+ await p3.select({
2273
+ message: "Where should this infrastructure implementation be placed?",
2274
+ options: [
2275
+ { value: "feature", label: "Feature-level (features/<feature>/infrastructure)" },
2276
+ { value: "global", label: "Global / Core (infrastructure/)" }
2277
+ ],
2278
+ initialValue: "feature"
2279
+ })
2280
+ );
2281
+ }
2282
+ if (scope === "feature" && !feature) {
2283
+ if (existingFeatures.length > 0) {
2284
+ const featureOptions = existingFeatures.map((f) => ({ value: f, label: f }));
2285
+ featureOptions.push({ value: "__custom__", label: "\u270F\uFE0F Enter custom feature name..." });
2286
+ const selected = handleCancel(
2287
+ await p3.select({
2288
+ message: "Select target feature:",
2289
+ options: featureOptions
2290
+ })
2291
+ );
2292
+ if (selected === "__custom__") {
2293
+ feature = handleCancel(
2294
+ await p3.text({
2295
+ message: "Enter feature name:",
2296
+ validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
2297
+ })
2298
+ ).trim();
2299
+ } else {
2300
+ feature = selected;
2301
+ }
2302
+ } else {
2303
+ feature = handleCancel(
2304
+ await p3.text({
2305
+ message: "Enter target feature name:",
2306
+ validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
2307
+ })
2308
+ ).trim();
2309
+ }
2310
+ }
2311
+ if (feature) {
2312
+ feature = toCamelCase(feature);
2313
+ }
2314
+ let name = classNameArg?.trim();
2315
+ if (!name) {
2316
+ name = handleCancel(
2317
+ await p3.text({
2318
+ message: "Infrastructure class name:",
2319
+ placeholder: "e.g. StubUserRepository, PostgresUserRepository, S3FileStorage",
2320
+ validate: (v) => v && v.trim() ? void 0 : "Class name cannot be empty"
2321
+ })
2322
+ ).trim();
2323
+ }
2324
+ const className = toPascalCase(name);
2325
+ let kind = flags.kind ? flags.kind === "stub" ? "stub" : "concrete" : className.startsWith("Stub") ? "stub" : "concrete";
2326
+ let implementsInterface = flags.interface?.trim() || void 0;
2327
+ let registerInProvider = false;
2328
+ if (scope === "feature" && feature && !implementsInterface) {
2329
+ const interfaces = listDomainInterfaces(projectRoot, feature);
2330
+ if (!flags.yes) {
2331
+ if (interfaces.length > 0) {
2332
+ const match = interfaces.find(
2333
+ (i) => className.toLowerCase().includes(removeInterfacePrefix(i.name).toLowerCase())
2334
+ );
2335
+ const ifaceChoice = handleCancel(
2336
+ await p3.select({
2337
+ message: "Does this class implement a domain interface or abstract class?",
2338
+ options: [
2339
+ ...interfaces.map((i) => ({ value: i.name, label: i.name })),
2340
+ { value: "__custom__", label: "\u270F\uFE0F Enter custom interface or abstract class..." },
2341
+ { value: "__none__", label: "None / Standalone" }
2342
+ ],
2343
+ initialValue: match?.name || interfaces[0]?.name
2344
+ })
2345
+ );
2346
+ if (ifaceChoice === "__custom__") {
2347
+ implementsInterface = handleCancel(
2348
+ await p3.text({
2349
+ message: "Enter interface or abstract class name:",
2350
+ placeholder: "e.g. IUserRepository, BaseStorageAdapter",
2351
+ validate: (v) => v && v.trim() ? void 0 : "Name cannot be empty"
2352
+ })
2353
+ ).trim();
2354
+ } else if (ifaceChoice !== "__none__") {
2355
+ implementsInterface = ifaceChoice;
2356
+ }
2357
+ } else {
2358
+ const wantImpl = handleCancel(
2359
+ await p3.confirm({
2360
+ message: "Does this class implement a domain interface or abstract class?",
2361
+ initialValue: false
2362
+ })
2363
+ );
2364
+ if (wantImpl) {
2365
+ implementsInterface = handleCancel(
2366
+ await p3.text({
2367
+ message: "Enter interface or abstract class name:",
2368
+ placeholder: "e.g. IUserRepository, BaseStorageAdapter",
2369
+ validate: (v) => v && v.trim() ? void 0 : "Name cannot be empty"
2370
+ })
2371
+ ).trim();
2372
+ }
2373
+ }
2374
+ } else if (flags.yes && interfaces.length > 0) {
2375
+ const match = interfaces.find(
2376
+ (i) => className.toLowerCase().includes(removeInterfacePrefix(i.name).toLowerCase())
2377
+ );
2378
+ if (match) {
2379
+ implementsInterface = match.name;
2380
+ }
2381
+ }
2382
+ } else if (scope === "global" && !implementsInterface && !flags.yes) {
2383
+ const allInterfaces = listDomainInterfaces(projectRoot);
2384
+ if (allInterfaces.length > 0) {
2385
+ const ifaceChoice = handleCancel(
2386
+ await p3.select({
2387
+ message: "Does this class implement an interface or abstract class?",
2388
+ options: [
2389
+ ...allInterfaces.map((i) => ({ value: i.name, label: `${i.name} (${i.feature})` })),
2390
+ { value: "__custom__", label: "\u270F\uFE0F Enter custom interface or abstract class..." },
2391
+ { value: "__none__", label: "None / Standalone" }
2392
+ ],
2393
+ initialValue: "__none__"
2394
+ })
2395
+ );
2396
+ if (ifaceChoice === "__custom__") {
2397
+ implementsInterface = handleCancel(
2398
+ await p3.text({
2399
+ message: "Enter interface or abstract class name:",
2400
+ placeholder: "e.g. IAuthRepository",
2401
+ validate: (v) => v && v.trim() ? void 0 : "Name cannot be empty"
2402
+ })
2403
+ ).trim();
2404
+ } else if (ifaceChoice !== "__none__") {
2405
+ implementsInterface = ifaceChoice;
2406
+ }
2407
+ }
2408
+ }
2409
+ if (!flags.yes && !flags.kind) {
2410
+ kind = handleCancel(
2411
+ await p3.select({
2412
+ message: "Select implementation kind:",
2413
+ options: [
2414
+ { value: "stub", label: "In-Memory Stub (with in-memory store and test helper methods)" },
2415
+ { value: "concrete", label: "Concrete Adapter / Service Skeleton (@MakeInjectable)" }
2416
+ ],
2417
+ initialValue: kind
2418
+ })
2419
+ );
2420
+ if (scope === "feature" && feature && implementsInterface) {
2421
+ registerInProvider = handleCancel(
2422
+ await p3.confirm({
2423
+ message: "Register this implementation in feature diProvider.ts?",
2424
+ initialValue: true
2425
+ })
2426
+ );
2427
+ }
2428
+ } else if (flags.yes && scope === "feature" && feature && implementsInterface) {
2429
+ registerInProvider = !flags.noProvider;
2430
+ }
2431
+ const s = p3.spinner();
2432
+ s.start(`Generating infrastructure class '${pc13.cyan(className)}'...`);
2433
+ const files = [];
2434
+ const options = {
2435
+ name: className,
2436
+ feature,
2437
+ scope,
2438
+ kind,
2439
+ implementsInterface,
2440
+ projectRoot,
2441
+ dryRun: Boolean(flags.dryRun),
2442
+ overwrite: Boolean(flags.overwrite)
2443
+ };
2444
+ const infraFile = generateInfrastructure(options);
2445
+ files.push(infraFile);
2446
+ if (registerInProvider && scope === "feature" && feature && implementsInterface) {
2447
+ const featureDir = path6.join(resolveFeaturesDir(projectRoot), feature);
2448
+ const providerFile = updateOrGenerateDiProvider(
2449
+ featureDir,
2450
+ feature,
2451
+ [
2452
+ {
2453
+ ifaceName: implementsInterface,
2454
+ implName: className
2455
+ }
2456
+ ],
2457
+ {
2458
+ projectRoot,
2459
+ dryRun: Boolean(flags.dryRun),
2460
+ overwrite: Boolean(flags.overwrite)
2461
+ }
2462
+ );
2463
+ files.push(providerFile);
2464
+ }
2465
+ s.stop("Infrastructure class generated successfully!");
2466
+ printGeneratedFiles(files, options.dryRun);
2467
+ p3.outro(pc13.green(`Infrastructure class '${className}' created!`));
2468
+ }
2469
+ async function runInterfaceCommand(interfaceNameArg, flags = {}) {
2470
+ const projectRoot = findProjectRoot();
2471
+ const existingFeatures = listExistingFeatures(projectRoot);
2472
+ let feature = flags.feature?.trim();
2473
+ if (!feature) {
2474
+ if (existingFeatures.length > 0) {
2475
+ const featureOptions = existingFeatures.map((f) => ({ value: f, label: f }));
2476
+ featureOptions.push({ value: "__custom__", label: "\u270F\uFE0F Enter custom feature name..." });
2477
+ const selected = handleCancel(
2478
+ await p3.select({
2479
+ message: "Select target feature:",
2480
+ options: featureOptions
2481
+ })
2482
+ );
2483
+ if (selected === "__custom__") {
2484
+ feature = handleCancel(
2485
+ await p3.text({
2486
+ message: "Enter feature name:",
2487
+ validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
2488
+ })
2489
+ ).trim();
2490
+ } else {
2491
+ feature = selected;
2492
+ }
2493
+ } else {
2494
+ feature = handleCancel(
2495
+ await p3.text({
2496
+ message: "Enter target feature name:",
2497
+ validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
2498
+ })
2499
+ ).trim();
2500
+ }
2501
+ }
2502
+ feature = toCamelCase(feature);
2503
+ let name = interfaceNameArg?.trim();
2504
+ if (!name) {
2505
+ name = handleCancel(
2506
+ await p3.text({
2507
+ message: "Domain interface name:",
2508
+ placeholder: "e.g. IUserRepository, IPaymentService",
2509
+ validate: (v) => v && v.trim() ? void 0 : "Interface name cannot be empty"
2510
+ })
2511
+ ).trim();
2512
+ }
2513
+ const ifaceName = ensureInterfacePrefix(name);
2514
+ const existingEntities = listDomainEntities(projectRoot, feature);
2515
+ const fallbackEntity = existingEntities.length > 0 ? existingEntities.find((e) => ifaceName.toLowerCase().includes(e.name.toLowerCase()))?.name || existingEntities[0]?.name : removeInterfacePrefix(ifaceName).replace(/Repository$|Service$/, "");
2516
+ let entityName = flags.entity || fallbackEntity;
2517
+ let kind = "abstractClass";
2518
+ let methodsPreset = "crud";
2519
+ let generateStub = true;
2520
+ let registerInProvider = true;
2521
+ if (!flags.yes) {
2522
+ if (existingEntities.length > 0) {
2523
+ const selectedEntity = handleCancel(
2524
+ await p3.select({
2525
+ message: "Select domain entity associated with this interface:",
2526
+ options: [
2527
+ ...existingEntities.map((e) => ({ value: e.name, label: e.name })),
2528
+ { value: "__custom__", label: "\u270F\uFE0F Enter custom entity name..." }
2529
+ ],
2530
+ initialValue: fallbackEntity
2531
+ })
2532
+ );
2533
+ if (selectedEntity === "__custom__") {
2534
+ entityName = handleCancel(
2535
+ await p3.text({
2536
+ message: "Enter entity name:",
2537
+ defaultValue: removeInterfacePrefix(ifaceName).replace(/Repository$|Service$/, "")
2538
+ })
2539
+ ).trim();
2540
+ } else {
2541
+ entityName = selectedEntity;
2542
+ }
2543
+ }
2544
+ kind = handleCancel(
2545
+ await p3.select({
2546
+ message: "Interface definition style:",
2547
+ options: [
2548
+ { value: "abstractClass", label: "Abstract Class (Recommended - acts as DI token & type)" },
2549
+ { value: "interface", label: "TypeScript Interface (type-only)" }
2550
+ ],
2551
+ initialValue: "abstractClass"
2552
+ })
2553
+ );
2554
+ methodsPreset = handleCancel(
2555
+ await p3.select({
2556
+ message: "Method signature preset:",
2557
+ options: [
2558
+ { value: "crud", label: "Standard CRUD (GetById, List, Create, Update, Delete)" },
2559
+ { value: "empty", label: "Empty / Minimal skeleton" }
2560
+ ],
2561
+ initialValue: "crud"
2562
+ })
2563
+ );
2564
+ generateStub = handleCancel(
2565
+ await p3.confirm({
2566
+ message: "Auto-generate Infrastructure Stub implementation?",
2567
+ initialValue: true
2568
+ })
2569
+ );
2570
+ if (generateStub) {
2571
+ registerInProvider = handleCancel(
2572
+ await p3.confirm({
2573
+ message: "Auto-register interface & stub in feature diProvider.ts?",
2574
+ initialValue: true
2575
+ })
2576
+ );
2577
+ }
2578
+ }
2579
+ const s = p3.spinner();
2580
+ s.start(`Generating interface '${pc13.cyan(ifaceName)}'...`);
2581
+ const files = [];
2582
+ const options = {
2583
+ name: ifaceName,
2584
+ feature,
2585
+ kind,
2586
+ entityName,
2587
+ methodsPreset,
2588
+ projectRoot,
2589
+ dryRun: Boolean(flags.dryRun),
2590
+ overwrite: Boolean(flags.overwrite)
2591
+ };
2592
+ const ifaceFile = generateInterface(options);
2593
+ files.push(ifaceFile);
2594
+ const stubClassName = toStubName(ifaceName);
2595
+ if (generateStub) {
2596
+ const stubFile = generateInfrastructure({
2597
+ name: stubClassName,
2598
+ feature,
2599
+ kind: "stub",
2600
+ implementsInterface: ifaceName,
2601
+ entityName,
2602
+ projectRoot,
2603
+ dryRun: Boolean(flags.dryRun),
2604
+ overwrite: Boolean(flags.overwrite)
2605
+ });
2606
+ files.push(stubFile);
2607
+ }
2608
+ if (registerInProvider && generateStub) {
2609
+ const featureDir = path6.join(resolveFeaturesDir(projectRoot), feature);
2610
+ const providerFile = updateOrGenerateDiProvider(
2611
+ featureDir,
2612
+ feature,
2613
+ [
2614
+ {
2615
+ ifaceName,
2616
+ implName: stubClassName
2617
+ }
2618
+ ],
2619
+ {
2620
+ projectRoot,
2621
+ dryRun: Boolean(flags.dryRun),
2622
+ overwrite: Boolean(flags.overwrite)
2623
+ }
2624
+ );
2625
+ files.push(providerFile);
2626
+ }
2627
+ s.stop("Interface generated successfully!");
2628
+ printGeneratedFiles(files, options.dryRun);
2629
+ p3.outro(pc13.green(`Interface '${ifaceName}' created in feature '${feature}'!`));
2630
+ }
2631
+ async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
2632
+ const projectRoot = findProjectRoot();
2633
+ const existingFeatures = listExistingFeatures(projectRoot);
2634
+ let feature = flags.feature?.trim();
2635
+ if (!feature) {
2636
+ if (existingFeatures.length > 0) {
2637
+ const featureOptions = existingFeatures.map((f) => ({ value: f, label: f }));
2638
+ featureOptions.push({ value: "__custom__", label: "\u270F\uFE0F Enter custom feature name..." });
2639
+ const selected = handleCancel(
2640
+ await p3.select({
2641
+ message: "Select target feature:",
2642
+ options: featureOptions
2643
+ })
2644
+ );
2645
+ if (selected === "__custom__") {
2646
+ feature = handleCancel(
2647
+ await p3.text({
2648
+ message: "Enter feature name:",
2649
+ validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
2650
+ })
2651
+ ).trim();
2652
+ } else {
2653
+ feature = selected;
2654
+ }
2655
+ } else {
2656
+ feature = handleCancel(
2657
+ await p3.text({
2658
+ message: "Enter target feature name:",
2659
+ validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
2660
+ })
2661
+ ).trim();
2662
+ }
2663
+ }
2664
+ feature = toCamelCase(feature);
2665
+ let name = handlerNameArg?.trim();
2666
+ if (!name) {
2667
+ name = handleCancel(
2668
+ await p3.text({
2669
+ message: "HTTP Route Handler class name:",
2670
+ placeholder: "e.g. CreateUserHttp, GetUserHttp, HealthHttp",
2671
+ validate: (v) => v && v.trim() ? void 0 : "Handler name cannot be empty"
2672
+ })
2673
+ ).trim();
2674
+ }
2675
+ let className = toPascalCase(name);
2676
+ if (!className.endsWith("Http") && !className.endsWith("Route")) {
2677
+ className = `${className}Http`;
2678
+ }
2679
+ let method = flags.method || "get";
2680
+ let subPath = flags.path || "/";
2681
+ let useCaseName = flags.usecase;
2682
+ let includeValidation = true;
2683
+ let validationSource = "body";
2684
+ let validationFields = [];
2685
+ if (!flags.yes) {
2686
+ method = handleCancel(
2687
+ await p3.select({
2688
+ message: "HTTP Method:",
2689
+ options: [
2690
+ { value: "get", label: "GET" },
2691
+ { value: "post", label: "POST" },
2692
+ { value: "put", label: "PUT" },
2693
+ { value: "patch", label: "PATCH" },
2694
+ { value: "delete", label: "DELETE" }
2695
+ ],
2696
+ initialValue: method
2697
+ })
2698
+ );
2699
+ const defaultPath = method === "get" ? "/" : "/";
2700
+ subPath = handleCancel(
2701
+ await p3.text({
2702
+ message: "Route subpath (relative to feature prefix):",
2703
+ initialValue: defaultPath,
2704
+ defaultValue: defaultPath
2705
+ })
2706
+ ).trim();
2707
+ const useCases = listUseCases(projectRoot, feature);
2708
+ if (useCases.length > 0) {
2709
+ const ucOptions = useCases.map((u) => ({ value: u.name, label: u.name }));
2710
+ ucOptions.push({ value: "__none__", label: "None / Standalone Handler" });
2711
+ const chosenUc = handleCancel(
2712
+ await p3.select({
2713
+ message: "Connect this handler to a domain Use Case?",
2714
+ options: ucOptions,
2715
+ initialValue: ucOptions[0]?.value
2716
+ })
2717
+ );
2718
+ if (chosenUc !== "__none__") {
2719
+ useCaseName = chosenUc;
2720
+ }
2721
+ }
2722
+ includeValidation = handleCancel(
2723
+ await p3.confirm({
2724
+ message: "Include Zod inbound request validation?",
2725
+ initialValue: method !== "delete"
2726
+ })
2727
+ );
2728
+ if (includeValidation) {
2729
+ const defaultSource = method === "get" ? "query" : "body";
2730
+ validationSource = handleCancel(
2731
+ await p3.select({
2732
+ message: "Validation data source:",
2733
+ options: [
2734
+ { value: "body", label: "req.body (for POST / PUT / PATCH payloads)" },
2735
+ { value: "query", label: "req.query (for GET search/filter queries)" },
2736
+ { value: "params", label: "req.params (for route parameters e.g. :id)" }
2737
+ ],
2738
+ initialValue: defaultSource
2739
+ })
2740
+ );
2741
+ validationFields = await promptProperties({
2742
+ });
2743
+ }
2744
+ }
2745
+ const s = p3.spinner();
2746
+ s.start(`Generating HTTP handler '${pc13.cyan(className)}'...`);
2747
+ const options = {
2748
+ name: className,
2749
+ feature,
2750
+ method,
2751
+ path: subPath,
2752
+ useCaseName,
2753
+ includeValidation,
2754
+ validationSource,
2755
+ validationFields,
2756
+ successMessage: flags.message,
2757
+ projectRoot,
2758
+ dryRun: Boolean(flags.dryRun),
2759
+ overwrite: Boolean(flags.overwrite)
2760
+ };
2761
+ const files = generateHttpHandler(options);
2762
+ s.stop("HTTP handler generated successfully!");
2763
+ printGeneratedFiles(files, options.dryRun);
2764
+ p3.outro(pc13.green(`HTTP handler '${className}' created in feature '${feature}'!`));
2765
+ }
2766
+ function generateInjectableCode(className, dependencies = [], methods = [], implementsInterface) {
2767
+ const formattedDeps = dependencies.map((dep) => ({
2768
+ key: toCamelCase(dep),
2769
+ name: dep
2770
+ }));
2771
+ const formattedMethods = methods.map((m) => {
2772
+ const isAsync = m.async !== false;
2773
+ const retType = m.returnType ? isAsync ? `Promise<${m.returnType}>` : m.returnType : isAsync ? "Promise<void>" : "void";
2774
+ const params = m.params && m.params.length > 0 ? m.params.map((p15) => `${p15.name}: ${p15.type}`).join(", ") : "";
2775
+ return {
2776
+ name: m.name,
2777
+ isAsync,
2778
+ params,
2779
+ returnType: retType
2780
+ };
2781
+ });
2782
+ return renderTemplate(injectableTemplate, {
2783
+ className,
2784
+ implementsInterface,
2785
+ dependencies: formattedDeps,
2786
+ hasDependencies: dependencies.length > 0,
2787
+ hasMethods: formattedMethods.length > 0,
2788
+ methods: formattedMethods
2789
+ });
2790
+ }
2791
+ function generateInjectable(options) {
2792
+ const files = [];
2793
+ const projectRoot = options.projectRoot || findProjectRoot();
2794
+ const className = toPascalCase(options.name);
2795
+ let targetDir = "";
2796
+ if (options.customPath) {
2797
+ targetDir = path6.isAbsolute(options.customPath) ? options.customPath : path6.join(projectRoot, options.customPath);
2798
+ } else if (options.location === "feature-service" && options.feature) {
2799
+ targetDir = path6.join(resolveFeaturesDir(projectRoot), options.feature, "services");
2800
+ } else if (options.location === "feature-transformer" && options.feature) {
2801
+ targetDir = path6.join(resolveFeaturesDir(projectRoot), options.feature, "pxExpress", "transformers");
2802
+ } else if (options.location === "module") {
2803
+ targetDir = path6.join(projectRoot, "modules", className);
2804
+ } else {
2805
+ targetDir = options.feature ? path6.join(resolveFeaturesDir(projectRoot), options.feature, "services") : path6.join(projectRoot, "shared", className.toLowerCase());
2806
+ }
2807
+ const filePath = path6.join(targetDir, `${className}.ts`);
2808
+ let ifaceName;
2809
+ if (options.generateInterface) {
2810
+ ifaceName = `I${className}`;
2811
+ const ifacePath = path6.join(targetDir, `${ifaceName}.ts`);
2812
+ const ifaceContent = renderTemplate(injectableInterfaceTemplate, { ifaceName });
2813
+ files.push(
2814
+ writeFileSafely(ifacePath, ifaceContent, {
2815
+ projectRoot,
2816
+ dryRun: options.dryRun,
2817
+ overwrite: options.overwrite
2818
+ })
2819
+ );
2820
+ }
2821
+ const code = generateInjectableCode(
2822
+ className,
2823
+ options.dependencies || [],
2824
+ options.methods || [],
2825
+ ifaceName
2826
+ );
2827
+ files.push(
2828
+ writeFileSafely(filePath, code, {
2829
+ projectRoot,
2830
+ dryRun: options.dryRun,
2831
+ overwrite: options.overwrite
2832
+ })
2833
+ );
2834
+ return files;
2835
+ }
2836
+
2837
+ // src/commands/injectable.ts
2838
+ async function runInjectableCommand(classNameArg, flags = {}) {
2839
+ const projectRoot = findProjectRoot();
2840
+ const existingFeatures = listExistingFeatures(projectRoot);
2841
+ let name = classNameArg?.trim();
2842
+ if (!name) {
2843
+ name = handleCancel(
2844
+ await p3.text({
2845
+ message: "Injectable class name:",
2846
+ placeholder: "e.g. PasswordHasher, TokenService, UserTransformer",
2847
+ validate: (v) => v && v.trim() ? void 0 : "Class name cannot be empty"
2848
+ })
2849
+ ).trim();
2850
+ }
2851
+ const className = toPascalCase(name);
2852
+ let location = "feature-service";
2853
+ let feature = flags.feature?.trim();
2854
+ let customPath;
2855
+ if (!flags.yes) {
2856
+ location = handleCancel(
2857
+ await p3.select({
2858
+ message: "Select target architectural location:",
2859
+ options: [
2860
+ { value: "feature-service", label: "Feature Service (features/<feature>/services/)" },
2861
+ { value: "feature-transformer", label: "Feature Transformer (features/<feature>/pxExpress/transformers/)" },
2862
+ { value: "shared", label: "Shared Module (shared/<name>/)" },
2863
+ { value: "module", label: "Core Module (modules/<Name>/)" },
2864
+ { value: "custom", label: "Custom directory path..." }
2865
+ ],
2866
+ initialValue: "feature-service"
2867
+ })
2868
+ );
2869
+ if (location === "feature-service" || location === "feature-transformer") {
2870
+ if (!feature) {
2871
+ if (existingFeatures.length > 0) {
2872
+ const featureOptions = existingFeatures.map((f) => ({ value: f, label: f }));
2873
+ featureOptions.push({ value: "__custom__", label: "\u270F\uFE0F Enter custom feature name..." });
2874
+ const selected = handleCancel(
2875
+ await p3.select({
2876
+ message: "Select target feature:",
2877
+ options: featureOptions
2878
+ })
2879
+ );
2880
+ if (selected === "__custom__") {
2881
+ feature = handleCancel(
2882
+ await p3.text({
2883
+ message: "Enter feature name:",
2884
+ validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
2885
+ })
2886
+ ).trim();
2887
+ } else {
2888
+ feature = selected;
2889
+ }
2890
+ } else {
2891
+ feature = handleCancel(
2892
+ await p3.text({
2893
+ message: "Enter target feature name:",
2894
+ validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
2895
+ })
2896
+ ).trim();
2897
+ }
2898
+ }
2899
+ feature = toCamelCase(feature);
2900
+ } else if (location === "custom") {
2901
+ customPath = handleCancel(
2902
+ await p3.text({
2903
+ message: "Enter destination directory path:",
2904
+ placeholder: "e.g. src/services or lib/utils",
2905
+ validate: (v) => v && v.trim() ? void 0 : "Path cannot be empty"
2906
+ })
2907
+ ).trim();
2908
+ }
2909
+ }
2910
+ let generateInterface2 = false;
2911
+ if (!flags.yes) {
2912
+ generateInterface2 = handleCancel(
2913
+ await p3.confirm({
2914
+ message: `Generate abstract class interface (I${className}) as runtime DI token?`,
2915
+ initialValue: false
2916
+ })
2917
+ );
2918
+ }
2919
+ const s = p3.spinner();
2920
+ s.start(`Generating injectable '${pc13.cyan(className)}'...`);
2921
+ const options = {
2922
+ name: className,
2923
+ feature,
2924
+ location,
2925
+ customPath,
2926
+ generateInterface: generateInterface2,
2927
+ projectRoot,
2928
+ dryRun: Boolean(flags.dryRun),
2929
+ overwrite: Boolean(flags.overwrite)
2930
+ };
2931
+ const files = generateInjectable(options);
2932
+ s.stop("Injectable class generated successfully!");
2933
+ printGeneratedFiles(files, options.dryRun);
2934
+ p3.outro(pc13.green(`Injectable '${className}' created successfully!`));
2935
+ }
2936
+ async function runInteractiveMenu(flags = {}) {
2937
+ printBanner();
2938
+ const choice = handleCancel(
2939
+ await p3.select({
2940
+ message: "What would you like to create in your architecture?",
2941
+ options: [
2942
+ {
2943
+ value: "feature",
2944
+ label: "Feature",
2945
+ hint: "Complete vertical slice (domain, infra, usecase, pxExpress, diProvider)"
2946
+ },
2947
+ {
2948
+ value: "usecase",
2949
+ label: "Use Case",
2950
+ hint: "Domain usecase with @MakeInjectable, DTOs & Vitest tests"
2951
+ },
2952
+ {
2953
+ value: "domain",
2954
+ label: "Domain Entity / Model",
2955
+ hint: "Zero-dependency pure TypeScript interface, class, or value object"
2956
+ },
2957
+ {
2958
+ value: "interface",
2959
+ label: "Domain Interface",
2960
+ hint: "Abstract class repository/service interface serving as DI token"
2961
+ },
2962
+ {
2963
+ value: "infrastructure",
2964
+ label: "Infrastructure",
2965
+ hint: "In-memory stub or concrete repository/client adapter"
2966
+ },
2967
+ {
2968
+ value: "httpHandler",
2969
+ label: "HTTP Handler",
2970
+ hint: "ExpressRoute endpoint with @MakeInjectable & Zod validation"
2971
+ },
2972
+ {
2973
+ value: "injectable",
2974
+ label: "Injectable Service",
2975
+ hint: "Generic @MakeInjectable class for services, transformers, or modules"
2976
+ }
2977
+ ]
2978
+ })
2979
+ );
2980
+ switch (choice) {
2981
+ case "feature":
2982
+ await runFeatureCommand(void 0, flags);
2983
+ break;
2984
+ case "usecase":
2985
+ await runUseCaseCommand(void 0, flags);
2986
+ break;
2987
+ case "domain":
2988
+ await runDomainCommand(void 0, flags);
2989
+ break;
2990
+ case "interface":
2991
+ await runInterfaceCommand(void 0, flags);
2992
+ break;
2993
+ case "infrastructure":
2994
+ await runInfrastructureCommand(void 0, flags);
2995
+ break;
2996
+ case "httpHandler":
2997
+ await runHttpHandlerCommand(void 0, flags);
2998
+ break;
2999
+ case "injectable":
3000
+ await runInjectableCommand(void 0, flags);
3001
+ break;
3002
+ }
3003
+ }
3004
+
3005
+ // src/commands/create.ts
3006
+ async function handleCreate(type, name, flags = {}) {
3007
+ if (!type) {
3008
+ await runInteractiveMenu(flags);
3009
+ return;
3010
+ }
3011
+ const normalizedType = type.trim();
3012
+ switch (normalizedType) {
3013
+ case "feature":
3014
+ await runFeatureCommand(name, flags);
3015
+ break;
3016
+ case "usecase":
3017
+ await runUseCaseCommand(name, flags);
3018
+ break;
3019
+ case "domain":
3020
+ await runDomainCommand(name, flags);
3021
+ break;
3022
+ case "infrastructure":
3023
+ await runInfrastructureCommand(name, flags);
3024
+ break;
3025
+ case "interface":
3026
+ await runInterfaceCommand(name, flags);
3027
+ break;
3028
+ case "httpHandler":
3029
+ case "httphandler":
3030
+ await runHttpHandlerCommand(name, flags);
3031
+ break;
3032
+ case "injectable":
3033
+ await runInjectableCommand(name, flags);
3034
+ break;
3035
+ default:
3036
+ p3.log.error(
3037
+ pc13.red(
3038
+ `Unknown component type: "${type}". Allowed types: feature, usecase, domain, infrastructure, interface, httpHandler, injectable.`
3039
+ )
3040
+ );
3041
+ process.exit(1);
3042
+ }
3043
+ }
3044
+ var DEFAULT_REGISTRY_BASE = "https://raw.githubusercontent.com/solid-stack-digital/mason-registry/main";
3045
+ function getRegistryBase(projectRoot, override) {
3046
+ if (override && override.trim()) {
3047
+ return override.trim();
3048
+ }
3049
+ if (process.env.MASON_REGISTRY_URL && process.env.MASON_REGISTRY_URL.trim()) {
3050
+ return process.env.MASON_REGISTRY_URL.trim();
3051
+ }
3052
+ if (projectRoot) {
3053
+ const configPath = path6.join(projectRoot, "mason.config.json");
3054
+ if (fs7.existsSync(configPath)) {
3055
+ try {
3056
+ const config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
3057
+ if (config.registry && typeof config.registry === "string") {
3058
+ return config.registry.trim();
3059
+ }
3060
+ } catch {
3061
+ }
3062
+ }
3063
+ }
3064
+ if (projectRoot) {
3065
+ const siblingRegistry = path6.resolve(projectRoot, "../mason-registry");
3066
+ if (fs7.existsSync(siblingRegistry) && fs7.existsSync(path6.join(siblingRegistry, "index.json"))) {
3067
+ return siblingRegistry;
3068
+ }
3069
+ }
3070
+ return DEFAULT_REGISTRY_BASE;
3071
+ }
3072
+ async function fetchRegistryFile(registryBase, relPath) {
3073
+ const cleanRelPath = relPath.replace(/^\/+/, "");
3074
+ if (registryBase.startsWith("http://") || registryBase.startsWith("https://")) {
3075
+ const url = `${registryBase.replace(/\/+$/, "")}/${cleanRelPath}`;
3076
+ const res = await fetch(url);
3077
+ if (!res.ok) {
3078
+ throw new Error(`Failed to fetch ${cleanRelPath} from ${url} (HTTP ${res.status}: ${res.statusText})`);
3079
+ }
3080
+ return await res.text();
3081
+ }
3082
+ let baseDir = registryBase;
3083
+ if (baseDir.startsWith("file://")) {
3084
+ baseDir = fileURLToPath(baseDir);
3085
+ }
3086
+ const filePath = path6.resolve(baseDir, cleanRelPath);
3087
+ if (!fs7.existsSync(filePath)) {
3088
+ throw new Error(`Registry file not found at ${filePath}`);
3089
+ }
3090
+ return fs7.promises.readFile(filePath, "utf-8");
3091
+ }
3092
+ async function fetchRegistryIndex(registryBase) {
3093
+ const content = await fetchRegistryFile(registryBase, "index.json");
3094
+ return JSON.parse(content);
3095
+ }
3096
+ async function fetchModuleManifest(registryBase, type, name) {
3097
+ const folder = type === "shared" ? "shared" : "features";
3098
+ const manifestPath = `${folder}/${name}/registry.json`;
3099
+ const content = await fetchRegistryFile(registryBase, manifestPath);
3100
+ return JSON.parse(content);
3101
+ }
3102
+ function levenshteinDistance(a, b) {
3103
+ const m = a.length;
3104
+ const n = b.length;
3105
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
3106
+ for (let i = 0; i <= m; i++) {
3107
+ dp[i][0] = i;
3108
+ }
3109
+ for (let j = 0; j <= n; j++) {
3110
+ dp[0][j] = j;
3111
+ }
3112
+ for (let i = 1; i <= m; i++) {
3113
+ for (let j = 1; j <= n; j++) {
3114
+ if (a[i - 1] === b[j - 1]) {
3115
+ dp[i][j] = dp[i - 1][j - 1];
3116
+ } else {
3117
+ dp[i][j] = Math.min(
3118
+ dp[i - 1][j] + 1,
3119
+ // deletion
3120
+ dp[i][j - 1] + 1,
3121
+ // insertion
3122
+ dp[i - 1][j - 1] + 1
3123
+ // substitution
3124
+ );
3125
+ }
3126
+ }
3127
+ }
3128
+ return dp[m][n];
3129
+ }
3130
+ function findClosestMatch(target, candidates) {
3131
+ if (candidates.length === 0) return null;
3132
+ const lower = target.toLowerCase();
3133
+ let closest = null;
3134
+ let minDistance = Infinity;
3135
+ for (const candidate of candidates) {
3136
+ const candLower = candidate.toLowerCase();
3137
+ if (candLower.includes(lower) || lower.includes(candLower)) {
3138
+ return candidate;
3139
+ }
3140
+ const dist = levenshteinDistance(lower, candLower);
3141
+ if (dist < minDistance) {
3142
+ minDistance = dist;
3143
+ closest = candidate;
3144
+ }
3145
+ }
3146
+ if (minDistance <= 3) {
3147
+ return closest;
3148
+ }
3149
+ return null;
3150
+ }
3151
+ function detectPackageManager(projectRoot) {
3152
+ if (fs7.existsSync(path6.join(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
3153
+ if (fs7.existsSync(path6.join(projectRoot, "yarn.lock"))) return "yarn";
3154
+ if (fs7.existsSync(path6.join(projectRoot, "bun.lockb")) || fs7.existsSync(path6.join(projectRoot, "bun.lock"))) return "bun";
3155
+ return "npm";
3156
+ }
3157
+ function getMissingNpmPackages(projectRoot, requiredPackages) {
3158
+ if (!requiredPackages || requiredPackages.length === 0) return [];
3159
+ const pkgJsonPath = path6.join(projectRoot, "package.json");
3160
+ if (!fs7.existsSync(pkgJsonPath)) {
3161
+ return requiredPackages;
3162
+ }
3163
+ try {
3164
+ const pkg = JSON.parse(fs7.readFileSync(pkgJsonPath, "utf-8"));
3165
+ const installed = /* @__PURE__ */ new Set([
3166
+ ...Object.keys(pkg.dependencies || {}),
3167
+ ...Object.keys(pkg.devDependencies || {}),
3168
+ ...Object.keys(pkg.peerDependencies || {})
3169
+ ]);
3170
+ return requiredPackages.filter((p15) => {
3171
+ const nameOnly = p15.startsWith("@") ? `@${p15.slice(1).split("@")[0] || ""}` : p15.split("@")[0] || p15;
3172
+ return !installed.has(nameOnly);
3173
+ });
3174
+ } catch {
3175
+ return requiredPackages;
3176
+ }
3177
+ }
3178
+ function installNpmPackages(projectRoot, packages, pm) {
3179
+ if (!packages || packages.length === 0) return;
3180
+ const manager = detectPackageManager(projectRoot);
3181
+ let installCmd;
3182
+ switch (manager) {
3183
+ case "pnpm":
3184
+ installCmd = `pnpm add ${packages.join(" ")}`;
3185
+ break;
3186
+ case "yarn":
3187
+ installCmd = `yarn add ${packages.join(" ")}`;
3188
+ break;
3189
+ case "bun":
3190
+ installCmd = `bun add ${packages.join(" ")}`;
3191
+ break;
3192
+ default:
3193
+ installCmd = `npm install ${packages.join(" ")}`;
3194
+ break;
3195
+ }
3196
+ p3.log.info(`Installing missing dependencies with ${manager}: ${pc13.cyan(packages.join(", "))}`);
3197
+ execSync(installCmd, {
3198
+ cwd: projectRoot,
3199
+ stdio: "inherit"
3200
+ });
3201
+ }
3202
+
3203
+ // src/commands/import.ts
3204
+ async function handleImport(typeArg, nameArg, flags = {}) {
3205
+ const projectRoot = findProjectRoot();
3206
+ let type = typeArg?.trim().toLowerCase();
3207
+ if (type === "features") type = "feature";
3208
+ if (!type) {
3209
+ type = handleCancel(
3210
+ await p3.select({
3211
+ message: "What kind of module would you like to import?",
3212
+ options: [
3213
+ {
3214
+ value: "shared",
3215
+ label: "Shared Module",
3216
+ hint: "Foundational utility or port placed in src/shared"
3217
+ },
3218
+ {
3219
+ value: "feature",
3220
+ label: "Feature",
3221
+ hint: "Complete Clean Architecture vertical slice placed in src/features"
3222
+ }
3223
+ ]
3224
+ })
3225
+ );
3226
+ }
3227
+ if (type !== "shared" && type !== "feature") {
3228
+ p3.log.error(pc13.red(`Invalid module type: "${typeArg}". Must be "shared" or "feature".`));
3229
+ process.exit(1);
3230
+ }
3231
+ const registryBase = getRegistryBase(projectRoot, flags.registry);
3232
+ const s = p3.spinner();
3233
+ s.start(`Connecting to registry at ${pc13.dim(registryBase)}...`);
3234
+ let index;
3235
+ try {
3236
+ index = await fetchRegistryIndex(registryBase);
3237
+ s.stop("Registry index loaded.");
3238
+ } catch (err) {
3239
+ s.stop(pc13.red("Failed to load registry index."));
3240
+ p3.log.error(pc13.red(err.message || String(err)));
3241
+ process.exit(1);
3242
+ }
3243
+ const availableList = type === "shared" ? index.shared : index.features;
3244
+ const candidateNames = availableList.map((m) => m.name);
3245
+ let name = nameArg?.trim();
3246
+ if (!name) {
3247
+ if (availableList.length === 0) {
3248
+ p3.log.warn(`No ${type} modules available in the registry.`);
3249
+ return;
3250
+ }
3251
+ name = handleCancel(
3252
+ await p3.select({
3253
+ message: `Select a ${type} to import:`,
3254
+ options: availableList.map((item) => ({
3255
+ value: item.name,
3256
+ label: item.name,
3257
+ hint: item.description
3258
+ }))
3259
+ })
3260
+ );
3261
+ }
3262
+ if (!candidateNames.includes(name)) {
3263
+ const closest = findClosestMatch(name, candidateNames);
3264
+ p3.log.error(pc13.red(`\u274C Module '${name}' not found in the ${type} registry.`));
3265
+ if (closest) {
3266
+ p3.log.info(pc13.yellow(`Did you mean '${pc13.cyan(closest)}'?`));
3267
+ }
3268
+ process.exit(1);
3269
+ }
3270
+ const importedModules = /* @__PURE__ */ new Set();
3271
+ const totalWrittenFiles = [];
3272
+ const requiredNpmPackages = /* @__PURE__ */ new Set();
3273
+ async function importModuleRecursive(modType, modName) {
3274
+ const modKey = `${modType}:${modName}`;
3275
+ if (importedModules.has(modKey)) return;
3276
+ p3.log.step(`Importing ${modType}: ${pc13.cyan(modName)}...`);
3277
+ let manifest;
3278
+ try {
3279
+ manifest = await fetchModuleManifest(registryBase, modType, modName);
3280
+ } catch (err) {
3281
+ p3.log.error(pc13.red(`Failed to fetch manifest for ${modType} '${modName}': ${err.message}`));
3282
+ process.exit(1);
3283
+ }
3284
+ const sharedDeps = manifest.dependencies?.shared || [];
3285
+ for (const dep of sharedDeps) {
3286
+ const depPath = path6.join(projectRoot, "src", "shared", dep);
3287
+ if (!fs7.existsSync(depPath) && !importedModules.has(`shared:${dep}`)) {
3288
+ p3.log.info(` \u21B3 Resolving required shared dependency: ${pc13.cyan(dep)}`);
3289
+ await importModuleRecursive("shared", dep);
3290
+ }
3291
+ }
3292
+ const featureDeps = manifest.dependencies?.features || [];
3293
+ for (const dep of featureDeps) {
3294
+ const depPath = path6.join(projectRoot, "src", "features", dep);
3295
+ if (!fs7.existsSync(depPath) && !importedModules.has(`feature:${dep}`)) {
3296
+ p3.log.info(` \u21B3 Resolving required feature dependency: ${pc13.cyan(dep)}`);
3297
+ await importModuleRecursive("feature", dep);
3298
+ }
3299
+ }
3300
+ for (const pkg of manifest.dependencies?.npm || []) {
3301
+ requiredNpmPackages.add(pkg);
3302
+ }
3303
+ const folder = modType === "shared" ? "shared" : "features";
3304
+ const targetDir = path6.join(projectRoot, "src", folder, modName);
3305
+ for (const file of manifest.files || []) {
3306
+ if (file === "registry.json") continue;
3307
+ const remoteFilePath = `${folder}/${modName}/${file}`;
3308
+ const localFilePath = path6.join(targetDir, file);
3309
+ if (fs7.existsSync(localFilePath) && !flags.overwrite) {
3310
+ p3.log.warn(` \u21B3 Skipping existing file: ${pc13.dim(path6.relative(projectRoot, localFilePath))} (use --overwrite to replace)`);
3311
+ continue;
3312
+ }
3313
+ const content = await fetchRegistryFile(registryBase, remoteFilePath);
3314
+ if (!flags.dryRun) {
3315
+ fs7.mkdirSync(path6.dirname(localFilePath), { recursive: true });
3316
+ fs7.writeFileSync(localFilePath, content, "utf-8");
3317
+ }
3318
+ const displayPath = path6.relative(projectRoot, localFilePath);
3319
+ totalWrittenFiles.push(displayPath);
3320
+ p3.log.message(` ${pc13.green("+")} ${displayPath}`);
3321
+ }
3322
+ importedModules.add(modKey);
3323
+ }
3324
+ await importModuleRecursive(type, name);
3325
+ const missingNpm = getMissingNpmPackages(projectRoot, Array.from(requiredNpmPackages));
3326
+ if (missingNpm.length > 0) {
3327
+ if (flags.skipInstall || flags.dryRun) {
3328
+ p3.log.info(`Required npm dependencies to install: ${pc13.cyan(missingNpm.join(", "))}`);
3329
+ } else {
3330
+ try {
3331
+ installNpmPackages(projectRoot, missingNpm);
3332
+ } catch (err) {
3333
+ p3.log.warn(`Could not automatically install packages (${err.message}). Please run: npm install ${missingNpm.join(" ")}`);
3334
+ }
3335
+ }
3336
+ }
3337
+ if (flags.dryRun) {
3338
+ p3.log.info(pc13.yellow("Dry run complete: No files were written to disk."));
3339
+ } else {
3340
+ p3.log.success(
3341
+ pc13.green(
3342
+ `Successfully imported ${type} '${name}' (${totalWrittenFiles.length} file${totalWrittenFiles.length === 1 ? "" : "s"} placed in src/${type === "shared" ? "shared" : "features"}/${name})`
3343
+ )
3344
+ );
3345
+ }
3346
+ }
3347
+
3348
+ // src/cli.ts
3349
+ function createCli() {
3350
+ const cli = cac("mason");
3351
+ cli.command("[...args]", "Interactive scaffolding and import wizard").option("-d, --dry-run", "Preview changes without writing to disk").option("-o, --overwrite", "Overwrite existing files").option("-y, --yes", "Accept defaults and skip interactive prompts").action(async (args, flags) => {
3352
+ if (args && args.length > 0) {
3353
+ p3.log.error(
3354
+ pc13.red(
3355
+ `Unknown command: "${args.join(" ")}". Use --help to list available commands.`
3356
+ )
3357
+ );
3358
+ process.exit(1);
3359
+ }
3360
+ try {
3361
+ const choice = handleCancel(
3362
+ await p3.select({
3363
+ message: "\u{1F9F1} Mason: What would you like to do?",
3364
+ options: [
3365
+ {
3366
+ value: "create",
3367
+ label: "Create local component",
3368
+ hint: "Scaffold a new feature, use case, domain entity, interface, etc."
3369
+ },
3370
+ {
3371
+ value: "import",
3372
+ label: "Import from registry",
3373
+ hint: "Download and link pre-built shared modules or features from the registry"
3374
+ }
3375
+ ]
3376
+ })
3377
+ );
3378
+ if (choice === "create") {
3379
+ await handleCreate(void 0, void 0, flags);
3380
+ } else {
3381
+ await handleImport(void 0, void 0, flags);
3382
+ }
3383
+ } catch (err) {
3384
+ handleCliError(err);
3385
+ }
3386
+ });
3387
+ cli.command(
3388
+ "create [type] [name]",
3389
+ "Scaffold local Clean Architecture components (feature, usecase, domain, etc.)"
3390
+ ).option("-d, --dry-run", "Preview changes without writing to disk").option("-o, --overwrite", "Overwrite existing files").option("-y, --yes", "Accept defaults and skip interactive prompts").option("-f, --feature <feature>", "Target feature name").option("-t, --target <dir>", "Override target directory").option("--express", "Include Express presentation layer (pxExpress)").option("--no-express", "Skip Express presentation layer").option("-i, --interface <interface>", "Interface or abstract class to implement").option("-k, --kind <kind>", "Implementation kind ('stub' or 'concrete')").option("-g, --global", "Place in global infrastructure/ instead of feature").option("-m, --method <method>", "HTTP method (get, post, put, patch, delete)").option("-p, --path <path>", "Route subpath (e.g. /:id)").option("-u, --usecase <usecase>", "Connected use case name").option("--message <message>", "Custom success message").option("--domainNames <names>", "Comma-separated domain entity names").example("mason create").example("mason create feature users").example("mason create usecase PlaceOrder -f orders").example("mason create domain User -f users").example("mason create infrastructure PostgresUserRepository -f users -i IUserRepository").example("mason create interface IUserRepository -f users").example("mason create httpHandler GetUserHttp -f users -m get -p /:id").example("mason create injectable PasswordHasher").action(async (type, name, flags = {}) => {
3391
+ try {
3392
+ await handleCreate(type, name, flags);
3393
+ } catch (err) {
3394
+ handleCliError(err);
3395
+ }
3396
+ });
3397
+ cli.command(
3398
+ "import [type] [name]",
3399
+ "Import pre-built shared modules or features from the Solid Stack Registry"
3400
+ ).option("-o, --overwrite", "Overwrite existing files").option("-d, --dry-run", "Preview changes without writing to disk").option("-y, --yes", "Accept defaults and skip interactive prompts").option("--registry <url>", "Registry base URL or local directory").option("--skip-install", "Skip automatic package manager dependency installation").example("mason import shared hasher").example("mason import feature authn").example("mason import shared").example("mason import feature").action(async (type, name, flags = {}) => {
3401
+ try {
3402
+ await handleImport(type, name, flags);
3403
+ } catch (err) {
3404
+ handleCliError(err);
3405
+ }
3406
+ });
3407
+ const createSubtypes = [
3408
+ "feature",
3409
+ "usecase",
3410
+ "domain",
3411
+ "infrastructure",
3412
+ "interface",
3413
+ "httpHandler",
3414
+ "injectable"
3415
+ ];
3416
+ for (const subtype of createSubtypes) {
3417
+ cli.command(
3418
+ `${subtype} [name]`,
3419
+ `Shorthand for 'mason create ${subtype} [name]'`
3420
+ ).allowUnknownOptions().action(async (name, flags = {}) => {
3421
+ try {
3422
+ await handleCreate(subtype, name, flags);
3423
+ } catch (err) {
3424
+ handleCliError(err);
3425
+ }
3426
+ });
3427
+ }
3428
+ cli.help();
3429
+ cli.version("1.0.10");
3430
+ return cli;
3431
+ }
3432
+ function handleCliError(err) {
3433
+ if (err instanceof PromptCancelledError) {
3434
+ process.exit(0);
3435
+ }
3436
+ const message = err instanceof Error ? err.message : String(err);
3437
+ p3.log.error(pc13.red(`Error: ${message}`));
3438
+ process.exit(1);
3439
+ }
3440
+ async function runCli(argv = process.argv) {
3441
+ const cli = createCli();
3442
+ cli.parse(argv, { run: true });
3443
+ }
3444
+
3445
+ // src/bin.ts
3446
+ runCli().catch((err) => {
3447
+ console.error(err);
3448
+ process.exit(1);
3449
+ });
3450
+ //# sourceMappingURL=bin.js.map
3451
+ //# sourceMappingURL=bin.js.map