@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.cjs +1508 -315
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1504 -311
- package/dist/bin.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/feature.d.ts.map +1 -1
- package/dist/commands/import.d.ts +23 -0
- package/dist/commands/import.d.ts.map +1 -1
- package/dist/commands/menu.d.ts.map +1 -1
- package/dist/commands/registry.d.ts +64 -0
- package/dist/commands/registry.d.ts.map +1 -0
- package/dist/commands/shared.d.ts +2 -0
- package/dist/commands/shared.d.ts.map +1 -0
- package/dist/core/sharedGenerator.d.ts +13 -0
- package/dist/core/sharedGenerator.d.ts.map +1 -0
- package/dist/index.cjs +1531 -315
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1515 -312
- package/dist/index.js.map +1 -1
- package/dist/utils/config.d.ts +35 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/integrity.d.ts +11 -0
- package/dist/utils/integrity.d.ts.map +1 -0
- package/dist/utils/project.d.ts +18 -2
- package/dist/utils/project.d.ts.map +1 -1
- package/dist/utils/registry.d.ts +20 -0
- package/dist/utils/registry.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { cac } from 'cac';
|
|
3
|
-
import * as
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
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 =
|
|
14
|
-
const existedBefore =
|
|
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 =
|
|
34
|
-
if (!
|
|
35
|
-
|
|
34
|
+
const dir = path2.dirname(absolutePath);
|
|
35
|
+
if (!fs12.existsSync(dir)) {
|
|
36
|
+
fs12.mkdirSync(dir, { recursive: true });
|
|
36
37
|
}
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
+
p15.log.success(`${pc15.bold(f.relativePath)} ${pc15.dim("(created)")}`);
|
|
54
55
|
} else if (f.action === "overwritten") {
|
|
55
|
-
|
|
56
|
+
p15.log.warn(`${pc15.bold(f.relativePath)} ${pc15.dim("(overwritten)")}`);
|
|
56
57
|
} else if (f.action === "skipped") {
|
|
57
|
-
|
|
58
|
+
p15.log.info(`${pc15.dim(f.relativePath)} ${pc15.dim("(skipped - already exists, pass -o to overwrite)")}`);
|
|
58
59
|
} else {
|
|
59
|
-
|
|
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 =
|
|
189
|
+
let current = path2.resolve(startDir);
|
|
129
190
|
while (true) {
|
|
130
|
-
if (
|
|
191
|
+
if (fs12.existsSync(path2.join(current, "package.json"))) {
|
|
131
192
|
return current;
|
|
132
193
|
}
|
|
133
|
-
const parent =
|
|
194
|
+
const parent = path2.dirname(current);
|
|
134
195
|
if (parent === current) {
|
|
135
|
-
return
|
|
196
|
+
return path2.resolve(startDir);
|
|
136
197
|
}
|
|
137
198
|
current = parent;
|
|
138
199
|
}
|
|
139
200
|
}
|
|
140
|
-
function resolveFeaturesDir(projectRoot) {
|
|
141
|
-
return
|
|
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 (!
|
|
209
|
+
if (!fs12.existsSync(featuresDir)) return [];
|
|
146
210
|
try {
|
|
147
|
-
const entries =
|
|
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 =
|
|
158
|
-
if (!
|
|
221
|
+
const domainDir = path2.join(resolveFeaturesDir(projectRoot), feat, "domain");
|
|
222
|
+
if (!fs12.existsSync(domainDir)) continue;
|
|
159
223
|
try {
|
|
160
|
-
const files =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
196
|
-
if (!
|
|
259
|
+
const domainDir = path2.join(resolveFeaturesDir(projectRoot), feat, "domain");
|
|
260
|
+
if (!fs12.existsSync(domainDir)) continue;
|
|
197
261
|
try {
|
|
198
|
-
const files =
|
|
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:
|
|
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 =
|
|
221
|
-
if (!
|
|
284
|
+
const useCasesDir = path2.join(resolveFeaturesDir(projectRoot), feat, "useCases");
|
|
285
|
+
if (!fs12.existsSync(useCasesDir)) continue;
|
|
222
286
|
try {
|
|
223
|
-
const files =
|
|
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:
|
|
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 &&
|
|
319
|
+
if (cachedTemplatesDir && fs12.existsSync(cachedTemplatesDir)) {
|
|
256
320
|
return cachedTemplatesDir;
|
|
257
321
|
}
|
|
258
|
-
const currentDir = typeof __dirname !== "undefined" ? __dirname :
|
|
259
|
-
const cand1 =
|
|
260
|
-
if (
|
|
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 =
|
|
265
|
-
if (
|
|
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 =
|
|
270
|
-
if (
|
|
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 =
|
|
277
|
-
if (
|
|
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 =
|
|
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 =
|
|
290
|
-
return
|
|
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 ||
|
|
378
|
-
const filePath =
|
|
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((
|
|
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 ||
|
|
428
|
-
const filePath =
|
|
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 &&
|
|
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 =
|
|
674
|
-
if (!
|
|
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 =
|
|
677
|
-
if (
|
|
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 =
|
|
684
|
-
if (
|
|
685
|
-
const p22 =
|
|
686
|
-
if (
|
|
687
|
-
const p32 =
|
|
688
|
-
if (
|
|
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 =
|
|
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 ? `${
|
|
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 ? `${
|
|
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 ?
|
|
882
|
-
const filePath =
|
|
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 =
|
|
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 =
|
|
910
|
-
let rel =
|
|
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
|
|
984
|
-
const rawType =
|
|
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((
|
|
991
|
-
name:
|
|
992
|
-
optional:
|
|
993
|
-
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((
|
|
996
|
-
name:
|
|
997
|
-
optional:
|
|
998
|
-
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 ||
|
|
1098
|
-
const filePath =
|
|
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 =
|
|
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 =
|
|
1183
|
-
const handlersDir =
|
|
1184
|
-
const pxIndexPath =
|
|
1185
|
-
if (!
|
|
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 =
|
|
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 =
|
|
1283
|
+
const providerPath = path2.join(featureDir, "diProvider.ts");
|
|
1220
1284
|
const pascalFeature = toPascalCase(featureName);
|
|
1221
1285
|
const providerModuleName = `${pascalFeature}Provider`;
|
|
1222
|
-
if (
|
|
1223
|
-
let content =
|
|
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 ||
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
1640
|
-
const pxIndexPath =
|
|
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 (
|
|
1670
|
-
|
|
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
|
-
|
|
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((
|
|
1683
|
-
|
|
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
|
|
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
|
-
|
|
1758
|
+
p15.log.info(pc15.cyan("Enter properties (leave name blank when finished):"));
|
|
1695
1759
|
while (true) {
|
|
1696
1760
|
const name = handleCancel(
|
|
1697
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1742
|
-
|
|
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
|
|
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
|
|
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
|
|
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 ?
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
2000
|
-
s.start(`Generating feature '${
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
2147
|
-
s.start(`Generating use case '${
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
2250
|
-
s.start(`Generating domain model '${
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
2432
|
-
s.start(`Generating infrastructure class '${
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
2580
|
-
s.start(`Generating interface '${
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
2746
|
-
s.start(`Generating HTTP handler '${
|
|
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
|
-
|
|
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((
|
|
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 =
|
|
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 =
|
|
3063
|
+
targetDir = path2.join(resolveFeaturesDir(projectRoot), options.feature, "services");
|
|
2800
3064
|
} else if (options.location === "feature-transformer" && options.feature) {
|
|
2801
|
-
targetDir =
|
|
3065
|
+
targetDir = path2.join(resolveFeaturesDir(projectRoot), options.feature, "pxExpress", "transformers");
|
|
2802
3066
|
} else if (options.location === "module") {
|
|
2803
|
-
targetDir =
|
|
3067
|
+
targetDir = path2.join(projectRoot, "modules", className);
|
|
2804
3068
|
} else {
|
|
2805
|
-
targetDir = options.feature ?
|
|
3069
|
+
targetDir = options.feature ? path2.join(resolveFeaturesDir(projectRoot), options.feature, "services") : path2.join(projectRoot, "shared", className.toLowerCase());
|
|
2806
3070
|
}
|
|
2807
|
-
const filePath =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
2920
|
-
s.start(`Generating injectable '${
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3037
|
-
|
|
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
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
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(
|
|
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
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
3087
|
-
if (!
|
|
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
|
|
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 (
|
|
3153
|
-
if (
|
|
3154
|
-
if (
|
|
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 =
|
|
3160
|
-
if (!
|
|
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(
|
|
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((
|
|
3171
|
-
const nameOnly =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
3233
|
-
s.start(`Connecting to registry at ${
|
|
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(
|
|
3240
|
-
|
|
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
|
-
|
|
3774
|
+
p15.log.warn(`No ${type} modules available in the registry.`);
|
|
3249
3775
|
return;
|
|
3250
3776
|
}
|
|
3251
3777
|
name = handleCancel(
|
|
3252
|
-
await
|
|
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
|
-
|
|
3790
|
+
p15.log.error(pc15.red(`\u274C Module '${name}' not found in the ${type} registry.`));
|
|
3265
3791
|
if (closest) {
|
|
3266
|
-
|
|
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,
|
|
3274
|
-
const modKey = `${modType}:${
|
|
3799
|
+
async function importModuleRecursive(modType, registryName, targetName = registryName, options) {
|
|
3800
|
+
const modKey = `${modType}:${targetName}`;
|
|
3275
3801
|
if (importedModules.has(modKey)) return;
|
|
3276
|
-
|
|
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,
|
|
3807
|
+
manifest = await fetchModuleManifest(registryBase, modType, registryName);
|
|
3280
3808
|
} catch (err) {
|
|
3281
|
-
|
|
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
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
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
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
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 =
|
|
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}/${
|
|
3308
|
-
const localFilePath =
|
|
3309
|
-
if (
|
|
3310
|
-
|
|
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
|
-
|
|
3861
|
+
let content = await fetchRegistryFile(registryBase, remoteFilePath);
|
|
3862
|
+
content = rewriteImports(content, aliasMap);
|
|
3314
3863
|
if (!flags.dryRun) {
|
|
3315
|
-
|
|
3316
|
-
|
|
3864
|
+
fs12.mkdirSync(path2.dirname(localFilePath), { recursive: true });
|
|
3865
|
+
fs12.writeFileSync(localFilePath, content, "utf-8");
|
|
3317
3866
|
}
|
|
3318
|
-
const displayPath =
|
|
3867
|
+
const displayPath = path2.relative(projectRoot, localFilePath);
|
|
3319
3868
|
totalWrittenFiles.push(displayPath);
|
|
3320
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3887
|
+
p15.log.info(pc15.yellow("Dry run complete: No files were written to disk."));
|
|
3339
3888
|
} else {
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
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
|
-
|
|
3354
|
-
|
|
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
|
|
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,
|
|
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.
|
|
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
|
-
|
|
4630
|
+
p15.log.error(pc15.red(`Error: ${message}`));
|
|
3438
4631
|
process.exit(1);
|
|
3439
4632
|
}
|
|
3440
4633
|
async function runCli(argv = process.argv) {
|