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