@ronaldjdevfs/forge 1.3.3 → 1.3.6
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/README.md +18 -10
- package/package.json +1 -1
- package/skills/forge/SKILL.md +13 -2
- package/skills/forge/command/forge.md +22 -22
- package/skills/forge/reference/assay.md +10 -10
- package/skills/forge/reference/cast.md +43 -0
- package/skills/forge/reference/chain.md +3 -3
- package/skills/forge/reference/evolutionary-architecture.md +2 -2
- package/skills/forge/reference/inspect.md +3 -3
- package/skills/forge/reference/patterns.md +60 -1
- package/skills/forge/reference/principles.md +2 -0
- package/skills/forge/reference/quench.md +3 -3
- package/skills/forge/reference/reforge.md +30 -8
- package/skills/forge/reference/relocate.md +25 -3
- package/skills/forge/scripts/armorer.mjs +21 -0
- package/skills/forge/scripts/detect.mjs +188 -0
- package/skills/forge/scripts/forgeSmith.mjs +85 -0
- package/skills/forge/scripts/inspect.mjs +4 -0
- package/skills/forge/scripts/registry/rules.mjs +25 -0
- package/skills/forge/scripts/rename.mjs +9 -0
- package/skills/forge/scripts/update.mjs +1 -1
- package/skills/forge/templates/feature/controller.ts.md +13 -5
- package/skills/forge/templates/feature/di.ts.md +33 -0
- package/skills/forge/templates/feature/mapper.ts.md +5 -0
- package/skills/forge/templates/feature/repository-impl.ts.md +6 -1
- package/skills/forge/templates/feature/routes.ts.md +5 -0
- package/skills/forge/templates/feature/schema.ts.md +4 -0
- package/skills/forge/templates/feature/test.ts.md +68 -0
- package/skills/forge/templates/feature/use-case.ts.md +5 -0
- package/skills/forge/tests/core.test.mjs +3 -2
- package/src/cli.js +21 -1
|
@@ -848,6 +848,67 @@ export function checkPlatform(ctx) {
|
|
|
848
848
|
return { score: Math.min(score, 15), checks };
|
|
849
849
|
}
|
|
850
850
|
|
|
851
|
+
/* ── R13: Domain logic in platform ── */
|
|
852
|
+
|
|
853
|
+
export function checkPlatformForDomain(ctx) {
|
|
854
|
+
const checks = [];
|
|
855
|
+
let score = 10;
|
|
856
|
+
|
|
857
|
+
if (!ctx.platform || !ctx.platform.exists) {
|
|
858
|
+
return { score, checks };
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const DOMAIN_PATTERNS = /\.(entity|uc|mapper|port|repository)\.(ts|js)$/i;
|
|
862
|
+
const DOMAIN_KEYWORDS = /\b(entity|useCase|use_case|valueObject|domainService|domain_event|dto)\b/i;
|
|
863
|
+
const LOGIC_KEYWORDS = /\b(if|for|while|switch|catch|throw|return)\s*\(/g;
|
|
864
|
+
|
|
865
|
+
const platformDir = join(ROOT, "src", "platform");
|
|
866
|
+
if (!isDir(platformDir)) return { score: score, checks };
|
|
867
|
+
|
|
868
|
+
const allPlatformFiles = findFiles(platformDir, ".ts", 6).concat(findFiles(platformDir, ".js", 6));
|
|
869
|
+
let violations = 0;
|
|
870
|
+
|
|
871
|
+
for (const f of allPlatformFiles) {
|
|
872
|
+
const relPath = relative(ROOT, f);
|
|
873
|
+
const basenamePath = basename(f);
|
|
874
|
+
|
|
875
|
+
// Check 1: File naming suggests domain artifact
|
|
876
|
+
if (DOMAIN_PATTERNS.test(basenamePath)) {
|
|
877
|
+
checks.push({
|
|
878
|
+
...severity(`[R13] Platform contiene artefacto de dominio: ${basenamePath}`, SEVERITY.CRITICAL),
|
|
879
|
+
pass: false,
|
|
880
|
+
detail: relPath,
|
|
881
|
+
fix: "Mover a src/features/<name>/domain/ o application/. Platform no debe tener lógica de negocio.",
|
|
882
|
+
});
|
|
883
|
+
violations++;
|
|
884
|
+
score -= 3;
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Check 2: Content references domain concepts
|
|
889
|
+
const content = read(f);
|
|
890
|
+
if (!content) continue;
|
|
891
|
+
|
|
892
|
+
if (DOMAIN_KEYWORDS.test(content)) {
|
|
893
|
+
checks.push({
|
|
894
|
+
...severity(`[R13] Platform contiene terminología de dominio en ${basenamePath}`, SEVERITY.WARNING),
|
|
895
|
+
pass: false,
|
|
896
|
+
detail: relPath,
|
|
897
|
+
fix: "Si contiene lógica de negocio, mover a features/. Si es naming accidental, renombrar.",
|
|
898
|
+
});
|
|
899
|
+
violations++;
|
|
900
|
+
score -= 2;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
if (violations === 0 && allPlatformFiles.length > 0) {
|
|
905
|
+
checks.push({ ...severity("Platform sin lógica de dominio", SEVERITY.INFO), pass: true });
|
|
906
|
+
score = 10;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
return { score: Math.max(score, 0), checks };
|
|
910
|
+
}
|
|
911
|
+
|
|
851
912
|
export function checkDependencies(ctx) {
|
|
852
913
|
const checks = [];
|
|
853
914
|
let score = 0;
|
|
@@ -997,6 +1058,111 @@ export function checkNaming(projectRoot = ROOT) {
|
|
|
997
1058
|
return { score: Math.max(score, 0), checks };
|
|
998
1059
|
}
|
|
999
1060
|
|
|
1061
|
+
export function checkImportConventions(features) {
|
|
1062
|
+
const checks = [];
|
|
1063
|
+
let score = 20;
|
|
1064
|
+
|
|
1065
|
+
if (features.length === 0) return { score: 20, checks };
|
|
1066
|
+
|
|
1067
|
+
const allFeatureFiles = findFiles(FEATURES, ".ts", 6).concat(findFiles(FEATURES, ".js", 6));
|
|
1068
|
+
const allPlatformFiles = isDir(join(SRC, "platform")) ? findFiles(join(SRC, "platform"), ".ts", 6) : [];
|
|
1069
|
+
const files = [...allFeatureFiles, ...allPlatformFiles];
|
|
1070
|
+
|
|
1071
|
+
let r10Violations = 0; // Bare specifiers
|
|
1072
|
+
let r11Violations = 0; // Missing .js extension
|
|
1073
|
+
let r12Violations = 0; // bootstrap.di.js imports
|
|
1074
|
+
|
|
1075
|
+
for (const f of files) {
|
|
1076
|
+
const content = read(f);
|
|
1077
|
+
if (!content) continue;
|
|
1078
|
+
const imports = parseImportsWithLines(content, f);
|
|
1079
|
+
|
|
1080
|
+
for (const imp of imports) {
|
|
1081
|
+
const src = imp.source;
|
|
1082
|
+
|
|
1083
|
+
// R10: Bare specifier — import from "domain/..." (no ./ ../ @/ prefix)
|
|
1084
|
+
if (
|
|
1085
|
+
!src.startsWith("./") &&
|
|
1086
|
+
!src.startsWith("../") &&
|
|
1087
|
+
!src.startsWith("@/") &&
|
|
1088
|
+
!src.startsWith("/") &&
|
|
1089
|
+
src.includes("/") &&
|
|
1090
|
+
!src.startsWith("tsyringe") &&
|
|
1091
|
+
!src.startsWith("reflect-metadata") &&
|
|
1092
|
+
!src.startsWith("express") &&
|
|
1093
|
+
!src.startsWith("node:") &&
|
|
1094
|
+
!src.startsWith("mongoose") &&
|
|
1095
|
+
!src.startsWith("prisma") &&
|
|
1096
|
+
src !== "tsyringe" &&
|
|
1097
|
+
!src.includes("node_modules")
|
|
1098
|
+
) {
|
|
1099
|
+
r10Violations++;
|
|
1100
|
+
checks.push({
|
|
1101
|
+
severity: SEVERITY.ERROR,
|
|
1102
|
+
label: `[R10] Bare specifier en import local — debe usar ./ o @/`,
|
|
1103
|
+
pass: false,
|
|
1104
|
+
detail: `${relative(ROOT, f)}:${imp.line} → "${src}"`,
|
|
1105
|
+
fix: `Reemplazar "${src}" por "./${src}.js" (relativo) o "@/shared/${src.split("/").pop()}" (alias)`,
|
|
1106
|
+
});
|
|
1107
|
+
score -= 2;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// R11: Import con extensión .ts en vez de .js
|
|
1111
|
+
if (src.endsWith(".ts") && !src.endsWith(".d.ts")) {
|
|
1112
|
+
r11Violations++;
|
|
1113
|
+
checks.push({
|
|
1114
|
+
severity: SEVERITY.ERROR,
|
|
1115
|
+
label: `[R11] Import con extensión .ts — debe usar .js`,
|
|
1116
|
+
pass: false,
|
|
1117
|
+
detail: `${relative(ROOT, f)}:${imp.line} → "${src}"`,
|
|
1118
|
+
fix: src.replace(/\.ts$/, ".js"),
|
|
1119
|
+
});
|
|
1120
|
+
score -= 2;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// R12: Import desde bootstrap.di.js
|
|
1124
|
+
if (src.includes("bootstrap.di")) {
|
|
1125
|
+
r12Violations++;
|
|
1126
|
+
checks.push({
|
|
1127
|
+
severity: SEVERITY.ERROR,
|
|
1128
|
+
label: `[R12] Import desde bootstrap.di.js — no existe en arquitectura actual`,
|
|
1129
|
+
pass: false,
|
|
1130
|
+
detail: `${relative(ROOT, f)}:${imp.line} → "${src}"`,
|
|
1131
|
+
fix: `Reemplazar por "./di.js" (feature con DI propia) o "@/setting/dependencies/<feature>.di.js" (feature sin DI propia)`,
|
|
1132
|
+
});
|
|
1133
|
+
score -= 3;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// R12b: registerSingleton con model() (Mongoose)
|
|
1138
|
+
if (content.includes("registerSingleton") && content.includes("model(")) {
|
|
1139
|
+
checks.push({
|
|
1140
|
+
severity: SEVERITY.WARNING,
|
|
1141
|
+
label: `[R12] registerSingleton usado con model() — debe usar register({ useValue })`,
|
|
1142
|
+
pass: false,
|
|
1143
|
+
detail: relative(ROOT, f),
|
|
1144
|
+
fix: 'Reemplazar container.registerSingleton(...) por container.register(..., { useValue: ... as any })',
|
|
1145
|
+
});
|
|
1146
|
+
score -= 2;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
if (r10Violations === 0 && files.length > 0) {
|
|
1151
|
+
checks.push({ ...severity("[R10] Sin bare specifiers en imports", SEVERITY.INFO), pass: true });
|
|
1152
|
+
score += 2;
|
|
1153
|
+
}
|
|
1154
|
+
if (r11Violations === 0 && files.length > 0) {
|
|
1155
|
+
checks.push({ ...severity("[R11] Sin imports con extensión .ts", SEVERITY.INFO), pass: true });
|
|
1156
|
+
score += 2;
|
|
1157
|
+
}
|
|
1158
|
+
if (r12Violations === 0 && files.length > 0) {
|
|
1159
|
+
checks.push({ ...severity("[R12] Sin imports a bootstrap.di.js", SEVERITY.INFO), pass: true });
|
|
1160
|
+
score += 2;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
return { score: Math.max(score, 0), checks };
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1000
1166
|
export function allChecks(features, graph, ctx) {
|
|
1001
1167
|
const g = graph || getGraph();
|
|
1002
1168
|
const c = ctx || {};
|
|
@@ -1006,10 +1172,12 @@ export function allChecks(features, graph, ctx) {
|
|
|
1006
1172
|
decorators: checkDecorators(features),
|
|
1007
1173
|
ownership: checkOwnership(c),
|
|
1008
1174
|
platform: checkPlatform(c),
|
|
1175
|
+
platformDomain: checkPlatformForDomain(c),
|
|
1009
1176
|
dependencies: checkDependencies(c),
|
|
1010
1177
|
graph: checkGraph(g),
|
|
1011
1178
|
customRules: checkCustomRules(features),
|
|
1012
1179
|
naming: checkNaming(),
|
|
1180
|
+
importConventions: checkImportConventions(features),
|
|
1013
1181
|
};
|
|
1014
1182
|
}
|
|
1015
1183
|
|
|
@@ -1103,6 +1271,26 @@ export function applyFixes(checks, projectRoot = ROOT) {
|
|
|
1103
1271
|
}
|
|
1104
1272
|
}
|
|
1105
1273
|
|
|
1274
|
+
// Fix R11: .ts → .js in import path
|
|
1275
|
+
if (check.label?.includes("[R11]") && check.fix && check.detail) {
|
|
1276
|
+
const filePath = join(projectRoot, check.detail.split(":")[0]);
|
|
1277
|
+
if (existsSync(filePath)) {
|
|
1278
|
+
const content = readFileSync(filePath, "utf-8");
|
|
1279
|
+
const newContent = content.replace(
|
|
1280
|
+
/(from\s+['"])([^'"]+)\.ts(['"])/g,
|
|
1281
|
+
(match, prefix, path, suffix) => {
|
|
1282
|
+
if (path.endsWith(".d")) return match;
|
|
1283
|
+
return `${prefix}${path}.js${suffix}`;
|
|
1284
|
+
}
|
|
1285
|
+
);
|
|
1286
|
+
if (newContent !== content) {
|
|
1287
|
+
writeFileSync(filePath, newContent);
|
|
1288
|
+
fixed++;
|
|
1289
|
+
details.push(` ${GREEN}✔${RESET} ${relative(projectRoot, filePath)}: extensión .ts → .js en imports`);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1106
1294
|
// Fix missing reflect-metadata import
|
|
1107
1295
|
if (check.fix?.includes('import "reflect-metadata"') && check.detail) {
|
|
1108
1296
|
const filePath = join(projectRoot, check.detail);
|
|
@@ -60,11 +60,96 @@ function hasCriticalViolations(result) {
|
|
|
60
60
|
* Quick check against proposed content.
|
|
61
61
|
* Returns violations that would be introduced by the proposed content.
|
|
62
62
|
*/
|
|
63
|
+
/**
|
|
64
|
+
* Quickly check proposed content for common import violations
|
|
65
|
+
* before the file is written to disk (preToolUse guard).
|
|
66
|
+
*/
|
|
67
|
+
function checkProposedContentViolations(filePath, content) {
|
|
68
|
+
const violations = [];
|
|
69
|
+
const lines = content.split("\n");
|
|
70
|
+
|
|
71
|
+
for (let i = 0; i < lines.length; i++) {
|
|
72
|
+
const line = lines[i];
|
|
73
|
+
|
|
74
|
+
// Match import/export from statements
|
|
75
|
+
const importMatch = line.match(
|
|
76
|
+
/(?:import|export)\s+(?:type\s+)?(?:\{[^}]*\}|[^;{]+?)\s+from\s+['"]([^'"]+)['"]/
|
|
77
|
+
);
|
|
78
|
+
if (!importMatch) continue;
|
|
79
|
+
|
|
80
|
+
const src = importMatch[1];
|
|
81
|
+
const lineNum = i + 1;
|
|
82
|
+
|
|
83
|
+
// R10: Bare specifier — import from "domain/..." (no ./ ../ @/ prefix)
|
|
84
|
+
if (
|
|
85
|
+
!src.startsWith("./") &&
|
|
86
|
+
!src.startsWith("../") &&
|
|
87
|
+
!src.startsWith("@/") &&
|
|
88
|
+
!src.startsWith("/") &&
|
|
89
|
+
src.includes("/") &&
|
|
90
|
+
!src.startsWith("tsyringe") &&
|
|
91
|
+
!src.startsWith("reflect-metadata") &&
|
|
92
|
+
!src.startsWith("express") &&
|
|
93
|
+
!src.startsWith("node:") &&
|
|
94
|
+
!src.startsWith("mongoose") &&
|
|
95
|
+
!src.startsWith("prisma") &&
|
|
96
|
+
src !== "tsyringe"
|
|
97
|
+
) {
|
|
98
|
+
violations.push({
|
|
99
|
+
severity: "CRITICAL",
|
|
100
|
+
label: `[R10] Bare specifier — debe usar ./ o @/ prefix`,
|
|
101
|
+
detail: `${filePath}:${lineNum} → "${src}"`,
|
|
102
|
+
fix: `Agregar prefijo "./" o "@/" al import`,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// R11: Import con extensión .ts en vez de .js
|
|
107
|
+
if (src.endsWith(".ts") && !src.endsWith(".d.ts")) {
|
|
108
|
+
violations.push({
|
|
109
|
+
severity: "ERROR",
|
|
110
|
+
label: `[R11] Import con extensión .ts — debe usar .js`,
|
|
111
|
+
detail: `${filePath}:${lineNum} → "${src}"`,
|
|
112
|
+
fix: src.replace(/\.ts$/, ".js"),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// R12: import desde bootstrap.di.js
|
|
117
|
+
if (src.includes("bootstrap.di")) {
|
|
118
|
+
violations.push({
|
|
119
|
+
severity: "CRITICAL",
|
|
120
|
+
label: `[R12] Import a bootstrap.di.js — no existe en esta arquitectura`,
|
|
121
|
+
detail: `${filePath}:${lineNum} → "${src}"`,
|
|
122
|
+
fix: 'Usar "./di.js" o "@/setting/dependencies/<feature>.di.js"',
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// R12b: registerSingleton con model() (Mongoose)
|
|
128
|
+
if (content.includes("registerSingleton") && content.includes("model(")) {
|
|
129
|
+
violations.push({
|
|
130
|
+
severity: "CRITICAL",
|
|
131
|
+
label: `[R12] registerSingleton con model() — usar register({ useValue })`,
|
|
132
|
+
detail: filePath,
|
|
133
|
+
fix: 'container.register("Token", { useValue: Model as any })',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return violations;
|
|
138
|
+
}
|
|
139
|
+
|
|
63
140
|
async function checkProposedFile(filePath, content) {
|
|
64
141
|
if (!isSourceFile(filePath)) {
|
|
65
142
|
return { violations: [], total: 0, hasCritical: false, hasErrors: false };
|
|
66
143
|
}
|
|
67
144
|
|
|
145
|
+
// Fast pre-check on proposed content (before it hits disk)
|
|
146
|
+
const contentViolations = checkProposedContentViolations(filePath, content);
|
|
147
|
+
if (contentViolations.length > 0) {
|
|
148
|
+
const hasCritical = contentViolations.some(v => v.severity === "CRITICAL");
|
|
149
|
+
const hasErrors = contentViolations.some(v => v.severity === "ERROR");
|
|
150
|
+
return { violations: contentViolations, total: contentViolations.length, hasCritical, hasErrors };
|
|
151
|
+
}
|
|
152
|
+
|
|
68
153
|
const ctx = await buildContext();
|
|
69
154
|
const features = detectFeaturesOnSrc();
|
|
70
155
|
const graph = ctx.graph || getGraph();
|
|
@@ -18,8 +18,10 @@ const CAT_NAMES = {
|
|
|
18
18
|
layers: "Capas",
|
|
19
19
|
ownership: "Ownership",
|
|
20
20
|
platform: "Platform",
|
|
21
|
+
platformDomain: "Platform Domain",
|
|
21
22
|
dependencies: "Dependencias",
|
|
22
23
|
graph: "Grafo",
|
|
24
|
+
importConventions: "Import Conventions",
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
const CAT_MAX = {
|
|
@@ -28,10 +30,12 @@ const CAT_MAX = {
|
|
|
28
30
|
decorators: 20,
|
|
29
31
|
ownership: 20,
|
|
30
32
|
platform: 15,
|
|
33
|
+
platformDomain: 10,
|
|
31
34
|
dependencies: 15,
|
|
32
35
|
graph: 20,
|
|
33
36
|
customRules: 5,
|
|
34
37
|
naming: 10,
|
|
38
|
+
importConventions: 20,
|
|
35
39
|
};
|
|
36
40
|
|
|
37
41
|
function countBySeverity(checks) {
|
|
@@ -249,6 +249,31 @@ export const RULES = [
|
|
|
249
249
|
return violations;
|
|
250
250
|
},
|
|
251
251
|
}),
|
|
252
|
+
|
|
253
|
+
defineRule({
|
|
254
|
+
id: "R13",
|
|
255
|
+
name: "Platform no contiene lógica de dominio",
|
|
256
|
+
severity: SEVERITY.CRITICAL,
|
|
257
|
+
category: CATEGORY.STRUCTURE,
|
|
258
|
+
description: "Platform es backbone técnico y no debe contener entidades, casos de uso, mappers de dominio, schemas de entidades, repositorios de dominio ni ninguna otra lógica de negocio.",
|
|
259
|
+
fix: "Mover el archivo con lógica de dominio a src/features/<name>/domain/, src/features/<name>/application/, o src/features/<name>/adapters/ según corresponda.",
|
|
260
|
+
example: "✘ platform/User.entity.ts, platform/payments/ creando lógica de dominio en platform/",
|
|
261
|
+
check: (graph, ctx) => {
|
|
262
|
+
const violations = [];
|
|
263
|
+
if (!ctx || !ctx.platform || !ctx.platform.exists) return violations;
|
|
264
|
+
for (const comp of (ctx.platform.components || [])) {
|
|
265
|
+
const lower = comp.toLowerCase();
|
|
266
|
+
if (lower.endsWith(".entity") || lower.endsWith(".uc") || lower.endsWith(".mapper") || lower.endsWith(".port")) {
|
|
267
|
+
violations.push({
|
|
268
|
+
rule: "R13", severity: SEVERITY.CRITICAL,
|
|
269
|
+
from: `platform:${comp}`, to: "(domain)",
|
|
270
|
+
description: `Platform contiene artefacto de dominio: "${comp}"`,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return violations;
|
|
275
|
+
},
|
|
276
|
+
}),
|
|
252
277
|
];
|
|
253
278
|
|
|
254
279
|
/**
|
|
@@ -147,6 +147,15 @@ export function computeExpectedName(filePath) {
|
|
|
147
147
|
// Platform layer
|
|
148
148
|
if (parts[1] === "platform" && parts[2]) {
|
|
149
149
|
const subdir = parts[2];
|
|
150
|
+
const DOMAIN_SUFFIXES = /\.(entity|uc|mapper|port|repository)\.(ts|js)$/i;
|
|
151
|
+
if (DOMAIN_SUFFIXES.test(filename)) {
|
|
152
|
+
return {
|
|
153
|
+
current: filePath,
|
|
154
|
+
expected: filePath,
|
|
155
|
+
relPath: relPath,
|
|
156
|
+
rule: `[R13] Platform no debe contener artefactos de dominio: "${filename}" pertenece a src/features/<name>/`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
150
159
|
const rule = PLATFORM_RULES[subdir];
|
|
151
160
|
if (!rule) return null;
|
|
152
161
|
|
|
@@ -17,7 +17,7 @@ const ROOT = process.cwd();
|
|
|
17
17
|
const CACHE_PATH = join(ROOT, ".forge", "version-cache.json");
|
|
18
18
|
const CACHE_TTL = 86400000; // 24h
|
|
19
19
|
const VERSION_URL = "https://forge.dev/latest";
|
|
20
|
-
const CURRENT_VERSION = "1.
|
|
20
|
+
const CURRENT_VERSION = "1.3.6";
|
|
21
21
|
|
|
22
22
|
function readJson(path) {
|
|
23
23
|
try {
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
```typescript
|
|
2
2
|
// src/features/<domain>/adapters/in/http/<Domain>.controller.ts
|
|
3
|
+
//
|
|
4
|
+
// IMPORTANTE — Convenciones:
|
|
5
|
+
// 1. Nombres de métodos: usar createHandler (no "add", "store", etc.)
|
|
6
|
+
// 2. Si la entidad <Domain> es compartida desde platform/domain/,
|
|
7
|
+
// importar con path alias: import type { <Domain> } from "@/domain/entities/<Domain>.js";
|
|
8
|
+
// 3. Si el feature NO tiene di.ts propio, el controller se importa desde
|
|
9
|
+
// @/setting/dependencies/<domain>.di.js en vez de bootstrap.di.js
|
|
10
|
+
|
|
3
11
|
import { injectable, inject } from "tsyringe";
|
|
4
12
|
import type { Request, Response, NextFunction } from "express";
|
|
5
|
-
import { Create<Domain> } from "
|
|
6
|
-
import { Get<Domain> } from "
|
|
7
|
-
import { List<Domain> } from "
|
|
8
|
-
import { Update<Domain> } from "
|
|
9
|
-
import { Delete<Domain> } from "
|
|
13
|
+
import { Create<Domain> } from "../../application/use-cases/Create<Domain>.uc.js";
|
|
14
|
+
import { Get<Domain> } from "../../application/use-cases/Get<Domain>.uc.js";
|
|
15
|
+
import { List<Domain> } from "../../application/use-cases/List<Domain>.uc.js";
|
|
16
|
+
import { Update<Domain> } from "../../application/use-cases/Update<Domain>.uc.js";
|
|
17
|
+
import { Delete<Domain> } from "../../application/use-cases/Delete<Domain>.uc.js";
|
|
10
18
|
|
|
11
19
|
@injectable()
|
|
12
20
|
export class <Domain>Controller {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
```typescript
|
|
2
|
+
// src/features/<domain>/di.ts
|
|
3
|
+
// DI container wiring para el feature <Domain>.
|
|
4
|
+
//
|
|
5
|
+
// NOTA: Si el repositorio exporta un modelo Mongoose (model()) en lugar de una clase,
|
|
6
|
+
// usar container.register() con useValue en vez de registerSingleton.
|
|
7
|
+
//
|
|
8
|
+
// Para entidades compartidas desde platform/domain/, usar path alias @/domain/ en vez de relativo.
|
|
9
|
+
|
|
10
|
+
import { container } from "tsyringe";
|
|
11
|
+
import type { I<Domain>Repository } from "./domain/repositories/I<Domain>.repository.js";
|
|
12
|
+
import { <Domain>Repository } from "./adapters/out/persistence/<Domain>.repository.js";
|
|
13
|
+
import { Create<Domain> } from "./application/use-cases/Create<Domain>.uc.js";
|
|
14
|
+
import { Get<Domain> } from "./application/use-cases/Get<Domain>.uc.js";
|
|
15
|
+
import { List<Domain> } from "./application/use-cases/List<Domain>.uc.js";
|
|
16
|
+
import { Update<Domain> } from "./application/use-cases/Update<Domain>.uc.js";
|
|
17
|
+
import { Delete<Domain> } from "./application/use-cases/Delete<Domain>.uc.js";
|
|
18
|
+
|
|
19
|
+
// ── Repositorio ──
|
|
20
|
+
// Si <Domain>Repository es una clase (implementación estándar):
|
|
21
|
+
container.registerSingleton<I<Domain>Repository>("I<Domain>Repository", <Domain>Repository);
|
|
22
|
+
|
|
23
|
+
// Si <Domain>Repository es un modelo Mongoose (export default model()), usar:
|
|
24
|
+
// import <Domain>Model from "./adapters/out/persistence/<Domain>.schema.js";
|
|
25
|
+
// container.register<I<Domain>Repository>("I<Domain>Repository", { useValue: <Domain>Model as any });
|
|
26
|
+
|
|
27
|
+
// ── Use Cases ──
|
|
28
|
+
container.registerSingleton(Create<Domain>);
|
|
29
|
+
container.registerSingleton(Get<Domain>);
|
|
30
|
+
container.registerSingleton(List<Domain>);
|
|
31
|
+
container.registerSingleton(Update<Domain>);
|
|
32
|
+
container.registerSingleton(Delete<Domain>);
|
|
33
|
+
```
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
```typescript
|
|
2
2
|
// src/features/<domain>/application/mappers/<Domain>.mapper.ts
|
|
3
|
+
//
|
|
4
|
+
// IMPORTANTE: Si la entidad <Domain> está en platform/domain/entities/ (compartida),
|
|
5
|
+
// reemplazar el import relativo por path alias:
|
|
6
|
+
// import type { <Domain> } from "@/domain/entities/<Domain>.js";
|
|
7
|
+
|
|
3
8
|
import type { <Domain> } from "../domain/entities/<Domain>.entity.js";
|
|
4
9
|
|
|
5
10
|
export class <Domain>Mapper {
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
```typescript
|
|
2
2
|
// src/features/<domain>/adapters/out/persistence/<Domain>.repository.ts
|
|
3
|
+
//
|
|
4
|
+
// IMPORTANTE: Si la entidad <Domain> está en platform/domain/entities/ (compartida),
|
|
5
|
+
// reemplazar el import relativo por path alias:
|
|
6
|
+
// import type { <Domain> } from "@/domain/entities/<Domain>.js";
|
|
7
|
+
|
|
3
8
|
import { injectable } from "tsyringe";
|
|
4
9
|
import type { <Domain> } from "../../../domain/entities/<Domain>.entity.js";
|
|
5
10
|
import type { I<Domain>Repository } from "../../../domain/repositories/I<Domain>.repository.js";
|
|
6
|
-
import { <Domain>Mapper } from "
|
|
11
|
+
import { <Domain>Mapper } from "../../application/mappers/<Domain>.mapper.js";
|
|
7
12
|
import <Domain>Model from "./<Domain>.schema.js";
|
|
8
13
|
import { RepositoryError } from "@/shared/errors/RepositoryError.js";
|
|
9
14
|
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
```typescript
|
|
2
2
|
// src/features/<domain>/adapters/in/http/<Domain>.routes.ts
|
|
3
|
+
//
|
|
4
|
+
// IMPORTANTE: Los nombres de método del controller deben coincidir
|
|
5
|
+
// con los invocados aquí. Si el controller usa "add" en vez de "createHandler",
|
|
6
|
+
// ajustar la ruta: router.post("/", controller.add);
|
|
7
|
+
|
|
3
8
|
import { Router } from "express";
|
|
4
9
|
import { container } from "tsyringe";
|
|
5
10
|
import { <Domain>Controller } from "./<Domain>.controller.js";
|
|
@@ -12,5 +12,9 @@ const <Domain>Schema = new Schema<<Domain>>(
|
|
|
12
12
|
|
|
13
13
|
<Domain>Schema.index({ /* índices */ });
|
|
14
14
|
|
|
15
|
+
// NOTA DI: model() exporta un objeto Model, NO una clase.
|
|
16
|
+
// En el contenedor DI usar container.register() con useValue, NO registerSingleton:
|
|
17
|
+
// import <Domain>Model from "./<Domain>.schema.js";
|
|
18
|
+
// container.register<I<Domain>Repository>("I<Domain>Repository", { useValue: <Domain>Model as any });
|
|
15
19
|
export default model<<Domain>>("<Domain>", <Domain>Schema);
|
|
16
20
|
```
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
```typescript
|
|
2
|
+
// src/features/<domain>/__tests__/Create<Domain>.test.ts
|
|
3
|
+
//
|
|
4
|
+
// Convenciones para tests:
|
|
5
|
+
// - Usar extension .js en imports (ESM + verbatimModuleSyntax)
|
|
6
|
+
// - Usar "as const" para literales que forman parte de union types
|
|
7
|
+
// - Usar "result!" (non-null assertion) cuando execute() retorna T | null
|
|
8
|
+
// - Usar "(result as any)._id" si _id no existe en el tipo de dominio
|
|
9
|
+
|
|
10
|
+
import { describe, it, before } from "node:test";
|
|
11
|
+
import assert from "node:assert";
|
|
12
|
+
import { Create<Domain> } from "../application/use-cases/Create<Domain>.uc.js";
|
|
13
|
+
import type { I<Domain>Repository } from "../domain/repositories/I<Domain>.repository.js";
|
|
14
|
+
import type { <Domain> } from "../domain/entities/<Domain>.entity.js";
|
|
15
|
+
|
|
16
|
+
class Mock<Domain>Repository implements I<Domain>Repository {
|
|
17
|
+
private store: <Domain>[] = [];
|
|
18
|
+
|
|
19
|
+
async create(data: Partial<<Domain>>): Promise<<Domain>> {
|
|
20
|
+
const entity = { id: "1", ...data } as <Domain>;
|
|
21
|
+
this.store.push(entity);
|
|
22
|
+
return entity;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async findById(id: string): Promise<<Domain> | null> {
|
|
26
|
+
return this.store.find(e => (e as any).id === id) || null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async findAll(): Promise<{ data: <Domain>[]; total: number }> {
|
|
30
|
+
return { data: this.store, total: this.store.length };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async update(id: string, data: Partial<<Domain>>): Promise<<Domain> | null> {
|
|
34
|
+
const idx = this.store.findIndex(e => (e as any).id === id);
|
|
35
|
+
if (idx === -1) return null;
|
|
36
|
+
this.store[idx] = { ...this.store[idx], ...data };
|
|
37
|
+
return this.store[idx];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async delete(id: string): Promise<void> {
|
|
41
|
+
this.store = this.store.filter(e => (e as any).id !== id);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe("Create<Domain>", () => {
|
|
46
|
+
let useCase: Create<Domain>;
|
|
47
|
+
let mockRepo: Mock<Domain>Repository;
|
|
48
|
+
|
|
49
|
+
before(() => {
|
|
50
|
+
mockRepo = new Mock<Domain>Repository();
|
|
51
|
+
useCase = new Create<Domain>(mockRepo);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("debería crear una entidad <domain>", async () => {
|
|
55
|
+
const input = { name: "test" as const };
|
|
56
|
+
const result = await useCase.execute(input);
|
|
57
|
+
assert.ok(result!);
|
|
58
|
+
assert.equal((result as any)._id || result!.id, "1");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("debería lanzar error si faltan datos requeridos", async () => {
|
|
62
|
+
await assert.rejects(
|
|
63
|
+
() => useCase.execute(null as any),
|
|
64
|
+
{ message: /requeridos|requerid/i }
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
```
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
```typescript
|
|
2
2
|
// src/features/<domain>/application/use-cases/Create<Domain>.uc.ts
|
|
3
|
+
//
|
|
4
|
+
// IMPORTANTE: Si la entidad <Domain> está en platform/domain/entities/ (compartida),
|
|
5
|
+
// reemplazar el import relativo por path alias:
|
|
6
|
+
// import type { <Domain> } from "@/domain/entities/<Domain>.js";
|
|
7
|
+
|
|
3
8
|
import { injectable, inject } from "tsyringe";
|
|
4
9
|
import type { <Domain> } from "../../domain/entities/<Domain>.entity.js";
|
|
5
10
|
import type { I<Domain>Repository } from "../../domain/repositories/I<Domain>.repository.js";
|
|
@@ -233,11 +233,12 @@ describe("formatter.mjs", () => {
|
|
|
233
233
|
});
|
|
234
234
|
|
|
235
235
|
describe("registry/rules.mjs", () => {
|
|
236
|
-
it("has
|
|
236
|
+
it("has 10 built-in rules (R1-R9 + R13)", async () => {
|
|
237
237
|
const { RULES, RULES_BY_ID } = await import("../scripts/registry/rules.mjs");
|
|
238
|
-
assert.equal(RULES.length,
|
|
238
|
+
assert.equal(RULES.length, 10);
|
|
239
239
|
assert.ok(RULES_BY_ID.R1);
|
|
240
240
|
assert.ok(RULES_BY_ID.R9);
|
|
241
|
+
assert.ok(RULES_BY_ID.R13);
|
|
241
242
|
for (const r of RULES) {
|
|
242
243
|
assert.ok(r.id);
|
|
243
244
|
assert.ok(r.name);
|
package/src/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { copyFileSync, mkdirSync, existsSync, writeFileSync, readFileSync, cpSync, readdirSync } from "fs";
|
|
3
|
+
import { copyFileSync, mkdirSync, existsSync, writeFileSync, readFileSync, cpSync, readdirSync, statSync } from "fs";
|
|
4
4
|
import { join, dirname, relative } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
import { execSync } from "child_process";
|
|
@@ -43,6 +43,20 @@ function copyRecursive(src, dest) {
|
|
|
43
43
|
cpSync(src, dest, { recursive: true, force: true });
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
function renderSkillPaths(skillDest, skillPath) {
|
|
47
|
+
for (const entry of readdirSync(skillDest)) {
|
|
48
|
+
const fullPath = join(skillDest, entry);
|
|
49
|
+
if (statSync(fullPath).isDirectory()) {
|
|
50
|
+
renderSkillPaths(fullPath, skillPath);
|
|
51
|
+
} else if (entry.endsWith(".md")) {
|
|
52
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
53
|
+
if (content.includes("{{AGENT_PATH}}")) {
|
|
54
|
+
writeFileSync(fullPath, content.replace(/\{\{AGENT_PATH\}\}/g, skillPath), "utf-8");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
46
60
|
function detectPM(configDir) {
|
|
47
61
|
if (existsSync(join(configDir, "pnpm-lock.yaml")) || existsSync(join(process.cwd(), "pnpm-lock.yaml"))) return "pnpm";
|
|
48
62
|
if (existsSync(join(configDir, "bun.lock")) || existsSync(join(configDir, "bun.lockb"))) return "bun";
|
|
@@ -143,8 +157,11 @@ async function installOpenCode(isGlobal = false) {
|
|
|
143
157
|
|
|
144
158
|
const s = spinner();
|
|
145
159
|
|
|
160
|
+
const skillPath = isGlobal ? join("~/.config/opencode", "skills", "forge") : join(".opencode", "skills", "forge");
|
|
161
|
+
|
|
146
162
|
s.start("Copiando skill a " + rel);
|
|
147
163
|
copyRecursive(SKILL_SRC, target);
|
|
164
|
+
renderSkillPaths(target, skillPath);
|
|
148
165
|
s.stop("Skill copiada a " + rel);
|
|
149
166
|
|
|
150
167
|
s.start("Generando comandos /forge-*");
|
|
@@ -192,6 +209,9 @@ async function installAgentTemplates(agentDir, agentName) {
|
|
|
192
209
|
writeFileSync(join(skillDest, "SKILL.md"), rendered, "utf-8");
|
|
193
210
|
}
|
|
194
211
|
|
|
212
|
+
// Render {{AGENT_PATH}} in remaining .md files (command/forge.md, reference/*.md)
|
|
213
|
+
renderSkillPaths(skillDest, config.skillPath);
|
|
214
|
+
|
|
195
215
|
s.stop(`${agentName} configurado en ${agentDir}/`);
|
|
196
216
|
log.success(`${agentName} listo (skill + ${config.postInstall || "templates"})`);
|
|
197
217
|
}
|