@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/src/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { copyFileSync, mkdirSync, existsSync, writeFileSync, readFileSync, cpSync } from "fs";
3
+ import { copyFileSync, mkdirSync, existsSync, writeFileSync, readFileSync, cpSync, readdirSync } from "fs";
4
4
  import { join, dirname, relative } from "path";
5
5
  import { fileURLToPath } from "url";
6
6
  import { execSync } from "child_process";
@@ -11,6 +11,14 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
11
11
  const SKILL_SRC = join(__dirname, "..", "skills", "forge");
12
12
  const AGENTS_TEMPLATES = join(SKILL_SRC, "templates", "agents");
13
13
 
14
+ const AGENT_CONFIGS = {
15
+ claude: { dir: ".claude", template: "claude", skillPath: ".claude/skills/forge", need: ["CLAUDE.md", "settings.local.json"], postInstall: "forgeSentinel" },
16
+ cursor: { dir: ".cursor", template: "cursor", skillPath: ".cursor/skills/forge", need: [".cursorrules", "hooks.json"], postInstall: "forgeSmith" },
17
+ codex: { dir: ".agents", template: "codex", skillPath: ".agents/skills/forge", need: ["hooks.json"], postInstall: "forgeSentinel" },
18
+ gemini: { dir: ".gemini", template: "gemini", skillPath: ".gemini/skills/forge", need: ["SKILL.md"], postInstall: null },
19
+ agents: { dir: ".agents", template: "agents", skillPath: ".agents/skills/forge", need: ["hooks.json"], postInstall: "forgeSentinel" },
20
+ };
21
+
14
22
  function getTargetDir(global) {
15
23
  if (global) {
16
24
  const home = process.env.HOME || process.env.USERPROFILE;
@@ -137,40 +145,70 @@ async function installOpenCode(isGlobal = false) {
137
145
  log.success("OpenCode configurado correctamente");
138
146
  }
139
147
 
140
- async function installCursor() {
141
- const src = join(AGENTS_TEMPLATES, "cursor", ".cursorrules");
142
- const dest = join(process.cwd(), ".cursorrules");
148
+ async function installAgentTemplates(agentDir, agentName) {
149
+ const config = AGENT_CONFIGS[agentName];
150
+ if (!config) return;
143
151
 
144
- if (!existsSync(src)) {
145
- log.error("Template .cursorrules no encontrado");
146
- process.exit(1);
152
+ const templateDir = join(AGENTS_TEMPLATES, config.template);
153
+ if (!existsSync(templateDir)) {
154
+ log.error(`Template para ${agentName} no encontrado en ${templateDir}`);
155
+ return;
147
156
  }
148
157
 
149
158
  const s = spinner();
150
- s.start("Creando .cursorrules");
151
- copyFileSync(src, dest);
152
- s.stop(".cursorrules creado");
159
+ s.start(`Copiando templates para ${agentName} en ${agentDir}/`);
160
+
161
+ mkdirSync(agentDir, { recursive: true });
162
+
163
+ // Copy skill into <agentDir>/skills/forge/
164
+ const skillDest = join(agentDir, "skills", "forge");
165
+ cpSync(SKILL_SRC, skillDest, { recursive: true, force: true });
153
166
 
154
- log.success("Cursor configurado correctamente");
167
+ // Copy template files (hooks, CLAUDE.md, .cursorrules, etc.)
168
+ for (const entry of readdirSync(templateDir)) {
169
+ const srcFile = join(templateDir, entry);
170
+ const destFile = join(agentDir, entry);
171
+ cpSync(srcFile, destFile, { recursive: true, force: true });
172
+ }
173
+
174
+ // Render SKILL.md with agent-specific path
175
+ const templatePath = join(AGENTS_TEMPLATES, "SKILL.md.template");
176
+ if (existsSync(templatePath)) {
177
+ const template = readFileSync(templatePath, "utf-8");
178
+ const rendered = template.replace(/\{\{AGENT_PATH\}\}/g, config.skillPath);
179
+ writeFileSync(join(skillDest, "SKILL.md"), rendered, "utf-8");
180
+ }
181
+
182
+ s.stop(`${agentName} configurado en ${agentDir}/`);
183
+ log.success(`${agentName} listo (skill + ${config.postInstall || "templates"})`);
184
+ }
185
+
186
+ async function installCursor() {
187
+ await installAgentTemplates(join(process.cwd(), ".cursor"), "cursor");
155
188
  }
156
189
 
157
190
  async function installClaude() {
158
- const claudeDir = join(process.cwd(), ".claude");
159
- const src = join(AGENTS_TEMPLATES, "claude", "CLAUDE.md");
160
- const dest = join(claudeDir, "CLAUDE.md");
191
+ await installAgentTemplates(join(process.cwd(), ".claude"), "claude");
192
+ }
161
193
 
162
- if (!existsSync(src)) {
163
- log.error("Template CLAUDE.md no encontrado");
164
- process.exit(1);
194
+ async function installCodex() {
195
+ // Codex hooks point to .agents/skills/forge/ via git root
196
+ await installAgentTemplates(join(process.cwd(), ".agents"), "codex");
197
+ // Also copy hooks.json into .codex/ so Codex CLI picks it up
198
+ const codexDir = join(process.cwd(), ".codex");
199
+ mkdirSync(codexDir, { recursive: true });
200
+ const hooksSrc = join(AGENTS_TEMPLATES, "codex", "hooks.json");
201
+ if (existsSync(hooksSrc)) {
202
+ cpSync(hooksSrc, join(codexDir, "hooks.json"), { force: true });
165
203
  }
204
+ }
166
205
 
167
- const s = spinner();
168
- s.start("Creando .claude/CLAUDE.md");
169
- mkdirSync(claudeDir, { recursive: true });
170
- copyFileSync(src, dest);
171
- s.stop(".claude/CLAUDE.md creado");
206
+ async function installGemini() {
207
+ await installAgentTemplates(join(process.cwd(), ".gemini"), "gemini");
208
+ }
172
209
 
173
- log.success("Claude Code configurado correctamente");
210
+ async function installAgentsGeneric() {
211
+ await installAgentTemplates(join(process.cwd(), ".agents"), "agents");
174
212
  }
175
213
 
176
214
  async function installAgents(selected, isGlobal = false) {
@@ -185,6 +223,15 @@ async function installAgents(selected, isGlobal = false) {
185
223
  case "claude":
186
224
  await installClaude();
187
225
  break;
226
+ case "codex":
227
+ await installCodex();
228
+ break;
229
+ case "gemini":
230
+ await installGemini();
231
+ break;
232
+ case "agents":
233
+ await installAgentsGeneric();
234
+ break;
188
235
  }
189
236
  }
190
237
  }
@@ -200,10 +247,19 @@ USO
200
247
  forge install --opencode Instalar solo para OpenCode
201
248
  forge install --cursor Instalar solo para Cursor
202
249
  forge install --claude Instalar solo para Claude Code
203
- forge install --all Instalar para todos los agentes
250
+ forge install --codex Instalar solo para Codex CLI
251
+ forge install --gemini Instalar solo para Gemini Code Assist
252
+ forge install --all Instalar para todos los agentes detectados
204
253
  forge install --global Instalar OpenCode globalmente (~/.config/opencode/)
205
254
  forge install --help Mostrar esta ayuda
206
255
 
256
+ AGENTES SOPORTADOS
257
+ OpenCode, Claude Code, Cursor, Codex CLI, Gemini Code Assist, Agentes Genéricos
258
+
259
+ HOOKS
260
+ forgeSentinel (PostToolUse) — guardia arquitectónico post-escritura
261
+ forgeSmith (preToolUse) — guardia arquitectónico pre-escritura (Cursor)
262
+
207
263
  OPCIONES
208
264
  -g, --global Instalar OpenCode en ~/.config/opencode/skills/forge/
209
265
  -h, --help Mostrar esta ayuda
@@ -223,6 +279,8 @@ async function main() {
223
279
  const hasOpenCode = args.includes("--opencode");
224
280
  const hasCursor = args.includes("--cursor");
225
281
  const hasClaude = args.includes("--claude");
282
+ const hasCodex = args.includes("--codex");
283
+ const hasGemini = args.includes("--gemini");
226
284
  const hasAll = args.includes("--all");
227
285
 
228
286
  const subcommandIndex = args.indexOf("skills");
@@ -230,12 +288,14 @@ async function main() {
230
288
 
231
289
  if (command === "install") {
232
290
  if (hasAll) {
233
- await installAgents(["opencode", "cursor", "claude"], isGlobal);
234
- } else if (hasOpenCode || hasCursor || hasClaude) {
291
+ await installAgents(["opencode", "cursor", "claude", "codex", "gemini", "agents"], isGlobal);
292
+ } else if (hasOpenCode || hasCursor || hasClaude || hasCodex || hasGemini) {
235
293
  const selected = [];
236
294
  if (hasOpenCode) selected.push("opencode");
237
295
  if (hasCursor) selected.push("cursor");
238
296
  if (hasClaude) selected.push("claude");
297
+ if (hasCodex) selected.push("codex");
298
+ if (hasGemini) selected.push("gemini");
239
299
  await installAgents(selected, isGlobal);
240
300
  } else {
241
301
  await runWizard();
package/src/wizard.mjs CHANGED
@@ -93,6 +93,56 @@ function copyAgentTemplate(name, dest) {
93
93
  }
94
94
  }
95
95
 
96
+ const AGENT_SKILL_PATHS = {
97
+ claude: ".claude/skills/forge",
98
+ cursor: ".cursor/skills/forge",
99
+ codex: ".agents/skills/forge",
100
+ gemini: ".gemini/skills/forge",
101
+ agents: ".agents/skills/forge",
102
+ };
103
+
104
+ function installAgentTemplates(agentDir, agentName) {
105
+ const templateDir = join(AGENTS_TEMPLATES, agentName);
106
+ if (!existsSync(templateDir)) {
107
+ return { ok: false, error: `Template ${agentName} no encontrado` };
108
+ }
109
+ mkdirSync(agentDir, { recursive: true });
110
+ // Copy skill into <agentDir>/skills/forge/
111
+ const skillDest = join(agentDir, "skills", "forge");
112
+ cpSync(SKILL_SRC, skillDest, { recursive: true, force: true });
113
+ // Copy template files (hooks, CLAUDE.md, .cursorrules, etc.)
114
+ copyAgentTemplate(agentName, agentDir);
115
+
116
+ // Render SKILL.md with agent-specific path
117
+ const templatePath = join(AGENTS_TEMPLATES, "SKILL.md.template");
118
+ if (existsSync(templatePath)) {
119
+ const template = readFileSync(templatePath, "utf-8");
120
+ const skillPath = AGENT_SKILL_PATHS[agentName] || agentName;
121
+ const rendered = template.replace(/\{\{AGENT_PATH\}\}/g, skillPath);
122
+ writeFileSync(join(skillDest, "SKILL.md"), rendered, "utf-8");
123
+ }
124
+
125
+ // Codex special case: also copy hooks.json to .codex/
126
+ if (agentName === "codex") {
127
+ const codexDir = join(process.cwd(), ".codex");
128
+ mkdirSync(codexDir, { recursive: true });
129
+ const hooksSrc = join(templateDir, "hooks.json");
130
+ if (existsSync(hooksSrc)) {
131
+ cpSync(hooksSrc, join(codexDir, "hooks.json"), { force: true });
132
+ }
133
+ }
134
+
135
+ return { ok: true, dest: agentDir };
136
+ }
137
+
138
+ const HOOK_LABELS = {
139
+ claude: "forgeSentinel (PostToolUse)",
140
+ cursor: "forgeSmith (preToolUse)",
141
+ codex: "forgeSentinel (PostToolUse)",
142
+ gemini: "—",
143
+ agents: "forgeSentinel (PostToolUse)",
144
+ };
145
+
96
146
  // --- Welcome ---
97
147
 
98
148
  async function welcomePhase() {
@@ -235,6 +285,10 @@ async function summaryPhase(agentSelection) {
235
285
  console.log(` ${pc.bold("Componentes")}`);
236
286
  console.log();
237
287
 
288
+ const hasHooks = agentSelection.selectedIds.some(id =>
289
+ ["claude-project", "claude-global", "cursor-project", "codex-project", "codex-global", "agents-project"].includes(id)
290
+ );
291
+
238
292
  const components = [
239
293
  "Forge Skill",
240
294
  "Reglas",
@@ -242,7 +296,8 @@ async function summaryPhase(agentSelection) {
242
296
  "Comandos",
243
297
  "Plantillas",
244
298
  "Agentes",
245
- ];
299
+ hasHooks ? "forgeSentinel (PostToolUse) / forgeSmith (preToolUse)" : null,
300
+ ].filter(Boolean);
246
301
  for (const comp of components) {
247
302
  console.log(` ${pc.green("\u2714")} ${comp}`);
248
303
  }
@@ -275,102 +330,97 @@ async function summaryPhase(agentSelection) {
275
330
 
276
331
  // --- Installation ---
277
332
 
333
+ const AGENT_MAP = {
334
+ "claude-global": { name: "claude", dir: () => join(HOME, ".claude"), verify: "CLAUDE.md" },
335
+ "claude-project": { name: "claude", dir: () => join(process.cwd(), ".claude"), verify: "CLAUDE.md" },
336
+ "cursor-project": { name: "cursor", dir: () => join(process.cwd(), ".cursor"), verify: ".cursorrules" },
337
+ "codex-project": { name: "codex", dir: () => join(process.cwd(), ".agents"), verify: "hooks.json" },
338
+ "codex-global": { name: "codex", dir: () => join(HOME, ".codex"), verify: "hooks.json" },
339
+ "gemini-project": { name: "gemini", dir: () => join(process.cwd(), ".gemini"), verify: "SKILL.md" },
340
+ "agents-project": { name: "agents", dir: () => join(process.cwd(), ".agents"), verify: "hooks.json" },
341
+ "opencode-global": null,
342
+ "opencode-project": null,
343
+ "copilot-project": null,
344
+ };
345
+
278
346
  function buildAgentSteps(agent, cwd) {
279
347
  const label = `${agent.label} (${agent.scope})`;
280
348
  const s = (title, fn) => ({ title: `${pc.dim("\u2502")} ${title}`, task: fn });
281
349
 
282
- switch (agent.id) {
283
- case "claude-global":
284
- case "claude-project": {
285
- const dest = agent.id === "claude-global" ? join(HOME, ".claude") : join(cwd, ".claude");
286
- const steps = [];
287
- steps.push(s("Copiando Forge en .claude/", async () => {
288
- mkdirSync(dest, { recursive: true });
289
- copyAgentTemplate("claude", dest);
290
- cpSync(SKILL_SRC, join(dest, "forge"), { recursive: true, force: true });
291
- return "Forge instalado en .claude/";
292
- }));
293
- steps.push(s("Verificando instalaci\u00f3n", async () => {
294
- const ok = existsSync(join(dest, "CLAUDE.md"));
295
- return ok ? "\u2714 Instalaci\u00f3n verificada" : "ERROR: CLAUDE.md no encontrado";
296
- }));
297
- return { label, dest, steps };
298
- }
299
-
300
- case "opencode-global": {
301
- const dest = join(HOME, ".config", "opencode", "skills", "forge");
302
- const configDir = join(HOME, ".config", "opencode");
303
- const steps = [];
304
- steps.push(s("Copiando Skill en opencode/", async () => {
305
- mkdirSync(dest, { recursive: true });
306
- cpSync(SKILL_SRC, dest, { recursive: true, force: true });
307
- return "Skill copiada";
308
- }));
309
- steps.push(s("Registrando comandos + dependencias", async () => {
310
- generateCommands(configDir);
311
- ensureDependencies(configDir);
312
- return "Comandos y dependencias listos";
313
- }));
314
- steps.push(s("Verificando instalaci\u00f3n", async () => {
315
- const ok = existsSync(join(dest, "scripts", "context.mjs"));
316
- return ok ? "\u2714 Instalaci\u00f3n verificada" : "ERROR: Skill no encontrada";
317
- }));
318
- return { label, dest, steps };
319
- }
350
+ // Handle special cases (opencode, copilot) separately
351
+ if (agent.id === "opencode-global") {
352
+ const dest = join(HOME, ".config", "opencode", "skills", "forge");
353
+ const configDir = join(HOME, ".config", "opencode");
354
+ const steps = [];
355
+ steps.push(s("Copiando Skill en opencode/", async () => {
356
+ mkdirSync(dest, { recursive: true });
357
+ cpSync(SKILL_SRC, dest, { recursive: true, force: true });
358
+ return "Skill copiada";
359
+ }));
360
+ steps.push(s("Registrando comandos + dependencias", async () => {
361
+ generateCommands(configDir);
362
+ ensureDependencies(configDir);
363
+ return "Comandos y dependencias listos";
364
+ }));
365
+ steps.push(s("Verificando instalaci\u00f3n", async () => {
366
+ const ok = existsSync(join(dest, "scripts", "context.mjs"));
367
+ return ok ? "\u2714 Instalaci\u00f3n verificada" : "ERROR: Skill no encontrada";
368
+ }));
369
+ return { label, dest, steps };
370
+ }
320
371
 
321
- case "opencode-project": {
322
- const dest = join(cwd, ".opencode", "skills", "forge");
323
- const configDir = join(cwd, ".opencode");
324
- const steps = [];
325
- steps.push(s("Copiando Skill en .opencode/", async () => {
326
- mkdirSync(dest, { recursive: true });
327
- cpSync(SKILL_SRC, dest, { recursive: true, force: true });
328
- return "Skill copiada";
329
- }));
330
- steps.push(s("Registrando comandos + dependencias", async () => {
331
- generateCommands(configDir);
332
- ensureDependencies(configDir);
333
- return "Comandos y dependencias listos";
334
- }));
335
- steps.push(s("Verificando instalaci\u00f3n", async () => {
336
- const ok = existsSync(join(dest, "scripts", "context.mjs"));
337
- return ok ? "\u2714 Instalaci\u00f3n verificada" : "ERROR: Skill no encontrada";
338
- }));
339
- return { label, dest, steps };
340
- }
372
+ if (agent.id === "opencode-project") {
373
+ const dest = join(cwd, ".opencode", "skills", "forge");
374
+ const configDir = join(cwd, ".opencode");
375
+ const steps = [];
376
+ steps.push(s("Copiando Skill en .opencode/", async () => {
377
+ mkdirSync(dest, { recursive: true });
378
+ cpSync(SKILL_SRC, dest, { recursive: true, force: true });
379
+ return "Skill copiada";
380
+ }));
381
+ steps.push(s("Registrando comandos + dependencias", async () => {
382
+ generateCommands(configDir);
383
+ ensureDependencies(configDir);
384
+ return "Comandos y dependencias listos";
385
+ }));
386
+ steps.push(s("Verificando instalaci\u00f3n", async () => {
387
+ const ok = existsSync(join(dest, "scripts", "context.mjs"));
388
+ return ok ? "\u2714 Instalaci\u00f3n verificada" : "ERROR: Skill no encontrada";
389
+ }));
390
+ return { label, dest, steps };
391
+ }
341
392
 
342
- case "copilot-project": {
343
- const ghDir = join(cwd, ".github");
344
- const dest = join(ghDir, "copilot-instructions.md");
345
- const steps = [];
346
- steps.push(s("Instalando reglas en .github/", async () => {
347
- mkdirSync(ghDir, { recursive: true });
348
- const src = join(AGENTS_TEMPLATES, "cursor", ".cursorrules");
349
- if (existsSync(src)) copyFileSync(src, dest);
350
- const ok = existsSync(dest);
351
- return ok ? "Reglas instaladas" : "ERROR";
352
- }));
353
- return { label, dest, steps };
354
- }
393
+ if (agent.id === "copilot-project") {
394
+ const ghDir = join(cwd, ".github");
395
+ const dest = join(ghDir, "copilot-instructions.md");
396
+ const steps = [];
397
+ steps.push(s("Instalando reglas en .github/", async () => {
398
+ mkdirSync(ghDir, { recursive: true });
399
+ const src = join(AGENTS_TEMPLATES, "cursor", ".cursorrules");
400
+ if (existsSync(src)) copyFileSync(src, dest);
401
+ const ok = existsSync(dest);
402
+ return ok ? "Reglas instaladas" : "ERROR";
403
+ }));
404
+ return { label, dest, steps };
405
+ }
355
406
 
356
- case "codex": {
357
- const dest = join(HOME, ".codex");
358
- const steps = [];
359
- steps.push(s("Instalando Forge en .codex/", async () => {
360
- mkdirSync(dest, { recursive: true });
361
- copyAgentTemplate("cursor", dest);
362
- cpSync(SKILL_SRC, join(dest, "forge"), { recursive: true, force: true });
363
- return "Forge instalado";
364
- }));
365
- steps.push(s("Verificando instalaci\u00f3n", async () => {
366
- return existsSync(dest) ? "\u2714 Instalaci\u00f3n verificada" : "ERROR";
367
- }));
368
- return { label, dest, steps };
369
- }
407
+ // Generic agent types via AGENT_MAP
408
+ const map = AGENT_MAP[agent.id];
409
+ if (!map) return null;
370
410
 
371
- default:
372
- return null;
373
- }
411
+ const dest = map.dir();
412
+ const agentName = map.name;
413
+ const steps = [];
414
+ steps.push(s(`Instalando Forge + hooks en ${dest}/`, async () => {
415
+ const r = installAgentTemplates(dest, agentName);
416
+ return r.ok ? `Forge + ${HOOK_LABELS[agentName] || "templates"} instalados` : `ERROR: ${r.error}`;
417
+ }));
418
+ steps.push(s("Verificando instalaci\u00f3n", async () => {
419
+ const ok = existsSync(join(dest, "skills", "forge", "scripts", "context.mjs")) &&
420
+ existsSync(join(dest, map.verify));
421
+ return ok ? "\u2714 Instalaci\u00f3n verificada" : "ERROR: archivos no encontrados";
422
+ }));
423
+ return { label, dest, steps };
374
424
  }
375
425
 
376
426
  async function installPhase(result, cwd) {
@@ -465,7 +515,9 @@ export async function runWizard() {
465
515
  console.log();
466
516
  console.log(` \u2022 Reinicia tu agente si est\u00e1 abierto.`);
467
517
  console.log(` \u2022 Ejecut\u00e1 el comando "forge" dentro del agente.`);
468
- console.log(` \u2022 Consult\u00e1 la documentaci\u00f3n para comenzar.`);
518
+ console.log(` \u2022 forgeSentinel se activa autom\u00e1ticamente tras cada escritura.`);
519
+ console.log(` \u2022 forgeSmith (Cursor) previene escrituras con violaciones CRITICAL.`);
520
+ console.log(` \u2022 Gesti\u00f3n de hooks: node .<agent>/skills/forge/scripts/forgeSmith-admin.mjs`);
469
521
  console.log();
470
522
  console.log(" " + SEP);
471
523
  console.log();