@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,373 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * forge-api.mjs — API Contract Validation (D3).
5
+ *
6
+ * Detecta OpenAPI specs y GraphQL schemas, y verifica:
7
+ * - Endpoints documentados pero no implementados
8
+ * - Endpoints implementados pero no documentados
9
+ * - DTOs/controllers consistentes con la spec
10
+ *
11
+ * Uso:
12
+ * node forge-api.mjs → validación completa
13
+ * node forge-api.mjs --json → salida JSON
14
+ * node forge-api.mjs --openapi → solo OpenAPI
15
+ * node forge-api.mjs --graphql → solo GraphQL
16
+ * node forge-api.mjs --find-specs → solo buscar specs
17
+ */
18
+
19
+ import { readFileSync, existsSync, readdirSync, statSync } from "fs";
20
+ import { join, relative, basename, extname, dirname } from "path";
21
+
22
+ const ROOT = process.cwd();
23
+ const SRC = join(ROOT, "src");
24
+
25
+ const SEVERITY = { ERROR: "ERROR", WARNING: "WARNING", INFO: "INFO", SUGGESTION: "SUGGESTION" };
26
+
27
+ function read(path) {
28
+ try { return readFileSync(path, "utf-8"); } catch { return null; }
29
+ }
30
+
31
+ function isDir(path) {
32
+ try { return statSync(path).isDirectory(); } catch { return false; }
33
+ }
34
+
35
+ function listDir(path, recursive = false) {
36
+ try {
37
+ const entries = readdirSync(path, { withFileTypes: true });
38
+ const files = [];
39
+ for (const e of entries) {
40
+ const full = join(path, e.name);
41
+ if (e.isDirectory()) {
42
+ if (recursive) files.push(...listDir(full, true));
43
+ } else {
44
+ files.push(full);
45
+ }
46
+ }
47
+ return files;
48
+ } catch { return []; }
49
+ }
50
+
51
+ // ── Spec detection ──
52
+
53
+ function findOpenAPISpecs() {
54
+ const specs = [];
55
+ const candidates = [
56
+ join(ROOT, "openapi.yaml"), join(ROOT, "openapi.yml"), join(ROOT, "openapi.json"),
57
+ join(ROOT, "spec.yaml"), join(ROOT, "spec.yml"), join(ROOT, "spec.json"),
58
+ join(ROOT, "swagger.yaml"), join(ROOT, "swagger.yml"), join(ROOT, "swagger.json"),
59
+ join(SRC, "openapi.yaml"), join(SRC, "openapi.yml"), join(SRC, "openapi.json"),
60
+ ];
61
+
62
+ for (const c of candidates) {
63
+ if (existsSync(c)) specs.push(c);
64
+ }
65
+
66
+ // Search recursively for openapi.* or swagger.*
67
+ if (isDir(SRC)) {
68
+ const all = listDir(SRC, true);
69
+ for (const f of all) {
70
+ const name = basename(f);
71
+ if (/^openapi\.(yaml|yml|json)$/.test(name) || /^swagger\.(yaml|yml|json)$/.test(name)) {
72
+ if (!specs.includes(f)) specs.push(f);
73
+ }
74
+ }
75
+ }
76
+
77
+ return specs;
78
+ }
79
+
80
+ function findGraphQLSchemas() {
81
+ const schemas = [];
82
+ const candidates = [
83
+ join(ROOT, "schema.graphql"), join(ROOT, "schema.gql"),
84
+ join(ROOT, "graphql"), join(ROOT, "graphql"),
85
+ join(SRC, "schema.graphql"), join(SRC, "schema.gql"),
86
+ ];
87
+
88
+ for (const c of candidates) {
89
+ if (existsSync(c)) {
90
+ if (isDir(c)) {
91
+ schemas.push(...listDir(c, true).filter(f => /\.(graphql|gql)$/.test(f)));
92
+ } else {
93
+ schemas.push(c);
94
+ }
95
+ }
96
+ }
97
+
98
+ if (isDir(SRC)) {
99
+ const all = listDir(SRC, true);
100
+ for (const f of all) {
101
+ if (/\.(graphql|gql)$/.test(f) && !schemas.includes(f)) schemas.push(f);
102
+ }
103
+ }
104
+
105
+ return schemas;
106
+ }
107
+
108
+ // ── Route extraction from code ──
109
+
110
+ function extractControllerRoutes(files) {
111
+ const routes = [];
112
+ const patterns = [
113
+ /@(Get|Post|Put|Patch|Delete|Head|Options)\s*\(\s*['"]([^'"]*)['"]\s*\)/g, // NestJS
114
+ /(?:router|app)\.(get|post|put|patch|delete|head|options)\s*\(\s*['"]([^'"]*)['"]/gi, // Express/Fastify
115
+ /\.(get|post|put|patch|delete|head|options)\s*\(\s*['"]([^'"]*)['"]\s*,/gi,
116
+ /['"]method['"]\s*:\s*['"](GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)['"].*['"]path['"]\s*:\s*['"]([^'"]+)['"]/gis, // config-based
117
+ ];
118
+
119
+ for (const f of files) {
120
+ const content = read(f);
121
+ if (!content) continue;
122
+ for (const re of patterns) {
123
+ re.lastIndex = 0;
124
+ let m;
125
+ while ((m = re.exec(content)) !== null) {
126
+ const method = m[1].toUpperCase();
127
+ let path = m[2] || "/";
128
+ if (path && !path.startsWith("/")) path = "/" + path;
129
+ routes.push({ method, path: path.replace(/\/\//g, "/"), file: relative(ROOT, f) });
130
+ }
131
+ }
132
+ }
133
+ return routes;
134
+ }
135
+
136
+ // ── OpenAPI route extraction ──
137
+
138
+ function parseOpenAPIPaths(content) {
139
+ const paths = [];
140
+ try {
141
+ const spec = JSON.parse(content);
142
+ const p = spec.paths || {};
143
+ for (const [path, methods] of Object.entries(p)) {
144
+ for (const [method, def] of Object.entries(methods)) {
145
+ if (["get", "post", "put", "patch", "delete", "head", "options"].includes(method)) {
146
+ paths.push({ method: method.toUpperCase(), path, summary: def.summary || def.operationId || "" });
147
+ }
148
+ }
149
+ }
150
+ } catch {
151
+ // Try YAML-like parsing (simple key extraction)
152
+ const methodRe = /^\s{4}(get|post|put|patch|delete|head|options):/gmi;
153
+ const pathRe = /^\s{2}\/([^:]+):/gm;
154
+ let m;
155
+ let lastPath = "";
156
+ while ((m = pathRe.exec(content)) !== null) {
157
+ lastPath = "/" + m[1];
158
+ }
159
+ while ((m = methodRe.exec(content)) !== null) {
160
+ if (lastPath) paths.push({ method: m[1].toUpperCase(), path: lastPath, summary: "" });
161
+ }
162
+ }
163
+ return paths;
164
+ }
165
+
166
+ // ── GraphQL extraction ──
167
+
168
+ function parseGraphQLSchema(content) {
169
+ const types = [];
170
+ const queryRe = /type\s+Query\s*\{([^}]+)\}/g;
171
+ const mutationRe = /type\s+Mutation\s*\{([^}]+)\}/g;
172
+
173
+ let m;
174
+ while ((m = queryRe.exec(content)) !== null) {
175
+ for (const line of m[1].split("\n")) {
176
+ const trimmed = line.trim();
177
+ if (trimmed && !trimmed.startsWith("#") && !trimmed.startsWith("//")) {
178
+ const name = trimmed.split(/[(:]/)[0].trim();
179
+ if (name) types.push({ type: "Query", name });
180
+ }
181
+ }
182
+ }
183
+ while ((m = mutationRe.exec(content)) !== null) {
184
+ for (const line of m[1].split("\n")) {
185
+ const trimmed = line.trim();
186
+ if (trimmed && !trimmed.startsWith("#") && !trimmed.startsWith("//")) {
187
+ const name = trimmed.split(/[(:]/)[0].trim();
188
+ if (name) types.push({ type: "Mutation", name });
189
+ }
190
+ }
191
+ }
192
+ return types;
193
+ }
194
+
195
+ function extractGraphQLResolvers(files) {
196
+ const resolvers = [];
197
+ const patterns = [
198
+ /['"]?(Query|Mutation)['"]?\s*:\s*\{([^}]+)\}/gs,
199
+ /@Resolver\s*\(/g,
200
+ /@(Query|Mutation)\s*\(\s*['"]([^'"]*)['"]\s*\)/g,
201
+ ];
202
+
203
+ for (const f of files) {
204
+ const content = read(f);
205
+ if (!content) continue;
206
+
207
+ // Resolver map pattern
208
+ let m;
209
+ while ((m = patterns[0].exec(content)) !== null) {
210
+ const type = m[1];
211
+ const body = m[2];
212
+ for (const line of body.split(",")) {
213
+ const name = line.trim().split(":")[0]?.trim().replace(/['"]/g, "");
214
+ if (name && !name.includes(" ")) resolvers.push({ type, name, file: relative(ROOT, f) });
215
+ }
216
+ }
217
+
218
+ // Decorator pattern
219
+ patterns[2].lastIndex = 0;
220
+ while ((m = patterns[2].exec(content)) !== null) {
221
+ resolvers.push({ type: m[1], name: m[2], file: relative(ROOT, f) });
222
+ }
223
+ }
224
+
225
+ return resolvers;
226
+ }
227
+
228
+ // ── Main validation ──
229
+
230
+ export function validateAPI() {
231
+ const results = { specs: [], openapi: { documented: [], implemented: [], onlyDoc: [], onlyCode: [] }, graphql: { documented: [], implemented: [], onlyDoc: [], onlyCode: [] }, errors: [] };
232
+
233
+ // Find specs
234
+ const openapiSpecs = findOpenAPISpecs();
235
+ const graphqlSchemas = findGraphQLSchemas();
236
+ results.specs = { openapi: openapiSpecs, graphql: graphqlSchemas };
237
+
238
+ // Guard clause: sin specs, no hay nada que validar
239
+ if (openapiSpecs.length === 0 && graphqlSchemas.length === 0) {
240
+ return results;
241
+ }
242
+
243
+ // Find controllers and resolvers (costoso, solo si hay specs)
244
+ const srcFiles = isDir(SRC) ? listDir(SRC, true).filter(f => /\.(ts|js)$/.test(f)) : [];
245
+ const controllerRoutes = extractControllerRoutes(srcFiles);
246
+ const graphqlResolvers = extractGraphQLResolvers(srcFiles);
247
+
248
+ // Validate OpenAPI
249
+ for (const specPath of openapiSpecs) {
250
+ const content = read(specPath);
251
+ if (!content) continue;
252
+ const docPaths = parseOpenAPIPaths(content);
253
+ results.openapi.documented = docPaths;
254
+
255
+ for (const dp of docPaths) {
256
+ const found = controllerRoutes.some(cr => cr.method === dp.method && (cr.path === dp.path || cr.path.endsWith(dp.path) || dp.path.endsWith(cr.path)));
257
+ if (!found) {
258
+ results.openapi.onlyDoc.push({ method: dp.method, path: dp.path, spec: relative(ROOT, specPath) });
259
+ }
260
+ }
261
+
262
+ for (const cr of controllerRoutes) {
263
+ const found = docPaths.some(dp => dp.method === cr.method && (dp.path === cr.path || dp.path.endsWith(cr.path) || cr.path.endsWith(dp.path)));
264
+ if (!found) {
265
+ results.openapi.onlyCode.push(cr);
266
+ }
267
+ }
268
+ }
269
+
270
+ // Validate GraphQL
271
+ for (const schemaPath of graphqlSchemas) {
272
+ const content = read(schemaPath);
273
+ if (!content) continue;
274
+ const types = parseGraphQLSchema(content);
275
+ results.graphql.documented = types;
276
+
277
+ for (const t of types) {
278
+ const found = graphqlResolvers.some(r => r.type === t.type && r.name === t.name);
279
+ if (!found) {
280
+ results.graphql.onlyDoc.push(t);
281
+ }
282
+ }
283
+
284
+ const seen = new Set();
285
+ for (const r of graphqlResolvers) {
286
+ const key = `${r.type}:${r.name}`;
287
+ if (seen.has(key)) continue;
288
+ seen.add(key);
289
+ const found = types.some(t => t.type === r.type && t.name === r.name);
290
+ if (!found) {
291
+ results.graphql.onlyCode.push(r);
292
+ }
293
+ }
294
+ }
295
+
296
+ results.errors = [
297
+ ...results.openapi.onlyDoc.map(d => `[DOCUMENTADO] ${d.method} ${d.path} — sin implementación`),
298
+ ...results.openapi.onlyCode.map(c => `[NO DOC] ${c.method} ${c.path} (${c.file})`),
299
+ ...results.graphql.onlyDoc.map(d => `[DOCUMENTADO] ${d.type}.${d.name} — sin resolver`),
300
+ ...results.graphql.onlyCode.map(r => `[NO DOC] ${r.type}.${r.name} (${r.file})`),
301
+ ];
302
+
303
+ return results;
304
+ }
305
+
306
+ function printReport(results) {
307
+ console.log("\n═══ API Contract Validation ═══\n");
308
+
309
+ console.log("── Specs encontrados ──");
310
+ if (results.specs.openapi.length > 0) {
311
+ for (const s of results.specs.openapi) console.log(` OpenAPI: ${relative(ROOT, s)}`);
312
+ } else {
313
+ console.log(" OpenAPI: (ninguno)");
314
+ }
315
+ if (results.specs.graphql.length > 0) {
316
+ for (const s of results.specs.graphql) console.log(` GraphQL: ${relative(ROOT, s)}`);
317
+ } else {
318
+ console.log(" GraphQL: (ninguno)");
319
+ }
320
+
321
+ if (results.errors.length === 0) {
322
+ console.log(`\n${"✓ Sin discrepancias — API contracts OK"}`);
323
+ return;
324
+ }
325
+
326
+ console.log(`\n── Discrepancias (${results.errors.length}) ──\n`);
327
+ for (const err of results.errors) {
328
+ const type = err.startsWith("[DOCUMENTADO]") ? "WARNING" : err.startsWith("[NO DOC]") ? "ERROR" : "INFO";
329
+ const icon = type === "ERROR" ? "✘" : type === "WARNING" ? "⚠" : "ℹ";
330
+ console.log(` ${icon} [${type}] ${err}`);
331
+ }
332
+
333
+ console.log(`\nTotal: ${results.errors.length} discrepancia(s)`);
334
+ if (results.openapi.onlyDoc.length > 0)
335
+ console.log(` ${results.openapi.onlyDoc.length} endpoint(s) documentados sin implementar`);
336
+ if (results.openapi.onlyCode.length > 0)
337
+ console.log(` ${results.openapi.onlyCode.length} endpoint(s) implementados sin documentar`);
338
+ if (results.graphql.onlyDoc.length > 0)
339
+ console.log(` ${results.graphql.onlyDoc.length} resolver(s) documentados sin implementar`);
340
+ if (results.graphql.onlyCode.length > 0)
341
+ console.log(` ${results.graphql.onlyCode.length} resolver(s) implementados sin documentar`);
342
+ }
343
+
344
+ async function main() {
345
+ const isJson = process.argv.includes("--json");
346
+ const onlyOpenAPI = process.argv.includes("--openapi");
347
+ const onlyGraphQL = process.argv.includes("--graphql");
348
+ const findSpecs = process.argv.includes("--find-specs");
349
+
350
+ if (findSpecs) {
351
+ const openapi = findOpenAPISpecs();
352
+ const graphql = findGraphQLSchemas();
353
+ if (isJson) {
354
+ console.log(JSON.stringify({ openapi: openapi.map(f => relative(ROOT, f)), graphql: graphql.map(f => relative(ROOT, f)) }, null, 2));
355
+ } else {
356
+ console.log("OpenAPI specs:"); openapi.forEach(f => console.log(` ${relative(ROOT, f)}`));
357
+ console.log("GraphQL schemas:"); graphql.forEach(f => console.log(` ${relative(ROOT, f)}`));
358
+ }
359
+ process.exit(0);
360
+ }
361
+
362
+ const results = validateAPI();
363
+ if (isJson) {
364
+ console.log(JSON.stringify(results, null, 2));
365
+ } else {
366
+ printReport(results);
367
+ }
368
+ process.exit(results.errors.length > 0 ? 1 : 0);
369
+ }
370
+
371
+ if (process.argv[1] && (process.argv[1].endsWith("forge-api.mjs") || process.argv[1].endsWith("forge-api.js"))) {
372
+ main().catch(console.error);
373
+ }
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "fs";
4
+ import { join, dirname } from "path";
5
+
6
+ const ROOT = process.cwd();
7
+ const CONFIG_DIR = join(ROOT, ".forge");
8
+ const CONFIG_PATH = join(CONFIG_DIR, "config.json");
9
+ const STATE_PATH = join(CONFIG_DIR, "state.json");
10
+ const HISTORY_DIR = join(CONFIG_DIR, "history");
11
+
12
+ const DEFAULT_CONFIG = {
13
+ profile: null,
14
+ framework: null,
15
+ database: null,
16
+ orm: null,
17
+ diStrategy: null,
18
+ runtime: null,
19
+ ignorePaths: ["src/generated", "src/__tests__/fixtures", "src/**/*.spec.ts", "src/**/*.test.ts"],
20
+ lastContextUpdate: null,
21
+ };
22
+
23
+ const DEFAULT_STATE = {
24
+ lastAudit: null,
25
+ lastScore: null,
26
+ lastGrade: null,
27
+ violationCount: 0,
28
+ totalFeatures: 0,
29
+ migratedFeatures: 0,
30
+ legacyFeatures: 0,
31
+ platformExists: false,
32
+ health: "unknown",
33
+ };
34
+
35
+ function readJson(path) {
36
+ try {
37
+ return JSON.parse(readFileSync(path, "utf-8"));
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ function writeJson(path, data) {
44
+ try {
45
+ mkdirSync(dirname(path), { recursive: true });
46
+ writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
47
+ return true;
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
52
+
53
+ export function loadConfig() {
54
+ const config = readJson(CONFIG_PATH);
55
+ if (!config) {
56
+ return { ...DEFAULT_CONFIG };
57
+ }
58
+ return { ...DEFAULT_CONFIG, ...config };
59
+ }
60
+
61
+ export function saveConfig(config) {
62
+ return writeJson(CONFIG_PATH, config);
63
+ }
64
+
65
+ export function loadState() {
66
+ const state = readJson(STATE_PATH);
67
+ if (!state) {
68
+ return { ...DEFAULT_STATE };
69
+ }
70
+ return { ...DEFAULT_STATE, ...state };
71
+ }
72
+
73
+ export function saveState(state) {
74
+ return writeJson(STATE_PATH, state);
75
+ }
76
+
77
+ export function updateStateFromAudit(auditResult) {
78
+ const state = loadState();
79
+ state.lastAudit = new Date().toISOString();
80
+ state.lastScore = auditResult.total || 0;
81
+ state.lastGrade = auditResult.grade || "F";
82
+ state.violationCount = (auditResult.violations || []).length;
83
+ state.health = auditResult.health || "unknown";
84
+ if (auditResult.context) {
85
+ state.totalFeatures = auditResult.context.features?.total || 0;
86
+ state.migratedFeatures = auditResult.context.features?.migrated?.length || 0;
87
+ state.legacyFeatures = auditResult.context.features?.legacy?.length || 0;
88
+ }
89
+ return saveState(state);
90
+ }
91
+
92
+ export function saveHistory(snapshot) {
93
+ mkdirSync(HISTORY_DIR, { recursive: true });
94
+ const ts = new Date().toISOString().replace(/[:.]/g, "-");
95
+ const path = join(HISTORY_DIR, `audit-${ts}.json`);
96
+ return writeJson(path, { ...snapshot, timestamp: new Date().toISOString() });
97
+ }
98
+
99
+ export function loadHistory(limit = 20) {
100
+ mkdirSync(HISTORY_DIR, { recursive: true });
101
+ try {
102
+ const files = readdirSync(HISTORY_DIR)
103
+ .filter(f => f.startsWith("audit-") && f.endsWith(".json"))
104
+ .sort()
105
+ .slice(-limit);
106
+ return files.map(f => readJson(join(HISTORY_DIR, f))).filter(Boolean);
107
+ } catch {
108
+ return [];
109
+ }
110
+ }
111
+
112
+ export function displayTrend(entries) {
113
+ if (entries.length === 0) return console.log("No hay historial de auditorías.");
114
+ console.log("\n── Forge Trend ──\n");
115
+ console.log(`${"Fecha".padEnd(22)} ${"Score".padEnd(6)} ${"Grade".padEnd(6)} ${"Violaciones".padEnd(12)} ${"Features"}`);
116
+ console.log("─".repeat(60));
117
+ for (const e of entries) {
118
+ const date = (e.timestamp || "").slice(0, 19).replace("T", " ");
119
+ const score = e.score !== undefined ? `${e.score}/110` : "—";
120
+ const grade = e.grade || "—";
121
+ const violations = e.violationCount !== undefined ? String(e.violationCount) : "—";
122
+ const features = e.totalFeatures !== undefined ? `${e.migratedFeatures || 0}/${e.totalFeatures}` : "—";
123
+ console.log(`${date.padEnd(22)} ${score.padEnd(6)} ${grade.padEnd(6)} ${violations.padEnd(12)} ${features}`);
124
+ }
125
+ if (entries.length >= 2) {
126
+ const first = entries[0];
127
+ const last = entries[entries.length - 1];
128
+ const delta = (last.score || 0) - (first.score || 0);
129
+ console.log("─".repeat(60));
130
+ console.log(`Δ total: ${delta > 0 ? "+" : ""}${delta} puntos (${delta > 0 ? "mejora" : delta < 0 ? "empeora" : "estable"})`);
131
+ }
132
+ }
133
+
134
+ export function updateConfigFromContext(ctx) {
135
+ const config = loadConfig();
136
+ let changed = false;
137
+
138
+ if (ctx.framework && ctx.framework !== "unknown" && ctx.framework !== config.framework) {
139
+ config.framework = ctx.framework;
140
+ changed = true;
141
+ }
142
+ if (ctx.database && ctx.database !== "unknown" && ctx.database !== config.database) {
143
+ config.database = ctx.database;
144
+ changed = true;
145
+ }
146
+ if (ctx.orm && ctx.orm !== "none" && ctx.orm !== config.orm) {
147
+ config.orm = ctx.orm;
148
+ changed = true;
149
+ }
150
+ if (ctx.diStrategy && ctx.diStrategy !== "manual" && ctx.diStrategy !== config.diStrategy) {
151
+ config.diStrategy = ctx.diStrategy;
152
+ changed = true;
153
+ }
154
+ if (ctx.runtime && ctx.runtime !== config.runtime) {
155
+ config.runtime = ctx.runtime;
156
+ changed = true;
157
+ }
158
+ if (ctx.profile && ctx.profile !== config.profile) {
159
+ config.profile = ctx.profile;
160
+ changed = true;
161
+ }
162
+
163
+ if (changed) {
164
+ config.lastContextUpdate = new Date().toISOString();
165
+ return saveConfig(config);
166
+ }
167
+ return true;
168
+ }
169
+
170
+ export function configNeedsRefresh(config) {
171
+ if (!config.lastContextUpdate) return true;
172
+ const daysSinceUpdate = (Date.now() - new Date(config.lastContextUpdate).getTime()) / 86400000;
173
+ return daysSinceUpdate > 7;
174
+ }
175
+
176
+ async function main() {
177
+ const args = process.argv.slice(2);
178
+ const isJson = args.includes("--json");
179
+ const action = args.find(a => a === "--init" || a === "--update" || a === "--show" || a === "--clear-state" || a === "--history" || a === "--trend");
180
+
181
+ if (action === "--init") {
182
+ if (existsSync(CONFIG_PATH)) {
183
+ console.log("forge-config: config.json ya existe en .forge/");
184
+ process.exit(0);
185
+ }
186
+ const ok = writeJson(CONFIG_PATH, DEFAULT_CONFIG);
187
+ writeJson(STATE_PATH, DEFAULT_STATE);
188
+ console.log(`forge-config: config.json ${ok ? "creado" : "ERROR"} en .forge/`);
189
+ process.exit(ok ? 0 : 1);
190
+ }
191
+
192
+ if (action === "--show") {
193
+ const config = loadConfig();
194
+ const state = loadState();
195
+ if (isJson) {
196
+ console.log(JSON.stringify({ config, state }, null, 2));
197
+ } else {
198
+ console.log("── Forge Config ──");
199
+ for (const [k, v] of Object.entries(config)) {
200
+ console.log(` ${k}: ${v ?? "(sin detectar)"}`);
201
+ }
202
+ console.log("\n── Forge State ──");
203
+ for (const [k, v] of Object.entries(state)) {
204
+ console.log(` ${k}: ${v ?? "(sin datos)"}`);
205
+ }
206
+ }
207
+ process.exit(0);
208
+ }
209
+
210
+ if (action === "--clear-state") {
211
+ writeJson(STATE_PATH, DEFAULT_STATE);
212
+ console.log("forge-config: state.json limpiado");
213
+ process.exit(0);
214
+ }
215
+
216
+ if (action === "--history") {
217
+ const entries = loadHistory(isJson ? 999 : 20);
218
+ if (isJson) {
219
+ console.log(JSON.stringify(entries, null, 2));
220
+ } else {
221
+ displayTrend(entries);
222
+ }
223
+ process.exit(0);
224
+ }
225
+
226
+ if (action === "--trend") {
227
+ const entries = loadHistory(20);
228
+ if (isJson) {
229
+ console.log(JSON.stringify(entries.map(e => ({ timestamp: e.timestamp, score: e.score, grade: e.grade })), null, 2));
230
+ } else {
231
+ displayTrend(entries);
232
+ }
233
+ process.exit(0);
234
+ }
235
+
236
+ if (action === "--update") {
237
+ const { buildContext } = await import("./context.mjs");
238
+ const { detectProfile } = await import("./profile.mjs");
239
+ const ctx = await buildContext();
240
+ const profile = detectProfile(ctx);
241
+ ctx.profile = profile;
242
+ updateConfigFromContext(ctx);
243
+ console.log("forge-config: config.json actualizado desde context");
244
+ process.exit(0);
245
+ }
246
+
247
+ // Default: print status
248
+ const config = loadConfig();
249
+ const state = loadState();
250
+ const needsRefresh = configNeedsRefresh(config);
251
+ const summary = {
252
+ configExists: existsSync(CONFIG_PATH),
253
+ stateExists: existsSync(STATE_PATH),
254
+ needsRefresh,
255
+ config,
256
+ state,
257
+ };
258
+ if (isJson) {
259
+ console.log(JSON.stringify(summary, null, 2));
260
+ } else {
261
+ console.log(`Config: ${summary.configExists ? "✓" : "✗"} | State: ${summary.stateExists ? "✓" : "✗"} | Refresh needed: ${needsRefresh}`);
262
+ }
263
+ process.exit(0);
264
+ }
265
+
266
+ if (process.argv[1] && (process.argv[1].endsWith("forge-config.mjs") || process.argv[1].endsWith("forge-config.js"))) {
267
+ main().catch(console.error);
268
+ }