@ronaldjdevfs/forge 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +130 -8
  2. package/package.json +2 -2
  3. package/skills/forge/SKILL.md +86 -15
  4. package/skills/forge/profiles/express-drizzle.md +107 -0
  5. package/skills/forge/profiles/fastify-mongodb.md +103 -0
  6. package/skills/forge/profiles/fastify-prisma.md +81 -0
  7. package/skills/forge/profiles/nestjs-mongodb.md +92 -0
  8. package/skills/forge/profiles/nestjs-postgres.md +98 -0
  9. package/skills/forge/reference/api-design.md +62 -0
  10. package/skills/forge/reference/assay.md +82 -0
  11. package/skills/forge/reference/cast.md +81 -7
  12. package/skills/forge/reference/data-patterns.md +86 -0
  13. package/skills/forge/reference/di-strategies.md +50 -0
  14. package/skills/forge/reference/errors.md +65 -0
  15. package/skills/forge/reference/events.md +95 -0
  16. package/skills/forge/reference/help.md +40 -0
  17. package/skills/forge/reference/hooks.md +62 -0
  18. package/skills/forge/reference/observability.md +66 -0
  19. package/skills/forge/reference/patterns.md +52 -0
  20. package/skills/forge/reference/principles.md +6 -0
  21. package/skills/forge/reference/reforge.md +69 -5
  22. package/skills/forge/reference/relocate.md +15 -2
  23. package/skills/forge/reference/security-patterns.md +87 -0
  24. package/skills/forge/reference/testing-patterns.md +69 -0
  25. package/skills/forge/scripts/assay.mjs +481 -0
  26. package/skills/forge/scripts/context.mjs +147 -43
  27. package/skills/forge/scripts/detect.mjs +371 -22
  28. package/skills/forge/scripts/forge-api.mjs +373 -0
  29. package/skills/forge/scripts/forge-config.mjs +268 -0
  30. package/skills/forge/scripts/forge-signals.mjs +131 -0
  31. package/skills/forge/scripts/forge-state.mjs +97 -0
  32. package/skills/forge/scripts/formatter.mjs +133 -0
  33. package/skills/forge/scripts/graph.mjs +5 -21
  34. package/skills/forge/scripts/hook.mjs +250 -0
  35. package/skills/forge/scripts/inspect.mjs +171 -22
  36. package/skills/forge/scripts/parse-imports.mjs +249 -0
  37. package/skills/forge/scripts/pin.mjs +151 -0
  38. package/skills/forge/scripts/posttool.mjs +224 -0
  39. package/skills/forge/scripts/profile.mjs +124 -20
  40. package/skills/forge/scripts/registry/rules.mjs +344 -0
  41. package/skills/forge/scripts/rename.mjs +669 -0
  42. package/skills/forge/scripts/rollback.mjs +213 -0
  43. package/skills/forge/scripts/update.mjs +114 -0
  44. package/skills/forge/templates/feature/domain-error.ts.md +9 -0
  45. package/skills/forge/templates/feature/domain-event.ts.md +9 -0
  46. package/skills/forge/templates/feature/event-handler.ts.md +10 -0
  47. package/skills/forge/templates/feature/use-case.ts.md +10 -2
  48. package/skills/forge/tests/core.test.mjs +403 -0
