@ronaldjdevfs/forge 1.1.0 → 1.2.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 CHANGED
@@ -1,6 +1,6 @@
1
- <img src="logo.png" alt="Forge Logo" width="300" height="300">
1
+ <img src="favicon.svg" alt="Forge Logo" width="100" height="100">
2
2
 
3
- # Forge — Backend Architecture Operating System
3
+ ## Forge — Backend Architecture Operating System
4
4
 
5
5
  **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**.
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ronaldjdevfs/forge",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
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": {
@@ -39,7 +39,10 @@
39
39
  "node": ">=18"
40
40
  },
41
41
  "main": "./src/cli.js",
42
- "dependencies": {},
42
+ "dependencies": {
43
+ "@clack/prompts": "^1.5.1",
44
+ "picocolors": "^1.1.1"
45
+ },
43
46
  "publishConfig": {
44
47
  "access": "public",
45
48
  "registry": "https://registry.npmjs.org/"
@@ -0,0 +1,98 @@
1
+ # Forge — Backend Architecture Operating System
2
+
3
+ Forge es un sistema operativo arquitectónico para backend. Diseña, construye, audita y evoluciona arquitecturas basadas en **Arquitectura Hexagonal**, **DDD pragmático** y **vertical slices**.
4
+
5
+ ## Boot Sequence
6
+
7
+ Ejecutar en orden antes de cualquier acción:
8
+
9
+ 1. `node .opencode/skills/forge/scripts/context.mjs` — stack + layers detection
10
+ 2. `node .opencode/skills/forge/scripts/armorer.mjs` — ownership, orphans, duplicates
11
+ 3. `node .opencode/skills/forge/scripts/profile.mjs --extended` — tech profile
12
+ 4. `node .opencode/skills/forge/scripts/graph.mjs --json` — 4-layer graph
13
+ 5. `node .opencode/skills/forge/scripts/chain.mjs --json` — dependency analysis
14
+ 6. `node .opencode/skills/forge/scripts/inspect.mjs --json` — full audit
15
+ 7. `node .opencode/skills/forge/scripts/architecture.mjs` — update ARCHITECTURE.md
16
+ 8. Execute user command (cast, quench, relocate, etc.)
17
+ 9. Run architecture.mjs again
18
+
19
+ ## Architecture Model
20
+
21
+ Cuatro capas obligatorias:
22
+
23
+ | Layer | Propósito |
24
+ |-------|-----------|
25
+ | `src/platform/` | config, database, http, server, logger, cache, security, events, di |
26
+ | `src/features/<name>/` | domain/, application/use-cases/, application/mappers/, adapters/in/http/, adapters/out/persistence/ |
27
+ | `src/shared/` | errors/, contracts/, types/, utils/ (puro, sin lógica de negocio) |
28
+ | `src/infra/` | prisma/, mongodb/, redis/, mail/ (implementaciones, sin reglas de negocio) |
29
+
30
+ ### Dependency Rules
31
+
32
+ **Permitido:** `feature → platform`, `feature → shared`, `platform → infra`, `adapter → infra`, `feature → domain`
33
+
34
+ **Prohibido (CRITICAL):**
35
+ - R1: `feature → infra`
36
+ - R2: `platform → feature`
37
+ - R3: `shared → feature`
38
+ - R4: `shared → infra`
39
+ - R5: `domain → infra`
40
+ - R6: `domain → platform`
41
+ - R7: `infra → feature`
42
+ - R8: cross-feature direct imports
43
+ - R9: cycles
44
+
45
+ ## Naming Conventions
46
+
47
+ | Elemento | Formato |
48
+ |----------|---------|
49
+ | Directorios | `kebab-case/` |
50
+ | Archivos | `<PascalCase>.<artefacto>.ts` |
51
+ | Interfaces | `I<PascalCase>.<artefacto>.ts` |
52
+ | Use cases | `<Action>.uc.ts` |
53
+ | Clases | `PascalCase` |
54
+ | Funciones/variables | `camelCase` |
55
+ | Constantes | `UPPER_SNAKE_CASE` |
56
+ | Barrel files | `index.ts` con named exports, no `export default` |
57
+ | Imports ESM | con extensión `.js`: `import { X } from "./foo.js"` |
58
+
59
+ ## Commands
60
+
61
+ | Comando | Acción |
62
+ |---------|--------|
63
+ | `forge` | Init project (context + bootstrap + profile + graph) |
64
+ | `cast` | New feature (verifica platform/shared/infra primero) |
65
+ | `inspect` | Full audit (6 categorías, 110pts → 0-100) |
66
+ | `quench` | Validate dependency rules |
67
+ | `chain` | Topological dependency sort |
68
+ | `graph` | Architecture graph con R1-R9 |
69
+ | `armorer` | Ownership report (orphans, duplicates, misplaced) |
70
+ | `smelt` | Extract code to shared/ |
71
+ | `relocate` | Migrate legacy to platform/, shared/, infra/ or features/ |
72
+ | `reforge` | Refactor considering all 4 layers |
73
+ | `temper` | Harden DI (constructor injection) |
74
+ | `inscribe` | Generate ARCHITECTURE.md |
75
+
76
+ ## Architecture Principles
77
+
78
+ 1. **Hexagonal basado en features** — unidad de organización = feature, no capa técnica
79
+ 2. **DDD ligero** — sin sobreingeniería, pragmatismo sobre dogma
80
+ 3. **Separación dominio e infraestructura** — domain/ no sabe de frameworks ni BD
81
+ 4. **Feature autónomo** — todo lo del dominio vive dentro de `features/<name>/`
82
+ 5. **Dependencias unidireccionales** — `adapters → application → domain → (nada)`
83
+ 6. **Cero lógica en controllers** — parsean, delegan, responden
84
+ 7. **Cero BD fuera de repositories** — única puerta a datos
85
+ 8. **DI disciplinada** — constructor injection, sin service locators
86
+ 9. **Errores tipados** — clases explícitas, no `throw Error()`
87
+ 10. **Grafo arquitectónico vivo** — todo componente es un nodo, toda relación un edge validado
88
+
89
+ ## Key Files
90
+
91
+ - `skills/forge/SKILL.md` — orchestrator principal
92
+ - `skills/forge/reference/principles.md` — manifiesto y 15 principios
93
+ - `skills/forge/reference/patterns.md` — naming conventions
94
+ - `skills/forge/scripts/` — context, detect, inspect, chain, profile, graph, architecture, armorer, bootstrap
95
+ - `skills/forge/profiles/` — 10 tech profiles (express, fastify, nestjs × mongodb, postgres, prisma, drizzle)
96
+ - `skills/forge/templates/` — templates para feature, platform, shared, infra
97
+ - `AGENTS.md` — guía para agentes de IA
98
+ - `ARCHITECTURE.md` — estado actual de la arquitectura
@@ -0,0 +1,80 @@
1
+ You are an AI assistant for a project that uses Forge — a Backend Architecture Operating System. Tu función es ayudar a diseñar, construir, auditar y evolucionar la arquitectura usando Arquitectura Hexagonal, DDD pragmático y vertical slices.
2
+
3
+ ## Architecture Model
4
+
5
+ The project uses four mandatory layers:
6
+
7
+ ```
8
+ src/
9
+ ├── platform/ # Config, database, http, server, logger, cache, security, events, di
10
+ ├── features/<name>/ # domain/, application/use-cases/, application/mappers/, adapters/in/http/, adapters/out/persistence/
11
+ ├── shared/ # errors/, contracts/, types/, utils/ (pure code, no business logic)
12
+ └── infra/ # prisma/, mongodb/, redis/, mail/ (implementations, no business rules)
13
+ ```
14
+
15
+ ## Dependency Rules (CRITICAL — never violate)
16
+
17
+ **Allowed:** `feature → platform`, `feature → shared`, `platform → infra`, `adapter → infra`, `feature → domain`
18
+
19
+ **Prohibited:**
20
+ - R1: `feature → infra`
21
+ - R2: `platform → feature`
22
+ - R3: `shared → feature`
23
+ - R4: `shared → infra`
24
+ - R5: `domain → infra`
25
+ - R6: `domain → platform`
26
+ - R7: `infra → feature`
27
+ - R8: cross-feature direct imports
28
+ - R9: cycles
29
+
30
+ ## Naming Conventions
31
+
32
+ | Element | Convention | Example |
33
+ |---------|-----------|---------|
34
+ | Directories | `kebab-case/` | `credit-card/` |
35
+ | Files | `<PascalCase>.<artifact>.ts` | `User.entity.ts` |
36
+ | Interfaces | `I<PascalCase>.<artifact>.ts` | `IUser.repository.ts` |
37
+ | Use cases | `<Action>.uc.ts` | `CreateUser.uc.ts` |
38
+ | Classes | `PascalCase` | `UserController` |
39
+ | Functions/vars | `camelCase` | `formatDate` |
40
+ | Constants | `UPPER_SNAKE_CASE` | `MAX_RETRY_COUNT` |
41
+ | Barrels | `index.ts` | named exports only, no `export default` |
42
+ | ESM imports | with `.js` extension | `import { X } from "./foo.js"` |
43
+
44
+ ## Code Principles
45
+
46
+ 1. **Zero business logic in controllers** — parse, delegate, respond
47
+ 2. **Zero DB access outside repositories** — only repositories touch the database
48
+ 3. **Constructor injection only** — no service locators, no `container.resolve()` in business logic
49
+ 4. **Typed domain errors** — explicit error classes, never generic `throw Error()`
50
+ 5. **Unidirectional dependencies** — `adapters → application → domain → (nothing)`
51
+ 6. **Domain knows nothing about infrastructure** — no framework, database, or external service imports in domain/
52
+ 7. **Features are autonomous** — no direct imports between features, only via injected interfaces
53
+ 8. **Explicit over magic** — prefer explicit imports and declared types over hidden decorators
54
+
55
+ ## Available Commands
56
+
57
+ | Command | Action |
58
+ |---------|--------|
59
+ | `forge` | Initialize project |
60
+ | `cast` | Create new feature |
61
+ | `inspect` | Full architecture audit |
62
+ | `quench` | Validate dependency rules |
63
+ | `chain` | Dependency graph (topological) |
64
+ | `graph` | Architecture graph with rules |
65
+ | `armorer` | Ownership report |
66
+ | `smelt` | Extract to shared/ |
67
+ | `relocate` | Migrate legacy code |
68
+ | `reforge` | Refactor architecture |
69
+ | `temper` | Harden dependency injection |
70
+ | `inscribe` | Generate ARCHITECTURE.md |
71
+
72
+ ## Key Files Reference
73
+
74
+ - `skills/forge/SKILL.md` — orchestrator
75
+ - `skills/forge/reference/principles.md` — 15 principles
76
+ - `skills/forge/reference/patterns.md` — naming conventions
77
+ - `skills/forge/scripts/` — all engine scripts
78
+ - `skills/forge/profiles/` — tech profiles
79
+ - `AGENTS.md` — agent guide
80
+ - `ARCHITECTURE.md` — current architecture state
package/src/agents.mjs ADDED
@@ -0,0 +1,55 @@
1
+ import { existsSync } from "fs";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+
5
+ const HOME = homedir();
6
+
7
+ function detect(cwd) {
8
+ const agents = [];
9
+
10
+ agents.push({
11
+ id: "claude-global",
12
+ label: "Claude Code",
13
+ scope: "Global",
14
+ detected: existsSync(join(HOME, ".claude")) || existsSync(join(HOME, ".config", "claude")),
15
+ });
16
+
17
+ agents.push({
18
+ id: "claude-project",
19
+ label: "Claude Code",
20
+ scope: "Proyecto",
21
+ detected: existsSync(join(cwd, ".claude")) || existsSync(join(cwd, "CLAUDE.md")),
22
+ });
23
+
24
+ agents.push({
25
+ id: "opencode-global",
26
+ label: "OpenCode",
27
+ scope: "Global",
28
+ detected: existsSync(join(HOME, ".config", "opencode")),
29
+ });
30
+
31
+ agents.push({
32
+ id: "opencode-project",
33
+ label: "OpenCode",
34
+ scope: "Proyecto",
35
+ detected: existsSync(join(cwd, ".opencode")),
36
+ });
37
+
38
+ agents.push({
39
+ id: "copilot-project",
40
+ label: "GitHub Copilot",
41
+ scope: "Proyecto",
42
+ detected: existsSync(join(cwd, ".github", "copilot-instructions.md")),
43
+ });
44
+
45
+ agents.push({
46
+ id: "codex",
47
+ label: "Codex CLI",
48
+ scope: "Global",
49
+ detected: existsSync(join(HOME, ".codex")),
50
+ });
51
+
52
+ return agents;
53
+ }
54
+
55
+ export { detect as detectForWizard };
package/src/cli.js CHANGED
@@ -1,13 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { copyFileSync, mkdirSync, existsSync, readdirSync, statSync, writeFileSync, readFileSync } from "fs";
3
+ import { copyFileSync, mkdirSync, existsSync, writeFileSync, readFileSync, cpSync } from "fs";
4
4
  import { join, dirname, relative } from "path";
5
5
  import { fileURLToPath } from "url";
6
6
  import { execSync } from "child_process";
7
+ import { spinner, log } from "@clack/prompts";
8
+ import { runWizard } from "./wizard.mjs";
7
9
 
8
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
9
- const PKG_DIR = join(__dirname, "..");
10
- const SKILL_SRC = join(PKG_DIR, "skills", "forge");
11
+ const SKILL_SRC = join(__dirname, "..", "skills", "forge");
12
+ const AGENTS_TEMPLATES = join(SKILL_SRC, "templates", "agents");
11
13
 
12
14
  function getTargetDir(global) {
13
15
  if (global) {
@@ -30,16 +32,7 @@ function getConfigDir(global) {
30
32
  }
31
33
 
32
34
  function copyRecursive(src, dest) {
33
- mkdirSync(dest, { recursive: true });
34
- for (const entry of readdirSync(src)) {
35
- const srcPath = join(src, entry);
36
- const destPath = join(dest, entry);
37
- if (statSync(srcPath).isDirectory()) {
38
- copyRecursive(srcPath, destPath);
39
- } else {
40
- copyFileSync(srcPath, destPath);
41
- }
42
- }
35
+ cpSync(src, dest, { recursive: true, force: true });
43
36
  }
44
37
 
45
38
  function detectPM(configDir) {
@@ -62,7 +55,6 @@ function ensureDependencies(configDir) {
62
55
  if (!pkg.dependencies["@opencode-ai/plugin"]) {
63
56
  pkg.dependencies["@opencode-ai/plugin"] = "1.17.9";
64
57
  writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
65
- console.log(` ✓ Actualizado ${relative(process.cwd(), pkgPath)}`);
66
58
  }
67
59
 
68
60
  const pm = detectPM(configDir);
@@ -74,30 +66,28 @@ function ensureDependencies(configDir) {
74
66
 
75
67
  try {
76
68
  runPM(pm);
77
- console.log(` ✓ Dependencias instaladas (${pm})`);
78
69
  } catch {
79
70
  for (const fallback of order[pm]) {
80
71
  try {
81
72
  runPM(fallback);
82
- console.log(` ✓ Dependencias instaladas (${fallback})`);
83
73
  return;
84
- } catch {}
74
+ } catch { }
85
75
  }
86
- console.warn("No se pudieron instalar dependencias. Ejecuta 'pnpm install' | 'npm install' manualmente en " + configDir);
76
+ log.warn("No se pudieron instalar dependencias. Ejecutá 'pnpm install' | 'npm install' manualmente en " + configDir);
87
77
  }
88
78
  }
89
79
 
90
80
  const COMMANDS = [
91
- { name: "forge-forge", desc: "Forge — inicializar proyecto arquitectónicamente" },
92
- { name: "forge-cast", desc: "Cast — crear un nuevo feature hexagonal desde cero" },
81
+ { name: "forge-forge", desc: "Forge — inicializar proyecto arquitectónicamente" },
82
+ { name: "forge-cast", desc: "Cast — crear un nuevo feature hexagonal desde cero" },
93
83
  { name: "forge-inspect", desc: "Inspect — inspeccionar la conformidad arquitectónica" },
94
- { name: "forge-relocate",desc: "Relocate — migrar feature legacy a la nueva estructura" },
84
+ { name: "forge-relocate", desc: "Relocate — migrar feature legacy a la nueva estructura" },
95
85
  { name: "forge-reforge", desc: "Reforge — refactorizar la arquitectura de un feature" },
96
- { name: "forge-quench", desc: "Quench — verificar reglas arquitectónicas del proyecto" },
97
- { name: "forge-temper", desc: "Temper — endurecer la arquitectura (DI, seguridad)" },
98
- { name: "forge-chain", desc: "Chain — analizar cadena de dependencias entre features" },
99
- { name: "forge-inscribe",desc: "Inscribe — generar y mantener ARCHITECTURE.md" },
100
- { name: "forge-smelt", desc: "Smelt — extraer código reutilizable a shared/" },
86
+ { name: "forge-quench", desc: "Quench — verificar reglas arquitectónicas del proyecto" },
87
+ { name: "forge-temper", desc: "Temper — endurecer la arquitectura (DI, seguridad)" },
88
+ { name: "forge-chain", desc: "Chain — analizar cadena de dependencias entre features" },
89
+ { name: "forge-inscribe", desc: "Inscribe — generar y mantener ARCHITECTURE.md" },
90
+ { name: "forge-smelt", desc: "Smelt — extraer código reutilizable a shared/" },
101
91
  ];
102
92
 
103
93
  function generateCommands(configDir) {
@@ -116,46 +106,111 @@ function generateCommands(configDir) {
116
106
  ].join("\n");
117
107
  writeFileSync(join(cmdsDir, `${cmd.name}.md`), content);
118
108
  }
119
- console.log(` ✓ Comandos /forge-* generados en .opencode/commands/`);
120
109
  }
121
110
 
122
- function printHelp() {
123
- console.log(`
124
- Forge — Architecture OS
125
-
126
- USO
127
- forge install Instalar skill en el proyecto actual
128
- forge install --global Instalar globalmente (~/.config/opencode/)
129
- forge skills install (alias del comando install)
130
-
131
- OPCIONES
132
- -g, --global Instalar en ~/.config/opencode/skills/forge/
133
- -h, --help Mostrar esta ayuda
134
- `);
135
- }
111
+ // --- Direct installers ---
136
112
 
137
- function install(global) {
113
+ async function installOpenCode(isGlobal = false) {
138
114
  if (!existsSync(SKILL_SRC)) {
139
- console.error("[ERROR] No se encuentra skills/forge/ en el paquete. ¿Corrupto?");
115
+ log.error("No se encuentra skills/forge/ en el paquete. ¿Corrupto?");
140
116
  process.exit(1);
141
117
  }
142
118
 
143
- const target = getTargetDir(global);
144
- const configDir = getConfigDir(global);
119
+ const target = getTargetDir(isGlobal);
120
+ const configDir = getConfigDir(isGlobal);
121
+ const rel = relative(process.cwd(), target);
145
122
 
146
- console.log(`\n Forge Architecture OS`);
147
- console.log(` ${"=".repeat(40)}`);
148
- console.log(` Destino: ${target}\n`);
123
+ const s = spinner();
149
124
 
125
+ s.start("Copiando skill a " + rel);
150
126
  copyRecursive(SKILL_SRC, target);
151
- console.log(` ✓ Skill copiada a ${relative(process.cwd(), target)}`);
127
+ s.stop("Skill copiada a " + rel);
128
+
129
+ s.start("Generando comandos /forge-*");
152
130
  generateCommands(configDir);
131
+ s.stop("Comandos generados en " + relative(process.cwd(), join(configDir, "commands")));
132
+
133
+ s.start("Instalando dependencias");
153
134
  ensureDependencies(configDir);
154
- console.log(`\n ✓ Forge instalado correctamente`);
155
- console.log(` ${"=".repeat(40)}\n`);
135
+ s.stop("Dependencias instaladas");
136
+
137
+ log.success("OpenCode configurado correctamente");
138
+ }
139
+
140
+ async function installCursor() {
141
+ const src = join(AGENTS_TEMPLATES, "cursor", ".cursorrules");
142
+ const dest = join(process.cwd(), ".cursorrules");
143
+
144
+ if (!existsSync(src)) {
145
+ log.error("Template .cursorrules no encontrado");
146
+ process.exit(1);
147
+ }
148
+
149
+ const s = spinner();
150
+ s.start("Creando .cursorrules");
151
+ copyFileSync(src, dest);
152
+ s.stop(".cursorrules creado");
153
+
154
+ log.success("Cursor configurado correctamente");
156
155
  }
157
156
 
158
- function main() {
157
+ 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");
161
+
162
+ if (!existsSync(src)) {
163
+ log.error("Template CLAUDE.md no encontrado");
164
+ process.exit(1);
165
+ }
166
+
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");
172
+
173
+ log.success("Claude Code configurado correctamente");
174
+ }
175
+
176
+ async function installAgents(selected, isGlobal = false) {
177
+ for (const agent of selected) {
178
+ switch (agent) {
179
+ case "opencode":
180
+ await installOpenCode(isGlobal);
181
+ break;
182
+ case "cursor":
183
+ await installCursor();
184
+ break;
185
+ case "claude":
186
+ await installClaude();
187
+ break;
188
+ }
189
+ }
190
+ }
191
+
192
+ // --- CLI ---
193
+
194
+ function printHelp() {
195
+ console.log(`
196
+ ⚔️ Forge — Architecture OS
197
+
198
+ USO
199
+ forge install Mostrar wizard interactivo de instalación
200
+ forge install --opencode Instalar solo para OpenCode
201
+ forge install --cursor Instalar solo para Cursor
202
+ forge install --claude Instalar solo para Claude Code
203
+ forge install --all Instalar para todos los agentes
204
+ forge install --global Instalar OpenCode globalmente (~/.config/opencode/)
205
+ forge install --help Mostrar esta ayuda
206
+
207
+ OPCIONES
208
+ -g, --global Instalar OpenCode en ~/.config/opencode/skills/forge/
209
+ -h, --help Mostrar esta ayuda
210
+ `);
211
+ }
212
+
213
+ async function main() {
159
214
  const args = process.argv.slice(2);
160
215
  const isHelp = args.includes("-h") || args.includes("--help");
161
216
  const isGlobal = args.includes("-g") || args.includes("--global");
@@ -165,15 +220,33 @@ function main() {
165
220
  return;
166
221
  }
167
222
 
223
+ const hasOpenCode = args.includes("--opencode");
224
+ const hasCursor = args.includes("--cursor");
225
+ const hasClaude = args.includes("--claude");
226
+ const hasAll = args.includes("--all");
227
+
168
228
  const subcommandIndex = args.indexOf("skills");
169
229
  const command = subcommandIndex !== -1 ? args[subcommandIndex + 1] : args[0];
170
230
 
171
231
  if (command === "install") {
172
- install(isGlobal);
232
+ if (hasAll) {
233
+ await installAgents(["opencode", "cursor", "claude"], isGlobal);
234
+ } else if (hasOpenCode || hasCursor || hasClaude) {
235
+ const selected = [];
236
+ if (hasOpenCode) selected.push("opencode");
237
+ if (hasCursor) selected.push("cursor");
238
+ if (hasClaude) selected.push("claude");
239
+ await installAgents(selected, isGlobal);
240
+ } else {
241
+ await runWizard();
242
+ }
173
243
  } else {
174
244
  printHelp();
175
245
  process.exit(1);
176
246
  }
177
247
  }
178
248
 
179
- main();
249
+ main().catch((err) => {
250
+ console.error("[ERROR]", err.message);
251
+ process.exit(1);
252
+ });
package/src/wizard.mjs ADDED
@@ -0,0 +1,474 @@
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
+ // --- Welcome ---
97
+
98
+ async function welcomePhase() {
99
+ console.clear();
100
+ printCenter(pc.bold(pc.cyan("\u2694 Forge")), 22);
101
+ console.log();
102
+ printCenter(pc.dim("Sistema Operativo de Arquitectura"), 17);
103
+ console.log();
104
+ console.log(" " + SEP);
105
+ console.log();
106
+ console.log(" Bienvenido a Forge.");
107
+ console.log();
108
+ console.log(" Forge instalar\u00e1 la Skill de Arquitectura");
109
+ console.log(" en uno o varios agentes de IA compatibles.");
110
+ console.log();
111
+
112
+ const result = await text({
113
+ message: "Presiona Enter para continuar",
114
+ placeholder: "",
115
+ initialValue: "",
116
+ });
117
+
118
+ if (isCancel(result)) {
119
+ cancel("Cancelado, no pasa nada");
120
+ process.exit(0);
121
+ }
122
+ }
123
+
124
+ // --- Detection ---
125
+
126
+ async function detectionPhase(cwd) {
127
+ console.clear();
128
+ log.message(pc.cyan("\u2694 Detectando agentes compatibles..."));
129
+
130
+ const s = spinner();
131
+ s.start("Buscando instalaciones...");
132
+
133
+ const agents = detectForWizard(cwd);
134
+ s.stop("B\u00fasqueda completada");
135
+ console.log();
136
+
137
+ const detectedCount = agents.filter((a) => a.detected).length;
138
+
139
+ for (const agent of agents) {
140
+ const label = agent.label.padEnd(20);
141
+ const scope = agent.scope;
142
+ if (agent.detected) {
143
+ console.log(` ${pc.green("\u2714")} ${label}${pc.dim(scope)}`);
144
+ } else {
145
+ console.log(` ${pc.red("\u2718")} ${label}${pc.dim(scope)}`);
146
+ }
147
+ }
148
+
149
+ console.log();
150
+ log.success(`Se encontraron ${detectedCount} instalaciones compatibles.`);
151
+ console.log();
152
+
153
+ await text({
154
+ message: "Presiona Enter para continuar",
155
+ placeholder: "",
156
+ initialValue: "",
157
+ });
158
+
159
+ return agents;
160
+ }
161
+
162
+ // --- Agent selection ---
163
+
164
+ async function selectionPhase(agents) {
165
+ console.clear();
166
+ log.message(pc.cyan("\u2694 Selecciona los agentes"));
167
+ console.log();
168
+
169
+ const options = agents.map((a) => ({
170
+ value: a.id,
171
+ label: `${a.label} (${a.scope})`,
172
+ hint: a.detected ? "disponible" : undefined,
173
+ }));
174
+
175
+ options.push({ value: "__custom__", label: "Ruta personalizada" });
176
+
177
+ const selected = await multiselect({
178
+ message: "Espacio = seleccionar | Enter = continuar",
179
+ options,
180
+ required: false,
181
+ });
182
+
183
+ if (isCancel(selected)) {
184
+ cancel("Cancelado, no pasa nada");
185
+ process.exit(0);
186
+ }
187
+
188
+ let customPath = null;
189
+ if (selected.includes("__custom__")) {
190
+ customPath = await text({
191
+ message: "\u00bfD\u00f3nde deseas instalar Forge?",
192
+ placeholder: "/home/usuario/.claude",
193
+ validate: (v) => (v ? undefined : "La ruta es requerida"),
194
+ });
195
+
196
+ if (isCancel(customPath)) {
197
+ cancel("Cancelado, no pasa nada");
198
+ process.exit(0);
199
+ }
200
+
201
+ if (existsSync(customPath)) {
202
+ log.success("Ruta v\u00e1lida");
203
+ } else {
204
+ log.info("La ruta no existe, se crear\u00e1 al instalar");
205
+ }
206
+ }
207
+
208
+ return { selectedIds: selected.filter((id) => id !== "__custom__"), customPath };
209
+ }
210
+
211
+ // --- Summary ---
212
+
213
+ async function summaryPhase(agentSelection) {
214
+ const allAgents = detectForWizard(process.cwd());
215
+ const selectedAgents = agentSelection.selectedIds
216
+ .map((id) => allAgents.find((a) => a.id === id))
217
+ .filter(Boolean);
218
+
219
+ while (true) {
220
+ console.clear();
221
+ log.message(pc.cyan("\u2694 Resumen"));
222
+ console.log(" " + SEP);
223
+ console.log();
224
+ console.log(` ${pc.bold("Agentes")}`);
225
+ console.log();
226
+
227
+ for (const agent of selectedAgents) {
228
+ console.log(` \u2022 ${agent.label} ${pc.dim(`(${agent.scope})`)}`);
229
+ }
230
+ if (agentSelection.customPath) {
231
+ console.log(` \u2022 Ruta personalizada ${pc.dim(`(${agentSelection.customPath})`)}`);
232
+ }
233
+
234
+ console.log();
235
+ console.log(` ${pc.bold("Componentes")}`);
236
+ console.log();
237
+
238
+ const components = [
239
+ "Forge Skill",
240
+ "Reglas",
241
+ "Prompts",
242
+ "Comandos",
243
+ "Plantillas",
244
+ "Agentes",
245
+ ];
246
+ for (const comp of components) {
247
+ console.log(` ${pc.green("\u2714")} ${comp}`);
248
+ }
249
+
250
+ console.log();
251
+ console.log(" " + SEP);
252
+ console.log();
253
+
254
+ const action = await select({
255
+ message: "\u00bfDeseas continuar?",
256
+ options: [
257
+ { value: "install", label: "Instalar" },
258
+ { value: "back", label: "Volver" },
259
+ { value: "cancel", label: "Cancelar" },
260
+ ],
261
+ });
262
+
263
+ if (isCancel(action) || action === "cancel") {
264
+ cancel("Cancelado, no pasa nada");
265
+ process.exit(0);
266
+ }
267
+
268
+ if (action === "back") {
269
+ return "back";
270
+ }
271
+
272
+ return { action: "install", selectedAgents, customPath: agentSelection.customPath };
273
+ }
274
+ }
275
+
276
+ // --- Installation ---
277
+
278
+ function buildAgentSteps(agent, cwd) {
279
+ const label = `${agent.label} (${agent.scope})`;
280
+ const s = (title, fn) => ({ title: `${pc.dim("\u2502")} ${title}`, task: fn });
281
+
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
+ }
320
+
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
+ }
341
+
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
+ }
355
+
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
+ }
370
+
371
+ default:
372
+ return null;
373
+ }
374
+ }
375
+
376
+ async function installPhase(result, cwd) {
377
+ console.clear();
378
+ log.message(pc.cyan("\u2694 Instalando Forge..."));
379
+
380
+ const agentsData = [];
381
+ for (const agent of result.selectedAgents) {
382
+ const data = buildAgentSteps(agent, cwd);
383
+ if (data) agentsData.push(data);
384
+ }
385
+
386
+ if (result.customPath) {
387
+ const dest = result.customPath;
388
+ const steps = [];
389
+ const s = (title, fn) => ({ title: `${pc.dim("\u2502")} ${title}`, task: fn });
390
+ steps.push(s("Instalando Forge en ruta personalizada", async () => {
391
+ mkdirSync(dest, { recursive: true });
392
+ cpSync(SKILL_SRC, join(dest, "forge"), { recursive: true, force: true });
393
+ copyAgentTemplate("claude", dest);
394
+ return "Forge instalado";
395
+ }));
396
+ steps.push(s("Verificando instalaci\u00f3n", async () => {
397
+ return existsSync(join(dest, "forge")) ? "\u2714 Instalaci\u00f3n verificada" : "ERROR";
398
+ }));
399
+ agentsData.push({ label: `Ruta personalizada (${dest})`, dest, steps });
400
+ }
401
+
402
+ const allSteps = [];
403
+ for (const ad of agentsData) {
404
+ allSteps.push({
405
+ title: pc.bold(pc.cyan(ad.label)),
406
+ task: async () => `Instalando en ${ad.dest}`,
407
+ });
408
+ for (const step of ad.steps) {
409
+ allSteps.push(step);
410
+ }
411
+ }
412
+
413
+ await tasks(allSteps);
414
+ }
415
+
416
+ // --- Main wizard orchestrator ---
417
+
418
+ export async function runWizard() {
419
+ const cwd = process.cwd();
420
+
421
+ await welcomePhase();
422
+
423
+ console.clear();
424
+ intro(pc.cyan("\u2694 Forge"));
425
+
426
+ const agents = await detectionPhase(cwd);
427
+
428
+ let agentSelection;
429
+ let result;
430
+
431
+ while (true) {
432
+ agentSelection = await selectionPhase(agents);
433
+
434
+ if (agentSelection.selectedIds.length === 0 && !agentSelection.customPath) {
435
+ log.warn("Seleccion\u00e1 al menos un agente o una ruta personalizada.");
436
+ continue;
437
+ }
438
+
439
+ result = await summaryPhase(agentSelection);
440
+
441
+ if (result === "back") continue;
442
+ break;
443
+ }
444
+
445
+ await installPhase(result, cwd);
446
+
447
+ console.log();
448
+ console.log(" " + SEP);
449
+ printCenter(pc.bold(pc.cyan("\u2694 Forge")), 22);
450
+ console.log();
451
+ printCenter(pc.green("Instalaci\u00f3n completada correctamente."), 12);
452
+ console.log();
453
+ console.log(` Forge se instal\u00f3 en:`);
454
+ console.log();
455
+
456
+ for (const agent of result.selectedAgents) {
457
+ console.log(` ${pc.green("\u2714")} ${agent.label} ${pc.dim(`(${agent.scope})`)}`);
458
+ }
459
+ if (result.customPath) {
460
+ console.log(` ${pc.green("\u2714")} Ruta personalizada ${pc.dim(`(${result.customPath})`)}`);
461
+ }
462
+
463
+ console.log();
464
+ console.log(` ${pc.bold("Pr\u00f3ximos pasos")}`);
465
+ console.log();
466
+ console.log(` \u2022 Reinicia tu agente si est\u00e1 abierto.`);
467
+ console.log(` \u2022 Ejecut\u00e1 el comando "forge" dentro del agente.`);
468
+ console.log(` \u2022 Consult\u00e1 la documentaci\u00f3n para comenzar.`);
469
+ console.log();
470
+ console.log(" " + SEP);
471
+ console.log();
472
+
473
+ outro(pc.cyan("\u00a1Forge est\u00e1 listo para usar! \u2694"));
474
+ }
package/logo.png DELETED
Binary file