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