@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 +2 -2
- package/package.json +5 -2
- package/skills/forge/templates/agents/claude/CLAUDE.md +98 -0
- package/skills/forge/templates/agents/cursor/.cursorrules +80 -0
- package/src/agents.mjs +55 -0
- package/src/cli.js +127 -54
- package/src/wizard.mjs +474 -0
- package/logo.png +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
<img src="
|
|
1
|
+
<img src="favicon.svg" alt="Forge Logo" width="100" height="100">
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
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,
|
|
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
|
|
10
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
92
|
-
{ name: "forge-cast",
|
|
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",
|
|
97
|
-
{ name: "forge-temper",
|
|
98
|
-
{ name: "forge-chain",
|
|
99
|
-
{ name: "forge-inscribe",desc: "Inscribe — generar y mantener ARCHITECTURE.md" },
|
|
100
|
-
{ name: "forge-smelt",
|
|
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
|
-
|
|
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
|
|
113
|
+
async function installOpenCode(isGlobal = false) {
|
|
138
114
|
if (!existsSync(SKILL_SRC)) {
|
|
139
|
-
|
|
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(
|
|
144
|
-
const configDir = getConfigDir(
|
|
119
|
+
const target = getTargetDir(isGlobal);
|
|
120
|
+
const configDir = getConfigDir(isGlobal);
|
|
121
|
+
const rel = relative(process.cwd(), target);
|
|
145
122
|
|
|
146
|
-
|
|
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
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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
|
|
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
|
-
|
|
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
|