@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.
- package/README.md +130 -8
- package/package.json +2 -2
- package/skills/forge/SKILL.md +86 -15
- package/skills/forge/profiles/express-drizzle.md +107 -0
- package/skills/forge/profiles/fastify-mongodb.md +103 -0
- package/skills/forge/profiles/fastify-prisma.md +81 -0
- package/skills/forge/profiles/nestjs-mongodb.md +92 -0
- package/skills/forge/profiles/nestjs-postgres.md +98 -0
- package/skills/forge/reference/api-design.md +62 -0
- package/skills/forge/reference/assay.md +82 -0
- package/skills/forge/reference/cast.md +81 -7
- package/skills/forge/reference/data-patterns.md +86 -0
- package/skills/forge/reference/di-strategies.md +50 -0
- package/skills/forge/reference/errors.md +65 -0
- package/skills/forge/reference/events.md +95 -0
- package/skills/forge/reference/help.md +40 -0
- package/skills/forge/reference/hooks.md +62 -0
- package/skills/forge/reference/observability.md +66 -0
- package/skills/forge/reference/patterns.md +52 -0
- package/skills/forge/reference/principles.md +6 -0
- package/skills/forge/reference/reforge.md +69 -5
- package/skills/forge/reference/relocate.md +15 -2
- package/skills/forge/reference/security-patterns.md +87 -0
- package/skills/forge/reference/testing-patterns.md +69 -0
- package/skills/forge/scripts/assay.mjs +481 -0
- package/skills/forge/scripts/context.mjs +147 -43
- package/skills/forge/scripts/detect.mjs +371 -22
- package/skills/forge/scripts/forge-api.mjs +373 -0
- package/skills/forge/scripts/forge-config.mjs +268 -0
- package/skills/forge/scripts/forge-signals.mjs +131 -0
- package/skills/forge/scripts/forge-state.mjs +97 -0
- package/skills/forge/scripts/formatter.mjs +133 -0
- package/skills/forge/scripts/graph.mjs +5 -21
- package/skills/forge/scripts/hook.mjs +250 -0
- package/skills/forge/scripts/inspect.mjs +171 -22
- package/skills/forge/scripts/parse-imports.mjs +249 -0
- package/skills/forge/scripts/pin.mjs +151 -0
- package/skills/forge/scripts/posttool.mjs +224 -0
- package/skills/forge/scripts/profile.mjs +124 -20
- package/skills/forge/scripts/registry/rules.mjs +344 -0
- package/skills/forge/scripts/rename.mjs +669 -0
- package/skills/forge/scripts/rollback.mjs +213 -0
- package/skills/forge/scripts/update.mjs +114 -0
- package/skills/forge/templates/feature/domain-error.ts.md +9 -0
- package/skills/forge/templates/feature/domain-event.ts.md +9 -0
- package/skills/forge/templates/feature/event-handler.ts.md +10 -0
- package/skills/forge/templates/feature/use-case.ts.md +10 -2
- package/skills/forge/tests/core.test.mjs +403 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* posttool.mjs — PostToolUse hook para Forge.
|
|
5
|
+
*
|
|
6
|
+
* Se ejecuta después de que el agente escribe código. Analiza los archivos
|
|
7
|
+
* modificados y reporta violaciones arquitectónicas.
|
|
8
|
+
*
|
|
9
|
+
* Uso (CLI):
|
|
10
|
+
* node posttool.mjs → analiza archivos changed en git
|
|
11
|
+
* node posttool.mjs <file1> [file2...] → analiza archivos específicos
|
|
12
|
+
* node posttool.mjs --diff → solo archivos changed vs default branch
|
|
13
|
+
* node posttool.mjs --json → salida JSON
|
|
14
|
+
* node posttool.mjs --strict → exit code 1 si hay CRITICAL/ERROR
|
|
15
|
+
* node posttool.mjs --reminder → solo recordatorio (no bloquea)
|
|
16
|
+
*
|
|
17
|
+
* Uso (API):
|
|
18
|
+
* import { postToolCheck } from "./posttool.mjs";
|
|
19
|
+
* const result = await postToolCheck([...files]);
|
|
20
|
+
*
|
|
21
|
+
* Integración en SKILL.md (Fase 7 del pipeline):
|
|
22
|
+
* node .opencode/skills/forge/scripts/posttool.mjs --reminder
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { readFileSync, existsSync, statSync, readdirSync } from "fs";
|
|
26
|
+
import { join, relative, resolve, extname } from "path";
|
|
27
|
+
import { execFileSync } from "child_process";
|
|
28
|
+
import { buildGraph } from "./graph.mjs";
|
|
29
|
+
import { buildContext } from "./context.mjs";
|
|
30
|
+
import { detectFeaturesOnSrc, allChecks, loadAllInlineIgnores, isIgnored } from "./detect.mjs";
|
|
31
|
+
import { evaluateRules } from "./registry/rules.mjs";
|
|
32
|
+
import {
|
|
33
|
+
CYAN, GREEN, RED, YELLOW, BOLD, RESET, DIM, GRAY,
|
|
34
|
+
formatViolation, formatCheck, formatJson,
|
|
35
|
+
} from "./formatter.mjs";
|
|
36
|
+
|
|
37
|
+
const ROOT = process.cwd();
|
|
38
|
+
|
|
39
|
+
function getChangedFiles() {
|
|
40
|
+
let files = [];
|
|
41
|
+
try {
|
|
42
|
+
const defaultBranch = execFileSync("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] })
|
|
43
|
+
.trim().replace("refs/remotes/origin/", "");
|
|
44
|
+
const raw = execFileSync("git", ["diff", "--name-only", "--diff-filter=ACMR", `${defaultBranch}...HEAD`], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
45
|
+
files = raw.trim().split("\n").filter(Boolean);
|
|
46
|
+
} catch {}
|
|
47
|
+
|
|
48
|
+
if (files.length === 0) {
|
|
49
|
+
try {
|
|
50
|
+
const raw = execFileSync("git", ["status", "--porcelain"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
51
|
+
files = raw.split("\n").filter(Boolean).map(l => l.slice(3));
|
|
52
|
+
} catch {}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return files.filter(f => f.startsWith("src/"));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readFile(path) {
|
|
59
|
+
try { return readFileSync(path, "utf-8"); } catch { return null; }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check modified files for violations.
|
|
64
|
+
* Returns { violations, graphViolations, total, hasCritical, hasErrors, summary }
|
|
65
|
+
*/
|
|
66
|
+
export async function postToolCheck(files, opts = {}) {
|
|
67
|
+
const { strict = false, reminder = false } = opts;
|
|
68
|
+
|
|
69
|
+
if (!files || files.length === 0) {
|
|
70
|
+
files = getChangedFiles();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Only check source files
|
|
74
|
+
files = files.filter(f =>
|
|
75
|
+
f.endsWith(".ts") || f.endsWith(".mjs") || f.endsWith(".js") || f.endsWith(".tsx")
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (files.length === 0) {
|
|
79
|
+
return { violations: [], graphViolations: [], total: 0, hasCritical: false, hasErrors: false, filesChecked: 0, summary: "Sin archivos fuente modificados" };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Build context and run full audit
|
|
83
|
+
const ctx = await buildContext();
|
|
84
|
+
const features = detectFeaturesOnSrc();
|
|
85
|
+
const graph = buildGraph(ROOT);
|
|
86
|
+
const results = allChecks(features, graph, ctx);
|
|
87
|
+
|
|
88
|
+
// Load inline ignores
|
|
89
|
+
const allIgnores = loadAllInlineIgnores(join(ROOT, "src"));
|
|
90
|
+
|
|
91
|
+
// Collect violations that touch the modified files
|
|
92
|
+
const violations = [];
|
|
93
|
+
for (const [, cat] of Object.entries(results)) {
|
|
94
|
+
for (const check of cat.checks) {
|
|
95
|
+
if (check.pass) continue;
|
|
96
|
+
|
|
97
|
+
// Only include violations touching modified files
|
|
98
|
+
const touchesFile = files.some(f =>
|
|
99
|
+
(check.detail && check.detail.includes(f)) ||
|
|
100
|
+
(check.label && check.label.includes(f))
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
if (!touchesFile && !reminder) continue;
|
|
104
|
+
|
|
105
|
+
// Check inline ignores
|
|
106
|
+
if (isIgnored(check, allIgnores)) continue;
|
|
107
|
+
|
|
108
|
+
violations.push(check);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Also evaluate graph rules for changed files
|
|
113
|
+
const graphViolations = evaluateRules(graph, ctx).filter(v => {
|
|
114
|
+
const touchesFile = files.some(f =>
|
|
115
|
+
(v.file && v.file.includes(f)) ||
|
|
116
|
+
(v.from && v.from.includes(f)) ||
|
|
117
|
+
(v.to && v.to.includes(f))
|
|
118
|
+
);
|
|
119
|
+
return touchesFile || reminder;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const allViolations = [...violations, ...graphViolations];
|
|
123
|
+
const hasCritical = allViolations.some(v => v.severity === "CRITICAL");
|
|
124
|
+
const hasErrors = allViolations.some(v => v.severity === "ERROR");
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
violations: allViolations,
|
|
128
|
+
total: allViolations.length,
|
|
129
|
+
hasCritical,
|
|
130
|
+
hasErrors,
|
|
131
|
+
filesChecked: files.length,
|
|
132
|
+
summary: hasCritical
|
|
133
|
+
? `⚠ ${allViolations.length} violación(es) — ${allViolations.filter(v => v.severity === "CRITICAL").length} CRITICAL, ${allViolations.filter(v => v.severity === "ERROR").length} ERROR`
|
|
134
|
+
: hasErrors
|
|
135
|
+
? `⚠ ${allViolations.length} violación(es) — ${allViolations.filter(v => v.severity === "ERROR").length} ERROR`
|
|
136
|
+
: `${allViolations.length} violación(es) menores (WARNING/INFO)`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function printResult(result, opts = {}) {
|
|
141
|
+
const { reminder = false } = opts;
|
|
142
|
+
|
|
143
|
+
console.log(`\n${BOLD}${CYAN}═══ Forge PostTool Hook ═══${RESET}`);
|
|
144
|
+
|
|
145
|
+
if (result.total === 0) {
|
|
146
|
+
console.log(` ${GREEN}✔${RESET} Sin violaciones arquitectónicas en archivos modificados`);
|
|
147
|
+
console.log(` ${DIM}Archivos revisados: ${result.filesChecked}${RESET}\n`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log(` ${DIM}Archivos revisados: ${result.filesChecked}${RESET}`);
|
|
152
|
+
console.log(` ${DIM}Violaciones: ${result.total}${RESET}\n`);
|
|
153
|
+
|
|
154
|
+
if (reminder) {
|
|
155
|
+
console.log(` ${YELLOW}Recordatorio:${RESET} Los archivos modificados tienen ${result.total} violación(es) arquitectónica(s).`);
|
|
156
|
+
console.log(` Ejecuta ${CYAN}forge quench${RESET} para ver el detalle completo.\n`);
|
|
157
|
+
|
|
158
|
+
if (result.hasCritical) {
|
|
159
|
+
const criticalCount = result.violations.filter(v => v.severity === "CRITICAL").length;
|
|
160
|
+
console.log(` ${RED}⚠ ${criticalCount} violación(es) CRITICAL detectadas${RESET}`);
|
|
161
|
+
}
|
|
162
|
+
if (result.hasErrors) {
|
|
163
|
+
const errorCount = result.violations.filter(v => v.severity === "ERROR").length;
|
|
164
|
+
console.log(` ${RED}⚠ ${errorCount} violación(es) ERROR detectadas${RESET}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Print first 5 violations as summary
|
|
168
|
+
const toShow = result.violations.slice(0, 5);
|
|
169
|
+
for (const v of toShow) {
|
|
170
|
+
console.log(formatCheck(v));
|
|
171
|
+
}
|
|
172
|
+
if (result.violations.length > 5) {
|
|
173
|
+
console.log(` ${DIM}... y ${result.violations.length - 5} más${RESET}`);
|
|
174
|
+
}
|
|
175
|
+
console.log();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Full output
|
|
180
|
+
for (const v of result.violations) {
|
|
181
|
+
console.log(formatCheck(v));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
console.log(`\n ${BOLD}Resumen:${RESET} ${result.summary}`);
|
|
185
|
+
if (result.hasCritical) {
|
|
186
|
+
console.log(` ${RED}⚠ Se requiere acción correctiva antes de continuar${RESET}`);
|
|
187
|
+
}
|
|
188
|
+
console.log();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function main() {
|
|
192
|
+
const args = process.argv.slice(2);
|
|
193
|
+
const isJson = args.includes("--json");
|
|
194
|
+
const strict = args.includes("--strict");
|
|
195
|
+
const reminder = args.includes("--reminder");
|
|
196
|
+
const isDiff = args.includes("--diff");
|
|
197
|
+
|
|
198
|
+
let files;
|
|
199
|
+
if (isDiff) {
|
|
200
|
+
files = getChangedFiles();
|
|
201
|
+
} else {
|
|
202
|
+
// Files passed as arguments
|
|
203
|
+
files = args.filter(a => !a.startsWith("--"));
|
|
204
|
+
if (files.length === 0) {
|
|
205
|
+
files = getChangedFiles();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const result = await postToolCheck(files, { strict, reminder });
|
|
210
|
+
|
|
211
|
+
if (isJson) {
|
|
212
|
+
console.log(formatJson(result));
|
|
213
|
+
} else {
|
|
214
|
+
printResult(result, { reminder });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (strict && (result.hasCritical || result.hasErrors)) {
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (process.argv[1]?.endsWith("posttool.mjs")) {
|
|
223
|
+
main().catch(console.error);
|
|
224
|
+
}
|
|
@@ -1,64 +1,142 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { readFileSync, existsSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
3
5
|
import { buildContext } from "./context.mjs";
|
|
4
6
|
|
|
7
|
+
const ROOT = process.cwd();
|
|
8
|
+
|
|
5
9
|
const PROFILES = [
|
|
6
10
|
{
|
|
7
11
|
name: "express-mongodb",
|
|
8
|
-
match: (ctx) =>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
ctx.orm === "Mongoose",
|
|
12
|
+
match: (ctx) => ctx.framework === "Express" && ctx.database === "MongoDB" && ctx.orm === "Mongoose",
|
|
13
|
+
deps: ["express", "mongoose"],
|
|
14
|
+
opts: { di: "tsyringe|awilix", test: "vitest|jest", validation: "zod|joi", logger: "pino|winston" },
|
|
12
15
|
},
|
|
13
16
|
{
|
|
14
17
|
name: "express-prisma",
|
|
15
|
-
match: (ctx) =>
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
match: (ctx) => ctx.framework === "Express" && ctx.orm === "Prisma",
|
|
19
|
+
deps: ["express", "@prisma/client"],
|
|
20
|
+
opts: { di: "tsyringe|awilix", test: "vitest|jest", validation: "zod|joi", logger: "pino|winston" },
|
|
18
21
|
},
|
|
19
22
|
{
|
|
20
23
|
name: "express-postgres",
|
|
21
|
-
match: (ctx) =>
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
match: (ctx) => ctx.framework === "Express" && ctx.database === "PostgreSQL" && ctx.orm === "native",
|
|
25
|
+
deps: ["express", "pg"],
|
|
26
|
+
opts: { di: "tsyringe|awilix", test: "vitest|jest", validation: "zod|joi", logger: "pino|winston" },
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "express-drizzle",
|
|
30
|
+
match: (ctx) => ctx.framework === "Express" && ctx.orm === "Drizzle",
|
|
31
|
+
deps: ["express", "drizzle-orm"],
|
|
32
|
+
opts: { di: "tsyringe|awilix", test: "vitest|jest", validation: "zod|joi", logger: "pino|winston" },
|
|
25
33
|
},
|
|
26
34
|
{
|
|
27
35
|
name: "fastify-postgres",
|
|
28
|
-
match: (ctx) =>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
match: (ctx) => ctx.framework === "Fastify" && ctx.database === "PostgreSQL" && ctx.orm === "Prisma",
|
|
37
|
+
deps: ["fastify", "@prisma/client"],
|
|
38
|
+
opts: { test: "vitest|jest", validation: "zod|typebox", logger: "pino" },
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "fastify-prisma",
|
|
42
|
+
match: (ctx) => ctx.framework === "Fastify" && ctx.orm === "Prisma",
|
|
43
|
+
deps: ["fastify", "@prisma/client"],
|
|
44
|
+
opts: { test: "vitest|jest", validation: "zod|typebox", logger: "pino" },
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "fastify-mongodb",
|
|
48
|
+
match: (ctx) => ctx.framework === "Fastify" && ctx.database === "MongoDB" && ctx.orm === "Mongoose",
|
|
49
|
+
deps: ["fastify", "mongoose"],
|
|
50
|
+
opts: { test: "vitest|jest", validation: "zod|typebox", logger: "pino" },
|
|
32
51
|
},
|
|
33
52
|
{
|
|
34
53
|
name: "nestjs-prisma",
|
|
35
|
-
match: (ctx) =>
|
|
36
|
-
|
|
37
|
-
|
|
54
|
+
match: (ctx) => ctx.framework === "NestJS" && ctx.orm === "Prisma",
|
|
55
|
+
deps: ["@nestjs/core", "@prisma/client"],
|
|
56
|
+
opts: { test: "vitest|jest", validation: "class-validator|zod" },
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "nestjs-postgres",
|
|
60
|
+
match: (ctx) => ctx.framework === "NestJS" && ctx.database === "PostgreSQL" && (ctx.orm === "native" || ctx.orm === "TypeORM"),
|
|
61
|
+
deps: ["@nestjs/core", "pg"],
|
|
62
|
+
opts: { test: "vitest|jest", validation: "class-validator|zod" },
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "nestjs-mongodb",
|
|
66
|
+
match: (ctx) => ctx.framework === "NestJS" && ctx.database === "MongoDB" && ctx.orm === "Mongoose",
|
|
67
|
+
deps: ["@nestjs/core", "mongoose"],
|
|
68
|
+
opts: { test: "vitest|jest", validation: "class-validator|zod" },
|
|
38
69
|
},
|
|
39
70
|
];
|
|
40
71
|
|
|
72
|
+
function readPkg() {
|
|
73
|
+
try {
|
|
74
|
+
const raw = readFileSync(join(ROOT, "package.json"), "utf-8");
|
|
75
|
+
return JSON.parse(raw);
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function findInstalled(depPattern, pkg) {
|
|
82
|
+
if (!pkg) return false;
|
|
83
|
+
const all = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
84
|
+
const parts = depPattern.split("|");
|
|
85
|
+
return parts.some((p) => all[p]);
|
|
86
|
+
}
|
|
87
|
+
|
|
41
88
|
export function detectProfile(ctx) {
|
|
42
89
|
for (const profile of PROFILES) {
|
|
43
90
|
if (profile.match(ctx)) return profile.name;
|
|
44
91
|
}
|
|
45
|
-
|
|
46
92
|
if (ctx.framework !== "unknown") {
|
|
47
93
|
return `${ctx.framework.toLowerCase()}-${ctx.database.toLowerCase()}`;
|
|
48
94
|
}
|
|
49
|
-
|
|
50
95
|
return "unknown";
|
|
51
96
|
}
|
|
52
97
|
|
|
53
98
|
export function detectProfileExtended(ctx) {
|
|
54
99
|
const profile = detectProfile(ctx);
|
|
100
|
+
const profileDef = PROFILES.find((p) => p.name === profile);
|
|
55
101
|
|
|
102
|
+
const pkg = readPkg();
|
|
56
103
|
const diStrategy = ctx.diStrategy || "manual";
|
|
57
104
|
const hasPlatform = ctx.platform?.exists || false;
|
|
58
105
|
const hasShared = ctx.shared?.exists || false;
|
|
59
106
|
const hasInfra = ctx.infra?.exists || false;
|
|
60
107
|
const platformComponents = ctx.platform?.components || [];
|
|
61
108
|
|
|
109
|
+
// Detección de librerías complementarias
|
|
110
|
+
const hasTest = pkg ? findInstalled("vitest|jest|mocha|ava", pkg) : false;
|
|
111
|
+
const hasValidation = pkg ? findInstalled("zod|joi|class-validator|typebox|yup", pkg) : false;
|
|
112
|
+
const hasLogger = pkg ? findInstalled("pino|winston|log4js", pkg) : false;
|
|
113
|
+
const hasAuth = pkg ? findInstalled("bcrypt|argon2|jsonwebtoken|jose|passport", pkg) : false;
|
|
114
|
+
const hasHelmet = pkg ? findInstalled("helmet|cors", pkg) : false;
|
|
115
|
+
const hasRateLimit = pkg ? findInstalled("express-rate-limit|rate-limiter-flexible|@fastify/rate-limit", pkg) : false;
|
|
116
|
+
const hasSwagger = pkg ? findInstalled("swagger-ui-express|@nestjs/swagger|@fastify/swagger|openapi", pkg) : false;
|
|
117
|
+
const hasEvents = pkg ? findInstalled("eventemitter3|kafkajs|amqplib|ioredis|bull|bullmq|@nestjs/event-emitter", pkg) : false;
|
|
118
|
+
|
|
119
|
+
// Sugerencias de paquetes faltantes
|
|
120
|
+
const suggestions = [];
|
|
121
|
+
if (!hasTest) suggestions.push("Agregar framework de testing: vitest o jest");
|
|
122
|
+
if (!hasValidation) suggestions.push("Agregar validación: zod o class-validator");
|
|
123
|
+
if (!hasLogger) suggestions.push("Agregar logger: pino (recomendado) o winston");
|
|
124
|
+
if (!hasAuth) suggestions.push("Agregar autenticación: bcrypt + jsonwebtoken o jose");
|
|
125
|
+
if (!hasHelmet) suggestions.push("Agregar seguridad HTTP: helmet + cors");
|
|
126
|
+
if (!hasRateLimit) suggestions.push("Agregar rate limiting: express-rate-limit o rate-limiter-flexible");
|
|
127
|
+
if (!hasSwagger) suggestions.push("Agregar documentación API: OpenAPI con zod-to-openapi o @nestjs/swagger");
|
|
128
|
+
if (!hasEvents) suggestions.push("Agregar event bus: eventemitter3 (local) o kafkajs (distribuido)");
|
|
129
|
+
|
|
130
|
+
// Validación de dependencias del perfil detectado
|
|
131
|
+
const depIssues = [];
|
|
132
|
+
if (profileDef && pkg) {
|
|
133
|
+
for (const dep of profileDef.deps) {
|
|
134
|
+
if (!findInstalled(dep, pkg)) {
|
|
135
|
+
depIssues.push(`Falta dependencia requerida: ${dep}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
62
140
|
return {
|
|
63
141
|
profile,
|
|
64
142
|
hasPlatform,
|
|
@@ -72,6 +150,18 @@ export function detectProfileExtended(ctx) {
|
|
|
72
150
|
shared: hasShared,
|
|
73
151
|
infra: hasInfra,
|
|
74
152
|
},
|
|
153
|
+
complementary: {
|
|
154
|
+
test: hasTest,
|
|
155
|
+
validation: hasValidation,
|
|
156
|
+
logger: hasLogger,
|
|
157
|
+
auth: hasAuth,
|
|
158
|
+
helmet: hasHelmet,
|
|
159
|
+
rateLimit: hasRateLimit,
|
|
160
|
+
swagger: hasSwagger,
|
|
161
|
+
events: hasEvents,
|
|
162
|
+
},
|
|
163
|
+
suggestions,
|
|
164
|
+
depIssues,
|
|
75
165
|
};
|
|
76
166
|
}
|
|
77
167
|
|
|
@@ -80,8 +170,22 @@ async function main() {
|
|
|
80
170
|
const profile = detectProfile(ctx);
|
|
81
171
|
const extended = detectProfileExtended(ctx);
|
|
82
172
|
const args = process.argv.slice(2);
|
|
173
|
+
|
|
83
174
|
if (args.includes("--extended")) {
|
|
84
175
|
console.log(JSON.stringify(extended, null, 2));
|
|
176
|
+
} else if (args.includes("--suggest")) {
|
|
177
|
+
if (extended.suggestions.length === 0 && extended.depIssues.length === 0) {
|
|
178
|
+
console.log(`✓ Perfil "${profile}" — todas las dependencias y complementos detectados`);
|
|
179
|
+
} else {
|
|
180
|
+
if (extended.depIssues.length > 0) {
|
|
181
|
+
console.log(`⚠ Perfil "${profile}" — problemas de dependencias:`);
|
|
182
|
+
extended.depIssues.forEach((d) => console.log(` ✘ ${d}`));
|
|
183
|
+
}
|
|
184
|
+
if (extended.suggestions.length > 0) {
|
|
185
|
+
console.log(`\n💡 Sugerencias para complementar "${profile}":`);
|
|
186
|
+
extended.suggestions.forEach((s, i) => console.log(` ${i + 1}. ${s}`));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
85
189
|
} else {
|
|
86
190
|
console.log(profile);
|
|
87
191
|
}
|