@cristiancorreau/forge 2.9.6 → 2.9.8

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.
Files changed (53) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/doctor.d.ts.map +1 -1
  3. package/dist/commands/doctor.js +4 -3
  4. package/dist/commands/doctor.js.map +1 -1
  5. package/dist/commands/generate.js +1 -1
  6. package/dist/commands/generate.js.map +1 -1
  7. package/dist/commands/init.js +2 -2
  8. package/dist/commands/init.js.map +1 -1
  9. package/dist/lib/generators/codex.d.ts.map +1 -1
  10. package/dist/lib/generators/codex.js +10 -8
  11. package/dist/lib/generators/codex.js.map +1 -1
  12. package/dist/lib/generators/kiro.d.ts +1 -1
  13. package/dist/lib/generators/kiro.d.ts.map +1 -1
  14. package/dist/lib/generators/kiro.js +15 -16
  15. package/dist/lib/generators/kiro.js.map +1 -1
  16. package/dist/lib/generators/opencode.d.ts.map +1 -1
  17. package/dist/lib/generators/opencode.js +13 -12
  18. package/dist/lib/generators/opencode.js.map +1 -1
  19. package/dist/lib/paths.d.ts +1 -2
  20. package/dist/lib/paths.d.ts.map +1 -1
  21. package/dist/lib/paths.js +12 -16
  22. package/dist/lib/paths.js.map +1 -1
  23. package/dist/version.d.ts +1 -1
  24. package/dist/version.js +1 -1
  25. package/package.json +2 -2
  26. package/assets/adapters/claude-code/generate-claude-md.py +0 -304
  27. package/assets/adapters/codex/generate-codex-config.py +0 -269
  28. package/assets/adapters/codex/hooks/codex.yaml.tpl +0 -43
  29. package/assets/adapters/codex/hooks/forge-codex-finish.sh +0 -158
  30. package/assets/adapters/codex/hooks/forge-codex-start.sh +0 -186
  31. package/assets/adapters/kiro/generate-steering.py +0 -367
  32. package/assets/adapters/opencode/generate-agents-md.py +0 -262
  33. package/assets/core/hooks/pre-bash-check.py +0 -202
  34. package/assets/core/hooks/pre-edit-check.py +0 -317
  35. package/assets/forge.py +0 -1265
  36. package/assets/requirements.txt +0 -2
  37. package/assets/scripts/aitmpl-search.py +0 -808
  38. package/assets/scripts/forge-add-opportunities.py +0 -92
  39. package/assets/scripts/forge-audit.py +0 -1061
  40. package/assets/scripts/forge-generate-all.py +0 -283
  41. package/assets/scripts/forge-init.py +0 -900
  42. package/assets/scripts/forge-migrate-project-yaml.py +0 -397
  43. package/assets/scripts/forge-scaffold-profile.py +0 -181
  44. package/assets/scripts/forge-teardown.py +0 -193
  45. package/assets/scripts/forge-validate-project-yaml.py +0 -457
  46. package/assets/scripts/forge-wizard.py +0 -1003
  47. package/assets/scripts/setup-codex.sh +0 -229
  48. package/assets/scripts/team-install.sh +0 -147
  49. package/assets/scripts/token-stats.py +0 -201
  50. package/dist/lib/python.d.ts +0 -4
  51. package/dist/lib/python.d.ts.map +0 -1
  52. package/dist/lib/python.js +0 -46
  53. 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()