@ronaldjdevfs/forge 1.1.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/wizard.mjs ADDED
@@ -0,0 +1,526 @@
1
+ import pc from "picocolors";
2
+ import { intro, outro, select, multiselect, text, spinner, isCancel, cancel, log, tasks } from "@clack/prompts";
3
+ import { existsSync, mkdirSync, copyFileSync, readdirSync, cpSync, writeFileSync, readFileSync } from "fs";
4
+ import { join, dirname } from "path";
5
+ import { fileURLToPath } from "url";
6
+ import { execSync } from "child_process";
7
+ import { homedir } from "os";
8
+ import { detectForWizard } from "./agents.mjs";
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const SKILL_SRC = join(__dirname, "..", "skills", "forge");
12
+ const AGENTS_TEMPLATES = join(SKILL_SRC, "templates", "agents");
13
+ const HOME = homedir();
14
+
15
+ const SEP = pc.dim("\u2500".repeat(46));
16
+
17
+ function printCenter(text, pad = 20) {
18
+ console.log(" ".repeat(pad) + text);
19
+ }
20
+
21
+ // --- Installer helpers ---
22
+
23
+ function detectPM(cwd) {
24
+ if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
25
+ if (existsSync(join(cwd, "bun.lock")) || existsSync(join(cwd, "bun.lockb"))) return "bun";
26
+ return "npm";
27
+ }
28
+
29
+ function ensureDependencies(cwd) {
30
+ const pkgPath = join(cwd, "package.json");
31
+ let pkg = {};
32
+ if (existsSync(pkgPath)) {
33
+ try {
34
+ pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
35
+ } catch {
36
+ pkg = {};
37
+ }
38
+ }
39
+ if (!pkg.dependencies) pkg.dependencies = {};
40
+ if (!pkg.dependencies["@opencode-ai/plugin"]) {
41
+ pkg.dependencies["@opencode-ai/plugin"] = "1.17.9";
42
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
43
+ }
44
+ const pm = detectPM(cwd);
45
+ const order = { pnpm: ["npm", "bun"], npm: ["bun", "pnpm"], bun: ["npm", "pnpm"] };
46
+ function runPM(bin) { execSync(`${bin} install`, { cwd, stdio: "pipe" }); }
47
+ try { runPM(pm); }
48
+ catch {
49
+ for (const fb of order[pm]) {
50
+ try { runPM(fb); return; } catch { }
51
+ }
52
+ log.warn("No se pudieron instalar dependencias. Ejecut\u00e1 'pnpm install' manualmente.");
53
+ }
54
+ }
55
+
56
+ function generateCommands(configDir) {
57
+ const COMMANDS = [
58
+ { name: "forge-forge", desc: "Forge \u2014 inicializar proyecto arquitect\u00f3nicamente" },
59
+ { name: "forge-cast", desc: "Cast \u2014 crear un nuevo feature hexagonal" },
60
+ { name: "forge-inspect", desc: "Inspect \u2014 auditor\u00eda arquitect\u00f3nica" },
61
+ { name: "forge-relocate", desc: "Relocate \u2014 migrar feature legacy" },
62
+ { name: "forge-reforge", desc: "Reforge \u2014 refactorizar arquitectura" },
63
+ { name: "forge-quench", desc: "Quench \u2014 verificar reglas arquitect\u00f3nicas" },
64
+ { name: "forge-temper", desc: "Temper \u2014 endurecer arquitectura" },
65
+ { name: "forge-chain", desc: "Chain \u2014 cadena de dependencias" },
66
+ { name: "forge-inscribe", desc: "Inscribe \u2014 generar ARCHITECTURE.md" },
67
+ { name: "forge-smelt", desc: "Smelt \u2014 extraer c\u00f3digo a shared/" },
68
+ ];
69
+ const cmdsDir = join(configDir, "commands");
70
+ mkdirSync(cmdsDir, { recursive: true });
71
+ for (const cmd of COMMANDS) {
72
+ const sub = cmd.name.replace("forge-", "");
73
+ const content = [
74
+ "---",
75
+ `description: ${cmd.desc}`,
76
+ "agent: build",
77
+ "---",
78
+ "",
79
+ `Ejecuta el subcomando ${sub} de Forge con los argumentos: $ARGUMENTS`,
80
+ "",
81
+ ].join("\n");
82
+ writeFileSync(join(cmdsDir, `${cmd.name}.md`), content);
83
+ }
84
+ }
85
+
86
+ function copyAgentTemplate(name, dest) {
87
+ const src = join(AGENTS_TEMPLATES, name);
88
+ if (!existsSync(src)) return;
89
+ for (const entry of readdirSync(src)) {
90
+ const srcFile = join(src, entry);
91
+ const destFile = join(dest, entry);
92
+ if (existsSync(srcFile)) cpSync(srcFile, destFile, { recursive: true, force: true });
93
+ }
94
+ }
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
+
146
+ // --- Welcome ---
147
+
148
+ async function welcomePhase() {
149
+ console.clear();
150
+ printCenter(pc.bold(pc.cyan("\u2694 Forge")), 22);
151
+ console.log();
152
+ printCenter(pc.dim("Sistema Operativo de Arquitectura"), 17);
153
+ console.log();
154
+ console.log(" " + SEP);
155
+ console.log();
156
+ console.log(" Bienvenido a Forge.");
157
+ console.log();
158
+ console.log(" Forge instalar\u00e1 la Skill de Arquitectura");
159
+ console.log(" en uno o varios agentes de IA compatibles.");
160
+ console.log();
161
+
162
+ const result = await text({
163
+ message: "Presiona Enter para continuar",
164
+ placeholder: "",
165
+ initialValue: "",
166
+ });
167
+
168
+ if (isCancel(result)) {
169
+ cancel("Cancelado, no pasa nada");
170
+ process.exit(0);
171
+ }
172
+ }
173
+
174
+ // --- Detection ---
175
+
176
+ async function detectionPhase(cwd) {
177
+ console.clear();
178
+ log.message(pc.cyan("\u2694 Detectando agentes compatibles..."));
179
+
180
+ const s = spinner();
181
+ s.start("Buscando instalaciones...");
182
+
183
+ const agents = detectForWizard(cwd);
184
+ s.stop("B\u00fasqueda completada");
185
+ console.log();
186
+
187
+ const detectedCount = agents.filter((a) => a.detected).length;
188
+
189
+ for (const agent of agents) {
190
+ const label = agent.label.padEnd(20);
191
+ const scope = agent.scope;
192
+ if (agent.detected) {
193
+ console.log(` ${pc.green("\u2714")} ${label}${pc.dim(scope)}`);
194
+ } else {
195
+ console.log(` ${pc.red("\u2718")} ${label}${pc.dim(scope)}`);
196
+ }
197
+ }
198
+
199
+ console.log();
200
+ log.success(`Se encontraron ${detectedCount} instalaciones compatibles.`);
201
+ console.log();
202
+
203
+ await text({
204
+ message: "Presiona Enter para continuar",
205
+ placeholder: "",
206
+ initialValue: "",
207
+ });
208
+
209
+ return agents;
210
+ }
211
+
212
+ // --- Agent selection ---
213
+
214
+ async function selectionPhase(agents) {
215
+ console.clear();
216
+ log.message(pc.cyan("\u2694 Selecciona los agentes"));
217
+ console.log();
218
+
219
+ const options = agents.map((a) => ({
220
+ value: a.id,
221
+ label: `${a.label} (${a.scope})`,
222
+ hint: a.detected ? "disponible" : undefined,
223
+ }));
224
+
225
+ options.push({ value: "__custom__", label: "Ruta personalizada" });
226
+
227
+ const selected = await multiselect({
228
+ message: "Espacio = seleccionar | Enter = continuar",
229
+ options,
230
+ required: false,
231
+ });
232
+
233
+ if (isCancel(selected)) {
234
+ cancel("Cancelado, no pasa nada");
235
+ process.exit(0);
236
+ }
237
+
238
+ let customPath = null;
239
+ if (selected.includes("__custom__")) {
240
+ customPath = await text({
241
+ message: "\u00bfD\u00f3nde deseas instalar Forge?",
242
+ placeholder: "/home/usuario/.claude",
243
+ validate: (v) => (v ? undefined : "La ruta es requerida"),
244
+ });
245
+
246
+ if (isCancel(customPath)) {
247
+ cancel("Cancelado, no pasa nada");
248
+ process.exit(0);
249
+ }
250
+
251
+ if (existsSync(customPath)) {
252
+ log.success("Ruta v\u00e1lida");
253
+ } else {
254
+ log.info("La ruta no existe, se crear\u00e1 al instalar");
255
+ }
256
+ }
257
+
258
+ return { selectedIds: selected.filter((id) => id !== "__custom__"), customPath };
259
+ }
260
+
261
+ // --- Summary ---
262
+
263
+ async function summaryPhase(agentSelection) {
264
+ const allAgents = detectForWizard(process.cwd());
265
+ const selectedAgents = agentSelection.selectedIds
266
+ .map((id) => allAgents.find((a) => a.id === id))
267
+ .filter(Boolean);
268
+
269
+ while (true) {
270
+ console.clear();
271
+ log.message(pc.cyan("\u2694 Resumen"));
272
+ console.log(" " + SEP);
273
+ console.log();
274
+ console.log(` ${pc.bold("Agentes")}`);
275
+ console.log();
276
+
277
+ for (const agent of selectedAgents) {
278
+ console.log(` \u2022 ${agent.label} ${pc.dim(`(${agent.scope})`)}`);
279
+ }
280
+ if (agentSelection.customPath) {
281
+ console.log(` \u2022 Ruta personalizada ${pc.dim(`(${agentSelection.customPath})`)}`);
282
+ }
283
+
284
+ console.log();
285
+ console.log(` ${pc.bold("Componentes")}`);
286
+ console.log();
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
+
292
+ const components = [
293
+ "Forge Skill",
294
+ "Reglas",
295
+ "Prompts",
296
+ "Comandos",
297
+ "Plantillas",
298
+ "Agentes",
299
+ hasHooks ? "forgeSentinel (PostToolUse) / forgeSmith (preToolUse)" : null,
300
+ ].filter(Boolean);
301
+ for (const comp of components) {
302
+ console.log(` ${pc.green("\u2714")} ${comp}`);
303
+ }
304
+
305
+ console.log();
306
+ console.log(" " + SEP);
307
+ console.log();
308
+
309
+ const action = await select({
310
+ message: "\u00bfDeseas continuar?",
311
+ options: [
312
+ { value: "install", label: "Instalar" },
313
+ { value: "back", label: "Volver" },
314
+ { value: "cancel", label: "Cancelar" },
315
+ ],
316
+ });
317
+
318
+ if (isCancel(action) || action === "cancel") {
319
+ cancel("Cancelado, no pasa nada");
320
+ process.exit(0);
321
+ }
322
+
323
+ if (action === "back") {
324
+ return "back";
325
+ }
326
+
327
+ return { action: "install", selectedAgents, customPath: agentSelection.customPath };
328
+ }
329
+ }
330
+
331
+ // --- Installation ---
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
+
346
+ function buildAgentSteps(agent, cwd) {
347
+ const label = `${agent.label} (${agent.scope})`;
348
+ const s = (title, fn) => ({ title: `${pc.dim("\u2502")} ${title}`, task: fn });
349
+
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
+ }
371
+
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
+ }
392
+
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
+ }
406
+
407
+ // Generic agent types via AGENT_MAP
408
+ const map = AGENT_MAP[agent.id];
409
+ if (!map) return null;
410
+
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 };
424
+ }
425
+
426
+ async function installPhase(result, cwd) {
427
+ console.clear();
428
+ log.message(pc.cyan("\u2694 Instalando Forge..."));
429
+
430
+ const agentsData = [];
431
+ for (const agent of result.selectedAgents) {
432
+ const data = buildAgentSteps(agent, cwd);
433
+ if (data) agentsData.push(data);
434
+ }
435
+
436
+ if (result.customPath) {
437
+ const dest = result.customPath;
438
+ const steps = [];
439
+ const s = (title, fn) => ({ title: `${pc.dim("\u2502")} ${title}`, task: fn });
440
+ steps.push(s("Instalando Forge en ruta personalizada", async () => {
441
+ mkdirSync(dest, { recursive: true });
442
+ cpSync(SKILL_SRC, join(dest, "forge"), { recursive: true, force: true });
443
+ copyAgentTemplate("claude", dest);
444
+ return "Forge instalado";
445
+ }));
446
+ steps.push(s("Verificando instalaci\u00f3n", async () => {
447
+ return existsSync(join(dest, "forge")) ? "\u2714 Instalaci\u00f3n verificada" : "ERROR";
448
+ }));
449
+ agentsData.push({ label: `Ruta personalizada (${dest})`, dest, steps });
450
+ }
451
+
452
+ const allSteps = [];
453
+ for (const ad of agentsData) {
454
+ allSteps.push({
455
+ title: pc.bold(pc.cyan(ad.label)),
456
+ task: async () => `Instalando en ${ad.dest}`,
457
+ });
458
+ for (const step of ad.steps) {
459
+ allSteps.push(step);
460
+ }
461
+ }
462
+
463
+ await tasks(allSteps);
464
+ }
465
+
466
+ // --- Main wizard orchestrator ---
467
+
468
+ export async function runWizard() {
469
+ const cwd = process.cwd();
470
+
471
+ await welcomePhase();
472
+
473
+ console.clear();
474
+ intro(pc.cyan("\u2694 Forge"));
475
+
476
+ const agents = await detectionPhase(cwd);
477
+
478
+ let agentSelection;
479
+ let result;
480
+
481
+ while (true) {
482
+ agentSelection = await selectionPhase(agents);
483
+
484
+ if (agentSelection.selectedIds.length === 0 && !agentSelection.customPath) {
485
+ log.warn("Seleccion\u00e1 al menos un agente o una ruta personalizada.");
486
+ continue;
487
+ }
488
+
489
+ result = await summaryPhase(agentSelection);
490
+
491
+ if (result === "back") continue;
492
+ break;
493
+ }
494
+
495
+ await installPhase(result, cwd);
496
+
497
+ console.log();
498
+ console.log(" " + SEP);
499
+ printCenter(pc.bold(pc.cyan("\u2694 Forge")), 22);
500
+ console.log();
501
+ printCenter(pc.green("Instalaci\u00f3n completada correctamente."), 12);
502
+ console.log();
503
+ console.log(` Forge se instal\u00f3 en:`);
504
+ console.log();
505
+
506
+ for (const agent of result.selectedAgents) {
507
+ console.log(` ${pc.green("\u2714")} ${agent.label} ${pc.dim(`(${agent.scope})`)}`);
508
+ }
509
+ if (result.customPath) {
510
+ console.log(` ${pc.green("\u2714")} Ruta personalizada ${pc.dim(`(${result.customPath})`)}`);
511
+ }
512
+
513
+ console.log();
514
+ console.log(` ${pc.bold("Pr\u00f3ximos pasos")}`);
515
+ console.log();
516
+ console.log(` \u2022 Reinicia tu agente si est\u00e1 abierto.`);
517
+ console.log(` \u2022 Ejecut\u00e1 el comando "forge" dentro del agente.`);
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`);
521
+ console.log();
522
+ console.log(" " + SEP);
523
+ console.log();
524
+
525
+ outro(pc.cyan("\u00a1Forge est\u00e1 listo para usar! \u2694"));
526
+ }
package/logo.png DELETED
Binary file