@ronaldjdevfs/forge 1.2.0 → 1.3.0-beta

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 CHANGED
@@ -1,5 +1,7 @@
1
1
  <img src="favicon.svg" alt="Forge Logo" width="100" height="100">
2
2
 
3
+ > **v1.3.0-beta** — Multi-Agent: OpenCode · Claude Code · Cursor · Codex CLI · Gemini
4
+
3
5
  ## Forge — Backend Architecture Operating System
4
6
 
5
7
  **Forge** es un **sistema operativo arquitectónico** para backend. Modela, construye, audita, protege y evoluciona sistemas completos en cuatro dominios arquitectónicos: **Platform**, **Features**, **Shared** e **Infrastructure**.
@@ -356,7 +358,11 @@ Donde vive toda la inteligencia arquitectónica:
356
358
  | `scripts/formatter.mjs` | Output unificado: colores, JSON, scoreBar, formatCheck |
357
359
  | `scripts/registry/rules.mjs` | Registry de reglas R1-R9 + custom rules desacoplado |
358
360
  | `scripts/assay.mjs` | Ensayo multi-persona (Bezos, Fowler, Hacker, PM, Arquitecta) |
359
- | `scripts/posttool.mjs` | PostToolUse hook con `--reminder` y `--strict` |
361
+ | `scripts/forgeSentinel.mjs` | PostToolUse hook adapter para Claude/Codex/agents |
362
+ | `scripts/forgeSentinel-lib.mjs` | Lógica compartida del hook PostToolUse |
363
+ | `scripts/forgeSmith.mjs` | preToolUse gate para Cursor (deniega escrituras con violaciones CRITICAL/ERROR) |
364
+ | `scripts/forgeSmith-admin.mjs` | Gestión de hooks (on/off/status) para Cursor |
365
+ | `scripts/posttool.mjs` | PostToolUse hook (deprecated — usar forgeSentinel) |
360
366
  | `scripts/forge-config.mjs` | Persistencia de config, estado e histórico |
361
367
  | `scripts/forge-state.mjs` | CLI wrapper de estado post-auditoría |
362
368
  | `scripts/forge-signals.mjs` | Manejo de señales (SIGINT, SIGTERM) |
@@ -381,29 +387,22 @@ Donde vive toda la inteligencia arquitectónica:
381
387
 
382
388
  ## Instalación
383
389
 
384
- ### En un proyecto
385
-
386
390
  ```bash
387
- npx @ronaldjdev/forge install
391
+ npx @ronaldjdevfs/forge install # Wizard interactivo
392
+ npx @ronaldjdevfs/forge install --all # Todos los agentes detectados
393
+ npx @ronaldjdevfs/forge install --cursor # Solo Cursor
394
+ npx @ronaldjdevfs/forge install --claude # Solo Claude Code
388
395
  ```
389
396
 
390
- Esto copia la skill en `.opencode/skills/forge/` del proyecto actual.
391
-
392
- ### Global (disponible en todos los proyectos)
393
-
394
- ```bash
395
- npx @ronaldjdev/forge install --global
396
- ```
397
-
398
- Esto copia la skill en `~/.config/opencode/skills/forge/`.
399
-
400
- ### Con instalación global del CLI
401
-
402
- ```bash
403
- npm install -g @ronaldjdev/forge
404
- forge install # proyecto actual
405
- forge install -g # global
406
- ```
397
+ | Flag | Agente | Hook activo |
398
+ |------|--------|-------------|
399
+ | `--opencode` | OpenCode | forgeSentinel (vía SKILL.md) |
400
+ | `--cursor` | Cursor | forgeSmith (preToolUse) |
401
+ | `--claude` | Claude Code | forgeSentinel (PostToolUse) |
402
+ | `--codex` | Codex CLI | forgeSentinel (PostToolUse) |
403
+ | `--gemini` | Gemini Code Assist | — |
404
+ | `--all` | Todos los anteriores | — |
405
+ | `--global` | `~/.config/opencode/` | forgeSentinel |
407
406
 
