@cristiancorreau/forge 2.9.6 → 2.9.7
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/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +2 -1
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.js +1 -1
- package/dist/lib/paths.d.ts +1 -2
- package/dist/lib/paths.d.ts.map +1 -1
- package/dist/lib/paths.js +12 -16
- package/dist/lib/paths.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/assets/adapters/claude-code/generate-claude-md.py +0 -304
- package/assets/adapters/codex/generate-codex-config.py +0 -269
- package/assets/adapters/kiro/generate-steering.py +0 -367
- package/assets/adapters/opencode/generate-agents-md.py +0 -262
- package/assets/core/hooks/pre-bash-check.py +0 -202
- package/assets/core/hooks/pre-edit-check.py +0 -317
- package/assets/forge.py +0 -1265
- package/assets/requirements.txt +0 -2
- package/assets/scripts/aitmpl-search.py +0 -808
- package/assets/scripts/forge-add-opportunities.py +0 -92
- package/assets/scripts/forge-audit.py +0 -1061
- package/assets/scripts/forge-generate-all.py +0 -283
- package/assets/scripts/forge-init.py +0 -900
- package/assets/scripts/forge-migrate-project-yaml.py +0 -397
- package/assets/scripts/forge-scaffold-profile.py +0 -181
- package/assets/scripts/forge-teardown.py +0 -193
- package/assets/scripts/forge-validate-project-yaml.py +0 -457
- package/assets/scripts/forge-wizard.py +0 -1003
- package/assets/scripts/setup-codex.sh +0 -229
- package/assets/scripts/team-install.sh +0 -147
- package/assets/scripts/token-stats.py +0 -201
- package/dist/lib/python.d.ts +0 -4
- package/dist/lib/python.d.ts.map +0 -1
- package/dist/lib/python.js +0 -46
- package/dist/lib/python.js.map +0 -1
|
@@ -1,1003 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
forge-wizard.py — Wizard interactivo para configurar un proyecto nuevo con forge.
|
|
4
|
-
|
|
5
|
-
Usage interactivo:
|
|
6
|
-
python3 .agentic/scripts/forge-wizard.py
|
|
7
|
-
python3 .agentic/scripts/forge-wizard.py --dry-run
|
|
8
|
-
python3 .agentic/scripts/forge-wizard.py --mode startup
|
|
9
|
-
python3 .agentic/scripts/forge-wizard.py --mode enterprise --no-init
|
|
10
|
-
|
|
11
|
-
Modo batch (no interactivo, para CI y scripts de onboarding):
|
|
12
|
-
python3 .agentic/scripts/forge-wizard.py \\
|
|
13
|
-
--name "Mi Proyecto" --backend laravel --frontend none \\
|
|
14
|
-
--database postgresql --deploy vercel \\
|
|
15
|
-
--mode standard --tool claude-code --output project.yaml
|
|
16
|
-
|
|
17
|
-
Flags batch disponibles:
|
|
18
|
-
--name NAME Nombre del proyecto
|
|
19
|
-
--slug SLUG Slug (se auto-genera desde --name si no se especifica)
|
|
20
|
-
--desc DESC Descripción breve del proyecto
|
|
21
|
-
--type TYPE Tipo: webapp|api|fullstack|saas|mobile|static|cli|crawler|wordpress
|
|
22
|
-
--backend BACKEND Framework backend (hono|fastapi|django|rails|express|nestjs|laravel|gin|none)
|
|
23
|
-
--frontend FRONTEND Framework frontend (nextjs|nuxt|sveltekit|astro|react|vue|angular|none)
|
|
24
|
-
--database DB Base de datos (postgresql|mysql|sqlite|mongodb|redis|none)
|
|
25
|
-
--deploy TARGET Deploy target (vercel|railway|fly|aws|gcp|azure|vps|none)
|
|
26
|
-
--tool TOOL Runtime (claude-code|opencode|kiro|codex|all)
|
|
27
|
-
--compliance LIST Frameworks de compliance separados por coma (gdpr,ley-21719,...)
|
|
28
|
-
--page-builder PB Page builder WordPress (divi|elementor|gutenberg|none)
|
|
29
|
-
--output PATH Ruta de salida (default: project.yaml en CWD)
|
|
30
|
-
--no-init No ejecutar forge-init después de generar project.yaml
|
|
31
|
-
--dry-run Mostrar el YAML sin escribirlo
|
|
32
|
-
"""
|
|
33
|
-
from __future__ import annotations
|
|
34
|
-
|
|
35
|
-
import os
|
|
36
|
-
import sys
|
|
37
|
-
import textwrap
|
|
38
|
-
try:
|
|
39
|
-
import termios
|
|
40
|
-
import tty
|
|
41
|
-
except ImportError:
|
|
42
|
-
print(
|
|
43
|
-
"ERROR: forge-wizard requiere Unix o macOS.\n"
|
|
44
|
-
" Los módulos 'termios' y 'tty' no están disponibles en Windows.\n"
|
|
45
|
-
" Alternativas: WSL (Windows Subsystem for Linux) o ejecutar en macOS/Linux.",
|
|
46
|
-
file=sys.stderr,
|
|
47
|
-
)
|
|
48
|
-
sys.exit(1)
|
|
49
|
-
import yaml
|
|
50
|
-
from pathlib import Path
|
|
51
|
-
from typing import List, Optional, Tuple
|
|
52
|
-
|
|
53
|
-
# ---------------------------------------------------------------------------
|
|
54
|
-
# Flags
|
|
55
|
-
# ---------------------------------------------------------------------------
|
|
56
|
-
|
|
57
|
-
DRY_RUN = "--dry-run" in sys.argv
|
|
58
|
-
NO_INIT = "--no-init" in sys.argv
|
|
59
|
-
|
|
60
|
-
MODE_OVERRIDE: Optional[str] = None
|
|
61
|
-
for _arg in sys.argv[1:]:
|
|
62
|
-
if _arg.startswith("--mode="):
|
|
63
|
-
MODE_OVERRIDE = _arg.split("=", 1)[1].strip()
|
|
64
|
-
break
|
|
65
|
-
if MODE_OVERRIDE is None and "--mode" in sys.argv:
|
|
66
|
-
_idx = sys.argv.index("--mode")
|
|
67
|
-
if _idx + 1 < len(sys.argv):
|
|
68
|
-
MODE_OVERRIDE = sys.argv[_idx + 1].strip()
|
|
69
|
-
|
|
70
|
-
if MODE_OVERRIDE and MODE_OVERRIDE not in ("startup", "standard", "enterprise"):
|
|
71
|
-
print(f"ERROR: --mode debe ser startup, standard o enterprise", file=sys.stderr)
|
|
72
|
-
sys.exit(1)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def _flag(name: str) -> Optional[str]:
|
|
76
|
-
"""Extrae el valor de --flag=value o --flag value de sys.argv."""
|
|
77
|
-
prefix = f"--{name}="
|
|
78
|
-
for i, arg in enumerate(sys.argv[1:], 1):
|
|
79
|
-
if arg.startswith(prefix):
|
|
80
|
-
return arg[len(prefix):]
|
|
81
|
-
if arg == f"--{name}" and i < len(sys.argv) - 1:
|
|
82
|
-
return sys.argv[i + 1]
|
|
83
|
-
return None
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
# Flags batch — si --name está presente, se activa el modo no-interactivo
|
|
87
|
-
BATCH_NAME = _flag("name")
|
|
88
|
-
BATCH_MODE = (
|
|
89
|
-
{
|
|
90
|
-
"startup": "startup", "standard": "standard", "enterprise": "enterprise",
|
|
91
|
-
"1": "startup", "2": "startup", "4": "standard", "9": "enterprise",
|
|
92
|
-
}.get(_flag("mode") or MODE_OVERRIDE or "", None)
|
|
93
|
-
or MODE_OVERRIDE
|
|
94
|
-
)
|
|
95
|
-
BATCH_SLUG = _flag("slug")
|
|
96
|
-
BATCH_DESC = _flag("desc") or _flag("description") or ""
|
|
97
|
-
BATCH_TYPE = _flag("type")
|
|
98
|
-
BATCH_BACKEND = _flag("backend")
|
|
99
|
-
BATCH_FRONTEND = _flag("frontend")
|
|
100
|
-
BATCH_DATABASE = _flag("database") or _flag("db")
|
|
101
|
-
BATCH_DEPLOY = _flag("deploy")
|
|
102
|
-
BATCH_TOOL = _flag("tool")
|
|
103
|
-
BATCH_COMPLIANCE = [c.strip() for c in (_flag("compliance") or "").split(",") if c.strip()]
|
|
104
|
-
BATCH_PAGE_BUILDER = _flag("page-builder")
|
|
105
|
-
BATCH_OUTPUT = _flag("output")
|
|
106
|
-
BATCH_MODE_ON = BATCH_NAME is not None # modo batch activo si --name está presente
|
|
107
|
-
|
|
108
|
-
IS_TTY = sys.stdin.isatty() and sys.stdout.isatty()
|
|
109
|
-
|
|
110
|
-
# ---------------------------------------------------------------------------
|
|
111
|
-
# ANSI helpers
|
|
112
|
-
# ---------------------------------------------------------------------------
|
|
113
|
-
|
|
114
|
-
USE_COLOR = IS_TTY
|
|
115
|
-
|
|
116
|
-
def _c(code: str, text: str) -> str:
|
|
117
|
-
return f"\033[{code}m{text}\033[0m" if USE_COLOR else text
|
|
118
|
-
|
|
119
|
-
def bold(t: str) -> str: return _c("1", t)
|
|
120
|
-
def cyan(t: str) -> str: return _c("36", t)
|
|
121
|
-
def green(t: str) -> str: return _c("32", t)
|
|
122
|
-
def yellow(t: str)-> str: return _c("33", t)
|
|
123
|
-
def dim(t: str) -> str: return _c("2", t)
|
|
124
|
-
|
|
125
|
-
HIDE_CURSOR = "\033[?25l" if USE_COLOR else ""
|
|
126
|
-
SHOW_CURSOR = "\033[?25h" if USE_COLOR else ""
|
|
127
|
-
|
|
128
|
-
def clr() -> None:
|
|
129
|
-
if IS_TTY:
|
|
130
|
-
os.system("clear")
|
|
131
|
-
|
|
132
|
-
def write(text: str) -> None:
|
|
133
|
-
sys.stdout.write(text)
|
|
134
|
-
sys.stdout.flush()
|
|
135
|
-
|
|
136
|
-
# ---------------------------------------------------------------------------
|
|
137
|
-
# Raw key reading
|
|
138
|
-
# ---------------------------------------------------------------------------
|
|
139
|
-
|
|
140
|
-
KEY_UP = "\x1b[A"
|
|
141
|
-
KEY_DOWN = "\x1b[B"
|
|
142
|
-
KEY_ENTER = "\r"
|
|
143
|
-
KEY_SPACE = " "
|
|
144
|
-
KEY_Q = "q"
|
|
145
|
-
KEY_CTRL_C = "\x03"
|
|
146
|
-
KEY_ESC = "\x1b"
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def getch() -> str:
|
|
150
|
-
if not IS_TTY:
|
|
151
|
-
return KEY_ENTER
|
|
152
|
-
fd = sys.stdin.fileno()
|
|
153
|
-
old = termios.tcgetattr(fd)
|
|
154
|
-
try:
|
|
155
|
-
tty.setraw(fd)
|
|
156
|
-
ch = sys.stdin.read(1)
|
|
157
|
-
if ch == "\x1b":
|
|
158
|
-
ch2 = sys.stdin.read(1)
|
|
159
|
-
ch3 = sys.stdin.read(1)
|
|
160
|
-
return f"\x1b{ch2}{ch3}"
|
|
161
|
-
return ch
|
|
162
|
-
finally:
|
|
163
|
-
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
|
164
|
-
|
|
165
|
-
# ---------------------------------------------------------------------------
|
|
166
|
-
# Menú de selección única con flechas
|
|
167
|
-
# ---------------------------------------------------------------------------
|
|
168
|
-
|
|
169
|
-
def pick(title: str, options: List[Tuple[str, str]], subtitle: str = "") -> Optional[str]:
|
|
170
|
-
"""
|
|
171
|
-
Muestra un menú de selección con ↑↓ Enter.
|
|
172
|
-
options: lista de (key, label). Retorna key seleccionado o None si ESC/q.
|
|
173
|
-
"""
|
|
174
|
-
if not options:
|
|
175
|
-
return None
|
|
176
|
-
cursor = 0
|
|
177
|
-
write(HIDE_CURSOR)
|
|
178
|
-
try:
|
|
179
|
-
while True:
|
|
180
|
-
clr()
|
|
181
|
-
_draw_section(title, subtitle)
|
|
182
|
-
for i, (key, label) in enumerate(options):
|
|
183
|
-
if i == cursor:
|
|
184
|
-
mark = cyan("▶")
|
|
185
|
-
line = f"\033[48;5;236m {label:<44}\033[0m" if USE_COLOR else f"[{label}]"
|
|
186
|
-
else:
|
|
187
|
-
mark = " "
|
|
188
|
-
line = f" {label}"
|
|
189
|
-
print(f" {mark}{line}")
|
|
190
|
-
print()
|
|
191
|
-
print(f" {dim('↑↓ navegar Enter seleccionar q salir')}")
|
|
192
|
-
|
|
193
|
-
ch = getch()
|
|
194
|
-
if ch in (KEY_CTRL_C, KEY_Q, KEY_ESC):
|
|
195
|
-
return None
|
|
196
|
-
if ch == KEY_UP:
|
|
197
|
-
cursor = (cursor - 1) % len(options)
|
|
198
|
-
elif ch == KEY_DOWN:
|
|
199
|
-
cursor = (cursor + 1) % len(options)
|
|
200
|
-
elif ch == KEY_ENTER:
|
|
201
|
-
return options[cursor][0]
|
|
202
|
-
finally:
|
|
203
|
-
write(SHOW_CURSOR)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
def pick_multi(title: str, options: List[Tuple[str, str]], subtitle: str = "") -> List[str]:
|
|
207
|
-
"""
|
|
208
|
-
Menú de selección múltiple con Space para marcar, Enter para confirmar.
|
|
209
|
-
Retorna lista de keys seleccionados.
|
|
210
|
-
"""
|
|
211
|
-
if not options:
|
|
212
|
-
return []
|
|
213
|
-
cursor = 0
|
|
214
|
-
selected: set[int] = set()
|
|
215
|
-
write(HIDE_CURSOR)
|
|
216
|
-
try:
|
|
217
|
-
while True:
|
|
218
|
-
clr()
|
|
219
|
-
_draw_section(title, subtitle)
|
|
220
|
-
for i, (key, label) in enumerate(options):
|
|
221
|
-
check = green("✓") if i in selected else dim("○")
|
|
222
|
-
arrow = cyan("▶") if i == cursor else " "
|
|
223
|
-
if i == cursor:
|
|
224
|
-
line = f"\033[48;5;236m {check} {label:<40}\033[0m" if USE_COLOR else f"[{check}] {label}"
|
|
225
|
-
else:
|
|
226
|
-
line = f" {check} {label}"
|
|
227
|
-
print(f" {arrow}{line}")
|
|
228
|
-
print()
|
|
229
|
-
count = len(selected)
|
|
230
|
-
print(f" {dim('Space marcar/desmarcar Enter confirmar')} {cyan(str(count))} seleccionado{'s' if count != 1 else ''}")
|
|
231
|
-
|
|
232
|
-
ch = getch()
|
|
233
|
-
if ch in (KEY_CTRL_C, KEY_ESC):
|
|
234
|
-
return []
|
|
235
|
-
if ch == KEY_UP:
|
|
236
|
-
cursor = (cursor - 1) % len(options)
|
|
237
|
-
elif ch == KEY_DOWN:
|
|
238
|
-
cursor = (cursor + 1) % len(options)
|
|
239
|
-
elif ch == KEY_SPACE:
|
|
240
|
-
if cursor in selected:
|
|
241
|
-
selected.discard(cursor)
|
|
242
|
-
else:
|
|
243
|
-
selected.add(cursor)
|
|
244
|
-
elif ch == KEY_ENTER:
|
|
245
|
-
return [options[i][0] for i in sorted(selected)]
|
|
246
|
-
finally:
|
|
247
|
-
write(SHOW_CURSOR)
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
def ask_text(prompt: str, default: str = "") -> str:
|
|
251
|
-
"""Input de texto libre — solo para nombre y descripción."""
|
|
252
|
-
write(SHOW_CURSOR)
|
|
253
|
-
hint = f" {dim(f'[{default}]')}" if default else ""
|
|
254
|
-
try:
|
|
255
|
-
val = input(f" {cyan('?')} {prompt}{hint}: ").strip()
|
|
256
|
-
except (KeyboardInterrupt, EOFError):
|
|
257
|
-
print("\n\nInterrumpido.")
|
|
258
|
-
sys.exit(0)
|
|
259
|
-
finally:
|
|
260
|
-
write(HIDE_CURSOR)
|
|
261
|
-
return val if val else default
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
def _draw_section(title: str, subtitle: str = "") -> None:
|
|
265
|
-
print()
|
|
266
|
-
print(f" {bold(title)}")
|
|
267
|
-
if subtitle:
|
|
268
|
-
print(f" {dim(subtitle)}")
|
|
269
|
-
print()
|
|
270
|
-
|
|
271
|
-
# ---------------------------------------------------------------------------
|
|
272
|
-
# Catálogo de opciones
|
|
273
|
-
# ---------------------------------------------------------------------------
|
|
274
|
-
|
|
275
|
-
PROJECT_TYPES = [
|
|
276
|
-
("static", "Sitio estático / SSG Astro, Hugo, 11ty"),
|
|
277
|
-
("webapp", "Web app dinámica Next.js, Nuxt, SvelteKit, Remix"),
|
|
278
|
-
("api", "API / Backend FastAPI, Express, NestJS, Rails, Hono"),
|
|
279
|
-
("fullstack", "Full-stack Frontend + Backend en el mismo repo"),
|
|
280
|
-
("wordpress", "WordPress CMS · Divi · Elementor · FSE · plugins"),
|
|
281
|
-
("mobile", "App móvil React Native / Expo"),
|
|
282
|
-
("saas", "SaaS Full-stack con auth, billing y multi-tenant"),
|
|
283
|
-
("crawler", "Crawler / Scraper Playwright, Puppeteer"),
|
|
284
|
-
("cli", "Herramienta CLI Python, Go, Node"),
|
|
285
|
-
]
|
|
286
|
-
|
|
287
|
-
FRONTEND_FRAMEWORKS = [
|
|
288
|
-
("nextjs", "Next.js React + SSR/SSG · TypeScript"),
|
|
289
|
-
("astro", "Astro SSG/SSR · islands · TypeScript"),
|
|
290
|
-
("nuxt", "Nuxt Vue 3 + SSR/SSG · TypeScript"),
|
|
291
|
-
("sveltekit", "SvelteKit Svelte + SSR/SSG · TypeScript"),
|
|
292
|
-
("remix", "Remix React + SSR · TypeScript"),
|
|
293
|
-
("react-vite", "React + Vite SPA · TypeScript"),
|
|
294
|
-
("vue-vite", "Vue + Vite SPA · TypeScript"),
|
|
295
|
-
("angular", "Angular SPA · TypeScript"),
|
|
296
|
-
("none", "Sin framework frontend"),
|
|
297
|
-
]
|
|
298
|
-
|
|
299
|
-
BACKEND_FRAMEWORKS = [
|
|
300
|
-
("hono", "Hono TypeScript · edge-ready · Drizzle ORM"),
|
|
301
|
-
("express", "Express Node.js · TypeScript"),
|
|
302
|
-
("nestjs", "NestJS Node.js · TypeScript · arquitectura modular"),
|
|
303
|
-
("fastapi", "FastAPI Python · async · Pydantic"),
|
|
304
|
-
("django", "Django Python · batteries-included · DRF"),
|
|
305
|
-
("rails", "Ruby on Rails Ruby · convención sobre config"),
|
|
306
|
-
("laravel", "Laravel PHP · Eloquent ORM · Sanctum"),
|
|
307
|
-
("gin", "Gin Go · alta performance"),
|
|
308
|
-
("none", "Sin framework backend"),
|
|
309
|
-
]
|
|
310
|
-
|
|
311
|
-
WORDPRESS_PAGE_BUILDERS = [
|
|
312
|
-
("divi", "Divi Elegant Themes · Theme Builder · módulos custom"),
|
|
313
|
-
("elementor", "Elementor Pro Page Builder · Dynamic Tags · Loop Grid"),
|
|
314
|
-
("gutenberg", "Gutenberg / FSE Block Editor nativo · Full Site Editing"),
|
|
315
|
-
("none", "Sin page builder desarrollo de plugins o themes puros"),
|
|
316
|
-
]
|
|
317
|
-
|
|
318
|
-
DATABASES = [
|
|
319
|
-
("postgresql", "PostgreSQL Relacional · producción"),
|
|
320
|
-
("mysql", "MySQL / MariaDB Relacional · hosting compartido"),
|
|
321
|
-
("sqlite", "SQLite Relacional · local / edge"),
|
|
322
|
-
("mongodb", "MongoDB Documento · NoSQL"),
|
|
323
|
-
("supabase", "Supabase PostgreSQL as a Service + Auth"),
|
|
324
|
-
("planetscale", "PlanetScale MySQL serverless · branching"),
|
|
325
|
-
("turso", "Turso SQLite edge · libSQL"),
|
|
326
|
-
("redis", "Redis Cache / pub-sub"),
|
|
327
|
-
("none", "Sin base de datos"),
|
|
328
|
-
]
|
|
329
|
-
|
|
330
|
-
DEPLOY_TARGETS = [
|
|
331
|
-
("vercel", "Vercel Serverless · Edge · integración Git"),
|
|
332
|
-
("netlify", "Netlify Serverless · Edge · forms"),
|
|
333
|
-
("cloudflare", "Cloudflare Workers · Pages · R2 · D1"),
|
|
334
|
-
("railway", "Railway PaaS · containers · DB managed"),
|
|
335
|
-
("fly", "Fly.io VMs · Docker · multi-región"),
|
|
336
|
-
("aws", "AWS EC2 / Lambda / ECS / S3"),
|
|
337
|
-
("gcp", "GCP Cloud Run / GKE / Firebase"),
|
|
338
|
-
("digitalocean","DigitalOcean Droplets · App Platform"),
|
|
339
|
-
("shared", "Hosting compartido cPanel / FTP / PHP"),
|
|
340
|
-
("selfhosted", "Self-hosted / VPS Docker Compose · Nginx"),
|
|
341
|
-
("none", "Sin deploy configurado aún"),
|
|
342
|
-
]
|
|
343
|
-
|
|
344
|
-
RUNTIMES = [
|
|
345
|
-
("claude-code", "Claude Code — CLI de Anthropic · .claude/agents/ (recomendado)"),
|
|
346
|
-
("opencode", "OpenCode — editor alternativo open source · AGENTS.md"),
|
|
347
|
-
("kiro", "Kiro — IDE de AWS con IA integrada · .kiro/steering/"),
|
|
348
|
-
("codex", "Codex CLI — CLI de OpenAI · AGENTS.md"),
|
|
349
|
-
("all", "Todos — genera los cuatro formatos (para migrar después)"),
|
|
350
|
-
]
|
|
351
|
-
|
|
352
|
-
COMPLIANCE_OPTIONS = [
|
|
353
|
-
("gdpr", "GDPR Unión Europea"),
|
|
354
|
-
("lgpd", "LGPD Brasil"),
|
|
355
|
-
("ley-21719", "Ley 21.719 Chile"),
|
|
356
|
-
("ccpa", "CCPA California, EE.UU."),
|
|
357
|
-
("hipaa", "HIPAA Salud · EE.UU."),
|
|
358
|
-
("pci-dss", "PCI-DSS Pagos con tarjeta"),
|
|
359
|
-
]
|
|
360
|
-
|
|
361
|
-
LANGUAGES = [
|
|
362
|
-
("typescript", "TypeScript"),
|
|
363
|
-
("python", "Python"),
|
|
364
|
-
("ruby", "Ruby"),
|
|
365
|
-
("php", "PHP"),
|
|
366
|
-
("go", "Go"),
|
|
367
|
-
("mixed", "Mixto (varios lenguajes)"),
|
|
368
|
-
]
|
|
369
|
-
|
|
370
|
-
# ---------------------------------------------------------------------------
|
|
371
|
-
# Lógica de perfil sugerido
|
|
372
|
-
# ---------------------------------------------------------------------------
|
|
373
|
-
|
|
374
|
-
def suggest_profiles(ptype: str, frontend: str, backend: str,
|
|
375
|
-
page_builder: str = "none") -> List[str]:
|
|
376
|
-
"""Retorna los profiles de forge más adecuados según el tipo y stack."""
|
|
377
|
-
profiles: List[str] = []
|
|
378
|
-
|
|
379
|
-
frontend_map = {
|
|
380
|
-
"nextjs": "nextjs-admin",
|
|
381
|
-
"astro": "astro",
|
|
382
|
-
"nuxt": "vuenuxt",
|
|
383
|
-
"sveltekit": "sveltekit",
|
|
384
|
-
}
|
|
385
|
-
backend_map = {
|
|
386
|
-
"hono": "hono-drizzle",
|
|
387
|
-
"express": "express",
|
|
388
|
-
"nestjs": "nestjs",
|
|
389
|
-
"fastapi": "fastapi",
|
|
390
|
-
"rails": "rails",
|
|
391
|
-
"django": "django",
|
|
392
|
-
"laravel": "laravel",
|
|
393
|
-
"gin": "go-gin",
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
if ptype == "mobile":
|
|
397
|
-
return ["expo"]
|
|
398
|
-
if ptype == "crawler":
|
|
399
|
-
return ["playwright-crawler"]
|
|
400
|
-
if ptype == "wordpress":
|
|
401
|
-
return ["wordpress"]
|
|
402
|
-
|
|
403
|
-
if frontend in frontend_map:
|
|
404
|
-
profiles.append(frontend_map[frontend])
|
|
405
|
-
if backend in backend_map:
|
|
406
|
-
p = backend_map[backend]
|
|
407
|
-
if p not in profiles:
|
|
408
|
-
profiles.append(p)
|
|
409
|
-
|
|
410
|
-
return profiles
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
def detect_language(frontend: str, backend: str, ptype: str = "") -> str:
|
|
414
|
-
ts = {"nextjs", "astro", "sveltekit", "remix", "react-vite", "vue-vite",
|
|
415
|
-
"angular", "nuxt", "hono", "express", "nestjs"}
|
|
416
|
-
py = {"fastapi", "django"}
|
|
417
|
-
rb = {"rails"}
|
|
418
|
-
php = {"laravel", "wordpress"}
|
|
419
|
-
go = {"gin"}
|
|
420
|
-
|
|
421
|
-
if ptype == "wordpress":
|
|
422
|
-
return "php"
|
|
423
|
-
|
|
424
|
-
langs: set[str] = set()
|
|
425
|
-
if frontend in ts or backend in ts:
|
|
426
|
-
langs.add("typescript")
|
|
427
|
-
if backend in py:
|
|
428
|
-
langs.add("python")
|
|
429
|
-
if backend in rb:
|
|
430
|
-
langs.add("ruby")
|
|
431
|
-
if backend in php:
|
|
432
|
-
langs.add("php")
|
|
433
|
-
if backend in go:
|
|
434
|
-
langs.add("go")
|
|
435
|
-
|
|
436
|
-
if not langs:
|
|
437
|
-
return "typescript"
|
|
438
|
-
if len(langs) == 1:
|
|
439
|
-
return langs.pop()
|
|
440
|
-
return "mixed"
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
def primary_engineer(frontend: str, backend: str) -> str:
|
|
444
|
-
if backend not in ("none", ""):
|
|
445
|
-
return "backend-engineer"
|
|
446
|
-
if frontend not in ("none", ""):
|
|
447
|
-
return "frontend-engineer"
|
|
448
|
-
return "backend-engineer"
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
def team_size_to_mode(size: int) -> str:
|
|
452
|
-
if size <= 2:
|
|
453
|
-
return "startup"
|
|
454
|
-
if size <= 8:
|
|
455
|
-
return "standard"
|
|
456
|
-
return "enterprise"
|
|
457
|
-
|
|
458
|
-
# ---------------------------------------------------------------------------
|
|
459
|
-
# YAML builders
|
|
460
|
-
# ---------------------------------------------------------------------------
|
|
461
|
-
|
|
462
|
-
def _yaml_str(value: str) -> str:
|
|
463
|
-
"""Serializa un string de input del usuario como valor YAML seguro entre comillas dobles."""
|
|
464
|
-
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
|
|
465
|
-
return f'"{escaped}"'
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
def _null(v: str) -> str:
|
|
469
|
-
return "null" if not v or v in ("none", "null") else f'"{v}"'
|
|
470
|
-
|
|
471
|
-
def _profiles_yaml(profiles: List[str]) -> str:
|
|
472
|
-
if not profiles:
|
|
473
|
-
return "[] # sin profile Tier 2 — usa forge-scaffold-profile.py para crear uno"
|
|
474
|
-
return "[" + ", ".join(f'"{p}"' for p in profiles) + "]"
|
|
475
|
-
|
|
476
|
-
def _compliance_yaml(frameworks: List[str]) -> str:
|
|
477
|
-
if not frameworks:
|
|
478
|
-
return "[]"
|
|
479
|
-
return "[" + ", ".join(f'"{f}"' for f in frameworks) + "]"
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
def build_yaml(data: dict) -> str:
|
|
483
|
-
mode = data["mode"]
|
|
484
|
-
name = data["name"]
|
|
485
|
-
slug = data["slug"]
|
|
486
|
-
desc = data["description"]
|
|
487
|
-
lang = data["language"]
|
|
488
|
-
ptype = data["project_type"]
|
|
489
|
-
frontend = data["frontend"]
|
|
490
|
-
backend = data["backend"]
|
|
491
|
-
database = data["database"]
|
|
492
|
-
deploy = data["deploy"]
|
|
493
|
-
profiles = data["profiles"]
|
|
494
|
-
runtime = data["runtime"]
|
|
495
|
-
compliance = data["compliance"]
|
|
496
|
-
|
|
497
|
-
pii = "true" if compliance else "false"
|
|
498
|
-
audit = "true" if any(f in compliance for f in ("gdpr", "lgpd", "ley-21719", "ccpa", "hipaa")) else "false"
|
|
499
|
-
|
|
500
|
-
if mode == "startup":
|
|
501
|
-
eng = primary_engineer(frontend, backend)
|
|
502
|
-
return textwrap.dedent(f"""\
|
|
503
|
-
# forge — project.yaml (modo startup)
|
|
504
|
-
project:
|
|
505
|
-
name: {_yaml_str(name)}
|
|
506
|
-
slug: {_yaml_str(slug)}
|
|
507
|
-
description: {_yaml_str(desc)}
|
|
508
|
-
language: "{lang}"
|
|
509
|
-
type: "{ptype}"
|
|
510
|
-
mode: "startup"
|
|
511
|
-
status: "active"
|
|
512
|
-
|
|
513
|
-
team:
|
|
514
|
-
name: "Equipo"
|
|
515
|
-
members: []
|
|
516
|
-
|
|
517
|
-
stack:
|
|
518
|
-
frontend: {_null(frontend)}
|
|
519
|
-
backend: {_null(backend)}
|
|
520
|
-
database: {_null(database)}
|
|
521
|
-
deploy: {_null(deploy)}
|
|
522
|
-
|
|
523
|
-
agents:
|
|
524
|
-
active:
|
|
525
|
-
- orchestrator
|
|
526
|
-
- {eng}
|
|
527
|
-
compliance: []
|
|
528
|
-
profiles: {_profiles_yaml(profiles)}
|
|
529
|
-
|
|
530
|
-
sprint:
|
|
531
|
-
current: 1
|
|
532
|
-
|
|
533
|
-
compliance:
|
|
534
|
-
frameworks: {_compliance_yaml(compliance)}
|
|
535
|
-
pii_handling: false
|
|
536
|
-
audit_logs: false
|
|
537
|
-
|
|
538
|
-
paths:
|
|
539
|
-
specs: "docs/specs"
|
|
540
|
-
progress: "docs/progress.html"
|
|
541
|
-
""")
|
|
542
|
-
|
|
543
|
-
compliance_agents = " - compliance-reviewer\n - security-auditor" if compliance else " []"
|
|
544
|
-
phases_block = _phases_for_mode(mode)
|
|
545
|
-
|
|
546
|
-
return textwrap.dedent(f"""\
|
|
547
|
-
# forge — project.yaml (modo {mode})
|
|
548
|
-
project:
|
|
549
|
-
name: {_yaml_str(name)}
|
|
550
|
-
slug: {_yaml_str(slug)}
|
|
551
|
-
description: {_yaml_str(desc)}
|
|
552
|
-
language: "{lang}"
|
|
553
|
-
type: "{ptype}"
|
|
554
|
-
mode: "{mode}"
|
|
555
|
-
status: "active"
|
|
556
|
-
|
|
557
|
-
team:
|
|
558
|
-
name: "Equipo"
|
|
559
|
-
members: []
|
|
560
|
-
|
|
561
|
-
stack:
|
|
562
|
-
frontend: {_null(frontend)}
|
|
563
|
-
backend: {_null(backend)}
|
|
564
|
-
database: {_null(database)}
|
|
565
|
-
deploy: {_null(deploy)}
|
|
566
|
-
|
|
567
|
-
agents:
|
|
568
|
-
active:
|
|
569
|
-
- orchestrator
|
|
570
|
-
- backend-engineer
|
|
571
|
-
- frontend-engineer
|
|
572
|
-
- test-engineer
|
|
573
|
-
- docs-writer
|
|
574
|
-
compliance:
|
|
575
|
-
{compliance_agents}
|
|
576
|
-
profiles: {_profiles_yaml(profiles)}
|
|
577
|
-
|
|
578
|
-
sprint:
|
|
579
|
-
current: 1
|
|
580
|
-
length_days: 14
|
|
581
|
-
phases:
|
|
582
|
-
{phases_block}
|
|
583
|
-
|
|
584
|
-
skills:
|
|
585
|
-
active:
|
|
586
|
-
- security-audit
|
|
587
|
-
- new-feature
|
|
588
|
-
{('- db-migrate' if database not in ('none', '') else '# - db-migrate')}
|
|
589
|
-
integrations: []
|
|
590
|
-
|
|
591
|
-
deploy:
|
|
592
|
-
provider: {_null(deploy)}
|
|
593
|
-
branch: "main"
|
|
594
|
-
|
|
595
|
-
compliance:
|
|
596
|
-
frameworks: {_compliance_yaml(compliance)}
|
|
597
|
-
pii_handling: {pii}
|
|
598
|
-
audit_logs: {audit}
|
|
599
|
-
|
|
600
|
-
paths:
|
|
601
|
-
specs: "docs/specs"
|
|
602
|
-
progress: "docs/progress.html"
|
|
603
|
-
migrations: null
|
|
604
|
-
tests: null
|
|
605
|
-
""")
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
def _phases_for_mode(mode: str) -> str:
|
|
609
|
-
if mode == "enterprise":
|
|
610
|
-
return textwrap.dedent("""\
|
|
611
|
-
- id: "A"
|
|
612
|
-
name: "Arquitectura y SDD"
|
|
613
|
-
specs: []
|
|
614
|
-
- id: "B"
|
|
615
|
-
name: "Core"
|
|
616
|
-
specs: []
|
|
617
|
-
- id: "C"
|
|
618
|
-
name: "Features"
|
|
619
|
-
specs: []
|
|
620
|
-
- id: "D"
|
|
621
|
-
name: "Compliance y Auditoría"
|
|
622
|
-
specs: []""")
|
|
623
|
-
return textwrap.dedent("""\
|
|
624
|
-
- id: "A"
|
|
625
|
-
name: "Core"
|
|
626
|
-
specs: []
|
|
627
|
-
- id: "B"
|
|
628
|
-
name: "Features"
|
|
629
|
-
specs: []""")
|
|
630
|
-
|
|
631
|
-
# ---------------------------------------------------------------------------
|
|
632
|
-
# Wizard principal
|
|
633
|
-
# ---------------------------------------------------------------------------
|
|
634
|
-
|
|
635
|
-
def _run_batch() -> None:
|
|
636
|
-
"""Modo no-interactivo: construye project.yaml desde flags CLI sin prompts."""
|
|
637
|
-
name = BATCH_NAME or "Mi Proyecto"
|
|
638
|
-
slug = BATCH_SLUG or name.lower().replace(" ", "-").replace("_", "-")
|
|
639
|
-
desc = BATCH_DESC
|
|
640
|
-
ptype = BATCH_TYPE or "webapp"
|
|
641
|
-
backend = BATCH_BACKEND or "none"
|
|
642
|
-
frontend= BATCH_FRONTEND or "none"
|
|
643
|
-
database= BATCH_DATABASE or "none"
|
|
644
|
-
deploy = BATCH_DEPLOY or "none"
|
|
645
|
-
runtime = BATCH_TOOL or "claude-code"
|
|
646
|
-
compliance = BATCH_COMPLIANCE
|
|
647
|
-
page_builder = BATCH_PAGE_BUILDER or "none"
|
|
648
|
-
mode = BATCH_MODE or MODE_OVERRIDE or "standard"
|
|
649
|
-
if mode not in ("startup", "standard", "enterprise"):
|
|
650
|
-
mode = "standard"
|
|
651
|
-
out_path = Path(BATCH_OUTPUT or "project.yaml").expanduser().resolve()
|
|
652
|
-
|
|
653
|
-
lang = detect_language(frontend, backend, ptype)
|
|
654
|
-
profiles = suggest_profiles(ptype, frontend, backend, page_builder)
|
|
655
|
-
|
|
656
|
-
data = {
|
|
657
|
-
"mode": mode, "name": name, "slug": slug, "description": desc,
|
|
658
|
-
"language": lang, "project_type": ptype,
|
|
659
|
-
"frontend": frontend, "backend": backend, "database": database,
|
|
660
|
-
"deploy": deploy, "profiles": profiles, "runtime": runtime,
|
|
661
|
-
"compliance": compliance,
|
|
662
|
-
}
|
|
663
|
-
yaml_content = build_yaml(data)
|
|
664
|
-
|
|
665
|
-
if DRY_RUN:
|
|
666
|
-
print(yaml_content)
|
|
667
|
-
return
|
|
668
|
-
|
|
669
|
-
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
670
|
-
out_path.write_text(yaml_content, encoding="utf-8")
|
|
671
|
-
try:
|
|
672
|
-
yaml.safe_load(out_path.read_text())
|
|
673
|
-
except yaml.YAMLError as e:
|
|
674
|
-
print(f"ERROR: el YAML generado no es válido: {e}", file=sys.stderr)
|
|
675
|
-
out_path.unlink(missing_ok=True)
|
|
676
|
-
sys.exit(1)
|
|
677
|
-
|
|
678
|
-
print(f"forge: project.yaml escrito en {out_path}")
|
|
679
|
-
if profiles:
|
|
680
|
-
print(f" Profiles detectados: {', '.join(profiles)}")
|
|
681
|
-
|
|
682
|
-
if not NO_INIT:
|
|
683
|
-
forge_init = _find_forge_init()
|
|
684
|
-
if forge_init:
|
|
685
|
-
import subprocess
|
|
686
|
-
tool_arg = runtime if runtime != "todos" else "all"
|
|
687
|
-
cmd = [sys.executable, str(forge_init), f"--tool={tool_arg}"]
|
|
688
|
-
print(f" $ {' '.join(cmd)}")
|
|
689
|
-
subprocess.run(cmd, check=False)
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
def main() -> None:
|
|
693
|
-
if BATCH_MODE_ON:
|
|
694
|
-
_run_batch()
|
|
695
|
-
return
|
|
696
|
-
|
|
697
|
-
clr()
|
|
698
|
-
print()
|
|
699
|
-
print(f" {bold('forge — Wizard de proyecto nuevo')}")
|
|
700
|
-
print()
|
|
701
|
-
print(f" forge organiza tu proyecto con un equipo de agentes IA.")
|
|
702
|
-
print(f" {dim('Cada agente tiene un rol fijo: backend, frontend, tests, documentación.')}")
|
|
703
|
-
print()
|
|
704
|
-
print(f" Al terminar tendrás un {bold('project.yaml')} que:")
|
|
705
|
-
print(f" {dim(' · instala los agentes correctos para tu stack tecnológico')}")
|
|
706
|
-
print(f" {dim(' · configura Claude Code (u otro runtime) automáticamente')}")
|
|
707
|
-
print(f" {dim(' · adapta el nivel de proceso al tamaño de tu equipo')}")
|
|
708
|
-
print()
|
|
709
|
-
if DRY_RUN:
|
|
710
|
-
print(f" {yellow('--dry-run activo: no se escribirá ningún archivo')}")
|
|
711
|
-
print()
|
|
712
|
-
print(f" {dim('↑↓ navegar Enter seleccionar q salir')}")
|
|
713
|
-
print()
|
|
714
|
-
input(f" Presiona Enter para comenzar... ")
|
|
715
|
-
|
|
716
|
-
# 1 — Modo / tamaño de equipo
|
|
717
|
-
if MODE_OVERRIDE:
|
|
718
|
-
mode = MODE_OVERRIDE
|
|
719
|
-
else:
|
|
720
|
-
size_key = pick(
|
|
721
|
-
"¿Cuántas personas hay en el equipo de desarrollo?",
|
|
722
|
-
[
|
|
723
|
-
("1", "Solo yo / 1 persona"),
|
|
724
|
-
("2", "2 personas"),
|
|
725
|
-
("4", "3-8 personas"),
|
|
726
|
-
("9", "9 o más personas"),
|
|
727
|
-
],
|
|
728
|
-
)
|
|
729
|
-
if size_key is None:
|
|
730
|
-
sys.exit(0)
|
|
731
|
-
mode = team_size_to_mode(int(size_key))
|
|
732
|
-
|
|
733
|
-
mode_label = {"startup": "Startup (1-2)", "standard": "Standard (3-8)", "enterprise": "Enterprise (9+)"}
|
|
734
|
-
print(f" Modo: {bold(mode_label[mode])}\n")
|
|
735
|
-
|
|
736
|
-
# 2 — Nombre del proyecto (único campo de texto libre)
|
|
737
|
-
clr()
|
|
738
|
-
_draw_section("Datos del proyecto")
|
|
739
|
-
name = ask_text("Nombre del proyecto", "Mi Proyecto")
|
|
740
|
-
slug = name.lower().replace(" ", "-").replace("_", "-")
|
|
741
|
-
slug = ask_text("Slug (lowercase, sin espacios)", slug)
|
|
742
|
-
desc = ask_text("Descripción breve", "")
|
|
743
|
-
|
|
744
|
-
# 3 — Tipo de proyecto
|
|
745
|
-
ptype_key = pick(
|
|
746
|
-
"¿Qué tipo de proyecto es?",
|
|
747
|
-
PROJECT_TYPES,
|
|
748
|
-
)
|
|
749
|
-
if ptype_key is None:
|
|
750
|
-
sys.exit(0)
|
|
751
|
-
|
|
752
|
-
# 4 — Frontend (si aplica)
|
|
753
|
-
frontend = "none"
|
|
754
|
-
page_builder = "none"
|
|
755
|
-
|
|
756
|
-
if ptype_key == "wordpress":
|
|
757
|
-
# WordPress: preguntar page builder en vez de frontend/backend
|
|
758
|
-
pb = pick(
|
|
759
|
-
"¿Qué page builder / entorno usa el proyecto?",
|
|
760
|
-
WORDPRESS_PAGE_BUILDERS,
|
|
761
|
-
subtitle="Determina qué agente especializado se instalará.",
|
|
762
|
-
)
|
|
763
|
-
page_builder = pb if pb else "none"
|
|
764
|
-
frontend = "none"
|
|
765
|
-
elif ptype_key not in ("api", "cli"):
|
|
766
|
-
if ptype_key == "mobile":
|
|
767
|
-
frontend = "expo"
|
|
768
|
-
elif ptype_key == "crawler":
|
|
769
|
-
frontend = "none"
|
|
770
|
-
else:
|
|
771
|
-
opts = FRONTEND_FRAMEWORKS
|
|
772
|
-
if ptype_key == "static":
|
|
773
|
-
opts = [o for o in opts if o[0] in ("astro", "nextjs", "nuxt", "sveltekit", "none")]
|
|
774
|
-
fk = pick("Framework frontend", opts)
|
|
775
|
-
frontend = fk if fk else "none"
|
|
776
|
-
|
|
777
|
-
# 5 — Backend (si aplica)
|
|
778
|
-
backend = "none"
|
|
779
|
-
if ptype_key == "wordpress":
|
|
780
|
-
backend = "wordpress"
|
|
781
|
-
elif ptype_key not in ("static", "mobile", "crawler"):
|
|
782
|
-
if ptype_key == "api":
|
|
783
|
-
bk = pick("Framework backend", [o for o in BACKEND_FRAMEWORKS if o[0] != "none"])
|
|
784
|
-
backend = bk if bk else "none"
|
|
785
|
-
else:
|
|
786
|
-
bk = pick("Framework backend", BACKEND_FRAMEWORKS)
|
|
787
|
-
backend = bk if bk else "none"
|
|
788
|
-
if ptype_key == "crawler":
|
|
789
|
-
backend = "none"
|
|
790
|
-
|
|
791
|
-
if ptype_key in ("mobile", "expo"):
|
|
792
|
-
frontend = "expo"
|
|
793
|
-
backend = "none"
|
|
794
|
-
if ptype_key == "crawler":
|
|
795
|
-
frontend = "none"
|
|
796
|
-
backend = "none"
|
|
797
|
-
|
|
798
|
-
# Aviso temprano: stack seleccionado sin profile Tier 2
|
|
799
|
-
_early_profiles = suggest_profiles(ptype_key, frontend, backend, page_builder)
|
|
800
|
-
_stack_specified = backend not in ("none", "") or frontend not in ("none", "")
|
|
801
|
-
if not _early_profiles and _stack_specified and ptype_key not in ("cli",):
|
|
802
|
-
clr()
|
|
803
|
-
_draw_section("Nota sobre tu stack")
|
|
804
|
-
_stack_label = " + ".join(x for x in (frontend, backend) if x and x != "none")
|
|
805
|
-
print(f" {yellow('Sin agente especializado para:')} {bold(_stack_label)}")
|
|
806
|
-
print()
|
|
807
|
-
print(f" forge instalará agentes genéricos que funcionan con cualquier stack.")
|
|
808
|
-
print(f" Los stacks con agente especializado (profile) son:")
|
|
809
|
-
print(f" {dim(' API/Backend: hono · express · nestjs · fastapi · django · rails · laravel · gin')}")
|
|
810
|
-
print(f" {dim(' Frontend: nextjs · astro · nuxt · sveltekit')}")
|
|
811
|
-
print(f" {dim(' CMS: wordpress (Divi · Elementor · FSE)')}")
|
|
812
|
-
print(f" {dim(' Otros: expo (mobile) · playwright (crawler)')}")
|
|
813
|
-
print()
|
|
814
|
-
print(f" {dim('Puedes crear un agente propio después con forge-scaffold-profile.py')}")
|
|
815
|
-
print()
|
|
816
|
-
input(f" Presiona Enter para continuar... ")
|
|
817
|
-
|
|
818
|
-
# 6 — Base de datos
|
|
819
|
-
database = "none"
|
|
820
|
-
if ptype_key not in ("static", "mobile", "cli", "crawler"):
|
|
821
|
-
db_opts = DATABASES
|
|
822
|
-
if ptype_key == "api" and backend in ("fastapi", "django", "rails"):
|
|
823
|
-
db_opts = [o for o in DATABASES if o[0] not in ("none",)]
|
|
824
|
-
dk = pick("Base de datos", db_opts)
|
|
825
|
-
database = dk if dk else "none"
|
|
826
|
-
|
|
827
|
-
# 7 — Deploy
|
|
828
|
-
deploy_key = pick(
|
|
829
|
-
"¿Dónde se va a desplegar?",
|
|
830
|
-
DEPLOY_TARGETS,
|
|
831
|
-
subtitle="Elige la infraestructura principal",
|
|
832
|
-
)
|
|
833
|
-
deploy = deploy_key if deploy_key else "none"
|
|
834
|
-
|
|
835
|
-
# 8 — Runtime
|
|
836
|
-
runtime_key = pick(
|
|
837
|
-
"¿Qué herramienta de IA usas?",
|
|
838
|
-
RUNTIMES,
|
|
839
|
-
subtitle="Elige la que ya tienes instalada. Si empiezas ahora, Claude Code es la más completa.",
|
|
840
|
-
)
|
|
841
|
-
runtime = runtime_key if runtime_key else "claude-code"
|
|
842
|
-
|
|
843
|
-
# 9 — Compliance (multi-select)
|
|
844
|
-
compliance = pick_multi(
|
|
845
|
-
"¿Tu proyecto debe cumplir alguna regulación de privacidad?",
|
|
846
|
-
COMPLIANCE_OPTIONS,
|
|
847
|
-
subtitle="Activa los que aplican a tu mercado. Si no sabes, deja todos sin marcar.",
|
|
848
|
-
)
|
|
849
|
-
|
|
850
|
-
# 10 — Lenguaje y profiles detectados
|
|
851
|
-
lang = detect_language(frontend, backend, ptype_key)
|
|
852
|
-
profiles = suggest_profiles(ptype_key, frontend, backend, page_builder)
|
|
853
|
-
|
|
854
|
-
# Mostrar resumen antes de escribir
|
|
855
|
-
clr()
|
|
856
|
-
_draw_section("Resumen de configuración")
|
|
857
|
-
rows = [
|
|
858
|
-
("Modo", mode_label[mode]),
|
|
859
|
-
("Proyecto", f"{name} ({slug})"),
|
|
860
|
-
("Tipo", ptype_key),
|
|
861
|
-
("Frontend", frontend),
|
|
862
|
-
("Backend", backend),
|
|
863
|
-
]
|
|
864
|
-
if ptype_key == "wordpress":
|
|
865
|
-
rows.append(("Page Builder", page_builder))
|
|
866
|
-
rows += [
|
|
867
|
-
("Base datos", database),
|
|
868
|
-
("Deploy", deploy),
|
|
869
|
-
("Runtime", runtime),
|
|
870
|
-
("Compliance", ", ".join(compliance) if compliance else "ninguno"),
|
|
871
|
-
("Profiles", ", ".join(profiles) if profiles else "ninguno (ver nota)"),
|
|
872
|
-
("Lenguaje", lang),
|
|
873
|
-
]
|
|
874
|
-
for label, value in rows:
|
|
875
|
-
print(f" {dim(f'{label:<12}')} {bold(value)}")
|
|
876
|
-
|
|
877
|
-
if not profiles:
|
|
878
|
-
print()
|
|
879
|
-
print(f" {yellow('Nota:')} No hay profile Tier 2 para esta combinación de stack.")
|
|
880
|
-
print(f" Los profiles disponibles son:")
|
|
881
|
-
print(f" {dim(' API/Backend:')}")
|
|
882
|
-
print(f" {dim(' hono-drizzle · express · nestjs · fastapi · django · rails · go-gin · laravel')}")
|
|
883
|
-
print(f" {dim(' Frontend:')}")
|
|
884
|
-
print(f" {dim(' nextjs-admin · astro · vuenuxt · sveltekit')}")
|
|
885
|
-
print(f" {dim(' CMS:')}")
|
|
886
|
-
print(f" {dim(' wordpress (Divi · Elementor · FSE)')}")
|
|
887
|
-
print(f" {dim(' Otros:')}")
|
|
888
|
-
print(f" {dim(' expo (mobile) · playwright-crawler (scraping)')}")
|
|
889
|
-
print()
|
|
890
|
-
print(f" Para crear un profile propio para tu stack:")
|
|
891
|
-
print(f" {dim('python3 .agentic/scripts/forge-scaffold-profile.py --name <stack> --engineer <agente>')}")
|
|
892
|
-
|
|
893
|
-
# 11 — Destino del project.yaml
|
|
894
|
-
print()
|
|
895
|
-
write(SHOW_CURSOR)
|
|
896
|
-
out_default = str(Path.cwd() / "project.yaml")
|
|
897
|
-
out_str = ask_text("Ruta para guardar project.yaml", out_default)
|
|
898
|
-
out_path = Path(out_str).expanduser().resolve()
|
|
899
|
-
write(HIDE_CURSOR)
|
|
900
|
-
|
|
901
|
-
# Build YAML
|
|
902
|
-
data = {
|
|
903
|
-
"mode": mode,
|
|
904
|
-
"name": name,
|
|
905
|
-
"slug": slug,
|
|
906
|
-
"description": desc,
|
|
907
|
-
"language": lang,
|
|
908
|
-
"project_type": ptype_key,
|
|
909
|
-
"frontend": frontend,
|
|
910
|
-
"backend": backend,
|
|
911
|
-
"database": database,
|
|
912
|
-
"deploy": deploy,
|
|
913
|
-
"profiles": profiles,
|
|
914
|
-
"runtime": runtime,
|
|
915
|
-
"compliance": compliance,
|
|
916
|
-
}
|
|
917
|
-
yaml_content = build_yaml(data)
|
|
918
|
-
|
|
919
|
-
if DRY_RUN:
|
|
920
|
-
print()
|
|
921
|
-
print(f" {bold('--- project.yaml (dry-run) ---')}")
|
|
922
|
-
print()
|
|
923
|
-
print(yaml_content)
|
|
924
|
-
print(f" {bold('--- fin ---')}")
|
|
925
|
-
else:
|
|
926
|
-
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
927
|
-
out_path.write_text(yaml_content, encoding="utf-8")
|
|
928
|
-
with open(out_path, encoding="utf-8") as _f:
|
|
929
|
-
try:
|
|
930
|
-
yaml.safe_load(_f)
|
|
931
|
-
except yaml.YAMLError as _e:
|
|
932
|
-
print(f"\n ERROR: el YAML generado no es válido: {_e}", file=sys.stderr)
|
|
933
|
-
out_path.unlink(missing_ok=True)
|
|
934
|
-
sys.exit(1)
|
|
935
|
-
print(f"\n {green('✓')} project.yaml escrito en: {bold(str(out_path))}")
|
|
936
|
-
|
|
937
|
-
# 12 — Ejecutar forge-init
|
|
938
|
-
if not NO_INIT and not DRY_RUN:
|
|
939
|
-
print()
|
|
940
|
-
write(SHOW_CURSOR)
|
|
941
|
-
tool_arg = runtime if runtime != "todos" else "all"
|
|
942
|
-
try:
|
|
943
|
-
raw = input(f" {cyan('?')} ¿Ejecutar forge-init --tool {tool_arg} ahora? {dim('[S/n]')}: ").strip().lower()
|
|
944
|
-
except (KeyboardInterrupt, EOFError):
|
|
945
|
-
raw = "n"
|
|
946
|
-
write(HIDE_CURSOR)
|
|
947
|
-
run_init = raw in ("", "s", "si", "sí", "y", "yes")
|
|
948
|
-
|
|
949
|
-
if run_init:
|
|
950
|
-
forge_init = _find_forge_init()
|
|
951
|
-
if forge_init:
|
|
952
|
-
import subprocess
|
|
953
|
-
cmd = [sys.executable, str(forge_init), f"--tool={tool_arg}"]
|
|
954
|
-
print(f" {dim('$ ' + ' '.join(cmd))}\n")
|
|
955
|
-
subprocess.run(cmd, check=False)
|
|
956
|
-
else:
|
|
957
|
-
print(f" {yellow('!')} forge-init.py no encontrado. Ejecutar manualmente:")
|
|
958
|
-
print(f" python3 .agentic/scripts/forge-init.py --tool {tool_arg}")
|
|
959
|
-
|
|
960
|
-
# 13 — Próximos pasos
|
|
961
|
-
print()
|
|
962
|
-
print(f" {bold('Próximos pasos')}")
|
|
963
|
-
steps = _next_steps(mode, out_path, runtime, compliance, profiles, DRY_RUN)
|
|
964
|
-
for step in steps:
|
|
965
|
-
print(f" {step}")
|
|
966
|
-
print()
|
|
967
|
-
write(SHOW_CURSOR)
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
def _find_forge_init() -> Optional[Path]:
|
|
971
|
-
for c in [Path(__file__).parent / "forge-init.py",
|
|
972
|
-
Path.cwd() / ".agentic" / "scripts" / "forge-init.py"]:
|
|
973
|
-
if c.exists():
|
|
974
|
-
return c
|
|
975
|
-
return None
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
def _next_steps(mode: str, out: Path, runtime: str, compliance: List[str],
|
|
979
|
-
profiles: List[str], dry: bool) -> List[str]:
|
|
980
|
-
steps: List[str] = []
|
|
981
|
-
if dry:
|
|
982
|
-
steps.append("Quita --dry-run y vuelve a ejecutar para escribir los archivos.")
|
|
983
|
-
return steps
|
|
984
|
-
|
|
985
|
-
steps.append(f"Revisar {bold(str(out))} y ajustar lo que necesites.")
|
|
986
|
-
tool_arg = runtime if runtime != "todos" else "all"
|
|
987
|
-
steps.append(f"Ejecutar: {bold(f'python3 .agentic/scripts/forge-init.py --tool {tool_arg}')}")
|
|
988
|
-
|
|
989
|
-
if not profiles:
|
|
990
|
-
steps.append(f"Crear un profile Tier 2 con forge-scaffold-profile.py para tu stack.")
|
|
991
|
-
|
|
992
|
-
if mode == "startup":
|
|
993
|
-
steps.append("Cuando el equipo crezca: cambiar mode: startup → standard.")
|
|
994
|
-
if compliance:
|
|
995
|
-
steps.append("Compliance activo — compliance-reviewer incluido en el roster.")
|
|
996
|
-
if mode == "enterprise":
|
|
997
|
-
steps.append("Integrar forge-audit --json en el pipeline de CI.")
|
|
998
|
-
|
|
999
|
-
return steps
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
if __name__ == "__main__":
|
|
1003
|
-
main()
|