@ronaldjdevfs/forge 1.0.1 → 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 +3 -3
- 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,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from "child_process";
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { loadConfig, loadState } from "./forge-config.mjs";
|
|
7
|
+
|
|
8
|
+
const ROOT = process.cwd();
|
|
9
|
+
|
|
10
|
+
function gitSignals(cwd) {
|
|
11
|
+
const run = (args, { trim = true } = {}) => {
|
|
12
|
+
try {
|
|
13
|
+
const out = execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
14
|
+
return trim ? out.trim() : out;
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const branch = run(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
21
|
+
const defaultBranch = run(["symbolic-ref", "refs/remotes/origin/HEAD"])?.replace("refs/remotes/origin/", "") || "main";
|
|
22
|
+
const topLevel = run(["rev-parse", "--show-toplevel"]);
|
|
23
|
+
|
|
24
|
+
let changedFiles = [];
|
|
25
|
+
try {
|
|
26
|
+
const raw = run(["diff", "--name-only", "--diff-filter=ACMR", `${defaultBranch}...HEAD`], { trim: false });
|
|
27
|
+
if (raw) changedFiles = raw.split("\n").filter(Boolean);
|
|
28
|
+
} catch {
|
|
29
|
+
// fallback to working tree
|
|
30
|
+
try {
|
|
31
|
+
const raw = run(["status", "--porcelain"]);
|
|
32
|
+
if (raw) changedFiles = raw.split("\n").filter(Boolean).map(l => l.slice(3));
|
|
33
|
+
} catch {}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
branch: branch ?? "unknown",
|
|
38
|
+
defaultBranch,
|
|
39
|
+
changedFiles,
|
|
40
|
+
hasChanges: changedFiles.length > 0,
|
|
41
|
+
isRepo: branch !== null,
|
|
42
|
+
topLevel,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function contextSignals(cwd) {
|
|
47
|
+
const config = loadConfig();
|
|
48
|
+
const state = loadState();
|
|
49
|
+
|
|
50
|
+
const hasFeatures = existsSync(join(cwd, "src", "features"));
|
|
51
|
+
const hasPlatform = existsSync(join(cwd, "src", "platform"));
|
|
52
|
+
const hasShared = existsSync(join(cwd, "src", "shared"));
|
|
53
|
+
const hasInfra = existsSync(join(cwd, "src", "infra"));
|
|
54
|
+
const hasLegacy = existsSync(join(cwd, "src", "application")) || existsSync(join(cwd, "src", "adapters", "in", "http", "controllers"));
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
setup: {
|
|
58
|
+
hasFeatures,
|
|
59
|
+
hasPlatform,
|
|
60
|
+
hasShared,
|
|
61
|
+
hasInfra,
|
|
62
|
+
hasLegacy,
|
|
63
|
+
hasSrc: existsSync(join(cwd, "src")),
|
|
64
|
+
hasPackageJson: existsSync(join(cwd, "package.json")),
|
|
65
|
+
},
|
|
66
|
+
config: {
|
|
67
|
+
exists: !!config.profile,
|
|
68
|
+
profile: config.profile,
|
|
69
|
+
framework: config.framework,
|
|
70
|
+
needsRefresh: config.lastContextUpdate ? (Date.now() - new Date(config.lastContextUpdate).getTime()) / 86400000 > 7 : true,
|
|
71
|
+
},
|
|
72
|
+
audit: {
|
|
73
|
+
lastScore: state.lastScore,
|
|
74
|
+
lastGrade: state.lastGrade,
|
|
75
|
+
lastAudit: state.lastAudit,
|
|
76
|
+
violationCount: state.violationCount,
|
|
77
|
+
health: state.health,
|
|
78
|
+
totalFeatures: state.totalFeatures,
|
|
79
|
+
migratedFeatures: state.migratedFeatures,
|
|
80
|
+
legacyFeatures: state.legacyFeatures,
|
|
81
|
+
hasAudit: !!state.lastAudit,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function buildSignals(cwd) {
|
|
87
|
+
const signals = {};
|
|
88
|
+
|
|
89
|
+
// Check tsconfig exists
|
|
90
|
+
signals.hasTsConfig = existsSync(join(cwd, "tsconfig.json"));
|
|
91
|
+
|
|
92
|
+
// Check for common build artifacts
|
|
93
|
+
signals.hasDist = existsSync(join(cwd, "dist"));
|
|
94
|
+
signals.hasNext = existsSync(join(cwd, ".next"));
|
|
95
|
+
|
|
96
|
+
return signals;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function main() {
|
|
100
|
+
const args = process.argv.slice(2);
|
|
101
|
+
const isJson = args.includes("--json");
|
|
102
|
+
const isMinimal = args.includes("--minimal");
|
|
103
|
+
|
|
104
|
+
const git = gitSignals(ROOT);
|
|
105
|
+
const ctx = contextSignals(ROOT);
|
|
106
|
+
const build = buildSignals(ROOT);
|
|
107
|
+
|
|
108
|
+
const signals = { git, context: ctx, build };
|
|
109
|
+
|
|
110
|
+
if (isJson) {
|
|
111
|
+
console.log(JSON.stringify(signals, null, 2));
|
|
112
|
+
} else if (isMinimal) {
|
|
113
|
+
const lines = [];
|
|
114
|
+
if (git.branch) lines.push(`Branch: ${git.branch}`);
|
|
115
|
+
if (git.hasChanges) lines.push(`Cambios: ${git.changedFiles.length} archivos`);
|
|
116
|
+
if (ctx.audit.hasAudit) lines.push(`Score: ${ctx.audit.lastGrade} (${ctx.audit.lastScore})`);
|
|
117
|
+
if (!ctx.setup.hasFeatures) lines.push("⚠ Sin src/features/");
|
|
118
|
+
if (ctx.config.needsRefresh) lines.push("⚠ Config necesita refresco");
|
|
119
|
+
lines.push(`Features: ${ctx.audit.migratedFeatures} migrados + ${ctx.audit.legacyFeatures} legacy = ${ctx.audit.totalFeatures} total`);
|
|
120
|
+
console.log(lines.join(" | "));
|
|
121
|
+
} else {
|
|
122
|
+
console.log("── Forge Signals ──");
|
|
123
|
+
console.log(`Git: ${git.branch} (${git.changedFiles.length} cambios)`);
|
|
124
|
+
console.log(`Audit: ${ctx.audit.lastGrade ?? "—"} (${ctx.audit.lastScore ?? "—"}) — ${ctx.audit.health}`);
|
|
125
|
+
console.log(`Features: ${ctx.audit.totalFeatures} total (${ctx.audit.migratedFeatures} migrados, ${ctx.audit.legacyFeatures} legacy)`);
|
|
126
|
+
console.log(`Layers: platform=${ctx.setup.hasPlatform} shared=${ctx.setup.hasShared} infra=${ctx.setup.hasInfra}`);
|
|
127
|
+
console.log(`Config: ${ctx.config.profile ?? "pendiente"} | Refresh: ${ctx.config.needsRefresh ? "necesario" : "ok"}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* forge-state.mjs — Wrapper de estado post-auditoría.
|
|
5
|
+
*
|
|
6
|
+
* Delega toda la lógica de persistencia en forge-config.mjs.
|
|
7
|
+
* Existe como CLI independiente para compatibilidad con SKILL.md
|
|
8
|
+
* y comandos ad-hoc.
|
|
9
|
+
*
|
|
10
|
+
* Uso:
|
|
11
|
+
* node forge-state.mjs --show
|
|
12
|
+
* node forge-state.mjs --save <score> <grade> <violations> <features> [migrated]
|
|
13
|
+
* node forge-state.mjs --history
|
|
14
|
+
* node forge-state.mjs --json
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { loadState, saveState, saveHistory, loadHistory, displayTrend } from "./forge-config.mjs";
|
|
18
|
+
import { existsSync } from "fs";
|
|
19
|
+
import { join } from "path";
|
|
20
|
+
|
|
21
|
+
const ROOT = process.cwd();
|
|
22
|
+
|
|
23
|
+
async function main() {
|
|
24
|
+
const args = process.argv.slice(2);
|
|
25
|
+
const isJson = args.includes("--json");
|
|
26
|
+
|
|
27
|
+
if (args.includes("--show")) {
|
|
28
|
+
const state = loadState();
|
|
29
|
+
if (isJson) {
|
|
30
|
+
console.log(JSON.stringify(state, null, 2));
|
|
31
|
+
} else {
|
|
32
|
+
console.log("── Forge State ──");
|
|
33
|
+
for (const [k, v] of Object.entries(state)) {
|
|
34
|
+
console.log(` ${k}: ${v ?? "(sin datos)"}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (args.includes("--save")) {
|
|
41
|
+
const scoreIdx = args.indexOf("--save") + 1;
|
|
42
|
+
const score = parseInt(args[scoreIdx], 10);
|
|
43
|
+
const grade = args[scoreIdx + 1] || "—";
|
|
44
|
+
const violations = parseInt(args[scoreIdx + 2] || "0", 10);
|
|
45
|
+
const features = parseInt(args[scoreIdx + 3] || "0", 10);
|
|
46
|
+
const migrated = parseInt(args[scoreIdx + 4] || "0", 10);
|
|
47
|
+
const platform = args.includes("--platform");
|
|
48
|
+
|
|
49
|
+
const state = {
|
|
50
|
+
lastAudit: new Date().toISOString(),
|
|
51
|
+
lastScore: isNaN(score) ? null : score,
|
|
52
|
+
lastGrade: grade,
|
|
53
|
+
violationCount: isNaN(violations) ? 0 : violations,
|
|
54
|
+
totalFeatures: isNaN(features) ? 0 : features,
|
|
55
|
+
migratedFeatures: isNaN(migrated) ? 0 : migrated,
|
|
56
|
+
legacyFeatures: isNaN(features) ? 0 : features - (isNaN(migrated) ? 0 : migrated),
|
|
57
|
+
platformExists: platform || existsSync(join(ROOT, "src", "platform")),
|
|
58
|
+
health: score >= 80 ? "healthy" : score >= 50 ? "fair" : "poor",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
saveState(state);
|
|
62
|
+
saveHistory({ score, grade, violationCount: state.violationCount, totalFeatures: features, migratedFeatures: migrated });
|
|
63
|
+
|
|
64
|
+
if (isJson) {
|
|
65
|
+
console.log(JSON.stringify({ saved: true, state }, null, 2));
|
|
66
|
+
} else {
|
|
67
|
+
console.log(`forge-state: estado guardado (score: ${score}, grade: ${grade})`);
|
|
68
|
+
}
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (args.includes("--history")) {
|
|
73
|
+
const entries = loadHistory(isJson ? 999 : 20);
|
|
74
|
+
if (isJson) {
|
|
75
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
76
|
+
} else {
|
|
77
|
+
displayTrend(entries);
|
|
78
|
+
}
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Default: show
|
|
83
|
+
const state = loadState();
|
|
84
|
+
const history = loadHistory(1);
|
|
85
|
+
if (isJson) {
|
|
86
|
+
console.log(JSON.stringify({ state, recentHistory: history }, null, 2));
|
|
87
|
+
} else {
|
|
88
|
+
const lastScore = state.lastScore !== null ? `${state.lastScore}/110` : "—";
|
|
89
|
+
const lastGrade = state.lastGrade || "—";
|
|
90
|
+
console.log(`Forge State | Score: ${lastScore} | Grade: ${lastGrade} | Violaciones: ${state.violationCount} | Features: ${state.migratedFeatures}/${state.totalFeatures}`);
|
|
91
|
+
}
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (process.argv[1] && (process.argv[1].endsWith("forge-state.mjs") || process.argv[1].endsWith("forge-state.js"))) {
|
|
96
|
+
main().catch(console.error);
|
|
97
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
export const CYAN = "\x1b[36m";
|
|
4
|
+
export const GREEN = "\x1b[32m";
|
|
5
|
+
export const RED = "\x1b[31m";
|
|
6
|
+
export const YELLOW = "\x1b[33m";
|
|
7
|
+
export const BOLD = "\x1b[1m";
|
|
8
|
+
export const RESET = "\x1b[0m";
|
|
9
|
+
export const DIM = "\x1b[2m";
|
|
10
|
+
export const GRAY = "\x1b[90m";
|
|
11
|
+
|
|
12
|
+
export const SEVERITY_COLORS = {
|
|
13
|
+
CRITICAL: RED,
|
|
14
|
+
ERROR: RED,
|
|
15
|
+
WARNING: YELLOW,
|
|
16
|
+
INFO: CYAN,
|
|
17
|
+
SUGGESTION: GRAY,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function col(sev) {
|
|
21
|
+
return SEVERITY_COLORS[sev] || RESET;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function formatJson(data) {
|
|
25
|
+
return JSON.stringify(data, null, 2);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function formatTable(rows, columns) {
|
|
29
|
+
const colWidths = columns.map((col, i) => {
|
|
30
|
+
const header = col.label || col;
|
|
31
|
+
const maxVal = Math.max(header.length, ...rows.map(r => String(r[col.key || col] || "").length));
|
|
32
|
+
return maxVal;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const line = (parts) => {
|
|
36
|
+
const inner = parts.map((p, i) => String(p).padEnd(colWidths[i])).join(" │ ");
|
|
37
|
+
return ` ${inner} `;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const sep = () => "─" + colWidths.map(w => "─".repeat(w)).join("─┼─") + "─";
|
|
41
|
+
|
|
42
|
+
const out = [];
|
|
43
|
+
out.push(line(columns.map(c => c.label || c)));
|
|
44
|
+
out.push(sep());
|
|
45
|
+
for (const row of rows) {
|
|
46
|
+
out.push(line(columns.map(c => String(row[c.key || c] ?? ""))));
|
|
47
|
+
}
|
|
48
|
+
return out.join("\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function formatViolation({ severity, label, detail, fix }, _opts = {}) {
|
|
52
|
+
const color = col(severity);
|
|
53
|
+
const parts = [`${color}[${severity}]${RESET} ${label}`];
|
|
54
|
+
if (detail) parts.push(` ${GRAY}— ${detail}${RESET}`);
|
|
55
|
+
if (fix) parts.push(`\n ${DIM}→ Fix: ${fix}${RESET}`);
|
|
56
|
+
return parts.join("");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function formatCheck(check, opts = {}) {
|
|
60
|
+
const icon = check.pass ? `${GREEN}✔${RESET}` : `${RED}✘${RESET}`;
|
|
61
|
+
const sev = check.pass ? "" : ` ${col(check.severity)}[${check.severity}]${RESET}`;
|
|
62
|
+
const detail = check.detail ? ` ${GRAY}— ${check.detail}${RESET}` : "";
|
|
63
|
+
let out = ` ${icon}${sev} ${check.label}${detail}`;
|
|
64
|
+
if (!check.pass && check.fix && opts.showFixes !== false) {
|
|
65
|
+
out += `\n ${DIM}→ Fix: ${check.fix}${RESET}`;
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function scoreBar(score, max, barLen = 40) {
|
|
71
|
+
const p = max > 0 ? Math.round((score / max) * barLen) : 0;
|
|
72
|
+
const filled = "█".repeat(p);
|
|
73
|
+
const empty = "░".repeat(barLen - p);
|
|
74
|
+
const color = score >= max * 0.8 ? GREEN : score >= max * 0.5 ? YELLOW : RED;
|
|
75
|
+
return `${color}${filled}${RESET}${DIM}${empty}${RESET}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function formatReport(report, opts = {}) {
|
|
79
|
+
const { total, max, categories, violations, recommendations, severityCounts } = report;
|
|
80
|
+
const barLen = opts.barLen || 40;
|
|
81
|
+
const pct = max > 0 ? Math.round((total / max) * 100) : 0;
|
|
82
|
+
const lines = [];
|
|
83
|
+
|
|
84
|
+
const grade = pct >= 90 ? "A" : pct >= 80 ? "B" : pct >= 65 ? "C" : pct >= 50 ? "D" : "F";
|
|
85
|
+
const gradeColor = pct >= 80 ? GREEN : pct >= 50 ? YELLOW : RED;
|
|
86
|
+
|
|
87
|
+
lines.push(`\n${BOLD}Puntaje total: ${gradeColor}${total}/${max} (${pct}%) — ${grade}${RESET}\n`);
|
|
88
|
+
|
|
89
|
+
if (Object.keys(severityCounts || {}).length > 0) {
|
|
90
|
+
lines.push(` ${BOLD}Resumen por severidad${RESET}`);
|
|
91
|
+
for (const [sev, count] of Object.entries(severityCounts)) {
|
|
92
|
+
lines.push(` ${col(sev)}${sev}${RESET}: ${count}`);
|
|
93
|
+
}
|
|
94
|
+
lines.push("");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const [key, cat] of Object.entries(categories || {})) {
|
|
98
|
+
const cmax = opts.catMax?.[key] || cat.max || 10;
|
|
99
|
+
const name = opts.catNames?.[key] || key;
|
|
100
|
+
lines.push(` ${BOLD}${name} (${cat.score}/${cmax})${RESET}`);
|
|
101
|
+
lines.push(` ${scoreBar(cat.score, cmax, barLen)}`);
|
|
102
|
+
for (const check of cat.checks) {
|
|
103
|
+
lines.push(formatCheck(check, opts));
|
|
104
|
+
}
|
|
105
|
+
lines.push("");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (recommendations && recommendations.length > 0) {
|
|
109
|
+
lines.push(` ${BOLD}${YELLOW}Recomendaciones${RESET}`);
|
|
110
|
+
const unique = [...new Set(recommendations)];
|
|
111
|
+
unique.forEach((r, i) => lines.push(` ${i + 1}. ${r}`));
|
|
112
|
+
lines.push("");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return lines.join("\n");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function printViolations(violations) {
|
|
119
|
+
for (const v of violations) {
|
|
120
|
+
console.log(formatViolation(v));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (process.argv[1]?.endsWith("formatter.mjs")) {
|
|
125
|
+
const args = process.argv.slice(2);
|
|
126
|
+
if (args.includes("--test")) {
|
|
127
|
+
console.log(formatViolation({ severity: "ERROR", label: "Test violation", detail: "src/test.ts:42", fix: "Do something" }));
|
|
128
|
+
console.log(formatCheck({ severity: "WARNING", label: "Test check", pass: false, detail: "detail", fix: "fix it" }));
|
|
129
|
+
console.log(formatCheck({ severity: "INFO", label: "Passing check", pass: true }));
|
|
130
|
+
console.log(scoreBar(15, 20));
|
|
131
|
+
console.log(formatJson({ test: true, items: [1, 2, 3] }));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { join, relative, dirname, basename } from "path";
|
|
4
4
|
import { readFileSync, existsSync, readdirSync, statSync } from "fs";
|
|
5
|
+
import { parseImportPaths } from "./parse-imports.mjs";
|
|
5
6
|
|
|
6
7
|
const ROOT = process.cwd();
|
|
7
8
|
const SRC = join(ROOT, "src");
|
|
8
9
|
|
|
9
|
-
const IMPORT_RE = /import\s+(?:type\s+)?(?:\{[^}]*\}|[^;{]+)\s+from\s+['"]([^'"]+)['"]/g;
|
|
10
|
-
|
|
11
10
|
const INFRA_PACKAGES = {
|
|
12
11
|
"prisma": "infra:prisma",
|
|
13
12
|
"@prisma/client": "infra:prisma",
|
|
@@ -66,21 +65,6 @@ function findFiles(dir, exts, maxDepth = 6) {
|
|
|
66
65
|
return results;
|
|
67
66
|
}
|
|
68
67
|
|
|
69
|
-
function parseImports(content) {
|
|
70
|
-
const imports = [];
|
|
71
|
-
const lines = content.split("\n");
|
|
72
|
-
for (let i = 0; i < lines.length; i++) {
|
|
73
|
-
const m = lines[i].match(IMPORT_RE);
|
|
74
|
-
if (m) {
|
|
75
|
-
for (const full of m) {
|
|
76
|
-
const src = full.match(/from\s+['"]([^'"]+)['"]/);
|
|
77
|
-
if (src) imports.push(src[1]);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return imports;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
68
|
function classifyPath(relPath) {
|
|
85
69
|
const parts = relPath.split("/");
|
|
86
70
|
if (parts.includes("platform")) return "platform";
|
|
@@ -264,9 +248,9 @@ export function buildGraph(projectRoot = ROOT) {
|
|
|
264
248
|
|
|
265
249
|
/* 6a. Detect used infra packages first */
|
|
266
250
|
const usedPackages = new Set();
|
|
267
|
-
for (const { content } of fileContents) {
|
|
268
|
-
const
|
|
269
|
-
for (const imp of
|
|
251
|
+
for (const { file: filePath, content } of fileContents) {
|
|
252
|
+
const gImports = parseImportPaths(content, filePath);
|
|
253
|
+
for (const imp of gImports) {
|
|
270
254
|
for (const [pkg, id] of Object.entries(INFRA_PACKAGES)) {
|
|
271
255
|
if (imp === pkg || imp.startsWith(pkg + "/")) usedPackages.add(id);
|
|
272
256
|
}
|
|
@@ -321,7 +305,7 @@ export function buildGraph(projectRoot = ROOT) {
|
|
|
321
305
|
|
|
322
306
|
if (!sourceId || !idSet.has(sourceId)) continue;
|
|
323
307
|
|
|
324
|
-
const
|
|
308
|
+
const gImports = parseImportPaths(content, filePath);
|
|
325
309
|
const seenTargets = new Set();
|
|
326
310
|
|
|
327
311
|
for (const imp of imports) {
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* hook.mjs — Git pre-commit hook para validación arquitectónica.
|
|
5
|
+
*
|
|
6
|
+
* Instala/uninstall un hook de git pre-commit que ejecuta forge detect
|
|
7
|
+
* sobre los archivos staged y bloquea el commit si hay violaciones CRITICAL
|
|
8
|
+
* o ERROR en los archivos tocados.
|
|
9
|
+
*
|
|
10
|
+
* Uso:
|
|
11
|
+
* node hook.mjs install → Instalar hook pre-commit
|
|
12
|
+
* node hook.mjs uninstall → Eliminar hook pre-commit
|
|
13
|
+
* node hook.mjs status → Mostrar estado del hook
|
|
14
|
+
* node hook.mjs check → Ejecutar validación sobre staged files
|
|
15
|
+
* node hook.mjs ignore <rule-id> → Ignorar regla específica
|
|
16
|
+
* node hook.mjs unignore <rule-id> → Dejar de ignorar regla
|
|
17
|
+
* node hook.mjs list-ignored → Listar reglas ignoradas
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from "fs";
|
|
21
|
+
import { join, relative, resolve } from "path";
|
|
22
|
+
import { execFileSync } from "child_process";
|
|
23
|
+
|
|
24
|
+
const ROOT = process.cwd();
|
|
25
|
+
const HOOKS_DIR = join(ROOT, ".git", "hooks");
|
|
26
|
+
const HOOK_PATH = join(HOOKS_DIR, "pre-commit");
|
|
27
|
+
const IGNORE_PATH = join(ROOT, ".forge", "hooks-ignore.json");
|
|
28
|
+
const SCRIPT_DIR = new URL(".", import.meta.url).pathname;
|
|
29
|
+
|
|
30
|
+
const HOOK_TEMPLATE = `#!/bin/sh
|
|
31
|
+
# forge pre-commit hook — valida arquitectura antes de cada commit
|
|
32
|
+
# Instalado por: node .opencode/skills/forge/scripts/hook.mjs install
|
|
33
|
+
|
|
34
|
+
FORGE_SCRIPTS="${SCRIPT_DIR}"
|
|
35
|
+
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR -- "*.ts" "*.js" "*.mjs" "*.tsx" "*.jsx" 2>/dev/null)
|
|
36
|
+
|
|
37
|
+
if [ -z "$STAGED_FILES" ]; then
|
|
38
|
+
exit 0
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Ejecutar detect sobre los archivos staged
|
|
42
|
+
node "$FORGE_SCRIPTS/hook.mjs" check
|
|
43
|
+
EXIT_CODE=$?
|
|
44
|
+
|
|
45
|
+
if [ $EXIT_CODE -ne 0 ]; then
|
|
46
|
+
echo ""
|
|
47
|
+
echo "✘ [forge] Commit bloqueado por violaciones arquitectónicas."
|
|
48
|
+
echo " Para ignorar: node .opencode/skills/forge/scripts/hook.mjs ignore <rule-id>"
|
|
49
|
+
echo " Para saltar: git commit --no-verify"
|
|
50
|
+
exit 1
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
exit 0
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
function readJson(path) {
|
|
57
|
+
try { return JSON.parse(readFileSync(path, "utf-8")); } catch { return null; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function writeJson(path, data) {
|
|
61
|
+
try {
|
|
62
|
+
mkdirSync(join(path, ".."), { recursive: true });
|
|
63
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
64
|
+
return true;
|
|
65
|
+
} catch { return false; }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function shell(cmd, args, opts = {}) {
|
|
69
|
+
try {
|
|
70
|
+
return execFileSync(cmd, args, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], ...opts }).trim();
|
|
71
|
+
} catch { return ""; }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getStagedFiles() {
|
|
75
|
+
const raw = shell("git", ["diff", "--cached", "--name-only", "--diff-filter=ACMR", "--", "*.ts", "*.js", "*.mjs", "*.tsx", "*.jsx"]);
|
|
76
|
+
return raw ? raw.split("\n").filter(Boolean) : [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isGitRepo() {
|
|
80
|
+
return shell("git", ["rev-parse", "--git-dir"]) !== "";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function loadIgnored() {
|
|
84
|
+
const data = readJson(IGNORE_PATH);
|
|
85
|
+
return Array.isArray(data?.ignored) ? data.ignored : [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function saveIgnored(ignored) {
|
|
89
|
+
return writeJson(IGNORE_PATH, { ignored });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function hookInstalled() {
|
|
93
|
+
if (!existsSync(HOOK_PATH)) return false;
|
|
94
|
+
const content = readFileSync(HOOK_PATH, "utf-8");
|
|
95
|
+
return content.includes("# forge pre-commit hook");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── Actions ──
|
|
99
|
+
|
|
100
|
+
function cmdInstall() {
|
|
101
|
+
if (!isGitRepo()) {
|
|
102
|
+
console.error("forge-hooks: no es un repositorio git");
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (hookInstalled()) {
|
|
107
|
+
console.log("forge-hooks: pre-commit hook ya instalado en .git/hooks/pre-commit");
|
|
108
|
+
process.exit(0);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
mkdirSync(HOOKS_DIR, { recursive: true });
|
|
112
|
+
writeFileSync(HOOK_PATH, HOOK_TEMPLATE, { mode: 0o755 });
|
|
113
|
+
console.log("forge-hooks: pre-commit hook instalado en .git/hooks/pre-commit");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function cmdUninstall() {
|
|
117
|
+
if (!existsSync(HOOK_PATH)) {
|
|
118
|
+
console.log("forge-hooks: no hay hook pre-commit instalado");
|
|
119
|
+
process.exit(0);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
rmSync(HOOK_PATH, { force: true });
|
|
123
|
+
console.log("forge-hooks: pre-commit hook eliminado");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function cmdStatus() {
|
|
127
|
+
const installed = hookInstalled();
|
|
128
|
+
const ignored = loadIgnored();
|
|
129
|
+
console.log("── Forge Hooks ──");
|
|
130
|
+
console.log(` Pre-commit hook: ${installed ? "✓ instalado" : "✘ no instalado"}`);
|
|
131
|
+
console.log(` Reglas ignoradas: ${ignored.length > 0 ? ignored.join(", ") : "(ninguna)"}`);
|
|
132
|
+
console.log(` Hook path: ${HOOK_PATH}`);
|
|
133
|
+
console.log(` Ignore file: ${IGNORE_PATH}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function cmdCheck() {
|
|
137
|
+
if (!isGitRepo()) {
|
|
138
|
+
console.error("forge-hooks: no es un repositorio git");
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const stagedFiles = getStagedFiles();
|
|
143
|
+
if (stagedFiles.length === 0) {
|
|
144
|
+
process.exit(0);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const ignored = loadIgnored();
|
|
148
|
+
const srcFiles = stagedFiles.filter(f => f.startsWith("src/"));
|
|
149
|
+
if (srcFiles.length === 0) process.exit(0);
|
|
150
|
+
|
|
151
|
+
// Run detect only over the staged source files
|
|
152
|
+
const { allChecks, detectFeaturesOnSrc } = await import("./detect.mjs");
|
|
153
|
+
const { buildGraph } = await import("./graph.mjs");
|
|
154
|
+
const { buildContext } = await import("./context.mjs");
|
|
155
|
+
|
|
156
|
+
const ctx = await buildContext();
|
|
157
|
+
const graph = buildGraph(ROOT);
|
|
158
|
+
const features = detectFeaturesOnSrc();
|
|
159
|
+
const results = allChecks(features, graph, ctx);
|
|
160
|
+
|
|
161
|
+
let violations = [];
|
|
162
|
+
for (const [, cat] of Object.entries(results)) {
|
|
163
|
+
for (const check of cat.checks) {
|
|
164
|
+
if (!check.pass && (check.severity === "CRITICAL" || check.severity === "ERROR")) {
|
|
165
|
+
// Check if this violation touches a staged file
|
|
166
|
+
const touchesStaged = !check.detail || srcFiles.some(sf => check.detail.includes(sf) || check.label.includes(sf));
|
|
167
|
+
if (touchesStaged) {
|
|
168
|
+
// Check if rule is ignored
|
|
169
|
+
const ruleMatch = check.label.match(/\[([^\]]+)\]/);
|
|
170
|
+
const ruleId = ruleMatch ? ruleMatch[1] : null;
|
|
171
|
+
if (!ruleId || !ignored.includes(ruleId)) {
|
|
172
|
+
violations.push(check);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (violations.length > 0) {
|
|
180
|
+
console.log(`\n✘ [forge-hooks] ${violations.length} violacion(es) CRITICAL/ERROR bloquean el commit:\n`);
|
|
181
|
+
for (const v of violations) {
|
|
182
|
+
console.log(` [${v.severity}] ${v.label}`);
|
|
183
|
+
if (v.fix) console.log(` → Fix: ${v.fix}`);
|
|
184
|
+
}
|
|
185
|
+
console.log(`\n Para ignorar: node .opencode/skills/forge/scripts/hook.mjs ignore <rule-id>`);
|
|
186
|
+
console.log(` Para saltar: git commit --no-verify\n`);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
process.exit(0);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function cmdIgnore(ruleId) {
|
|
194
|
+
if (!ruleId) {
|
|
195
|
+
console.error("Uso: node hook.mjs ignore <rule-id>");
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
const ignored = loadIgnored();
|
|
199
|
+
if (ignored.includes(ruleId)) {
|
|
200
|
+
console.log(`forge-hooks: regla "${ruleId}" ya está ignorada`);
|
|
201
|
+
} else {
|
|
202
|
+
ignored.push(ruleId);
|
|
203
|
+
saveIgnored(ignored);
|
|
204
|
+
console.log(`forge-hooks: regla "${ruleId}" añadida a ignorados`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function cmdUnignore(ruleId) {
|
|
209
|
+
if (!ruleId) {
|
|
210
|
+
console.error("Uso: node hook.mjs unignore <rule-id>");
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
const ignored = loadIgnored().filter(r => r !== ruleId);
|
|
214
|
+
saveIgnored(ignored);
|
|
215
|
+
console.log(`forge-hooks: regla "${ruleId}" eliminada de ignorados`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function cmdListIgnored() {
|
|
219
|
+
const ignored = loadIgnored();
|
|
220
|
+
if (ignored.length === 0) {
|
|
221
|
+
console.log("forge-hooks: no hay reglas ignoradas");
|
|
222
|
+
} else {
|
|
223
|
+
console.log("── Reglas ignoradas ──");
|
|
224
|
+
for (const r of ignored) console.log(` ${r}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── CLI ──
|
|
229
|
+
|
|
230
|
+
const [,, action, arg] = process.argv;
|
|
231
|
+
|
|
232
|
+
switch (action) {
|
|
233
|
+
case "install": cmdInstall(); break;
|
|
234
|
+
case "uninstall": cmdUninstall(); break;
|
|
235
|
+
case "status": cmdStatus(); break;
|
|
236
|
+
case "check": await cmdCheck(); break;
|
|
237
|
+
case "ignore": cmdIgnore(arg); break;
|
|
238
|
+
case "unignore": cmdUnignore(arg); break;
|
|
239
|
+
case "list-ignored": cmdListIgnored(); break;
|
|
240
|
+
default:
|
|
241
|
+
console.log("Uso: node hook.mjs <install|uninstall|status|check|ignore|unignore|list-ignored>");
|
|
242
|
+
console.log("\n install → Instalar hook pre-commit");
|
|
243
|
+
console.log(" uninstall → Eliminar hook pre-commit");
|
|
244
|
+
console.log(" status → Mostrar estado del hook");
|
|
245
|
+
console.log(" check → Validar archivos staged");
|
|
246
|
+
console.log(" ignore <rule-id> → Ignorar regla específica");
|
|
247
|
+
console.log(" unignore <rule-id>→ Dejar de ignorar regla");
|
|
248
|
+
console.log(" list-ignored → Listar reglas ignoradas");
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|