408
407
  **Requisitos**: Node.js ≥ 18
409
408
 
@@ -436,6 +435,22 @@ Una vez instalada, OpenCode carga automáticamente la skill `forge` al trabajar
436
435
 
437
436
  ---
438
437
 
438
+ ### Sistema de Hooks Multi-Agent
439
+
440
+ Forge se despliega como **skill** en múltiples agentes de IA simultáneamente, con hooks adaptados a cada plataforma:
441
+
442
+ | Agente | Hook | Cuándo se ejecuta | Efecto |
443
+ |--------|------|-------------------|--------|
444
+ | **OpenCode** | forgeSentinel | PostToolUse tras Edit/Write | Reporta violaciones como reminder |
445
+ | **Claude Code** | forgeSentinel | PostToolUse tras Edit/Write/MultiEdit | Reporta violaciones como reminder |
446
+ | **Cursor** | forgeSmith | preToolUse antes de cada escritura | Puede DENEGAR la escritura |
447
+ | **Codex CLI** | forgeSentinel | PostToolUse tras Edit/Write/apply_patch | Reporta violaciones como reminder |
448
+ | **Gemini** | SKILL.md | Al cargar el agente | Instrucciones arquitectónicas |
449
+
450
+ Todos los hooks comparten la misma lógica de detección de violaciones R1-R9 a través de `forgeSentinel-lib.mjs`.
451
+
452
+ ---
453
+
439
454
  ## Desarrollo
440
455
 
441
456
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ronaldjdevfs/forge",
3
- "version": "1.2.0",
3
+ "version": "1.3.0-beta",
4
4
  "description": "Forge — Architecture Operating System for backend systems. Arquitectura Hexagonal, DDD pragmático y vertical slices.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,12 @@
13
13
  "NOTICE"
14
14
  ],
15
15
  "scripts": {
16
- "install:local": "node ./src/cli.js install"
16
+ "install:local": "node ./src/cli.js install",
17
+ "prepublishOnly": "node -e \"const p=require('./package.json'); if(p.dependencies && p.dependencies[p.name]) { console.error('ERROR: self-referencing dependency detected. Remove before publish.'); process.exit(1); }\"",
18
+ "test:forgeSentinel": "node .opencode/skills/forge/scripts/forgeSentinel.mjs --reminder",
19
+ "test:forgeSmith": "echo '{\"toolName\":\"Write\",\"args\":{\"filePath\":\"src/test.ts\"}}' | node .opencode/skills/forge/scripts/forgeSmith.mjs",
20
+ "test:pin": "node .opencode/skills/forge/scripts/pin.mjs nail cast",
21
+ "test:all": "pnpm install:local && pnpm test:forgeSentinel && pnpm test:forgeSmith"
17
22
  },
