@solid-stack/mason 1.0.12 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -1,17 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import { cac } from 'cac';
3
3
  import * as p3 from '@clack/prompts';
4
- import pc13 from 'picocolors';
5
- import path15 from 'path';
6
- import fs7 from 'fs';
4
+ import pc14 from 'picocolors';
5
+ import fs9 from 'fs';
6
+ import path2 from 'path';
7
7
  import { fileURLToPath } from 'url';
8
8
  import Handlebars from 'handlebars';
9
9
  import { execSync } from 'child_process';
10
+ import { createHash } from 'crypto';
10
11
 
11
12
  function writeFileSafely(absolutePath, content, options = {}) {
12
13
  const projectRoot = options.projectRoot || process.cwd();
13
- const relPath = path15.relative(projectRoot, absolutePath).replace(/\\/g, "/");
14
- const existedBefore = fs7.existsSync(absolutePath);
14
+ const relPath = path2.relative(projectRoot, absolutePath).replace(/\\/g, "/");
15
+ const existedBefore = fs9.existsSync(absolutePath);
15
16
  if (options.dryRun) {
16
17
  return {
17
18
  relativePath: relPath,
@@ -30,11 +31,11 @@ function writeFileSafely(absolutePath, content, options = {}) {
30
31
  existedBefore: true
31
32
  };
32
33
  }
33
- const dir = path15.dirname(absolutePath);
34
- if (!fs7.existsSync(dir)) {
35
- fs7.mkdirSync(dir, { recursive: true });
34
+ const dir = path2.dirname(absolutePath);
35
+ if (!fs9.existsSync(dir)) {
36
+ fs9.mkdirSync(dir, { recursive: true });
36
37
  }
37
- fs7.writeFileSync(absolutePath, content, "utf-8");
38
+ fs9.writeFileSync(absolutePath, content, "utf-8");
38
39
  return {
39
40
  relativePath: relPath,
40
41
  absolutePath,
@@ -46,17 +47,17 @@ function writeFileSafely(absolutePath, content, options = {}) {
46
47
  function printGeneratedFiles(files, isDryRun = false) {
47
48
  console.log("");
48
49
  if (isDryRun) {
49
- p3.log.warn(pc13.yellow(pc13.bold("[DRY RUN PREVIEW] No files were written to disk.")));
50
+ p3.log.warn(pc14.yellow(pc14.bold("[DRY RUN PREVIEW] No files were written to disk.")));
50
51
  }
51
52
  for (const f of files) {
52
53
  if (f.action === "created") {
53
- p3.log.success(`${pc13.bold(f.relativePath)} ${pc13.dim("(created)")}`);
54
+ p3.log.success(`${pc14.bold(f.relativePath)} ${pc14.dim("(created)")}`);
54
55
  } else if (f.action === "overwritten") {
55
- p3.log.warn(`${pc13.bold(f.relativePath)} ${pc13.dim("(overwritten)")}`);
56
+ p3.log.warn(`${pc14.bold(f.relativePath)} ${pc14.dim("(overwritten)")}`);
56
57
  } else if (f.action === "skipped") {
57
- p3.log.info(`${pc13.dim(f.relativePath)} ${pc13.dim("(skipped - already exists, pass -o to overwrite)")}`);
58
+ p3.log.info(`${pc14.dim(f.relativePath)} ${pc14.dim("(skipped - already exists, pass -o to overwrite)")}`);
58
59
  } else {
59
- p3.log.info(`${pc13.bold(f.relativePath)} ${pc13.dim("(preview)")}`);
60
+ p3.log.info(`${pc14.bold(f.relativePath)} ${pc14.dim("(preview)")}`);
60
61
  }
61
62
  }
62
63
  }
@@ -124,27 +125,90 @@ function toStubName(ifaceName) {
124
125
  }
125
126
  return `Stub${base}`;
126
127
  }
128
+ function loadMasonConfig(startDir = process.cwd()) {
129
+ let current = path2.resolve(startDir);
130
+ while (true) {
131
+ const configPath = path2.join(current, "mason.config.json");
132
+ if (fs9.existsSync(configPath)) {
133
+ try {
134
+ return JSON.parse(fs9.readFileSync(configPath, "utf-8"));
135
+ } catch {
136
+ return null;
137
+ }
138
+ }
139
+ const parent = path2.dirname(current);
140
+ if (parent === current) break;
141
+ current = parent;
142
+ }
143
+ return null;
144
+ }
145
+ function getEffectiveMode(projectRoot, flags) {
146
+ if (flags?.registry) return "registry";
147
+ const config = loadMasonConfig(projectRoot);
148
+ if (config?.mode) return config.mode;
149
+ if (fs9.existsSync(path2.join(projectRoot, "index.json")) && (fs9.existsSync(path2.join(projectRoot, "features")) || fs9.existsSync(path2.join(projectRoot, "shared"))) && !fs9.existsSync(path2.join(projectRoot, "src"))) {
150
+ return "registry";
151
+ }
152
+ return "app";
153
+ }
154
+ function getConfiguredFeaturesDir(projectRoot, flags) {
155
+ if (flags?.target) return path2.resolve(projectRoot, flags.target);
156
+ if (flags?.dest) return path2.resolve(projectRoot, flags.dest);
157
+ const config = loadMasonConfig(projectRoot);
158
+ if (config?.paths?.features) {
159
+ return path2.resolve(projectRoot, config.paths.features);
160
+ }
161
+ const mode = getEffectiveMode(projectRoot, flags);
162
+ if (mode === "registry") {
163
+ return path2.join(projectRoot, "features");
164
+ }
165
+ if (fs9.existsSync(path2.join(projectRoot, "src/features"))) {
166
+ return path2.join(projectRoot, "src/features");
167
+ }
168
+ return path2.join(projectRoot, "features");
169
+ }
170
+ function getConfiguredSharedDir(projectRoot, flags) {
171
+ if (flags?.target) return path2.resolve(projectRoot, flags.target);
172
+ if (flags?.dest) return path2.resolve(projectRoot, flags.dest);
173
+ const config = loadMasonConfig(projectRoot);
174
+ if (config?.paths?.shared) {
175
+ return path2.resolve(projectRoot, config.paths.shared);
176
+ }
177
+ const mode = getEffectiveMode(projectRoot, flags);
178
+ if (mode === "registry") {
179
+ return path2.join(projectRoot, "shared");
180
+ }
181
+ if (fs9.existsSync(path2.join(projectRoot, "src/shared"))) {
182
+ return path2.join(projectRoot, "src/shared");
183
+ }
184
+ return path2.join(projectRoot, "shared");
185
+ }
186
+
187
+ // src/utils/project.ts
127
188
  function findProjectRoot(startDir = process.cwd()) {
128
- let current = path15.resolve(startDir);
189
+ let current = path2.resolve(startDir);
129
190
  while (true) {
130
- if (fs7.existsSync(path15.join(current, "package.json"))) {
191
+ if (fs9.existsSync(path2.join(current, "package.json"))) {
131
192
  return current;
132
193
  }
133
- const parent = path15.dirname(current);
194
+ const parent = path2.dirname(current);
134
195
  if (parent === current) {
135
- return path15.resolve(startDir);
196
+ return path2.resolve(startDir);
136
197
  }
137
198
  current = parent;
138
199
  }
139
200
  }
140
- function resolveFeaturesDir(projectRoot) {
141
- return path15.join(projectRoot, "features");
201
+ function resolveFeaturesDir(projectRoot, flags) {
202
+ return getConfiguredFeaturesDir(projectRoot, flags);
203
+ }
204
+ function resolveSharedDir(projectRoot, flags) {
205
+ return getConfiguredSharedDir(projectRoot, flags);
142
206
  }
143
207
  function listExistingFeatures(projectRoot) {
144
208
  const featuresDir = resolveFeaturesDir(projectRoot);
145
- if (!fs7.existsSync(featuresDir)) return [];
209
+ if (!fs9.existsSync(featuresDir)) return [];
146
210
  try {
147
- const entries = fs7.readdirSync(featuresDir, { withFileTypes: true });
211
+ const entries = fs9.readdirSync(featuresDir, { withFileTypes: true });
148
212
  return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
149
213
  } catch {
150
214
  return [];
@@ -154,14 +218,14 @@ function listDomainInterfaces(projectRoot, featureName) {
154
218
  const results = [];
155
219
  const features = featureName ? [featureName] : listExistingFeatures(projectRoot);
156
220
  for (const feat of features) {
157
- const domainDir = path15.join(resolveFeaturesDir(projectRoot), feat, "domain");
158
- if (!fs7.existsSync(domainDir)) continue;
221
+ const domainDir = path2.join(resolveFeaturesDir(projectRoot), feat, "domain");
222
+ if (!fs9.existsSync(domainDir)) continue;
159
223
  try {
160
- const files = fs7.readdirSync(domainDir);
224
+ const files = fs9.readdirSync(domainDir);
161
225
  for (const file of files) {
162
226
  if (file.endsWith(".ts") && !file.endsWith(".test.ts") && !file.endsWith(".d.ts")) {
163
227
  const name = file.replace(/\.ts$/, "");
164
- const fullPath = path15.join(domainDir, file);
228
+ const fullPath = path2.join(domainDir, file);
165
229
  if (/^I[A-Z]/.test(name)) {
166
230
  results.push({
167
231
  name,
@@ -170,7 +234,7 @@ function listDomainInterfaces(projectRoot, featureName) {
170
234
  });
171
235
  } else {
172
236
  try {
173
- const content = fs7.readFileSync(fullPath, "utf-8");
237
+ const content = fs9.readFileSync(fullPath, "utf-8");
174
238
  if (/export\s+(?:abstract\s+class|interface)\s+(\w+)/.test(content)) {
175
239
  results.push({
176
240
  name,
@@ -192,17 +256,17 @@ function listDomainEntities(projectRoot, featureName) {
192
256
  const results = [];
193
257
  const features = featureName ? [featureName] : listExistingFeatures(projectRoot);
194
258
  for (const feat of features) {
195
- const domainDir = path15.join(resolveFeaturesDir(projectRoot), feat, "domain");
196
- if (!fs7.existsSync(domainDir)) continue;
259
+ const domainDir = path2.join(resolveFeaturesDir(projectRoot), feat, "domain");
260
+ if (!fs9.existsSync(domainDir)) continue;
197
261
  try {
198
- const files = fs7.readdirSync(domainDir);
262
+ const files = fs9.readdirSync(domainDir);
199
263
  for (const file of files) {
200
264
  if (file.endsWith(".ts") && !file.endsWith(".test.ts") && !file.endsWith(".d.ts")) {
201
265
  const name = file.replace(/\.ts$/, "");
202
266
  if (!/^I[A-Z]/.test(name)) {
203
267
  results.push({
204
268
  name,
205
- fullPath: path15.join(domainDir, file),
269
+ fullPath: path2.join(domainDir, file),
206
270
  feature: feat
207
271
  });
208
272
  }
@@ -217,16 +281,16 @@ function listUseCases(projectRoot, featureName) {
217
281
  const results = [];
218
282
  const features = featureName ? [featureName] : listExistingFeatures(projectRoot);
219
283
  for (const feat of features) {
220
- const useCasesDir = path15.join(resolveFeaturesDir(projectRoot), feat, "useCases");
221
- if (!fs7.existsSync(useCasesDir)) continue;
284
+ const useCasesDir = path2.join(resolveFeaturesDir(projectRoot), feat, "useCases");
285
+ if (!fs9.existsSync(useCasesDir)) continue;
222
286
  try {
223
- const files = fs7.readdirSync(useCasesDir);
287
+ const files = fs9.readdirSync(useCasesDir);
224
288
  for (const file of files) {
225
289
  if (file.endsWith(".ts") && !file.endsWith(".test.ts") && !file.endsWith(".d.ts")) {
226
290
  const name = file.replace(/\.ts$/, "");
227
291
  results.push({
228
292
  name,
229
- fullPath: path15.join(useCasesDir, file),
293
+ fullPath: path2.join(useCasesDir, file),
230
294
  feature: feat
231
295
  });
232
296
  }
@@ -252,33 +316,33 @@ hbs.registerHelper("join", (arr, sep) => {
252
316
  });
253
317
  var cachedTemplatesDir = null;
254
318
  function getTemplatesDir() {
255
- if (cachedTemplatesDir && fs7.existsSync(cachedTemplatesDir)) {
319
+ if (cachedTemplatesDir && fs9.existsSync(cachedTemplatesDir)) {
256
320
  return cachedTemplatesDir;
257
321
  }
258
- const currentDir = typeof __dirname !== "undefined" ? __dirname : path15.dirname(fileURLToPath(import.meta.url));
259
- const cand1 = path15.resolve(currentDir, "../../templates");
260
- if (fs7.existsSync(cand1)) {
322
+ const currentDir = typeof __dirname !== "undefined" ? __dirname : path2.dirname(fileURLToPath(import.meta.url));
323
+ const cand1 = path2.resolve(currentDir, "../../templates");
324
+ if (fs9.existsSync(cand1)) {
261
325
  cachedTemplatesDir = cand1;
262
326
  return cand1;
263
327
  }
264
- const cand2 = path15.resolve(currentDir, "../templates");
265
- if (fs7.existsSync(cand2)) {
328
+ const cand2 = path2.resolve(currentDir, "../templates");
329
+ if (fs9.existsSync(cand2)) {
266
330
  cachedTemplatesDir = cand2;
267
331
  return cand2;
268
332
  }
269
- const cand3 = path15.resolve(currentDir, "templates");
270
- if (fs7.existsSync(cand3)) {
333
+ const cand3 = path2.resolve(currentDir, "templates");
334
+ if (fs9.existsSync(cand3)) {
271
335
  cachedTemplatesDir = cand3;
272
336
  return cand3;
273
337
  }
274
338
  let curr = currentDir;
275
339
  while (true) {
276
- const candTemplates = path15.join(curr, "templates");
277
- if (fs7.existsSync(candTemplates)) {
340
+ const candTemplates = path2.join(curr, "templates");
341
+ if (fs9.existsSync(candTemplates)) {
278
342
  cachedTemplatesDir = candTemplates;
279
343
  return candTemplates;
280
344
  }
281
- const parent = path15.dirname(curr);
345
+ const parent = path2.dirname(curr);
282
346
  if (parent === curr) break;
283
347
  curr = parent;
284
348
  }
@@ -286,8 +350,8 @@ function getTemplatesDir() {
286
350
  }
287
351
  var templateCache = /* @__PURE__ */ new Map();
288
352
  function loadTemplate(relativePath) {
289
- const fullPath = path15.isAbsolute(relativePath) ? relativePath : path15.join(getTemplatesDir(), relativePath);
290
- return fs7.readFileSync(fullPath, "utf-8");
353
+ const fullPath = path2.isAbsolute(relativePath) ? relativePath : path2.join(getTemplatesDir(), relativePath);
354
+ return fs9.readFileSync(fullPath, "utf-8");
291
355
  }
292
356
  function renderTemplateFile(relativePath, context) {
293
357
  let compiled = templateCache.get(relativePath);
@@ -374,8 +438,8 @@ function generateDomainEntityCode(name, kind = "interface", properties = []) {
374
438
  function generateDomain(options) {
375
439
  const projectRoot = options.projectRoot || findProjectRoot();
376
440
  const entityName = toPascalCase(options.name);
377
- const targetDir = options.targetDir || path15.join(resolveFeaturesDir(projectRoot), options.feature, "domain");
378
- const filePath = path15.join(targetDir, `${entityName}.ts`);
441
+ const targetDir = options.targetDir || path2.join(resolveFeaturesDir(projectRoot), options.feature, "domain");
442
+ const filePath = path2.join(targetDir, `${entityName}.ts`);
379
443
  const code = generateDomainEntityCode(
380
444
  entityName,
381
445
  options.kind || "interface",
@@ -399,7 +463,7 @@ function generateInterfaceCode(ifaceName, kind = "abstractClass", entityName, me
399
463
  const idKey = `${entityKey}Id`;
400
464
  const listKey = `${entityKey}s`;
401
465
  const formattedCustomMethods = customMethods.map((m) => {
402
- const params = m.params && m.params.length > 0 ? m.params.map((p15) => `${p15.name}: ${p15.type}`).join("; ") : "";
466
+ const params = m.params && m.params.length > 0 ? m.params.map((p16) => `${p16.name}: ${p16.type}`).join("; ") : "";
403
467
  const returnType = m.returnType ? `Promise<${m.returnType}>` : "Promise<void>";
404
468
  return {
405
469
  name: m.name,
@@ -424,8 +488,8 @@ function generateInterfaceCode(ifaceName, kind = "abstractClass", entityName, me
424
488
  function generateInterface(options) {
425
489
  const projectRoot = options.projectRoot || findProjectRoot();
426
490
  const ifaceName = ensureInterfacePrefix(options.name);
427
- const targetDir = options.targetDir || path15.join(resolveFeaturesDir(projectRoot), options.feature, "domain");
428
- const filePath = path15.join(targetDir, `${ifaceName}.ts`);
491
+ const targetDir = options.targetDir || path2.join(resolveFeaturesDir(projectRoot), options.feature, "domain");
492
+ const filePath = path2.join(targetDir, `${ifaceName}.ts`);
429
493
  const code = generateInterfaceCode(
430
494
  ifaceName,
431
495
  options.kind || "abstractClass",
@@ -660,7 +724,7 @@ function parseInterfaceMethodsAndImports(content, targetIfaceName) {
660
724
  };
661
725
  }
662
726
  function findInterfaceFile(projectRoot, ifaceName, featureName, customPath) {
663
- if (customPath && fs7.existsSync(customPath)) {
727
+ if (customPath && fs9.existsSync(customPath)) {
664
728
  return { filePath: customPath, ifaceName };
665
729
  }
666
730
  const candidateNames = [
@@ -670,22 +734,22 @@ function findInterfaceFile(projectRoot, ifaceName, featureName, customPath) {
670
734
  ];
671
735
  const features = featureName ? [featureName, ...listExistingFeatures(projectRoot).filter((f) => f !== featureName)] : listExistingFeatures(projectRoot);
672
736
  for (const feat of features) {
673
- const domainDir = path15.join(resolveFeaturesDir(projectRoot), feat, "domain");
674
- if (!fs7.existsSync(domainDir)) continue;
737
+ const domainDir = path2.join(resolveFeaturesDir(projectRoot), feat, "domain");
738
+ if (!fs9.existsSync(domainDir)) continue;
675
739
  for (const cand of candidateNames) {
676
- const p1 = path15.join(domainDir, `${cand}.ts`);
677
- if (fs7.existsSync(p1)) {
740
+ const p1 = path2.join(domainDir, `${cand}.ts`);
741
+ if (fs9.existsSync(p1)) {
678
742
  return { filePath: p1, ifaceName: cand, feature: feat };
679
743
  }
680
744
  }
681
745
  }
682
746
  for (const cand of candidateNames) {
683
- const p1 = path15.join(projectRoot, "domain", `${cand}.ts`);
684
- if (fs7.existsSync(p1)) return { filePath: p1, ifaceName: cand };
685
- const p22 = path15.join(projectRoot, "src", "domain", `${cand}.ts`);
686
- if (fs7.existsSync(p22)) return { filePath: p22, ifaceName: cand };
687
- const p32 = path15.join(projectRoot, "interfaces", `${cand}.ts`);
688
- if (fs7.existsSync(p32)) return { filePath: p32, ifaceName: cand };
747
+ const p1 = path2.join(projectRoot, "domain", `${cand}.ts`);
748
+ if (fs9.existsSync(p1)) return { filePath: p1, ifaceName: cand };
749
+ const p22 = path2.join(projectRoot, "src", "domain", `${cand}.ts`);
750
+ if (fs9.existsSync(p22)) return { filePath: p22, ifaceName: cand };
751
+ const p32 = path2.join(projectRoot, "interfaces", `${cand}.ts`);
752
+ if (fs9.existsSync(p32)) return { filePath: p32, ifaceName: cand };
689
753
  }
690
754
  return null;
691
755
  }
@@ -708,7 +772,7 @@ function loadInterfaceHierarchy(projectRoot, initialIfaceName, featureName, cust
708
772
  while (currentFile && !visited.has(currentFile)) {
709
773
  visited.add(currentFile);
710
774
  try {
711
- const content = fs7.readFileSync(currentFile, "utf-8");
775
+ const content = fs9.readFileSync(currentFile, "utf-8");
712
776
  const parsed = parseInterfaceMethodsAndImports(content, currentName);
713
777
  for (const m of parsed.methods) {
714
778
  if (!allMethods.some((existing) => existing.name === m.name)) {
@@ -784,7 +848,7 @@ function generateStubCode(stubClassName, ifaceName, entityName, featureName, met
784
848
  const entitiesVar = `${entityVar}s`;
785
849
  const idKey = `${entityVar}Id`;
786
850
  const impPath = ifaceImportPath || `../domain/${ifaceName}.js`;
787
- const entityImportPath = ifaceImportPath ? `${path15.dirname(ifaceImportPath)}/${baseEntity}.js` : `../domain/${baseEntity}.js`;
851
+ const entityImportPath = ifaceImportPath ? `${path2.dirname(ifaceImportPath)}/${baseEntity}.js` : `../domain/${baseEntity}.js`;
788
852
  const importsByFrom = /* @__PURE__ */ new Map();
789
853
  for (const imp of domainImports) {
790
854
  if (imp.name === ifaceName || imp.name === baseEntity) continue;
@@ -833,7 +897,7 @@ function generateConcreteInfraCode(className, ifaceName, dependencies = [], meth
833
897
  if (baseEntity) {
834
898
  finalMethods = synthesizeCrudMethods(baseEntity);
835
899
  if (!finalDomainImports.some((imp) => imp.name === baseEntity)) {
836
- const importPath = ifaceImportPath ? `${path15.dirname(ifaceImportPath)}/${baseEntity}.js` : `../domain/${baseEntity}.js`;
900
+ const importPath = ifaceImportPath ? `${path2.dirname(ifaceImportPath)}/${baseEntity}.js` : `../domain/${baseEntity}.js`;
837
901
  finalDomainImports.push({
838
902
  name: baseEntity,
839
903
  from: importPath,
@@ -878,8 +942,8 @@ function generateInfrastructure(options) {
878
942
  const projectRoot = options.projectRoot || findProjectRoot();
879
943
  const className = toPascalCase(options.name);
880
944
  const isFeatureScope = options.scope !== "global" && Boolean(options.feature);
881
- const targetDir = options.targetDir || (isFeatureScope ? path15.join(resolveFeaturesDir(projectRoot), options.feature, "infrastructure") : path15.join(projectRoot, "infrastructure"));
882
- const filePath = path15.join(targetDir, `${className}.ts`);
945
+ const targetDir = options.targetDir || (isFeatureScope ? path2.join(resolveFeaturesDir(projectRoot), options.feature, "infrastructure") : path2.join(projectRoot, "infrastructure"));
946
+ const filePath = path2.join(targetDir, `${className}.ts`);
883
947
  let code;
884
948
  if (options.implementsInterface) {
885
949
  const rawIfaceName = options.implementsInterface;
@@ -893,7 +957,7 @@ function generateInfrastructure(options) {
893
957
  const ifaceFilePath = hierarchy.ifaceFilePath;
894
958
  let ifaceImportPath;
895
959
  if (ifaceFilePath) {
896
- let rel = path15.relative(targetDir, ifaceFilePath).replace(/\\/g, "/").replace(/\.ts$/, ".js");
960
+ let rel = path2.relative(targetDir, ifaceFilePath).replace(/\\/g, "/").replace(/\.ts$/, ".js");
897
961
  if (!rel.startsWith(".")) rel = "./" + rel;
898
962
  ifaceImportPath = rel;
899
963
  } else if (isFeatureScope) {
@@ -906,8 +970,8 @@ function generateInfrastructure(options) {
906
970
  if (new RegExp(`\\b${item.name}\\b`).test(allSignatures)) {
907
971
  let fromPath;
908
972
  if (imp.specifier.startsWith(".") && ifaceFilePath) {
909
- const absTarget = path15.resolve(path15.dirname(ifaceFilePath), imp.specifier);
910
- let rel = path15.relative(targetDir, absTarget).replace(/\\/g, "/");
973
+ const absTarget = path2.resolve(path2.dirname(ifaceFilePath), imp.specifier);
974
+ let rel = path2.relative(targetDir, absTarget).replace(/\\/g, "/");
911
975
  if (!rel.startsWith(".")) rel = "./" + rel;
912
976
  fromPath = rel;
913
977
  } else {
@@ -980,22 +1044,22 @@ function generateUseCaseCode(useCaseName, dependencies = [], inputProps = [], ou
980
1044
  from: /^I[A-Z]/.test(dep) ? `../domain/${dep}.js` : `./${dep}.js`
981
1045
  }));
982
1046
  const domainTypesToImport = new Set(domainImports);
983
- for (const p15 of [...inputProps, ...outputProps]) {
984
- const rawType = p15.type.trim();
1047
+ for (const p16 of [...inputProps, ...outputProps]) {
1048
+ const rawType = p16.type.trim();
985
1049
  const baseType = rawType.replace(/\[\]$/, "").trim();
986
1050
  if (!primitiveTypes.has(baseType) && /^[A-Z][A-Za-z0-9_]*$/.test(baseType) && !dependencies.includes(baseType)) {
987
1051
  domainTypesToImport.add(baseType);
988
1052
  }
989
1053
  }
990
- const formattedInputProps = inputProps.map((p15) => ({
991
- name: p15.name,
992
- optional: p15.optional,
993
- type: p15.optional && !p15.type.includes("undefined") ? `${p15.type} | undefined` : p15.type
1054
+ const formattedInputProps = inputProps.map((p16) => ({
1055
+ name: p16.name,
1056
+ optional: p16.optional,
1057
+ type: p16.optional && !p16.type.includes("undefined") ? `${p16.type} | undefined` : p16.type
994
1058
  }));
995
- const formattedOutputProps = outputProps.map((p15) => ({
996
- name: p15.name,
997
- optional: p15.optional,
998
- type: p15.optional && !p15.type.includes("undefined") ? `${p15.type} | undefined` : p15.type
1059
+ const formattedOutputProps = outputProps.map((p16) => ({
1060
+ name: p16.name,
1061
+ optional: p16.optional,
1062
+ type: p16.optional && !p16.type.includes("undefined") ? `${p16.type} | undefined` : p16.type
999
1063
  }));
1000
1064
  const formattedDeps = dependencies.map((dep) => ({
1001
1065
  key: toCamelCase(dep),
@@ -1094,8 +1158,8 @@ function generateUseCase(options) {
1094
1158
  const files = [];
1095
1159
  const projectRoot = options.projectRoot || findProjectRoot();
1096
1160
  const ucName = toPascalCase(options.name);
1097
- const targetDir = options.targetDir || path15.join(resolveFeaturesDir(projectRoot), options.feature, "useCases");
1098
- const filePath = path15.join(targetDir, `${ucName}.ts`);
1161
+ const targetDir = options.targetDir || path2.join(resolveFeaturesDir(projectRoot), options.feature, "useCases");
1162
+ const filePath = path2.join(targetDir, `${ucName}.ts`);
1099
1163
  const code = generateUseCaseCode(
1100
1164
  ucName,
1101
1165
  options.dependencies || [],
@@ -1111,7 +1175,7 @@ function generateUseCase(options) {
1111
1175
  })
1112
1176
  );
1113
1177
  if (options.includeTest) {
1114
- const testPath = path15.join(targetDir, `${ucName}.test.ts`);
1178
+ const testPath = path2.join(targetDir, `${ucName}.test.ts`);
1115
1179
  const testCode = generateUseCaseTestCode(ucName, options.dependencies || []);
1116
1180
  files.push(
1117
1181
  writeFileSafely(testPath, testCode, {
@@ -1179,10 +1243,10 @@ function generateHttpHandler(options) {
1179
1243
  const files = [];
1180
1244
  const projectRoot = options.projectRoot || findProjectRoot();
1181
1245
  const className = toPascalCase(options.name);
1182
- const pxDir = path15.join(resolveFeaturesDir(projectRoot), options.feature, "pxExpress");
1183
- const handlersDir = path15.join(pxDir, "handlers");
1184
- const pxIndexPath = path15.join(pxDir, "index.ts");
1185
- if (!fs7.existsSync(pxIndexPath)) {
1246
+ const pxDir = path2.join(resolveFeaturesDir(projectRoot), options.feature, "pxExpress");
1247
+ const handlersDir = path2.join(pxDir, "handlers");
1248
+ const pxIndexPath = path2.join(pxDir, "index.ts");
1249
+ if (!fs9.existsSync(pxIndexPath)) {
1186
1250
  const featurePath = options.routePrefix || `/${options.feature.toLowerCase()}`;
1187
1251
  const pxIndexContent = renderTemplate(expressIndexTemplate, {
1188
1252
  routePath: featurePath
@@ -1195,7 +1259,7 @@ function generateHttpHandler(options) {
1195
1259
  })
1196
1260
  );
1197
1261
  }
1198
- const handlerFilePath = path15.join(handlersDir, `${className}.ts`);
1262
+ const handlerFilePath = path2.join(handlersDir, `${className}.ts`);
1199
1263
  const code = generateHttpHandlerCode(
1200
1264
  className,
1201
1265
  options.method || "get",
@@ -1216,11 +1280,11 @@ function generateHttpHandler(options) {
1216
1280
  return files;
1217
1281
  }
1218
1282
  function updateOrGenerateDiProvider(featureDir, featureName, bindings = [], options = {}) {
1219
- const providerPath = path15.join(featureDir, "diProvider.ts");
1283
+ const providerPath = path2.join(featureDir, "diProvider.ts");
1220
1284
  const pascalFeature = toPascalCase(featureName);
1221
1285
  const providerModuleName = `${pascalFeature}Provider`;
1222
- if (fs7.existsSync(providerPath) && !options.overwrite) {
1223
- let content = fs7.readFileSync(providerPath, "utf-8");
1286
+ if (fs9.existsSync(providerPath) && !options.overwrite) {
1287
+ let content = fs9.readFileSync(providerPath, "utf-8");
1224
1288
  content = content.replace(/\n*export\s+const\s+ScaffoldProvider\s*=[^;]+;?/g, "");
1225
1289
  for (const b of bindings) {
1226
1290
  const ifaceImp = b.ifaceImportPath || `./domain/${b.ifaceName}.js`;
@@ -1279,7 +1343,7 @@ function generateFeature(options) {
1279
1343
  const projectRoot = options.projectRoot || findProjectRoot();
1280
1344
  const rawName = options.name.trim();
1281
1345
  const featureName = toCamelCase(rawName);
1282
- const targetDir = options.targetDir || path15.join(resolveFeaturesDir(projectRoot), featureName);
1346
+ const targetDir = options.targetDir || path2.join(resolveFeaturesDir(projectRoot), featureName);
1283
1347
  const routePath = options.routePath || `/${featureName}`;
1284
1348
  const rawEntities = [];
1285
1349
  if (options.entityNames && options.entityNames.length > 0) {
@@ -1300,7 +1364,7 @@ function generateFeature(options) {
1300
1364
  const domainFile = generateDomain({
1301
1365
  name: ent,
1302
1366
  feature: featureName,
1303
- targetDir: path15.join(targetDir, "domain"),
1367
+ targetDir: path2.join(targetDir, "domain"),
1304
1368
  projectRoot,
1305
1369
  dryRun: options.dryRun,
1306
1370
  overwrite: options.overwrite,
@@ -1320,7 +1384,7 @@ function generateFeature(options) {
1320
1384
  const ifaceFile = generateInterface({
1321
1385
  name: ifaceName,
1322
1386
  feature: featureName,
1323
- targetDir: path15.join(targetDir, "domain"),
1387
+ targetDir: path2.join(targetDir, "domain"),
1324
1388
  entityName: ent,
1325
1389
  projectRoot,
1326
1390
  dryRun: options.dryRun,
@@ -1337,7 +1401,7 @@ function generateFeature(options) {
1337
1401
  const stubFile = generateInfrastructure({
1338
1402
  name: stubName,
1339
1403
  feature: featureName,
1340
- targetDir: path15.join(targetDir, "infrastructure"),
1404
+ targetDir: path2.join(targetDir, "infrastructure"),
1341
1405
  kind: "stub",
1342
1406
  implementsInterface: ifaceName,
1343
1407
  entityName: ent,
@@ -1379,7 +1443,7 @@ function generateFeature(options) {
1379
1443
  const ucFiles = generateUseCase({
1380
1444
  name: ucName,
1381
1445
  feature: featureName,
1382
- targetDir: path15.join(targetDir, "useCases"),
1446
+ targetDir: path2.join(targetDir, "useCases"),
1383
1447
  dependencies: options.includeInterface !== false ? [ifaceName] : [],
1384
1448
  inputProperties: [{ name: idParam, type: "string" }],
1385
1449
  outputProperties: [{ name: entVar, type: primaryEntity }],
@@ -1424,7 +1488,7 @@ function generateFeature(options) {
1424
1488
  const ucFiles = generateUseCase({
1425
1489
  name: ucName,
1426
1490
  feature: featureName,
1427
- targetDir: path15.join(targetDir, "useCases"),
1491
+ targetDir: path2.join(targetDir, "useCases"),
1428
1492
  dependencies: deps,
1429
1493
  inputProperties: [{ name: entVar, type: ent }],
1430
1494
  outputProperties: [{ name: entVar, type: ent }],
@@ -1457,7 +1521,7 @@ function generateFeature(options) {
1457
1521
  const ucFiles = generateUseCase({
1458
1522
  name: ucName,
1459
1523
  feature: featureName,
1460
- targetDir: path15.join(targetDir, "useCases"),
1524
+ targetDir: path2.join(targetDir, "useCases"),
1461
1525
  dependencies: deps,
1462
1526
  inputProperties: [{ name: idParam, type: "string" }],
1463
1527
  outputProperties: [{ name: entVar, type: ent }],
@@ -1490,7 +1554,7 @@ function generateFeature(options) {
1490
1554
  const ucFiles = generateUseCase({
1491
1555
  name: ucName,
1492
1556
  feature: featureName,
1493
- targetDir: path15.join(targetDir, "useCases"),
1557
+ targetDir: path2.join(targetDir, "useCases"),
1494
1558
  dependencies: deps,
1495
1559
  inputProperties: [
1496
1560
  { name: "page", type: "number", optional: true },
@@ -1534,7 +1598,7 @@ function generateFeature(options) {
1534
1598
  const ucFiles = generateUseCase({
1535
1599
  name: ucName,
1536
1600
  feature: featureName,
1537
- targetDir: path15.join(targetDir, "useCases"),
1601
+ targetDir: path2.join(targetDir, "useCases"),
1538
1602
  dependencies: deps,
1539
1603
  inputProperties: [{ name: entVar, type: ent }],
1540
1604
  outputProperties: [{ name: entVar, type: ent }],
@@ -1567,7 +1631,7 @@ function generateFeature(options) {
1567
1631
  const ucFiles = generateUseCase({
1568
1632
  name: ucName,
1569
1633
  feature: featureName,
1570
- targetDir: path15.join(targetDir, "useCases"),
1634
+ targetDir: path2.join(targetDir, "useCases"),
1571
1635
  dependencies: deps,
1572
1636
  inputProperties: [{ name: idParam, type: "string" }],
1573
1637
  outputProperties: [{ name: "success", type: "boolean" }],
@@ -1604,7 +1668,7 @@ function generateFeature(options) {
1604
1668
  const ucFiles = generateUseCase({
1605
1669
  name: ucName,
1606
1670
  feature: featureName,
1607
- targetDir: path15.join(targetDir, "useCases"),
1671
+ targetDir: path2.join(targetDir, "useCases"),
1608
1672
  dependencies: customUc.dependencies || [],
1609
1673
  inputProperties: customUc.inputProperties || [],
1610
1674
  outputProperties: customUc.outputProperties || [],
@@ -1636,8 +1700,8 @@ function generateFeature(options) {
1636
1700
  }
1637
1701
  }
1638
1702
  if (options.includeExpress !== false) {
1639
- const pxDir = path15.join(targetDir, "pxExpress");
1640
- const pxIndexPath = path15.join(pxDir, "index.ts");
1703
+ const pxDir = path2.join(targetDir, "pxExpress");
1704
+ const pxIndexPath = path2.join(pxDir, "index.ts");
1641
1705
  if (!files.some((f) => f.absolutePath === pxIndexPath)) {
1642
1706
  const pxIndexContent = renderTemplate(expressIndexTemplate, { routePath });
1643
1707
  files.push(
@@ -1674,13 +1738,13 @@ function handleCancel(result) {
1674
1738
  }
1675
1739
  function printBanner() {
1676
1740
  console.log("");
1677
- p3.intro(pc13.bgCyan(pc13.black(" @solid-stack/mason ")));
1741
+ p3.intro(pc14.bgCyan(pc14.black(" @solid-stack/mason ")));
1678
1742
  }
1679
1743
  async function promptProperties(options = {}) {
1680
1744
  const properties = options.defaults ? [...options.defaults] : [];
1681
1745
  if (properties.length > 0) {
1682
- const summary = properties.map((p15) => `${p15.name}: ${p15.type}${p15.optional ? "?" : ""}`).join(", ");
1683
- p3.log.info(pc13.dim(`Pre-configured fields: ${summary}`));
1746
+ const summary = properties.map((p16) => `${p16.name}: ${p16.type}${p16.optional ? "?" : ""}`).join(", ");
1747
+ p3.log.info(pc14.dim(`Pre-configured fields: ${summary}`));
1684
1748
  const addMore = handleCancel(
1685
1749
  await p3.confirm({
1686
1750
  message: "Would you like to add custom fields/properties?",
@@ -1691,7 +1755,7 @@ async function promptProperties(options = {}) {
1691
1755
  return properties;
1692
1756
  }
1693
1757
  }
1694
- p3.log.info(pc13.cyan("Enter properties (leave name blank when finished):"));
1758
+ p3.log.info(pc14.cyan("Enter properties (leave name blank when finished):"));
1695
1759
  while (true) {
1696
1760
  const name = handleCancel(
1697
1761
  await p3.text({
@@ -1739,7 +1803,7 @@ async function promptProperties(options = {}) {
1739
1803
  optional
1740
1804
  });
1741
1805
  p3.log.success(
1742
- pc13.green(`Added: ${name.trim()}${optional ? "?" : ""}: ${finalType.trim()}`)
1806
+ pc14.green(`Added: ${name.trim()}${optional ? "?" : ""}: ${finalType.trim()}`)
1743
1807
  );
1744
1808
  }
1745
1809
  return properties;
@@ -1748,6 +1812,7 @@ async function promptProperties(options = {}) {
1748
1812
  // src/commands/feature.ts
1749
1813
  async function runFeatureCommand(featureNameArg, flags = {}) {
1750
1814
  const projectRoot = findProjectRoot();
1815
+ const isRegistry = getEffectiveMode(projectRoot, flags) === "registry";
1751
1816
  let name = featureNameArg?.trim();
1752
1817
  if (!name && flags.yes) {
1753
1818
  name = "feature";
@@ -1765,9 +1830,10 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
1765
1830
  ).trim();
1766
1831
  }
1767
1832
  const normalizedName = toCamelCase(name);
1768
- const defaultDir = path15.relative(projectRoot, path15.join(resolveFeaturesDir(projectRoot), normalizedName));
1833
+ const baseFeaturesDir = resolveFeaturesDir(projectRoot, flags);
1834
+ const defaultDir = path2.relative(projectRoot, path2.join(baseFeaturesDir, normalizedName));
1769
1835
  let targetDirRel = defaultDir;
1770
- if (!flags.yes && !flags.target) {
1836
+ if (!flags.yes && !flags.target && !flags.dest) {
1771
1837
  targetDirRel = handleCancel(
1772
1838
  await p3.text({
1773
1839
  message: "Target directory for the feature:",
@@ -1776,7 +1842,7 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
1776
1842
  })
1777
1843
  ).trim();
1778
1844
  }
1779
- const targetDir = flags.target ? path15.resolve(projectRoot, flags.target) : path15.resolve(projectRoot, targetDirRel);
1845
+ const targetDir = flags.dest ? path2.resolve(projectRoot, flags.dest) : flags.target ? path2.resolve(projectRoot, flags.target) : path2.resolve(projectRoot, targetDirRel);
1780
1846
  let includeExpress = flags.express !== void 0 ? Boolean(flags.express) : true;
1781
1847
  let routePath = `/${normalizedName}`;
1782
1848
  let includeDomain = true;
@@ -1997,7 +2063,7 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
1997
2063
  }
1998
2064
  }
1999
2065
  const s = p3.spinner();
2000
- s.start(`Generating feature '${pc13.cyan(normalizedName)}'...`);
2066
+ s.start(`Generating feature '${pc14.cyan(normalizedName)}'...`);
2001
2067
  const options = {
2002
2068
  name: normalizedName,
2003
2069
  projectRoot,
@@ -2017,9 +2083,207 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
2017
2083
  includeDiProvider
2018
2084
  };
2019
2085
  const files = generateFeature(options);
2086
+ if (isRegistry) {
2087
+ const registryPath = path2.join(targetDir, "registry.json");
2088
+ if (!fs9.existsSync(registryPath)) {
2089
+ const manifest = {
2090
+ name: toKebabCase(normalizedName),
2091
+ type: "feature",
2092
+ description: flags.description || `${normalizedName} feature module`,
2093
+ version: "1.0.0",
2094
+ dependencies: {
2095
+ npm: includeExpress ? ["@solid-stack/agnos-express", "express"] : [],
2096
+ shared: [],
2097
+ features: []
2098
+ },
2099
+ files: []
2100
+ };
2101
+ const content = JSON.stringify(manifest, null, 2) + "\n";
2102
+ const genFile = writeFileSafely(registryPath, content, {
2103
+ projectRoot,
2104
+ overwrite: options.overwrite,
2105
+ dryRun: options.dryRun
2106
+ });
2107
+ files.push(genFile);
2108
+ }
2109
+ }
2020
2110
  s.stop("Feature scaffolded successfully!");
2021
2111
  printGeneratedFiles(files, options.dryRun);
2022
- p3.outro(pc13.green(`Feature '${normalizedName}' ready to use!`));
2112
+ p3.outro(
2113
+ pc14.green(
2114
+ `\u2713 Feature '${normalizedName}' initialized in ${path2.relative(projectRoot, targetDir)} (uncommitted, unregistered)`
2115
+ )
2116
+ );
2117
+ }
2118
+ function generateShared(options) {
2119
+ const files = [];
2120
+ const projectRoot = options.projectRoot || findProjectRoot();
2121
+ const rawName = options.name.trim();
2122
+ const pascalName = toPascalCase(rawName);
2123
+ const camelName = toCamelCase(rawName);
2124
+ const kebabName = toKebabCase(rawName);
2125
+ const targetDir = options.targetDir || path2.join(resolveSharedDir(projectRoot), kebabName);
2126
+ const fileDefs = [];
2127
+ const facadeContent = `import { MakeInjectable, type DepsType } from "@solid-stack/di";
2128
+ import { I${pascalName}Engine } from "./ports/I${pascalName}Engine.js";
2129
+
2130
+ @MakeInjectable
2131
+ export class ${pascalName} {
2132
+ public static deps = { ${camelName}Engine: I${pascalName}Engine };
2133
+ constructor(public readonly deps: DepsType<typeof ${pascalName}.deps>) {}
2134
+ }
2135
+ `;
2136
+ fileDefs.push({
2137
+ absolutePath: path2.join(targetDir, `${pascalName}.ts`),
2138
+ content: facadeContent
2139
+ });
2140
+ const portContent = `export abstract class I${pascalName}Engine {
2141
+ // Define abstract port contracts for ${pascalName} here
2142
+ }
2143
+ `;
2144
+ fileDefs.push({
2145
+ absolutePath: path2.join(targetDir, "ports", `I${pascalName}Engine.ts`),
2146
+ content: portContent
2147
+ });
2148
+ const concreteContent = `import { MakeInjectable } from "@solid-stack/di";
2149
+ import { I${pascalName}Engine } from "../ports/I${pascalName}Engine.js";
2150
+
2151
+ @MakeInjectable
2152
+ export class Default${pascalName}Engine implements I${pascalName}Engine {
2153
+ // Default concrete implementation for production
2154
+ }
2155
+ `;
2156
+ fileDefs.push({
2157
+ absolutePath: path2.join(targetDir, "infrastructure", `Default${pascalName}Engine.ts`),
2158
+ content: concreteContent
2159
+ });
2160
+ if (options.includeStub !== false) {
2161
+ const stubContent = `import { MakeInjectable } from "@solid-stack/di";
2162
+ import { I${pascalName}Engine } from "../ports/I${pascalName}Engine.js";
2163
+
2164
+ @MakeInjectable
2165
+ export class Stub${pascalName}Engine implements I${pascalName}Engine {
2166
+ // In-memory or stub implementation for testing
2167
+ }
2168
+ `;
2169
+ fileDefs.push({
2170
+ absolutePath: path2.join(targetDir, "infrastructure", `Stub${pascalName}Engine.ts`),
2171
+ content: stubContent
2172
+ });
2173
+ }
2174
+ const providerContent = `import type { Container } from "@solid-stack/di";
2175
+ import { I${pascalName}Engine } from "./ports/I${pascalName}Engine.js";
2176
+ import { Default${pascalName}Engine } from "./infrastructure/Default${pascalName}Engine.js";
2177
+
2178
+ export const diProvider = (c: Container): void => {
2179
+ c.provide(I${pascalName}Engine, Default${pascalName}Engine);
2180
+ };
2181
+
2182
+ export default diProvider;
2183
+ `;
2184
+ fileDefs.push({
2185
+ absolutePath: path2.join(targetDir, "diProvider.ts"),
2186
+ content: providerContent
2187
+ });
2188
+ if (options.isRegistry) {
2189
+ const registryManifest = {
2190
+ name: kebabName,
2191
+ type: "shared",
2192
+ description: options.description || `${kebabName} shared module`,
2193
+ version: "1.0.0",
2194
+ dependencies: {
2195
+ npm: [],
2196
+ shared: [],
2197
+ features: []
2198
+ },
2199
+ files: []
2200
+ };
2201
+ fileDefs.push({
2202
+ absolutePath: path2.join(targetDir, "registry.json"),
2203
+ content: JSON.stringify(registryManifest, null, 2) + "\n"
2204
+ });
2205
+ }
2206
+ for (const def of fileDefs) {
2207
+ const genFile = writeFileSafely(def.absolutePath, def.content, {
2208
+ projectRoot,
2209
+ overwrite: options.overwrite,
2210
+ dryRun: options.dryRun
2211
+ });
2212
+ files.push(genFile);
2213
+ }
2214
+ return files;
2215
+ }
2216
+
2217
+ // src/commands/shared.ts
2218
+ async function runSharedCommand(sharedNameArg, flags = {}) {
2219
+ const projectRoot = findProjectRoot();
2220
+ const isRegistry = getEffectiveMode(projectRoot, flags) === "registry";
2221
+ let name = sharedNameArg?.trim();
2222
+ if (!name && flags.yes) {
2223
+ name = "shared";
2224
+ }
2225
+ if (!name) {
2226
+ name = handleCancel(
2227
+ await p3.text({
2228
+ message: "What is the shared module name?",
2229
+ placeholder: "e.g. hasher, time, uuid, cache, logger",
2230
+ validate: (val) => {
2231
+ if (!val || !val.trim()) return "Shared module name cannot be empty.";
2232
+ return void 0;
2233
+ }
2234
+ })
2235
+ ).trim();
2236
+ }
2237
+ const normalizedName = toKebabCase(name);
2238
+ const baseSharedDir = resolveSharedDir(projectRoot, flags);
2239
+ const defaultDir = path2.relative(projectRoot, path2.join(baseSharedDir, normalizedName));
2240
+ let targetDirRel = defaultDir;
2241
+ if (!flags.yes && !flags.target && !flags.dest) {
2242
+ targetDirRel = handleCancel(
2243
+ await p3.text({
2244
+ message: "Target directory for the shared module:",
2245
+ initialValue: defaultDir,
2246
+ defaultValue: defaultDir
2247
+ })
2248
+ ).trim();
2249
+ }
2250
+ const targetDir = flags.dest ? path2.resolve(projectRoot, flags.dest) : flags.target ? path2.resolve(projectRoot, flags.target) : path2.resolve(projectRoot, targetDirRel);
2251
+ let description = flags.description || "";
2252
+ let includeStub = true;
2253
+ if (!flags.yes) {
2254
+ description = handleCancel(
2255
+ await p3.text({
2256
+ message: "Module description (optional):",
2257
+ placeholder: "e.g. In-memory caching and Redis port abstraction",
2258
+ initialValue: ""
2259
+ })
2260
+ ).trim();
2261
+ includeStub = handleCancel(
2262
+ await p3.confirm({
2263
+ message: "Include stub implementation in infrastructure/?",
2264
+ initialValue: true
2265
+ })
2266
+ );
2267
+ }
2268
+ const s = p3.spinner();
2269
+ s.start(`Generating shared module '${pc14.cyan(normalizedName)}'...`);
2270
+ const files = generateShared({
2271
+ name: normalizedName,
2272
+ description,
2273
+ projectRoot,
2274
+ targetDir,
2275
+ dryRun: Boolean(flags.dryRun),
2276
+ overwrite: Boolean(flags.overwrite),
2277
+ includeStub,
2278
+ isRegistry
2279
+ });
2280
+ s.stop("Shared module generated successfully!");
2281
+ printGeneratedFiles(files, Boolean(flags.dryRun));
2282
+ p3.outro(
2283
+ pc14.green(
2284
+ `\u2713 Shared module '${normalizedName}' initialized in ${path2.relative(projectRoot, targetDir)} (uncommitted, unregistered)`
2285
+ )
2286
+ );
2023
2287
  }
2024
2288
  async function runUseCaseCommand(useCaseNameArg, flags = {}) {
2025
2289
  const projectRoot = findProjectRoot();
@@ -2144,7 +2408,7 @@ async function runUseCaseCommand(useCaseNameArg, flags = {}) {
2144
2408
  }
2145
2409
  }
2146
2410
  const s = p3.spinner();
2147
- s.start(`Generating use case '${pc13.cyan(ucName)}'...`);
2411
+ s.start(`Generating use case '${pc14.cyan(ucName)}'...`);
2148
2412
  const options = {
2149
2413
  name: ucName,
2150
2414
  feature,
@@ -2176,7 +2440,7 @@ async function runUseCaseCommand(useCaseNameArg, flags = {}) {
2176
2440
  }
2177
2441
  s.stop("Use case generated successfully!");
2178
2442
  printGeneratedFiles(files, options.dryRun);
2179
- p3.outro(pc13.green(`Use case '${ucName}' created in feature '${feature}'!`));
2443
+ p3.outro(pc14.green(`Use case '${ucName}' created in feature '${feature}'!`));
2180
2444
  }
2181
2445
  async function runDomainCommand(domainNameArg, flags = {}) {
2182
2446
  const projectRoot = findProjectRoot();
@@ -2247,7 +2511,7 @@ async function runDomainCommand(domainNameArg, flags = {}) {
2247
2511
  });
2248
2512
  }
2249
2513
  const s = p3.spinner();
2250
- s.start(`Generating domain model '${pc13.cyan(entityName)}'...`);
2514
+ s.start(`Generating domain model '${pc14.cyan(entityName)}'...`);
2251
2515
  const options = {
2252
2516
  name: entityName,
2253
2517
  feature,
@@ -2260,7 +2524,7 @@ async function runDomainCommand(domainNameArg, flags = {}) {
2260
2524
  const file = generateDomain(options);
2261
2525
  s.stop("Domain model generated successfully!");
2262
2526
  printGeneratedFiles([file], options.dryRun);
2263
- p3.outro(pc13.green(`Domain model '${entityName}' created in feature '${feature}'!`));
2527
+ p3.outro(pc14.green(`Domain model '${entityName}' created in feature '${feature}'!`));
2264
2528
  }
2265
2529
  async function runInfrastructureCommand(classNameArg, flags = {}) {
2266
2530
  const projectRoot = findProjectRoot();
@@ -2429,7 +2693,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
2429
2693
  registerInProvider = !flags.noProvider;
2430
2694
  }
2431
2695
  const s = p3.spinner();
2432
- s.start(`Generating infrastructure class '${pc13.cyan(className)}'...`);
2696
+ s.start(`Generating infrastructure class '${pc14.cyan(className)}'...`);
2433
2697
  const files = [];
2434
2698
  const options = {
2435
2699
  name: className,
@@ -2444,7 +2708,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
2444
2708
  const infraFile = generateInfrastructure(options);
2445
2709
  files.push(infraFile);
2446
2710
  if (registerInProvider && scope === "feature" && feature && implementsInterface) {
2447
- const featureDir = path15.join(resolveFeaturesDir(projectRoot), feature);
2711
+ const featureDir = path2.join(resolveFeaturesDir(projectRoot), feature);
2448
2712
  const providerFile = updateOrGenerateDiProvider(
2449
2713
  featureDir,
2450
2714
  feature,
@@ -2464,7 +2728,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
2464
2728
  }
2465
2729
  s.stop("Infrastructure class generated successfully!");
2466
2730
  printGeneratedFiles(files, options.dryRun);
2467
- p3.outro(pc13.green(`Infrastructure class '${className}' created!`));
2731
+ p3.outro(pc14.green(`Infrastructure class '${className}' created!`));
2468
2732
  }
2469
2733
  async function runInterfaceCommand(interfaceNameArg, flags = {}) {
2470
2734
  const projectRoot = findProjectRoot();
@@ -2577,7 +2841,7 @@ async function runInterfaceCommand(interfaceNameArg, flags = {}) {
2577
2841
  }
2578
2842
  }
2579
2843
  const s = p3.spinner();
2580
- s.start(`Generating interface '${pc13.cyan(ifaceName)}'...`);
2844
+ s.start(`Generating interface '${pc14.cyan(ifaceName)}'...`);
2581
2845
  const files = [];
2582
2846
  const options = {
2583
2847
  name: ifaceName,
@@ -2606,7 +2870,7 @@ async function runInterfaceCommand(interfaceNameArg, flags = {}) {
2606
2870
  files.push(stubFile);
2607
2871
  }
2608
2872
  if (registerInProvider && generateStub) {
2609
- const featureDir = path15.join(resolveFeaturesDir(projectRoot), feature);
2873
+ const featureDir = path2.join(resolveFeaturesDir(projectRoot), feature);
2610
2874
  const providerFile = updateOrGenerateDiProvider(
2611
2875
  featureDir,
2612
2876
  feature,
@@ -2626,7 +2890,7 @@ async function runInterfaceCommand(interfaceNameArg, flags = {}) {
2626
2890
  }
2627
2891
  s.stop("Interface generated successfully!");
2628
2892
  printGeneratedFiles(files, options.dryRun);
2629
- p3.outro(pc13.green(`Interface '${ifaceName}' created in feature '${feature}'!`));
2893
+ p3.outro(pc14.green(`Interface '${ifaceName}' created in feature '${feature}'!`));
2630
2894
  }
2631
2895
  async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
2632
2896
  const projectRoot = findProjectRoot();
@@ -2743,7 +3007,7 @@ async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
2743
3007
  }
2744
3008
  }
2745
3009
  const s = p3.spinner();
2746
- s.start(`Generating HTTP handler '${pc13.cyan(className)}'...`);
3010
+ s.start(`Generating HTTP handler '${pc14.cyan(className)}'...`);
2747
3011
  const options = {
2748
3012
  name: className,
2749
3013
  feature,
@@ -2761,7 +3025,7 @@ async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
2761
3025
  const files = generateHttpHandler(options);
2762
3026
  s.stop("HTTP handler generated successfully!");
2763
3027
  printGeneratedFiles(files, options.dryRun);
2764
- p3.outro(pc13.green(`HTTP handler '${className}' created in feature '${feature}'!`));
3028
+ p3.outro(pc14.green(`HTTP handler '${className}' created in feature '${feature}'!`));
2765
3029
  }
2766
3030
  function generateInjectableCode(className, dependencies = [], methods = [], implementsInterface) {
2767
3031
  const formattedDeps = dependencies.map((dep) => ({
@@ -2771,7 +3035,7 @@ function generateInjectableCode(className, dependencies = [], methods = [], impl
2771
3035
  const formattedMethods = methods.map((m) => {
2772
3036
  const isAsync = m.async !== false;
2773
3037
  const retType = m.returnType ? isAsync ? `Promise<${m.returnType}>` : m.returnType : isAsync ? "Promise<void>" : "void";
2774
- const params = m.params && m.params.length > 0 ? m.params.map((p15) => `${p15.name}: ${p15.type}`).join(", ") : "";
3038
+ const params = m.params && m.params.length > 0 ? m.params.map((p16) => `${p16.name}: ${p16.type}`).join(", ") : "";
2775
3039
  return {
2776
3040
  name: m.name,
2777
3041
  isAsync,
@@ -2794,21 +3058,21 @@ function generateInjectable(options) {
2794
3058
  const className = toPascalCase(options.name);
2795
3059
  let targetDir = "";
2796
3060
  if (options.customPath) {
2797
- targetDir = path15.isAbsolute(options.customPath) ? options.customPath : path15.join(projectRoot, options.customPath);
3061
+ targetDir = path2.isAbsolute(options.customPath) ? options.customPath : path2.join(projectRoot, options.customPath);
2798
3062
  } else if (options.location === "feature-service" && options.feature) {
2799
- targetDir = path15.join(resolveFeaturesDir(projectRoot), options.feature, "services");
3063
+ targetDir = path2.join(resolveFeaturesDir(projectRoot), options.feature, "services");
2800
3064
  } else if (options.location === "feature-transformer" && options.feature) {
2801
- targetDir = path15.join(resolveFeaturesDir(projectRoot), options.feature, "pxExpress", "transformers");
3065
+ targetDir = path2.join(resolveFeaturesDir(projectRoot), options.feature, "pxExpress", "transformers");
2802
3066
  } else if (options.location === "module") {
2803
- targetDir = path15.join(projectRoot, "modules", className);
3067
+ targetDir = path2.join(projectRoot, "modules", className);
2804
3068
  } else {
2805
- targetDir = options.feature ? path15.join(resolveFeaturesDir(projectRoot), options.feature, "services") : path15.join(projectRoot, "shared", className.toLowerCase());
3069
+ targetDir = options.feature ? path2.join(resolveFeaturesDir(projectRoot), options.feature, "services") : path2.join(projectRoot, "shared", className.toLowerCase());
2806
3070
  }
2807
- const filePath = path15.join(targetDir, `${className}.ts`);
3071
+ const filePath = path2.join(targetDir, `${className}.ts`);
2808
3072
  let ifaceName;
2809
3073
  if (options.generateInterface) {
2810
3074
  ifaceName = `I${className}`;
2811
- const ifacePath = path15.join(targetDir, `${ifaceName}.ts`);
3075
+ const ifacePath = path2.join(targetDir, `${ifaceName}.ts`);
2812
3076
  const ifaceContent = renderTemplate(injectableInterfaceTemplate, { ifaceName });
2813
3077
  files.push(
2814
3078
  writeFileSafely(ifacePath, ifaceContent, {
@@ -2917,7 +3181,7 @@ async function runInjectableCommand(classNameArg, flags = {}) {
2917
3181
  );
2918
3182
  }
2919
3183
  const s = p3.spinner();
2920
- s.start(`Generating injectable '${pc13.cyan(className)}'...`);
3184
+ s.start(`Generating injectable '${pc14.cyan(className)}'...`);
2921
3185
  const options = {
2922
3186
  name: className,
2923
3187
  feature,
@@ -2931,7 +3195,7 @@ async function runInjectableCommand(classNameArg, flags = {}) {
2931
3195
  const files = generateInjectable(options);
2932
3196
  s.stop("Injectable class generated successfully!");
2933
3197
  printGeneratedFiles(files, options.dryRun);
2934
- p3.outro(pc13.green(`Injectable '${className}' created successfully!`));
3198
+ p3.outro(pc14.green(`Injectable '${className}' created successfully!`));
2935
3199
  }
2936
3200
  async function runInteractiveMenu(flags = {}) {
2937
3201
  printBanner();
@@ -2944,6 +3208,11 @@ async function runInteractiveMenu(flags = {}) {
2944
3208
  label: "Feature",
2945
3209
  hint: "Complete vertical slice (domain, infra, usecase, pxExpress, diProvider)"
2946
3210
  },
3211
+ {
3212
+ value: "shared",
3213
+ label: "Shared Module",
3214
+ hint: "Foundational utility or port with ports, infrastructure & diProvider"
3215
+ },
2947
3216
  {
2948
3217
  value: "usecase",
2949
3218
  label: "Use Case",
@@ -2981,6 +3250,9 @@ async function runInteractiveMenu(flags = {}) {
2981
3250
  case "feature":
2982
3251
  await runFeatureCommand(void 0, flags);
2983
3252
  break;
3253
+ case "shared":
3254
+ await runSharedCommand(void 0, flags);
3255
+ break;
2984
3256
  case "usecase":
2985
3257
  await runUseCaseCommand(void 0, flags);
2986
3258
  break;
@@ -3013,6 +3285,9 @@ async function handleCreate(type, name, flags = {}) {
3013
3285
  case "feature":
3014
3286
  await runFeatureCommand(name, flags);
3015
3287
  break;
3288
+ case "shared":
3289
+ await runSharedCommand(name, flags);
3290
+ break;
3016
3291
  case "usecase":
3017
3292
  await runUseCaseCommand(name, flags);
3018
3293
  break;
@@ -3034,8 +3309,8 @@ async function handleCreate(type, name, flags = {}) {
3034
3309
  break;
3035
3310
  default:
3036
3311
  p3.log.error(
3037
- pc13.red(
3038
- `Unknown component type: "${type}". Allowed types: feature, usecase, domain, infrastructure, interface, httpHandler, injectable.`
3312
+ pc14.red(
3313
+ `Unknown component type: "${type}". Allowed types: feature, shared, usecase, domain, infrastructure, interface, httpHandler, injectable.`
3039
3314
  )
3040
3315
  );
3041
3316
  process.exit(1);
@@ -3058,25 +3333,25 @@ function normalizeRegistryUrl(urlOrPath, baseDir) {
3058
3333
  return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}`;
3059
3334
  }
3060
3335
  if (baseDir && (trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed === "." || trimmed === "..")) {
3061
- return path15.resolve(baseDir, trimmed);
3336
+ return path2.resolve(baseDir, trimmed);
3062
3337
  }
3063
3338
  return trimmed;
3064
3339
  }
3065
3340
  function findLocalRegistry(startDir = process.cwd()) {
3066
- let current = path15.resolve(startDir);
3341
+ let current = path2.resolve(startDir);
3067
3342
  while (true) {
3068
- const candidate = path15.join(current, "mason-registry");
3069
- if (fs7.existsSync(candidate) && fs7.existsSync(path15.join(candidate, "index.json"))) {
3343
+ const candidate = path2.join(current, "mason-registry");
3344
+ if (fs9.existsSync(candidate) && fs9.existsSync(path2.join(candidate, "index.json"))) {
3070
3345
  return candidate;
3071
3346
  }
3072
- const parent = path15.dirname(current);
3347
+ const parent = path2.dirname(current);
3073
3348
  if (parent === current) break;
3074
3349
  current = parent;
3075
3350
  }
3076
3351
  return null;
3077
3352
  }
3078
3353
  function getRegistryBase(projectRoot, override) {
3079
- const rootDir = projectRoot ? path15.resolve(projectRoot) : process.cwd();
3354
+ const rootDir = projectRoot ? path2.resolve(projectRoot) : process.cwd();
3080
3355
  if (override && override.trim()) {
3081
3356
  return normalizeRegistryUrl(override.trim(), rootDir);
3082
3357
  }
@@ -3084,30 +3359,30 @@ function getRegistryBase(projectRoot, override) {
3084
3359
  return normalizeRegistryUrl(process.env.MASON_REGISTRY_URL.trim(), rootDir);
3085
3360
  }
3086
3361
  const configLocations = [
3087
- path15.join(rootDir, "mason.config.json"),
3088
- path15.join(process.cwd(), "mason.config.json")
3362
+ path2.join(rootDir, "mason.config.json"),
3363
+ path2.join(process.cwd(), "mason.config.json")
3089
3364
  ];
3090
3365
  for (const configPath of configLocations) {
3091
- if (fs7.existsSync(configPath)) {
3366
+ if (fs9.existsSync(configPath)) {
3092
3367
  try {
3093
- const config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
3368
+ const config = JSON.parse(fs9.readFileSync(configPath, "utf-8"));
3094
3369
  if (config.registry && typeof config.registry === "string") {
3095
- return normalizeRegistryUrl(config.registry.trim(), path15.dirname(configPath));
3370
+ return normalizeRegistryUrl(config.registry.trim(), path2.dirname(configPath));
3096
3371
  }
3097
3372
  } catch {
3098
3373
  }
3099
3374
  }
3100
3375
  }
3101
3376
  const pkgLocations = [
3102
- path15.join(rootDir, "package.json"),
3103
- path15.join(process.cwd(), "package.json")
3377
+ path2.join(rootDir, "package.json"),
3378
+ path2.join(process.cwd(), "package.json")
3104
3379
  ];
3105
3380
  for (const pkgPath of pkgLocations) {
3106
- if (fs7.existsSync(pkgPath)) {
3381
+ if (fs9.existsSync(pkgPath)) {
3107
3382
  try {
3108
- const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
3383
+ const pkg = JSON.parse(fs9.readFileSync(pkgPath, "utf-8"));
3109
3384
  if (pkg.mason?.registry && typeof pkg.mason.registry === "string") {
3110
- return normalizeRegistryUrl(pkg.mason.registry.trim(), path15.dirname(pkgPath));
3385
+ return normalizeRegistryUrl(pkg.mason.registry.trim(), path2.dirname(pkgPath));
3111
3386
  }
3112
3387
  } catch {
3113
3388
  }
@@ -3148,11 +3423,11 @@ async function fetchRegistryFile(registryBase, relPath) {
3148
3423
  if (baseDir.startsWith("file://")) {
3149
3424
  baseDir = fileURLToPath(baseDir);
3150
3425
  }
3151
- const filePath = path15.resolve(baseDir, cleanRelPath);
3152
- if (!fs7.existsSync(filePath)) {
3426
+ const filePath = path2.resolve(baseDir, cleanRelPath);
3427
+ if (!fs9.existsSync(filePath)) {
3153
3428
  throw new Error(`Registry file not found at ${filePath}`);
3154
3429
  }
3155
- return fs7.promises.readFile(filePath, "utf-8");
3430
+ return fs9.promises.readFile(filePath, "utf-8");
3156
3431
  }
3157
3432
  async function fetchRegistryIndex(registryBase) {
3158
3433
  const content = await fetchRegistryFile(registryBase, "index.json");
@@ -3214,26 +3489,26 @@ function findClosestMatch(target, candidates) {
3214
3489
  return null;
3215
3490
  }
3216
3491
  function detectPackageManager(projectRoot) {
3217
- if (fs7.existsSync(path15.join(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
3218
- if (fs7.existsSync(path15.join(projectRoot, "yarn.lock"))) return "yarn";
3219
- if (fs7.existsSync(path15.join(projectRoot, "bun.lockb")) || fs7.existsSync(path15.join(projectRoot, "bun.lock"))) return "bun";
3492
+ if (fs9.existsSync(path2.join(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
3493
+ if (fs9.existsSync(path2.join(projectRoot, "yarn.lock"))) return "yarn";
3494
+ if (fs9.existsSync(path2.join(projectRoot, "bun.lockb")) || fs9.existsSync(path2.join(projectRoot, "bun.lock"))) return "bun";
3220
3495
  return "npm";
3221
3496
  }
3222
3497
  function getMissingNpmPackages(projectRoot, requiredPackages) {
3223
3498
  if (!requiredPackages || requiredPackages.length === 0) return [];
3224
- const pkgJsonPath = path15.join(projectRoot, "package.json");
3225
- if (!fs7.existsSync(pkgJsonPath)) {
3499
+ const pkgJsonPath = path2.join(projectRoot, "package.json");
3500
+ if (!fs9.existsSync(pkgJsonPath)) {
3226
3501
  return requiredPackages;
3227
3502
  }
3228
3503
  try {
3229
- const pkg = JSON.parse(fs7.readFileSync(pkgJsonPath, "utf-8"));
3504
+ const pkg = JSON.parse(fs9.readFileSync(pkgJsonPath, "utf-8"));
3230
3505
  const installed = /* @__PURE__ */ new Set([
3231
3506
  ...Object.keys(pkg.dependencies || {}),
3232
3507
  ...Object.keys(pkg.devDependencies || {}),
3233
3508
  ...Object.keys(pkg.peerDependencies || {})
3234
3509
  ]);
3235
- return requiredPackages.filter((p15) => {
3236
- const nameOnly = p15.startsWith("@") ? `@${p15.slice(1).split("@")[0] || ""}` : p15.split("@")[0] || p15;
3510
+ return requiredPackages.filter((p16) => {
3511
+ const nameOnly = p16.startsWith("@") ? `@${p16.slice(1).split("@")[0] || ""}` : p16.split("@")[0] || p16;
3237
3512
  return !installed.has(nameOnly);
3238
3513
  });
3239
3514
  } catch {
@@ -3258,21 +3533,207 @@ function installNpmPackages(projectRoot, packages, pm) {
3258
3533
  installCmd = `npm install ${packages.join(" ")}`;
3259
3534
  break;
3260
3535
  }
3261
- p3.log.info(`Installing missing dependencies with ${manager}: ${pc13.cyan(packages.join(", "))}`);
3536
+ p3.log.info(`Installing missing dependencies with ${manager}: ${pc14.cyan(packages.join(", "))}`);
3262
3537
  execSync(installCmd, {
3263
3538
  cwd: projectRoot,
3264
3539
  stdio: "inherit"
3265
3540
  });
3266
3541
  }
3542
+ function getAllFiles(dir, baseDir = dir) {
3543
+ if (!fs9.existsSync(dir)) return [];
3544
+ const entries = fs9.readdirSync(dir, { withFileTypes: true });
3545
+ const files = [];
3546
+ for (const entry of entries) {
3547
+ const fullPath = path2.join(dir, entry.name);
3548
+ const relPath = path2.relative(baseDir, fullPath).replace(/\\/g, "/");
3549
+ if (entry.isDirectory()) {
3550
+ if (entry.name === "__tests__" || entry.name.startsWith(".")) {
3551
+ continue;
3552
+ }
3553
+ files.push(...getAllFiles(fullPath, baseDir));
3554
+ } else if (entry.isFile()) {
3555
+ if (entry.name === "registry.json" || entry.name.endsWith(".test.ts") || entry.name.endsWith(".spec.ts") || entry.name.startsWith(".")) {
3556
+ continue;
3557
+ }
3558
+ files.push(relPath);
3559
+ }
3560
+ }
3561
+ return files.sort();
3562
+ }
3563
+ function computeDirectoryHash(dirPath, files) {
3564
+ if (!fs9.existsSync(dirPath)) return "";
3565
+ const hash = createHash("sha256");
3566
+ const fileList = (getAllFiles(dirPath)).sort();
3567
+ for (const relativePath of fileList) {
3568
+ const absolutePath = path2.join(dirPath, relativePath);
3569
+ if (!fs9.existsSync(absolutePath)) continue;
3570
+ const content = fs9.readFileSync(absolutePath, "utf8");
3571
+ const normalizedContent = content.replace(/\r\n/g, "\n");
3572
+ hash.update(relativePath);
3573
+ hash.update(normalizedContent);
3574
+ }
3575
+ return `sha256-${hash.digest("hex")}`;
3576
+ }
3267
3577
 
3268
3578
  // src/commands/import.ts
3579
+ var importPrompts = {
3580
+ select: p3.select,
3581
+ text: p3.text
3582
+ };
3583
+ function rewriteImports(content, aliasMap) {
3584
+ if (aliasMap.size === 0) return content;
3585
+ let rewritten = content;
3586
+ for (const [oldImport, newImport] of aliasMap.entries()) {
3587
+ const rawOld = oldImport.endsWith("/") ? oldImport.slice(0, -1) : oldImport;
3588
+ const rawNew = newImport.endsWith("/") ? newImport.slice(0, -1) : newImport;
3589
+ const escaped = rawOld.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3590
+ const regex = new RegExp(`${escaped}(?=[\\/'"\`]|$)`, "g");
3591
+ rewritten = rewritten.replace(regex, rawNew);
3592
+ }
3593
+ return rewritten;
3594
+ }
3595
+ function getModuleBaseDir(projectRoot, modType) {
3596
+ const folder = modType === "shared" ? "shared" : "features";
3597
+ return path2.join(projectRoot, "src", folder);
3598
+ }
3599
+ function getExistingModuleDir(projectRoot, modType, modName) {
3600
+ const folder = modType === "shared" ? "shared" : "features";
3601
+ const inSrc = path2.join(projectRoot, "src", folder, modName);
3602
+ if (fs9.existsSync(inSrc)) return inSrc;
3603
+ const inRoot = path2.join(projectRoot, folder, modName);
3604
+ if (fs9.existsSync(inRoot)) return inRoot;
3605
+ return null;
3606
+ }
3607
+ function checkModuleExists(projectRoot, modType, modName) {
3608
+ return getExistingModuleDir(projectRoot, modType, modName) !== null;
3609
+ }
3610
+ async function resolveDependency(depType, depName, aliasMap, projectRoot, flags, importModuleRecursive, importedModules, registryBase, index) {
3611
+ const folder = depType === "shared" ? "shared" : "features";
3612
+ const isAlreadyImportedThisRun = importedModules.has(`${depType}:${depName}`);
3613
+ if (isAlreadyImportedThisRun) return;
3614
+ const programmaticAlias = flags.alias?.[depName] || flags.alias?.[`${depType}/${depName}`];
3615
+ if (programmaticAlias) {
3616
+ aliasMap.set(`@/${folder}/${depName}/`, `@/${folder}/${programmaticAlias}/`);
3617
+ const baseDir = getModuleBaseDir(projectRoot, depType);
3618
+ const displayDest = path2.relative(
3619
+ projectRoot,
3620
+ path2.join(baseDir, programmaticAlias)
3621
+ );
3622
+ p3.log.step(`Importing '${depName}' into '${displayDest}'...`);
3623
+ await importModuleRecursive(depType, depName, programmaticAlias);
3624
+ return;
3625
+ }
3626
+ const existingDir = getExistingModuleDir(projectRoot, depType, depName);
3627
+ if (!existingDir) {
3628
+ p3.log.info(` \u21B3 Resolving required ${depType} dependency: ${pc14.cyan(depName)}`);
3629
+ await importModuleRecursive(depType, depName, depName);
3630
+ return;
3631
+ }
3632
+ const entry = (depType === "shared" ? index?.shared : index?.features)?.find(
3633
+ (item) => item.name === depName
3634
+ );
3635
+ let remoteIntegrity = entry?.integrity;
3636
+ if (!remoteIntegrity) {
3637
+ try {
3638
+ const depManifest = await fetchModuleManifest(registryBase, depType, depName);
3639
+ remoteIntegrity = depManifest.integrity;
3640
+ } catch {
3641
+ }
3642
+ }
3643
+ const localHash = computeDirectoryHash(existingDir);
3644
+ if (remoteIntegrity && localHash === remoteIntegrity) {
3645
+ p3.log.step(`\u2713 ${depType}/${depName} is up to date (reusing existing)`);
3646
+ importedModules.add(`${depType}:${depName}`);
3647
+ return;
3648
+ }
3649
+ if (flags.overwrite) {
3650
+ p3.log.step(`Overwriting '${depType}/${depName}' with registry version (--overwrite)...`);
3651
+ const baseDir = getModuleBaseDir(projectRoot, depType);
3652
+ const targetDir = path2.join(baseDir, depName);
3653
+ if (!flags.dryRun) {
3654
+ if (existingDir !== targetDir) {
3655
+ fs9.rmSync(existingDir, { recursive: true, force: true });
3656
+ }
3657
+ fs9.rmSync(targetDir, { recursive: true, force: true });
3658
+ }
3659
+ await importModuleRecursive(depType, depName, depName, { overwrite: true });
3660
+ return;
3661
+ }
3662
+ if (flags.yes) {
3663
+ p3.log.info(`Using existing '${depType}/${depName}'`);
3664
+ importedModules.add(`${depType}:${depName}`);
3665
+ return;
3666
+ }
3667
+ const choice = handleCancel(
3668
+ await importPrompts.select({
3669
+ message: `Local '${depType}/${depName}' differs from the registry version.`,
3670
+ options: [
3671
+ {
3672
+ value: "keep_existing",
3673
+ label: `Keep existing local version (Recommended)`
3674
+ },
3675
+ {
3676
+ value: "rename",
3677
+ label: "Import fresh copy under a new name (alias)"
3678
+ },
3679
+ {
3680
+ value: "overwrite",
3681
+ label: "Overwrite local version with registry version"
3682
+ }
3683
+ ]
3684
+ })
3685
+ );
3686
+ if (choice === "keep_existing" || choice === "use_existing") {
3687
+ p3.log.step(`Skipping '${depType}/${depName}' (keeping existing)`);
3688
+ importedModules.add(`${depType}:${depName}`);
3689
+ return;
3690
+ }
3691
+ if (choice === "rename") {
3692
+ const newName = handleCancel(
3693
+ await importPrompts.text({
3694
+ message: "What should we name the new copy?",
3695
+ placeholder: `${depName}-copy`,
3696
+ validate(val) {
3697
+ const v = val?.trim();
3698
+ if (!v) return "Name cannot be empty";
3699
+ if (v === depName) return `Name must be different from '${depName}'`;
3700
+ if (!/^[a-zA-Z0-9_-]+$/.test(v)) {
3701
+ return "Name can only contain alphanumeric characters, dashes, and underscores";
3702
+ }
3703
+ if (checkModuleExists(projectRoot, depType, v)) {
3704
+ return `'${v}' already exists in ${folder}, please choose another name`;
3705
+ }
3706
+ }
3707
+ })
3708
+ ).trim();
3709
+ aliasMap.set(`@/${folder}/${depName}/`, `@/${folder}/${newName}/`);
3710
+ const baseDir = getModuleBaseDir(projectRoot, depType);
3711
+ const displayDest = path2.relative(projectRoot, path2.join(baseDir, newName));
3712
+ p3.log.step(`Importing '${depName}' into '${displayDest}'...`);
3713
+ await importModuleRecursive(depType, depName, newName);
3714
+ return;
3715
+ }
3716
+ if (choice === "overwrite") {
3717
+ p3.log.step(`Overwriting '${depType}/${depName}' with registry version...`);
3718
+ const baseDir = getModuleBaseDir(projectRoot, depType);
3719
+ const targetDir = path2.join(baseDir, depName);
3720
+ if (!flags.dryRun) {
3721
+ if (existingDir !== targetDir) {
3722
+ fs9.rmSync(existingDir, { recursive: true, force: true });
3723
+ }
3724
+ fs9.rmSync(targetDir, { recursive: true, force: true });
3725
+ }
3726
+ await importModuleRecursive(depType, depName, depName, { overwrite: true });
3727
+ return;
3728
+ }
3729
+ }
3269
3730
  async function handleImport(typeArg, nameArg, flags = {}) {
3270
3731
  const projectRoot = findProjectRoot();
3271
3732
  let type = typeArg?.trim().toLowerCase();
3272
3733
  if (type === "features") type = "feature";
3273
3734
  if (!type) {
3274
3735
  type = handleCancel(
3275
- await p3.select({
3736
+ await importPrompts.select({
3276
3737
  message: "What kind of module would you like to import?",
3277
3738
  options: [
3278
3739
  {
@@ -3290,19 +3751,19 @@ async function handleImport(typeArg, nameArg, flags = {}) {
3290
3751
  );
3291
3752
  }
3292
3753
  if (type !== "shared" && type !== "feature") {
3293
- p3.log.error(pc13.red(`Invalid module type: "${typeArg}". Must be "shared" or "feature".`));
3754
+ p3.log.error(pc14.red(`Invalid module type: "${typeArg}". Must be "shared" or "feature".`));
3294
3755
  process.exit(1);
3295
3756
  }
3296
3757
  const registryBase = getRegistryBase(projectRoot, flags.registry);
3297
3758
  const s = p3.spinner();
3298
- s.start(`Connecting to registry at ${pc13.dim(registryBase)}...`);
3759
+ s.start(`Connecting to registry at ${pc14.dim(registryBase)}...`);
3299
3760
  let index;
3300
3761
  try {
3301
3762
  index = await fetchRegistryIndex(registryBase);
3302
3763
  s.stop("Registry index loaded.");
3303
3764
  } catch (err) {
3304
- s.stop(pc13.red("Failed to load registry index."));
3305
- p3.log.error(pc13.red(err.message || String(err)));
3765
+ s.stop(pc14.red("Failed to load registry index."));
3766
+ p3.log.error(pc14.red(err.message || String(err)));
3306
3767
  process.exit(1);
3307
3768
  }
3308
3769
  const availableList = type === "shared" ? index.shared : index.features;
@@ -3314,7 +3775,7 @@ async function handleImport(typeArg, nameArg, flags = {}) {
3314
3775
  return;
3315
3776
  }
3316
3777
  name = handleCancel(
3317
- await p3.select({
3778
+ await importPrompts.select({
3318
3779
  message: `Select a ${type} to import:`,
3319
3780
  options: availableList.map((item) => ({
3320
3781
  value: item.name,
@@ -3326,71 +3787,94 @@ async function handleImport(typeArg, nameArg, flags = {}) {
3326
3787
  }
3327
3788
  if (!candidateNames.includes(name)) {
3328
3789
  const closest = findClosestMatch(name, candidateNames);
3329
- p3.log.error(pc13.red(`\u274C Module '${name}' not found in the ${type} registry.`));
3790
+ p3.log.error(pc14.red(`\u274C Module '${name}' not found in the ${type} registry.`));
3330
3791
  if (closest) {
3331
- p3.log.info(pc13.yellow(`Did you mean '${pc13.cyan(closest)}'?`));
3792
+ p3.log.info(pc14.yellow(`Did you mean '${pc14.cyan(closest)}'?`));
3332
3793
  }
3333
3794
  process.exit(1);
3334
3795
  }
3335
3796
  const importedModules = /* @__PURE__ */ new Set();
3336
3797
  const totalWrittenFiles = [];
3337
3798
  const requiredNpmPackages = /* @__PURE__ */ new Set();
3338
- async function importModuleRecursive(modType, modName) {
3339
- const modKey = `${modType}:${modName}`;
3799
+ async function importModuleRecursive(modType, registryName, targetName = registryName, options) {
3800
+ const modKey = `${modType}:${targetName}`;
3340
3801
  if (importedModules.has(modKey)) return;
3341
- p3.log.step(`Importing ${modType}: ${pc13.cyan(modName)}...`);
3802
+ p3.log.step(
3803
+ `Importing ${modType}: ${pc14.cyan(targetName)}${targetName !== registryName ? pc14.dim(` (from ${registryName})`) : ""}...`
3804
+ );
3342
3805
  let manifest;
3343
3806
  try {
3344
- manifest = await fetchModuleManifest(registryBase, modType, modName);
3807
+ manifest = await fetchModuleManifest(registryBase, modType, registryName);
3345
3808
  } catch (err) {
3346
- p3.log.error(pc13.red(`Failed to fetch manifest for ${modType} '${modName}': ${err.message}`));
3809
+ p3.log.error(pc14.red(`Failed to fetch manifest for ${modType} '${registryName}': ${err.message}`));
3347
3810
  process.exit(1);
3348
3811
  }
3812
+ const aliasMap = /* @__PURE__ */ new Map();
3813
+ if (targetName !== registryName) {
3814
+ const folder2 = modType === "shared" ? "shared" : "features";
3815
+ aliasMap.set(`@/${folder2}/${registryName}/`, `@/${folder2}/${targetName}/`);
3816
+ }
3349
3817
  const sharedDeps = manifest.dependencies?.shared || [];
3350
3818
  for (const dep of sharedDeps) {
3351
- const depPath = path15.join(projectRoot, "src", "shared", dep);
3352
- if (!fs7.existsSync(depPath) && !importedModules.has(`shared:${dep}`)) {
3353
- p3.log.info(` \u21B3 Resolving required shared dependency: ${pc13.cyan(dep)}`);
3354
- await importModuleRecursive("shared", dep);
3355
- }
3819
+ await resolveDependency(
3820
+ "shared",
3821
+ dep,
3822
+ aliasMap,
3823
+ projectRoot,
3824
+ flags,
3825
+ importModuleRecursive,
3826
+ importedModules,
3827
+ registryBase,
3828
+ index
3829
+ );
3356
3830
  }
3357
3831
  const featureDeps = manifest.dependencies?.features || [];
3358
3832
  for (const dep of featureDeps) {
3359
- const depPath = path15.join(projectRoot, "src", "features", dep);
3360
- if (!fs7.existsSync(depPath) && !importedModules.has(`feature:${dep}`)) {
3361
- p3.log.info(` \u21B3 Resolving required feature dependency: ${pc13.cyan(dep)}`);
3362
- await importModuleRecursive("feature", dep);
3363
- }
3833
+ await resolveDependency(
3834
+ "feature",
3835
+ dep,
3836
+ aliasMap,
3837
+ projectRoot,
3838
+ flags,
3839
+ importModuleRecursive,
3840
+ importedModules,
3841
+ registryBase,
3842
+ index
3843
+ );
3364
3844
  }
3365
3845
  for (const pkg of manifest.dependencies?.npm || []) {
3366
3846
  requiredNpmPackages.add(pkg);
3367
3847
  }
3368
3848
  const folder = modType === "shared" ? "shared" : "features";
3369
- const targetDir = path15.join(projectRoot, "src", folder, modName);
3849
+ const targetDir = path2.join(getModuleBaseDir(projectRoot, modType), targetName);
3850
+ const isOverwrite = Boolean(flags.overwrite || options?.overwrite);
3370
3851
  for (const file of manifest.files || []) {
3371
3852
  if (file === "registry.json") continue;
3372
- const remoteFilePath = `${folder}/${modName}/${file}`;
3373
- const localFilePath = path15.join(targetDir, file);
3374
- if (fs7.existsSync(localFilePath) && !flags.overwrite) {
3375
- p3.log.warn(` \u21B3 Skipping existing file: ${pc13.dim(path15.relative(projectRoot, localFilePath))} (use --overwrite to replace)`);
3853
+ const remoteFilePath = `${folder}/${registryName}/${file}`;
3854
+ const localFilePath = path2.join(targetDir, file);
3855
+ if (fs9.existsSync(localFilePath) && !isOverwrite) {
3856
+ p3.log.warn(
3857
+ ` \u21B3 Skipping existing file: ${pc14.dim(path2.relative(projectRoot, localFilePath))} (use --overwrite to replace)`
3858
+ );
3376
3859
  continue;
3377
3860
  }
3378
- const content = await fetchRegistryFile(registryBase, remoteFilePath);
3861
+ let content = await fetchRegistryFile(registryBase, remoteFilePath);
3862
+ content = rewriteImports(content, aliasMap);
3379
3863
  if (!flags.dryRun) {
3380
- fs7.mkdirSync(path15.dirname(localFilePath), { recursive: true });
3381
- fs7.writeFileSync(localFilePath, content, "utf-8");
3864
+ fs9.mkdirSync(path2.dirname(localFilePath), { recursive: true });
3865
+ fs9.writeFileSync(localFilePath, content, "utf-8");
3382
3866
  }
3383
- const displayPath = path15.relative(projectRoot, localFilePath);
3867
+ const displayPath = path2.relative(projectRoot, localFilePath);
3384
3868
  totalWrittenFiles.push(displayPath);
3385
- p3.log.message(` ${pc13.green("+")} ${displayPath}`);
3869
+ p3.log.message(` ${pc14.green("+")} ${displayPath}`);
3386
3870
  }
3387
3871
  importedModules.add(modKey);
3388
3872
  }
3389
- await importModuleRecursive(type, name);
3873
+ await importModuleRecursive(type, name, name);
3390
3874
  const missingNpm = getMissingNpmPackages(projectRoot, Array.from(requiredNpmPackages));
3391
3875
  if (missingNpm.length > 0) {
3392
3876
  if (flags.skipInstall || flags.dryRun) {
3393
- p3.log.info(`Required npm dependencies to install: ${pc13.cyan(missingNpm.join(", "))}`);
3877
+ p3.log.info(`Required npm dependencies to install: ${pc14.cyan(missingNpm.join(", "))}`);
3394
3878
  } else {
3395
3879
  try {
3396
3880
  installNpmPackages(projectRoot, missingNpm);
@@ -3400,11 +3884,13 @@ async function handleImport(typeArg, nameArg, flags = {}) {
3400
3884
  }
3401
3885
  }
3402
3886
  if (flags.dryRun) {
3403
- p3.log.info(pc13.yellow("Dry run complete: No files were written to disk."));
3887
+ p3.log.info(pc14.yellow("Dry run complete: No files were written to disk."));
3404
3888
  } else {
3889
+ const baseDir = getModuleBaseDir(projectRoot, type);
3890
+ const displayTarget = path2.relative(projectRoot, path2.join(baseDir, name));
3405
3891
  p3.log.success(
3406
- pc13.green(
3407
- `Successfully imported ${type} '${name}' (${totalWrittenFiles.length} file${totalWrittenFiles.length === 1 ? "" : "s"} placed in src/${type === "shared" ? "shared" : "features"}/${name})`
3892
+ pc14.green(
3893
+ `Successfully imported ${type} '${name}' (${totalWrittenFiles.length} file${totalWrittenFiles.length === 1 ? "" : "s"} placed in ${displayTarget})`
3408
3894
  )
3409
3895
  );
3410
3896
  }
@@ -3413,24 +3899,70 @@ async function handleImport(typeArg, nameArg, flags = {}) {
3413
3899
  // src/cli.ts
3414
3900
  function createCli() {
3415
3901
  const cli = cac("mason");
3416
- 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) => {
3902
+ 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").option("--registry", "Run in registry mode").action(async (args, flags) => {
3417
3903
  if (args && args.length > 0) {
3418
3904
  p3.log.error(
3419
- pc13.red(
3905
+ pc14.red(
3420
3906
  `Unknown command: "${args.join(" ")}". Use --help to list available commands.`
3421
3907
  )
3422
3908
  );
3423
3909
  process.exit(1);
3424
3910
  }
3425
3911
  try {
3912
+ const projectRoot = findProjectRoot();
3913
+ const mode = getEffectiveMode(projectRoot, flags);
3914
+ if (mode === "registry") {
3915
+ const choice2 = handleCancel(
3916
+ await p3.select({
3917
+ message: "\u{1F3DB}\uFE0F Mason (Registry Mode): What would you like to scaffold?",
3918
+ options: [
3919
+ {
3920
+ value: "create_feature",
3921
+ label: "Initialize New Feature",
3922
+ hint: "Scaffold a new feature slice under features/"
3923
+ },
3924
+ {
3925
+ value: "create_shared",
3926
+ label: "Initialize New Shared Module",
3927
+ hint: "Scaffold a new shared utility/port under shared/"
3928
+ },
3929
+ {
3930
+ value: "create_usecase",
3931
+ label: "Create New Use Case",
3932
+ hint: "Add a new use case to an existing feature"
3933
+ },
3934
+ {
3935
+ value: "import",
3936
+ label: "Import from Remote Registry",
3937
+ hint: "Download a module from upstream"
3938
+ }
3939
+ ]
3940
+ })
3941
+ );
3942
+ switch (choice2) {
3943
+ case "create_feature":
3944
+ await handleCreate("feature", void 0, flags);
3945
+ break;
3946
+ case "create_shared":
3947
+ await handleCreate("shared", void 0, flags);
3948
+ break;
3949
+ case "create_usecase":
3950
+ await handleCreate("usecase", void 0, flags);
3951
+ break;
3952
+ case "import":
3953
+ await handleImport(void 0, void 0, flags);
3954
+ break;
3955
+ }
3956
+ return;
3957
+ }
3426
3958
  const choice = handleCancel(
3427
3959
  await p3.select({
3428
3960
  message: "\u{1F9F1} Mason: What would you like to do?",
3429
3961
  options: [
3430
3962
  {
3431
3963
  value: "create",
3432
- label: "Create local component",
3433
- hint: "Scaffold a new feature, use case, domain entity, interface, etc."
3964
+ label: "Create component",
3965
+ hint: "Scaffold a new feature, shared module, use case, domain entity, etc."
3434
3966
  },
3435
3967
  {
3436
3968
  value: "import",
@@ -3451,8 +3983,8 @@ function createCli() {
3451
3983
  });
3452
3984
  cli.command(
3453
3985
  "create [type] [name]",
3454
- "Scaffold local Clean Architecture components (feature, usecase, domain, etc.)"
3455
- ).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 = {}) => {
3986
+ "Scaffold local Clean Architecture components (feature, shared, usecase, domain, etc.)"
3987
+ ).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("--dest <dir>", "Override target directory (alias for --target)").option("--registry", "Scaffold in registry mode (stamps registry.json)").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 = {}) => {
3456
3988
  try {
3457
3989
  await handleCreate(type, name, flags);
3458
3990
  } catch (err) {
@@ -3471,6 +4003,7 @@ function createCli() {
3471
4003
  });
3472
4004
  const createSubtypes = [
3473
4005
  "feature",
4006
+ "shared",
3474
4007
  "usecase",
3475
4008
  "domain",
3476
4009
  "infrastructure",
@@ -3491,7 +4024,7 @@ function createCli() {
3491
4024
  });
3492
4025
  }
3493
4026
  cli.help();
3494
- cli.version("1.0.10");
4027
+ cli.version("1.0.14");
3495
4028
  return cli;
3496
4029
  }
3497
4030
  function handleCliError(err) {
@@ -3499,7 +4032,7 @@ function handleCliError(err) {
3499
4032
  process.exit(0);
3500
4033
  }
3501
4034
  const message = err instanceof Error ? err.message : String(err);
3502
- p3.log.error(pc13.red(`Error: ${message}`));
4035
+ p3.log.error(pc14.red(`Error: ${message}`));
3503
4036
  process.exit(1);
3504
4037
  }
3505
4038
  async function runCli(argv = process.argv) {