@@ -0,0 +1,669 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "fs";
4
+ import { join, relative, dirname, basename, extname, parse } from "path";
5
+
6
+ const ROOT = process.cwd();
7
+ const SRC = join(ROOT, "src");
8
+
9
+ function isDir(path) { return existsSync(path) && statSync(path).isDirectory(); }
10
+
11
+ function listDir(path) { try { return readdirSync(path); } catch { return []; } }
12
+
13
+ function read(path) { try { return readFileSync(path, "utf-8"); } catch { return null; } }
14
+
15
+ function findFiles(dir, maxDepth = 8) {
16
+ const results = [];
17
+ function walk(d, depth) {
18
+ if (depth > maxDepth) return;
19
+ try {
20
+ for (const entry of readdirSync(d)) {
21
+ const full = join(d, entry);
22
+ if (statSync(full).isDirectory()) {
23
+ if (!entry.startsWith(".") && entry !== "node_modules") walk(full, depth + 1);
24
+ } else if (/\.(ts|js)$/.test(entry) && !entry.endsWith(".d.ts")) {
25
+ results.push(full);
26
+ }
27
+ }
28
+ } catch { /* skip */ }
29
+ }
30
+ if (existsSync(dir)) walk(dir, 0);
31
+ return results;
32
+ }
33
+
34
+ function toPascalCase(str) {
35
+ return str
36
+ .replace(/[-_]/g, " ")
37
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
38
+ .split(/\s+/)
39
+ .map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
40
+ .join("");
41
+ }
42
+
43
+ function toCamelCase(str) {
44
+ const pascal = toPascalCase(str);
45
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
46
+ }
47
+
48
+ function kebabToPascal(str) {
49
+ return str.split("-").map(w => w.charAt(0).toUpperCase() + w.slice(1)).join("");
50
+ }
51
+
52
+ // ── Naming rules per directory (from reference/patterns.md) ──
53
+
54
+ const PLATFORM_RULES = {
55
+ config: { defaultSuffix: "config", case: "pascal", description: "<Name>.config.ts" },
56
+ server: { defaultSuffix: null, case: "pascal", description: "PascalCase.ts" },
57
+ database: { defaultSuffix: "config", case: "pascal", description: "<Name>.config.ts" },
58
+ http: { defaultSuffix: null, case: "pascal", description: "PascalCase.ts (Router, *middleware)" },
59
+ logger: { defaultSuffix: "config", allowedSuffixes: ["config", "service"], case: "pascal", description: "<Name>.config.ts / <Name>.service.ts" },
60
+ cache: { defaultSuffix: "config", allowedSuffixes: ["config", "service"], case: "pascal", description: "<Name>.config.ts / <Name>.service.ts" },
61
+ security: { defaultSuffix: "middleware", allowedSuffixes: ["middleware", "service"], case: "pascal", description: "<Name>.middleware.ts / <Name>.service.ts" },
62
+ events: { defaultSuffix: null, case: "pascal", description: "PascalCase.ts (EventBus, EventHandler)" },
63
+ scheduler:{ defaultSuffix: "config", allowedSuffixes: ["config", "service"], case: "pascal", description: "<Name>.config.ts / <Name>.service.ts" },
64
+ observability: { defaultSuffix: null, case: "pascal", description: "PascalCase.ts (Metrics, Tracing, Health)" },
65
+ di: { defaultSuffix: null, case: "pascal", description: "PascalCase.ts (Container, Tokens, Module)" },
66
+ };
67
+
68
+ const FEATURE_SUBDIR_RULES = [
69
+ { subdir: "domain", pattern: "entity", case: "pascal", description: "<Name>.entity.ts" },
70
+ { subdir: "domain", pattern: "repository", prefix: "I", case: "pascal", description: "I<Name>.repository.ts" },
71
+ { subdir: "application/use-cases", pattern: "uc", case: "pascal", description: "<Action>.uc.ts" },
72
+ { subdir: "application/mappers", pattern: "mapper", case: "pascal", description: "<Name>.mapper.ts" },
73
+ { subdir: "adapters/in/http", pattern: "controller", case: "pascal", description: "<Name>.controller.ts" },
74
+ { subdir: "adapters/in/http", pattern: "routes", case: "pascal", description: "<Name>.routes.ts" },
75
+ { subdir: "adapters/out/persistence", pattern: "repository", case: "pascal", description: "<Name>.repository.ts" },
76
+ { subdir: "adapters/out/persistence", pattern: "schema", case: "pascal", description: "<Name>.schema.ts" },
77
+ ];
78
+
79
+ const SHARED_RULES = {
80
+ errors: { suffix: "Error", case: "pascal", description: "<Name>Error.ts" },
81
+ contracts: { prefix: "I", case: "pascal", description: "I<Name>.ts" },
82
+ types: { suffix: "types", case: "camel", description: "<name>.types.ts" },
83
+ utils: { case: "camel", description: "<name>.ts (camelCase)" },
84
+ };
85
+
86
+ const INFRA_RULES = {
87
+ prisma: { prefix: "Prisma", case: "pascal", description: "Prisma.<Name>.ts" },
88
+ mongodb: { defaultSuffix: "config", allowedSuffixes: ["config", "model"], case: "pascal", description: "<Name>.config.ts / <Name>.model.ts" },
89
+ redis: { defaultSuffix: "config", allowedSuffixes: ["config", "service"], case: "pascal", description: "<Name>.config.ts / <Name>.service.ts" },
90
+ mail: { defaultSuffix: "config", allowedSuffixes: ["config", "service"], case: "pascal", description: "<Name>.config.ts / <Name>.service.ts" },
91
+ s3: { defaultSuffix: "config", allowedSuffixes: ["config", "service"], case: "pascal", description: "<Name>.config.ts / <Name>.service.ts" },
92
+ };
93
+
94
+ function isValidExtension(file) {
95
+ return /\.(ts|js)$/.test(file) && !file.endsWith(".d.ts");
96
+ }
97
+
98
+ function getRelativePath(filePath) {
99
+ return relative(ROOT, filePath);
100
+ }
101
+
102
+ function getStem(filename) {
103
+ return basename(filename).replace(/\.(ts|js)$/, "");
104
+ }
105
+
106
+ function hasSuffix(stem, suffix) {
107
+ if (!suffix) return true;
108
+ const parts = stem.split(".");
109
+ return parts.length > 1 && parts.slice(1).join(".") === suffix;
110
+ }
111
+
112
+ function ensureSuffix(stem, suffix) {
113
+ if (!suffix) return stem;
114
+ if (hasSuffix(stem, suffix)) return stem;
115
+ return `${stem}.${suffix}`;
116
+ }
117
+
118
+ function ensurePrefix(stem, prefix) {
119
+ if (!prefix) return stem;
120
+ if (stem.startsWith(prefix)) return stem;
121
+ return prefix + stem;
122
+ }
123
+
124
+ // ── Core: compute expected name for a file ──
125
+
126
+ export function computeExpectedName(filePath) {
127
+ const relPath = getRelativePath(filePath);
128
+ if (!relPath.startsWith("src/")) return null;
129
+
130
+ const parts = relPath.split("/");
131
+ const filename = parts[parts.length - 1];
132
+ if (!isValidExtension(filename)) return null;
133
+
134
+ const stem = getStem(filename);
135
+ const ext = extname(filename); // .ts or .js
136
+
137
+ // Platform layer
138
+ if (parts[1] === "platform" && parts[2]) {
139
+ const subdir = parts[2];
140
+ const rule = PLATFORM_RULES[subdir];
141
+ if (!rule) return null;
142
+
143
+ const suffixRule = rule.allowedSuffixes || (rule.defaultSuffix ? [rule.defaultSuffix] : [null]);
144
+ const base = toPascalCase(stem);
145
+ const currentHasAllowedSuffix = suffixRule.some(s => s && hasSuffix(stem, s));
146
+
147
+ let expected;
148
+ if (currentHasAllowedSuffix) {
149
+ const matchedSuffix = suffixRule.find(s => s && hasSuffix(stem, s));
150
+ const rawStem = stem.replace(`.${matchedSuffix}`, "");
151
+ const fixedStem = rule.case === "pascal" ? toPascalCase(rawStem) : toCamelCase(rawStem);
152
+ expected = `${fixedStem}.${matchedSuffix}${ext}`;
153
+ } else if (rule.defaultSuffix) {
154
+ const fixedStem = rule.case === "pascal" ? toPascalCase(stem) : toCamelCase(stem);
155
+ expected = `${fixedStem}.${rule.defaultSuffix}${ext}`;
156
+ } else {
157
+ expected = `${base}${ext}`;
158
+ }
159
+
160
+ if (expected !== filename) {
161
+ return {
162
+ current: join(dirname(filePath), filename),
163
+ expected: join(dirname(filePath), expected),
164
+ relPath: join(dirname(relPath), expected),
165
+ rule: `platform/${subdir}: ${rule.description}`,
166
+ };
167
+ }
168
+ return null;
169
+ }
170
+
171
+ // Feature layer
172
+ if (parts[1] === "features" && parts[2]) {
173
+ const featureName = parts[2];
174
+ const entityName = kebabToPascal(featureName);
175
+ const featureSubpath = parts.slice(3, -1).join("/");
176
+
177
+ for (const rule of FEATURE_SUBDIR_RULES) {
178
+ if (featureSubpath === rule.subdir || featureSubpath.startsWith(rule.subdir + "/")) {
179
+ const stemLower = stem.toLowerCase();
180
+
181
+ // For domain/entity: <Entity>.entity.ts
182
+ // Entity name derived from feature dir or current filename
183
+ if (rule.subdir === "domain" && rule.pattern === "entity") {
184
+ let inferredEntity;
185
+ if (hasSuffix(stem, "entity")) {
186
+ inferredEntity = stem.replace(/\.entity$/i, "");
187
+ } else {
188
+ const noEntity = stem.replace(/[Ee]ntity$/, "");
189
+ inferredEntity = noEntity || featureName;
190
+ }
191
+ const entityStem = toPascalCase(inferredEntity);
192
+ const expectedName = `${entityStem}.entity${ext}`;
193
+ if (expectedName !== filename) {
194
+ return { current: filePath, expected: join(dirname(filePath), expectedName), relPath: join(dirname(relPath), expectedName), rule: `feature/${featureName}/domain: ${rule.description}` };
195
+ }
196
+ }
197
+
198
+ // For domain/repository interface: I<Entity>.repository.ts
199
+ if (rule.subdir === "domain" && rule.pattern === "repository") {
200
+ const expectedStem = `I${entityName}.repository`;
201
+ const expectedName = `${expectedStem}${ext}`;
202
+ const currentMatches = stem === expectedStem;
203
+ if (!currentMatches) {
204
+ return { current: filePath, expected: join(dirname(filePath), expectedName), relPath: join(dirname(relPath), expectedName), rule: `feature/${featureName}/domain: ${rule.description}` };
205
+ }
206
+ return null;
207
+ }
208
+
209
+ // For use-cases: <Action>.uc.ts
210
+ if (rule.subdir === "application/use-cases") {
211
+ const expectedName = `${toPascalCase(stem)}.uc${ext}`;
212
+ if (expectedName !== filename) {
213
+ return { current: filePath, expected: join(dirname(filePath), expectedName), relPath: join(dirname(relPath), expectedName), rule: `feature/${featureName}/application/use-cases: ${rule.description}` };
214
+ }
215
+ return null;
216
+ }
217
+
218
+ // For mappers, controllers, routes, repositories (persistence), schemas: <Entity>.<pattern>.ts
219
+ if (rule.subdir !== "domain") {
220
+ const hasCorrectSuffix = hasSuffix(stem, rule.pattern);
221
+ if (hasCorrectSuffix) {
222
+ const rawStem = stem.replace(`.${rule.pattern}`, "");
223
+ const fixedStem = rule.prefix === "I" ? `I${toPascalCase(rawStem.replace(/^I/, ""))}` : toPascalCase(rawStem);
224
+ const expectedName = `${fixedStem}.${rule.pattern}${ext}`;
225
+ if (expectedName !== filename) {
226
+ return { current: filePath, expected: join(dirname(filePath), expectedName), relPath: join(dirname(relPath), expectedName), rule: `feature/${featureName}${rule.subdir ? "/" + rule.subdir : ""}: ${rule.description}` };
227
+ }
228
+ } else {
229
+ const expectedName = rule.prefix === "I"
230
+ ? `I${entityName}.${rule.pattern}${ext}`
231
+ : `${entityName}.${rule.pattern}${ext}`;
232
+ return { current: filePath, expected: join(dirname(filePath), expectedName), relPath: join(dirname(relPath), expectedName), rule: `feature/${featureName}${rule.subdir ? "/" + rule.subdir : ""}: ${rule.description}` };
233
+ }
234
+ }
235
+
236
+ return null;
237
+ }
238
+ }
239
+
240
+ // Check domain/ for files that look like entity/repository but don't match (loose files)
241
+ if (featureSubpath === "domain" && !stem.includes(".")) {
242
+ const lower = stem.toLowerCase();
243
+ if (lower.includes("repository") || stem.startsWith("I")) {
244
+ const expectedName = `I${entityName}.repository${ext}`;
245
+ return { current: filePath, expected: join(dirname(filePath), expectedName), relPath: join(dirname(relPath), expectedName), rule: `feature/${featureName}/domain: I<Name>.repository.ts` };
246
+ }
247
+ const expectedName = `${entityName}.entity${ext}`;
248
+ return { current: filePath, expected: join(dirname(filePath), expectedName), relPath: join(dirname(relPath), expectedName), rule: `feature/${featureName}/domain: <Name>.entity.ts` };
249
+ }
250
+
251
+ return null;
252
+ }
253
+
254
+ // Shared layer
255
+ if (parts[1] === "shared" && parts[2]) {
256
+ const subdir = parts[2];
257
+ const rule = SHARED_RULES[subdir];
258
+ if (!rule) return null;
259
+
260
+ if (subdir === "types") {
261
+ if (hasSuffix(stem, "types")) {
262
+ const rawStem = stem.replace(".types", "");
263
+ const fixedStem = toCamelCase(rawStem);
264
+ const expectedName = `${fixedStem}.types${ext}`;
265
+ if (expectedName !== filename) return { current: filePath, expected: join(dirname(filePath), expectedName), relPath: join(dirname(relPath), expectedName), rule: `shared/types: ${rule.description}` };
266
+ } else {
267
+ const fixedStem = toCamelCase(stem);
268
+ const expectedName = `${fixedStem}.types${ext}`;
269
+ return { current: filePath, expected: join(dirname(filePath), expectedName), relPath: join(dirname(relPath), expectedName), rule: `shared/types: ${rule.description}` };
270
+ }
271
+ }
272
+
273
+ if (subdir === "errors") {
274
+ if (hasSuffix(stem, "Error")) {
275
+ const rawStem = stem.replace(".Error", "");
276
+ const fixedStem = toPascalCase(rawStem);
277
+ const expectedName = `${fixedStem}.Error${ext}`;
278
+ if (expectedName !== filename) return { current: filePath, expected: join(dirname(filePath), expectedName), relPath: join(dirname(relPath), expectedName), rule: `shared/errors: ${rule.description}` };
279
+ } else if (stem.endsWith("error") || stem.endsWith("Error")) {
280
+ const rawStem = stem.replace(/error$/i, "");
281
+ const fixedStem = toPascalCase(rawStem);
282
+ const expectedName = `${fixedStem}Error${ext}`;
283
+ return { current: filePath, expected: join(dirname(filePath), expectedName), relPath: join(dirname(relPath), expectedName), rule: `shared/errors: ${rule.description}` };
284
+ } else {
285
+ const fixedStem = toPascalCase(stem);
286
+ const expectedName = `${fixedStem}Error${ext}`;
287
+ return { current: filePath, expected: join(dirname(filePath), expectedName), relPath: join(dirname(relPath), expectedName), rule: `shared/errors: ${rule.description}` };
288
+ }
289
+ }
290
+
291
+ if (subdir === "contracts") {
292
+ const hasI = stem.startsWith("I");
293
+ if (hasI) {
294
+ const fixedStem = toPascalCase(stem);
295
+ if (fixedStem !== stem) {
296
+ const expectedName = `${fixedStem}${ext}`;
297
+ return { current: filePath, expected: join(dirname(filePath), expectedName), relPath: join(dirname(relPath), expectedName), rule: `shared/contracts: ${rule.description}` };
298
+ }
299
+ } else {
300
+ const expectedName = `I${toPascalCase(stem)}${ext}`;
301
+ return { current: filePath, expected: join(dirname(filePath), expectedName), relPath: join(dirname(relPath), expectedName), rule: `shared/contracts: ${rule.description}` };
302
+ }
303
+ }
304
+
305
+ if (subdir === "utils") {
306
+ const fixedStem = toCamelCase(stem);
307
+ if (fixedStem !== stem) {
308
+ const expectedName = `${fixedStem}${ext}`;
309
+ return { current: filePath, expected: join(dirname(filePath), expectedName), relPath: join(dirname(relPath), expectedName), rule: `shared/utils: ${rule.description}` };
310
+ }
311
+ return null;
312
+ }
313
+
314
+ return null;
315
+ }
316
+
317
+ // Infra layer
318
+ if ((parts[1] === "infra" || parts[1] === "infrastructure") && parts[2]) {
319
+ const subdir = parts[2];
320
+ const rule = INFRA_RULES[subdir];
321
+ if (!rule) return null;
322
+
323
+ if (subdir === "prisma") {
324
+ const hasPrisma = stem.startsWith("Prisma");
325
+ if (hasPrisma) {
326
+ const rawStem = stem.replace(/^Prisma/, "");
327
+ if (rawStem.startsWith(".")) {
328
+ const actualStem = rawStem.slice(1);
329
+ const fixedStem = toPascalCase(actualStem);
330
+ const expectedName = `Prisma.${fixedStem}${ext}`;
331
+ if (expectedName !== filename) return { current: filePath, expected: join(dirname(filePath), expectedName), relPath: join(dirname(relPath), expectedName), rule: `infra/prisma: ${rule.description}` };
332
+ } else {
333
+ const fixedStem = toPascalCase(rawStem);
334
+ const expectedName = `Prisma${fixedStem}${ext}`;
335
+ if (expectedName !== filename) return { current: filePath, expected: join(dirname(filePath), expectedName), relPath: join(dirname(relPath), expectedName), rule: `infra/prisma: ${rule.description}` };
336
+ }
337
+ } else if (stem.includes(".")) {
338
+ const parts2 = stem.split(".");
339
+ const fixedStem = toPascalCase(parts2[0]);
340
+ const suffix = parts2.slice(1).join(".");
341
+ const expectedName = `Prisma.${fixedStem}.${suffix}${ext}`;
342
+ return { current: filePath, expected: join(dirname(filePath), expectedName), relPath: join(dirname(relPath), expectedName), rule: `infra/prisma: ${rule.description}` };
343
+ } else {
344
+ const expectedName = `Prisma.${toPascalCase(stem)}${ext}`;
345
+ return { current: filePath, expected: join(dirname(filePath), expectedName), relPath: join(dirname(relPath), expectedName), rule: `infra/prisma: ${rule.description}` };
346
+ }
347
+ }
348
+
349
+ if (rule.defaultSuffix || rule.allowedSuffixes) {
350
+ const suffixList = rule.allowedSuffixes || [rule.defaultSuffix];
351
+ const hasAllowedSuffix = suffixList.some(s => s && hasSuffix(stem, s));
352
+ if (hasAllowedSuffix) {
353
+ const matched = suffixList.find(s => s && hasSuffix(stem, s));
354
+ const rawStem = stem.replace(`.${matched}`, "");
355
+ const fixedStem = toPascalCase(rawStem);
356
+ const expectedName = `${fixedStem}.${matched}${ext}`;
357
+ if (expectedName !== filename) return { current: filePath, expected: join(dirname(filePath), expectedName), relPath: join(dirname(relPath), expectedName), rule: `infra/${subdir}: ${rule.description}` };
358
+ } else {
359
+ const fixedStem = toPascalCase(stem);
360
+ const expectedName = `${fixedStem}.${rule.defaultSuffix}${ext}`;
361
+ return { current: filePath, expected: join(dirname(filePath), expectedName), relPath: join(dirname(relPath), expectedName), rule: `infra/${subdir}: ${rule.description}` };
362
+ }
363
+ }
364
+
365
+ return null;
366
+ }
367
+
368
+ return null;
369
+ }
370
+
371
+ // ── Detect all naming violations in the project ──
372
+
373
+ export function detectNamingViolations(projectRoot = ROOT) {
374
+ const src = join(projectRoot, "src");
375
+ if (!isDir(src)) return [];
376
+
377
+ const violations = [];
378
+
379
+ // Scan features/<name>/domain/ loose files (entity/repository without subdir in name)
380
+ const featuresDir = join(src, "features");
381
+ if (isDir(featuresDir)) {
382
+ for (const feat of listDir(featuresDir)) {
383
+ if (!isDir(join(featuresDir, feat))) continue;
384
+ const domainDir = join(featuresDir, feat, "domain");
385
+ if (!isDir(domainDir)) continue;
386
+ // Check for bare .ts files in domain/ that should use .entity or .repository suffix
387
+ for (const f of listDir(domainDir)) {
388
+ if (!isValidExtension(f)) continue;
389
+ const fullPath = join(domainDir, f);
390
+ const result = computeExpectedName(fullPath);
391
+ if (result) violations.push(result);
392
+ }
393
+ }
394
+ }
395
+
396
+ // Scan all .ts/.js files in src/ (except node_modules, .d.ts)
397
+ const allFiles = findFiles(src, 8);
398
+ for (const file of allFiles) {
399
+ const relPath = getRelativePath(file);
400
+ if (relPath === file) continue; // already normalized
401
+
402
+ // Skip barrel files
403
+ const base = basename(file);
404
+ if (base === "index.ts" || base === "index.js") continue;
405
+
406
+ // Skip test files
407
+ if (base.endsWith(".test.ts") || base.endsWith(".spec.ts") || base.endsWith(".test.js") || base.endsWith(".spec.js")) continue;
408
+
409
+ // Skip files already checked via the feature domain loop
410
+ if (relPath.includes("/features/") && relPath.includes("/domain/")) continue;
411
+
412
+ const result = computeExpectedName(file);
413
+ if (result) violations.push(result);
414
+ }
415
+
416
+ return violations;
417
+ }
418
+
419
+ // ── Update imports across the project after rename ──
420
+
421
+ export function updateImportsAfterRename(oldPath, newPath, projectRoot = ROOT) {
422
+ const oldRel = relative(projectRoot, oldPath);
423
+ const newRel = relative(projectRoot, newPath);
424
+ const oldBase = basename(oldRel).replace(/\.(ts|js)$/, "");
425
+ const newBase = basename(newRel).replace(/\.(ts|js)$/, "");
426
+
427
+ // If the filename didn't change except extension, skip
428
+ if (oldBase === newBase) return [];
429
+
430
+ const oldDir = dirname(oldRel);
431
+ const newDir = dirname(newRel);
432
+ const sameDir = oldDir === newDir;
433
+
434
+ const allFiles = findFiles(join(projectRoot, "src"), 8);
435
+ const changes = [];
436
+
437
+ for (const file of allFiles) {
438
+ const content = read(file);
439
+ if (!content) continue;
440
+
441
+ const fileRel = relative(projectRoot, file);
442
+ const fileDir = dirname(fileRel);
443
+ const newContentLines = [];
444
+ let modified = false;
445
+
446
+ const lines = content.split("\n");
447
+ for (const line of lines) {
448
+ // Match import/export/require statements
449
+ const importMatch = line.match(/(?:from\s+|require\s*\()['"]([^'"]+)['"]/);
450
+ if (importMatch) {
451
+ const importPath = importMatch[1];
452
+ const newImportPath = resolveImportPath(importPath, fileDir, oldDir, oldBase, newDir, newBase, sameDir);
453
+ if (newImportPath !== importPath) {
454
+ newContentLines.push(line.replace(importPath, newImportPath));
455
+ modified = true;
456
+ continue;
457
+ }
458
+ }
459
+ newContentLines.push(line);
460
+ }
461
+
462
+ if (modified) {
463
+ writeFileSync(file, newContentLines.join("\n"), "utf-8");
464
+ changes.push({ file: fileRel, changes: `imports actualizados: ${oldBase} → ${newBase}` });
465
+ }
466
+ }
467
+
468
+ return changes;
469
+ }
470
+
471
+ function resolveImportPath(importPath, fileDir, oldDir, oldBase, newDir, newBase, sameDir) {
472
+ const isRelative = importPath.startsWith("./") || importPath.startsWith("../");
473
+
474
+ if (sameDir) {
475
+ // If same directory, just update the filename reference
476
+ if (isRelative) {
477
+ const importBase = basename(importPath).replace(/\.(ts|js)$/, "");
478
+ if (importBase === oldBase) {
479
+ return importPath.replace(basename(importPath), newBase);
480
+ }
481
+ }
482
+ return importPath;
483
+ }
484
+
485
+ if (!isRelative) return importPath;
486
+
487
+ // Resolve relative path to absolute-like, check, convert back
488
+ const resolved = resolveRelative(importPath, fileDir);
489
+ const oldAbs = resolveRelative(`./${oldBase}`, oldDir);
490
+
491
+ if (resolved === oldAbs || resolved.replace(/\.(ts|js)$/, "") === oldAbs.replace(/\.(ts|js)$/, "")) {
492
+ // Compute new relative path from fileDir to newFile
493
+ const newRelPath = computeRelativePath(fileDir, join(newDir, newBase));
494
+ return newRelPath;
495
+ }
496
+
497
+ return importPath;
498
+ }
499
+
500
+ function resolveRelative(relPath, fromDir) {
501
+ const parts = fromDir.split("/");
502
+ const importParts = relPath.split("/");
503
+ for (const p of importParts) {
504
+ if (p === ".") continue;
505
+ if (p === "..") parts.pop();
506
+ else parts.push(p);
507
+ }
508
+ return parts.join("/");
509
+ }
510
+
511
+ function computeRelativePath(fromDir, toPath) {
512
+ const fromParts = fromDir.split("/");
513
+ const toParts = toPath.split("/");
514
+
515
+ let i = 0;
516
+ while (i < fromParts.length && i < toParts.length && fromParts[i] === toParts[i]) i++;
517
+
518
+ const up = fromParts.length - i;
519
+ const down = toParts.slice(i);
520
+
521
+ const rel = [];
522
+ for (let j = 0; j < up; j++) rel.push("..");
523
+ rel.push(...down);
524
+
525
+ return rel.join("/");
526
+ }
527
+
528
+ // ── Execute rename ──
529
+
530
+ export function renameFile(oldPath, newPath, projectRoot = ROOT) {
531
+ if (!existsSync(oldPath)) {
532
+ return { success: false, error: `Archivo no encontrado: ${oldPath}` };
533
+ }
534
+
535
+ if (oldPath === newPath) {
536
+ return { success: true, updatedImports: [], note: "El archivo ya tiene el nombre correcto" };
537
+ }
538
+
539
+ // Physical rename
540
+ try {
541
+ renameSync(oldPath, newPath);
542
+ } catch (err) {
543
+ return { success: false, error: `Error al renombrar: ${err.message}` };
544
+ }
545
+
546
+ // Update imports
547
+ const updatedImports = updateImportsAfterRename(oldPath, newPath, projectRoot);
548
+
549
+ return {
550
+ success: true,
551
+ from: oldPath,
552
+ to: newPath,
553
+ updatedImports,
554
+ };
555
+ }
556
+
557
+ // ── CLI ──
558
+
559
+ async function main() {
560
+ const args = process.argv.slice(2);
561
+ const format = args.includes("--json") ? "json" : "text";
562
+ const isDryRun = args.includes("--dry-run");
563
+ const detectOnly = args.includes("--detect");
564
+ const allMode = args.includes("--all");
565
+ const fileIndex = args.indexOf("--file");
566
+ const singleFile = fileIndex !== -1 ? args[fileIndex + 1] : null;
567
+
568
+ if (detectOnly || (!singleFile && !allMode)) {
569
+ const violations = detectNamingViolations(ROOT);
570
+ if (format === "json") {
571
+ console.log(JSON.stringify({ violations, count: violations.length }, null, 2));
572
+ } else {
573
+ if (violations.length === 0) {
574
+ console.log("✓ No se encontraron violaciones de naming");
575
+ return;
576
+ }
577
+ console.log(`\n Naming violations (${violations.length}):\n`);
578
+ for (const v of violations) {
579
+ console.log(` [WARNING] ${relative(ROOT, v.current)}`);
580
+ console.log(` → ${relative(ROOT, v.expected)}`);
581
+ console.log(` Regla: ${v.rule}`);
582
+ console.log();
583
+ }
584
+ }
585
+ return;
586
+ }
587
+
588
+ if (singleFile) {
589
+ const fullPath = join(ROOT, singleFile);
590
+ if (!existsSync(fullPath)) {
591
+ console.error(`Archivo no encontrado: ${singleFile}`);
592
+ process.exit(1);
593
+ }
594
+
595
+ if (!isValidExtension(fullPath)) {
596
+ console.error(`Extensión no soportada: ${fullPath}`);
597
+ process.exit(1);
598
+ }
599
+
600
+ const expected = computeExpectedName(fullPath);
601
+ if (!expected) {
602
+ console.log(`✓ ${singleFile} ya cumple con las convenciones de naming`);
603
+ return;
604
+ }
605
+
606
+ console.log(`\n Renombrar:\n ${relative(ROOT, expected.current)}\n → ${relative(ROOT, expected.expected)}\n Regla: ${expected.rule}\n`);
607
+
608
+ if (isDryRun) {
609
+ console.log(" (dry-run — no se realizaron cambios)\n");
610
+ return;
611
+ }
612
+
613
+ const result = renameFile(expected.current, expected.expected, ROOT);
614
+ if (!result.success) {
615
+ console.error(` Error: ${result.error}`);
616
+ process.exit(1);
617
+ }
618
+
619
+ console.log(` ✓ Archivo renombrado: ${basename(result.from)} → ${basename(result.to)}`);
620
+ if (result.updatedImports.length > 0) {
621
+ console.log(` ✓ Imports actualizados en ${result.updatedImports.length} archivo(s):`);
622
+ for (const c of result.updatedImports) {
623
+ console.log(` ${c.file}`);
624
+ }
625
+ } else {
626
+ console.log(` ✓ Sin imports que actualizar`);
627
+ }
628
+ console.log();
629
+ return;
630
+ }
631
+
632
+ if (allMode) {
633
+ const violations = detectNamingViolations(ROOT);
634
+ if (violations.length === 0) {
635
+ console.log("✓ No se encontraron violaciones de naming");
636
+ return;
637
+ }
638
+
639
+ if (isDryRun) {
640
+ console.log(`\n Se renombrarían ${violations.length} archivo(s) (dry-run):\n`);
641
+ for (const v of violations) {
642
+ console.log(` ${relative(ROOT, v.current)}`);
643
+ console.log(` → ${relative(ROOT, v.expected)}`);
644
+ console.log();
645
+ }
646
+ return;
647
+ }
648
+
649
+ let successCount = 0;
650
+ let errorCount = 0;
651
+ for (const v of violations) {
652
+ const result = renameFile(v.current, v.expected, ROOT);
653
+ if (result.success) {
654
+ successCount++;
655
+ console.log(` ✓ ${basename(v.current)} → ${basename(v.expected)}${result.updatedImports.length > 0 ? ` (${result.updatedImports.length} imports)` : ""}`);
656
+ } else {
657
+ errorCount++;
658
+ console.log(` ✘ ${basename(v.current)}: ${result.error}`);
659
+ }
660
+ }
661
+
662
+ console.log(`\n Resultado: ${successCount} renombrados, ${errorCount} errores\n`);
663
+ return;
664
+ }
665
+ }
666
+
667
+ if (process.argv[1] && (process.argv[1].endsWith("rename.mjs") || process.argv[1].endsWith("rename.js"))) {
668
+ main().catch(console.error);
669
+ }