18
23
  "keywords": [
19
24
  "opencode",
@@ -82,7 +82,8 @@ ANTES de cualquier acción, Forge DEBE ejecutar esta secuencia. Si no lo haces,
82
82
  6. **inspect.mjs** — Auditoría completa con ownership + platform
83
83
  7. **architecture.mjs** — Generar/actualizar ARCHITECTURE.md
84
84
  8. **Ejecutar comando solicitado** — cast, quench, temper, etc.
85
- 9. **Actualizar ARCHITECTURE.md** — Reflejar nuevo estado
85
+ 9. **forgeSentinel** — Verificar cambios tras escritura (`scripts/forgeSentinel.mjs --reminder`)
86
+ 10. **Actualizar ARCHITECTURE.md** — Reflejar nuevo estado
86
87
 
87
88
  ```bash
88
89
  # Template de setup que debes ejecutar:
@@ -133,8 +134,9 @@ Para cada comando, Forge sigue este flujo:
133
134
  4. **Referencia**: Cargar `reference/<command>.md`
134
135
  5. **Ejecutar**: Aplicar el flujo definido en la referencia, usando los scripts según corresponda
135
136
  6. **Verificar**: Ejecutar `scripts/detect.mjs` para verificar que no se introdujeron violaciones
136
- 7. **Actualizar ARCHITECTURE.md**: Reflejar el nuevo estado (`architecture.mjs`)
137
- 8. **Reportar**: Mostrar resultado al usuario con severidades
137
+ 7. **forgeSentinel**: Ejecutar `scripts/forgeSentinel.mjs --reminder` para reportar cambios
138
+ 8. **Actualizar ARCHITECTURE.md**: Reflejar el nuevo estado (`architecture.mjs`)
139
+ 9. **Reportar**: Mostrar resultado al usuario con severidades
138
140
 
139
141
  ---
140
142
 
@@ -235,10 +237,13 @@ El agente DEBE leer este archivo al inicio de cada interacción y actualizarlo a
235
237
  | `reference/help.md` | Lista completa de comandos y flags de Forge |
236
238
  | `reference/assay.md` | Ensayo arquitectónico multi-persona — interpretación cualitativa del audit |
237
239
  | `profiles/` | Perfiles tecnológicos detallados (Express, Fastify, NestJS, etc.) |
238
- | `scripts/` | Scripts: context, detect, inspect, chain, profile, graph, architecture, armorer, bootstrap, forge-config, forge-signals, forge-state, forge-api, pin, update, rollback, hook, posttool, formatter, assay, registry/rules |
240
+ | `scripts/` | Scripts: context, detect, inspect, chain, profile, graph, architecture, armorer, bootstrap, forge-config, forge-signals, forge-state, forge-api, pin, update, rollback, hook, forgeSentinel, forgeSentinel-lib, forgeSmith, forgeSmith-admin, formatter, assay, registry/rules |
239
241
  | `scripts/registry/rules.mjs` | Anti-pattern rule registry (R1-R9 + custom rules desacopladas de detect.mjs) |
240
242
  | `scripts/formatter.mjs` | Output formatter unificado (JSON, tabla, severidad coloreada, scoreBar, formatCheck, formatViolation) |
241
- | `scripts/posttool.mjs` | PostToolUse hook — analiza archivos modificados tras escritura y reporta violaciones |
243
+ | `scripts/forgeSentinel.mjs` | PostToolUse hook — analiza archivos modificados tras escritura y reporta violaciones |
244
+ | `scripts/forgeSentinel-lib.mjs` | Lógica compartida para hooks forgeSentinel y forgeSmith |
245
+ | `scripts/forgeSmith.mjs` | preToolUse gate — previene escrituras con violaciones CRITICAL/ERROR (Cursor) |
246
+ | `scripts/forgeSmith-admin.mjs` | Gestión de hooks (on/off/status) |
242
247
  | `scripts/assay.mjs` | Motor de ensayo arquitectónico multi-persona (Bezos, Fowler, Hacker, PM, Arquitecta Senior) |
243
248
  | `templates/feature/` | Templates de feature (entity, repository, uc, controller, routes, schema, mapper) |
244
249
  | `templates/platform/` | Templates de platform (config, server, database, logger, http, di) |
@@ -264,7 +269,7 @@ node --test .opencode/skills/forge/tests/core.test.mjs
264
269
  | `formatter.mjs` | 4 | Output format, colores, JSON |
265
270
  | `registry/rules.mjs` | 4 | R1-R9, evaluación, custom rules |
266
271
  | `detect.mjs` (inline ignores) | 5 | parseInlineIgnores, isIgnored |
267
- | `posttool.mjs` | 1 | PostToolUse hook |
272
+ | `forgeSentinel.mjs` | 1 | PostToolUse hook |
268
273
  | `assay.mjs` | 4 | Personas, generateAssay, opiniones |
269
274
 
270
275
  ### Flags adicionales
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
4
+ import { join } from "path";
5
+ import { buildGraph } from "./graph.mjs";
6
+ import { buildContext } from "./context.mjs";
7
+ import { detectFeaturesOnSrc, allChecks, loadAllInlineIgnores, isIgnored } from "./detect.mjs";
8
+ import { evaluateRules } from "./registry/rules.mjs";
9
+
10
+ const ROOT = process.cwd();
11
+
12
+ export async function runSentinelCheck(files, opts = {}) {
13
+ const { strict = false, reminder = false } = opts;
14
+
15
+ if (!files || files.length === 0) {
16
+ return { violations: [], graphViolations: [], total: 0, hasCritical: false, hasErrors: false, filesChecked: 0, summary: "Sin archivos fuente modificados" };
17
+ }
18
+
19
+ const ctx = await buildContext();
20
+ const features = detectFeaturesOnSrc();
21
+ const graph = buildGraph(ROOT);
22
+ const results = allChecks(features, graph, ctx);
23
+
24
+ const allIgnores = loadAllInlineIgnores(join(ROOT, "src"));
25
+
26
+ const violations = [];
27
+ for (const [, cat] of Object.entries(results)) {
28
+ for (const check of cat.checks) {
29
+ if (check.pass) continue;
30
+
31
+ const touchesFile = files.some(f =>
32
+ (check.detail && check.detail.includes(f)) ||
33
+ (check.label && check.label.includes(f))
34
+ );
35
+
36
+ if (!touchesFile && !reminder) continue;
37
+
38
+ if (isIgnored(check, allIgnores)) continue;
39
+
40
+ violations.push(check);
41
+ }
42
+ }
43
+
44
+ const graphViolations = evaluateRules(graph, ctx).filter(v => {
45
+ const touchesFile = files.some(f =>
46
+ (v.file && v.file.includes(f)) ||
47
+ (v.from && v.from.includes(f)) ||
48
+ (v.to && v.to.includes(f))
49
+ );
50
+ return touchesFile || reminder;
51
+ });
52
+
53
+ const allViolations = [...violations, ...graphViolations];
54
+ const hasCritical = allViolations.some(v => v.severity === "CRITICAL");
55
+ const hasErrors = allViolations.some(v => v.severity === "ERROR");
56
+
57
+ return {
58
+ violations: allViolations,
59
+ total: allViolations.length,
60
+ hasCritical,
61
+ hasErrors,
62
+ filesChecked: files.length,
63
+ summary: hasCritical
64
+ ? `⚠ ${allViolations.length} violación(es) — ${allViolations.filter(v => v.severity === "CRITICAL").length} CRITICAL, ${allViolations.filter(v => v.severity === "ERROR").length} ERROR`
65
+ : hasErrors
66
+ ? `⚠ ${allViolations.length} violación(es) — ${allViolations.filter(v => v.severity === "ERROR").length} ERROR`
67
+ : `${allViolations.length} violación(es) menores (WARNING/INFO)`,
68
+ };
69
+ }
70
+
71
+ export function writeAuditLog(env, data, cwd) {
72
+ try {
73
+ const dir = join(cwd, ".forge", "audit");
74
+ mkdirSync(dir, { recursive: true });
75
+ const path = join(dir, "forgeSentinel.json");
76
+ const log = [];
77
+ if (existsSync(path)) {
78
+ try {
79
+ const existing = JSON.parse(readFileSync(path, "utf-8"));
80
+ if (Array.isArray(existing)) log.push(...existing);
81
+ } catch {}
82
+ }
83
+ log.push({ ...data, ts: data.ts || new Date().toISOString() });
84
+ writeFileSync(path, JSON.stringify(log, null, 2) + "\n", "utf-8");
85
+ } catch {}
86
+ }
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, existsSync } from "fs";
4
+ import { join } from "path";
5
+ import { execFileSync } from "child_process";
6
+ import { buildGraph } from "./graph.mjs";
7
+ import { buildContext } from "./context.mjs";
8
+ import { detectFeaturesOnSrc, allChecks, loadAllInlineIgnores, isIgnored } from "./detect.mjs";
9
+ import { evaluateRules } from "./registry/rules.mjs";
10
+ import {
11
+ CYAN, GREEN, RED, YELLOW, BOLD, RESET, DIM,
12
+ formatCheck, formatJson,
13
+ } from "./formatter.mjs";
14
+ import { runSentinelCheck, writeAuditLog } from "./forgeSentinel-lib.mjs";
15
+
16
+ const ROOT = process.cwd();
17
+
18
+ function getChangedFiles() {
19
+ let files = [];
20
+ try {
21
+ const defaultBranch = execFileSync("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] })
22
+ .trim().replace("refs/remotes/origin/", "");
23
+ const raw = execFileSync("git", ["diff", "--name-only", "--diff-filter=ACMR", `${defaultBranch}...HEAD`], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
24
+ files = raw.trim().split("\n").filter(Boolean);
25
+ } catch {}
26
+
27
+ if (files.length === 0) {
28
+ try {
29
+ const raw = execFileSync("git", ["status", "--porcelain"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
30
+ files = raw.split("\n").filter(Boolean).map(l => l.slice(3));
31
+ } catch {}
32
+ }
33
+
34
+ return files.filter(f => f.startsWith("src/"));
35
+ }
36
+
37
+ function parseHookEvent(stdin) {
38
+ if (!stdin) return null;
39
+ try {
40
+ const event = JSON.parse(stdin);
41
+ let files = [];
42
+
43
+ // Claude Code format: { toolUse: { name, input: { filePath, content } } }
44
+ if (event.toolUse?.input?.filePath) {
45
+ files = [event.toolUse.input.filePath];
46
+ }
47
+
48
+ // Cursor/Codex format: may have files array or filePath property
49
+ if (Array.isArray(event.files)) {
50
+ files = event.files.map(f => typeof f === "string" ? f : f.filePath).filter(Boolean);
51
+ }
52
+ if (event.filePath) {
53
+ files.push(event.filePath);
54
+ }
55
+
56
+ return files.length > 0 ? [...new Set(files)] : null;
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ function printResult(result, opts = {}) {
63
+ const { reminder = false, hook = false } = opts;
64
+
65
+ const prefix = hook ? "" : `\n${BOLD}${CYAN}═══ forgeSentinel ═══${RESET}`;
66
+ if (!hook) console.log(prefix);
67
+
68
+ if (result.total === 0) {
69
+ if (!hook) {
70
+ console.log(` ${GREEN}✔${RESET} Sin violaciones arquitectónicas en archivos modificados`);
71
+ console.log(` ${DIM}Archivos revisados: ${result.filesChecked}${RESET}\n`);
72
+ }
73
+ return;
74
+ }
75
+
76
+ if (reminder || hook) {
77
+ if (!hook) {
78
+ console.log(` ${YELLOW}Recordatorio:${RESET} ${result.total} violación(es) arquitectónica(s).`);
79
+ console.log(` Ejecuta ${CYAN}forge quench${RESET} para ver el detalle completo.\n`);
80
+ }
81
+
82
+ if (result.hasCritical) {
83
+ const c = result.violations.filter(v => v.severity === "CRITICAL").length;
84
+ if (hook) {
85
+ const lines = [`[forgeSentinel] ⚠ ${c} CRITICAL, ${result.violations.filter(v => v.severity === "ERROR").length} ERROR`];
86
+ const toShow = result.violations.slice(0, 5);
87
+ for (const v of toShow) {
88
+ lines.push(` [${v.severity}] ${v.label}${v.file ? ` (${v.file})` : ""}`);
89
+ }
90
+ if (result.violations.length > 5) {
91
+ lines.push(` ${DIM}... y ${result.violations.length - 5} más. Ejecutá 'forge quench' para el detalle completo.${RESET}`);
92
+ }
93
+ console.log(lines.join("\n"));
94
+ } else {
95
+ console.log(` ${RED}⚠ ${c} violación(es) CRITICAL detectadas${RESET}`);
96
+ }
97
+ }
98
+ if (result.hasErrors) {
99
+ const e = result.violations.filter(v => v.severity === "ERROR").length;
100
+ console.log(` ${RED}⚠ ${e} violación(es) ERROR detectadas${RESET}`);
101
+ }
102
+
103
+ if (!hook) {
104
+ const toShow = result.violations.slice(0, 5);
105
+ for (const v of toShow) {
106
+ console.log(formatCheck(v));
107
+ }
108
+ if (result.violations.length > 5) {
109
+ console.log(` ${DIM}... y ${result.violations.length - 5} más${RESET}`);
110
+ }
111
+ console.log();
112
+ }
113
+ return;
114
+ }
115
+
116
+ for (const v of result.violations) {
117
+ console.log(formatCheck(v));
118
+ }
119
+
120
+ console.log(`\n ${BOLD}Resumen:${RESET} ${result.summary}`);
121
+ if (result.hasCritical) {
122
+ console.log(` ${RED}⚠ Se requiere acción correctiva antes de continuar${RESET}`);
123
+ }
124
+ console.log();
125
+ }
126
+
127
+ async function main() {
128
+ const args = process.argv.slice(2);
129
+ const isJson = args.includes("--json");
130
+ const isHook = args.includes("--hook");
131
+ const reminder = args.includes("--reminder");
132
+ const isDiff = args.includes("--diff");
133
+
134
+ let files;
135
+ if (isHook) {
136
+ const chunks = [];
137
+ if (!process.stdin.isTTY) {
138
+ for await (const chunk of process.stdin) chunks.push(chunk);
139
+ }
140
+ const stdin = Buffer.concat(chunks).toString("utf-8");
141
+ const hookFiles = parseHookEvent(stdin);
142
+ if (hookFiles) {
143
+ files = hookFiles;
144
+ }
145
+ }
146
+
147
+ if (!files || files.length === 0) {
148
+ if (isDiff) {
149
+ files = getChangedFiles();
150
+ } else {
151
+ files = args.filter(a => !a.startsWith("--"));
152
+ if (files.length === 0) {
153
+ files = getChangedFiles();
154
+ }
155
+ }
156
+ }
157
+
158
+ files = (files || []).filter(f =>
159
+ f.endsWith(".ts") || f.endsWith(".mjs") || f.endsWith(".js") || f.endsWith(".tsx")
160
+ );
161
+
162
+ const result = await runSentinelCheck(files, { strict: false, reminder: reminder || isHook });
163
+
164
+ writeAuditLog(process.env, {
165
+ ts: new Date().toISOString(),
166
+ hook: isHook ? "forgeSentinel" : "cli",
167
+ total: result.total,
168
+ hasCritical: result.hasCritical,
169
+ hasErrors: result.hasErrors,
170
+ filesChecked: result.filesChecked,
171
+ }, ROOT);
172
+
173
+ if (isJson) {
174
+ console.log(formatJson(result));
175
+ } else {
176
+ printResult(result, { reminder: reminder || isHook, hook: isHook });
177
+ }
178
+
179
+ process.exit(0);
180
+ }
181
+
182
+ if (process.argv[1]?.endsWith("forgeSentinel.mjs")) {
183
+ main().catch(() => process.exit(0));
184
+ }
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from "fs";
4
+ import { join } from "path";
5
+
6
+ const ROOT = process.cwd();
7
+ const FORGE_DIR = join(ROOT, ".forge");
8
+ const CONFIG_PATH = join(FORGE_DIR, "hooks-config.json");
9
+ const AUDIT_DIR = join(FORGE_DIR, "audit");
10
+
11
+ const DEFAULT_CONFIG = {
12
+ forgeSentinel: { enabled: true },
13
+ forgeSmith: { enabled: true },
14
+ };
15
+
16
+ function readConfig() {
17
+ try {
18
+ if (existsSync(CONFIG_PATH)) {
19
+ const data = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
20
+ return { ...DEFAULT_CONFIG, ...data };
21
+ }
22
+ } catch {}
23
+ return { ...DEFAULT_CONFIG };
24
+ }
25
+
26
+ function writeConfig(config) {
27
+ mkdirSync(FORGE_DIR, { recursive: true });
28
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf-8");
29
+ }
30
+
31
+ function cmdStatus() {
32
+ const config = readConfig();
33
+ const auditPath = join(AUDIT_DIR, "forgeSentinel.json");
34
+ let lastRun = null;
35
+ try {
36
+ if (existsSync(auditPath)) {
37
+ const log = JSON.parse(readFileSync(auditPath, "utf-8"));
38
+ if (Array.isArray(log) && log.length > 0) {
39
+ lastRun = log[log.length - 1];
40
+ }
41
+ }
42
+ } catch {}
43
+
44
+ console.log("── forgeSmith-admin ──");
45
+ console.log(` forgeSentinel (PostToolUse): ${config.forgeSentinel.enabled ? "✓ activo" : "✘ inactivo"}`);
46
+ console.log(` forgeSmith (preToolUse): ${config.forgeSmith.enabled ? "✓ activo" : "✘ inactivo"}`);
47
+ if (lastRun) {
48
+ console.log(` Último forgeSentinel: ${lastRun.ts} (${lastRun.filesChecked || 0} archivos, ${lastRun.total || 0} violaciones)`);
49
+ }
50
+ console.log(` Config: ${CONFIG_PATH}`);
51
+ }
52
+
53
+ function cmdOn(hookName) {
54
+ const config = readConfig();
55
+ if (hookName === "all" || hookName === "forgeSentinel") {
56
+ config.forgeSentinel.enabled = true;
57
+ }
58
+ if (hookName === "all" || hookName === "forgeSmith") {
59
+ config.forgeSmith.enabled = true;
60
+ }
61
+ writeConfig(config);
62
+ console.log(`forgeSmith-admin: hooks activados (${hookName})`);
63
+ }
64
+
65
+ function cmdOff(hookName) {
66
+ const config = readConfig();
67
+ if (hookName === "all" || hookName === "forgeSentinel") {
68
+ config.forgeSentinel.enabled = false;
69
+ }
70
+ if (hookName === "all" || hookName === "forgeSmith") {
71
+ config.forgeSmith.enabled = false;
72
+ }
73
+ writeConfig(config);
74
+ console.log(`forgeSmith-admin: hooks desactivados (${hookName})`);
75
+ }
76
+
77
+ function cmdReset() {
78
+ writeConfig(DEFAULT_CONFIG);
79
+ console.log("forgeSmith-admin: configuración restaurada a valores por defecto");
80
+ }
81
+
82
+ const [,, action, arg] = process.argv;
83
+
84
+ switch (action) {
85
+ case "status":
86
+ cmdStatus();
87
+ break;
88
+ case "on":
89
+ cmdOn(arg || "all");
90
+ break;
91
+ case "off":
92
+ cmdOff(arg || "all");
93
+ break;
94
+ case "reset":
95
+ cmdReset();
96
+ break;
97
+ default:
98
+ console.log("Uso: node forgeSmith-admin.mjs <status|on|off|reset> [hook-name]");
99
+ console.log("\n status Mostrar estado de hooks");
100
+ console.log(" on [forgeSentinel|forgeSmith|all] Activar hook(s)");
101
+ console.log(" off [forgeSentinel|forgeSmith|all] Desactivar hook(s)");
102
+ console.log(" reset Restaurar configuración por defecto");
103
+ process.exit(1);
104
+ }