@solid-stack/mason 1.0.11 → 1.0.13

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
- import * as p3 from '@clack/prompts';
4
- import pc13 from 'picocolors';
5
- import path6 from 'path';
6
- import fs7 from 'fs';
3
+ import * as p15 from '@clack/prompts';
4
+ import pc15 from 'picocolors';
5
+ import fs12 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 = path6.relative(projectRoot, absolutePath).replace(/\\/g, "/");
14
- const existedBefore = fs7.existsSync(absolutePath);
14
+ const relPath = path2.relative(projectRoot, absolutePath).replace(/\\/g, "/");
15
+ const existedBefore = fs12.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 = path6.dirname(absolutePath);
34
- if (!fs7.existsSync(dir)) {
35
- fs7.mkdirSync(dir, { recursive: true });
34
+ const dir = path2.dirname(absolutePath);
35
+ if (!fs12.existsSync(dir)) {
36
+ fs12.mkdirSync(dir, { recursive: true });
36
37
  }
37
- fs7.writeFileSync(absolutePath, content, "utf-8");
38
+ fs12.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
+ p15.log.warn(pc15.yellow(pc15.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
+ p15.log.success(`${pc15.bold(f.relativePath)} ${pc15.dim("(created)")}`);
54
55
  } else if (f.action === "overwritten") {
55
- p3.log.warn(`${pc13.bold(f.relativePath)} ${pc13.dim("(overwritten)")}`);
56
+ p15.log.warn(`${pc15.bold(f.relativePath)} ${pc15.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
+ p15.log.info(`${pc15.dim(f.relativePath)} ${pc15.dim("(skipped - already exists, pass -o to overwrite)")}`);
58
59
  } else {
59
- p3.log.info(`${pc13.bold(f.relativePath)} ${pc13.dim("(preview)")}`);
60
+ p15.log.info(`${pc15.bold(f.relativePath)} ${pc15.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 (fs12.existsSync(configPath)) {
133
+ try {
134
+ return JSON.parse(fs12.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 (fs12.existsSync(path2.join(projectRoot, "index.json")) && (fs12.existsSync(path2.join(projectRoot, "features")) || fs12.existsSync(path2.join(projectRoot, "shared"))) && !fs12.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 (fs12.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 (fs12.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 = path6.resolve(startDir);
189
+ let current = path2.resolve(startDir);
129
190
  while (true) {
130
- if (fs7.existsSync(path6.join(current, "package.json"))) {
191
+ if (fs12.existsSync(path2.join(current, "package.json"))) {
131
192
  return current;
132
193
  }
133
- const parent = path6.dirname(current);
194
+ const parent = path2.dirname(current);
134
195
  if (parent === current) {
135
- return path6.resolve(startDir);
196
+ return path2.resolve(startDir);
136
197
  }
137
198
  current = parent;
138
199
  }
139
200
  }
140
- function resolveFeaturesDir(projectRoot) {
141
- return path6.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 (!fs12.existsSync(featuresDir)) return [];
146
210
  try {
147
- const entries = fs7.readdirSync(featuresDir, { withFileTypes: true });
211
+ const entries = fs12.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 = path6.join(resolveFeaturesDir(projectRoot), feat, "domain");
158
- if (!fs7.existsSync(domainDir)) continue;
221
+ const domainDir = path2.join(resolveFeaturesDir(projectRoot), feat, "domain");
222
+ if (!fs12.existsSync(domainDir)) continue;
159
223
  try {
160
- const files = fs7.readdirSync(domainDir);
224
+ const files = fs12.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 = path6.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 = fs12.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 = path6.join(resolveFeaturesDir(projectRoot), feat, "domain");
196
- if (!fs7.existsSync(domainDir)) continue;
259
+ const domainDir = path2.join(resolveFeaturesDir(projectRoot), feat, "domain");
260
+ if (!fs12.existsSync(domainDir)) continue;
197
261
  try {
198
- const files = fs7.readdirSync(domainDir);
262
+ const files = fs12.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: path6.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 = path6.join(resolveFeaturesDir(projectRoot), feat, "useCases");
221
- if (!fs7.existsSync(useCasesDir)) continue;
284
+ const useCasesDir = path2.join(resolveFeaturesDir(projectRoot), feat, "useCases");
285
+ if (!fs12.existsSync(useCasesDir)) continue;
222
286
  try {
223
- const files = fs7.readdirSync(useCasesDir);
287
+ const files = fs12.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: path6.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 && fs12.existsSync(cachedTemplatesDir)) {
256
320
  return cachedTemplatesDir;
257
321
  }
258
- const currentDir = typeof __dirname !== "undefined" ? __dirname : path6.dirname(fileURLToPath(import.meta.url));
259
- const cand1 = path6.resolve(currentDir, "../../templates");
260
- if (fs7.existsSync(cand1)) {
322
+ const currentDir = typeof __dirname !== "undefined" ? __dirname : path2.dirname(fileURLToPath(import.meta.url));
323
+ const cand1 = path2.resolve(currentDir, "../../templates");
324
+ if (fs12.existsSync(cand1)) {
261
325
  cachedTemplatesDir = cand1;
262
326
  return cand1;
263
327
  }
264
- const cand2 = path6.resolve(currentDir, "../templates");
265
- if (fs7.existsSync(cand2)) {
328
+ const cand2 = path2.resolve(currentDir, "../templates");
329
+ if (fs12.existsSync(cand2)) {
266
330
  cachedTemplatesDir = cand2;
267
331
  return cand2;
268
332
  }
269
- const cand3 = path6.resolve(currentDir, "templates");
270
- if (fs7.existsSync(cand3)) {
333
+ const cand3 = path2.resolve(currentDir, "templates");
334
+ if (fs12.existsSync(cand3)) {
271
335
  cachedTemplatesDir = cand3;
272
336
  return cand3;
273
337
  }
274
338
  let curr = currentDir;
275
339
  while (true) {
276
- const candTemplates = path6.join(curr, "templates");
277
- if (fs7.existsSync(candTemplates)) {
340
+ const candTemplates = path2.join(curr, "templates");
341
+ if (fs12.existsSync(candTemplates)) {
278
342
  cachedTemplatesDir = candTemplates;
279
343
  return candTemplates;
280
344
  }
281
- const parent = path6.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 = path6.isAbsolute(relativePath) ? relativePath : path6.join(getTemplatesDir(), relativePath);
290
- return fs7.readFileSync(fullPath, "utf-8");
353
+ const fullPath = path2.isAbsolute(relativePath) ? relativePath : path2.join(getTemplatesDir(), relativePath);
354
+ return fs12.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 || path6.join(resolveFeaturesDir(projectRoot), options.feature, "domain");
378
- const filePath = path6.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((p17) => `${p17.name}: ${p17.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 || path6.join(resolveFeaturesDir(projectRoot), options.feature, "domain");
428
- const filePath = path6.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 && fs12.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 = path6.join(resolveFeaturesDir(projectRoot), feat, "domain");
674
- if (!fs7.existsSync(domainDir)) continue;
737
+ const domainDir = path2.join(resolveFeaturesDir(projectRoot), feat, "domain");
738
+ if (!fs12.existsSync(domainDir)) continue;
675
739
  for (const cand of candidateNames) {
676
- const p1 = path6.join(domainDir, `${cand}.ts`);
677
- if (fs7.existsSync(p1)) {
740
+ const p1 = path2.join(domainDir, `${cand}.ts`);
741
+ if (fs12.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 = path6.join(projectRoot, "domain", `${cand}.ts`);
684
- if (fs7.existsSync(p1)) return { filePath: p1, ifaceName: cand };
685
- const p22 = path6.join(projectRoot, "src", "domain", `${cand}.ts`);
686
- if (fs7.existsSync(p22)) return { filePath: p22, ifaceName: cand };
687
- const p32 = path6.join(projectRoot, "interfaces", `${cand}.ts`);
688
- if (fs7.existsSync(p32)) return { filePath: p32, ifaceName: cand };
747
+ const p1 = path2.join(projectRoot, "domain", `${cand}.ts`);
748
+ if (fs12.existsSync(p1)) return { filePath: p1, ifaceName: cand };
749
+ const p22 = path2.join(projectRoot, "src", "domain", `${cand}.ts`);
750
+ if (fs12.existsSync(p22)) return { filePath: p22, ifaceName: cand };
751
+ const p32 = path2.join(projectRoot, "interfaces", `${cand}.ts`);
752
+ if (fs12.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 = fs12.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 ? `${path6.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 ? `${path6.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 ? path6.join(resolveFeaturesDir(projectRoot), options.feature, "infrastructure") : path6.join(projectRoot, "infrastructure"));
882
- const filePath = path6.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 = path6.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 = path6.resolve(path6.dirname(ifaceFilePath), imp.specifier);
910
- let rel = path6.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 p17 of [...inputProps, ...outputProps]) {
1048
+ const rawType = p17.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((p17) => ({
1055
+ name: p17.name,
1056
+ optional: p17.optional,
1057
+ type: p17.optional && !p17.type.includes("undefined") ? `${p17.type} | undefined` : p17.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((p17) => ({
1060
+ name: p17.name,
1061
+ optional: p17.optional,
1062
+ type: p17.optional && !p17.type.includes("undefined") ? `${p17.type} | undefined` : p17.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 || path6.join(resolveFeaturesDir(projectRoot), options.feature, "useCases");
1098
- const filePath = path6.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 = path6.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 = path6.join(resolveFeaturesDir(projectRoot), options.feature, "pxExpress");
1183
- const handlersDir = path6.join(pxDir, "handlers");
1184
- const pxIndexPath = path6.join(pxDir, "index.ts");
1185
- if (!fs7.existsSync(pxIndexPath)) {
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 (!fs12.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 = path6.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 = path6.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 (fs12.existsSync(providerPath) && !options.overwrite) {
1287
+ let content = fs12.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 || path6.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: path6.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: path6.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: path6.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: path6.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: path6.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: path6.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: path6.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: path6.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: path6.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: path6.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 = path6.join(targetDir, "pxExpress");
1640
- const pxIndexPath = path6.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(
@@ -1666,23 +1730,23 @@ var PromptCancelledError = class extends Error {
1666
1730
  }
1667
1731
  };
1668
1732
  function handleCancel(result) {
1669
- if (p3.isCancel(result)) {
1670
- p3.cancel("Operation cancelled.");
1733
+ if (p15.isCancel(result)) {
1734
+ p15.cancel("Operation cancelled.");
1671
1735
  throw new PromptCancelledError();
1672
1736
  }
1673
1737
  return result;
1674
1738
  }
1675
1739
  function printBanner() {
1676
1740
  console.log("");
1677
- p3.intro(pc13.bgCyan(pc13.black(" @solid-stack/mason ")));
1741
+ p15.intro(pc15.bgCyan(pc15.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((p17) => `${p17.name}: ${p17.type}${p17.optional ? "?" : ""}`).join(", ");
1747
+ p15.log.info(pc15.dim(`Pre-configured fields: ${summary}`));
1684
1748
  const addMore = handleCancel(
1685
- await p3.confirm({
1749
+ await p15.confirm({
1686
1750
  message: "Would you like to add custom fields/properties?",
1687
1751
  initialValue: false
1688
1752
  })
@@ -1691,10 +1755,10 @@ 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
+ p15.log.info(pc15.cyan("Enter properties (leave name blank when finished):"));
1695
1759
  while (true) {
1696
1760
  const name = handleCancel(
1697
- await p3.text({
1761
+ await p15.text({
1698
1762
  message: "Property name (or press Enter to finish):",
1699
1763
  placeholder: "e.g. title, email, status"
1700
1764
  })
@@ -1703,7 +1767,7 @@ async function promptProperties(options = {}) {
1703
1767
  break;
1704
1768
  }
1705
1769
  const type = handleCancel(
1706
- await p3.select({
1770
+ await p15.select({
1707
1771
  message: `Type for property "${name.trim()}":`,
1708
1772
  options: [
1709
1773
  { value: "string", label: "string" },
@@ -1720,7 +1784,7 @@ async function promptProperties(options = {}) {
1720
1784
  let finalType = type;
1721
1785
  if (type === "custom") {
1722
1786
  finalType = handleCancel(
1723
- await p3.text({
1787
+ await p15.text({
1724
1788
  message: `Enter custom type for "${name.trim()}":`,
1725
1789
  placeholder: "e.g. UserRole, string | null",
1726
1790
  validate: (val) => val && val.trim() ? void 0 : "Type cannot be empty"
@@ -1728,7 +1792,7 @@ async function promptProperties(options = {}) {
1728
1792
  );
1729
1793
  }
1730
1794
  const optional = handleCancel(
1731
- await p3.confirm({
1795
+ await p15.confirm({
1732
1796
  message: `Is "${name.trim()}" optional?`,
1733
1797
  initialValue: false
1734
1798
  })
@@ -1738,8 +1802,8 @@ async function promptProperties(options = {}) {
1738
1802
  type: finalType.trim(),
1739
1803
  optional
1740
1804
  });
1741
- p3.log.success(
1742
- pc13.green(`Added: ${name.trim()}${optional ? "?" : ""}: ${finalType.trim()}`)
1805
+ p15.log.success(
1806
+ pc15.green(`Added: ${name.trim()}${optional ? "?" : ""}: ${finalType.trim()}`)
1743
1807
  );
1744
1808
  }
1745
1809
  return properties;
@@ -1748,13 +1812,14 @@ 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";
1754
1819
  }
1755
1820
  if (!name) {
1756
1821
  name = handleCancel(
1757
- await p3.text({
1822
+ await p15.text({
1758
1823
  message: "What is the feature name?",
1759
1824
  placeholder: "e.g. users, auth, orders, billing",
1760
1825
  validate: (val) => {
@@ -1765,18 +1830,19 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
1765
1830
  ).trim();
1766
1831
  }
1767
1832
  const normalizedName = toCamelCase(name);
1768
- const defaultDir = path6.relative(projectRoot, path6.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
- await p3.text({
1838
+ await p15.text({
1773
1839
  message: "Target directory for the feature:",
1774
1840
  initialValue: defaultDir,
1775
1841
  defaultValue: defaultDir
1776
1842
  })
1777
1843
  ).trim();
1778
1844
  }
1779
- const targetDir = flags.target ? path6.resolve(projectRoot, flags.target) : path6.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;
@@ -1791,14 +1857,14 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
1791
1857
  const customUseCases = [];
1792
1858
  if (!flags.yes) {
1793
1859
  includeExpress = handleCancel(
1794
- await p3.confirm({
1860
+ await p15.confirm({
1795
1861
  message: "Include Express Presentation layer (pxExpress routes & handlers)?",
1796
1862
  initialValue: true
1797
1863
  })
1798
1864
  );
1799
1865
  if (includeExpress) {
1800
1866
  routePath = handleCancel(
1801
- await p3.text({
1867
+ await p15.text({
1802
1868
  message: "Express route prefix:",
1803
1869
  initialValue: `/${normalizedName}`,
1804
1870
  defaultValue: `/${normalizedName}`
@@ -1806,14 +1872,14 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
1806
1872
  ).trim();
1807
1873
  }
1808
1874
  includeDomain = handleCancel(
1809
- await p3.confirm({
1875
+ await p15.confirm({
1810
1876
  message: "Include Domain Entity/Entities?",
1811
1877
  initialValue: true
1812
1878
  })
1813
1879
  );
1814
1880
  if (includeDomain) {
1815
1881
  const domainInput = handleCancel(
1816
- await p3.text({
1882
+ await p15.text({
1817
1883
  message: "Domain entity name(s) (comma-separated):",
1818
1884
  initialValue: defaultEntity,
1819
1885
  defaultValue: defaultEntity,
@@ -1832,21 +1898,21 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
1832
1898
  entityNames = [defaultEntity];
1833
1899
  }
1834
1900
  includeInterface = handleCancel(
1835
- await p3.confirm({
1901
+ await p15.confirm({
1836
1902
  message: `Include Domain Repository interface(s) for ${entityNames.join(", ")}?`,
1837
1903
  initialValue: true
1838
1904
  })
1839
1905
  );
1840
1906
  if (includeInterface) {
1841
1907
  includeInfra = handleCancel(
1842
- await p3.confirm({
1908
+ await p15.confirm({
1843
1909
  message: `Include Infrastructure Stub implementation(s) for ${entityNames.join(", ")}?`,
1844
1910
  initialValue: true
1845
1911
  })
1846
1912
  );
1847
1913
  }
1848
1914
  includeUseCase = handleCancel(
1849
- await p3.confirm({
1915
+ await p15.confirm({
1850
1916
  message: "Include Domain Use Cases?",
1851
1917
  initialValue: true
1852
1918
  })
@@ -1860,7 +1926,7 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
1860
1926
  { value: "delete", label: "Delete (Delete<Entity>)", hint: "DELETE /:id" }
1861
1927
  ];
1862
1928
  crudOperations = handleCancel(
1863
- await p3.multiselect({
1929
+ await p15.multiselect({
1864
1930
  message: "Select standard CRUD use cases to generate:",
1865
1931
  options: crudChoices,
1866
1932
  initialValues: ["create", "read", "list", "update", "delete"],
@@ -1868,13 +1934,13 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
1868
1934
  })
1869
1935
  );
1870
1936
  includeTests = handleCancel(
1871
- await p3.confirm({
1937
+ await p15.confirm({
1872
1938
  message: "Generate Vitest unit tests for use cases?",
1873
1939
  initialValue: true
1874
1940
  })
1875
1941
  );
1876
1942
  const wantCustom = handleCancel(
1877
- await p3.confirm({
1943
+ await p15.confirm({
1878
1944
  message: "Do you want to add any custom use cases to this feature?",
1879
1945
  initialValue: false
1880
1946
  })
@@ -1884,7 +1950,7 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
1884
1950
  const availableIfaces = entityNames.map((e) => `I${e}Repository`);
1885
1951
  while (addMore) {
1886
1952
  const customName = handleCancel(
1887
- await p3.text({
1953
+ await p15.text({
1888
1954
  message: "Custom Use Case name:",
1889
1955
  placeholder: "e.g. ResetPassword, VerifyEmail, AssignRole",
1890
1956
  validate: (v) => v && v.trim() ? void 0 : "Use Case name cannot be empty"
@@ -1894,7 +1960,7 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
1894
1960
  let selectedDeps = [];
1895
1961
  if (availableIfaces.length > 0) {
1896
1962
  selectedDeps = handleCancel(
1897
- await p3.multiselect({
1963
+ await p15.multiselect({
1898
1964
  message: `Select domain interfaces to inject into ${customUcName}:`,
1899
1965
  options: availableIfaces.map((i) => ({ value: i, label: i })),
1900
1966
  required: false
@@ -1903,7 +1969,7 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
1903
1969
  }
1904
1970
  let customInputProps = [];
1905
1971
  const hasInput = handleCancel(
1906
- await p3.confirm({
1972
+ await p15.confirm({
1907
1973
  message: `Does ${customUcName} accept input arguments?`,
1908
1974
  initialValue: true
1909
1975
  })
@@ -1914,7 +1980,7 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
1914
1980
  }
1915
1981
  let customOutputProps = [];
1916
1982
  const hasCustomOutput = handleCancel(
1917
- await p3.confirm({
1983
+ await p15.confirm({
1918
1984
  message: `Does ${customUcName} return custom output data?`,
1919
1985
  initialValue: false
1920
1986
  })
@@ -1924,7 +1990,7 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
1924
1990
  });
1925
1991
  }
1926
1992
  const customIncludeTest = handleCancel(
1927
- await p3.confirm({
1993
+ await p15.confirm({
1928
1994
  message: `Generate Vitest unit test for ${customUcName}?`,
1929
1995
  initialValue: true
1930
1996
  })
@@ -1934,14 +2000,14 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
1934
2000
  let customHttpPath = `/${toKebabCase(customUcName)}`;
1935
2001
  if (includeExpress) {
1936
2002
  customCreateHttp = handleCancel(
1937
- await p3.confirm({
2003
+ await p15.confirm({
1938
2004
  message: `Generate a matching ExpressRoute HTTP handler for ${customUcName}?`,
1939
2005
  initialValue: true
1940
2006
  })
1941
2007
  );
1942
2008
  if (customCreateHttp) {
1943
2009
  customHttpMethod = handleCancel(
1944
- await p3.select({
2010
+ await p15.select({
1945
2011
  message: "HTTP method for route:",
1946
2012
  options: [
1947
2013
  { value: "post", label: "POST" },
@@ -1954,7 +2020,7 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
1954
2020
  })
1955
2021
  );
1956
2022
  customHttpPath = handleCancel(
1957
- await p3.text({
2023
+ await p15.text({
1958
2024
  message: "Route subpath (relative to feature prefix):",
1959
2025
  initialValue: `/${toKebabCase(customUcName)}`,
1960
2026
  defaultValue: `/${toKebabCase(customUcName)}`
@@ -1975,7 +2041,7 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
1975
2041
  validationFields: customInputProps
1976
2042
  });
1977
2043
  addMore = handleCancel(
1978
- await p3.confirm({
2044
+ await p15.confirm({
1979
2045
  message: "Add another custom use case?",
1980
2046
  initialValue: false
1981
2047
  })
@@ -1984,7 +2050,7 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
1984
2050
  }
1985
2051
  }
1986
2052
  includeDiProvider = handleCancel(
1987
- await p3.confirm({
2053
+ await p15.confirm({
1988
2054
  message: "Generate / update feature DI provider (diProvider.ts)?",
1989
2055
  initialValue: true
1990
2056
  })
@@ -1996,8 +2062,8 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
1996
2062
  entityNames = [defaultEntity];
1997
2063
  }
1998
2064
  }
1999
- const s = p3.spinner();
2000
- s.start(`Generating feature '${pc13.cyan(normalizedName)}'...`);
2065
+ const s = p15.spinner();
2066
+ s.start(`Generating feature '${pc15.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 (!fs12.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
+ p15.outro(
2113
+ pc15.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 p15.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 p15.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 p15.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 p15.confirm({
2263
+ message: "Include stub implementation in infrastructure/?",
2264
+ initialValue: true
2265
+ })
2266
+ );
2267
+ }
2268
+ const s = p15.spinner();
2269
+ s.start(`Generating shared module '${pc15.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
+ p15.outro(
2283
+ pc15.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();
@@ -2030,14 +2294,14 @@ async function runUseCaseCommand(useCaseNameArg, flags = {}) {
2030
2294
  const featureOptions = existingFeatures.map((f) => ({ value: f, label: f }));
2031
2295
  featureOptions.push({ value: "__custom__", label: "\u270F\uFE0F Enter custom feature name..." });
2032
2296
  const selected = handleCancel(
2033
- await p3.select({
2297
+ await p15.select({
2034
2298
  message: "Select target feature:",
2035
2299
  options: featureOptions
2036
2300
  })
2037
2301
  );
2038
2302
  if (selected === "__custom__") {
2039
2303
  feature = handleCancel(
2040
- await p3.text({
2304
+ await p15.text({
2041
2305
  message: "Enter feature name:",
2042
2306
  validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
2043
2307
  })
@@ -2047,7 +2311,7 @@ async function runUseCaseCommand(useCaseNameArg, flags = {}) {
2047
2311
  }
2048
2312
  } else {
2049
2313
  feature = handleCancel(
2050
- await p3.text({
2314
+ await p15.text({
2051
2315
  message: "Enter target feature name:",
2052
2316
  validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
2053
2317
  })
@@ -2058,7 +2322,7 @@ async function runUseCaseCommand(useCaseNameArg, flags = {}) {
2058
2322
  let name = useCaseNameArg?.trim();
2059
2323
  if (!name) {
2060
2324
  name = handleCancel(
2061
- await p3.text({
2325
+ await p15.text({
2062
2326
  message: "Use Case name:",
2063
2327
  placeholder: "e.g. CreateUser, GetUser, CancelOrder",
2064
2328
  validate: (v) => v && v.trim() ? void 0 : "Use Case name cannot be empty"
@@ -2074,7 +2338,7 @@ async function runUseCaseCommand(useCaseNameArg, flags = {}) {
2074
2338
  label: i.name
2075
2339
  }));
2076
2340
  selectedDeps = handleCancel(
2077
- await p3.multiselect({
2341
+ await p15.multiselect({
2078
2342
  message: "Select domain interfaces to inject into use case:",
2079
2343
  options: multiOptions,
2080
2344
  required: false
@@ -2089,7 +2353,7 @@ async function runUseCaseCommand(useCaseNameArg, flags = {}) {
2089
2353
  let httpPath = "/";
2090
2354
  if (!flags.yes) {
2091
2355
  const hasInput = handleCancel(
2092
- await p3.confirm({
2356
+ await p15.confirm({
2093
2357
  message: "Does this use case accept input arguments?",
2094
2358
  initialValue: true
2095
2359
  })
@@ -2099,7 +2363,7 @@ async function runUseCaseCommand(useCaseNameArg, flags = {}) {
2099
2363
  });
2100
2364
  }
2101
2365
  const hasCustomOutput = handleCancel(
2102
- await p3.confirm({
2366
+ await p15.confirm({
2103
2367
  message: "Does this use case return custom output data?",
2104
2368
  initialValue: false
2105
2369
  })
@@ -2109,20 +2373,20 @@ async function runUseCaseCommand(useCaseNameArg, flags = {}) {
2109
2373
  });
2110
2374
  }
2111
2375
  includeTest = handleCancel(
2112
- await p3.confirm({
2376
+ await p15.confirm({
2113
2377
  message: "Generate Vitest unit test skeleton (.test.ts)?",
2114
2378
  initialValue: true
2115
2379
  })
2116
2380
  );
2117
2381
  createHttp = handleCancel(
2118
- await p3.confirm({
2382
+ await p15.confirm({
2119
2383
  message: "Generate a matching ExpressRoute HTTP handler?",
2120
2384
  initialValue: false
2121
2385
  })
2122
2386
  );
2123
2387
  if (createHttp) {
2124
2388
  httpMethod = handleCancel(
2125
- await p3.select({
2389
+ await p15.select({
2126
2390
  message: "HTTP method for route:",
2127
2391
  options: [
2128
2392
  { value: "post", label: "POST" },
@@ -2135,7 +2399,7 @@ async function runUseCaseCommand(useCaseNameArg, flags = {}) {
2135
2399
  })
2136
2400
  );
2137
2401
  httpPath = handleCancel(
2138
- await p3.text({
2402
+ await p15.text({
2139
2403
  message: "Route subpath (relative to feature prefix):",
2140
2404
  initialValue: "/",
2141
2405
  defaultValue: "/"
@@ -2143,8 +2407,8 @@ async function runUseCaseCommand(useCaseNameArg, flags = {}) {
2143
2407
  ).trim();
2144
2408
  }
2145
2409
  }
2146
- const s = p3.spinner();
2147
- s.start(`Generating use case '${pc13.cyan(ucName)}'...`);
2410
+ const s = p15.spinner();
2411
+ s.start(`Generating use case '${pc15.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
+ p15.outro(pc15.green(`Use case '${ucName}' created in feature '${feature}'!`));
2180
2444
  }
2181
2445
  async function runDomainCommand(domainNameArg, flags = {}) {
2182
2446
  const projectRoot = findProjectRoot();
@@ -2187,14 +2451,14 @@ async function runDomainCommand(domainNameArg, flags = {}) {
2187
2451
  const featureOptions = existingFeatures.map((f) => ({ value: f, label: f }));
2188
2452
  featureOptions.push({ value: "__custom__", label: "\u270F\uFE0F Enter custom feature name..." });
2189
2453
  const selected = handleCancel(
2190
- await p3.select({
2454
+ await p15.select({
2191
2455
  message: "Select target feature:",
2192
2456
  options: featureOptions
2193
2457
  })
2194
2458
  );
2195
2459
  if (selected === "__custom__") {
2196
2460
  feature = handleCancel(
2197
- await p3.text({
2461
+ await p15.text({
2198
2462
  message: "Enter feature name:",
2199
2463
  validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
2200
2464
  })
@@ -2204,7 +2468,7 @@ async function runDomainCommand(domainNameArg, flags = {}) {
2204
2468
  }
2205
2469
  } else {
2206
2470
  feature = handleCancel(
2207
- await p3.text({
2471
+ await p15.text({
2208
2472
  message: "Enter target feature name:",
2209
2473
  validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
2210
2474
  })
@@ -2215,7 +2479,7 @@ async function runDomainCommand(domainNameArg, flags = {}) {
2215
2479
  let name = domainNameArg?.trim();
2216
2480
  if (!name) {
2217
2481
  name = handleCancel(
2218
- await p3.text({
2482
+ await p15.text({
2219
2483
  message: "Domain model / entity name:",
2220
2484
  placeholder: "e.g. User, Order, Product, Address",
2221
2485
  validate: (v) => v && v.trim() ? void 0 : "Domain name cannot be empty"
@@ -2227,7 +2491,7 @@ async function runDomainCommand(domainNameArg, flags = {}) {
2227
2491
  let properties = [];
2228
2492
  if (!flags.yes) {
2229
2493
  kind = handleCancel(
2230
- await p3.select({
2494
+ await p15.select({
2231
2495
  message: "Select domain model type:",
2232
2496
  options: [
2233
2497
  { value: "interface", label: "TypeScript Interface (pure data structure)" },
@@ -2246,8 +2510,8 @@ async function runDomainCommand(domainNameArg, flags = {}) {
2246
2510
  defaults: kind === "valueObject" ? [{ name: "value", type: "string" }] : defaultProps
2247
2511
  });
2248
2512
  }
2249
- const s = p3.spinner();
2250
- s.start(`Generating domain model '${pc13.cyan(entityName)}'...`);
2513
+ const s = p15.spinner();
2514
+ s.start(`Generating domain model '${pc15.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
+ p15.outro(pc15.green(`Domain model '${entityName}' created in feature '${feature}'!`));
2264
2528
  }
2265
2529
  async function runInfrastructureCommand(classNameArg, flags = {}) {
2266
2530
  const projectRoot = findProjectRoot();
@@ -2269,7 +2533,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
2269
2533
  let feature = flags.feature?.trim();
2270
2534
  if (!flags.yes && !flags.global && !flags.feature) {
2271
2535
  scope = handleCancel(
2272
- await p3.select({
2536
+ await p15.select({
2273
2537
  message: "Where should this infrastructure implementation be placed?",
2274
2538
  options: [
2275
2539
  { value: "feature", label: "Feature-level (features/<feature>/infrastructure)" },
@@ -2284,14 +2548,14 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
2284
2548
  const featureOptions = existingFeatures.map((f) => ({ value: f, label: f }));
2285
2549
  featureOptions.push({ value: "__custom__", label: "\u270F\uFE0F Enter custom feature name..." });
2286
2550
  const selected = handleCancel(
2287
- await p3.select({
2551
+ await p15.select({
2288
2552
  message: "Select target feature:",
2289
2553
  options: featureOptions
2290
2554
  })
2291
2555
  );
2292
2556
  if (selected === "__custom__") {
2293
2557
  feature = handleCancel(
2294
- await p3.text({
2558
+ await p15.text({
2295
2559
  message: "Enter feature name:",
2296
2560
  validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
2297
2561
  })
@@ -2301,7 +2565,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
2301
2565
  }
2302
2566
  } else {
2303
2567
  feature = handleCancel(
2304
- await p3.text({
2568
+ await p15.text({
2305
2569
  message: "Enter target feature name:",
2306
2570
  validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
2307
2571
  })
@@ -2314,7 +2578,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
2314
2578
  let name = classNameArg?.trim();
2315
2579
  if (!name) {
2316
2580
  name = handleCancel(
2317
- await p3.text({
2581
+ await p15.text({
2318
2582
  message: "Infrastructure class name:",
2319
2583
  placeholder: "e.g. StubUserRepository, PostgresUserRepository, S3FileStorage",
2320
2584
  validate: (v) => v && v.trim() ? void 0 : "Class name cannot be empty"
@@ -2333,7 +2597,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
2333
2597
  (i) => className.toLowerCase().includes(removeInterfacePrefix(i.name).toLowerCase())
2334
2598
  );
2335
2599
  const ifaceChoice = handleCancel(
2336
- await p3.select({
2600
+ await p15.select({
2337
2601
  message: "Does this class implement a domain interface or abstract class?",
2338
2602
  options: [
2339
2603
  ...interfaces.map((i) => ({ value: i.name, label: i.name })),
@@ -2345,7 +2609,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
2345
2609
  );
2346
2610
  if (ifaceChoice === "__custom__") {
2347
2611
  implementsInterface = handleCancel(
2348
- await p3.text({
2612
+ await p15.text({
2349
2613
  message: "Enter interface or abstract class name:",
2350
2614
  placeholder: "e.g. IUserRepository, BaseStorageAdapter",
2351
2615
  validate: (v) => v && v.trim() ? void 0 : "Name cannot be empty"
@@ -2356,14 +2620,14 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
2356
2620
  }
2357
2621
  } else {
2358
2622
  const wantImpl = handleCancel(
2359
- await p3.confirm({
2623
+ await p15.confirm({
2360
2624
  message: "Does this class implement a domain interface or abstract class?",
2361
2625
  initialValue: false
2362
2626
  })
2363
2627
  );
2364
2628
  if (wantImpl) {
2365
2629
  implementsInterface = handleCancel(
2366
- await p3.text({
2630
+ await p15.text({
2367
2631
  message: "Enter interface or abstract class name:",
2368
2632
  placeholder: "e.g. IUserRepository, BaseStorageAdapter",
2369
2633
  validate: (v) => v && v.trim() ? void 0 : "Name cannot be empty"
@@ -2383,7 +2647,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
2383
2647
  const allInterfaces = listDomainInterfaces(projectRoot);
2384
2648
  if (allInterfaces.length > 0) {
2385
2649
  const ifaceChoice = handleCancel(
2386
- await p3.select({
2650
+ await p15.select({
2387
2651
  message: "Does this class implement an interface or abstract class?",
2388
2652
  options: [
2389
2653
  ...allInterfaces.map((i) => ({ value: i.name, label: `${i.name} (${i.feature})` })),
@@ -2395,7 +2659,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
2395
2659
  );
2396
2660
  if (ifaceChoice === "__custom__") {
2397
2661
  implementsInterface = handleCancel(
2398
- await p3.text({
2662
+ await p15.text({
2399
2663
  message: "Enter interface or abstract class name:",
2400
2664
  placeholder: "e.g. IAuthRepository",
2401
2665
  validate: (v) => v && v.trim() ? void 0 : "Name cannot be empty"
@@ -2408,7 +2672,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
2408
2672
  }
2409
2673
  if (!flags.yes && !flags.kind) {
2410
2674
  kind = handleCancel(
2411
- await p3.select({
2675
+ await p15.select({
2412
2676
  message: "Select implementation kind:",
2413
2677
  options: [
2414
2678
  { value: "stub", label: "In-Memory Stub (with in-memory store and test helper methods)" },
@@ -2419,7 +2683,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
2419
2683
  );
2420
2684
  if (scope === "feature" && feature && implementsInterface) {
2421
2685
  registerInProvider = handleCancel(
2422
- await p3.confirm({
2686
+ await p15.confirm({
2423
2687
  message: "Register this implementation in feature diProvider.ts?",
2424
2688
  initialValue: true
2425
2689
  })
@@ -2428,8 +2692,8 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
2428
2692
  } else if (flags.yes && scope === "feature" && feature && implementsInterface) {
2429
2693
  registerInProvider = !flags.noProvider;
2430
2694
  }
2431
- const s = p3.spinner();
2432
- s.start(`Generating infrastructure class '${pc13.cyan(className)}'...`);
2695
+ const s = p15.spinner();
2696
+ s.start(`Generating infrastructure class '${pc15.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 = path6.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
+ p15.outro(pc15.green(`Infrastructure class '${className}' created!`));
2468
2732
  }
2469
2733
  async function runInterfaceCommand(interfaceNameArg, flags = {}) {
2470
2734
  const projectRoot = findProjectRoot();
@@ -2475,14 +2739,14 @@ async function runInterfaceCommand(interfaceNameArg, flags = {}) {
2475
2739
  const featureOptions = existingFeatures.map((f) => ({ value: f, label: f }));
2476
2740
  featureOptions.push({ value: "__custom__", label: "\u270F\uFE0F Enter custom feature name..." });
2477
2741
  const selected = handleCancel(
2478
- await p3.select({
2742
+ await p15.select({
2479
2743
  message: "Select target feature:",
2480
2744
  options: featureOptions
2481
2745
  })
2482
2746
  );
2483
2747
  if (selected === "__custom__") {
2484
2748
  feature = handleCancel(
2485
- await p3.text({
2749
+ await p15.text({
2486
2750
  message: "Enter feature name:",
2487
2751
  validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
2488
2752
  })
@@ -2492,7 +2756,7 @@ async function runInterfaceCommand(interfaceNameArg, flags = {}) {
2492
2756
  }
2493
2757
  } else {
2494
2758
  feature = handleCancel(
2495
- await p3.text({
2759
+ await p15.text({
2496
2760
  message: "Enter target feature name:",
2497
2761
  validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
2498
2762
  })
@@ -2503,7 +2767,7 @@ async function runInterfaceCommand(interfaceNameArg, flags = {}) {
2503
2767
  let name = interfaceNameArg?.trim();
2504
2768
  if (!name) {
2505
2769
  name = handleCancel(
2506
- await p3.text({
2770
+ await p15.text({
2507
2771
  message: "Domain interface name:",
2508
2772
  placeholder: "e.g. IUserRepository, IPaymentService",
2509
2773
  validate: (v) => v && v.trim() ? void 0 : "Interface name cannot be empty"
@@ -2521,7 +2785,7 @@ async function runInterfaceCommand(interfaceNameArg, flags = {}) {
2521
2785
  if (!flags.yes) {
2522
2786
  if (existingEntities.length > 0) {
2523
2787
  const selectedEntity = handleCancel(
2524
- await p3.select({
2788
+ await p15.select({
2525
2789
  message: "Select domain entity associated with this interface:",
2526
2790
  options: [
2527
2791
  ...existingEntities.map((e) => ({ value: e.name, label: e.name })),
@@ -2532,7 +2796,7 @@ async function runInterfaceCommand(interfaceNameArg, flags = {}) {
2532
2796
  );
2533
2797
  if (selectedEntity === "__custom__") {
2534
2798
  entityName = handleCancel(
2535
- await p3.text({
2799
+ await p15.text({
2536
2800
  message: "Enter entity name:",
2537
2801
  defaultValue: removeInterfacePrefix(ifaceName).replace(/Repository$|Service$/, "")
2538
2802
  })
@@ -2542,7 +2806,7 @@ async function runInterfaceCommand(interfaceNameArg, flags = {}) {
2542
2806
  }
2543
2807
  }
2544
2808
  kind = handleCancel(
2545
- await p3.select({
2809
+ await p15.select({
2546
2810
  message: "Interface definition style:",
2547
2811
  options: [
2548
2812
  { value: "abstractClass", label: "Abstract Class (Recommended - acts as DI token & type)" },
@@ -2552,7 +2816,7 @@ async function runInterfaceCommand(interfaceNameArg, flags = {}) {
2552
2816
  })
2553
2817
  );
2554
2818
  methodsPreset = handleCancel(
2555
- await p3.select({
2819
+ await p15.select({
2556
2820
  message: "Method signature preset:",
2557
2821
  options: [
2558
2822
  { value: "crud", label: "Standard CRUD (GetById, List, Create, Update, Delete)" },
@@ -2562,22 +2826,22 @@ async function runInterfaceCommand(interfaceNameArg, flags = {}) {
2562
2826
  })
2563
2827
  );
2564
2828
  generateStub = handleCancel(
2565
- await p3.confirm({
2829
+ await p15.confirm({
2566
2830
  message: "Auto-generate Infrastructure Stub implementation?",
2567
2831
  initialValue: true
2568
2832
  })
2569
2833
  );
2570
2834
  if (generateStub) {
2571
2835
  registerInProvider = handleCancel(
2572
- await p3.confirm({
2836
+ await p15.confirm({
2573
2837
  message: "Auto-register interface & stub in feature diProvider.ts?",
2574
2838
  initialValue: true
2575
2839
  })
2576
2840
  );
2577
2841
  }
2578
2842
  }
2579
- const s = p3.spinner();
2580
- s.start(`Generating interface '${pc13.cyan(ifaceName)}'...`);
2843
+ const s = p15.spinner();
2844
+ s.start(`Generating interface '${pc15.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 = path6.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
+ p15.outro(pc15.green(`Interface '${ifaceName}' created in feature '${feature}'!`));
2630
2894
  }
2631
2895
  async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
2632
2896
  const projectRoot = findProjectRoot();
@@ -2637,14 +2901,14 @@ async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
2637
2901
  const featureOptions = existingFeatures.map((f) => ({ value: f, label: f }));
2638
2902
  featureOptions.push({ value: "__custom__", label: "\u270F\uFE0F Enter custom feature name..." });
2639
2903
  const selected = handleCancel(
2640
- await p3.select({
2904
+ await p15.select({
2641
2905
  message: "Select target feature:",
2642
2906
  options: featureOptions
2643
2907
  })
2644
2908
  );
2645
2909
  if (selected === "__custom__") {
2646
2910
  feature = handleCancel(
2647
- await p3.text({
2911
+ await p15.text({
2648
2912
  message: "Enter feature name:",
2649
2913
  validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
2650
2914
  })
@@ -2654,7 +2918,7 @@ async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
2654
2918
  }
2655
2919
  } else {
2656
2920
  feature = handleCancel(
2657
- await p3.text({
2921
+ await p15.text({
2658
2922
  message: "Enter target feature name:",
2659
2923
  validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
2660
2924
  })
@@ -2665,7 +2929,7 @@ async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
2665
2929
  let name = handlerNameArg?.trim();
2666
2930
  if (!name) {
2667
2931
  name = handleCancel(
2668
- await p3.text({
2932
+ await p15.text({
2669
2933
  message: "HTTP Route Handler class name:",
2670
2934
  placeholder: "e.g. CreateUserHttp, GetUserHttp, HealthHttp",
2671
2935
  validate: (v) => v && v.trim() ? void 0 : "Handler name cannot be empty"
@@ -2684,7 +2948,7 @@ async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
2684
2948
  let validationFields = [];
2685
2949
  if (!flags.yes) {
2686
2950
  method = handleCancel(
2687
- await p3.select({
2951
+ await p15.select({
2688
2952
  message: "HTTP Method:",
2689
2953
  options: [
2690
2954
  { value: "get", label: "GET" },
@@ -2698,7 +2962,7 @@ async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
2698
2962
  );
2699
2963
  const defaultPath = method === "get" ? "/" : "/";
2700
2964
  subPath = handleCancel(
2701
- await p3.text({
2965
+ await p15.text({
2702
2966
  message: "Route subpath (relative to feature prefix):",
2703
2967
  initialValue: defaultPath,
2704
2968
  defaultValue: defaultPath
@@ -2709,7 +2973,7 @@ async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
2709
2973
  const ucOptions = useCases.map((u) => ({ value: u.name, label: u.name }));
2710
2974
  ucOptions.push({ value: "__none__", label: "None / Standalone Handler" });
2711
2975
  const chosenUc = handleCancel(
2712
- await p3.select({
2976
+ await p15.select({
2713
2977
  message: "Connect this handler to a domain Use Case?",
2714
2978
  options: ucOptions,
2715
2979
  initialValue: ucOptions[0]?.value
@@ -2720,7 +2984,7 @@ async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
2720
2984
  }
2721
2985
  }
2722
2986
  includeValidation = handleCancel(
2723
- await p3.confirm({
2987
+ await p15.confirm({
2724
2988
  message: "Include Zod inbound request validation?",
2725
2989
  initialValue: method !== "delete"
2726
2990
  })
@@ -2728,7 +2992,7 @@ async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
2728
2992
  if (includeValidation) {
2729
2993
  const defaultSource = method === "get" ? "query" : "body";
2730
2994
  validationSource = handleCancel(
2731
- await p3.select({
2995
+ await p15.select({
2732
2996
  message: "Validation data source:",
2733
2997
  options: [
2734
2998
  { value: "body", label: "req.body (for POST / PUT / PATCH payloads)" },
@@ -2742,8 +3006,8 @@ async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
2742
3006
  });
2743
3007
  }
2744
3008
  }
2745
- const s = p3.spinner();
2746
- s.start(`Generating HTTP handler '${pc13.cyan(className)}'...`);
3009
+ const s = p15.spinner();
3010
+ s.start(`Generating HTTP handler '${pc15.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
+ p15.outro(pc15.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((p17) => `${p17.name}: ${p17.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 = path6.isAbsolute(options.customPath) ? options.customPath : path6.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 = path6.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 = path6.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 = path6.join(projectRoot, "modules", className);
3067
+ targetDir = path2.join(projectRoot, "modules", className);
2804
3068
  } else {
2805
- targetDir = options.feature ? path6.join(resolveFeaturesDir(projectRoot), options.feature, "services") : path6.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 = path6.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 = path6.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, {
@@ -2841,7 +3105,7 @@ async function runInjectableCommand(classNameArg, flags = {}) {
2841
3105
  let name = classNameArg?.trim();
2842
3106
  if (!name) {
2843
3107
  name = handleCancel(
2844
- await p3.text({
3108
+ await p15.text({
2845
3109
  message: "Injectable class name:",
2846
3110
  placeholder: "e.g. PasswordHasher, TokenService, UserTransformer",
2847
3111
  validate: (v) => v && v.trim() ? void 0 : "Class name cannot be empty"
@@ -2854,7 +3118,7 @@ async function runInjectableCommand(classNameArg, flags = {}) {
2854
3118
  let customPath;
2855
3119
  if (!flags.yes) {
2856
3120
  location = handleCancel(
2857
- await p3.select({
3121
+ await p15.select({
2858
3122
  message: "Select target architectural location:",
2859
3123
  options: [
2860
3124
  { value: "feature-service", label: "Feature Service (features/<feature>/services/)" },
@@ -2872,14 +3136,14 @@ async function runInjectableCommand(classNameArg, flags = {}) {
2872
3136
  const featureOptions = existingFeatures.map((f) => ({ value: f, label: f }));
2873
3137
  featureOptions.push({ value: "__custom__", label: "\u270F\uFE0F Enter custom feature name..." });
2874
3138
  const selected = handleCancel(
2875
- await p3.select({
3139
+ await p15.select({
2876
3140
  message: "Select target feature:",
2877
3141
  options: featureOptions
2878
3142
  })
2879
3143
  );
2880
3144
  if (selected === "__custom__") {
2881
3145
  feature = handleCancel(
2882
- await p3.text({
3146
+ await p15.text({
2883
3147
  message: "Enter feature name:",
2884
3148
  validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
2885
3149
  })
@@ -2889,7 +3153,7 @@ async function runInjectableCommand(classNameArg, flags = {}) {
2889
3153
  }
2890
3154
  } else {
2891
3155
  feature = handleCancel(
2892
- await p3.text({
3156
+ await p15.text({
2893
3157
  message: "Enter target feature name:",
2894
3158
  validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
2895
3159
  })
@@ -2899,7 +3163,7 @@ async function runInjectableCommand(classNameArg, flags = {}) {
2899
3163
  feature = toCamelCase(feature);
2900
3164
  } else if (location === "custom") {
2901
3165
  customPath = handleCancel(
2902
- await p3.text({
3166
+ await p15.text({
2903
3167
  message: "Enter destination directory path:",
2904
3168
  placeholder: "e.g. src/services or lib/utils",
2905
3169
  validate: (v) => v && v.trim() ? void 0 : "Path cannot be empty"
@@ -2910,14 +3174,14 @@ async function runInjectableCommand(classNameArg, flags = {}) {
2910
3174
  let generateInterface2 = false;
2911
3175
  if (!flags.yes) {
2912
3176
  generateInterface2 = handleCancel(
2913
- await p3.confirm({
3177
+ await p15.confirm({
2914
3178
  message: `Generate abstract class interface (I${className}) as runtime DI token?`,
2915
3179
  initialValue: false
2916
3180
  })
2917
3181
  );
2918
3182
  }
2919
- const s = p3.spinner();
2920
- s.start(`Generating injectable '${pc13.cyan(className)}'...`);
3183
+ const s = p15.spinner();
3184
+ s.start(`Generating injectable '${pc15.cyan(className)}'...`);
2921
3185
  const options = {
2922
3186
  name: className,
2923
3187
  feature,
@@ -2931,12 +3195,12 @@ 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
+ p15.outro(pc15.green(`Injectable '${className}' created successfully!`));
2935
3199
  }
2936
3200
  async function runInteractiveMenu(flags = {}) {
2937
3201
  printBanner();
2938
3202
  const choice = handleCancel(
2939
- await p3.select({
3203
+ await p15.select({
2940
3204
  message: "What would you like to create in your architecture?",
2941
3205
  options: [
2942
3206
  {
@@ -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;
@@ -3033,49 +3308,114 @@ async function handleCreate(type, name, flags = {}) {
3033
3308
  await runInjectableCommand(name, flags);
3034
3309
  break;
3035
3310
  default:
3036
- p3.log.error(
3037
- pc13.red(
3038
- `Unknown component type: "${type}". Allowed types: feature, usecase, domain, infrastructure, interface, httpHandler, injectable.`
3311
+ p15.log.error(
3312
+ pc15.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);
3042
3317
  }
3043
3318
  }
3044
3319
  var DEFAULT_REGISTRY_BASE = "https://raw.githubusercontent.com/solid-stack-digital/mason-registry/main";
3320
+ function normalizeRegistryUrl(urlOrPath, baseDir) {
3321
+ let trimmed = urlOrPath.trim();
3322
+ const gitSshRegex = /^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/;
3323
+ const sshMatch = trimmed.match(gitSshRegex);
3324
+ if (sshMatch && sshMatch[1] && sshMatch[2]) {
3325
+ return `https://raw.githubusercontent.com/${sshMatch[1]}/${sshMatch[2]}/main`;
3326
+ }
3327
+ const githubWebRegex = /^https?:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/tree\/([^/]+))?\/?$/;
3328
+ const webMatch = trimmed.match(githubWebRegex);
3329
+ if (webMatch && webMatch[1] && webMatch[2]) {
3330
+ const owner = webMatch[1];
3331
+ const repo = webMatch[2];
3332
+ const branch = webMatch[3] || "main";
3333
+ return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}`;
3334
+ }
3335
+ if (baseDir && (trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed === "." || trimmed === "..")) {
3336
+ return path2.resolve(baseDir, trimmed);
3337
+ }
3338
+ return trimmed;
3339
+ }
3340
+ function findLocalRegistry(startDir = process.cwd()) {
3341
+ let current = path2.resolve(startDir);
3342
+ while (true) {
3343
+ const candidate = path2.join(current, "mason-registry");
3344
+ if (fs12.existsSync(candidate) && fs12.existsSync(path2.join(candidate, "index.json"))) {
3345
+ return candidate;
3346
+ }
3347
+ const parent = path2.dirname(current);
3348
+ if (parent === current) break;
3349
+ current = parent;
3350
+ }
3351
+ return null;
3352
+ }
3045
3353
  function getRegistryBase(projectRoot, override) {
3354
+ const rootDir = projectRoot ? path2.resolve(projectRoot) : process.cwd();
3046
3355
  if (override && override.trim()) {
3047
- return override.trim();
3356
+ return normalizeRegistryUrl(override.trim(), rootDir);
3048
3357
  }
3049
3358
  if (process.env.MASON_REGISTRY_URL && process.env.MASON_REGISTRY_URL.trim()) {
3050
- return process.env.MASON_REGISTRY_URL.trim();
3359
+ return normalizeRegistryUrl(process.env.MASON_REGISTRY_URL.trim(), rootDir);
3051
3360
  }
3052
- if (projectRoot) {
3053
- const configPath = path6.join(projectRoot, "mason.config.json");
3054
- if (fs7.existsSync(configPath)) {
3361
+ const configLocations = [
3362
+ path2.join(rootDir, "mason.config.json"),
3363
+ path2.join(process.cwd(), "mason.config.json")
3364
+ ];
3365
+ for (const configPath of configLocations) {
3366
+ if (fs12.existsSync(configPath)) {
3055
3367
  try {
3056
- const config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
3368
+ const config = JSON.parse(fs12.readFileSync(configPath, "utf-8"));
3057
3369
  if (config.registry && typeof config.registry === "string") {
3058
- return config.registry.trim();
3370
+ return normalizeRegistryUrl(config.registry.trim(), path2.dirname(configPath));
3059
3371
  }
3060
3372
  } catch {
3061
3373
  }
3062
3374
  }
3063
3375
  }
3064
- if (projectRoot) {
3065
- const siblingRegistry = path6.resolve(projectRoot, "../mason-registry");
3066
- if (fs7.existsSync(siblingRegistry) && fs7.existsSync(path6.join(siblingRegistry, "index.json"))) {
3067
- return siblingRegistry;
3376
+ const pkgLocations = [
3377
+ path2.join(rootDir, "package.json"),
3378
+ path2.join(process.cwd(), "package.json")
3379
+ ];
3380
+ for (const pkgPath of pkgLocations) {
3381
+ if (fs12.existsSync(pkgPath)) {
3382
+ try {
3383
+ const pkg = JSON.parse(fs12.readFileSync(pkgPath, "utf-8"));
3384
+ if (pkg.mason?.registry && typeof pkg.mason.registry === "string") {
3385
+ return normalizeRegistryUrl(pkg.mason.registry.trim(), path2.dirname(pkgPath));
3386
+ }
3387
+ } catch {
3388
+ }
3068
3389
  }
3069
3390
  }
3391
+ const localRegistry = findLocalRegistry(rootDir);
3392
+ if (localRegistry) {
3393
+ return localRegistry;
3394
+ }
3070
3395
  return DEFAULT_REGISTRY_BASE;
3071
3396
  }
3072
3397
  async function fetchRegistryFile(registryBase, relPath) {
3073
3398
  const cleanRelPath = relPath.replace(/^\/+/, "");
3074
3399
  if (registryBase.startsWith("http://") || registryBase.startsWith("https://")) {
3075
3400
  const url = `${registryBase.replace(/\/+$/, "")}/${cleanRelPath}`;
3076
- const res = await fetch(url);
3401
+ const headers = {};
3402
+ const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
3403
+ if (token) {
3404
+ headers["Authorization"] = `Bearer ${token}`;
3405
+ }
3406
+ const res = await fetch(url, { headers });
3077
3407
  if (!res.ok) {
3078
- throw new Error(`Failed to fetch ${cleanRelPath} from ${url} (HTTP ${res.status}: ${res.statusText})`);
3408
+ let extraHint = "";
3409
+ if (res.status === 404) {
3410
+ extraHint = `
3411
+ Possible reasons:
3412
+ \u2022 If using a private GitHub repository, ensure GITHUB_TOKEN is set in your environment.
3413
+ \u2022 If your registry has not yet been pushed to GitHub, point to your local directory:
3414
+ - CLI flag: mason import ... --registry /path/to/mason-registry
3415
+ - Environment: export MASON_REGISTRY_URL=/path/to/mason-registry
3416
+ - Config file: Create mason.config.json with { "registry": "/path/to/mason-registry" }`;
3417
+ }
3418
+ throw new Error(`Failed to fetch ${cleanRelPath} from ${url} (HTTP ${res.status}: ${res.statusText})${extraHint}`);
3079
3419
  }
3080
3420
  return await res.text();
3081
3421
  }
@@ -3083,11 +3423,11 @@ async function fetchRegistryFile(registryBase, relPath) {
3083
3423
  if (baseDir.startsWith("file://")) {
3084
3424
  baseDir = fileURLToPath(baseDir);
3085
3425
  }
3086
- const filePath = path6.resolve(baseDir, cleanRelPath);
3087
- if (!fs7.existsSync(filePath)) {
3426
+ const filePath = path2.resolve(baseDir, cleanRelPath);
3427
+ if (!fs12.existsSync(filePath)) {
3088
3428
  throw new Error(`Registry file not found at ${filePath}`);
3089
3429
  }
3090
- return fs7.promises.readFile(filePath, "utf-8");
3430
+ return fs12.promises.readFile(filePath, "utf-8");
3091
3431
  }
3092
3432
  async function fetchRegistryIndex(registryBase) {
3093
3433
  const content = await fetchRegistryFile(registryBase, "index.json");
@@ -3149,26 +3489,26 @@ function findClosestMatch(target, candidates) {
3149
3489
  return null;
3150
3490
  }
3151
3491
  function detectPackageManager(projectRoot) {
3152
- if (fs7.existsSync(path6.join(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
3153
- if (fs7.existsSync(path6.join(projectRoot, "yarn.lock"))) return "yarn";
3154
- if (fs7.existsSync(path6.join(projectRoot, "bun.lockb")) || fs7.existsSync(path6.join(projectRoot, "bun.lock"))) return "bun";
3492
+ if (fs12.existsSync(path2.join(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
3493
+ if (fs12.existsSync(path2.join(projectRoot, "yarn.lock"))) return "yarn";
3494
+ if (fs12.existsSync(path2.join(projectRoot, "bun.lockb")) || fs12.existsSync(path2.join(projectRoot, "bun.lock"))) return "bun";
3155
3495
  return "npm";
3156
3496
  }
3157
3497
  function getMissingNpmPackages(projectRoot, requiredPackages) {
3158
3498
  if (!requiredPackages || requiredPackages.length === 0) return [];
3159
- const pkgJsonPath = path6.join(projectRoot, "package.json");
3160
- if (!fs7.existsSync(pkgJsonPath)) {
3499
+ const pkgJsonPath = path2.join(projectRoot, "package.json");
3500
+ if (!fs12.existsSync(pkgJsonPath)) {
3161
3501
  return requiredPackages;
3162
3502
  }
3163
3503
  try {
3164
- const pkg = JSON.parse(fs7.readFileSync(pkgJsonPath, "utf-8"));
3504
+ const pkg = JSON.parse(fs12.readFileSync(pkgJsonPath, "utf-8"));
3165
3505
  const installed = /* @__PURE__ */ new Set([
3166
3506
  ...Object.keys(pkg.dependencies || {}),
3167
3507
  ...Object.keys(pkg.devDependencies || {}),
3168
3508
  ...Object.keys(pkg.peerDependencies || {})
3169
3509
  ]);
3170
- return requiredPackages.filter((p15) => {
3171
- const nameOnly = p15.startsWith("@") ? `@${p15.slice(1).split("@")[0] || ""}` : p15.split("@")[0] || p15;
3510
+ return requiredPackages.filter((p17) => {
3511
+ const nameOnly = p17.startsWith("@") ? `@${p17.slice(1).split("@")[0] || ""}` : p17.split("@")[0] || p17;
3172
3512
  return !installed.has(nameOnly);
3173
3513
  });
3174
3514
  } catch {
@@ -3193,21 +3533,207 @@ function installNpmPackages(projectRoot, packages, pm) {
3193
3533
  installCmd = `npm install ${packages.join(" ")}`;
3194
3534
  break;
3195
3535
  }
3196
- p3.log.info(`Installing missing dependencies with ${manager}: ${pc13.cyan(packages.join(", "))}`);
3536
+ p15.log.info(`Installing missing dependencies with ${manager}: ${pc15.cyan(packages.join(", "))}`);
3197
3537
  execSync(installCmd, {
3198
3538
  cwd: projectRoot,
3199
3539
  stdio: "inherit"
3200
3540
  });
3201
3541
  }
3542
+ function getAllFiles(dir, baseDir = dir) {
3543
+ if (!fs12.existsSync(dir)) return [];
3544
+ const entries = fs12.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 (!fs12.existsSync(dirPath)) return "";
3565
+ const hash = createHash("sha256");
3566
+ const fileList = (files ?? getAllFiles(dirPath)).sort();
3567
+ for (const relativePath of fileList) {
3568
+ const absolutePath = path2.join(dirPath, relativePath);
3569
+ if (!fs12.existsSync(absolutePath)) continue;
3570
+ const content = fs12.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
+ }
3202
3577
 
3203
3578
  // src/commands/import.ts
3579
+ var importPrompts = {
3580
+ select: p15.select,
3581
+ text: p15.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 (fs12.existsSync(inSrc)) return inSrc;
3603
+ const inRoot = path2.join(projectRoot, folder, modName);
3604
+ if (fs12.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
+ p15.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
+ p15.log.info(` \u21B3 Resolving required ${depType} dependency: ${pc15.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
+ p15.log.step(`\u2713 ${depType}/${depName} is up to date (reusing existing)`);
3646
+ importedModules.add(`${depType}:${depName}`);
3647
+ return;
3648
+ }
3649
+ if (flags.overwrite) {
3650
+ p15.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
+ fs12.rmSync(existingDir, { recursive: true, force: true });
3656
+ }
3657
+ fs12.rmSync(targetDir, { recursive: true, force: true });
3658
+ }
3659
+ await importModuleRecursive(depType, depName, depName, { overwrite: true });
3660
+ return;
3661
+ }
3662
+ if (flags.yes) {
3663
+ p15.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
+ p15.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
+ p15.log.step(`Importing '${depName}' into '${displayDest}'...`);
3713
+ await importModuleRecursive(depType, depName, newName);
3714
+ return;
3715
+ }
3716
+ if (choice === "overwrite") {
3717
+ p15.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
+ fs12.rmSync(existingDir, { recursive: true, force: true });
3723
+ }
3724
+ fs12.rmSync(targetDir, { recursive: true, force: true });
3725
+ }
3726
+ await importModuleRecursive(depType, depName, depName, { overwrite: true });
3727
+ return;
3728
+ }
3729
+ }
3204
3730
  async function handleImport(typeArg, nameArg, flags = {}) {
3205
3731
  const projectRoot = findProjectRoot();
3206
3732
  let type = typeArg?.trim().toLowerCase();
3207
3733
  if (type === "features") type = "feature";
3208
3734
  if (!type) {
3209
3735
  type = handleCancel(
3210
- await p3.select({
3736
+ await importPrompts.select({
3211
3737
  message: "What kind of module would you like to import?",
3212
3738
  options: [
3213
3739
  {
@@ -3225,19 +3751,19 @@ async function handleImport(typeArg, nameArg, flags = {}) {
3225
3751
  );
3226
3752
  }
3227
3753
  if (type !== "shared" && type !== "feature") {
3228
- p3.log.error(pc13.red(`Invalid module type: "${typeArg}". Must be "shared" or "feature".`));
3754
+ p15.log.error(pc15.red(`Invalid module type: "${typeArg}". Must be "shared" or "feature".`));
3229
3755
  process.exit(1);
3230
3756
  }
3231
3757
  const registryBase = getRegistryBase(projectRoot, flags.registry);
3232
- const s = p3.spinner();
3233
- s.start(`Connecting to registry at ${pc13.dim(registryBase)}...`);
3758
+ const s = p15.spinner();
3759
+ s.start(`Connecting to registry at ${pc15.dim(registryBase)}...`);
3234
3760
  let index;
3235
3761
  try {
3236
3762
  index = await fetchRegistryIndex(registryBase);
3237
3763
  s.stop("Registry index loaded.");
3238
3764
  } catch (err) {
3239
- s.stop(pc13.red("Failed to load registry index."));
3240
- p3.log.error(pc13.red(err.message || String(err)));
3765
+ s.stop(pc15.red("Failed to load registry index."));
3766
+ p15.log.error(pc15.red(err.message || String(err)));
3241
3767
  process.exit(1);
3242
3768
  }
3243
3769
  const availableList = type === "shared" ? index.shared : index.features;
@@ -3245,11 +3771,11 @@ async function handleImport(typeArg, nameArg, flags = {}) {
3245
3771
  let name = nameArg?.trim();
3246
3772
  if (!name) {
3247
3773
  if (availableList.length === 0) {
3248
- p3.log.warn(`No ${type} modules available in the registry.`);
3774
+ p15.log.warn(`No ${type} modules available in the registry.`);
3249
3775
  return;
3250
3776
  }
3251
3777
  name = handleCancel(
3252
- await p3.select({
3778
+ await importPrompts.select({
3253
3779
  message: `Select a ${type} to import:`,
3254
3780
  options: availableList.map((item) => ({
3255
3781
  value: item.name,
@@ -3261,122 +3787,769 @@ async function handleImport(typeArg, nameArg, flags = {}) {
3261
3787
  }
3262
3788
  if (!candidateNames.includes(name)) {
3263
3789
  const closest = findClosestMatch(name, candidateNames);
3264
- p3.log.error(pc13.red(`\u274C Module '${name}' not found in the ${type} registry.`));
3790
+ p15.log.error(pc15.red(`\u274C Module '${name}' not found in the ${type} registry.`));
3265
3791
  if (closest) {
3266
- p3.log.info(pc13.yellow(`Did you mean '${pc13.cyan(closest)}'?`));
3792
+ p15.log.info(pc15.yellow(`Did you mean '${pc15.cyan(closest)}'?`));
3267
3793
  }
3268
3794
  process.exit(1);
3269
3795
  }
3270
3796
  const importedModules = /* @__PURE__ */ new Set();
3271
3797
  const totalWrittenFiles = [];
3272
3798
  const requiredNpmPackages = /* @__PURE__ */ new Set();
3273
- async function importModuleRecursive(modType, modName) {
3274
- const modKey = `${modType}:${modName}`;
3799
+ async function importModuleRecursive(modType, registryName, targetName = registryName, options) {
3800
+ const modKey = `${modType}:${targetName}`;
3275
3801
  if (importedModules.has(modKey)) return;
3276
- p3.log.step(`Importing ${modType}: ${pc13.cyan(modName)}...`);
3802
+ p15.log.step(
3803
+ `Importing ${modType}: ${pc15.cyan(targetName)}${targetName !== registryName ? pc15.dim(` (from ${registryName})`) : ""}...`
3804
+ );
3277
3805
  let manifest;
3278
3806
  try {
3279
- manifest = await fetchModuleManifest(registryBase, modType, modName);
3807
+ manifest = await fetchModuleManifest(registryBase, modType, registryName);
3280
3808
  } catch (err) {
3281
- p3.log.error(pc13.red(`Failed to fetch manifest for ${modType} '${modName}': ${err.message}`));
3809
+ p15.log.error(pc15.red(`Failed to fetch manifest for ${modType} '${registryName}': ${err.message}`));
3282
3810
  process.exit(1);
3283
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
+ }
3284
3817
  const sharedDeps = manifest.dependencies?.shared || [];
3285
3818
  for (const dep of sharedDeps) {
3286
- const depPath = path6.join(projectRoot, "src", "shared", dep);
3287
- if (!fs7.existsSync(depPath) && !importedModules.has(`shared:${dep}`)) {
3288
- p3.log.info(` \u21B3 Resolving required shared dependency: ${pc13.cyan(dep)}`);
3289
- await importModuleRecursive("shared", dep);
3290
- }
3819
+ await resolveDependency(
3820
+ "shared",
3821
+ dep,
3822
+ aliasMap,
3823
+ projectRoot,
3824
+ flags,
3825
+ importModuleRecursive,
3826
+ importedModules,
3827
+ registryBase,
3828
+ index
3829
+ );
3291
3830
  }
3292
3831
  const featureDeps = manifest.dependencies?.features || [];
3293
3832
  for (const dep of featureDeps) {
3294
- const depPath = path6.join(projectRoot, "src", "features", dep);
3295
- if (!fs7.existsSync(depPath) && !importedModules.has(`feature:${dep}`)) {
3296
- p3.log.info(` \u21B3 Resolving required feature dependency: ${pc13.cyan(dep)}`);
3297
- await importModuleRecursive("feature", dep);
3298
- }
3833
+ await resolveDependency(
3834
+ "feature",
3835
+ dep,
3836
+ aliasMap,
3837
+ projectRoot,
3838
+ flags,
3839
+ importModuleRecursive,
3840
+ importedModules,
3841
+ registryBase,
3842
+ index
3843
+ );
3299
3844
  }
3300
3845
  for (const pkg of manifest.dependencies?.npm || []) {
3301
3846
  requiredNpmPackages.add(pkg);
3302
3847
  }
3303
3848
  const folder = modType === "shared" ? "shared" : "features";
3304
- const targetDir = path6.join(projectRoot, "src", folder, modName);
3849
+ const targetDir = path2.join(getModuleBaseDir(projectRoot, modType), targetName);
3850
+ const isOverwrite = Boolean(flags.overwrite || options?.overwrite);
3305
3851
  for (const file of manifest.files || []) {
3306
3852
  if (file === "registry.json") continue;
3307
- const remoteFilePath = `${folder}/${modName}/${file}`;
3308
- const localFilePath = path6.join(targetDir, file);
3309
- if (fs7.existsSync(localFilePath) && !flags.overwrite) {
3310
- p3.log.warn(` \u21B3 Skipping existing file: ${pc13.dim(path6.relative(projectRoot, localFilePath))} (use --overwrite to replace)`);
3853
+ const remoteFilePath = `${folder}/${registryName}/${file}`;
3854
+ const localFilePath = path2.join(targetDir, file);
3855
+ if (fs12.existsSync(localFilePath) && !isOverwrite) {
3856
+ p15.log.warn(
3857
+ ` \u21B3 Skipping existing file: ${pc15.dim(path2.relative(projectRoot, localFilePath))} (use --overwrite to replace)`
3858
+ );
3311
3859
  continue;
3312
3860
  }
3313
- const content = await fetchRegistryFile(registryBase, remoteFilePath);
3861
+ let content = await fetchRegistryFile(registryBase, remoteFilePath);
3862
+ content = rewriteImports(content, aliasMap);
3314
3863
  if (!flags.dryRun) {
3315
- fs7.mkdirSync(path6.dirname(localFilePath), { recursive: true });
3316
- fs7.writeFileSync(localFilePath, content, "utf-8");
3864
+ fs12.mkdirSync(path2.dirname(localFilePath), { recursive: true });
3865
+ fs12.writeFileSync(localFilePath, content, "utf-8");
3317
3866
  }
3318
- const displayPath = path6.relative(projectRoot, localFilePath);
3867
+ const displayPath = path2.relative(projectRoot, localFilePath);
3319
3868
  totalWrittenFiles.push(displayPath);
3320
- p3.log.message(` ${pc13.green("+")} ${displayPath}`);
3869
+ p15.log.message(` ${pc15.green("+")} ${displayPath}`);
3321
3870
  }
3322
3871
  importedModules.add(modKey);
3323
3872
  }
3324
- await importModuleRecursive(type, name);
3873
+ await importModuleRecursive(type, name, name);
3325
3874
  const missingNpm = getMissingNpmPackages(projectRoot, Array.from(requiredNpmPackages));
3326
3875
  if (missingNpm.length > 0) {
3327
3876
  if (flags.skipInstall || flags.dryRun) {
3328
- p3.log.info(`Required npm dependencies to install: ${pc13.cyan(missingNpm.join(", "))}`);
3877
+ p15.log.info(`Required npm dependencies to install: ${pc15.cyan(missingNpm.join(", "))}`);
3329
3878
  } else {
3330
3879
  try {
3331
3880
  installNpmPackages(projectRoot, missingNpm);
3332
3881
  } catch (err) {
3333
- p3.log.warn(`Could not automatically install packages (${err.message}). Please run: npm install ${missingNpm.join(" ")}`);
3882
+ p15.log.warn(`Could not automatically install packages (${err.message}). Please run: npm install ${missingNpm.join(" ")}`);
3334
3883
  }
3335
3884
  }
3336
3885
  }
3337
3886
  if (flags.dryRun) {
3338
- p3.log.info(pc13.yellow("Dry run complete: No files were written to disk."));
3887
+ p15.log.info(pc15.yellow("Dry run complete: No files were written to disk."));
3339
3888
  } else {
3340
- p3.log.success(
3341
- pc13.green(
3342
- `Successfully imported ${type} '${name}' (${totalWrittenFiles.length} file${totalWrittenFiles.length === 1 ? "" : "s"} placed in src/${type === "shared" ? "shared" : "features"}/${name})`
3889
+ const baseDir = getModuleBaseDir(projectRoot, type);
3890
+ const displayTarget = path2.relative(projectRoot, path2.join(baseDir, name));
3891
+ p15.log.success(
3892
+ pc15.green(
3893
+ `Successfully imported ${type} '${name}' (${totalWrittenFiles.length} file${totalWrittenFiles.length === 1 ? "" : "s"} placed in ${displayTarget})`
3343
3894
  )
3344
3895
  );
3345
3896
  }
3346
3897
  }
3898
+ function loadMasterIndex(projectRoot) {
3899
+ const indexPath = path2.join(projectRoot, "index.json");
3900
+ if (!fs12.existsSync(indexPath)) {
3901
+ const initialIndex = { shared: [], features: [] };
3902
+ fs12.writeFileSync(indexPath, JSON.stringify(initialIndex, null, 2) + "\n", "utf-8");
3903
+ return initialIndex;
3904
+ }
3905
+ return JSON.parse(fs12.readFileSync(indexPath, "utf-8"));
3906
+ }
3907
+ function saveMasterIndex(projectRoot, index) {
3908
+ const indexPath = path2.join(projectRoot, "index.json");
3909
+ index.shared.sort((a, b) => a.name.localeCompare(b.name));
3910
+ index.features.sort((a, b) => a.name.localeCompare(b.name));
3911
+ fs12.writeFileSync(indexPath, JSON.stringify(index, null, 2) + "\n", "utf-8");
3912
+ }
3913
+ function discoverDiskModules(projectRoot) {
3914
+ const results = [];
3915
+ const sharedDir = resolveSharedDir(projectRoot);
3916
+ if (fs12.existsSync(sharedDir)) {
3917
+ const entries = fs12.readdirSync(sharedDir, { withFileTypes: true });
3918
+ for (const entry of entries) {
3919
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
3920
+ results.push({ name: entry.name, type: "shared", dir: path2.join(sharedDir, entry.name) });
3921
+ }
3922
+ }
3923
+ }
3924
+ const featuresDir = resolveFeaturesDir(projectRoot);
3925
+ if (fs12.existsSync(featuresDir)) {
3926
+ const entries = fs12.readdirSync(featuresDir, { withFileTypes: true });
3927
+ for (const entry of entries) {
3928
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
3929
+ results.push({ name: entry.name, type: "feature", dir: path2.join(featuresDir, entry.name) });
3930
+ }
3931
+ }
3932
+ }
3933
+ return results;
3934
+ }
3935
+ function checkGitStatusForDir(projectRoot, dirPath) {
3936
+ try {
3937
+ const relPath = path2.relative(projectRoot, dirPath);
3938
+ const output = execSync(`git status --porcelain -- "${relPath}"`, {
3939
+ cwd: projectRoot,
3940
+ stdio: ["ignore", "pipe", "ignore"],
3941
+ encoding: "utf-8"
3942
+ }).trim();
3943
+ if (!output) {
3944
+ return { hasChanges: false, filesCount: 0 };
3945
+ }
3946
+ const lines = output.split("\n").filter((l) => l.trim().length > 0);
3947
+ return { hasChanges: true, filesCount: lines.length };
3948
+ } catch {
3949
+ return { hasChanges: false, filesCount: 0 };
3950
+ }
3951
+ }
3952
+ function getRegistryDiff(projectRoot) {
3953
+ const index = loadMasterIndex(projectRoot);
3954
+ const diskModules = discoverDiskModules(projectRoot);
3955
+ const diskMap = /* @__PURE__ */ new Map();
3956
+ for (const mod of diskModules) {
3957
+ diskMap.set(`${mod.type}:${mod.name}`, mod);
3958
+ }
3959
+ const allKeys = /* @__PURE__ */ new Set();
3960
+ for (const item of index.shared) {
3961
+ allKeys.add(`shared:${item.name}`);
3962
+ }
3963
+ for (const item of index.features) {
3964
+ allKeys.add(`feature:${item.name}`);
3965
+ }
3966
+ for (const mod of diskModules) {
3967
+ allKeys.add(`${mod.type}:${mod.name}`);
3968
+ }
3969
+ const statuses = [];
3970
+ for (const key of allKeys) {
3971
+ const [typeStr, name] = key.split(":");
3972
+ const type = typeStr;
3973
+ const registeredEntry = (type === "shared" ? index.shared : index.features).find(
3974
+ (e) => e.name === name
3975
+ );
3976
+ const isRegistered = Boolean(registeredEntry);
3977
+ const diskInfo = diskMap.get(key);
3978
+ const existsOnDisk = Boolean(diskInfo && fs12.existsSync(diskInfo.dir));
3979
+ let hasUncommittedGitChanges = false;
3980
+ let uncommittedFilesCount = 0;
3981
+ let hasIntegrityDrift = false;
3982
+ let localIntegrity;
3983
+ if (existsOnDisk && diskInfo) {
3984
+ const git = checkGitStatusForDir(projectRoot, diskInfo.dir);
3985
+ hasUncommittedGitChanges = git.hasChanges;
3986
+ uncommittedFilesCount = git.filesCount;
3987
+ localIntegrity = computeDirectoryHash(diskInfo.dir);
3988
+ if (isRegistered && registeredEntry?.integrity) {
3989
+ hasIntegrityDrift = localIntegrity !== registeredEntry.integrity;
3990
+ }
3991
+ }
3992
+ statuses.push({
3993
+ name,
3994
+ type,
3995
+ isRegistered,
3996
+ existsOnDisk,
3997
+ hasUncommittedGitChanges,
3998
+ uncommittedFilesCount,
3999
+ hasIntegrityDrift,
4000
+ localIntegrity,
4001
+ registeredIntegrity: registeredEntry?.integrity
4002
+ });
4003
+ }
4004
+ return statuses.sort((a, b) => a.type.localeCompare(b.type) || a.name.localeCompare(b.name));
4005
+ }
4006
+ async function registerModule(typeArg, nameArg, flags = {}) {
4007
+ const projectRoot = findProjectRoot();
4008
+ const index = loadMasterIndex(projectRoot);
4009
+ let type = typeArg?.trim();
4010
+ if (!type) {
4011
+ type = handleCancel(
4012
+ await p15.select({
4013
+ message: "Module type to register:",
4014
+ options: [
4015
+ { value: "feature", label: "Feature" },
4016
+ { value: "shared", label: "Shared Module" }
4017
+ ]
4018
+ })
4019
+ );
4020
+ }
4021
+ const diskModules = discoverDiskModules(projectRoot).filter((m) => m.type === type);
4022
+ const registeredNames = new Set((type === "shared" ? index.shared : index.features).map((m) => m.name));
4023
+ const unregistered = diskModules.filter((m) => !registeredNames.has(m.name));
4024
+ let name = nameArg?.trim();
4025
+ if (!name) {
4026
+ if (unregistered.length > 0) {
4027
+ const opts = unregistered.map((m) => ({ value: m.name, label: m.name, hint: "unregistered on disk" }));
4028
+ opts.push({ value: "__custom__", label: "\u270F\uFE0F Enter custom name...", hint: "" });
4029
+ const choice = handleCancel(
4030
+ await p15.select({
4031
+ message: `Select a ${type} to register:`,
4032
+ options: opts
4033
+ })
4034
+ );
4035
+ if (choice === "__custom__") {
4036
+ name = handleCancel(
4037
+ await p15.text({
4038
+ message: `Enter ${type} name:`,
4039
+ validate: (v) => v && v.trim() ? void 0 : "Name cannot be empty"
4040
+ })
4041
+ ).trim();
4042
+ } else {
4043
+ name = choice;
4044
+ }
4045
+ } else {
4046
+ name = handleCancel(
4047
+ await p15.text({
4048
+ message: `Enter ${type} name to register:`,
4049
+ validate: (v) => v && v.trim() ? void 0 : "Name cannot be empty"
4050
+ })
4051
+ ).trim();
4052
+ }
4053
+ }
4054
+ const folder = type === "shared" ? resolveSharedDir(projectRoot) : resolveFeaturesDir(projectRoot);
4055
+ const modDir = path2.join(folder, name);
4056
+ if (!fs12.existsSync(modDir)) {
4057
+ p15.log.error(pc15.red(`Module directory does not exist: ${path2.relative(projectRoot, modDir)}`));
4058
+ process.exit(1);
4059
+ }
4060
+ const files = getAllFiles(modDir);
4061
+ const integrity = computeDirectoryHash(modDir, files);
4062
+ const manifestPath = path2.join(modDir, "registry.json");
4063
+ let manifest;
4064
+ if (fs12.existsSync(manifestPath)) {
4065
+ manifest = JSON.parse(fs12.readFileSync(manifestPath, "utf-8"));
4066
+ } else {
4067
+ manifest = {
4068
+ name,
4069
+ type,
4070
+ description: `${name} ${type} module`,
4071
+ version: "1.0.0",
4072
+ dependencies: {
4073
+ npm: [],
4074
+ shared: [],
4075
+ features: []
4076
+ },
4077
+ files: []
4078
+ };
4079
+ }
4080
+ manifest.name = name;
4081
+ manifest.type = type;
4082
+ manifest.files = files;
4083
+ manifest.integrity = integrity;
4084
+ fs12.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
4085
+ const targetList = type === "shared" ? index.shared : index.features;
4086
+ const existingIdx = targetList.findIndex((item) => item.name === name);
4087
+ if (existingIdx >= 0) {
4088
+ targetList[existingIdx] = {
4089
+ name,
4090
+ description: manifest.description,
4091
+ version: manifest.version,
4092
+ integrity
4093
+ };
4094
+ } else {
4095
+ targetList.push({
4096
+ name,
4097
+ description: manifest.description,
4098
+ version: manifest.version,
4099
+ integrity
4100
+ });
4101
+ }
4102
+ saveMasterIndex(projectRoot, index);
4103
+ p15.log.success(
4104
+ pc15.green(
4105
+ `\u2713 Module '${type}/${name}' registered in index.json (${files.length} files, ${integrity.slice(0, 18)}...)`
4106
+ )
4107
+ );
4108
+ }
4109
+ async function commitModule(typeArg, nameArg, flags = {}) {
4110
+ const projectRoot = findProjectRoot();
4111
+ let type = typeArg?.trim();
4112
+ if (!type) {
4113
+ type = handleCancel(
4114
+ await p15.select({
4115
+ message: "Module type to commit:",
4116
+ options: [
4117
+ { value: "feature", label: "Feature" },
4118
+ { value: "shared", label: "Shared Module" }
4119
+ ]
4120
+ })
4121
+ );
4122
+ }
4123
+ let name = nameArg?.trim();
4124
+ if (!name) {
4125
+ const diskModules = discoverDiskModules(projectRoot).filter((m) => m.type === type);
4126
+ const opts = diskModules.map((m) => {
4127
+ const git = checkGitStatusForDir(projectRoot, m.dir);
4128
+ return {
4129
+ value: m.name,
4130
+ label: m.name,
4131
+ hint: git.hasChanges ? `${git.filesCount} uncommitted files` : "clean working tree"
4132
+ };
4133
+ });
4134
+ name = handleCancel(
4135
+ await p15.select({
4136
+ message: `Select ${type} to commit:`,
4137
+ options: opts
4138
+ })
4139
+ );
4140
+ }
4141
+ const folder = type === "shared" ? resolveSharedDir(projectRoot) : resolveFeaturesDir(projectRoot);
4142
+ const modDir = path2.join(folder, name);
4143
+ if (!fs12.existsSync(modDir)) {
4144
+ p15.log.error(pc15.red(`Module directory does not exist: ${path2.relative(projectRoot, modDir)}`));
4145
+ process.exit(1);
4146
+ }
4147
+ const relModDir = path2.relative(projectRoot, modDir);
4148
+ const gitStatus = checkGitStatusForDir(projectRoot, modDir);
4149
+ if (!gitStatus.hasChanges) {
4150
+ p15.log.warn(pc15.yellow(`No uncommitted git changes found in '${type}/${name}'.`));
4151
+ return;
4152
+ }
4153
+ let message = flags.message?.trim();
4154
+ if (!message) {
4155
+ message = handleCancel(
4156
+ await p15.text({
4157
+ message: `Commit message for ${type}/${name}:`,
4158
+ placeholder: `e.g. feat(${name}): update use cases and dependencies`,
4159
+ validate: (v) => v && v.trim() ? void 0 : "Commit message cannot be empty"
4160
+ })
4161
+ ).trim();
4162
+ }
4163
+ const s = p15.spinner();
4164
+ s.start(`Committing '${type}/${name}' to git repository...`);
4165
+ try {
4166
+ execSync(`git add "${relModDir}"`, { cwd: projectRoot, stdio: ["ignore", "pipe", "pipe"] });
4167
+ execSync(`git commit -m "${message.replace(/"/g, '\\"')}" -- "${relModDir}"`, {
4168
+ cwd: projectRoot,
4169
+ stdio: ["ignore", "pipe", "pipe"]
4170
+ });
4171
+ s.stop(`Changes for '${type}/${name}' committed to git.`);
4172
+ p15.log.success(pc15.green(`\u2713 Committed '${type}/${name}': "${message}"`));
4173
+ } catch (err) {
4174
+ s.stop(pc15.red("Git commit failed."));
4175
+ p15.log.error(pc15.red(err.message || String(err)));
4176
+ process.exit(1);
4177
+ }
4178
+ }
4179
+ async function registerModuleChanges(typeArg, nameArg, flags = {}) {
4180
+ const projectRoot = findProjectRoot();
4181
+ const index = loadMasterIndex(projectRoot);
4182
+ let type = typeArg?.trim();
4183
+ if (!type) {
4184
+ type = handleCancel(
4185
+ await p15.select({
4186
+ message: "Module type to update:",
4187
+ options: [
4188
+ { value: "feature", label: "Feature" },
4189
+ { value: "shared", label: "Shared Module" }
4190
+ ]
4191
+ })
4192
+ );
4193
+ }
4194
+ let name = nameArg?.trim();
4195
+ if (!name) {
4196
+ const diskModules = discoverDiskModules(projectRoot).filter((m) => m.type === type);
4197
+ const diffs = getRegistryDiff(projectRoot).filter((d) => d.type === type && d.isRegistered);
4198
+ const opts = diskModules.map((m) => {
4199
+ const d = diffs.find((item) => item.name === m.name);
4200
+ return {
4201
+ value: m.name,
4202
+ label: m.name,
4203
+ hint: d?.hasIntegrityDrift ? "drift detected (needs register)" : "up to date"
4204
+ };
4205
+ });
4206
+ name = handleCancel(
4207
+ await p15.select({
4208
+ message: `Select ${type} to register changes:`,
4209
+ options: opts
4210
+ })
4211
+ );
4212
+ }
4213
+ const folder = type === "shared" ? resolveSharedDir(projectRoot) : resolveFeaturesDir(projectRoot);
4214
+ const modDir = path2.join(folder, name);
4215
+ if (!fs12.existsSync(modDir)) {
4216
+ p15.log.error(pc15.red(`Module directory does not exist: ${path2.relative(projectRoot, modDir)}`));
4217
+ process.exit(1);
4218
+ }
4219
+ const files = getAllFiles(modDir);
4220
+ const integrity = computeDirectoryHash(modDir, files);
4221
+ const manifestPath = path2.join(modDir, "registry.json");
4222
+ let manifest;
4223
+ if (fs12.existsSync(manifestPath)) {
4224
+ manifest = JSON.parse(fs12.readFileSync(manifestPath, "utf-8"));
4225
+ } else {
4226
+ manifest = {
4227
+ name,
4228
+ type,
4229
+ description: `${name} ${type} module`,
4230
+ version: "1.0.0",
4231
+ dependencies: { npm: [], shared: [], features: [] },
4232
+ files: []
4233
+ };
4234
+ }
4235
+ manifest.files = files;
4236
+ manifest.integrity = integrity;
4237
+ if (flags.version) {
4238
+ manifest.version = flags.version;
4239
+ }
4240
+ fs12.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
4241
+ const targetList = type === "shared" ? index.shared : index.features;
4242
+ const existingIdx = targetList.findIndex((item) => item.name === name);
4243
+ if (existingIdx >= 0) {
4244
+ targetList[existingIdx] = {
4245
+ name,
4246
+ description: manifest.description,
4247
+ version: manifest.version,
4248
+ integrity
4249
+ };
4250
+ } else {
4251
+ targetList.push({
4252
+ name,
4253
+ description: manifest.description,
4254
+ version: manifest.version,
4255
+ integrity
4256
+ });
4257
+ }
4258
+ saveMasterIndex(projectRoot, index);
4259
+ p15.log.success(
4260
+ pc15.green(`\u2713 Registered changes for '${type}/${name}' (new integrity: ${integrity.slice(0, 18)}...)`)
4261
+ );
4262
+ }
4263
+ async function unregisterModule(typeArg, nameArg, flags = {}) {
4264
+ const projectRoot = findProjectRoot();
4265
+ const index = loadMasterIndex(projectRoot);
4266
+ let type = typeArg?.trim();
4267
+ if (!type) {
4268
+ type = handleCancel(
4269
+ await p15.select({
4270
+ message: "Module type to unregister:",
4271
+ options: [
4272
+ { value: "feature", label: "Feature" },
4273
+ { value: "shared", label: "Shared Module" }
4274
+ ]
4275
+ })
4276
+ );
4277
+ }
4278
+ const registeredList = type === "shared" ? index.shared : index.features;
4279
+ if (registeredList.length === 0) {
4280
+ p15.log.warn(`No registered ${type} modules found in index.json.`);
4281
+ return;
4282
+ }
4283
+ let name = nameArg?.trim();
4284
+ if (!name) {
4285
+ name = handleCancel(
4286
+ await p15.select({
4287
+ message: `Select ${type} to unregister from index.json:`,
4288
+ options: registeredList.map((item) => ({
4289
+ value: item.name,
4290
+ label: item.name,
4291
+ hint: item.description
4292
+ }))
4293
+ })
4294
+ );
4295
+ }
4296
+ if (type === "shared") {
4297
+ index.shared = index.shared.filter((item) => item.name !== name);
4298
+ } else {
4299
+ index.features = index.features.filter((item) => item.name !== name);
4300
+ }
4301
+ saveMasterIndex(projectRoot, index);
4302
+ p15.log.success(
4303
+ pc15.green(
4304
+ `\u2713 Unregistered module '${type}/${name}' from index.json (source code retained on disk in ${type === "shared" ? "shared" : "features"}/${name})`
4305
+ )
4306
+ );
4307
+ }
4308
+ function diffRegistry(flags = {}) {
4309
+ const projectRoot = findProjectRoot();
4310
+ const diffs = getRegistryDiff(projectRoot);
4311
+ p15.log.step(pc15.bold("\u{1F4CB} Mason Registry Diff & Status Summary"));
4312
+ const unregistered = diffs.filter((d) => d.existsOnDisk && !d.isRegistered);
4313
+ const uncommitted = diffs.filter((d) => d.existsOnDisk && d.hasUncommittedGitChanges);
4314
+ const committedDrift = diffs.filter(
4315
+ (d) => d.existsOnDisk && d.isRegistered && !d.hasUncommittedGitChanges && d.hasIntegrityDrift
4316
+ );
4317
+ const upToDate = diffs.filter(
4318
+ (d) => d.existsOnDisk && d.isRegistered && !d.hasUncommittedGitChanges && !d.hasIntegrityDrift
4319
+ );
4320
+ const missing = diffs.filter((d) => !d.existsOnDisk && d.isRegistered);
4321
+ if (unregistered.length > 0) {
4322
+ p15.log.message(pc15.yellow(`
4323
+ \u{1F4E6} Unregistered Modules (${unregistered.length}):`));
4324
+ for (const mod of unregistered) {
4325
+ p15.log.message(` ${pc15.red("\u25CF")} ${pc15.bold(`${mod.type}/${mod.name}`)} ${pc15.dim("(not registered in index.json)")}`);
4326
+ }
4327
+ }
4328
+ if (uncommitted.length > 0) {
4329
+ p15.log.message(pc15.yellow(`
4330
+ \u{1F4BE} Uncommitted Changes in Git (${uncommitted.length}):`));
4331
+ for (const mod of uncommitted) {
4332
+ p15.log.message(
4333
+ ` ${pc15.yellow("\u25B2")} ${pc15.bold(`${mod.type}/${mod.name}`)} ${pc15.dim(`(${mod.uncommittedFilesCount} modified/untracked files)`)}`
4334
+ );
4335
+ }
4336
+ }
4337
+ if (committedDrift.length > 0) {
4338
+ p15.log.message(pc15.cyan(`
4339
+ \u{1F680} Committed But Unregistered Changes (${committedDrift.length}):`));
4340
+ for (const mod of committedDrift) {
4341
+ p15.log.message(
4342
+ ` ${pc15.cyan("\u25C6")} ${pc15.bold(`${mod.type}/${mod.name}`)} ${pc15.dim(
4343
+ `local: ${mod.localIntegrity?.slice(0, 16)}... != registered: ${mod.registeredIntegrity?.slice(0, 16)}...`
4344
+ )}`
4345
+ );
4346
+ }
4347
+ }
4348
+ if (missing.length > 0) {
4349
+ p15.log.message(pc15.red(`
4350
+ \u26A0\uFE0F Missing on Disk (${missing.length}):`));
4351
+ for (const mod of missing) {
4352
+ p15.log.message(` ${pc15.red("\u2715")} ${pc15.bold(`${mod.type}/${mod.name}`)} ${pc15.dim("(in index.json, but folder missing)")}`);
4353
+ }
4354
+ }
4355
+ if (upToDate.length > 0) {
4356
+ p15.log.message(pc15.green(`
4357
+ \u2713 Up to Date & Registered (${upToDate.length}):`));
4358
+ for (const mod of upToDate) {
4359
+ p15.log.message(` ${pc15.green("\u2714")} ${mod.type}/${mod.name} ${pc15.dim(`(${mod.registeredIntegrity?.slice(0, 18)}...)`)}`);
4360
+ }
4361
+ }
4362
+ p15.log.message("");
4363
+ }
4364
+ async function handleRegistry(actionArg, typeArg, nameArg, flags = {}) {
4365
+ let action = actionArg?.trim().toLowerCase();
4366
+ if (!action) {
4367
+ action = handleCancel(
4368
+ await p15.select({
4369
+ message: "\u{1F3DB}\uFE0F Mason Registry: What would you like to do?",
4370
+ options: [
4371
+ {
4372
+ value: "diff",
4373
+ label: "Inspect Diff & Status",
4374
+ hint: "Summary of unregistered, uncommitted, and drifted modules"
4375
+ },
4376
+ {
4377
+ value: "register",
4378
+ label: "Register Module",
4379
+ hint: "Add an existing module to index.json with integrity checksum"
4380
+ },
4381
+ {
4382
+ value: "commit",
4383
+ label: "Commit Changes",
4384
+ hint: "Commit a module's working changes to git in the background"
4385
+ },
4386
+ {
4387
+ value: "register-changes",
4388
+ label: "Register Changes",
4389
+ hint: "Update index.json & registry.json with latest checksums"
4390
+ },
4391
+ {
4392
+ value: "unregister",
4393
+ label: "Unregister Module",
4394
+ hint: "Remove module from index.json (source code stays on disk)"
4395
+ }
4396
+ ]
4397
+ })
4398
+ );
4399
+ }
4400
+ switch (action) {
4401
+ case "diff":
4402
+ case "status":
4403
+ diffRegistry(flags);
4404
+ break;
4405
+ case "register":
4406
+ await registerModule(typeArg, nameArg, flags);
4407
+ break;
4408
+ case "commit":
4409
+ await commitModule(typeArg, nameArg, flags);
4410
+ break;
4411
+ case "register-changes":
4412
+ case "update":
4413
+ await registerModuleChanges(typeArg, nameArg, flags);
4414
+ break;
4415
+ case "unregister":
4416
+ await unregisterModule(typeArg, nameArg, flags);
4417
+ break;
4418
+ default:
4419
+ p15.log.error(
4420
+ pc15.red(
4421
+ `Unknown registry action: "${action}". Valid actions: diff, register, commit, register-changes, unregister.`
4422
+ )
4423
+ );
4424
+ process.exit(1);
4425
+ }
4426
+ }
3347
4427
 
3348
4428
  // src/cli.ts
3349
4429
  function createCli() {
3350
4430
  const cli = cac("mason");
3351
- cli.command("[...args]", "Interactive scaffolding and import wizard").option("-d, --dry-run", "Preview changes without writing to disk").option("-o, --overwrite", "Overwrite existing files").option("-y, --yes", "Accept defaults and skip interactive prompts").action(async (args, flags) => {
4431
+ 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) => {
3352
4432
  if (args && args.length > 0) {
3353
- p3.log.error(
3354
- pc13.red(
4433
+ p15.log.error(
4434
+ pc15.red(
3355
4435
  `Unknown command: "${args.join(" ")}". Use --help to list available commands.`
3356
4436
  )
3357
4437
  );
3358
4438
  process.exit(1);
3359
4439
  }
3360
4440
  try {
4441
+ const projectRoot = findProjectRoot();
4442
+ const mode = getEffectiveMode(projectRoot, flags);
4443
+ if (mode === "registry") {
4444
+ const choice2 = handleCancel(
4445
+ await p15.select({
4446
+ message: "\u{1F3DB}\uFE0F Mason Registry Maintainer: What would you like to do?",
4447
+ options: [
4448
+ {
4449
+ value: "diff",
4450
+ label: "Inspect Diff & Status",
4451
+ hint: "Overview of unregistered, uncommitted, or drifted modules"
4452
+ },
4453
+ {
4454
+ value: "create_feature",
4455
+ label: "Initialize New Feature",
4456
+ hint: "Scaffold a new feature slice under features/"
4457
+ },
4458
+ {
4459
+ value: "create_shared",
4460
+ label: "Initialize New Shared Module",
4461
+ hint: "Scaffold a new shared utility/port under shared/"
4462
+ },
4463
+ {
4464
+ value: "create_usecase",
4465
+ label: "Create New Use Case",
4466
+ hint: "Add a new use case to an existing feature"
4467
+ },
4468
+ {
4469
+ value: "register",
4470
+ label: "Register Module",
4471
+ hint: "Add a module to index.json with integrity checksum"
4472
+ },
4473
+ {
4474
+ value: "commit",
4475
+ label: "Commit Module Changes",
4476
+ hint: "Commit working changes in a module to git"
4477
+ },
4478
+ {
4479
+ value: "register_changes",
4480
+ label: "Register Changes",
4481
+ hint: "Recompute checksums and update index.json"
4482
+ },
4483
+ {
4484
+ value: "unregister",
4485
+ label: "Unregister Module",
4486
+ hint: "Remove from index.json but keep source on disk"
4487
+ },
4488
+ {
4489
+ value: "import",
4490
+ label: "Import from Remote Registry",
4491
+ hint: "Download a module from upstream"
4492
+ }
4493
+ ]
4494
+ })
4495
+ );
4496
+ switch (choice2) {
4497
+ case "diff":
4498
+ await handleRegistry("diff", void 0, void 0, flags);
4499
+ break;
4500
+ case "create_feature":
4501
+ await handleCreate("feature", void 0, flags);
4502
+ break;
4503
+ case "create_shared":
4504
+ await handleCreate("shared", void 0, flags);
4505
+ break;
4506
+ case "create_usecase":
4507
+ await handleCreate("usecase", void 0, flags);
4508
+ break;
4509
+ case "register":
4510
+ await handleRegistry("register", void 0, void 0, flags);
4511
+ break;
4512
+ case "commit":
4513
+ await handleRegistry("commit", void 0, void 0, flags);
4514
+ break;
4515
+ case "register_changes":
4516
+ await handleRegistry("register-changes", void 0, void 0, flags);
4517
+ break;
4518
+ case "unregister":
4519
+ await handleRegistry("unregister", void 0, void 0, flags);
4520
+ break;
4521
+ case "import":
4522
+ await handleImport(void 0, void 0, flags);
4523
+ break;
4524
+ }
4525
+ return;
4526
+ }
3361
4527
  const choice = handleCancel(
3362
- await p3.select({
4528
+ await p15.select({
3363
4529
  message: "\u{1F9F1} Mason: What would you like to do?",
3364
4530
  options: [
3365
4531
  {
3366
4532
  value: "create",
3367
4533
  label: "Create local component",
3368
- hint: "Scaffold a new feature, use case, domain entity, interface, etc."
4534
+ hint: "Scaffold a new feature, shared module, use case, domain entity, etc."
3369
4535
  },
3370
4536
  {
3371
4537
  value: "import",
3372
4538
  label: "Import from registry",
3373
4539
  hint: "Download and link pre-built shared modules or features from the registry"
4540
+ },
4541
+ {
4542
+ value: "registry",
4543
+ label: "Registry management",
4544
+ hint: "Inspect diff, register modules, or commit module changes"
3374
4545
  }
3375
4546
  ]
3376
4547
  })
3377
4548
  );
3378
4549
  if (choice === "create") {
3379
4550
  await handleCreate(void 0, void 0, flags);
4551
+ } else if (choice === "registry") {
4552
+ await handleRegistry(void 0, void 0, void 0, flags);
3380
4553
  } else {
3381
4554
  await handleImport(void 0, void 0, flags);
3382
4555
  }
@@ -3386,8 +4559,8 @@ function createCli() {
3386
4559
  });
3387
4560
  cli.command(
3388
4561
  "create [type] [name]",
3389
- "Scaffold local Clean Architecture components (feature, usecase, domain, etc.)"
3390
- ).option("-d, --dry-run", "Preview changes without writing to disk").option("-o, --overwrite", "Overwrite existing files").option("-y, --yes", "Accept defaults and skip interactive prompts").option("-f, --feature <feature>", "Target feature name").option("-t, --target <dir>", "Override target directory").option("--express", "Include Express presentation layer (pxExpress)").option("--no-express", "Skip Express presentation layer").option("-i, --interface <interface>", "Interface or abstract class to implement").option("-k, --kind <kind>", "Implementation kind ('stub' or 'concrete')").option("-g, --global", "Place in global infrastructure/ instead of feature").option("-m, --method <method>", "HTTP method (get, post, put, patch, delete)").option("-p, --path <path>", "Route subpath (e.g. /:id)").option("-u, --usecase <usecase>", "Connected use case name").option("--message <message>", "Custom success message").option("--domainNames <names>", "Comma-separated domain entity names").example("mason create").example("mason create feature users").example("mason create usecase PlaceOrder -f orders").example("mason create domain User -f users").example("mason create infrastructure PostgresUserRepository -f users -i IUserRepository").example("mason create interface IUserRepository -f users").example("mason create httpHandler GetUserHttp -f users -m get -p /:id").example("mason create injectable PasswordHasher").action(async (type, name, flags = {}) => {
4562
+ "Scaffold local Clean Architecture components (feature, shared, usecase, domain, etc.)"
4563
+ ).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 = {}) => {
3391
4564
  try {
3392
4565
  await handleCreate(type, name, flags);
3393
4566
  } catch (err) {
@@ -3404,8 +4577,28 @@ function createCli() {
3404
4577
  handleCliError(err);
3405
4578
  }
3406
4579
  });
4580
+ cli.command(
4581
+ "registry [action] [type] [name]",
4582
+ "Registry & version management (diff, register, commit, register-changes, unregister)"
4583
+ ).option("-m, --message <message>", "Commit message for 'commit' command").option("-y, --yes", "Accept defaults and skip interactive prompts").example("mason registry diff").example("mason registry register feature authn").example("mason registry commit shared hasher -m 'Add bcrypt engine'").example("mason registry register-changes shared hasher").example("mason registry unregister feature oldFeature").action(
4584
+ async (action, type, name, flags = {}) => {
4585
+ try {
4586
+ await handleRegistry(action, type, name, flags);
4587
+ } catch (err) {
4588
+ handleCliError(err);
4589
+ }
4590
+ }
4591
+ );
4592
+ cli.command("diff", "Inspect registry status and diff (alias for 'mason registry diff')").allowUnknownOptions().action(async (flags = {}) => {
4593
+ try {
4594
+ await handleRegistry("diff", void 0, void 0, flags);
4595
+ } catch (err) {
4596
+ handleCliError(err);
4597
+ }
4598
+ });
3407
4599
  const createSubtypes = [
3408
4600
  "feature",
4601
+ "shared",
3409
4602
  "usecase",
3410
4603
  "domain",
3411
4604
  "infrastructure",
@@ -3426,7 +4619,7 @@ function createCli() {
3426
4619
  });
3427
4620
  }
3428
4621
  cli.help();
3429
- cli.version("1.0.10");
4622
+ cli.version("1.0.13");
3430
4623
  return cli;
3431
4624
  }
3432
4625
  function handleCliError(err) {
@@ -3434,7 +4627,7 @@ function handleCliError(err) {
3434
4627
  process.exit(0);
3435
4628
  }
3436
4629
  const message = err instanceof Error ? err.message : String(err);
3437
- p3.log.error(pc13.red(`Error: ${message}`));
4630
+ p15.log.error(pc15.red(`Error: ${message}`));
3438
4631
  process.exit(1);
3439
4632
  }
3440
4633
  async function runCli(argv = process.argv) {