@solid-stack/mason 1.0.12 → 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 +1446 -318
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1442 -314
- 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 +1467 -318
- 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 +1453 -315
- 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 +2 -0
- package/dist/utils/registry.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import * as
|
|
4
|
-
import
|
|
1
|
+
import fs12 from 'fs';
|
|
2
|
+
import path2 from 'path';
|
|
3
|
+
import * as p15 from '@clack/prompts';
|
|
4
|
+
import pc15 from 'picocolors';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { execSync } from 'child_process';
|
|
7
|
+
import { createHash } from 'crypto';
|
|
7
8
|
import Handlebars from 'handlebars';
|
|
8
9
|
import { cac } from 'cac';
|
|
9
10
|
|
|
@@ -75,8 +76,8 @@ function isValidIdentifier(str) {
|
|
|
75
76
|
}
|
|
76
77
|
function writeFileSafely(absolutePath, content, options = {}) {
|
|
77
78
|
const projectRoot = options.projectRoot || process.cwd();
|
|
78
|
-
const relPath =
|
|
79
|
-
const existedBefore =
|
|
79
|
+
const relPath = path2.relative(projectRoot, absolutePath).replace(/\\/g, "/");
|
|
80
|
+
const existedBefore = fs12.existsSync(absolutePath);
|
|
80
81
|
if (options.dryRun) {
|
|
81
82
|
return {
|
|
82
83
|
relativePath: relPath,
|
|
@@ -95,11 +96,11 @@ function writeFileSafely(absolutePath, content, options = {}) {
|
|
|
95
96
|
existedBefore: true
|
|
96
97
|
};
|
|
97
98
|
}
|
|
98
|
-
const dir =
|
|
99
|
-
if (!
|
|
100
|
-
|
|
99
|
+
const dir = path2.dirname(absolutePath);
|
|
100
|
+
if (!fs12.existsSync(dir)) {
|
|
101
|
+
fs12.mkdirSync(dir, { recursive: true });
|
|
101
102
|
}
|
|
102
|
-
|
|
103
|
+
fs12.writeFileSync(absolutePath, content, "utf-8");
|
|
103
104
|
return {
|
|
104
105
|
relativePath: relPath,
|
|
105
106
|
absolutePath,
|
|
@@ -111,41 +112,114 @@ function writeFileSafely(absolutePath, content, options = {}) {
|
|
|
111
112
|
function printGeneratedFiles(files, isDryRun = false) {
|
|
112
113
|
console.log("");
|
|
113
114
|
if (isDryRun) {
|
|
114
|
-
|
|
115
|
+
p15.log.warn(pc15.yellow(pc15.bold("[DRY RUN PREVIEW] No files were written to disk.")));
|
|
115
116
|
}
|
|
116
117
|
for (const f of files) {
|
|
117
118
|
if (f.action === "created") {
|
|
118
|
-
|
|
119
|
+
p15.log.success(`${pc15.bold(f.relativePath)} ${pc15.dim("(created)")}`);
|
|
119
120
|
} else if (f.action === "overwritten") {
|
|
120
|
-
|
|
121
|
+
p15.log.warn(`${pc15.bold(f.relativePath)} ${pc15.dim("(overwritten)")}`);
|
|
121
122
|
} else if (f.action === "skipped") {
|
|
122
|
-
|
|
123
|
+
p15.log.info(`${pc15.dim(f.relativePath)} ${pc15.dim("(skipped - already exists, pass -o to overwrite)")}`);
|
|
123
124
|
} else {
|
|
124
|
-
|
|
125
|
+
p15.log.info(`${pc15.bold(f.relativePath)} ${pc15.dim("(preview)")}`);
|
|
125
126
|
}
|
|
126
127
|
}
|
|
127
128
|
}
|
|
129
|
+
function loadMasonConfig(startDir = process.cwd()) {
|
|
130
|
+
let current = path2.resolve(startDir);
|
|
131
|
+
while (true) {
|
|
132
|
+
const configPath = path2.join(current, "mason.config.json");
|
|
133
|
+
if (fs12.existsSync(configPath)) {
|
|
134
|
+
try {
|
|
135
|
+
return JSON.parse(fs12.readFileSync(configPath, "utf-8"));
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const parent = path2.dirname(current);
|
|
141
|
+
if (parent === current) break;
|
|
142
|
+
current = parent;
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
function getEffectiveMode(projectRoot, flags) {
|
|
147
|
+
if (flags?.registry) return "registry";
|
|
148
|
+
const config = loadMasonConfig(projectRoot);
|
|
149
|
+
if (config?.mode) return config.mode;
|
|
150
|
+
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"))) {
|
|
151
|
+
return "registry";
|
|
152
|
+
}
|
|
153
|
+
return "app";
|
|
154
|
+
}
|
|
155
|
+
function getConfiguredFeaturesDir(projectRoot, flags) {
|
|
156
|
+
if (flags?.target) return path2.resolve(projectRoot, flags.target);
|
|
157
|
+
if (flags?.dest) return path2.resolve(projectRoot, flags.dest);
|
|
158
|
+
const config = loadMasonConfig(projectRoot);
|
|
159
|
+
if (config?.paths?.features) {
|
|
160
|
+
return path2.resolve(projectRoot, config.paths.features);
|
|
161
|
+
}
|
|
162
|
+
const mode = getEffectiveMode(projectRoot, flags);
|
|
163
|
+
if (mode === "registry") {
|
|
164
|
+
return path2.join(projectRoot, "features");
|
|
165
|
+
}
|
|
166
|
+
if (fs12.existsSync(path2.join(projectRoot, "src/features"))) {
|
|
167
|
+
return path2.join(projectRoot, "src/features");
|
|
168
|
+
}
|
|
169
|
+
return path2.join(projectRoot, "features");
|
|
170
|
+
}
|
|
171
|
+
function getConfiguredSharedDir(projectRoot, flags) {
|
|
172
|
+
if (flags?.target) return path2.resolve(projectRoot, flags.target);
|
|
173
|
+
if (flags?.dest) return path2.resolve(projectRoot, flags.dest);
|
|
174
|
+
const config = loadMasonConfig(projectRoot);
|
|
175
|
+
if (config?.paths?.shared) {
|
|
176
|
+
return path2.resolve(projectRoot, config.paths.shared);
|
|
177
|
+
}
|
|
178
|
+
const mode = getEffectiveMode(projectRoot, flags);
|
|
179
|
+
if (mode === "registry") {
|
|
180
|
+
return path2.join(projectRoot, "shared");
|
|
181
|
+
}
|
|
182
|
+
if (fs12.existsSync(path2.join(projectRoot, "src/shared"))) {
|
|
183
|
+
return path2.join(projectRoot, "src/shared");
|
|
184
|
+
}
|
|
185
|
+
return path2.join(projectRoot, "shared");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/utils/project.ts
|
|
128
189
|
function findProjectRoot(startDir = process.cwd()) {
|
|
129
|
-
let current =
|
|
190
|
+
let current = path2.resolve(startDir);
|
|
130
191
|
while (true) {
|
|
131
|
-
if (
|
|
192
|
+
if (fs12.existsSync(path2.join(current, "package.json"))) {
|
|
132
193
|
return current;
|
|
133
194
|
}
|
|
134
|
-
const parent =
|
|
195
|
+
const parent = path2.dirname(current);
|
|
135
196
|
if (parent === current) {
|
|
136
|
-
return
|
|
197
|
+
return path2.resolve(startDir);
|
|
137
198
|
}
|
|
138
199
|
current = parent;
|
|
139
200
|
}
|
|
140
201
|
}
|
|
141
|
-
function resolveFeaturesDir(projectRoot) {
|
|
142
|
-
return
|
|
202
|
+
function resolveFeaturesDir(projectRoot, flags) {
|
|
203
|
+
return getConfiguredFeaturesDir(projectRoot, flags);
|
|
204
|
+
}
|
|
205
|
+
function resolveSharedDir(projectRoot, flags) {
|
|
206
|
+
return getConfiguredSharedDir(projectRoot, flags);
|
|
207
|
+
}
|
|
208
|
+
function listExistingShared(projectRoot) {
|
|
209
|
+
const sharedDir = resolveSharedDir(projectRoot);
|
|
210
|
+
if (!fs12.existsSync(sharedDir)) return [];
|
|
211
|
+
try {
|
|
212
|
+
const entries = fs12.readdirSync(sharedDir, { withFileTypes: true });
|
|
213
|
+
return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort();
|
|
214
|
+
} catch {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
143
217
|
}
|
|
144
218
|
function listExistingFeatures(projectRoot) {
|
|
145
219
|
const featuresDir = resolveFeaturesDir(projectRoot);
|
|
146
|
-
if (!
|
|
220
|
+
if (!fs12.existsSync(featuresDir)) return [];
|
|
147
221
|
try {
|
|
148
|
-
const entries =
|
|
222
|
+
const entries = fs12.readdirSync(featuresDir, { withFileTypes: true });
|
|
149
223
|
return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
150
224
|
} catch {
|
|
151
225
|
return [];
|
|
@@ -155,14 +229,14 @@ function listDomainInterfaces(projectRoot, featureName) {
|
|
|
155
229
|
const results = [];
|
|
156
230
|
const features = featureName ? [featureName] : listExistingFeatures(projectRoot);
|
|
157
231
|
for (const feat of features) {
|
|
158
|
-
const domainDir =
|
|
159
|
-
if (!
|
|
232
|
+
const domainDir = path2.join(resolveFeaturesDir(projectRoot), feat, "domain");
|
|
233
|
+
if (!fs12.existsSync(domainDir)) continue;
|
|
160
234
|
try {
|
|
161
|
-
const files =
|
|
235
|
+
const files = fs12.readdirSync(domainDir);
|
|
162
236
|
for (const file of files) {
|
|
163
237
|
if (file.endsWith(".ts") && !file.endsWith(".test.ts") && !file.endsWith(".d.ts")) {
|
|
164
238
|
const name = file.replace(/\.ts$/, "");
|
|
165
|
-
const fullPath =
|
|
239
|
+
const fullPath = path2.join(domainDir, file);
|
|
166
240
|
if (/^I[A-Z]/.test(name)) {
|
|
167
241
|
results.push({
|
|
168
242
|
name,
|
|
@@ -171,7 +245,7 @@ function listDomainInterfaces(projectRoot, featureName) {
|
|
|
171
245
|
});
|
|
172
246
|
} else {
|
|
173
247
|
try {
|
|
174
|
-
const content =
|
|
248
|
+
const content = fs12.readFileSync(fullPath, "utf-8");
|
|
175
249
|
if (/export\s+(?:abstract\s+class|interface)\s+(\w+)/.test(content)) {
|
|
176
250
|
results.push({
|
|
177
251
|
name,
|
|
@@ -193,17 +267,17 @@ function listDomainEntities(projectRoot, featureName) {
|
|
|
193
267
|
const results = [];
|
|
194
268
|
const features = featureName ? [featureName] : listExistingFeatures(projectRoot);
|
|
195
269
|
for (const feat of features) {
|
|
196
|
-
const domainDir =
|
|
197
|
-
if (!
|
|
270
|
+
const domainDir = path2.join(resolveFeaturesDir(projectRoot), feat, "domain");
|
|
271
|
+
if (!fs12.existsSync(domainDir)) continue;
|
|
198
272
|
try {
|
|
199
|
-
const files =
|
|
273
|
+
const files = fs12.readdirSync(domainDir);
|
|
200
274
|
for (const file of files) {
|
|
201
275
|
if (file.endsWith(".ts") && !file.endsWith(".test.ts") && !file.endsWith(".d.ts")) {
|
|
202
276
|
const name = file.replace(/\.ts$/, "");
|
|
203
277
|
if (!/^I[A-Z]/.test(name)) {
|
|
204
278
|
results.push({
|
|
205
279
|
name,
|
|
206
|
-
fullPath:
|
|
280
|
+
fullPath: path2.join(domainDir, file),
|
|
207
281
|
feature: feat
|
|
208
282
|
});
|
|
209
283
|
}
|
|
@@ -218,16 +292,16 @@ function listUseCases(projectRoot, featureName) {
|
|
|
218
292
|
const results = [];
|
|
219
293
|
const features = featureName ? [featureName] : listExistingFeatures(projectRoot);
|
|
220
294
|
for (const feat of features) {
|
|
221
|
-
const useCasesDir =
|
|
222
|
-
if (!
|
|
295
|
+
const useCasesDir = path2.join(resolveFeaturesDir(projectRoot), feat, "useCases");
|
|
296
|
+
if (!fs12.existsSync(useCasesDir)) continue;
|
|
223
297
|
try {
|
|
224
|
-
const files =
|
|
298
|
+
const files = fs12.readdirSync(useCasesDir);
|
|
225
299
|
for (const file of files) {
|
|
226
300
|
if (file.endsWith(".ts") && !file.endsWith(".test.ts") && !file.endsWith(".d.ts")) {
|
|
227
301
|
const name = file.replace(/\.ts$/, "");
|
|
228
302
|
results.push({
|
|
229
303
|
name,
|
|
230
|
-
fullPath:
|
|
304
|
+
fullPath: path2.join(useCasesDir, file),
|
|
231
305
|
feature: feat
|
|
232
306
|
});
|
|
233
307
|
}
|
|
@@ -244,23 +318,23 @@ var PromptCancelledError = class extends Error {
|
|
|
244
318
|
}
|
|
245
319
|
};
|
|
246
320
|
function handleCancel(result) {
|
|
247
|
-
if (
|
|
248
|
-
|
|
321
|
+
if (p15.isCancel(result)) {
|
|
322
|
+
p15.cancel("Operation cancelled.");
|
|
249
323
|
throw new PromptCancelledError();
|
|
250
324
|
}
|
|
251
325
|
return result;
|
|
252
326
|
}
|
|
253
327
|
function printBanner() {
|
|
254
328
|
console.log("");
|
|
255
|
-
|
|
329
|
+
p15.intro(pc15.bgCyan(pc15.black(" @solid-stack/mason ")));
|
|
256
330
|
}
|
|
257
331
|
async function promptProperties(options = {}) {
|
|
258
332
|
const properties = options.defaults ? [...options.defaults] : [];
|
|
259
333
|
if (properties.length > 0) {
|
|
260
|
-
const summary = properties.map((
|
|
261
|
-
|
|
334
|
+
const summary = properties.map((p17) => `${p17.name}: ${p17.type}${p17.optional ? "?" : ""}`).join(", ");
|
|
335
|
+
p15.log.info(pc15.dim(`Pre-configured fields: ${summary}`));
|
|
262
336
|
const addMore = handleCancel(
|
|
263
|
-
await
|
|
337
|
+
await p15.confirm({
|
|
264
338
|
message: "Would you like to add custom fields/properties?",
|
|
265
339
|
initialValue: false
|
|
266
340
|
})
|
|
@@ -269,10 +343,10 @@ async function promptProperties(options = {}) {
|
|
|
269
343
|
return properties;
|
|
270
344
|
}
|
|
271
345
|
}
|
|
272
|
-
|
|
346
|
+
p15.log.info(pc15.cyan("Enter properties (leave name blank when finished):"));
|
|
273
347
|
while (true) {
|
|
274
348
|
const name = handleCancel(
|
|
275
|
-
await
|
|
349
|
+
await p15.text({
|
|
276
350
|
message: "Property name (or press Enter to finish):",
|
|
277
351
|
placeholder: "e.g. title, email, status"
|
|
278
352
|
})
|
|
@@ -281,7 +355,7 @@ async function promptProperties(options = {}) {
|
|
|
281
355
|
break;
|
|
282
356
|
}
|
|
283
357
|
const type = handleCancel(
|
|
284
|
-
await
|
|
358
|
+
await p15.select({
|
|
285
359
|
message: `Type for property "${name.trim()}":`,
|
|
286
360
|
options: [
|
|
287
361
|
{ value: "string", label: "string" },
|
|
@@ -298,7 +372,7 @@ async function promptProperties(options = {}) {
|
|
|
298
372
|
let finalType = type;
|
|
299
373
|
if (type === "custom") {
|
|
300
374
|
finalType = handleCancel(
|
|
301
|
-
await
|
|
375
|
+
await p15.text({
|
|
302
376
|
message: `Enter custom type for "${name.trim()}":`,
|
|
303
377
|
placeholder: "e.g. UserRole, string | null",
|
|
304
378
|
validate: (val) => val && val.trim() ? void 0 : "Type cannot be empty"
|
|
@@ -306,7 +380,7 @@ async function promptProperties(options = {}) {
|
|
|
306
380
|
);
|
|
307
381
|
}
|
|
308
382
|
const optional = handleCancel(
|
|
309
|
-
await
|
|
383
|
+
await p15.confirm({
|
|
310
384
|
message: `Is "${name.trim()}" optional?`,
|
|
311
385
|
initialValue: false
|
|
312
386
|
})
|
|
@@ -316,8 +390,8 @@ async function promptProperties(options = {}) {
|
|
|
316
390
|
type: finalType.trim(),
|
|
317
391
|
optional
|
|
318
392
|
});
|
|
319
|
-
|
|
320
|
-
|
|
393
|
+
p15.log.success(
|
|
394
|
+
pc15.green(`Added: ${name.trim()}${optional ? "?" : ""}: ${finalType.trim()}`)
|
|
321
395
|
);
|
|
322
396
|
}
|
|
323
397
|
return properties;
|
|
@@ -339,25 +413,25 @@ function normalizeRegistryUrl(urlOrPath, baseDir) {
|
|
|
339
413
|
return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}`;
|
|
340
414
|
}
|
|
341
415
|
if (baseDir && (trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed === "." || trimmed === "..")) {
|
|
342
|
-
return
|
|
416
|
+
return path2.resolve(baseDir, trimmed);
|
|
343
417
|
}
|
|
344
418
|
return trimmed;
|
|
345
419
|
}
|
|
346
420
|
function findLocalRegistry(startDir = process.cwd()) {
|
|
347
|
-
let current =
|
|
421
|
+
let current = path2.resolve(startDir);
|
|
348
422
|
while (true) {
|
|
349
|
-
const candidate =
|
|
350
|
-
if (
|
|
423
|
+
const candidate = path2.join(current, "mason-registry");
|
|
424
|
+
if (fs12.existsSync(candidate) && fs12.existsSync(path2.join(candidate, "index.json"))) {
|
|
351
425
|
return candidate;
|
|
352
426
|
}
|
|
353
|
-
const parent =
|
|
427
|
+
const parent = path2.dirname(current);
|
|
354
428
|
if (parent === current) break;
|
|
355
429
|
current = parent;
|
|
356
430
|
}
|
|
357
431
|
return null;
|
|
358
432
|
}
|
|
359
433
|
function getRegistryBase(projectRoot, override) {
|
|
360
|
-
const rootDir = projectRoot ?
|
|
434
|
+
const rootDir = projectRoot ? path2.resolve(projectRoot) : process.cwd();
|
|
361
435
|
if (override && override.trim()) {
|
|
362
436
|
return normalizeRegistryUrl(override.trim(), rootDir);
|
|
363
437
|
}
|
|
@@ -365,30 +439,30 @@ function getRegistryBase(projectRoot, override) {
|
|
|
365
439
|
return normalizeRegistryUrl(process.env.MASON_REGISTRY_URL.trim(), rootDir);
|
|
366
440
|
}
|
|
367
441
|
const configLocations = [
|
|
368
|
-
|
|
369
|
-
|
|
442
|
+
path2.join(rootDir, "mason.config.json"),
|
|
443
|
+
path2.join(process.cwd(), "mason.config.json")
|
|
370
444
|
];
|
|
371
445
|
for (const configPath of configLocations) {
|
|
372
|
-
if (
|
|
446
|
+
if (fs12.existsSync(configPath)) {
|
|
373
447
|
try {
|
|
374
|
-
const config = JSON.parse(
|
|
448
|
+
const config = JSON.parse(fs12.readFileSync(configPath, "utf-8"));
|
|
375
449
|
if (config.registry && typeof config.registry === "string") {
|
|
376
|
-
return normalizeRegistryUrl(config.registry.trim(),
|
|
450
|
+
return normalizeRegistryUrl(config.registry.trim(), path2.dirname(configPath));
|
|
377
451
|
}
|
|
378
452
|
} catch {
|
|
379
453
|
}
|
|
380
454
|
}
|
|
381
455
|
}
|
|
382
456
|
const pkgLocations = [
|
|
383
|
-
|
|
384
|
-
|
|
457
|
+
path2.join(rootDir, "package.json"),
|
|
458
|
+
path2.join(process.cwd(), "package.json")
|
|
385
459
|
];
|
|
386
460
|
for (const pkgPath of pkgLocations) {
|
|
387
|
-
if (
|
|
461
|
+
if (fs12.existsSync(pkgPath)) {
|
|
388
462
|
try {
|
|
389
|
-
const pkg = JSON.parse(
|
|
463
|
+
const pkg = JSON.parse(fs12.readFileSync(pkgPath, "utf-8"));
|
|
390
464
|
if (pkg.mason?.registry && typeof pkg.mason.registry === "string") {
|
|
391
|
-
return normalizeRegistryUrl(pkg.mason.registry.trim(),
|
|
465
|
+
return normalizeRegistryUrl(pkg.mason.registry.trim(), path2.dirname(pkgPath));
|
|
392
466
|
}
|
|
393
467
|
} catch {
|
|
394
468
|
}
|
|
@@ -429,11 +503,11 @@ async function fetchRegistryFile(registryBase, relPath) {
|
|
|
429
503
|
if (baseDir.startsWith("file://")) {
|
|
430
504
|
baseDir = fileURLToPath(baseDir);
|
|
431
505
|
}
|
|
432
|
-
const filePath =
|
|
433
|
-
if (!
|
|
506
|
+
const filePath = path2.resolve(baseDir, cleanRelPath);
|
|
507
|
+
if (!fs12.existsSync(filePath)) {
|
|
434
508
|
throw new Error(`Registry file not found at ${filePath}`);
|
|
435
509
|
}
|
|
436
|
-
return
|
|
510
|
+
return fs12.promises.readFile(filePath, "utf-8");
|
|
437
511
|
}
|
|
438
512
|
async function fetchRegistryIndex(registryBase) {
|
|
439
513
|
const content = await fetchRegistryFile(registryBase, "index.json");
|
|
@@ -495,26 +569,26 @@ function findClosestMatch(target, candidates) {
|
|
|
495
569
|
return null;
|
|
496
570
|
}
|
|
497
571
|
function detectPackageManager(projectRoot) {
|
|
498
|
-
if (
|
|
499
|
-
if (
|
|
500
|
-
if (
|
|
572
|
+
if (fs12.existsSync(path2.join(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
|
|
573
|
+
if (fs12.existsSync(path2.join(projectRoot, "yarn.lock"))) return "yarn";
|
|
574
|
+
if (fs12.existsSync(path2.join(projectRoot, "bun.lockb")) || fs12.existsSync(path2.join(projectRoot, "bun.lock"))) return "bun";
|
|
501
575
|
return "npm";
|
|
502
576
|
}
|
|
503
577
|
function getMissingNpmPackages(projectRoot, requiredPackages) {
|
|
504
578
|
if (!requiredPackages || requiredPackages.length === 0) return [];
|
|
505
|
-
const pkgJsonPath =
|
|
506
|
-
if (!
|
|
579
|
+
const pkgJsonPath = path2.join(projectRoot, "package.json");
|
|
580
|
+
if (!fs12.existsSync(pkgJsonPath)) {
|
|
507
581
|
return requiredPackages;
|
|
508
582
|
}
|
|
509
583
|
try {
|
|
510
|
-
const pkg = JSON.parse(
|
|
584
|
+
const pkg = JSON.parse(fs12.readFileSync(pkgJsonPath, "utf-8"));
|
|
511
585
|
const installed = /* @__PURE__ */ new Set([
|
|
512
586
|
...Object.keys(pkg.dependencies || {}),
|
|
513
587
|
...Object.keys(pkg.devDependencies || {}),
|
|
514
588
|
...Object.keys(pkg.peerDependencies || {})
|
|
515
589
|
]);
|
|
516
|
-
return requiredPackages.filter((
|
|
517
|
-
const nameOnly =
|
|
590
|
+
return requiredPackages.filter((p17) => {
|
|
591
|
+
const nameOnly = p17.startsWith("@") ? `@${p17.slice(1).split("@")[0] || ""}` : p17.split("@")[0] || p17;
|
|
518
592
|
return !installed.has(nameOnly);
|
|
519
593
|
});
|
|
520
594
|
} catch {
|
|
@@ -539,12 +613,47 @@ function installNpmPackages(projectRoot, packages, pm) {
|
|
|
539
613
|
installCmd = `npm install ${packages.join(" ")}`;
|
|
540
614
|
break;
|
|
541
615
|
}
|
|
542
|
-
|
|
616
|
+
p15.log.info(`Installing missing dependencies with ${manager}: ${pc15.cyan(packages.join(", "))}`);
|
|
543
617
|
execSync(installCmd, {
|
|
544
618
|
cwd: projectRoot,
|
|
545
619
|
stdio: "inherit"
|
|
546
620
|
});
|
|
547
621
|
}
|
|
622
|
+
function getAllFiles(dir, baseDir = dir) {
|
|
623
|
+
if (!fs12.existsSync(dir)) return [];
|
|
624
|
+
const entries = fs12.readdirSync(dir, { withFileTypes: true });
|
|
625
|
+
const files = [];
|
|
626
|
+
for (const entry of entries) {
|
|
627
|
+
const fullPath = path2.join(dir, entry.name);
|
|
628
|
+
const relPath = path2.relative(baseDir, fullPath).replace(/\\/g, "/");
|
|
629
|
+
if (entry.isDirectory()) {
|
|
630
|
+
if (entry.name === "__tests__" || entry.name.startsWith(".")) {
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
files.push(...getAllFiles(fullPath, baseDir));
|
|
634
|
+
} else if (entry.isFile()) {
|
|
635
|
+
if (entry.name === "registry.json" || entry.name.endsWith(".test.ts") || entry.name.endsWith(".spec.ts") || entry.name.startsWith(".")) {
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
files.push(relPath);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return files.sort();
|
|
642
|
+
}
|
|
643
|
+
function computeDirectoryHash(dirPath, files) {
|
|
644
|
+
if (!fs12.existsSync(dirPath)) return "";
|
|
645
|
+
const hash = createHash("sha256");
|
|
646
|
+
const fileList = (files ?? getAllFiles(dirPath)).sort();
|
|
647
|
+
for (const relativePath of fileList) {
|
|
648
|
+
const absolutePath = path2.join(dirPath, relativePath);
|
|
649
|
+
if (!fs12.existsSync(absolutePath)) continue;
|
|
650
|
+
const content = fs12.readFileSync(absolutePath, "utf8");
|
|
651
|
+
const normalizedContent = content.replace(/\r\n/g, "\n");
|
|
652
|
+
hash.update(relativePath);
|
|
653
|
+
hash.update(normalizedContent);
|
|
654
|
+
}
|
|
655
|
+
return `sha256-${hash.digest("hex")}`;
|
|
656
|
+
}
|
|
548
657
|
var hbs = Handlebars.create();
|
|
549
658
|
hbs.registerHelper("eq", (a, b) => a === b);
|
|
550
659
|
hbs.registerHelper("ne", (a, b) => a !== b);
|
|
@@ -561,33 +670,33 @@ hbs.registerHelper("join", (arr, sep) => {
|
|
|
561
670
|
});
|
|
562
671
|
var cachedTemplatesDir = null;
|
|
563
672
|
function getTemplatesDir() {
|
|
564
|
-
if (cachedTemplatesDir &&
|
|
673
|
+
if (cachedTemplatesDir && fs12.existsSync(cachedTemplatesDir)) {
|
|
565
674
|
return cachedTemplatesDir;
|
|
566
675
|
}
|
|
567
|
-
const currentDir = typeof __dirname !== "undefined" ? __dirname :
|
|
568
|
-
const cand1 =
|
|
569
|
-
if (
|
|
676
|
+
const currentDir = typeof __dirname !== "undefined" ? __dirname : path2.dirname(fileURLToPath(import.meta.url));
|
|
677
|
+
const cand1 = path2.resolve(currentDir, "../../templates");
|
|
678
|
+
if (fs12.existsSync(cand1)) {
|
|
570
679
|
cachedTemplatesDir = cand1;
|
|
571
680
|
return cand1;
|
|
572
681
|
}
|
|
573
|
-
const cand2 =
|
|
574
|
-
if (
|
|
682
|
+
const cand2 = path2.resolve(currentDir, "../templates");
|
|
683
|
+
if (fs12.existsSync(cand2)) {
|
|
575
684
|
cachedTemplatesDir = cand2;
|
|
576
685
|
return cand2;
|
|
577
686
|
}
|
|
578
|
-
const cand3 =
|
|
579
|
-
if (
|
|
687
|
+
const cand3 = path2.resolve(currentDir, "templates");
|
|
688
|
+
if (fs12.existsSync(cand3)) {
|
|
580
689
|
cachedTemplatesDir = cand3;
|
|
581
690
|
return cand3;
|
|
582
691
|
}
|
|
583
692
|
let curr = currentDir;
|
|
584
693
|
while (true) {
|
|
585
|
-
const candTemplates =
|
|
586
|
-
if (
|
|
694
|
+
const candTemplates = path2.join(curr, "templates");
|
|
695
|
+
if (fs12.existsSync(candTemplates)) {
|
|
587
696
|
cachedTemplatesDir = candTemplates;
|
|
588
697
|
return candTemplates;
|
|
589
698
|
}
|
|
590
|
-
const parent =
|
|
699
|
+
const parent = path2.dirname(curr);
|
|
591
700
|
if (parent === curr) break;
|
|
592
701
|
curr = parent;
|
|
593
702
|
}
|
|
@@ -595,8 +704,8 @@ function getTemplatesDir() {
|
|
|
595
704
|
}
|
|
596
705
|
var templateCache = /* @__PURE__ */ new Map();
|
|
597
706
|
function loadTemplate(relativePath) {
|
|
598
|
-
const fullPath =
|
|
599
|
-
return
|
|
707
|
+
const fullPath = path2.isAbsolute(relativePath) ? relativePath : path2.join(getTemplatesDir(), relativePath);
|
|
708
|
+
return fs12.readFileSync(fullPath, "utf-8");
|
|
600
709
|
}
|
|
601
710
|
function renderTemplateFile(relativePath, context) {
|
|
602
711
|
let compiled = templateCache.get(relativePath);
|
|
@@ -681,8 +790,8 @@ function generateDomainEntityCode(name, kind = "interface", properties = []) {
|
|
|
681
790
|
function generateDomain(options) {
|
|
682
791
|
const projectRoot = options.projectRoot || findProjectRoot();
|
|
683
792
|
const entityName = toPascalCase(options.name);
|
|
684
|
-
const targetDir = options.targetDir ||
|
|
685
|
-
const filePath =
|
|
793
|
+
const targetDir = options.targetDir || path2.join(resolveFeaturesDir(projectRoot), options.feature, "domain");
|
|
794
|
+
const filePath = path2.join(targetDir, `${entityName}.ts`);
|
|
686
795
|
const code = generateDomainEntityCode(
|
|
687
796
|
entityName,
|
|
688
797
|
options.kind || "interface",
|
|
@@ -706,7 +815,7 @@ function generateInterfaceCode(ifaceName, kind = "abstractClass", entityName, me
|
|
|
706
815
|
const idKey = `${entityKey}Id`;
|
|
707
816
|
const listKey = `${entityKey}s`;
|
|
708
817
|
const formattedCustomMethods = customMethods.map((m) => {
|
|
709
|
-
const params = m.params && m.params.length > 0 ? m.params.map((
|
|
818
|
+
const params = m.params && m.params.length > 0 ? m.params.map((p17) => `${p17.name}: ${p17.type}`).join("; ") : "";
|
|
710
819
|
const returnType = m.returnType ? `Promise<${m.returnType}>` : "Promise<void>";
|
|
711
820
|
return {
|
|
712
821
|
name: m.name,
|
|
@@ -731,8 +840,8 @@ function generateInterfaceCode(ifaceName, kind = "abstractClass", entityName, me
|
|
|
731
840
|
function generateInterface(options) {
|
|
732
841
|
const projectRoot = options.projectRoot || findProjectRoot();
|
|
733
842
|
const ifaceName = ensureInterfacePrefix(options.name);
|
|
734
|
-
const targetDir = options.targetDir ||
|
|
735
|
-
const filePath =
|
|
843
|
+
const targetDir = options.targetDir || path2.join(resolveFeaturesDir(projectRoot), options.feature, "domain");
|
|
844
|
+
const filePath = path2.join(targetDir, `${ifaceName}.ts`);
|
|
736
845
|
const code = generateInterfaceCode(
|
|
737
846
|
ifaceName,
|
|
738
847
|
options.kind || "abstractClass",
|
|
@@ -967,7 +1076,7 @@ function parseInterfaceMethodsAndImports(content, targetIfaceName) {
|
|
|
967
1076
|
};
|
|
968
1077
|
}
|
|
969
1078
|
function findInterfaceFile(projectRoot, ifaceName, featureName, customPath) {
|
|
970
|
-
if (customPath &&
|
|
1079
|
+
if (customPath && fs12.existsSync(customPath)) {
|
|
971
1080
|
return { filePath: customPath, ifaceName };
|
|
972
1081
|
}
|
|
973
1082
|
const candidateNames = [
|
|
@@ -977,22 +1086,22 @@ function findInterfaceFile(projectRoot, ifaceName, featureName, customPath) {
|
|
|
977
1086
|
];
|
|
978
1087
|
const features = featureName ? [featureName, ...listExistingFeatures(projectRoot).filter((f) => f !== featureName)] : listExistingFeatures(projectRoot);
|
|
979
1088
|
for (const feat of features) {
|
|
980
|
-
const domainDir =
|
|
981
|
-
if (!
|
|
1089
|
+
const domainDir = path2.join(resolveFeaturesDir(projectRoot), feat, "domain");
|
|
1090
|
+
if (!fs12.existsSync(domainDir)) continue;
|
|
982
1091
|
for (const cand of candidateNames) {
|
|
983
|
-
const p1 =
|
|
984
|
-
if (
|
|
1092
|
+
const p1 = path2.join(domainDir, `${cand}.ts`);
|
|
1093
|
+
if (fs12.existsSync(p1)) {
|
|
985
1094
|
return { filePath: p1, ifaceName: cand, feature: feat };
|
|
986
1095
|
}
|
|
987
1096
|
}
|
|
988
1097
|
}
|
|
989
1098
|
for (const cand of candidateNames) {
|
|
990
|
-
const p1 =
|
|
991
|
-
if (
|
|
992
|
-
const p22 =
|
|
993
|
-
if (
|
|
994
|
-
const p32 =
|
|
995
|
-
if (
|
|
1099
|
+
const p1 = path2.join(projectRoot, "domain", `${cand}.ts`);
|
|
1100
|
+
if (fs12.existsSync(p1)) return { filePath: p1, ifaceName: cand };
|
|
1101
|
+
const p22 = path2.join(projectRoot, "src", "domain", `${cand}.ts`);
|
|
1102
|
+
if (fs12.existsSync(p22)) return { filePath: p22, ifaceName: cand };
|
|
1103
|
+
const p32 = path2.join(projectRoot, "interfaces", `${cand}.ts`);
|
|
1104
|
+
if (fs12.existsSync(p32)) return { filePath: p32, ifaceName: cand };
|
|
996
1105
|
}
|
|
997
1106
|
return null;
|
|
998
1107
|
}
|
|
@@ -1015,7 +1124,7 @@ function loadInterfaceHierarchy(projectRoot, initialIfaceName, featureName, cust
|
|
|
1015
1124
|
while (currentFile && !visited.has(currentFile)) {
|
|
1016
1125
|
visited.add(currentFile);
|
|
1017
1126
|
try {
|
|
1018
|
-
const content =
|
|
1127
|
+
const content = fs12.readFileSync(currentFile, "utf-8");
|
|
1019
1128
|
const parsed = parseInterfaceMethodsAndImports(content, currentName);
|
|
1020
1129
|
for (const m of parsed.methods) {
|
|
1021
1130
|
if (!allMethods.some((existing) => existing.name === m.name)) {
|
|
@@ -1091,7 +1200,7 @@ function generateStubCode(stubClassName, ifaceName, entityName, featureName, met
|
|
|
1091
1200
|
const entitiesVar = `${entityVar}s`;
|
|
1092
1201
|
const idKey = `${entityVar}Id`;
|
|
1093
1202
|
const impPath = ifaceImportPath || `../domain/${ifaceName}.js`;
|
|
1094
|
-
const entityImportPath = ifaceImportPath ? `${
|
|
1203
|
+
const entityImportPath = ifaceImportPath ? `${path2.dirname(ifaceImportPath)}/${baseEntity}.js` : `../domain/${baseEntity}.js`;
|
|
1095
1204
|
const importsByFrom = /* @__PURE__ */ new Map();
|
|
1096
1205
|
for (const imp of domainImports) {
|
|
1097
1206
|
if (imp.name === ifaceName || imp.name === baseEntity) continue;
|
|
@@ -1140,7 +1249,7 @@ function generateConcreteInfraCode(className, ifaceName, dependencies = [], meth
|
|
|
1140
1249
|
if (baseEntity) {
|
|
1141
1250
|
finalMethods = synthesizeCrudMethods(baseEntity);
|
|
1142
1251
|
if (!finalDomainImports.some((imp) => imp.name === baseEntity)) {
|
|
1143
|
-
const importPath = ifaceImportPath ? `${
|
|
1252
|
+
const importPath = ifaceImportPath ? `${path2.dirname(ifaceImportPath)}/${baseEntity}.js` : `../domain/${baseEntity}.js`;
|
|
1144
1253
|
finalDomainImports.push({
|
|
1145
1254
|
name: baseEntity,
|
|
1146
1255
|
from: importPath,
|
|
@@ -1185,8 +1294,8 @@ function generateInfrastructure(options) {
|
|
|
1185
1294
|
const projectRoot = options.projectRoot || findProjectRoot();
|
|
1186
1295
|
const className = toPascalCase(options.name);
|
|
1187
1296
|
const isFeatureScope = options.scope !== "global" && Boolean(options.feature);
|
|
1188
|
-
const targetDir = options.targetDir || (isFeatureScope ?
|
|
1189
|
-
const filePath =
|
|
1297
|
+
const targetDir = options.targetDir || (isFeatureScope ? path2.join(resolveFeaturesDir(projectRoot), options.feature, "infrastructure") : path2.join(projectRoot, "infrastructure"));
|
|
1298
|
+
const filePath = path2.join(targetDir, `${className}.ts`);
|
|
1190
1299
|
let code;
|
|
1191
1300
|
if (options.implementsInterface) {
|
|
1192
1301
|
const rawIfaceName = options.implementsInterface;
|
|
@@ -1200,7 +1309,7 @@ function generateInfrastructure(options) {
|
|
|
1200
1309
|
const ifaceFilePath = hierarchy.ifaceFilePath;
|
|
1201
1310
|
let ifaceImportPath;
|
|
1202
1311
|
if (ifaceFilePath) {
|
|
1203
|
-
let rel =
|
|
1312
|
+
let rel = path2.relative(targetDir, ifaceFilePath).replace(/\\/g, "/").replace(/\.ts$/, ".js");
|
|
1204
1313
|
if (!rel.startsWith(".")) rel = "./" + rel;
|
|
1205
1314
|
ifaceImportPath = rel;
|
|
1206
1315
|
} else if (isFeatureScope) {
|
|
@@ -1213,8 +1322,8 @@ function generateInfrastructure(options) {
|
|
|
1213
1322
|
if (new RegExp(`\\b${item.name}\\b`).test(allSignatures)) {
|
|
1214
1323
|
let fromPath;
|
|
1215
1324
|
if (imp.specifier.startsWith(".") && ifaceFilePath) {
|
|
1216
|
-
const absTarget =
|
|
1217
|
-
let rel =
|
|
1325
|
+
const absTarget = path2.resolve(path2.dirname(ifaceFilePath), imp.specifier);
|
|
1326
|
+
let rel = path2.relative(targetDir, absTarget).replace(/\\/g, "/");
|
|
1218
1327
|
if (!rel.startsWith(".")) rel = "./" + rel;
|
|
1219
1328
|
fromPath = rel;
|
|
1220
1329
|
} else {
|
|
@@ -1287,22 +1396,22 @@ function generateUseCaseCode(useCaseName, dependencies = [], inputProps = [], ou
|
|
|
1287
1396
|
from: /^I[A-Z]/.test(dep) ? `../domain/${dep}.js` : `./${dep}.js`
|
|
1288
1397
|
}));
|
|
1289
1398
|
const domainTypesToImport = new Set(domainImports);
|
|
1290
|
-
for (const
|
|
1291
|
-
const rawType =
|
|
1399
|
+
for (const p17 of [...inputProps, ...outputProps]) {
|
|
1400
|
+
const rawType = p17.type.trim();
|
|
1292
1401
|
const baseType = rawType.replace(/\[\]$/, "").trim();
|
|
1293
1402
|
if (!primitiveTypes.has(baseType) && /^[A-Z][A-Za-z0-9_]*$/.test(baseType) && !dependencies.includes(baseType)) {
|
|
1294
1403
|
domainTypesToImport.add(baseType);
|
|
1295
1404
|
}
|
|
1296
1405
|
}
|
|
1297
|
-
const formattedInputProps = inputProps.map((
|
|
1298
|
-
name:
|
|
1299
|
-
optional:
|
|
1300
|
-
type:
|
|
1406
|
+
const formattedInputProps = inputProps.map((p17) => ({
|
|
1407
|
+
name: p17.name,
|
|
1408
|
+
optional: p17.optional,
|
|
1409
|
+
type: p17.optional && !p17.type.includes("undefined") ? `${p17.type} | undefined` : p17.type
|
|
1301
1410
|
}));
|
|
1302
|
-
const formattedOutputProps = outputProps.map((
|
|
1303
|
-
name:
|
|
1304
|
-
optional:
|
|
1305
|
-
type:
|
|
1411
|
+
const formattedOutputProps = outputProps.map((p17) => ({
|
|
1412
|
+
name: p17.name,
|
|
1413
|
+
optional: p17.optional,
|
|
1414
|
+
type: p17.optional && !p17.type.includes("undefined") ? `${p17.type} | undefined` : p17.type
|
|
1306
1415
|
}));
|
|
1307
1416
|
const formattedDeps = dependencies.map((dep) => ({
|
|
1308
1417
|
key: toCamelCase(dep),
|
|
@@ -1401,8 +1510,8 @@ function generateUseCase(options) {
|
|
|
1401
1510
|
const files = [];
|
|
1402
1511
|
const projectRoot = options.projectRoot || findProjectRoot();
|
|
1403
1512
|
const ucName = toPascalCase(options.name);
|
|
1404
|
-
const targetDir = options.targetDir ||
|
|
1405
|
-
const filePath =
|
|
1513
|
+
const targetDir = options.targetDir || path2.join(resolveFeaturesDir(projectRoot), options.feature, "useCases");
|
|
1514
|
+
const filePath = path2.join(targetDir, `${ucName}.ts`);
|
|
1406
1515
|
const code = generateUseCaseCode(
|
|
1407
1516
|
ucName,
|
|
1408
1517
|
options.dependencies || [],
|
|
@@ -1418,7 +1527,7 @@ function generateUseCase(options) {
|
|
|
1418
1527
|
})
|
|
1419
1528
|
);
|
|
1420
1529
|
if (options.includeTest) {
|
|
1421
|
-
const testPath =
|
|
1530
|
+
const testPath = path2.join(targetDir, `${ucName}.test.ts`);
|
|
1422
1531
|
const testCode = generateUseCaseTestCode(ucName, options.dependencies || []);
|
|
1423
1532
|
files.push(
|
|
1424
1533
|
writeFileSafely(testPath, testCode, {
|
|
@@ -1486,10 +1595,10 @@ function generateHttpHandler(options) {
|
|
|
1486
1595
|
const files = [];
|
|
1487
1596
|
const projectRoot = options.projectRoot || findProjectRoot();
|
|
1488
1597
|
const className = toPascalCase(options.name);
|
|
1489
|
-
const pxDir =
|
|
1490
|
-
const handlersDir =
|
|
1491
|
-
const pxIndexPath =
|
|
1492
|
-
if (!
|
|
1598
|
+
const pxDir = path2.join(resolveFeaturesDir(projectRoot), options.feature, "pxExpress");
|
|
1599
|
+
const handlersDir = path2.join(pxDir, "handlers");
|
|
1600
|
+
const pxIndexPath = path2.join(pxDir, "index.ts");
|
|
1601
|
+
if (!fs12.existsSync(pxIndexPath)) {
|
|
1493
1602
|
const featurePath = options.routePrefix || `/${options.feature.toLowerCase()}`;
|
|
1494
1603
|
const pxIndexContent = renderTemplate(expressIndexTemplate, {
|
|
1495
1604
|
routePath: featurePath
|
|
@@ -1502,7 +1611,7 @@ function generateHttpHandler(options) {
|
|
|
1502
1611
|
})
|
|
1503
1612
|
);
|
|
1504
1613
|
}
|
|
1505
|
-
const handlerFilePath =
|
|
1614
|
+
const handlerFilePath = path2.join(handlersDir, `${className}.ts`);
|
|
1506
1615
|
const code = generateHttpHandlerCode(
|
|
1507
1616
|
className,
|
|
1508
1617
|
options.method || "get",
|
|
@@ -1523,11 +1632,11 @@ function generateHttpHandler(options) {
|
|
|
1523
1632
|
return files;
|
|
1524
1633
|
}
|
|
1525
1634
|
function updateOrGenerateDiProvider(featureDir, featureName, bindings = [], options = {}) {
|
|
1526
|
-
const providerPath =
|
|
1635
|
+
const providerPath = path2.join(featureDir, "diProvider.ts");
|
|
1527
1636
|
const pascalFeature = toPascalCase(featureName);
|
|
1528
1637
|
const providerModuleName = `${pascalFeature}Provider`;
|
|
1529
|
-
if (
|
|
1530
|
-
let content =
|
|
1638
|
+
if (fs12.existsSync(providerPath) && !options.overwrite) {
|
|
1639
|
+
let content = fs12.readFileSync(providerPath, "utf-8");
|
|
1531
1640
|
content = content.replace(/\n*export\s+const\s+ScaffoldProvider\s*=[^;]+;?/g, "");
|
|
1532
1641
|
for (const b of bindings) {
|
|
1533
1642
|
const ifaceImp = b.ifaceImportPath || `./domain/${b.ifaceName}.js`;
|
|
@@ -1586,7 +1695,7 @@ function generateFeature(options) {
|
|
|
1586
1695
|
const projectRoot = options.projectRoot || findProjectRoot();
|
|
1587
1696
|
const rawName = options.name.trim();
|
|
1588
1697
|
const featureName = toCamelCase(rawName);
|
|
1589
|
-
const targetDir = options.targetDir ||
|
|
1698
|
+
const targetDir = options.targetDir || path2.join(resolveFeaturesDir(projectRoot), featureName);
|
|
1590
1699
|
const routePath = options.routePath || `/${featureName}`;
|
|
1591
1700
|
const rawEntities = [];
|
|
1592
1701
|
if (options.entityNames && options.entityNames.length > 0) {
|
|
@@ -1607,7 +1716,7 @@ function generateFeature(options) {
|
|
|
1607
1716
|
const domainFile = generateDomain({
|
|
1608
1717
|
name: ent,
|
|
1609
1718
|
feature: featureName,
|
|
1610
|
-
targetDir:
|
|
1719
|
+
targetDir: path2.join(targetDir, "domain"),
|
|
1611
1720
|
projectRoot,
|
|
1612
1721
|
dryRun: options.dryRun,
|
|
1613
1722
|
overwrite: options.overwrite,
|
|
@@ -1627,7 +1736,7 @@ function generateFeature(options) {
|
|
|
1627
1736
|
const ifaceFile = generateInterface({
|
|
1628
1737
|
name: ifaceName,
|
|
1629
1738
|
feature: featureName,
|
|
1630
|
-
targetDir:
|
|
1739
|
+
targetDir: path2.join(targetDir, "domain"),
|
|
1631
1740
|
entityName: ent,
|
|
1632
1741
|
projectRoot,
|
|
1633
1742
|
dryRun: options.dryRun,
|
|
@@ -1644,7 +1753,7 @@ function generateFeature(options) {
|
|
|
1644
1753
|
const stubFile = generateInfrastructure({
|
|
1645
1754
|
name: stubName,
|
|
1646
1755
|
feature: featureName,
|
|
1647
|
-
targetDir:
|
|
1756
|
+
targetDir: path2.join(targetDir, "infrastructure"),
|
|
1648
1757
|
kind: "stub",
|
|
1649
1758
|
implementsInterface: ifaceName,
|
|
1650
1759
|
entityName: ent,
|
|
@@ -1686,7 +1795,7 @@ function generateFeature(options) {
|
|
|
1686
1795
|
const ucFiles = generateUseCase({
|
|
1687
1796
|
name: ucName,
|
|
1688
1797
|
feature: featureName,
|
|
1689
|
-
targetDir:
|
|
1798
|
+
targetDir: path2.join(targetDir, "useCases"),
|
|
1690
1799
|
dependencies: options.includeInterface !== false ? [ifaceName] : [],
|
|
1691
1800
|
inputProperties: [{ name: idParam, type: "string" }],
|
|
1692
1801
|
outputProperties: [{ name: entVar, type: primaryEntity }],
|
|
@@ -1731,7 +1840,7 @@ function generateFeature(options) {
|
|
|
1731
1840
|
const ucFiles = generateUseCase({
|
|
1732
1841
|
name: ucName,
|
|
1733
1842
|
feature: featureName,
|
|
1734
|
-
targetDir:
|
|
1843
|
+
targetDir: path2.join(targetDir, "useCases"),
|
|
1735
1844
|
dependencies: deps,
|
|
1736
1845
|
inputProperties: [{ name: entVar, type: ent }],
|
|
1737
1846
|
outputProperties: [{ name: entVar, type: ent }],
|
|
@@ -1764,7 +1873,7 @@ function generateFeature(options) {
|
|
|
1764
1873
|
const ucFiles = generateUseCase({
|
|
1765
1874
|
name: ucName,
|
|
1766
1875
|
feature: featureName,
|
|
1767
|
-
targetDir:
|
|
1876
|
+
targetDir: path2.join(targetDir, "useCases"),
|
|
1768
1877
|
dependencies: deps,
|
|
1769
1878
|
inputProperties: [{ name: idParam, type: "string" }],
|
|
1770
1879
|
outputProperties: [{ name: entVar, type: ent }],
|
|
@@ -1797,7 +1906,7 @@ function generateFeature(options) {
|
|
|
1797
1906
|
const ucFiles = generateUseCase({
|
|
1798
1907
|
name: ucName,
|
|
1799
1908
|
feature: featureName,
|
|
1800
|
-
targetDir:
|
|
1909
|
+
targetDir: path2.join(targetDir, "useCases"),
|
|
1801
1910
|
dependencies: deps,
|
|
1802
1911
|
inputProperties: [
|
|
1803
1912
|
{ name: "page", type: "number", optional: true },
|
|
@@ -1841,7 +1950,7 @@ function generateFeature(options) {
|
|
|
1841
1950
|
const ucFiles = generateUseCase({
|
|
1842
1951
|
name: ucName,
|
|
1843
1952
|
feature: featureName,
|
|
1844
|
-
targetDir:
|
|
1953
|
+
targetDir: path2.join(targetDir, "useCases"),
|
|
1845
1954
|
dependencies: deps,
|
|
1846
1955
|
inputProperties: [{ name: entVar, type: ent }],
|
|
1847
1956
|
outputProperties: [{ name: entVar, type: ent }],
|
|
@@ -1874,7 +1983,7 @@ function generateFeature(options) {
|
|
|
1874
1983
|
const ucFiles = generateUseCase({
|
|
1875
1984
|
name: ucName,
|
|
1876
1985
|
feature: featureName,
|
|
1877
|
-
targetDir:
|
|
1986
|
+
targetDir: path2.join(targetDir, "useCases"),
|
|
1878
1987
|
dependencies: deps,
|
|
1879
1988
|
inputProperties: [{ name: idParam, type: "string" }],
|
|
1880
1989
|
outputProperties: [{ name: "success", type: "boolean" }],
|
|
@@ -1911,7 +2020,7 @@ function generateFeature(options) {
|
|
|
1911
2020
|
const ucFiles = generateUseCase({
|
|
1912
2021
|
name: ucName,
|
|
1913
2022
|
feature: featureName,
|
|
1914
|
-
targetDir:
|
|
2023
|
+
targetDir: path2.join(targetDir, "useCases"),
|
|
1915
2024
|
dependencies: customUc.dependencies || [],
|
|
1916
2025
|
inputProperties: customUc.inputProperties || [],
|
|
1917
2026
|
outputProperties: customUc.outputProperties || [],
|
|
@@ -1943,8 +2052,8 @@ function generateFeature(options) {
|
|
|
1943
2052
|
}
|
|
1944
2053
|
}
|
|
1945
2054
|
if (options.includeExpress !== false) {
|
|
1946
|
-
const pxDir =
|
|
1947
|
-
const pxIndexPath =
|
|
2055
|
+
const pxDir = path2.join(targetDir, "pxExpress");
|
|
2056
|
+
const pxIndexPath = path2.join(pxDir, "index.ts");
|
|
1948
2057
|
if (!files.some((f) => f.absolutePath === pxIndexPath)) {
|
|
1949
2058
|
const pxIndexContent = renderTemplate(expressIndexTemplate, { routePath });
|
|
1950
2059
|
files.push(
|
|
@@ -1970,13 +2079,14 @@ function generateFeature(options) {
|
|
|
1970
2079
|
// src/commands/feature.ts
|
|
1971
2080
|
async function runFeatureCommand(featureNameArg, flags = {}) {
|
|
1972
2081
|
const projectRoot = findProjectRoot();
|
|
2082
|
+
const isRegistry = getEffectiveMode(projectRoot, flags) === "registry";
|
|
1973
2083
|
let name = featureNameArg?.trim();
|
|
1974
2084
|
if (!name && flags.yes) {
|
|
1975
2085
|
name = "feature";
|
|
1976
2086
|
}
|
|
1977
2087
|
if (!name) {
|
|
1978
2088
|
name = handleCancel(
|
|
1979
|
-
await
|
|
2089
|
+
await p15.text({
|
|
1980
2090
|
message: "What is the feature name?",
|
|
1981
2091
|
placeholder: "e.g. users, auth, orders, billing",
|
|
1982
2092
|
validate: (val) => {
|
|
@@ -1987,18 +2097,19 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
|
|
|
1987
2097
|
).trim();
|
|
1988
2098
|
}
|
|
1989
2099
|
const normalizedName = toCamelCase(name);
|
|
1990
|
-
const
|
|
2100
|
+
const baseFeaturesDir = resolveFeaturesDir(projectRoot, flags);
|
|
2101
|
+
const defaultDir = path2.relative(projectRoot, path2.join(baseFeaturesDir, normalizedName));
|
|
1991
2102
|
let targetDirRel = defaultDir;
|
|
1992
|
-
if (!flags.yes && !flags.target) {
|
|
2103
|
+
if (!flags.yes && !flags.target && !flags.dest) {
|
|
1993
2104
|
targetDirRel = handleCancel(
|
|
1994
|
-
await
|
|
2105
|
+
await p15.text({
|
|
1995
2106
|
message: "Target directory for the feature:",
|
|
1996
2107
|
initialValue: defaultDir,
|
|
1997
2108
|
defaultValue: defaultDir
|
|
1998
2109
|
})
|
|
1999
2110
|
).trim();
|
|
2000
2111
|
}
|
|
2001
|
-
const targetDir = flags.target ?
|
|
2112
|
+
const targetDir = flags.dest ? path2.resolve(projectRoot, flags.dest) : flags.target ? path2.resolve(projectRoot, flags.target) : path2.resolve(projectRoot, targetDirRel);
|
|
2002
2113
|
let includeExpress = flags.express !== void 0 ? Boolean(flags.express) : true;
|
|
2003
2114
|
let routePath = `/${normalizedName}`;
|
|
2004
2115
|
let includeDomain = true;
|
|
@@ -2013,14 +2124,14 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
|
|
|
2013
2124
|
const customUseCases = [];
|
|
2014
2125
|
if (!flags.yes) {
|
|
2015
2126
|
includeExpress = handleCancel(
|
|
2016
|
-
await
|
|
2127
|
+
await p15.confirm({
|
|
2017
2128
|
message: "Include Express Presentation layer (pxExpress routes & handlers)?",
|
|
2018
2129
|
initialValue: true
|
|
2019
2130
|
})
|
|
2020
2131
|
);
|
|
2021
2132
|
if (includeExpress) {
|
|
2022
2133
|
routePath = handleCancel(
|
|
2023
|
-
await
|
|
2134
|
+
await p15.text({
|
|
2024
2135
|
message: "Express route prefix:",
|
|
2025
2136
|
initialValue: `/${normalizedName}`,
|
|
2026
2137
|
defaultValue: `/${normalizedName}`
|
|
@@ -2028,14 +2139,14 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
|
|
|
2028
2139
|
).trim();
|
|
2029
2140
|
}
|
|
2030
2141
|
includeDomain = handleCancel(
|
|
2031
|
-
await
|
|
2142
|
+
await p15.confirm({
|
|
2032
2143
|
message: "Include Domain Entity/Entities?",
|
|
2033
2144
|
initialValue: true
|
|
2034
2145
|
})
|
|
2035
2146
|
);
|
|
2036
2147
|
if (includeDomain) {
|
|
2037
2148
|
const domainInput = handleCancel(
|
|
2038
|
-
await
|
|
2149
|
+
await p15.text({
|
|
2039
2150
|
message: "Domain entity name(s) (comma-separated):",
|
|
2040
2151
|
initialValue: defaultEntity,
|
|
2041
2152
|
defaultValue: defaultEntity,
|
|
@@ -2054,21 +2165,21 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
|
|
|
2054
2165
|
entityNames = [defaultEntity];
|
|
2055
2166
|
}
|
|
2056
2167
|
includeInterface = handleCancel(
|
|
2057
|
-
await
|
|
2168
|
+
await p15.confirm({
|
|
2058
2169
|
message: `Include Domain Repository interface(s) for ${entityNames.join(", ")}?`,
|
|
2059
2170
|
initialValue: true
|
|
2060
2171
|
})
|
|
2061
2172
|
);
|
|
2062
2173
|
if (includeInterface) {
|
|
2063
2174
|
includeInfra = handleCancel(
|
|
2064
|
-
await
|
|
2175
|
+
await p15.confirm({
|
|
2065
2176
|
message: `Include Infrastructure Stub implementation(s) for ${entityNames.join(", ")}?`,
|
|
2066
2177
|
initialValue: true
|
|
2067
2178
|
})
|
|
2068
2179
|
);
|
|
2069
2180
|
}
|
|
2070
2181
|
includeUseCase = handleCancel(
|
|
2071
|
-
await
|
|
2182
|
+
await p15.confirm({
|
|
2072
2183
|
message: "Include Domain Use Cases?",
|
|
2073
2184
|
initialValue: true
|
|
2074
2185
|
})
|
|
@@ -2082,7 +2193,7 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
|
|
|
2082
2193
|
{ value: "delete", label: "Delete (Delete<Entity>)", hint: "DELETE /:id" }
|
|
2083
2194
|
];
|
|
2084
2195
|
crudOperations = handleCancel(
|
|
2085
|
-
await
|
|
2196
|
+
await p15.multiselect({
|
|
2086
2197
|
message: "Select standard CRUD use cases to generate:",
|
|
2087
2198
|
options: crudChoices,
|
|
2088
2199
|
initialValues: ["create", "read", "list", "update", "delete"],
|
|
@@ -2090,13 +2201,13 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
|
|
|
2090
2201
|
})
|
|
2091
2202
|
);
|
|
2092
2203
|
includeTests = handleCancel(
|
|
2093
|
-
await
|
|
2204
|
+
await p15.confirm({
|
|
2094
2205
|
message: "Generate Vitest unit tests for use cases?",
|
|
2095
2206
|
initialValue: true
|
|
2096
2207
|
})
|
|
2097
2208
|
);
|
|
2098
2209
|
const wantCustom = handleCancel(
|
|
2099
|
-
await
|
|
2210
|
+
await p15.confirm({
|
|
2100
2211
|
message: "Do you want to add any custom use cases to this feature?",
|
|
2101
2212
|
initialValue: false
|
|
2102
2213
|
})
|
|
@@ -2106,7 +2217,7 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
|
|
|
2106
2217
|
const availableIfaces = entityNames.map((e) => `I${e}Repository`);
|
|
2107
2218
|
while (addMore) {
|
|
2108
2219
|
const customName = handleCancel(
|
|
2109
|
-
await
|
|
2220
|
+
await p15.text({
|
|
2110
2221
|
message: "Custom Use Case name:",
|
|
2111
2222
|
placeholder: "e.g. ResetPassword, VerifyEmail, AssignRole",
|
|
2112
2223
|
validate: (v) => v && v.trim() ? void 0 : "Use Case name cannot be empty"
|
|
@@ -2116,7 +2227,7 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
|
|
|
2116
2227
|
let selectedDeps = [];
|
|
2117
2228
|
if (availableIfaces.length > 0) {
|
|
2118
2229
|
selectedDeps = handleCancel(
|
|
2119
|
-
await
|
|
2230
|
+
await p15.multiselect({
|
|
2120
2231
|
message: `Select domain interfaces to inject into ${customUcName}:`,
|
|
2121
2232
|
options: availableIfaces.map((i) => ({ value: i, label: i })),
|
|
2122
2233
|
required: false
|
|
@@ -2125,7 +2236,7 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
|
|
|
2125
2236
|
}
|
|
2126
2237
|
let customInputProps = [];
|
|
2127
2238
|
const hasInput = handleCancel(
|
|
2128
|
-
await
|
|
2239
|
+
await p15.confirm({
|
|
2129
2240
|
message: `Does ${customUcName} accept input arguments?`,
|
|
2130
2241
|
initialValue: true
|
|
2131
2242
|
})
|
|
@@ -2136,7 +2247,7 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
|
|
|
2136
2247
|
}
|
|
2137
2248
|
let customOutputProps = [];
|
|
2138
2249
|
const hasCustomOutput = handleCancel(
|
|
2139
|
-
await
|
|
2250
|
+
await p15.confirm({
|
|
2140
2251
|
message: `Does ${customUcName} return custom output data?`,
|
|
2141
2252
|
initialValue: false
|
|
2142
2253
|
})
|
|
@@ -2146,7 +2257,7 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
|
|
|
2146
2257
|
});
|
|
2147
2258
|
}
|
|
2148
2259
|
const customIncludeTest = handleCancel(
|
|
2149
|
-
await
|
|
2260
|
+
await p15.confirm({
|
|
2150
2261
|
message: `Generate Vitest unit test for ${customUcName}?`,
|
|
2151
2262
|
initialValue: true
|
|
2152
2263
|
})
|
|
@@ -2156,14 +2267,14 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
|
|
|
2156
2267
|
let customHttpPath = `/${toKebabCase(customUcName)}`;
|
|
2157
2268
|
if (includeExpress) {
|
|
2158
2269
|
customCreateHttp = handleCancel(
|
|
2159
|
-
await
|
|
2270
|
+
await p15.confirm({
|
|
2160
2271
|
message: `Generate a matching ExpressRoute HTTP handler for ${customUcName}?`,
|
|
2161
2272
|
initialValue: true
|
|
2162
2273
|
})
|
|
2163
2274
|
);
|
|
2164
2275
|
if (customCreateHttp) {
|
|
2165
2276
|
customHttpMethod = handleCancel(
|
|
2166
|
-
await
|
|
2277
|
+
await p15.select({
|
|
2167
2278
|
message: "HTTP method for route:",
|
|
2168
2279
|
options: [
|
|
2169
2280
|
{ value: "post", label: "POST" },
|
|
@@ -2176,7 +2287,7 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
|
|
|
2176
2287
|
})
|
|
2177
2288
|
);
|
|
2178
2289
|
customHttpPath = handleCancel(
|
|
2179
|
-
await
|
|
2290
|
+
await p15.text({
|
|
2180
2291
|
message: "Route subpath (relative to feature prefix):",
|
|
2181
2292
|
initialValue: `/${toKebabCase(customUcName)}`,
|
|
2182
2293
|
defaultValue: `/${toKebabCase(customUcName)}`
|
|
@@ -2197,7 +2308,7 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
|
|
|
2197
2308
|
validationFields: customInputProps
|
|
2198
2309
|
});
|
|
2199
2310
|
addMore = handleCancel(
|
|
2200
|
-
await
|
|
2311
|
+
await p15.confirm({
|
|
2201
2312
|
message: "Add another custom use case?",
|
|
2202
2313
|
initialValue: false
|
|
2203
2314
|
})
|
|
@@ -2206,7 +2317,7 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
|
|
|
2206
2317
|
}
|
|
2207
2318
|
}
|
|
2208
2319
|
includeDiProvider = handleCancel(
|
|
2209
|
-
await
|
|
2320
|
+
await p15.confirm({
|
|
2210
2321
|
message: "Generate / update feature DI provider (diProvider.ts)?",
|
|
2211
2322
|
initialValue: true
|
|
2212
2323
|
})
|
|
@@ -2218,8 +2329,8 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
|
|
|
2218
2329
|
entityNames = [defaultEntity];
|
|
2219
2330
|
}
|
|
2220
2331
|
}
|
|
2221
|
-
const s =
|
|
2222
|
-
s.start(`Generating feature '${
|
|
2332
|
+
const s = p15.spinner();
|
|
2333
|
+
s.start(`Generating feature '${pc15.cyan(normalizedName)}'...`);
|
|
2223
2334
|
const options = {
|
|
2224
2335
|
name: normalizedName,
|
|
2225
2336
|
projectRoot,
|
|
@@ -2239,9 +2350,207 @@ async function runFeatureCommand(featureNameArg, flags = {}) {
|
|
|
2239
2350
|
includeDiProvider
|
|
2240
2351
|
};
|
|
2241
2352
|
const files = generateFeature(options);
|
|
2353
|
+
if (isRegistry) {
|
|
2354
|
+
const registryPath = path2.join(targetDir, "registry.json");
|
|
2355
|
+
if (!fs12.existsSync(registryPath)) {
|
|
2356
|
+
const manifest = {
|
|
2357
|
+
name: toKebabCase(normalizedName),
|
|
2358
|
+
type: "feature",
|
|
2359
|
+
description: flags.description || `${normalizedName} feature module`,
|
|
2360
|
+
version: "1.0.0",
|
|
2361
|
+
dependencies: {
|
|
2362
|
+
npm: includeExpress ? ["@solid-stack/agnos-express", "express"] : [],
|
|
2363
|
+
shared: [],
|
|
2364
|
+
features: []
|
|
2365
|
+
},
|
|
2366
|
+
files: []
|
|
2367
|
+
};
|
|
2368
|
+
const content = JSON.stringify(manifest, null, 2) + "\n";
|
|
2369
|
+
const genFile = writeFileSafely(registryPath, content, {
|
|
2370
|
+
projectRoot,
|
|
2371
|
+
overwrite: options.overwrite,
|
|
2372
|
+
dryRun: options.dryRun
|
|
2373
|
+
});
|
|
2374
|
+
files.push(genFile);
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2242
2377
|
s.stop("Feature scaffolded successfully!");
|
|
2243
2378
|
printGeneratedFiles(files, options.dryRun);
|
|
2244
|
-
|
|
2379
|
+
p15.outro(
|
|
2380
|
+
pc15.green(
|
|
2381
|
+
`\u2713 Feature '${normalizedName}' initialized in ${path2.relative(projectRoot, targetDir)} (uncommitted, unregistered)`
|
|
2382
|
+
)
|
|
2383
|
+
);
|
|
2384
|
+
}
|
|
2385
|
+
function generateShared(options) {
|
|
2386
|
+
const files = [];
|
|
2387
|
+
const projectRoot = options.projectRoot || findProjectRoot();
|
|
2388
|
+
const rawName = options.name.trim();
|
|
2389
|
+
const pascalName = toPascalCase(rawName);
|
|
2390
|
+
const camelName = toCamelCase(rawName);
|
|
2391
|
+
const kebabName = toKebabCase(rawName);
|
|
2392
|
+
const targetDir = options.targetDir || path2.join(resolveSharedDir(projectRoot), kebabName);
|
|
2393
|
+
const fileDefs = [];
|
|
2394
|
+
const facadeContent = `import { MakeInjectable, type DepsType } from "@solid-stack/di";
|
|
2395
|
+
import { I${pascalName}Engine } from "./ports/I${pascalName}Engine.js";
|
|
2396
|
+
|
|
2397
|
+
@MakeInjectable
|
|
2398
|
+
export class ${pascalName} {
|
|
2399
|
+
public static deps = { ${camelName}Engine: I${pascalName}Engine };
|
|
2400
|
+
constructor(public readonly deps: DepsType<typeof ${pascalName}.deps>) {}
|
|
2401
|
+
}
|
|
2402
|
+
`;
|
|
2403
|
+
fileDefs.push({
|
|
2404
|
+
absolutePath: path2.join(targetDir, `${pascalName}.ts`),
|
|
2405
|
+
content: facadeContent
|
|
2406
|
+
});
|
|
2407
|
+
const portContent = `export abstract class I${pascalName}Engine {
|
|
2408
|
+
// Define abstract port contracts for ${pascalName} here
|
|
2409
|
+
}
|
|
2410
|
+
`;
|
|
2411
|
+
fileDefs.push({
|
|
2412
|
+
absolutePath: path2.join(targetDir, "ports", `I${pascalName}Engine.ts`),
|
|
2413
|
+
content: portContent
|
|
2414
|
+
});
|
|
2415
|
+
const concreteContent = `import { MakeInjectable } from "@solid-stack/di";
|
|
2416
|
+
import { I${pascalName}Engine } from "../ports/I${pascalName}Engine.js";
|
|
2417
|
+
|
|
2418
|
+
@MakeInjectable
|
|
2419
|
+
export class Default${pascalName}Engine implements I${pascalName}Engine {
|
|
2420
|
+
// Default concrete implementation for production
|
|
2421
|
+
}
|
|
2422
|
+
`;
|
|
2423
|
+
fileDefs.push({
|
|
2424
|
+
absolutePath: path2.join(targetDir, "infrastructure", `Default${pascalName}Engine.ts`),
|
|
2425
|
+
content: concreteContent
|
|
2426
|
+
});
|
|
2427
|
+
if (options.includeStub !== false) {
|
|
2428
|
+
const stubContent = `import { MakeInjectable } from "@solid-stack/di";
|
|
2429
|
+
import { I${pascalName}Engine } from "../ports/I${pascalName}Engine.js";
|
|
2430
|
+
|
|
2431
|
+
@MakeInjectable
|
|
2432
|
+
export class Stub${pascalName}Engine implements I${pascalName}Engine {
|
|
2433
|
+
// In-memory or stub implementation for testing
|
|
2434
|
+
}
|
|
2435
|
+
`;
|
|
2436
|
+
fileDefs.push({
|
|
2437
|
+
absolutePath: path2.join(targetDir, "infrastructure", `Stub${pascalName}Engine.ts`),
|
|
2438
|
+
content: stubContent
|
|
2439
|
+
});
|
|
2440
|
+
}
|
|
2441
|
+
const providerContent = `import type { Container } from "@solid-stack/di";
|
|
2442
|
+
import { I${pascalName}Engine } from "./ports/I${pascalName}Engine.js";
|
|
2443
|
+
import { Default${pascalName}Engine } from "./infrastructure/Default${pascalName}Engine.js";
|
|
2444
|
+
|
|
2445
|
+
export const diProvider = (c: Container): void => {
|
|
2446
|
+
c.provide(I${pascalName}Engine, Default${pascalName}Engine);
|
|
2447
|
+
};
|
|
2448
|
+
|
|
2449
|
+
export default diProvider;
|
|
2450
|
+
`;
|
|
2451
|
+
fileDefs.push({
|
|
2452
|
+
absolutePath: path2.join(targetDir, "diProvider.ts"),
|
|
2453
|
+
content: providerContent
|
|
2454
|
+
});
|
|
2455
|
+
if (options.isRegistry) {
|
|
2456
|
+
const registryManifest = {
|
|
2457
|
+
name: kebabName,
|
|
2458
|
+
type: "shared",
|
|
2459
|
+
description: options.description || `${kebabName} shared module`,
|
|
2460
|
+
version: "1.0.0",
|
|
2461
|
+
dependencies: {
|
|
2462
|
+
npm: [],
|
|
2463
|
+
shared: [],
|
|
2464
|
+
features: []
|
|
2465
|
+
},
|
|
2466
|
+
files: []
|
|
2467
|
+
};
|
|
2468
|
+
fileDefs.push({
|
|
2469
|
+
absolutePath: path2.join(targetDir, "registry.json"),
|
|
2470
|
+
content: JSON.stringify(registryManifest, null, 2) + "\n"
|
|
2471
|
+
});
|
|
2472
|
+
}
|
|
2473
|
+
for (const def of fileDefs) {
|
|
2474
|
+
const genFile = writeFileSafely(def.absolutePath, def.content, {
|
|
2475
|
+
projectRoot,
|
|
2476
|
+
overwrite: options.overwrite,
|
|
2477
|
+
dryRun: options.dryRun
|
|
2478
|
+
});
|
|
2479
|
+
files.push(genFile);
|
|
2480
|
+
}
|
|
2481
|
+
return files;
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
// src/commands/shared.ts
|
|
2485
|
+
async function runSharedCommand(sharedNameArg, flags = {}) {
|
|
2486
|
+
const projectRoot = findProjectRoot();
|
|
2487
|
+
const isRegistry = getEffectiveMode(projectRoot, flags) === "registry";
|
|
2488
|
+
let name = sharedNameArg?.trim();
|
|
2489
|
+
if (!name && flags.yes) {
|
|
2490
|
+
name = "shared";
|
|
2491
|
+
}
|
|
2492
|
+
if (!name) {
|
|
2493
|
+
name = handleCancel(
|
|
2494
|
+
await p15.text({
|
|
2495
|
+
message: "What is the shared module name?",
|
|
2496
|
+
placeholder: "e.g. hasher, time, uuid, cache, logger",
|
|
2497
|
+
validate: (val) => {
|
|
2498
|
+
if (!val || !val.trim()) return "Shared module name cannot be empty.";
|
|
2499
|
+
return void 0;
|
|
2500
|
+
}
|
|
2501
|
+
})
|
|
2502
|
+
).trim();
|
|
2503
|
+
}
|
|
2504
|
+
const normalizedName = toKebabCase(name);
|
|
2505
|
+
const baseSharedDir = resolveSharedDir(projectRoot, flags);
|
|
2506
|
+
const defaultDir = path2.relative(projectRoot, path2.join(baseSharedDir, normalizedName));
|
|
2507
|
+
let targetDirRel = defaultDir;
|
|
2508
|
+
if (!flags.yes && !flags.target && !flags.dest) {
|
|
2509
|
+
targetDirRel = handleCancel(
|
|
2510
|
+
await p15.text({
|
|
2511
|
+
message: "Target directory for the shared module:",
|
|
2512
|
+
initialValue: defaultDir,
|
|
2513
|
+
defaultValue: defaultDir
|
|
2514
|
+
})
|
|
2515
|
+
).trim();
|
|
2516
|
+
}
|
|
2517
|
+
const targetDir = flags.dest ? path2.resolve(projectRoot, flags.dest) : flags.target ? path2.resolve(projectRoot, flags.target) : path2.resolve(projectRoot, targetDirRel);
|
|
2518
|
+
let description = flags.description || "";
|
|
2519
|
+
let includeStub = true;
|
|
2520
|
+
if (!flags.yes) {
|
|
2521
|
+
description = handleCancel(
|
|
2522
|
+
await p15.text({
|
|
2523
|
+
message: "Module description (optional):",
|
|
2524
|
+
placeholder: "e.g. In-memory caching and Redis port abstraction",
|
|
2525
|
+
initialValue: ""
|
|
2526
|
+
})
|
|
2527
|
+
).trim();
|
|
2528
|
+
includeStub = handleCancel(
|
|
2529
|
+
await p15.confirm({
|
|
2530
|
+
message: "Include stub implementation in infrastructure/?",
|
|
2531
|
+
initialValue: true
|
|
2532
|
+
})
|
|
2533
|
+
);
|
|
2534
|
+
}
|
|
2535
|
+
const s = p15.spinner();
|
|
2536
|
+
s.start(`Generating shared module '${pc15.cyan(normalizedName)}'...`);
|
|
2537
|
+
const files = generateShared({
|
|
2538
|
+
name: normalizedName,
|
|
2539
|
+
description,
|
|
2540
|
+
projectRoot,
|
|
2541
|
+
targetDir,
|
|
2542
|
+
dryRun: Boolean(flags.dryRun),
|
|
2543
|
+
overwrite: Boolean(flags.overwrite),
|
|
2544
|
+
includeStub,
|
|
2545
|
+
isRegistry
|
|
2546
|
+
});
|
|
2547
|
+
s.stop("Shared module generated successfully!");
|
|
2548
|
+
printGeneratedFiles(files, Boolean(flags.dryRun));
|
|
2549
|
+
p15.outro(
|
|
2550
|
+
pc15.green(
|
|
2551
|
+
`\u2713 Shared module '${normalizedName}' initialized in ${path2.relative(projectRoot, targetDir)} (uncommitted, unregistered)`
|
|
2552
|
+
)
|
|
2553
|
+
);
|
|
2245
2554
|
}
|
|
2246
2555
|
async function runUseCaseCommand(useCaseNameArg, flags = {}) {
|
|
2247
2556
|
const projectRoot = findProjectRoot();
|
|
@@ -2252,14 +2561,14 @@ async function runUseCaseCommand(useCaseNameArg, flags = {}) {
|
|
|
2252
2561
|
const featureOptions = existingFeatures.map((f) => ({ value: f, label: f }));
|
|
2253
2562
|
featureOptions.push({ value: "__custom__", label: "\u270F\uFE0F Enter custom feature name..." });
|
|
2254
2563
|
const selected = handleCancel(
|
|
2255
|
-
await
|
|
2564
|
+
await p15.select({
|
|
2256
2565
|
message: "Select target feature:",
|
|
2257
2566
|
options: featureOptions
|
|
2258
2567
|
})
|
|
2259
2568
|
);
|
|
2260
2569
|
if (selected === "__custom__") {
|
|
2261
2570
|
feature = handleCancel(
|
|
2262
|
-
await
|
|
2571
|
+
await p15.text({
|
|
2263
2572
|
message: "Enter feature name:",
|
|
2264
2573
|
validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
|
|
2265
2574
|
})
|
|
@@ -2269,7 +2578,7 @@ async function runUseCaseCommand(useCaseNameArg, flags = {}) {
|
|
|
2269
2578
|
}
|
|
2270
2579
|
} else {
|
|
2271
2580
|
feature = handleCancel(
|
|
2272
|
-
await
|
|
2581
|
+
await p15.text({
|
|
2273
2582
|
message: "Enter target feature name:",
|
|
2274
2583
|
validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
|
|
2275
2584
|
})
|
|
@@ -2280,7 +2589,7 @@ async function runUseCaseCommand(useCaseNameArg, flags = {}) {
|
|
|
2280
2589
|
let name = useCaseNameArg?.trim();
|
|
2281
2590
|
if (!name) {
|
|
2282
2591
|
name = handleCancel(
|
|
2283
|
-
await
|
|
2592
|
+
await p15.text({
|
|
2284
2593
|
message: "Use Case name:",
|
|
2285
2594
|
placeholder: "e.g. CreateUser, GetUser, CancelOrder",
|
|
2286
2595
|
validate: (v) => v && v.trim() ? void 0 : "Use Case name cannot be empty"
|
|
@@ -2296,7 +2605,7 @@ async function runUseCaseCommand(useCaseNameArg, flags = {}) {
|
|
|
2296
2605
|
label: i.name
|
|
2297
2606
|
}));
|
|
2298
2607
|
selectedDeps = handleCancel(
|
|
2299
|
-
await
|
|
2608
|
+
await p15.multiselect({
|
|
2300
2609
|
message: "Select domain interfaces to inject into use case:",
|
|
2301
2610
|
options: multiOptions,
|
|
2302
2611
|
required: false
|
|
@@ -2311,7 +2620,7 @@ async function runUseCaseCommand(useCaseNameArg, flags = {}) {
|
|
|
2311
2620
|
let httpPath = "/";
|
|
2312
2621
|
if (!flags.yes) {
|
|
2313
2622
|
const hasInput = handleCancel(
|
|
2314
|
-
await
|
|
2623
|
+
await p15.confirm({
|
|
2315
2624
|
message: "Does this use case accept input arguments?",
|
|
2316
2625
|
initialValue: true
|
|
2317
2626
|
})
|
|
@@ -2321,7 +2630,7 @@ async function runUseCaseCommand(useCaseNameArg, flags = {}) {
|
|
|
2321
2630
|
});
|
|
2322
2631
|
}
|
|
2323
2632
|
const hasCustomOutput = handleCancel(
|
|
2324
|
-
await
|
|
2633
|
+
await p15.confirm({
|
|
2325
2634
|
message: "Does this use case return custom output data?",
|
|
2326
2635
|
initialValue: false
|
|
2327
2636
|
})
|
|
@@ -2331,20 +2640,20 @@ async function runUseCaseCommand(useCaseNameArg, flags = {}) {
|
|
|
2331
2640
|
});
|
|
2332
2641
|
}
|
|
2333
2642
|
includeTest = handleCancel(
|
|
2334
|
-
await
|
|
2643
|
+
await p15.confirm({
|
|
2335
2644
|
message: "Generate Vitest unit test skeleton (.test.ts)?",
|
|
2336
2645
|
initialValue: true
|
|
2337
2646
|
})
|
|
2338
2647
|
);
|
|
2339
2648
|
createHttp = handleCancel(
|
|
2340
|
-
await
|
|
2649
|
+
await p15.confirm({
|
|
2341
2650
|
message: "Generate a matching ExpressRoute HTTP handler?",
|
|
2342
2651
|
initialValue: false
|
|
2343
2652
|
})
|
|
2344
2653
|
);
|
|
2345
2654
|
if (createHttp) {
|
|
2346
2655
|
httpMethod = handleCancel(
|
|
2347
|
-
await
|
|
2656
|
+
await p15.select({
|
|
2348
2657
|
message: "HTTP method for route:",
|
|
2349
2658
|
options: [
|
|
2350
2659
|
{ value: "post", label: "POST" },
|
|
@@ -2357,7 +2666,7 @@ async function runUseCaseCommand(useCaseNameArg, flags = {}) {
|
|
|
2357
2666
|
})
|
|
2358
2667
|
);
|
|
2359
2668
|
httpPath = handleCancel(
|
|
2360
|
-
await
|
|
2669
|
+
await p15.text({
|
|
2361
2670
|
message: "Route subpath (relative to feature prefix):",
|
|
2362
2671
|
initialValue: "/",
|
|
2363
2672
|
defaultValue: "/"
|
|
@@ -2365,8 +2674,8 @@ async function runUseCaseCommand(useCaseNameArg, flags = {}) {
|
|
|
2365
2674
|
).trim();
|
|
2366
2675
|
}
|
|
2367
2676
|
}
|
|
2368
|
-
const s =
|
|
2369
|
-
s.start(`Generating use case '${
|
|
2677
|
+
const s = p15.spinner();
|
|
2678
|
+
s.start(`Generating use case '${pc15.cyan(ucName)}'...`);
|
|
2370
2679
|
const options = {
|
|
2371
2680
|
name: ucName,
|
|
2372
2681
|
feature,
|
|
@@ -2398,7 +2707,7 @@ async function runUseCaseCommand(useCaseNameArg, flags = {}) {
|
|
|
2398
2707
|
}
|
|
2399
2708
|
s.stop("Use case generated successfully!");
|
|
2400
2709
|
printGeneratedFiles(files, options.dryRun);
|
|
2401
|
-
|
|
2710
|
+
p15.outro(pc15.green(`Use case '${ucName}' created in feature '${feature}'!`));
|
|
2402
2711
|
}
|
|
2403
2712
|
async function runDomainCommand(domainNameArg, flags = {}) {
|
|
2404
2713
|
const projectRoot = findProjectRoot();
|
|
@@ -2409,14 +2718,14 @@ async function runDomainCommand(domainNameArg, flags = {}) {
|
|
|
2409
2718
|
const featureOptions = existingFeatures.map((f) => ({ value: f, label: f }));
|
|
2410
2719
|
featureOptions.push({ value: "__custom__", label: "\u270F\uFE0F Enter custom feature name..." });
|
|
2411
2720
|
const selected = handleCancel(
|
|
2412
|
-
await
|
|
2721
|
+
await p15.select({
|
|
2413
2722
|
message: "Select target feature:",
|
|
2414
2723
|
options: featureOptions
|
|
2415
2724
|
})
|
|
2416
2725
|
);
|
|
2417
2726
|
if (selected === "__custom__") {
|
|
2418
2727
|
feature = handleCancel(
|
|
2419
|
-
await
|
|
2728
|
+
await p15.text({
|
|
2420
2729
|
message: "Enter feature name:",
|
|
2421
2730
|
validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
|
|
2422
2731
|
})
|
|
@@ -2426,7 +2735,7 @@ async function runDomainCommand(domainNameArg, flags = {}) {
|
|
|
2426
2735
|
}
|
|
2427
2736
|
} else {
|
|
2428
2737
|
feature = handleCancel(
|
|
2429
|
-
await
|
|
2738
|
+
await p15.text({
|
|
2430
2739
|
message: "Enter target feature name:",
|
|
2431
2740
|
validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
|
|
2432
2741
|
})
|
|
@@ -2437,7 +2746,7 @@ async function runDomainCommand(domainNameArg, flags = {}) {
|
|
|
2437
2746
|
let name = domainNameArg?.trim();
|
|
2438
2747
|
if (!name) {
|
|
2439
2748
|
name = handleCancel(
|
|
2440
|
-
await
|
|
2749
|
+
await p15.text({
|
|
2441
2750
|
message: "Domain model / entity name:",
|
|
2442
2751
|
placeholder: "e.g. User, Order, Product, Address",
|
|
2443
2752
|
validate: (v) => v && v.trim() ? void 0 : "Domain name cannot be empty"
|
|
@@ -2449,7 +2758,7 @@ async function runDomainCommand(domainNameArg, flags = {}) {
|
|
|
2449
2758
|
let properties = [];
|
|
2450
2759
|
if (!flags.yes) {
|
|
2451
2760
|
kind = handleCancel(
|
|
2452
|
-
await
|
|
2761
|
+
await p15.select({
|
|
2453
2762
|
message: "Select domain model type:",
|
|
2454
2763
|
options: [
|
|
2455
2764
|
{ value: "interface", label: "TypeScript Interface (pure data structure)" },
|
|
@@ -2468,8 +2777,8 @@ async function runDomainCommand(domainNameArg, flags = {}) {
|
|
|
2468
2777
|
defaults: kind === "valueObject" ? [{ name: "value", type: "string" }] : defaultProps
|
|
2469
2778
|
});
|
|
2470
2779
|
}
|
|
2471
|
-
const s =
|
|
2472
|
-
s.start(`Generating domain model '${
|
|
2780
|
+
const s = p15.spinner();
|
|
2781
|
+
s.start(`Generating domain model '${pc15.cyan(entityName)}'...`);
|
|
2473
2782
|
const options = {
|
|
2474
2783
|
name: entityName,
|
|
2475
2784
|
feature,
|
|
@@ -2482,7 +2791,7 @@ async function runDomainCommand(domainNameArg, flags = {}) {
|
|
|
2482
2791
|
const file = generateDomain(options);
|
|
2483
2792
|
s.stop("Domain model generated successfully!");
|
|
2484
2793
|
printGeneratedFiles([file], options.dryRun);
|
|
2485
|
-
|
|
2794
|
+
p15.outro(pc15.green(`Domain model '${entityName}' created in feature '${feature}'!`));
|
|
2486
2795
|
}
|
|
2487
2796
|
async function runInfrastructureCommand(classNameArg, flags = {}) {
|
|
2488
2797
|
const projectRoot = findProjectRoot();
|
|
@@ -2491,7 +2800,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
|
|
|
2491
2800
|
let feature = flags.feature?.trim();
|
|
2492
2801
|
if (!flags.yes && !flags.global && !flags.feature) {
|
|
2493
2802
|
scope = handleCancel(
|
|
2494
|
-
await
|
|
2803
|
+
await p15.select({
|
|
2495
2804
|
message: "Where should this infrastructure implementation be placed?",
|
|
2496
2805
|
options: [
|
|
2497
2806
|
{ value: "feature", label: "Feature-level (features/<feature>/infrastructure)" },
|
|
@@ -2506,14 +2815,14 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
|
|
|
2506
2815
|
const featureOptions = existingFeatures.map((f) => ({ value: f, label: f }));
|
|
2507
2816
|
featureOptions.push({ value: "__custom__", label: "\u270F\uFE0F Enter custom feature name..." });
|
|
2508
2817
|
const selected = handleCancel(
|
|
2509
|
-
await
|
|
2818
|
+
await p15.select({
|
|
2510
2819
|
message: "Select target feature:",
|
|
2511
2820
|
options: featureOptions
|
|
2512
2821
|
})
|
|
2513
2822
|
);
|
|
2514
2823
|
if (selected === "__custom__") {
|
|
2515
2824
|
feature = handleCancel(
|
|
2516
|
-
await
|
|
2825
|
+
await p15.text({
|
|
2517
2826
|
message: "Enter feature name:",
|
|
2518
2827
|
validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
|
|
2519
2828
|
})
|
|
@@ -2523,7 +2832,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
|
|
|
2523
2832
|
}
|
|
2524
2833
|
} else {
|
|
2525
2834
|
feature = handleCancel(
|
|
2526
|
-
await
|
|
2835
|
+
await p15.text({
|
|
2527
2836
|
message: "Enter target feature name:",
|
|
2528
2837
|
validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
|
|
2529
2838
|
})
|
|
@@ -2536,7 +2845,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
|
|
|
2536
2845
|
let name = classNameArg?.trim();
|
|
2537
2846
|
if (!name) {
|
|
2538
2847
|
name = handleCancel(
|
|
2539
|
-
await
|
|
2848
|
+
await p15.text({
|
|
2540
2849
|
message: "Infrastructure class name:",
|
|
2541
2850
|
placeholder: "e.g. StubUserRepository, PostgresUserRepository, S3FileStorage",
|
|
2542
2851
|
validate: (v) => v && v.trim() ? void 0 : "Class name cannot be empty"
|
|
@@ -2555,7 +2864,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
|
|
|
2555
2864
|
(i) => className.toLowerCase().includes(removeInterfacePrefix(i.name).toLowerCase())
|
|
2556
2865
|
);
|
|
2557
2866
|
const ifaceChoice = handleCancel(
|
|
2558
|
-
await
|
|
2867
|
+
await p15.select({
|
|
2559
2868
|
message: "Does this class implement a domain interface or abstract class?",
|
|
2560
2869
|
options: [
|
|
2561
2870
|
...interfaces.map((i) => ({ value: i.name, label: i.name })),
|
|
@@ -2567,7 +2876,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
|
|
|
2567
2876
|
);
|
|
2568
2877
|
if (ifaceChoice === "__custom__") {
|
|
2569
2878
|
implementsInterface = handleCancel(
|
|
2570
|
-
await
|
|
2879
|
+
await p15.text({
|
|
2571
2880
|
message: "Enter interface or abstract class name:",
|
|
2572
2881
|
placeholder: "e.g. IUserRepository, BaseStorageAdapter",
|
|
2573
2882
|
validate: (v) => v && v.trim() ? void 0 : "Name cannot be empty"
|
|
@@ -2578,14 +2887,14 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
|
|
|
2578
2887
|
}
|
|
2579
2888
|
} else {
|
|
2580
2889
|
const wantImpl = handleCancel(
|
|
2581
|
-
await
|
|
2890
|
+
await p15.confirm({
|
|
2582
2891
|
message: "Does this class implement a domain interface or abstract class?",
|
|
2583
2892
|
initialValue: false
|
|
2584
2893
|
})
|
|
2585
2894
|
);
|
|
2586
2895
|
if (wantImpl) {
|
|
2587
2896
|
implementsInterface = handleCancel(
|
|
2588
|
-
await
|
|
2897
|
+
await p15.text({
|
|
2589
2898
|
message: "Enter interface or abstract class name:",
|
|
2590
2899
|
placeholder: "e.g. IUserRepository, BaseStorageAdapter",
|
|
2591
2900
|
validate: (v) => v && v.trim() ? void 0 : "Name cannot be empty"
|
|
@@ -2605,7 +2914,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
|
|
|
2605
2914
|
const allInterfaces = listDomainInterfaces(projectRoot);
|
|
2606
2915
|
if (allInterfaces.length > 0) {
|
|
2607
2916
|
const ifaceChoice = handleCancel(
|
|
2608
|
-
await
|
|
2917
|
+
await p15.select({
|
|
2609
2918
|
message: "Does this class implement an interface or abstract class?",
|
|
2610
2919
|
options: [
|
|
2611
2920
|
...allInterfaces.map((i) => ({ value: i.name, label: `${i.name} (${i.feature})` })),
|
|
@@ -2617,7 +2926,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
|
|
|
2617
2926
|
);
|
|
2618
2927
|
if (ifaceChoice === "__custom__") {
|
|
2619
2928
|
implementsInterface = handleCancel(
|
|
2620
|
-
await
|
|
2929
|
+
await p15.text({
|
|
2621
2930
|
message: "Enter interface or abstract class name:",
|
|
2622
2931
|
placeholder: "e.g. IAuthRepository",
|
|
2623
2932
|
validate: (v) => v && v.trim() ? void 0 : "Name cannot be empty"
|
|
@@ -2630,7 +2939,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
|
|
|
2630
2939
|
}
|
|
2631
2940
|
if (!flags.yes && !flags.kind) {
|
|
2632
2941
|
kind = handleCancel(
|
|
2633
|
-
await
|
|
2942
|
+
await p15.select({
|
|
2634
2943
|
message: "Select implementation kind:",
|
|
2635
2944
|
options: [
|
|
2636
2945
|
{ value: "stub", label: "In-Memory Stub (with in-memory store and test helper methods)" },
|
|
@@ -2641,7 +2950,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
|
|
|
2641
2950
|
);
|
|
2642
2951
|
if (scope === "feature" && feature && implementsInterface) {
|
|
2643
2952
|
registerInProvider = handleCancel(
|
|
2644
|
-
await
|
|
2953
|
+
await p15.confirm({
|
|
2645
2954
|
message: "Register this implementation in feature diProvider.ts?",
|
|
2646
2955
|
initialValue: true
|
|
2647
2956
|
})
|
|
@@ -2650,8 +2959,8 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
|
|
|
2650
2959
|
} else if (flags.yes && scope === "feature" && feature && implementsInterface) {
|
|
2651
2960
|
registerInProvider = !flags.noProvider;
|
|
2652
2961
|
}
|
|
2653
|
-
const s =
|
|
2654
|
-
s.start(`Generating infrastructure class '${
|
|
2962
|
+
const s = p15.spinner();
|
|
2963
|
+
s.start(`Generating infrastructure class '${pc15.cyan(className)}'...`);
|
|
2655
2964
|
const files = [];
|
|
2656
2965
|
const options = {
|
|
2657
2966
|
name: className,
|
|
@@ -2666,7 +2975,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
|
|
|
2666
2975
|
const infraFile = generateInfrastructure(options);
|
|
2667
2976
|
files.push(infraFile);
|
|
2668
2977
|
if (registerInProvider && scope === "feature" && feature && implementsInterface) {
|
|
2669
|
-
const featureDir =
|
|
2978
|
+
const featureDir = path2.join(resolveFeaturesDir(projectRoot), feature);
|
|
2670
2979
|
const providerFile = updateOrGenerateDiProvider(
|
|
2671
2980
|
featureDir,
|
|
2672
2981
|
feature,
|
|
@@ -2686,7 +2995,7 @@ async function runInfrastructureCommand(classNameArg, flags = {}) {
|
|
|
2686
2995
|
}
|
|
2687
2996
|
s.stop("Infrastructure class generated successfully!");
|
|
2688
2997
|
printGeneratedFiles(files, options.dryRun);
|
|
2689
|
-
|
|
2998
|
+
p15.outro(pc15.green(`Infrastructure class '${className}' created!`));
|
|
2690
2999
|
}
|
|
2691
3000
|
async function runInterfaceCommand(interfaceNameArg, flags = {}) {
|
|
2692
3001
|
const projectRoot = findProjectRoot();
|
|
@@ -2697,14 +3006,14 @@ async function runInterfaceCommand(interfaceNameArg, flags = {}) {
|
|
|
2697
3006
|
const featureOptions = existingFeatures.map((f) => ({ value: f, label: f }));
|
|
2698
3007
|
featureOptions.push({ value: "__custom__", label: "\u270F\uFE0F Enter custom feature name..." });
|
|
2699
3008
|
const selected = handleCancel(
|
|
2700
|
-
await
|
|
3009
|
+
await p15.select({
|
|
2701
3010
|
message: "Select target feature:",
|
|
2702
3011
|
options: featureOptions
|
|
2703
3012
|
})
|
|
2704
3013
|
);
|
|
2705
3014
|
if (selected === "__custom__") {
|
|
2706
3015
|
feature = handleCancel(
|
|
2707
|
-
await
|
|
3016
|
+
await p15.text({
|
|
2708
3017
|
message: "Enter feature name:",
|
|
2709
3018
|
validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
|
|
2710
3019
|
})
|
|
@@ -2714,7 +3023,7 @@ async function runInterfaceCommand(interfaceNameArg, flags = {}) {
|
|
|
2714
3023
|
}
|
|
2715
3024
|
} else {
|
|
2716
3025
|
feature = handleCancel(
|
|
2717
|
-
await
|
|
3026
|
+
await p15.text({
|
|
2718
3027
|
message: "Enter target feature name:",
|
|
2719
3028
|
validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
|
|
2720
3029
|
})
|
|
@@ -2725,7 +3034,7 @@ async function runInterfaceCommand(interfaceNameArg, flags = {}) {
|
|
|
2725
3034
|
let name = interfaceNameArg?.trim();
|
|
2726
3035
|
if (!name) {
|
|
2727
3036
|
name = handleCancel(
|
|
2728
|
-
await
|
|
3037
|
+
await p15.text({
|
|
2729
3038
|
message: "Domain interface name:",
|
|
2730
3039
|
placeholder: "e.g. IUserRepository, IPaymentService",
|
|
2731
3040
|
validate: (v) => v && v.trim() ? void 0 : "Interface name cannot be empty"
|
|
@@ -2743,7 +3052,7 @@ async function runInterfaceCommand(interfaceNameArg, flags = {}) {
|
|
|
2743
3052
|
if (!flags.yes) {
|
|
2744
3053
|
if (existingEntities.length > 0) {
|
|
2745
3054
|
const selectedEntity = handleCancel(
|
|
2746
|
-
await
|
|
3055
|
+
await p15.select({
|
|
2747
3056
|
message: "Select domain entity associated with this interface:",
|
|
2748
3057
|
options: [
|
|
2749
3058
|
...existingEntities.map((e) => ({ value: e.name, label: e.name })),
|
|
@@ -2754,7 +3063,7 @@ async function runInterfaceCommand(interfaceNameArg, flags = {}) {
|
|
|
2754
3063
|
);
|
|
2755
3064
|
if (selectedEntity === "__custom__") {
|
|
2756
3065
|
entityName = handleCancel(
|
|
2757
|
-
await
|
|
3066
|
+
await p15.text({
|
|
2758
3067
|
message: "Enter entity name:",
|
|
2759
3068
|
defaultValue: removeInterfacePrefix(ifaceName).replace(/Repository$|Service$/, "")
|
|
2760
3069
|
})
|
|
@@ -2764,7 +3073,7 @@ async function runInterfaceCommand(interfaceNameArg, flags = {}) {
|
|
|
2764
3073
|
}
|
|
2765
3074
|
}
|
|
2766
3075
|
kind = handleCancel(
|
|
2767
|
-
await
|
|
3076
|
+
await p15.select({
|
|
2768
3077
|
message: "Interface definition style:",
|
|
2769
3078
|
options: [
|
|
2770
3079
|
{ value: "abstractClass", label: "Abstract Class (Recommended - acts as DI token & type)" },
|
|
@@ -2774,7 +3083,7 @@ async function runInterfaceCommand(interfaceNameArg, flags = {}) {
|
|
|
2774
3083
|
})
|
|
2775
3084
|
);
|
|
2776
3085
|
methodsPreset = handleCancel(
|
|
2777
|
-
await
|
|
3086
|
+
await p15.select({
|
|
2778
3087
|
message: "Method signature preset:",
|
|
2779
3088
|
options: [
|
|
2780
3089
|
{ value: "crud", label: "Standard CRUD (GetById, List, Create, Update, Delete)" },
|
|
@@ -2784,22 +3093,22 @@ async function runInterfaceCommand(interfaceNameArg, flags = {}) {
|
|
|
2784
3093
|
})
|
|
2785
3094
|
);
|
|
2786
3095
|
generateStub = handleCancel(
|
|
2787
|
-
await
|
|
3096
|
+
await p15.confirm({
|
|
2788
3097
|
message: "Auto-generate Infrastructure Stub implementation?",
|
|
2789
3098
|
initialValue: true
|
|
2790
3099
|
})
|
|
2791
3100
|
);
|
|
2792
3101
|
if (generateStub) {
|
|
2793
3102
|
registerInProvider = handleCancel(
|
|
2794
|
-
await
|
|
3103
|
+
await p15.confirm({
|
|
2795
3104
|
message: "Auto-register interface & stub in feature diProvider.ts?",
|
|
2796
3105
|
initialValue: true
|
|
2797
3106
|
})
|
|
2798
3107
|
);
|
|
2799
3108
|
}
|
|
2800
3109
|
}
|
|
2801
|
-
const s =
|
|
2802
|
-
s.start(`Generating interface '${
|
|
3110
|
+
const s = p15.spinner();
|
|
3111
|
+
s.start(`Generating interface '${pc15.cyan(ifaceName)}'...`);
|
|
2803
3112
|
const files = [];
|
|
2804
3113
|
const options = {
|
|
2805
3114
|
name: ifaceName,
|
|
@@ -2828,7 +3137,7 @@ async function runInterfaceCommand(interfaceNameArg, flags = {}) {
|
|
|
2828
3137
|
files.push(stubFile);
|
|
2829
3138
|
}
|
|
2830
3139
|
if (registerInProvider && generateStub) {
|
|
2831
|
-
const featureDir =
|
|
3140
|
+
const featureDir = path2.join(resolveFeaturesDir(projectRoot), feature);
|
|
2832
3141
|
const providerFile = updateOrGenerateDiProvider(
|
|
2833
3142
|
featureDir,
|
|
2834
3143
|
feature,
|
|
@@ -2848,7 +3157,7 @@ async function runInterfaceCommand(interfaceNameArg, flags = {}) {
|
|
|
2848
3157
|
}
|
|
2849
3158
|
s.stop("Interface generated successfully!");
|
|
2850
3159
|
printGeneratedFiles(files, options.dryRun);
|
|
2851
|
-
|
|
3160
|
+
p15.outro(pc15.green(`Interface '${ifaceName}' created in feature '${feature}'!`));
|
|
2852
3161
|
}
|
|
2853
3162
|
async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
|
|
2854
3163
|
const projectRoot = findProjectRoot();
|
|
@@ -2859,14 +3168,14 @@ async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
|
|
|
2859
3168
|
const featureOptions = existingFeatures.map((f) => ({ value: f, label: f }));
|
|
2860
3169
|
featureOptions.push({ value: "__custom__", label: "\u270F\uFE0F Enter custom feature name..." });
|
|
2861
3170
|
const selected = handleCancel(
|
|
2862
|
-
await
|
|
3171
|
+
await p15.select({
|
|
2863
3172
|
message: "Select target feature:",
|
|
2864
3173
|
options: featureOptions
|
|
2865
3174
|
})
|
|
2866
3175
|
);
|
|
2867
3176
|
if (selected === "__custom__") {
|
|
2868
3177
|
feature = handleCancel(
|
|
2869
|
-
await
|
|
3178
|
+
await p15.text({
|
|
2870
3179
|
message: "Enter feature name:",
|
|
2871
3180
|
validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
|
|
2872
3181
|
})
|
|
@@ -2876,7 +3185,7 @@ async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
|
|
|
2876
3185
|
}
|
|
2877
3186
|
} else {
|
|
2878
3187
|
feature = handleCancel(
|
|
2879
|
-
await
|
|
3188
|
+
await p15.text({
|
|
2880
3189
|
message: "Enter target feature name:",
|
|
2881
3190
|
validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
|
|
2882
3191
|
})
|
|
@@ -2887,7 +3196,7 @@ async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
|
|
|
2887
3196
|
let name = handlerNameArg?.trim();
|
|
2888
3197
|
if (!name) {
|
|
2889
3198
|
name = handleCancel(
|
|
2890
|
-
await
|
|
3199
|
+
await p15.text({
|
|
2891
3200
|
message: "HTTP Route Handler class name:",
|
|
2892
3201
|
placeholder: "e.g. CreateUserHttp, GetUserHttp, HealthHttp",
|
|
2893
3202
|
validate: (v) => v && v.trim() ? void 0 : "Handler name cannot be empty"
|
|
@@ -2906,7 +3215,7 @@ async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
|
|
|
2906
3215
|
let validationFields = [];
|
|
2907
3216
|
if (!flags.yes) {
|
|
2908
3217
|
method = handleCancel(
|
|
2909
|
-
await
|
|
3218
|
+
await p15.select({
|
|
2910
3219
|
message: "HTTP Method:",
|
|
2911
3220
|
options: [
|
|
2912
3221
|
{ value: "get", label: "GET" },
|
|
@@ -2920,7 +3229,7 @@ async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
|
|
|
2920
3229
|
);
|
|
2921
3230
|
const defaultPath = method === "get" ? "/" : "/";
|
|
2922
3231
|
subPath = handleCancel(
|
|
2923
|
-
await
|
|
3232
|
+
await p15.text({
|
|
2924
3233
|
message: "Route subpath (relative to feature prefix):",
|
|
2925
3234
|
initialValue: defaultPath,
|
|
2926
3235
|
defaultValue: defaultPath
|
|
@@ -2931,7 +3240,7 @@ async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
|
|
|
2931
3240
|
const ucOptions = useCases.map((u) => ({ value: u.name, label: u.name }));
|
|
2932
3241
|
ucOptions.push({ value: "__none__", label: "None / Standalone Handler" });
|
|
2933
3242
|
const chosenUc = handleCancel(
|
|
2934
|
-
await
|
|
3243
|
+
await p15.select({
|
|
2935
3244
|
message: "Connect this handler to a domain Use Case?",
|
|
2936
3245
|
options: ucOptions,
|
|
2937
3246
|
initialValue: ucOptions[0]?.value
|
|
@@ -2942,7 +3251,7 @@ async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
|
|
|
2942
3251
|
}
|
|
2943
3252
|
}
|
|
2944
3253
|
includeValidation = handleCancel(
|
|
2945
|
-
await
|
|
3254
|
+
await p15.confirm({
|
|
2946
3255
|
message: "Include Zod inbound request validation?",
|
|
2947
3256
|
initialValue: method !== "delete"
|
|
2948
3257
|
})
|
|
@@ -2950,7 +3259,7 @@ async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
|
|
|
2950
3259
|
if (includeValidation) {
|
|
2951
3260
|
const defaultSource = method === "get" ? "query" : "body";
|
|
2952
3261
|
validationSource = handleCancel(
|
|
2953
|
-
await
|
|
3262
|
+
await p15.select({
|
|
2954
3263
|
message: "Validation data source:",
|
|
2955
3264
|
options: [
|
|
2956
3265
|
{ value: "body", label: "req.body (for POST / PUT / PATCH payloads)" },
|
|
@@ -2964,8 +3273,8 @@ async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
|
|
|
2964
3273
|
});
|
|
2965
3274
|
}
|
|
2966
3275
|
}
|
|
2967
|
-
const s =
|
|
2968
|
-
s.start(`Generating HTTP handler '${
|
|
3276
|
+
const s = p15.spinner();
|
|
3277
|
+
s.start(`Generating HTTP handler '${pc15.cyan(className)}'...`);
|
|
2969
3278
|
const options = {
|
|
2970
3279
|
name: className,
|
|
2971
3280
|
feature,
|
|
@@ -2983,7 +3292,7 @@ async function runHttpHandlerCommand(handlerNameArg, flags = {}) {
|
|
|
2983
3292
|
const files = generateHttpHandler(options);
|
|
2984
3293
|
s.stop("HTTP handler generated successfully!");
|
|
2985
3294
|
printGeneratedFiles(files, options.dryRun);
|
|
2986
|
-
|
|
3295
|
+
p15.outro(pc15.green(`HTTP handler '${className}' created in feature '${feature}'!`));
|
|
2987
3296
|
}
|
|
2988
3297
|
function generateInjectableCode(className, dependencies = [], methods = [], implementsInterface) {
|
|
2989
3298
|
const formattedDeps = dependencies.map((dep) => ({
|
|
@@ -2993,7 +3302,7 @@ function generateInjectableCode(className, dependencies = [], methods = [], impl
|
|
|
2993
3302
|
const formattedMethods = methods.map((m) => {
|
|
2994
3303
|
const isAsync = m.async !== false;
|
|
2995
3304
|
const retType = m.returnType ? isAsync ? `Promise<${m.returnType}>` : m.returnType : isAsync ? "Promise<void>" : "void";
|
|
2996
|
-
const params = m.params && m.params.length > 0 ? m.params.map((
|
|
3305
|
+
const params = m.params && m.params.length > 0 ? m.params.map((p17) => `${p17.name}: ${p17.type}`).join(", ") : "";
|
|
2997
3306
|
return {
|
|
2998
3307
|
name: m.name,
|
|
2999
3308
|
isAsync,
|
|
@@ -3016,21 +3325,21 @@ function generateInjectable(options) {
|
|
|
3016
3325
|
const className = toPascalCase(options.name);
|
|
3017
3326
|
let targetDir = "";
|
|
3018
3327
|
if (options.customPath) {
|
|
3019
|
-
targetDir =
|
|
3328
|
+
targetDir = path2.isAbsolute(options.customPath) ? options.customPath : path2.join(projectRoot, options.customPath);
|
|
3020
3329
|
} else if (options.location === "feature-service" && options.feature) {
|
|
3021
|
-
targetDir =
|
|
3330
|
+
targetDir = path2.join(resolveFeaturesDir(projectRoot), options.feature, "services");
|
|
3022
3331
|
} else if (options.location === "feature-transformer" && options.feature) {
|
|
3023
|
-
targetDir =
|
|
3332
|
+
targetDir = path2.join(resolveFeaturesDir(projectRoot), options.feature, "pxExpress", "transformers");
|
|
3024
3333
|
} else if (options.location === "module") {
|
|
3025
|
-
targetDir =
|
|
3334
|
+
targetDir = path2.join(projectRoot, "modules", className);
|
|
3026
3335
|
} else {
|
|
3027
|
-
targetDir = options.feature ?
|
|
3336
|
+
targetDir = options.feature ? path2.join(resolveFeaturesDir(projectRoot), options.feature, "services") : path2.join(projectRoot, "shared", className.toLowerCase());
|
|
3028
3337
|
}
|
|
3029
|
-
const filePath =
|
|
3338
|
+
const filePath = path2.join(targetDir, `${className}.ts`);
|
|
3030
3339
|
let ifaceName;
|
|
3031
3340
|
if (options.generateInterface) {
|
|
3032
3341
|
ifaceName = `I${className}`;
|
|
3033
|
-
const ifacePath =
|
|
3342
|
+
const ifacePath = path2.join(targetDir, `${ifaceName}.ts`);
|
|
3034
3343
|
const ifaceContent = renderTemplate(injectableInterfaceTemplate, { ifaceName });
|
|
3035
3344
|
files.push(
|
|
3036
3345
|
writeFileSafely(ifacePath, ifaceContent, {
|
|
@@ -3063,7 +3372,7 @@ async function runInjectableCommand(classNameArg, flags = {}) {
|
|
|
3063
3372
|
let name = classNameArg?.trim();
|
|
3064
3373
|
if (!name) {
|
|
3065
3374
|
name = handleCancel(
|
|
3066
|
-
await
|
|
3375
|
+
await p15.text({
|
|
3067
3376
|
message: "Injectable class name:",
|
|
3068
3377
|
placeholder: "e.g. PasswordHasher, TokenService, UserTransformer",
|
|
3069
3378
|
validate: (v) => v && v.trim() ? void 0 : "Class name cannot be empty"
|
|
@@ -3076,7 +3385,7 @@ async function runInjectableCommand(classNameArg, flags = {}) {
|
|
|
3076
3385
|
let customPath;
|
|
3077
3386
|
if (!flags.yes) {
|
|
3078
3387
|
location = handleCancel(
|
|
3079
|
-
await
|
|
3388
|
+
await p15.select({
|
|
3080
3389
|
message: "Select target architectural location:",
|
|
3081
3390
|
options: [
|
|
3082
3391
|
{ value: "feature-service", label: "Feature Service (features/<feature>/services/)" },
|
|
@@ -3094,14 +3403,14 @@ async function runInjectableCommand(classNameArg, flags = {}) {
|
|
|
3094
3403
|
const featureOptions = existingFeatures.map((f) => ({ value: f, label: f }));
|
|
3095
3404
|
featureOptions.push({ value: "__custom__", label: "\u270F\uFE0F Enter custom feature name..." });
|
|
3096
3405
|
const selected = handleCancel(
|
|
3097
|
-
await
|
|
3406
|
+
await p15.select({
|
|
3098
3407
|
message: "Select target feature:",
|
|
3099
3408
|
options: featureOptions
|
|
3100
3409
|
})
|
|
3101
3410
|
);
|
|
3102
3411
|
if (selected === "__custom__") {
|
|
3103
3412
|
feature = handleCancel(
|
|
3104
|
-
await
|
|
3413
|
+
await p15.text({
|
|
3105
3414
|
message: "Enter feature name:",
|
|
3106
3415
|
validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
|
|
3107
3416
|
})
|
|
@@ -3111,7 +3420,7 @@ async function runInjectableCommand(classNameArg, flags = {}) {
|
|
|
3111
3420
|
}
|
|
3112
3421
|
} else {
|
|
3113
3422
|
feature = handleCancel(
|
|
3114
|
-
await
|
|
3423
|
+
await p15.text({
|
|
3115
3424
|
message: "Enter target feature name:",
|
|
3116
3425
|
validate: (v) => v && v.trim() ? void 0 : "Feature name cannot be empty"
|
|
3117
3426
|
})
|
|
@@ -3121,7 +3430,7 @@ async function runInjectableCommand(classNameArg, flags = {}) {
|
|
|
3121
3430
|
feature = toCamelCase(feature);
|
|
3122
3431
|
} else if (location === "custom") {
|
|
3123
3432
|
customPath = handleCancel(
|
|
3124
|
-
await
|
|
3433
|
+
await p15.text({
|
|
3125
3434
|
message: "Enter destination directory path:",
|
|
3126
3435
|
placeholder: "e.g. src/services or lib/utils",
|
|
3127
3436
|
validate: (v) => v && v.trim() ? void 0 : "Path cannot be empty"
|
|
@@ -3132,14 +3441,14 @@ async function runInjectableCommand(classNameArg, flags = {}) {
|
|
|
3132
3441
|
let generateInterface2 = false;
|
|
3133
3442
|
if (!flags.yes) {
|
|
3134
3443
|
generateInterface2 = handleCancel(
|
|
3135
|
-
await
|
|
3444
|
+
await p15.confirm({
|
|
3136
3445
|
message: `Generate abstract class interface (I${className}) as runtime DI token?`,
|
|
3137
3446
|
initialValue: false
|
|
3138
3447
|
})
|
|
3139
3448
|
);
|
|
3140
3449
|
}
|
|
3141
|
-
const s =
|
|
3142
|
-
s.start(`Generating injectable '${
|
|
3450
|
+
const s = p15.spinner();
|
|
3451
|
+
s.start(`Generating injectable '${pc15.cyan(className)}'...`);
|
|
3143
3452
|
const options = {
|
|
3144
3453
|
name: className,
|
|
3145
3454
|
feature,
|
|
@@ -3153,12 +3462,12 @@ async function runInjectableCommand(classNameArg, flags = {}) {
|
|
|
3153
3462
|
const files = generateInjectable(options);
|
|
3154
3463
|
s.stop("Injectable class generated successfully!");
|
|
3155
3464
|
printGeneratedFiles(files, options.dryRun);
|
|
3156
|
-
|
|
3465
|
+
p15.outro(pc15.green(`Injectable '${className}' created successfully!`));
|
|
3157
3466
|
}
|
|
3158
3467
|
async function runInteractiveMenu(flags = {}) {
|
|
3159
3468
|
printBanner();
|
|
3160
3469
|
const choice = handleCancel(
|
|
3161
|
-
await
|
|
3470
|
+
await p15.select({
|
|
3162
3471
|
message: "What would you like to create in your architecture?",
|
|
3163
3472
|
options: [
|
|
3164
3473
|
{
|
|
@@ -3166,6 +3475,11 @@ async function runInteractiveMenu(flags = {}) {
|
|
|
3166
3475
|
label: "Feature",
|
|
3167
3476
|
hint: "Complete vertical slice (domain, infra, usecase, pxExpress, diProvider)"
|
|
3168
3477
|
},
|
|
3478
|
+
{
|
|
3479
|
+
value: "shared",
|
|
3480
|
+
label: "Shared Module",
|
|
3481
|
+
hint: "Foundational utility or port with ports, infrastructure & diProvider"
|
|
3482
|
+
},
|
|
3169
3483
|
{
|
|
3170
3484
|
value: "usecase",
|
|
3171
3485
|
label: "Use Case",
|
|
@@ -3203,6 +3517,9 @@ async function runInteractiveMenu(flags = {}) {
|
|
|
3203
3517
|
case "feature":
|
|
3204
3518
|
await runFeatureCommand(void 0, flags);
|
|
3205
3519
|
break;
|
|
3520
|
+
case "shared":
|
|
3521
|
+
await runSharedCommand(void 0, flags);
|
|
3522
|
+
break;
|
|
3206
3523
|
case "usecase":
|
|
3207
3524
|
await runUseCaseCommand(void 0, flags);
|
|
3208
3525
|
break;
|
|
@@ -3235,6 +3552,9 @@ async function handleCreate(type, name, flags = {}) {
|
|
|
3235
3552
|
case "feature":
|
|
3236
3553
|
await runFeatureCommand(name, flags);
|
|
3237
3554
|
break;
|
|
3555
|
+
case "shared":
|
|
3556
|
+
await runSharedCommand(name, flags);
|
|
3557
|
+
break;
|
|
3238
3558
|
case "usecase":
|
|
3239
3559
|
await runUseCaseCommand(name, flags);
|
|
3240
3560
|
break;
|
|
@@ -3255,21 +3575,172 @@ async function handleCreate(type, name, flags = {}) {
|
|
|
3255
3575
|
await runInjectableCommand(name, flags);
|
|
3256
3576
|
break;
|
|
3257
3577
|
default:
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
`Unknown component type: "${type}". Allowed types: feature, usecase, domain, infrastructure, interface, httpHandler, injectable.`
|
|
3578
|
+
p15.log.error(
|
|
3579
|
+
pc15.red(
|
|
3580
|
+
`Unknown component type: "${type}". Allowed types: feature, shared, usecase, domain, infrastructure, interface, httpHandler, injectable.`
|
|
3261
3581
|
)
|
|
3262
3582
|
);
|
|
3263
3583
|
process.exit(1);
|
|
3264
3584
|
}
|
|
3265
3585
|
}
|
|
3586
|
+
var importPrompts = {
|
|
3587
|
+
select: p15.select,
|
|
3588
|
+
text: p15.text
|
|
3589
|
+
};
|
|
3590
|
+
function rewriteImports(content, aliasMap) {
|
|
3591
|
+
if (aliasMap.size === 0) return content;
|
|
3592
|
+
let rewritten = content;
|
|
3593
|
+
for (const [oldImport, newImport] of aliasMap.entries()) {
|
|
3594
|
+
const rawOld = oldImport.endsWith("/") ? oldImport.slice(0, -1) : oldImport;
|
|
3595
|
+
const rawNew = newImport.endsWith("/") ? newImport.slice(0, -1) : newImport;
|
|
3596
|
+
const escaped = rawOld.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3597
|
+
const regex = new RegExp(`${escaped}(?=[\\/'"\`]|$)`, "g");
|
|
3598
|
+
rewritten = rewritten.replace(regex, rawNew);
|
|
3599
|
+
}
|
|
3600
|
+
return rewritten;
|
|
3601
|
+
}
|
|
3602
|
+
function getModuleBaseDir(projectRoot, modType) {
|
|
3603
|
+
const folder = modType === "shared" ? "shared" : "features";
|
|
3604
|
+
return path2.join(projectRoot, "src", folder);
|
|
3605
|
+
}
|
|
3606
|
+
function getExistingModuleDir(projectRoot, modType, modName) {
|
|
3607
|
+
const folder = modType === "shared" ? "shared" : "features";
|
|
3608
|
+
const inSrc = path2.join(projectRoot, "src", folder, modName);
|
|
3609
|
+
if (fs12.existsSync(inSrc)) return inSrc;
|
|
3610
|
+
const inRoot = path2.join(projectRoot, folder, modName);
|
|
3611
|
+
if (fs12.existsSync(inRoot)) return inRoot;
|
|
3612
|
+
return null;
|
|
3613
|
+
}
|
|
3614
|
+
function checkModuleExists(projectRoot, modType, modName) {
|
|
3615
|
+
return getExistingModuleDir(projectRoot, modType, modName) !== null;
|
|
3616
|
+
}
|
|
3617
|
+
async function resolveDependency(depType, depName, aliasMap, projectRoot, flags, importModuleRecursive, importedModules, registryBase, index) {
|
|
3618
|
+
const folder = depType === "shared" ? "shared" : "features";
|
|
3619
|
+
const isAlreadyImportedThisRun = importedModules.has(`${depType}:${depName}`);
|
|
3620
|
+
if (isAlreadyImportedThisRun) return;
|
|
3621
|
+
const programmaticAlias = flags.alias?.[depName] || flags.alias?.[`${depType}/${depName}`];
|
|
3622
|
+
if (programmaticAlias) {
|
|
3623
|
+
aliasMap.set(`@/${folder}/${depName}/`, `@/${folder}/${programmaticAlias}/`);
|
|
3624
|
+
const baseDir = getModuleBaseDir(projectRoot, depType);
|
|
3625
|
+
const displayDest = path2.relative(
|
|
3626
|
+
projectRoot,
|
|
3627
|
+
path2.join(baseDir, programmaticAlias)
|
|
3628
|
+
);
|
|
3629
|
+
p15.log.step(`Importing '${depName}' into '${displayDest}'...`);
|
|
3630
|
+
await importModuleRecursive(depType, depName, programmaticAlias);
|
|
3631
|
+
return;
|
|
3632
|
+
}
|
|
3633
|
+
const existingDir = getExistingModuleDir(projectRoot, depType, depName);
|
|
3634
|
+
if (!existingDir) {
|
|
3635
|
+
p15.log.info(` \u21B3 Resolving required ${depType} dependency: ${pc15.cyan(depName)}`);
|
|
3636
|
+
await importModuleRecursive(depType, depName, depName);
|
|
3637
|
+
return;
|
|
3638
|
+
}
|
|
3639
|
+
const entry = (depType === "shared" ? index?.shared : index?.features)?.find(
|
|
3640
|
+
(item) => item.name === depName
|
|
3641
|
+
);
|
|
3642
|
+
let remoteIntegrity = entry?.integrity;
|
|
3643
|
+
if (!remoteIntegrity) {
|
|
3644
|
+
try {
|
|
3645
|
+
const depManifest = await fetchModuleManifest(registryBase, depType, depName);
|
|
3646
|
+
remoteIntegrity = depManifest.integrity;
|
|
3647
|
+
} catch {
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
const localHash = computeDirectoryHash(existingDir);
|
|
3651
|
+
if (remoteIntegrity && localHash === remoteIntegrity) {
|
|
3652
|
+
p15.log.step(`\u2713 ${depType}/${depName} is up to date (reusing existing)`);
|
|
3653
|
+
importedModules.add(`${depType}:${depName}`);
|
|
3654
|
+
return;
|
|
3655
|
+
}
|
|
3656
|
+
if (flags.overwrite) {
|
|
3657
|
+
p15.log.step(`Overwriting '${depType}/${depName}' with registry version (--overwrite)...`);
|
|
3658
|
+
const baseDir = getModuleBaseDir(projectRoot, depType);
|
|
3659
|
+
const targetDir = path2.join(baseDir, depName);
|
|
3660
|
+
if (!flags.dryRun) {
|
|
3661
|
+
if (existingDir !== targetDir) {
|
|
3662
|
+
fs12.rmSync(existingDir, { recursive: true, force: true });
|
|
3663
|
+
}
|
|
3664
|
+
fs12.rmSync(targetDir, { recursive: true, force: true });
|
|
3665
|
+
}
|
|
3666
|
+
await importModuleRecursive(depType, depName, depName, { overwrite: true });
|
|
3667
|
+
return;
|
|
3668
|
+
}
|
|
3669
|
+
if (flags.yes) {
|
|
3670
|
+
p15.log.info(`Using existing '${depType}/${depName}'`);
|
|
3671
|
+
importedModules.add(`${depType}:${depName}`);
|
|
3672
|
+
return;
|
|
3673
|
+
}
|
|
3674
|
+
const choice = handleCancel(
|
|
3675
|
+
await importPrompts.select({
|
|
3676
|
+
message: `Local '${depType}/${depName}' differs from the registry version.`,
|
|
3677
|
+
options: [
|
|
3678
|
+
{
|
|
3679
|
+
value: "keep_existing",
|
|
3680
|
+
label: `Keep existing local version (Recommended)`
|
|
3681
|
+
},
|
|
3682
|
+
{
|
|
3683
|
+
value: "rename",
|
|
3684
|
+
label: "Import fresh copy under a new name (alias)"
|
|
3685
|
+
},
|
|
3686
|
+
{
|
|
3687
|
+
value: "overwrite",
|
|
3688
|
+
label: "Overwrite local version with registry version"
|
|
3689
|
+
}
|
|
3690
|
+
]
|
|
3691
|
+
})
|
|
3692
|
+
);
|
|
3693
|
+
if (choice === "keep_existing" || choice === "use_existing") {
|
|
3694
|
+
p15.log.step(`Skipping '${depType}/${depName}' (keeping existing)`);
|
|
3695
|
+
importedModules.add(`${depType}:${depName}`);
|
|
3696
|
+
return;
|
|
3697
|
+
}
|
|
3698
|
+
if (choice === "rename") {
|
|
3699
|
+
const newName = handleCancel(
|
|
3700
|
+
await importPrompts.text({
|
|
3701
|
+
message: "What should we name the new copy?",
|
|
3702
|
+
placeholder: `${depName}-copy`,
|
|
3703
|
+
validate(val) {
|
|
3704
|
+
const v = val?.trim();
|
|
3705
|
+
if (!v) return "Name cannot be empty";
|
|
3706
|
+
if (v === depName) return `Name must be different from '${depName}'`;
|
|
3707
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(v)) {
|
|
3708
|
+
return "Name can only contain alphanumeric characters, dashes, and underscores";
|
|
3709
|
+
}
|
|
3710
|
+
if (checkModuleExists(projectRoot, depType, v)) {
|
|
3711
|
+
return `'${v}' already exists in ${folder}, please choose another name`;
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
})
|
|
3715
|
+
).trim();
|
|
3716
|
+
aliasMap.set(`@/${folder}/${depName}/`, `@/${folder}/${newName}/`);
|
|
3717
|
+
const baseDir = getModuleBaseDir(projectRoot, depType);
|
|
3718
|
+
const displayDest = path2.relative(projectRoot, path2.join(baseDir, newName));
|
|
3719
|
+
p15.log.step(`Importing '${depName}' into '${displayDest}'...`);
|
|
3720
|
+
await importModuleRecursive(depType, depName, newName);
|
|
3721
|
+
return;
|
|
3722
|
+
}
|
|
3723
|
+
if (choice === "overwrite") {
|
|
3724
|
+
p15.log.step(`Overwriting '${depType}/${depName}' with registry version...`);
|
|
3725
|
+
const baseDir = getModuleBaseDir(projectRoot, depType);
|
|
3726
|
+
const targetDir = path2.join(baseDir, depName);
|
|
3727
|
+
if (!flags.dryRun) {
|
|
3728
|
+
if (existingDir !== targetDir) {
|
|
3729
|
+
fs12.rmSync(existingDir, { recursive: true, force: true });
|
|
3730
|
+
}
|
|
3731
|
+
fs12.rmSync(targetDir, { recursive: true, force: true });
|
|
3732
|
+
}
|
|
3733
|
+
await importModuleRecursive(depType, depName, depName, { overwrite: true });
|
|
3734
|
+
return;
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3266
3737
|
async function handleImport(typeArg, nameArg, flags = {}) {
|
|
3267
3738
|
const projectRoot = findProjectRoot();
|
|
3268
3739
|
let type = typeArg?.trim().toLowerCase();
|
|
3269
3740
|
if (type === "features") type = "feature";
|
|
3270
3741
|
if (!type) {
|
|
3271
3742
|
type = handleCancel(
|
|
3272
|
-
await
|
|
3743
|
+
await importPrompts.select({
|
|
3273
3744
|
message: "What kind of module would you like to import?",
|
|
3274
3745
|
options: [
|
|
3275
3746
|
{
|
|
@@ -3287,19 +3758,19 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
3287
3758
|
);
|
|
3288
3759
|
}
|
|
3289
3760
|
if (type !== "shared" && type !== "feature") {
|
|
3290
|
-
|
|
3761
|
+
p15.log.error(pc15.red(`Invalid module type: "${typeArg}". Must be "shared" or "feature".`));
|
|
3291
3762
|
process.exit(1);
|
|
3292
3763
|
}
|
|
3293
3764
|
const registryBase = getRegistryBase(projectRoot, flags.registry);
|
|
3294
|
-
const s =
|
|
3295
|
-
s.start(`Connecting to registry at ${
|
|
3765
|
+
const s = p15.spinner();
|
|
3766
|
+
s.start(`Connecting to registry at ${pc15.dim(registryBase)}...`);
|
|
3296
3767
|
let index;
|
|
3297
3768
|
try {
|
|
3298
3769
|
index = await fetchRegistryIndex(registryBase);
|
|
3299
3770
|
s.stop("Registry index loaded.");
|
|
3300
3771
|
} catch (err) {
|
|
3301
|
-
s.stop(
|
|
3302
|
-
|
|
3772
|
+
s.stop(pc15.red("Failed to load registry index."));
|
|
3773
|
+
p15.log.error(pc15.red(err.message || String(err)));
|
|
3303
3774
|
process.exit(1);
|
|
3304
3775
|
}
|
|
3305
3776
|
const availableList = type === "shared" ? index.shared : index.features;
|
|
@@ -3307,11 +3778,11 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
3307
3778
|
let name = nameArg?.trim();
|
|
3308
3779
|
if (!name) {
|
|
3309
3780
|
if (availableList.length === 0) {
|
|
3310
|
-
|
|
3781
|
+
p15.log.warn(`No ${type} modules available in the registry.`);
|
|
3311
3782
|
return;
|
|
3312
3783
|
}
|
|
3313
3784
|
name = handleCancel(
|
|
3314
|
-
await
|
|
3785
|
+
await importPrompts.select({
|
|
3315
3786
|
message: `Select a ${type} to import:`,
|
|
3316
3787
|
options: availableList.map((item) => ({
|
|
3317
3788
|
value: item.name,
|
|
@@ -3323,122 +3794,769 @@ async function handleImport(typeArg, nameArg, flags = {}) {
|
|
|
3323
3794
|
}
|
|
3324
3795
|
if (!candidateNames.includes(name)) {
|
|
3325
3796
|
const closest = findClosestMatch(name, candidateNames);
|
|
3326
|
-
|
|
3797
|
+
p15.log.error(pc15.red(`\u274C Module '${name}' not found in the ${type} registry.`));
|
|
3327
3798
|
if (closest) {
|
|
3328
|
-
|
|
3799
|
+
p15.log.info(pc15.yellow(`Did you mean '${pc15.cyan(closest)}'?`));
|
|
3329
3800
|
}
|
|
3330
3801
|
process.exit(1);
|
|
3331
3802
|
}
|
|
3332
3803
|
const importedModules = /* @__PURE__ */ new Set();
|
|
3333
3804
|
const totalWrittenFiles = [];
|
|
3334
3805
|
const requiredNpmPackages = /* @__PURE__ */ new Set();
|
|
3335
|
-
async function importModuleRecursive(modType,
|
|
3336
|
-
const modKey = `${modType}:${
|
|
3806
|
+
async function importModuleRecursive(modType, registryName, targetName = registryName, options) {
|
|
3807
|
+
const modKey = `${modType}:${targetName}`;
|
|
3337
3808
|
if (importedModules.has(modKey)) return;
|
|
3338
|
-
|
|
3809
|
+
p15.log.step(
|
|
3810
|
+
`Importing ${modType}: ${pc15.cyan(targetName)}${targetName !== registryName ? pc15.dim(` (from ${registryName})`) : ""}...`
|
|
3811
|
+
);
|
|
3339
3812
|
let manifest;
|
|
3340
3813
|
try {
|
|
3341
|
-
manifest = await fetchModuleManifest(registryBase, modType,
|
|
3814
|
+
manifest = await fetchModuleManifest(registryBase, modType, registryName);
|
|
3342
3815
|
} catch (err) {
|
|
3343
|
-
|
|
3816
|
+
p15.log.error(pc15.red(`Failed to fetch manifest for ${modType} '${registryName}': ${err.message}`));
|
|
3344
3817
|
process.exit(1);
|
|
3345
3818
|
}
|
|
3819
|
+
const aliasMap = /* @__PURE__ */ new Map();
|
|
3820
|
+
if (targetName !== registryName) {
|
|
3821
|
+
const folder2 = modType === "shared" ? "shared" : "features";
|
|
3822
|
+
aliasMap.set(`@/${folder2}/${registryName}/`, `@/${folder2}/${targetName}/`);
|
|
3823
|
+
}
|
|
3346
3824
|
const sharedDeps = manifest.dependencies?.shared || [];
|
|
3347
3825
|
for (const dep of sharedDeps) {
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3826
|
+
await resolveDependency(
|
|
3827
|
+
"shared",
|
|
3828
|
+
dep,
|
|
3829
|
+
aliasMap,
|
|
3830
|
+
projectRoot,
|
|
3831
|
+
flags,
|
|
3832
|
+
importModuleRecursive,
|
|
3833
|
+
importedModules,
|
|
3834
|
+
registryBase,
|
|
3835
|
+
index
|
|
3836
|
+
);
|
|
3353
3837
|
}
|
|
3354
3838
|
const featureDeps = manifest.dependencies?.features || [];
|
|
3355
3839
|
for (const dep of featureDeps) {
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3840
|
+
await resolveDependency(
|
|
3841
|
+
"feature",
|
|
3842
|
+
dep,
|
|
3843
|
+
aliasMap,
|
|
3844
|
+
projectRoot,
|
|
3845
|
+
flags,
|
|
3846
|
+
importModuleRecursive,
|
|
3847
|
+
importedModules,
|
|
3848
|
+
registryBase,
|
|
3849
|
+
index
|
|
3850
|
+
);
|
|
3361
3851
|
}
|
|
3362
3852
|
for (const pkg of manifest.dependencies?.npm || []) {
|
|
3363
3853
|
requiredNpmPackages.add(pkg);
|
|
3364
3854
|
}
|
|
3365
3855
|
const folder = modType === "shared" ? "shared" : "features";
|
|
3366
|
-
const targetDir =
|
|
3856
|
+
const targetDir = path2.join(getModuleBaseDir(projectRoot, modType), targetName);
|
|
3857
|
+
const isOverwrite = Boolean(flags.overwrite || options?.overwrite);
|
|
3367
3858
|
for (const file of manifest.files || []) {
|
|
3368
3859
|
if (file === "registry.json") continue;
|
|
3369
|
-
const remoteFilePath = `${folder}/${
|
|
3370
|
-
const localFilePath =
|
|
3371
|
-
if (
|
|
3372
|
-
|
|
3860
|
+
const remoteFilePath = `${folder}/${registryName}/${file}`;
|
|
3861
|
+
const localFilePath = path2.join(targetDir, file);
|
|
3862
|
+
if (fs12.existsSync(localFilePath) && !isOverwrite) {
|
|
3863
|
+
p15.log.warn(
|
|
3864
|
+
` \u21B3 Skipping existing file: ${pc15.dim(path2.relative(projectRoot, localFilePath))} (use --overwrite to replace)`
|
|
3865
|
+
);
|
|
3373
3866
|
continue;
|
|
3374
3867
|
}
|
|
3375
|
-
|
|
3868
|
+
let content = await fetchRegistryFile(registryBase, remoteFilePath);
|
|
3869
|
+
content = rewriteImports(content, aliasMap);
|
|
3376
3870
|
if (!flags.dryRun) {
|
|
3377
|
-
|
|
3378
|
-
|
|
3871
|
+
fs12.mkdirSync(path2.dirname(localFilePath), { recursive: true });
|
|
3872
|
+
fs12.writeFileSync(localFilePath, content, "utf-8");
|
|
3379
3873
|
}
|
|
3380
|
-
const displayPath =
|
|
3874
|
+
const displayPath = path2.relative(projectRoot, localFilePath);
|
|
3381
3875
|
totalWrittenFiles.push(displayPath);
|
|
3382
|
-
|
|
3876
|
+
p15.log.message(` ${pc15.green("+")} ${displayPath}`);
|
|
3383
3877
|
}
|
|
3384
3878
|
importedModules.add(modKey);
|
|
3385
3879
|
}
|
|
3386
|
-
await importModuleRecursive(type, name);
|
|
3880
|
+
await importModuleRecursive(type, name, name);
|
|
3387
3881
|
const missingNpm = getMissingNpmPackages(projectRoot, Array.from(requiredNpmPackages));
|
|
3388
3882
|
if (missingNpm.length > 0) {
|
|
3389
3883
|
if (flags.skipInstall || flags.dryRun) {
|
|
3390
|
-
|
|
3884
|
+
p15.log.info(`Required npm dependencies to install: ${pc15.cyan(missingNpm.join(", "))}`);
|
|
3391
3885
|
} else {
|
|
3392
3886
|
try {
|
|
3393
3887
|
installNpmPackages(projectRoot, missingNpm);
|
|
3394
3888
|
} catch (err) {
|
|
3395
|
-
|
|
3889
|
+
p15.log.warn(`Could not automatically install packages (${err.message}). Please run: npm install ${missingNpm.join(" ")}`);
|
|
3396
3890
|
}
|
|
3397
3891
|
}
|
|
3398
3892
|
}
|
|
3399
3893
|
if (flags.dryRun) {
|
|
3400
|
-
|
|
3894
|
+
p15.log.info(pc15.yellow("Dry run complete: No files were written to disk."));
|
|
3401
3895
|
} else {
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3896
|
+
const baseDir = getModuleBaseDir(projectRoot, type);
|
|
3897
|
+
const displayTarget = path2.relative(projectRoot, path2.join(baseDir, name));
|
|
3898
|
+
p15.log.success(
|
|
3899
|
+
pc15.green(
|
|
3900
|
+
`Successfully imported ${type} '${name}' (${totalWrittenFiles.length} file${totalWrittenFiles.length === 1 ? "" : "s"} placed in ${displayTarget})`
|
|
3405
3901
|
)
|
|
3406
3902
|
);
|
|
3407
3903
|
}
|
|
3408
3904
|
}
|
|
3905
|
+
function loadMasterIndex(projectRoot) {
|
|
3906
|
+
const indexPath = path2.join(projectRoot, "index.json");
|
|
3907
|
+
if (!fs12.existsSync(indexPath)) {
|
|
3908
|
+
const initialIndex = { shared: [], features: [] };
|
|
3909
|
+
fs12.writeFileSync(indexPath, JSON.stringify(initialIndex, null, 2) + "\n", "utf-8");
|
|
3910
|
+
return initialIndex;
|
|
3911
|
+
}
|
|
3912
|
+
return JSON.parse(fs12.readFileSync(indexPath, "utf-8"));
|
|
3913
|
+
}
|
|
3914
|
+
function saveMasterIndex(projectRoot, index) {
|
|
3915
|
+
const indexPath = path2.join(projectRoot, "index.json");
|
|
3916
|
+
index.shared.sort((a, b) => a.name.localeCompare(b.name));
|
|
3917
|
+
index.features.sort((a, b) => a.name.localeCompare(b.name));
|
|
3918
|
+
fs12.writeFileSync(indexPath, JSON.stringify(index, null, 2) + "\n", "utf-8");
|
|
3919
|
+
}
|
|
3920
|
+
function discoverDiskModules(projectRoot) {
|
|
3921
|
+
const results = [];
|
|
3922
|
+
const sharedDir = resolveSharedDir(projectRoot);
|
|
3923
|
+
if (fs12.existsSync(sharedDir)) {
|
|
3924
|
+
const entries = fs12.readdirSync(sharedDir, { withFileTypes: true });
|
|
3925
|
+
for (const entry of entries) {
|
|
3926
|
+
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
3927
|
+
results.push({ name: entry.name, type: "shared", dir: path2.join(sharedDir, entry.name) });
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3931
|
+
const featuresDir = resolveFeaturesDir(projectRoot);
|
|
3932
|
+
if (fs12.existsSync(featuresDir)) {
|
|
3933
|
+
const entries = fs12.readdirSync(featuresDir, { withFileTypes: true });
|
|
3934
|
+
for (const entry of entries) {
|
|
3935
|
+
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
3936
|
+
results.push({ name: entry.name, type: "feature", dir: path2.join(featuresDir, entry.name) });
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3940
|
+
return results;
|
|
3941
|
+
}
|
|
3942
|
+
function checkGitStatusForDir(projectRoot, dirPath) {
|
|
3943
|
+
try {
|
|
3944
|
+
const relPath = path2.relative(projectRoot, dirPath);
|
|
3945
|
+
const output = execSync(`git status --porcelain -- "${relPath}"`, {
|
|
3946
|
+
cwd: projectRoot,
|
|
3947
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
3948
|
+
encoding: "utf-8"
|
|
3949
|
+
}).trim();
|
|
3950
|
+
if (!output) {
|
|
3951
|
+
return { hasChanges: false, filesCount: 0 };
|
|
3952
|
+
}
|
|
3953
|
+
const lines = output.split("\n").filter((l) => l.trim().length > 0);
|
|
3954
|
+
return { hasChanges: true, filesCount: lines.length };
|
|
3955
|
+
} catch {
|
|
3956
|
+
return { hasChanges: false, filesCount: 0 };
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
function getRegistryDiff(projectRoot) {
|
|
3960
|
+
const index = loadMasterIndex(projectRoot);
|
|
3961
|
+
const diskModules = discoverDiskModules(projectRoot);
|
|
3962
|
+
const diskMap = /* @__PURE__ */ new Map();
|
|
3963
|
+
for (const mod of diskModules) {
|
|
3964
|
+
diskMap.set(`${mod.type}:${mod.name}`, mod);
|
|
3965
|
+
}
|
|
3966
|
+
const allKeys = /* @__PURE__ */ new Set();
|
|
3967
|
+
for (const item of index.shared) {
|
|
3968
|
+
allKeys.add(`shared:${item.name}`);
|
|
3969
|
+
}
|
|
3970
|
+
for (const item of index.features) {
|
|
3971
|
+
allKeys.add(`feature:${item.name}`);
|
|
3972
|
+
}
|
|
3973
|
+
for (const mod of diskModules) {
|
|
3974
|
+
allKeys.add(`${mod.type}:${mod.name}`);
|
|
3975
|
+
}
|
|
3976
|
+
const statuses = [];
|
|
3977
|
+
for (const key of allKeys) {
|
|
3978
|
+
const [typeStr, name] = key.split(":");
|
|
3979
|
+
const type = typeStr;
|
|
3980
|
+
const registeredEntry = (type === "shared" ? index.shared : index.features).find(
|
|
3981
|
+
(e) => e.name === name
|
|
3982
|
+
);
|
|
3983
|
+
const isRegistered = Boolean(registeredEntry);
|
|
3984
|
+
const diskInfo = diskMap.get(key);
|
|
3985
|
+
const existsOnDisk = Boolean(diskInfo && fs12.existsSync(diskInfo.dir));
|
|
3986
|
+
let hasUncommittedGitChanges = false;
|
|
3987
|
+
let uncommittedFilesCount = 0;
|
|
3988
|
+
let hasIntegrityDrift = false;
|
|
3989
|
+
let localIntegrity;
|
|
3990
|
+
if (existsOnDisk && diskInfo) {
|
|
3991
|
+
const git = checkGitStatusForDir(projectRoot, diskInfo.dir);
|
|
3992
|
+
hasUncommittedGitChanges = git.hasChanges;
|
|
3993
|
+
uncommittedFilesCount = git.filesCount;
|
|
3994
|
+
localIntegrity = computeDirectoryHash(diskInfo.dir);
|
|
3995
|
+
if (isRegistered && registeredEntry?.integrity) {
|
|
3996
|
+
hasIntegrityDrift = localIntegrity !== registeredEntry.integrity;
|
|
3997
|
+
}
|
|
3998
|
+
}
|
|
3999
|
+
statuses.push({
|
|
4000
|
+
name,
|
|
4001
|
+
type,
|
|
4002
|
+
isRegistered,
|
|
4003
|
+
existsOnDisk,
|
|
4004
|
+
hasUncommittedGitChanges,
|
|
4005
|
+
uncommittedFilesCount,
|
|
4006
|
+
hasIntegrityDrift,
|
|
4007
|
+
localIntegrity,
|
|
4008
|
+
registeredIntegrity: registeredEntry?.integrity
|
|
4009
|
+
});
|
|
4010
|
+
}
|
|
4011
|
+
return statuses.sort((a, b) => a.type.localeCompare(b.type) || a.name.localeCompare(b.name));
|
|
4012
|
+
}
|
|
4013
|
+
async function registerModule(typeArg, nameArg, flags = {}) {
|
|
4014
|
+
const projectRoot = findProjectRoot();
|
|
4015
|
+
const index = loadMasterIndex(projectRoot);
|
|
4016
|
+
let type = typeArg?.trim();
|
|
4017
|
+
if (!type) {
|
|
4018
|
+
type = handleCancel(
|
|
4019
|
+
await p15.select({
|
|
4020
|
+
message: "Module type to register:",
|
|
4021
|
+
options: [
|
|
4022
|
+
{ value: "feature", label: "Feature" },
|
|
4023
|
+
{ value: "shared", label: "Shared Module" }
|
|
4024
|
+
]
|
|
4025
|
+
})
|
|
4026
|
+
);
|
|
4027
|
+
}
|
|
4028
|
+
const diskModules = discoverDiskModules(projectRoot).filter((m) => m.type === type);
|
|
4029
|
+
const registeredNames = new Set((type === "shared" ? index.shared : index.features).map((m) => m.name));
|
|
4030
|
+
const unregistered = diskModules.filter((m) => !registeredNames.has(m.name));
|
|
4031
|
+
let name = nameArg?.trim();
|
|
4032
|
+
if (!name) {
|
|
4033
|
+
if (unregistered.length > 0) {
|
|
4034
|
+
const opts = unregistered.map((m) => ({ value: m.name, label: m.name, hint: "unregistered on disk" }));
|
|
4035
|
+
opts.push({ value: "__custom__", label: "\u270F\uFE0F Enter custom name...", hint: "" });
|
|
4036
|
+
const choice = handleCancel(
|
|
4037
|
+
await p15.select({
|
|
4038
|
+
message: `Select a ${type} to register:`,
|
|
4039
|
+
options: opts
|
|
4040
|
+
})
|
|
4041
|
+
);
|
|
4042
|
+
if (choice === "__custom__") {
|
|
4043
|
+
name = handleCancel(
|
|
4044
|
+
await p15.text({
|
|
4045
|
+
message: `Enter ${type} name:`,
|
|
4046
|
+
validate: (v) => v && v.trim() ? void 0 : "Name cannot be empty"
|
|
4047
|
+
})
|
|
4048
|
+
).trim();
|
|
4049
|
+
} else {
|
|
4050
|
+
name = choice;
|
|
4051
|
+
}
|
|
4052
|
+
} else {
|
|
4053
|
+
name = handleCancel(
|
|
4054
|
+
await p15.text({
|
|
4055
|
+
message: `Enter ${type} name to register:`,
|
|
4056
|
+
validate: (v) => v && v.trim() ? void 0 : "Name cannot be empty"
|
|
4057
|
+
})
|
|
4058
|
+
).trim();
|
|
4059
|
+
}
|
|
4060
|
+
}
|
|
4061
|
+
const folder = type === "shared" ? resolveSharedDir(projectRoot) : resolveFeaturesDir(projectRoot);
|
|
4062
|
+
const modDir = path2.join(folder, name);
|
|
4063
|
+
if (!fs12.existsSync(modDir)) {
|
|
4064
|
+
p15.log.error(pc15.red(`Module directory does not exist: ${path2.relative(projectRoot, modDir)}`));
|
|
4065
|
+
process.exit(1);
|
|
4066
|
+
}
|
|
4067
|
+
const files = getAllFiles(modDir);
|
|
4068
|
+
const integrity = computeDirectoryHash(modDir, files);
|
|
4069
|
+
const manifestPath = path2.join(modDir, "registry.json");
|
|
4070
|
+
let manifest;
|
|
4071
|
+
if (fs12.existsSync(manifestPath)) {
|
|
4072
|
+
manifest = JSON.parse(fs12.readFileSync(manifestPath, "utf-8"));
|
|
4073
|
+
} else {
|
|
4074
|
+
manifest = {
|
|
4075
|
+
name,
|
|
4076
|
+
type,
|
|
4077
|
+
description: `${name} ${type} module`,
|
|
4078
|
+
version: "1.0.0",
|
|
4079
|
+
dependencies: {
|
|
4080
|
+
npm: [],
|
|
4081
|
+
shared: [],
|
|
4082
|
+
features: []
|
|
4083
|
+
},
|
|
4084
|
+
files: []
|
|
4085
|
+
};
|
|
4086
|
+
}
|
|
4087
|
+
manifest.name = name;
|
|
4088
|
+
manifest.type = type;
|
|
4089
|
+
manifest.files = files;
|
|
4090
|
+
manifest.integrity = integrity;
|
|
4091
|
+
fs12.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
4092
|
+
const targetList = type === "shared" ? index.shared : index.features;
|
|
4093
|
+
const existingIdx = targetList.findIndex((item) => item.name === name);
|
|
4094
|
+
if (existingIdx >= 0) {
|
|
4095
|
+
targetList[existingIdx] = {
|
|
4096
|
+
name,
|
|
4097
|
+
description: manifest.description,
|
|
4098
|
+
version: manifest.version,
|
|
4099
|
+
integrity
|
|
4100
|
+
};
|
|
4101
|
+
} else {
|
|
4102
|
+
targetList.push({
|
|
4103
|
+
name,
|
|
4104
|
+
description: manifest.description,
|
|
4105
|
+
version: manifest.version,
|
|
4106
|
+
integrity
|
|
4107
|
+
});
|
|
4108
|
+
}
|
|
4109
|
+
saveMasterIndex(projectRoot, index);
|
|
4110
|
+
p15.log.success(
|
|
4111
|
+
pc15.green(
|
|
4112
|
+
`\u2713 Module '${type}/${name}' registered in index.json (${files.length} files, ${integrity.slice(0, 18)}...)`
|
|
4113
|
+
)
|
|
4114
|
+
);
|
|
4115
|
+
}
|
|
4116
|
+
async function commitModule(typeArg, nameArg, flags = {}) {
|
|
4117
|
+
const projectRoot = findProjectRoot();
|
|
4118
|
+
let type = typeArg?.trim();
|
|
4119
|
+
if (!type) {
|
|
4120
|
+
type = handleCancel(
|
|
4121
|
+
await p15.select({
|
|
4122
|
+
message: "Module type to commit:",
|
|
4123
|
+
options: [
|
|
4124
|
+
{ value: "feature", label: "Feature" },
|
|
4125
|
+
{ value: "shared", label: "Shared Module" }
|
|
4126
|
+
]
|
|
4127
|
+
})
|
|
4128
|
+
);
|
|
4129
|
+
}
|
|
4130
|
+
let name = nameArg?.trim();
|
|
4131
|
+
if (!name) {
|
|
4132
|
+
const diskModules = discoverDiskModules(projectRoot).filter((m) => m.type === type);
|
|
4133
|
+
const opts = diskModules.map((m) => {
|
|
4134
|
+
const git = checkGitStatusForDir(projectRoot, m.dir);
|
|
4135
|
+
return {
|
|
4136
|
+
value: m.name,
|
|
4137
|
+
label: m.name,
|
|
4138
|
+
hint: git.hasChanges ? `${git.filesCount} uncommitted files` : "clean working tree"
|
|
4139
|
+
};
|
|
4140
|
+
});
|
|
4141
|
+
name = handleCancel(
|
|
4142
|
+
await p15.select({
|
|
4143
|
+
message: `Select ${type} to commit:`,
|
|
4144
|
+
options: opts
|
|
4145
|
+
})
|
|
4146
|
+
);
|
|
4147
|
+
}
|
|
4148
|
+
const folder = type === "shared" ? resolveSharedDir(projectRoot) : resolveFeaturesDir(projectRoot);
|
|
4149
|
+
const modDir = path2.join(folder, name);
|
|
4150
|
+
if (!fs12.existsSync(modDir)) {
|
|
4151
|
+
p15.log.error(pc15.red(`Module directory does not exist: ${path2.relative(projectRoot, modDir)}`));
|
|
4152
|
+
process.exit(1);
|
|
4153
|
+
}
|
|
4154
|
+
const relModDir = path2.relative(projectRoot, modDir);
|
|
4155
|
+
const gitStatus = checkGitStatusForDir(projectRoot, modDir);
|
|
4156
|
+
if (!gitStatus.hasChanges) {
|
|
4157
|
+
p15.log.warn(pc15.yellow(`No uncommitted git changes found in '${type}/${name}'.`));
|
|
4158
|
+
return;
|
|
4159
|
+
}
|
|
4160
|
+
let message = flags.message?.trim();
|
|
4161
|
+
if (!message) {
|
|
4162
|
+
message = handleCancel(
|
|
4163
|
+
await p15.text({
|
|
4164
|
+
message: `Commit message for ${type}/${name}:`,
|
|
4165
|
+
placeholder: `e.g. feat(${name}): update use cases and dependencies`,
|
|
4166
|
+
validate: (v) => v && v.trim() ? void 0 : "Commit message cannot be empty"
|
|
4167
|
+
})
|
|
4168
|
+
).trim();
|
|
4169
|
+
}
|
|
4170
|
+
const s = p15.spinner();
|
|
4171
|
+
s.start(`Committing '${type}/${name}' to git repository...`);
|
|
4172
|
+
try {
|
|
4173
|
+
execSync(`git add "${relModDir}"`, { cwd: projectRoot, stdio: ["ignore", "pipe", "pipe"] });
|
|
4174
|
+
execSync(`git commit -m "${message.replace(/"/g, '\\"')}" -- "${relModDir}"`, {
|
|
4175
|
+
cwd: projectRoot,
|
|
4176
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4177
|
+
});
|
|
4178
|
+
s.stop(`Changes for '${type}/${name}' committed to git.`);
|
|
4179
|
+
p15.log.success(pc15.green(`\u2713 Committed '${type}/${name}': "${message}"`));
|
|
4180
|
+
} catch (err) {
|
|
4181
|
+
s.stop(pc15.red("Git commit failed."));
|
|
4182
|
+
p15.log.error(pc15.red(err.message || String(err)));
|
|
4183
|
+
process.exit(1);
|
|
4184
|
+
}
|
|
4185
|
+
}
|
|
4186
|
+
async function registerModuleChanges(typeArg, nameArg, flags = {}) {
|
|
4187
|
+
const projectRoot = findProjectRoot();
|
|
4188
|
+
const index = loadMasterIndex(projectRoot);
|
|
4189
|
+
let type = typeArg?.trim();
|
|
4190
|
+
if (!type) {
|
|
4191
|
+
type = handleCancel(
|
|
4192
|
+
await p15.select({
|
|
4193
|
+
message: "Module type to update:",
|
|
4194
|
+
options: [
|
|
4195
|
+
{ value: "feature", label: "Feature" },
|
|
4196
|
+
{ value: "shared", label: "Shared Module" }
|
|
4197
|
+
]
|
|
4198
|
+
})
|
|
4199
|
+
);
|
|
4200
|
+
}
|
|
4201
|
+
let name = nameArg?.trim();
|
|
4202
|
+
if (!name) {
|
|
4203
|
+
const diskModules = discoverDiskModules(projectRoot).filter((m) => m.type === type);
|
|
4204
|
+
const diffs = getRegistryDiff(projectRoot).filter((d) => d.type === type && d.isRegistered);
|
|
4205
|
+
const opts = diskModules.map((m) => {
|
|
4206
|
+
const d = diffs.find((item) => item.name === m.name);
|
|
4207
|
+
return {
|
|
4208
|
+
value: m.name,
|
|
4209
|
+
label: m.name,
|
|
4210
|
+
hint: d?.hasIntegrityDrift ? "drift detected (needs register)" : "up to date"
|
|
4211
|
+
};
|
|
4212
|
+
});
|
|
4213
|
+
name = handleCancel(
|
|
4214
|
+
await p15.select({
|
|
4215
|
+
message: `Select ${type} to register changes:`,
|
|
4216
|
+
options: opts
|
|
4217
|
+
})
|
|
4218
|
+
);
|
|
4219
|
+
}
|
|
4220
|
+
const folder = type === "shared" ? resolveSharedDir(projectRoot) : resolveFeaturesDir(projectRoot);
|
|
4221
|
+
const modDir = path2.join(folder, name);
|
|
4222
|
+
if (!fs12.existsSync(modDir)) {
|
|
4223
|
+
p15.log.error(pc15.red(`Module directory does not exist: ${path2.relative(projectRoot, modDir)}`));
|
|
4224
|
+
process.exit(1);
|
|
4225
|
+
}
|
|
4226
|
+
const files = getAllFiles(modDir);
|
|
4227
|
+
const integrity = computeDirectoryHash(modDir, files);
|
|
4228
|
+
const manifestPath = path2.join(modDir, "registry.json");
|
|
4229
|
+
let manifest;
|
|
4230
|
+
if (fs12.existsSync(manifestPath)) {
|
|
4231
|
+
manifest = JSON.parse(fs12.readFileSync(manifestPath, "utf-8"));
|
|
4232
|
+
} else {
|
|
4233
|
+
manifest = {
|
|
4234
|
+
name,
|
|
4235
|
+
type,
|
|
4236
|
+
description: `${name} ${type} module`,
|
|
4237
|
+
version: "1.0.0",
|
|
4238
|
+
dependencies: { npm: [], shared: [], features: [] },
|
|
4239
|
+
files: []
|
|
4240
|
+
};
|
|
4241
|
+
}
|
|
4242
|
+
manifest.files = files;
|
|
4243
|
+
manifest.integrity = integrity;
|
|
4244
|
+
if (flags.version) {
|
|
4245
|
+
manifest.version = flags.version;
|
|
4246
|
+
}
|
|
4247
|
+
fs12.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
4248
|
+
const targetList = type === "shared" ? index.shared : index.features;
|
|
4249
|
+
const existingIdx = targetList.findIndex((item) => item.name === name);
|
|
4250
|
+
if (existingIdx >= 0) {
|
|
4251
|
+
targetList[existingIdx] = {
|
|
4252
|
+
name,
|
|
4253
|
+
description: manifest.description,
|
|
4254
|
+
version: manifest.version,
|
|
4255
|
+
integrity
|
|
4256
|
+
};
|
|
4257
|
+
} else {
|
|
4258
|
+
targetList.push({
|
|
4259
|
+
name,
|
|
4260
|
+
description: manifest.description,
|
|
4261
|
+
version: manifest.version,
|
|
4262
|
+
integrity
|
|
4263
|
+
});
|
|
4264
|
+
}
|
|
4265
|
+
saveMasterIndex(projectRoot, index);
|
|
4266
|
+
p15.log.success(
|
|
4267
|
+
pc15.green(`\u2713 Registered changes for '${type}/${name}' (new integrity: ${integrity.slice(0, 18)}...)`)
|
|
4268
|
+
);
|
|
4269
|
+
}
|
|
4270
|
+
async function unregisterModule(typeArg, nameArg, flags = {}) {
|
|
4271
|
+
const projectRoot = findProjectRoot();
|
|
4272
|
+
const index = loadMasterIndex(projectRoot);
|
|
4273
|
+
let type = typeArg?.trim();
|
|
4274
|
+
if (!type) {
|
|
4275
|
+
type = handleCancel(
|
|
4276
|
+
await p15.select({
|
|
4277
|
+
message: "Module type to unregister:",
|
|
4278
|
+
options: [
|
|
4279
|
+
{ value: "feature", label: "Feature" },
|
|
4280
|
+
{ value: "shared", label: "Shared Module" }
|
|
4281
|
+
]
|
|
4282
|
+
})
|
|
4283
|
+
);
|
|
4284
|
+
}
|
|
4285
|
+
const registeredList = type === "shared" ? index.shared : index.features;
|
|
4286
|
+
if (registeredList.length === 0) {
|
|
4287
|
+
p15.log.warn(`No registered ${type} modules found in index.json.`);
|
|
4288
|
+
return;
|
|
4289
|
+
}
|
|
4290
|
+
let name = nameArg?.trim();
|
|
4291
|
+
if (!name) {
|
|
4292
|
+
name = handleCancel(
|
|
4293
|
+
await p15.select({
|
|
4294
|
+
message: `Select ${type} to unregister from index.json:`,
|
|
4295
|
+
options: registeredList.map((item) => ({
|
|
4296
|
+
value: item.name,
|
|
4297
|
+
label: item.name,
|
|
4298
|
+
hint: item.description
|
|
4299
|
+
}))
|
|
4300
|
+
})
|
|
4301
|
+
);
|
|
4302
|
+
}
|
|
4303
|
+
if (type === "shared") {
|
|
4304
|
+
index.shared = index.shared.filter((item) => item.name !== name);
|
|
4305
|
+
} else {
|
|
4306
|
+
index.features = index.features.filter((item) => item.name !== name);
|
|
4307
|
+
}
|
|
4308
|
+
saveMasterIndex(projectRoot, index);
|
|
4309
|
+
p15.log.success(
|
|
4310
|
+
pc15.green(
|
|
4311
|
+
`\u2713 Unregistered module '${type}/${name}' from index.json (source code retained on disk in ${type === "shared" ? "shared" : "features"}/${name})`
|
|
4312
|
+
)
|
|
4313
|
+
);
|
|
4314
|
+
}
|
|
4315
|
+
function diffRegistry(flags = {}) {
|
|
4316
|
+
const projectRoot = findProjectRoot();
|
|
4317
|
+
const diffs = getRegistryDiff(projectRoot);
|
|
4318
|
+
p15.log.step(pc15.bold("\u{1F4CB} Mason Registry Diff & Status Summary"));
|
|
4319
|
+
const unregistered = diffs.filter((d) => d.existsOnDisk && !d.isRegistered);
|
|
4320
|
+
const uncommitted = diffs.filter((d) => d.existsOnDisk && d.hasUncommittedGitChanges);
|
|
4321
|
+
const committedDrift = diffs.filter(
|
|
4322
|
+
(d) => d.existsOnDisk && d.isRegistered && !d.hasUncommittedGitChanges && d.hasIntegrityDrift
|
|
4323
|
+
);
|
|
4324
|
+
const upToDate = diffs.filter(
|
|
4325
|
+
(d) => d.existsOnDisk && d.isRegistered && !d.hasUncommittedGitChanges && !d.hasIntegrityDrift
|
|
4326
|
+
);
|
|
4327
|
+
const missing = diffs.filter((d) => !d.existsOnDisk && d.isRegistered);
|
|
4328
|
+
if (unregistered.length > 0) {
|
|
4329
|
+
p15.log.message(pc15.yellow(`
|
|
4330
|
+
\u{1F4E6} Unregistered Modules (${unregistered.length}):`));
|
|
4331
|
+
for (const mod of unregistered) {
|
|
4332
|
+
p15.log.message(` ${pc15.red("\u25CF")} ${pc15.bold(`${mod.type}/${mod.name}`)} ${pc15.dim("(not registered in index.json)")}`);
|
|
4333
|
+
}
|
|
4334
|
+
}
|
|
4335
|
+
if (uncommitted.length > 0) {
|
|
4336
|
+
p15.log.message(pc15.yellow(`
|
|
4337
|
+
\u{1F4BE} Uncommitted Changes in Git (${uncommitted.length}):`));
|
|
4338
|
+
for (const mod of uncommitted) {
|
|
4339
|
+
p15.log.message(
|
|
4340
|
+
` ${pc15.yellow("\u25B2")} ${pc15.bold(`${mod.type}/${mod.name}`)} ${pc15.dim(`(${mod.uncommittedFilesCount} modified/untracked files)`)}`
|
|
4341
|
+
);
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
4344
|
+
if (committedDrift.length > 0) {
|
|
4345
|
+
p15.log.message(pc15.cyan(`
|
|
4346
|
+
\u{1F680} Committed But Unregistered Changes (${committedDrift.length}):`));
|
|
4347
|
+
for (const mod of committedDrift) {
|
|
4348
|
+
p15.log.message(
|
|
4349
|
+
` ${pc15.cyan("\u25C6")} ${pc15.bold(`${mod.type}/${mod.name}`)} ${pc15.dim(
|
|
4350
|
+
`local: ${mod.localIntegrity?.slice(0, 16)}... != registered: ${mod.registeredIntegrity?.slice(0, 16)}...`
|
|
4351
|
+
)}`
|
|
4352
|
+
);
|
|
4353
|
+
}
|
|
4354
|
+
}
|
|
4355
|
+
if (missing.length > 0) {
|
|
4356
|
+
p15.log.message(pc15.red(`
|
|
4357
|
+
\u26A0\uFE0F Missing on Disk (${missing.length}):`));
|
|
4358
|
+
for (const mod of missing) {
|
|
4359
|
+
p15.log.message(` ${pc15.red("\u2715")} ${pc15.bold(`${mod.type}/${mod.name}`)} ${pc15.dim("(in index.json, but folder missing)")}`);
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
if (upToDate.length > 0) {
|
|
4363
|
+
p15.log.message(pc15.green(`
|
|
4364
|
+
\u2713 Up to Date & Registered (${upToDate.length}):`));
|
|
4365
|
+
for (const mod of upToDate) {
|
|
4366
|
+
p15.log.message(` ${pc15.green("\u2714")} ${mod.type}/${mod.name} ${pc15.dim(`(${mod.registeredIntegrity?.slice(0, 18)}...)`)}`);
|
|
4367
|
+
}
|
|
4368
|
+
}
|
|
4369
|
+
p15.log.message("");
|
|
4370
|
+
}
|
|
4371
|
+
async function handleRegistry(actionArg, typeArg, nameArg, flags = {}) {
|
|
4372
|
+
let action = actionArg?.trim().toLowerCase();
|
|
4373
|
+
if (!action) {
|
|
4374
|
+
action = handleCancel(
|
|
4375
|
+
await p15.select({
|
|
4376
|
+
message: "\u{1F3DB}\uFE0F Mason Registry: What would you like to do?",
|
|
4377
|
+
options: [
|
|
4378
|
+
{
|
|
4379
|
+
value: "diff",
|
|
4380
|
+
label: "Inspect Diff & Status",
|
|
4381
|
+
hint: "Summary of unregistered, uncommitted, and drifted modules"
|
|
4382
|
+
},
|
|
4383
|
+
{
|
|
4384
|
+
value: "register",
|
|
4385
|
+
label: "Register Module",
|
|
4386
|
+
hint: "Add an existing module to index.json with integrity checksum"
|
|
4387
|
+
},
|
|
4388
|
+
{
|
|
4389
|
+
value: "commit",
|
|
4390
|
+
label: "Commit Changes",
|
|
4391
|
+
hint: "Commit a module's working changes to git in the background"
|
|
4392
|
+
},
|
|
4393
|
+
{
|
|
4394
|
+
value: "register-changes",
|
|
4395
|
+
label: "Register Changes",
|
|
4396
|
+
hint: "Update index.json & registry.json with latest checksums"
|
|
4397
|
+
},
|
|
4398
|
+
{
|
|
4399
|
+
value: "unregister",
|
|
4400
|
+
label: "Unregister Module",
|
|
4401
|
+
hint: "Remove module from index.json (source code stays on disk)"
|
|
4402
|
+
}
|
|
4403
|
+
]
|
|
4404
|
+
})
|
|
4405
|
+
);
|
|
4406
|
+
}
|
|
4407
|
+
switch (action) {
|
|
4408
|
+
case "diff":
|
|
4409
|
+
case "status":
|
|
4410
|
+
diffRegistry(flags);
|
|
4411
|
+
break;
|
|
4412
|
+
case "register":
|
|
4413
|
+
await registerModule(typeArg, nameArg, flags);
|
|
4414
|
+
break;
|
|
4415
|
+
case "commit":
|
|
4416
|
+
await commitModule(typeArg, nameArg, flags);
|
|
4417
|
+
break;
|
|
4418
|
+
case "register-changes":
|
|
4419
|
+
case "update":
|
|
4420
|
+
await registerModuleChanges(typeArg, nameArg, flags);
|
|
4421
|
+
break;
|
|
4422
|
+
case "unregister":
|
|
4423
|
+
await unregisterModule(typeArg, nameArg, flags);
|
|
4424
|
+
break;
|
|
4425
|
+
default:
|
|
4426
|
+
p15.log.error(
|
|
4427
|
+
pc15.red(
|
|
4428
|
+
`Unknown registry action: "${action}". Valid actions: diff, register, commit, register-changes, unregister.`
|
|
4429
|
+
)
|
|
4430
|
+
);
|
|
4431
|
+
process.exit(1);
|
|
4432
|
+
}
|
|
4433
|
+
}
|
|
3409
4434
|
|
|
3410
4435
|
// src/cli.ts
|
|
3411
4436
|
function createCli() {
|
|
3412
4437
|
const cli = cac("mason");
|
|
3413
|
-
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) => {
|
|
4438
|
+
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) => {
|
|
3414
4439
|
if (args && args.length > 0) {
|
|
3415
|
-
|
|
3416
|
-
|
|
4440
|
+
p15.log.error(
|
|
4441
|
+
pc15.red(
|
|
3417
4442
|
`Unknown command: "${args.join(" ")}". Use --help to list available commands.`
|
|
3418
4443
|
)
|
|
3419
4444
|
);
|
|
3420
4445
|
process.exit(1);
|
|
3421
4446
|
}
|
|
3422
4447
|
try {
|
|
4448
|
+
const projectRoot = findProjectRoot();
|
|
4449
|
+
const mode = getEffectiveMode(projectRoot, flags);
|
|
4450
|
+
if (mode === "registry") {
|
|
4451
|
+
const choice2 = handleCancel(
|
|
4452
|
+
await p15.select({
|
|
4453
|
+
message: "\u{1F3DB}\uFE0F Mason Registry Maintainer: What would you like to do?",
|
|
4454
|
+
options: [
|
|
4455
|
+
{
|
|
4456
|
+
value: "diff",
|
|
4457
|
+
label: "Inspect Diff & Status",
|
|
4458
|
+
hint: "Overview of unregistered, uncommitted, or drifted modules"
|
|
4459
|
+
},
|
|
4460
|
+
{
|
|
4461
|
+
value: "create_feature",
|
|
4462
|
+
label: "Initialize New Feature",
|
|
4463
|
+
hint: "Scaffold a new feature slice under features/"
|
|
4464
|
+
},
|
|
4465
|
+
{
|
|
4466
|
+
value: "create_shared",
|
|
4467
|
+
label: "Initialize New Shared Module",
|
|
4468
|
+
hint: "Scaffold a new shared utility/port under shared/"
|
|
4469
|
+
},
|
|
4470
|
+
{
|
|
4471
|
+
value: "create_usecase",
|
|
4472
|
+
label: "Create New Use Case",
|
|
4473
|
+
hint: "Add a new use case to an existing feature"
|
|
4474
|
+
},
|
|
4475
|
+
{
|
|
4476
|
+
value: "register",
|
|
4477
|
+
label: "Register Module",
|
|
4478
|
+
hint: "Add a module to index.json with integrity checksum"
|
|
4479
|
+
},
|
|
4480
|
+
{
|
|
4481
|
+
value: "commit",
|
|
4482
|
+
label: "Commit Module Changes",
|
|
4483
|
+
hint: "Commit working changes in a module to git"
|
|
4484
|
+
},
|
|
4485
|
+
{
|
|
4486
|
+
value: "register_changes",
|
|
4487
|
+
label: "Register Changes",
|
|
4488
|
+
hint: "Recompute checksums and update index.json"
|
|
4489
|
+
},
|
|
4490
|
+
{
|
|
4491
|
+
value: "unregister",
|
|
4492
|
+
label: "Unregister Module",
|
|
4493
|
+
hint: "Remove from index.json but keep source on disk"
|
|
4494
|
+
},
|
|
4495
|
+
{
|
|
4496
|
+
value: "import",
|
|
4497
|
+
label: "Import from Remote Registry",
|
|
4498
|
+
hint: "Download a module from upstream"
|
|
4499
|
+
}
|
|
4500
|
+
]
|
|
4501
|
+
})
|
|
4502
|
+
);
|
|
4503
|
+
switch (choice2) {
|
|
4504
|
+
case "diff":
|
|
4505
|
+
await handleRegistry("diff", void 0, void 0, flags);
|
|
4506
|
+
break;
|
|
4507
|
+
case "create_feature":
|
|
4508
|
+
await handleCreate("feature", void 0, flags);
|
|
4509
|
+
break;
|
|
4510
|
+
case "create_shared":
|
|
4511
|
+
await handleCreate("shared", void 0, flags);
|
|
4512
|
+
break;
|
|
4513
|
+
case "create_usecase":
|
|
4514
|
+
await handleCreate("usecase", void 0, flags);
|
|
4515
|
+
break;
|
|
4516
|
+
case "register":
|
|
4517
|
+
await handleRegistry("register", void 0, void 0, flags);
|
|
4518
|
+
break;
|
|
4519
|
+
case "commit":
|
|
4520
|
+
await handleRegistry("commit", void 0, void 0, flags);
|
|
4521
|
+
break;
|
|
4522
|
+
case "register_changes":
|
|
4523
|
+
await handleRegistry("register-changes", void 0, void 0, flags);
|
|
4524
|
+
break;
|
|
4525
|
+
case "unregister":
|
|
4526
|
+
await handleRegistry("unregister", void 0, void 0, flags);
|
|
4527
|
+
break;
|
|
4528
|
+
case "import":
|
|
4529
|
+
await handleImport(void 0, void 0, flags);
|
|
4530
|
+
break;
|
|
4531
|
+
}
|
|
4532
|
+
return;
|
|
4533
|
+
}
|
|
3423
4534
|
const choice = handleCancel(
|
|
3424
|
-
await
|
|
4535
|
+
await p15.select({
|
|
3425
4536
|
message: "\u{1F9F1} Mason: What would you like to do?",
|
|
3426
4537
|
options: [
|
|
3427
4538
|
{
|
|
3428
4539
|
value: "create",
|
|
3429
4540
|
label: "Create local component",
|
|
3430
|
-
hint: "Scaffold a new feature, use case, domain entity,
|
|
4541
|
+
hint: "Scaffold a new feature, shared module, use case, domain entity, etc."
|
|
3431
4542
|
},
|
|
3432
4543
|
{
|
|
3433
4544
|
value: "import",
|
|
3434
4545
|
label: "Import from registry",
|
|
3435
4546
|
hint: "Download and link pre-built shared modules or features from the registry"
|
|
4547
|
+
},
|
|
4548
|
+
{
|
|
4549
|
+
value: "registry",
|
|
4550
|
+
label: "Registry management",
|
|
4551
|
+
hint: "Inspect diff, register modules, or commit module changes"
|
|
3436
4552
|
}
|
|
3437
4553
|
]
|
|
3438
4554
|
})
|
|
3439
4555
|
);
|
|
3440
4556
|
if (choice === "create") {
|
|
3441
4557
|
await handleCreate(void 0, void 0, flags);
|
|
4558
|
+
} else if (choice === "registry") {
|
|
4559
|
+
await handleRegistry(void 0, void 0, void 0, flags);
|
|
3442
4560
|
} else {
|
|
3443
4561
|
await handleImport(void 0, void 0, flags);
|
|
3444
4562
|
}
|
|
@@ -3448,8 +4566,8 @@ function createCli() {
|
|
|
3448
4566
|
});
|
|
3449
4567
|
cli.command(
|
|
3450
4568
|
"create [type] [name]",
|
|
3451
|
-
"Scaffold local Clean Architecture components (feature, usecase, domain, etc.)"
|
|
3452
|
-
).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 = {}) => {
|
|
4569
|
+
"Scaffold local Clean Architecture components (feature, shared, usecase, domain, etc.)"
|
|
4570
|
+
).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 = {}) => {
|
|
3453
4571
|
try {
|
|
3454
4572
|
await handleCreate(type, name, flags);
|
|
3455
4573
|
} catch (err) {
|
|
@@ -3466,8 +4584,28 @@ function createCli() {
|
|
|
3466
4584
|
handleCliError(err);
|
|
3467
4585
|
}
|
|
3468
4586
|
});
|
|
4587
|
+
cli.command(
|
|
4588
|
+
"registry [action] [type] [name]",
|
|
4589
|
+
"Registry & version management (diff, register, commit, register-changes, unregister)"
|
|
4590
|
+
).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(
|
|
4591
|
+
async (action, type, name, flags = {}) => {
|
|
4592
|
+
try {
|
|
4593
|
+
await handleRegistry(action, type, name, flags);
|
|
4594
|
+
} catch (err) {
|
|
4595
|
+
handleCliError(err);
|
|
4596
|
+
}
|
|
4597
|
+
}
|
|
4598
|
+
);
|
|
4599
|
+
cli.command("diff", "Inspect registry status and diff (alias for 'mason registry diff')").allowUnknownOptions().action(async (flags = {}) => {
|
|
4600
|
+
try {
|
|
4601
|
+
await handleRegistry("diff", void 0, void 0, flags);
|
|
4602
|
+
} catch (err) {
|
|
4603
|
+
handleCliError(err);
|
|
4604
|
+
}
|
|
4605
|
+
});
|
|
3469
4606
|
const createSubtypes = [
|
|
3470
4607
|
"feature",
|
|
4608
|
+
"shared",
|
|
3471
4609
|
"usecase",
|
|
3472
4610
|
"domain",
|
|
3473
4611
|
"infrastructure",
|
|
@@ -3488,7 +4626,7 @@ function createCli() {
|
|
|
3488
4626
|
});
|
|
3489
4627
|
}
|
|
3490
4628
|
cli.help();
|
|
3491
|
-
cli.version("1.0.
|
|
4629
|
+
cli.version("1.0.13");
|
|
3492
4630
|
return cli;
|
|
3493
4631
|
}
|
|
3494
4632
|
function handleCliError(err) {
|
|
@@ -3496,7 +4634,7 @@ function handleCliError(err) {
|
|
|
3496
4634
|
process.exit(0);
|
|
3497
4635
|
}
|
|
3498
4636
|
const message = err instanceof Error ? err.message : String(err);
|
|
3499
|
-
|
|
4637
|
+
p15.log.error(pc15.red(`Error: ${message}`));
|
|
3500
4638
|
process.exit(1);
|
|
3501
4639
|
}
|
|
3502
4640
|
async function runCli(argv = process.argv) {
|
|
@@ -3504,6 +4642,6 @@ async function runCli(argv = process.argv) {
|
|
|
3504
4642
|
cli.parse(argv, { run: true });
|
|
3505
4643
|
}
|
|
3506
4644
|
|
|
3507
|
-
export { DEFAULT_REGISTRY_BASE, PromptCancelledError, concreteInfraTemplate, createCli, detectPackageManager, diProviderTemplate, domainClassTemplate, domainInterfaceTemplate, domainValueObjectTemplate, ensureInterfacePrefix, expressIndexTemplate, fetchModuleManifest, fetchRegistryFile, fetchRegistryIndex, findClosestMatch, findInterfaceFile, findLocalRegistry, findProjectRoot, generateConcreteInfraCode, generateDomain, generateDomainEntityCode, generateFeature, generateHttpHandler, generateHttpHandlerCode, generateInfrastructure, generateInjectable, generateInjectableCode, generateInterface, generateInterfaceCode, generateStubCode, generateUseCase, generateUseCaseCode, generateUseCaseTestCode, getMissingNpmPackages, getRegistryBase, getTemplatesDir, handleCancel, handleCreate, handleImport, hbs, httpHandlerTemplate, injectableInterfaceTemplate, injectableTemplate, installNpmPackages, interfaceTemplate, isValidIdentifier, levenshteinDistance, listDomainEntities, listDomainInterfaces, listExistingFeatures, listUseCases, loadInterfaceHierarchy, loadTemplate, normalizeRegistryUrl, parseInterfaceMethodsAndImports, printBanner, printGeneratedFiles, promptProperties, removeInterfacePrefix, renderTemplate, renderTemplateFile, resolveFeaturesDir, runCli, stubInfraTemplate, synthesizeCrudMethods, toCamelCase, toKebabCase, toPascalCase, toPlural, toSingular, toStubName, updateOrGenerateDiProvider, useCaseTemplate, useCaseTestTemplate, writeFileSafely };
|
|
4645
|
+
export { DEFAULT_REGISTRY_BASE, PromptCancelledError, computeDirectoryHash, concreteInfraTemplate, createCli, detectPackageManager, diProviderTemplate, domainClassTemplate, domainInterfaceTemplate, domainValueObjectTemplate, ensureInterfacePrefix, expressIndexTemplate, fetchModuleManifest, fetchRegistryFile, fetchRegistryIndex, findClosestMatch, findInterfaceFile, findLocalRegistry, findProjectRoot, generateConcreteInfraCode, generateDomain, generateDomainEntityCode, generateFeature, generateHttpHandler, generateHttpHandlerCode, generateInfrastructure, generateInjectable, generateInjectableCode, generateInterface, generateInterfaceCode, generateShared, generateStubCode, generateUseCase, generateUseCaseCode, generateUseCaseTestCode, getAllFiles, getConfiguredFeaturesDir, getConfiguredSharedDir, getEffectiveMode, getMissingNpmPackages, getRegistryBase, getTemplatesDir, handleCancel, handleCreate, handleImport, handleRegistry, hbs, httpHandlerTemplate, injectableInterfaceTemplate, injectableTemplate, installNpmPackages, interfaceTemplate, isValidIdentifier, levenshteinDistance, listDomainEntities, listDomainInterfaces, listExistingFeatures, listExistingShared, listUseCases, loadInterfaceHierarchy, loadMasonConfig, loadTemplate, normalizeRegistryUrl, parseInterfaceMethodsAndImports, printBanner, printGeneratedFiles, promptProperties, removeInterfacePrefix, renderTemplate, renderTemplateFile, resolveFeaturesDir, resolveSharedDir, runCli, runSharedCommand, stubInfraTemplate, synthesizeCrudMethods, toCamelCase, toKebabCase, toPascalCase, toPlural, toSingular, toStubName, updateOrGenerateDiProvider, useCaseTemplate, useCaseTestTemplate, writeFileSafely };
|
|
3508
4646
|
//# sourceMappingURL=index.js.map
|
|
3509
4647
|
//# sourceMappingURL=index.js.map
|