@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
package/assets/forge.py DELETED
@@ -1,1265 +0,0 @@
1
- #!/usr/bin/env python3
2
- # Copyright 2026 Cristian Correa — Apache License 2.0
3
- # https://github.com/cristiancorreau/forge
4
- """
5
- forge — CLI principal del framework de desarrollo con agentes IA.
6
-
7
- Uso:
8
- python3 .agentic/forge.py
9
- python3 .agentic/forge.py --help
10
- """
11
- from __future__ import annotations
12
-
13
- import importlib.util
14
- import json
15
- import os
16
- import re
17
- import sys
18
- import subprocess
19
- import textwrap
20
- try:
21
- import termios
22
- import tty
23
- except ImportError:
24
- print(
25
- "ERROR: forge requiere Unix o macOS.\n"
26
- " Los módulos 'termios' y 'tty' no están disponibles en Windows.\n"
27
- " Alternativas: WSL (Windows Subsystem for Linux) o ejecutar en macOS/Linux.",
28
- file=sys.stderr,
29
- )
30
- sys.exit(1)
31
- from pathlib import Path
32
- from typing import Optional
33
-
34
- # ---------------------------------------------------------------------------
35
- # Versión y rutas
36
- # ---------------------------------------------------------------------------
37
-
38
- VERSION = "2.0.1"
39
- FORGE_DIR = Path(__file__).parent
40
- SCRIPTS = FORGE_DIR / "scripts"
41
-
42
- # ---------------------------------------------------------------------------
43
- # Terminal helpers
44
- # ---------------------------------------------------------------------------
45
-
46
- IS_TTY = sys.stdin.isatty() and sys.stdout.isatty()
47
-
48
- HIDE_CURSOR = "\033[?25l"
49
- SHOW_CURSOR = "\033[?25h"
50
- RESET = "\033[0m"
51
- BOLD = "\033[1m"
52
- DIM = "\033[2m"
53
- CYAN = "\033[36m"
54
- GREEN = "\033[32m"
55
- YELLOW = "\033[33m"
56
- RED = "\033[31m"
57
- MAGENTA = "\033[35m"
58
- BG_SEL = "\033[48;5;235m" # selección: fondo gris oscuro
59
- FG_DESC = "\033[38;5;252m" # descripción: gris claro
60
- ACCENT = "\033[38;5;75m" # acento: azul claro (key hints)
61
- FG_MUTED = "\033[38;5;240m" # separadores y texto secundario
62
-
63
- # Pills de categoría: (bg_color, fg_color, label_4chars)
64
- _CAT_PILL: dict[str, tuple[str, str, str]] = {
65
- "framework": ("\033[48;5;55m", "\033[97m", " FW "),
66
- "mcp-server": ("\033[48;5;23m", "\033[97m", "MCP "),
67
- "profile": ("\033[48;5;22m", "\033[97m", "PRF "),
68
- "tool": ("\033[48;5;130m", "\033[97m", " TL "),
69
- "resource": ("\033[48;5;17m", "\033[97m", "DOC "),
70
- }
71
-
72
- _ANSI_ESC = re.compile(r'\x1b\[[0-9;?]*[a-zA-Z]')
73
-
74
-
75
- def clr() -> None:
76
- os.system("clear")
77
-
78
- def write(text: str) -> None:
79
- sys.stdout.write(text)
80
- sys.stdout.flush()
81
-
82
- def b(t: str) -> str: return f"{BOLD}{t}{RESET}"
83
- def d(t: str) -> str: return f"{DIM}{t}{RESET}"
84
- def c(t: str) -> str: return f"{CYAN}{t}{RESET}"
85
- def g(t: str) -> str: return f"{GREEN}{t}{RESET}"
86
- def y(t: str) -> str: return f"{YELLOW}{t}{RESET}"
87
- def r(t: str) -> str: return f"{RED}{t}{RESET}"
88
-
89
- def _strip_ansi(s: str) -> str:
90
- return _ANSI_ESC.sub("", s)
91
-
92
- def _padded(s: str, width: int) -> str:
93
- """Pad s to visible width, ignorando códigos ANSI."""
94
- return s + " " * max(0, width - len(_strip_ansi(s)))
95
-
96
- def _hint(key: str, action: str) -> str:
97
- return f"{ACCENT}{key}{RESET} {DIM}{action}{RESET}"
98
-
99
- def _cat_pill(category: str) -> str:
100
- bg, fg, label = _CAT_PILL.get(category, ("\033[48;5;238m", "\033[97m", " ?? "))
101
- return f"{bg}{fg}{label}{RESET}"
102
-
103
-
104
- def getch() -> str:
105
- fd = sys.stdin.fileno()
106
- old = termios.tcgetattr(fd)
107
- try:
108
- tty.setraw(fd)
109
- ch = sys.stdin.read(1)
110
- if ch == "\x1b":
111
- ch2 = sys.stdin.read(1)
112
- ch3 = sys.stdin.read(1)
113
- return f"\x1b{ch2}{ch3}"
114
- return ch
115
- finally:
116
- termios.tcsetattr(fd, termios.TCSADRAIN, old)
117
-
118
-
119
- KEY_UP = "\x1b[A"
120
- KEY_DOWN = "\x1b[B"
121
- KEY_ENTER = "\r"
122
- KEY_Q = "q"
123
- KEY_ESC = "\x1b"
124
- KEY_CTRL_C = "\x03"
125
-
126
- # ---------------------------------------------------------------------------
127
- # MenuItem
128
- # ---------------------------------------------------------------------------
129
-
130
- class MenuItem:
131
- def __init__(
132
- self,
133
- label: str,
134
- key: Optional[str] = None,
135
- separator: bool = False,
136
- description: str = "",
137
- ):
138
- self.label = label
139
- self.key = key
140
- self.separator = separator
141
- self.description = description # texto explicativo que aparece al seleccionar
142
-
143
- # ---------------------------------------------------------------------------
144
- # Panel de descripción
145
- # ---------------------------------------------------------------------------
146
-
147
- DESC_WIDTH = 50 # ancho interior del panel
148
-
149
- def _draw_description(text: str) -> None:
150
- if not text:
151
- print()
152
- return
153
-
154
- border = "─" * DESC_WIDTH
155
- wrapped = textwrap.wrap(text, width=DESC_WIDTH - 2)
156
-
157
- print(f" {FG_MUTED}╭{border}╮{RESET}")
158
- for line in wrapped:
159
- padding = DESC_WIDTH - 2 - len(line)
160
- print(f" {FG_MUTED}│{RESET} {FG_DESC}{line}{' ' * padding}{RESET} {FG_MUTED}│{RESET}")
161
- print(f" {FG_MUTED}╰{border}╯{RESET}")
162
-
163
- # ---------------------------------------------------------------------------
164
- # Menú genérico con flechas + panel de descripción
165
- # ---------------------------------------------------------------------------
166
-
167
- def show_menu(
168
- title: str,
169
- items: list[MenuItem],
170
- subtitle: str = "",
171
- initial: int = 0,
172
- ) -> Optional[str]:
173
- """
174
- Menú navegable con ↑↓ Enter. Muestra la descripción del ítem activo
175
- en un panel debajo de la lista. Retorna el key seleccionado o None.
176
- """
177
- selectable = [i for i, it in enumerate(items) if not it.separator]
178
- if not selectable:
179
- return None
180
-
181
- cursor = initial
182
- if cursor not in selectable:
183
- cursor = selectable[0]
184
-
185
- write(HIDE_CURSOR)
186
- try:
187
- while True:
188
- clr()
189
- _draw_header()
190
- print(f"\n {b(title)}")
191
- if subtitle:
192
- print(f" {d(subtitle)}")
193
- print()
194
-
195
- for idx, item in enumerate(items):
196
- if item.separator:
197
- print(f" {FG_MUTED}{'─' * 40}{RESET}")
198
- continue
199
- if idx == cursor:
200
- marker = f"{ACCENT}❯{RESET}"
201
- label = f"{BG_SEL} {_padded(item.label, 43)}{RESET}"
202
- else:
203
- marker = " "
204
- label = f" {item.label}"
205
- print(f" {marker}{label}")
206
-
207
- print()
208
- current_desc = items[cursor].description if not items[cursor].separator else ""
209
- _draw_description(current_desc)
210
- print()
211
- print(f" {_hint('↑↓', 'navegar')} {_hint('⏎', 'seleccionar')} {_hint('q', 'salir')}")
212
-
213
- ch = getch()
214
-
215
- if ch in (KEY_CTRL_C, KEY_Q, KEY_ESC):
216
- return None
217
- if ch == KEY_UP:
218
- pos = selectable.index(cursor)
219
- cursor = selectable[(pos - 1) % len(selectable)]
220
- elif ch == KEY_DOWN:
221
- pos = selectable.index(cursor)
222
- cursor = selectable[(pos + 1) % len(selectable)]
223
- elif ch == KEY_ENTER:
224
- return items[cursor].key
225
- finally:
226
- write(SHOW_CURSOR)
227
-
228
- # ---------------------------------------------------------------------------
229
- # Header
230
- # ---------------------------------------------------------------------------
231
-
232
- def _draw_header() -> None:
233
- W = 54
234
- inner = "─" * W
235
- badge = f"\033[48;5;238m\033[97m v{VERSION} {RESET}" # pill de versión
236
- name = f"{CYAN}{BOLD}forge{RESET}"
237
- dot = f" {FG_MUTED}·{RESET} "
238
- tag = f"{DIM}Agentic Development Framework{RESET}"
239
- content = f" {name} {badge}{dot}{tag}"
240
- print(f" {CYAN}╭{inner}╮{RESET}")
241
- print(f" {CYAN}│{RESET}{_padded(content, W)}{CYAN}│{RESET}")
242
- print(f" {CYAN}╰{inner}╯{RESET}")
243
-
244
- # ---------------------------------------------------------------------------
245
- # Utilidades
246
- # ---------------------------------------------------------------------------
247
-
248
- def pause(msg: str = "Presiona Enter para volver al menú…") -> None:
249
- write(SHOW_CURSOR)
250
- print(f"\n {d(msg)}", flush=True)
251
- try:
252
- # Limpiar stdin residual del subprocess antes de leer
253
- import termios as _t
254
- _t.tcflush(sys.stdin.fileno(), _t.TCIFLUSH)
255
- except Exception:
256
- pass
257
- try:
258
- input()
259
- except (KeyboardInterrupt, EOFError):
260
- pass
261
- write(HIDE_CURSOR)
262
-
263
-
264
- def run_script(script: Path, *args: str) -> int:
265
- clr()
266
- _draw_header()
267
- print()
268
- cmd = [sys.executable, str(script)] + list(args)
269
- print(f" {d('$ ' + ' '.join(cmd))}\n")
270
- result = subprocess.run(cmd)
271
- return result.returncode
272
-
273
-
274
- def _ask_input(prompt: str, default: str = "") -> str:
275
- write(SHOW_CURSOR)
276
- hint = f" {d(f'[{default}]')}" if default else ""
277
- try:
278
- val = input(f" {c('?')} {prompt}{hint}: ").strip()
279
- except (KeyboardInterrupt, EOFError):
280
- return default
281
- finally:
282
- write(HIDE_CURSOR)
283
- return val if val else default
284
-
285
-
286
- def _ask_yes_no(prompt: str, default: bool = True) -> bool:
287
- write(SHOW_CURSOR)
288
- hint = "S/n" if default else "s/N"
289
- try:
290
- raw = input(f" {c('?')} {prompt} {d(f'[{hint}]')}: ").strip().lower()
291
- except (KeyboardInterrupt, EOFError):
292
- return default
293
- finally:
294
- write(HIDE_CURSOR)
295
- if not raw:
296
- return default
297
- return raw in ("s", "si", "sí", "y", "yes")
298
-
299
- # ---------------------------------------------------------------------------
300
- # Catálogo — helpers de integración
301
- # ---------------------------------------------------------------------------
302
-
303
- _AITMPL_MOD = None # cache del módulo cargado
304
-
305
- def _load_aitmpl_mod():
306
- global _AITMPL_MOD
307
- if _AITMPL_MOD is not None:
308
- return _AITMPL_MOD
309
- script = SCRIPTS / "aitmpl-search.py"
310
- spec = importlib.util.spec_from_file_location("aitmpl_search", script)
311
- mod = importlib.util.module_from_spec(spec)
312
- spec.loader.exec_module(mod)
313
- _AITMPL_MOD = mod
314
- return mod
315
-
316
-
317
- CATEGORY_LABELS_MENU = {
318
- "framework": "Framework",
319
- "mcp-server": "MCP Server",
320
- "profile": "Profile forge",
321
- "tool": "Herramienta",
322
- "resource": "Recurso",
323
- }
324
-
325
-
326
- def _mcp_slug(item: dict) -> str:
327
- install = item.get("install")
328
- if install:
329
- return install["slug"]
330
- name = item.get("name", "")
331
- if "—" in name:
332
- return name.split("—", 1)[1].strip().split()[0].lower()
333
- return name.lower().replace(" ", "-")
334
-
335
-
336
- def _profile_slug(item: dict) -> str:
337
- name = item.get("name", "")
338
- if "—" in name:
339
- return name.split("—", 1)[1].strip().lower()
340
- return name.lower().replace(" ", "-")
341
-
342
-
343
- def _copy_to_clipboard(text: str) -> None:
344
- try:
345
- if sys.platform == "darwin":
346
- subprocess.run(["pbcopy"], input=text.encode(), check=True, timeout=3)
347
- else:
348
- try:
349
- subprocess.run(["xclip", "-selection", "clipboard"], input=text.encode(), check=True, timeout=3)
350
- except (FileNotFoundError, subprocess.CalledProcessError):
351
- subprocess.run(["xsel", "--clipboard", "--input"], input=text.encode(), check=True, timeout=3)
352
- clr()
353
- _draw_header()
354
- print(f"\n {g('Copiado al portapapeles.')}\n {d(text)}")
355
- except Exception:
356
- clr()
357
- _draw_header()
358
- print(f"\n URL: {text}")
359
- pause()
360
-
361
-
362
- def _load_settings() -> dict:
363
- f = Path.cwd() / ".claude" / "settings.json"
364
- if not f.exists():
365
- return {}
366
- try:
367
- with open(f) as fh:
368
- return json.load(fh)
369
- except (json.JSONDecodeError, OSError):
370
- return {}
371
-
372
-
373
- def _save_settings(data: dict) -> None:
374
- f = Path.cwd() / ".claude" / "settings.json"
375
- f.parent.mkdir(parents=True, exist_ok=True)
376
- with open(f, "w") as fh:
377
- json.dump(data, fh, indent=2, ensure_ascii=False)
378
- fh.write("\n")
379
-
380
-
381
- def _is_mcp_installed(slug: str) -> bool:
382
- return slug in _load_settings().get("mcpServers", {})
383
-
384
-
385
- def _install_mcp_server(item: dict) -> None:
386
- install = item["install"]
387
- slug = install["slug"]
388
- params = install.get("params", [])
389
- env_defs = install.get("env", [])
390
-
391
- clr()
392
- _draw_header()
393
- print(f"\n Instalando {b(item['name'])}\n")
394
-
395
- # Prompts para parámetros en args
396
- values: dict[str, str] = {}
397
- for p in params:
398
- default = p.get("default", "")
399
- val = _ask_input(p["label"], default)
400
- values[p["key"]] = val if val else default
401
-
402
- # Construir args finales sustituyendo placeholders
403
- final_args = []
404
- for a in install.get("args", []):
405
- try:
406
- final_args.append(a.format_map(values))
407
- except KeyError:
408
- final_args.append(a)
409
-
410
- # Prompts para variables de entorno
411
- env_block: dict[str, str] = {}
412
- for e in env_defs:
413
- val = _ask_input(e["label"], e.get("default", ""))
414
- if val:
415
- env_block[e["key"]] = val
416
-
417
- # Construir config del server
418
- server_cfg: dict = {
419
- "command": install["command"],
420
- "args": final_args,
421
- }
422
- if env_block:
423
- server_cfg["env"] = env_block
424
-
425
- # Merge en settings.json
426
- settings = _load_settings()
427
- already = slug in settings.get("mcpServers", {})
428
- if "mcpServers" not in settings:
429
- settings["mcpServers"] = {}
430
- settings["mcpServers"][slug] = server_cfg
431
- _save_settings(settings)
432
-
433
- print()
434
- if already:
435
- print(f" {y('Actualizado')} — '{slug}' ya existía en .claude/settings.json (reemplazado).")
436
- else:
437
- print(f" {g('Instalado')} — '{slug}' agregado a .claude/settings.json.")
438
- print(f"\n Reinicia Claude Code para activar el servidor MCP.")
439
- pause()
440
-
441
-
442
- def _find_project_yaml() -> Optional[Path]:
443
- cwd = Path.cwd()
444
- for candidate in [cwd / "project.yaml", cwd / ".claude" / "project.yaml"]:
445
- if candidate.exists():
446
- return candidate
447
- return None
448
-
449
-
450
- def _is_profile_in_project(slug: str) -> bool:
451
- yaml_file = _find_project_yaml()
452
- if not yaml_file:
453
- return False
454
- try:
455
- import yaml # type: ignore
456
- with open(yaml_file) as f:
457
- data = yaml.safe_load(f)
458
- return slug in (data or {}).get("agents", {}).get("profiles", [])
459
- except Exception:
460
- return False
461
-
462
-
463
- def _add_profile_to_project(slug: str) -> None:
464
- yaml_file = _find_project_yaml()
465
- if not yaml_file:
466
- clr()
467
- _draw_header()
468
- print(f"\n {r('No se encontró project.yaml')} en el directorio actual.")
469
- print(f" Ejecuta primero el wizard para crear el proyecto.")
470
- pause()
471
- return
472
-
473
- try:
474
- import yaml # type: ignore
475
- except ImportError:
476
- clr()
477
- _draw_header()
478
- print(f"\n {r('PyYAML no está instalado.')} Instálalo con: pip install pyyaml")
479
- pause()
480
- return
481
-
482
- try:
483
- with open(yaml_file) as f:
484
- data = yaml.safe_load(f) or {}
485
-
486
- agents = data.setdefault("agents", {})
487
- profiles = agents.setdefault("profiles", [])
488
-
489
- clr()
490
- _draw_header()
491
- if slug in profiles:
492
- print(f"\n El profile '{b(slug)}' ya está en project.yaml.")
493
- else:
494
- profiles.append(slug)
495
- with open(yaml_file, "w") as f:
496
- yaml.dump(data, f, allow_unicode=True, default_flow_style=False, sort_keys=False)
497
- print(f"\n {g('Agregado')} — profile '{b(slug)}' añadido a project.yaml.")
498
- print(f"\n Ejecuta 'Inicializar agentes' para instalar el agente en el proyecto.")
499
- pause()
500
- except Exception as exc:
501
- clr()
502
- _draw_header()
503
- print(f"\n {r('Error al modificar project.yaml:')} {exc}")
504
- pause()
505
-
506
-
507
- def _build_result_menu(results: list[dict]) -> list[MenuItem]:
508
- items = []
509
- for i, r in enumerate(results):
510
- cat = r.get("category", "")
511
- pill = _cat_pill(cat) # 4 visible chars
512
- name = r.get("name", "")
513
- display = name.split("—", 1)[1].strip() if "—" in name else name
514
- if len(display) > 37:
515
- display = display[:34] + "…"
516
- label = f"{pill} {display}" # pill(4) + space(1) + display(≤37) = ≤42 visible
517
- items.append(MenuItem(label, key=str(i), description=r.get("description", "")))
518
- items.append(MenuItem("", separator=True))
519
- items.append(MenuItem("← Volver", key="back", description="Regresa al menú de búsqueda."))
520
- return items
521
-
522
-
523
- def _action_menu_item(item: dict) -> None:
524
- category = item.get("category", "")
525
- name = item.get("name", "")
526
- url = item.get("url", "")
527
-
528
- actions: list[MenuItem] = []
529
-
530
- # MCP server: instalar
531
- if category == "mcp-server" and item.get("install"):
532
- slug = _mcp_slug(item)
533
- already = _is_mcp_installed(slug)
534
- if already:
535
- label = f"{g('✓')} Instalado — reinstalar ({slug})"
536
- desc = f"'{slug}' ya está en .claude/settings.json. Reinstalar sobreescribirá la config."
537
- else:
538
- label = f"+ Instalar en proyecto (.claude/settings.json)"
539
- desc = "Agrega este MCP server a .claude/settings.json con los parámetros que indiques."
540
- actions.append(MenuItem(label, key="install", description=desc))
541
-
542
- # Profile: agregar a project.yaml
543
- if category == "profile":
544
- slug = _profile_slug(item)
545
- already = _is_profile_in_project(slug)
546
- if already:
547
- actions.append(MenuItem(
548
- f"{g('✓')} En project.yaml ({slug})", key="noop",
549
- description=f"El profile '{slug}' ya está en agents.profiles de project.yaml.",
550
- ))
551
- else:
552
- actions.append(MenuItem(
553
- f"+ Agregar a project.yaml ({slug})", key="add-profile",
554
- description=f"Añade '{slug}' a agents.profiles en project.yaml. Luego ejecuta 'Inicializar agentes'.",
555
- ))
556
-
557
- # Abrir en browser / copiar URL
558
- if url:
559
- actions.append(MenuItem(
560
- "↗ Abrir en browser", key="open",
561
- description=f"Abre {url[:60]} en el navegador predeterminado.",
562
- ))
563
- actions.append(MenuItem(
564
- "⎘ Copiar URL", key="copy",
565
- description=url,
566
- ))
567
-
568
- actions.append(MenuItem("", separator=True))
569
- actions.append(MenuItem("← Volver", key="back", description="Regresa a la lista de resultados."))
570
-
571
- title_short = name if len(name) <= 52 else name[:49] + "…"
572
- key = show_menu(title_short, actions)
573
-
574
- if key == "install":
575
- _install_mcp_server(item)
576
- elif key == "add-profile":
577
- _add_profile_to_project(_profile_slug(item))
578
- elif key == "open":
579
- opener = "open" if sys.platform == "darwin" else "xdg-open"
580
- subprocess.Popen([opener, url])
581
- elif key == "copy":
582
- _copy_to_clipboard(url)
583
-
584
- # ---------------------------------------------------------------------------
585
- # Submenús
586
- # ---------------------------------------------------------------------------
587
-
588
- def menu_wizard() -> None:
589
- items = [
590
- MenuItem(
591
- "Automático — detecta el modo según equipo", key="auto",
592
- description=(
593
- "Pregunta cuántas personas hay en el equipo y elige el modo correcto: "
594
- "Startup (1-2 personas), Standard (3-8) o Enterprise (9+). "
595
- "Luego guía la selección de stack, deploy, runtime y compliance."
596
- ),
597
- ),
598
- MenuItem(
599
- "Startup — 1-2 personas", key="startup",
600
- description=(
601
- "Configuración mínima para equipos pequeños o proyectos en exploración. "
602
- "Un solo agente de implementación, sin fases de sprint formales y SDD opcional. "
603
- "Ideal para prototipos y MVPs donde el overhead debe ser mínimo."
604
- ),
605
- ),
606
- MenuItem(
607
- "Standard — 3-8 personas", key="standard",
608
- description=(
609
- "Configuración completa con roster de agentes, fases de sprint A/B, "
610
- "skills de seguridad y features. Pensado para equipos de producto "
611
- "que trabajan con Claude Code de forma activa en el loop de desarrollo."
612
- ),
613
- ),
614
- MenuItem(
615
- "Enterprise — 9+ personas", key="enterprise",
616
- description=(
617
- "Configuración con compliance activo (GDPR, Ley 21719, etc.), "
618
- "audit logs, security-auditor obligatorio y 4 fases de sprint. "
619
- "Incluye hint para integrar forge-audit --json en pipelines de CI."
620
- ),
621
- ),
622
- MenuItem("", separator=True),
623
- MenuItem("← Volver", key="back", description="Regresa al menú principal."),
624
- ]
625
- key = show_menu(
626
- "Nuevo proyecto — elegir modo",
627
- items,
628
- subtitle="El wizard genera project.yaml y opcionalmente ejecuta forge-init",
629
- )
630
- if not key or key == "back":
631
- return
632
- args: list[str] = [] if key == "auto" else [f"--mode={key}"]
633
- run_script(SCRIPTS / "forge-wizard.py", *args)
634
- pause()
635
-
636
-
637
- def menu_init() -> None:
638
- items = [
639
- MenuItem(
640
- "Claude Code → .claude/agents/ + CLAUDE.md", key="claude-code",
641
- description=(
642
- "Instala los agentes de forge en .claude/agents/ con scope inyectado "
643
- "en el frontmatter. Genera también: CLAUDE.md (tabla agente/scope/trigger), "
644
- ".claude/settings.json (permisos según el stack) y slash commands "
645
- "/new-feature, /deploy-check, /review en .claude/commands/."
646
- ),
647
- ),
648
- MenuItem(
649
- "OpenCode → AGENTS.md", key="opencode",
650
- description=(
651
- "Genera AGENTS.md con el roster y descripción de cada agente "
652
- "en el formato que OpenCode y Codex esperan. "
653
- "No toca .claude/ ni otros archivos del proyecto."
654
- ),
655
- ),
656
- MenuItem(
657
- "Kiro → .kiro/steering/", key="kiro",
658
- description=(
659
- "Genera los steering files de Kiro: product.md, structure.md, "
660
- "agents.md y compliance.md (si hay frameworks configurados). "
661
- "Las reglas de compliance se propagan automáticamente desde project.yaml."
662
- ),
663
- ),
664
- MenuItem(
665
- "Todos → genera los tres formatos", key="all",
666
- description=(
667
- "Ejecuta los tres adapters en secuencia: Claude Code, OpenCode y Kiro. "
668
- "Útil cuando el equipo usa más de un runtime o quiere tener "
669
- "todos los formatos disponibles sin pasos adicionales."
670
- ),
671
- ),
672
- MenuItem("", separator=True),
673
- MenuItem(
674
- "Regenerar CLAUDE.md — sin reinstalar agentes", key="claude-md",
675
- description=(
676
- "Regenera solo el CLAUDE.md desde project.yaml (tabla de agentes con scope, "
677
- "comandos del stack, fases del sprint). Útil cuando actualizaste project.yaml "
678
- "y quieres refrescar el contexto sin re-instalar todos los agentes."
679
- ),
680
- ),
681
- MenuItem("← Volver", key="back", description="Regresa al menú principal."),
682
- ]
683
- key = show_menu(
684
- "Inicializar agentes — elegir runtime",
685
- items,
686
- subtitle="Lee project.yaml e instala los agentes del proyecto",
687
- )
688
- if not key or key == "back":
689
- return
690
- if key == "claude-md":
691
- run_script(FORGE_DIR / "adapters" / "claude-code" / "generate-claude-md.py", "--force")
692
- pause()
693
- return
694
- clr()
695
- _draw_header()
696
- print(f"\n Runtime: {b(key)}\n")
697
- force = _ask_yes_no("¿Sobreescribir agentes existentes? (--force)")
698
- extra = ["--force"] if force else []
699
- run_script(SCRIPTS / "forge-init.py", f"--tool={key}", *extra)
700
- pause()
701
-
702
-
703
- def menu_audit() -> None:
704
- items = [
705
- MenuItem(
706
- "Auditoría completa", key="full",
707
- description=(
708
- "Compara todos los agentes instalados en .claude/agents/ contra "
709
- "los archivos de referencia en forge. Muestra similitud, errores "
710
- "de frontmatter, campos faltantes y acciones correctivas con el "
711
- "comando exacto para corregir cada problema."
712
- ),
713
- ),
714
- MenuItem(
715
- "Agente específico (--only)", key="only",
716
- description=(
717
- "Audita un solo agente por nombre. Útil cuando actualizaste "
718
- "un agente y quieres verificar que está sincronizado sin "
719
- "revisar el roster completo."
720
- ),
721
- ),
722
- MenuItem(
723
- "Salida JSON — para CI/CD", key="json",
724
- description=(
725
- "Imprime el resultado en JSON estructurado. Retorna exit code 1 "
726
- "si hay errores de severidad 'error' o 'critical'. "
727
- "Integrar en pipelines con: forge-audit.py --json | jq '.summary'"
728
- ),
729
- ),
730
- MenuItem("", separator=True),
731
- MenuItem("← Volver", key="back", description="Regresa al menú principal."),
732
- ]
733
- key = show_menu("Auditar proyecto", items)
734
- if not key or key == "back":
735
- return
736
- if key == "full":
737
- run_script(SCRIPTS / "forge-audit.py")
738
- elif key == "only":
739
- clr()
740
- _draw_header()
741
- print()
742
- agent = _ask_input("Nombre del agente a auditar", "backend-engineer")
743
- run_script(SCRIPTS / "forge-audit.py", f"--only={agent}")
744
- elif key == "json":
745
- run_script(SCRIPTS / "forge-audit.py", "--json")
746
- pause()
747
-
748
-
749
- def menu_aitmpl() -> None:
750
- mod = _load_aitmpl_mod()
751
-
752
- while True:
753
- items_mode = [
754
- MenuItem(
755
- "Buscar por palabra clave", key="query",
756
- description=(
757
- "Busca en el catálogo por nombre, descripción o tecnología. "
758
- "Ejemplos: 'postgres', 'rails', 'nextjs typescript', 'playwright'."
759
- ),
760
- ),
761
- MenuItem(
762
- "Ver por categoría framework · mcp-server · profile · tool", key="category",
763
- description=(
764
- "Muestra todos los items de una categoría: frameworks de agentes, "
765
- "MCP servers (20 disponibles), profiles de forge (15), herramientas y recursos."
766
- ),
767
- ),
768
- MenuItem("", separator=True),
769
- MenuItem("← Volver", key="back", description="Regresa al menú principal."),
770
- ]
771
- mode = show_menu("Buscar templates y recursos", items_mode,
772
- subtitle="Catálogo curado — instala MCP servers y profiles directamente")
773
- if not mode or mode == "back":
774
- return
775
-
776
- results: list[dict] = []
777
-
778
- if mode == "category":
779
- cat_items = [
780
- MenuItem("framework Frameworks de agentes IA", key="framework",
781
- description="forge, aider, micro-agent, anthropic-quickstarts, claude-code-action."),
782
- MenuItem("mcp-server Servidores MCP instalables", key="mcp-server",
783
- description="20 servers con instalación directa: filesystem, git, github, postgres, slack, playwright, docker, cloudflare, vercel y más."),
784
- MenuItem("profile Profiles de stack para forge", key="profile",
785
- description="15 profiles: hono-drizzle, nextjs-admin, astro, fastapi, rails, nestjs, express, expo, playwright-crawler, django, vuenuxt, go-gin, sveltekit, laravel, wordpress."),
786
- MenuItem("tool Herramientas CLI", key="tool",
787
- description="Claude Code CLI, MCP Inspector."),
788
- MenuItem("resource Documentación y listas", key="resource",
789
- description="Docs oficiales MCP, docs Claude Code, awesome-mcp-servers."),
790
- MenuItem("", separator=True),
791
- MenuItem("← Volver", key="back", description="Regresa al menú anterior."),
792
- ]
793
- cat = show_menu("Seleccionar categoría", cat_items)
794
- if not cat or cat == "back":
795
- continue
796
- results = mod._search_local("", category=cat)
797
- else:
798
- clr()
799
- _draw_header()
800
- print()
801
- query = _ask_input("¿Qué buscas?", "mcp postgres")
802
- if not query:
803
- continue
804
- results = mod._search_local(query)
805
-
806
- if not results:
807
- clr()
808
- _draw_header()
809
- print(f"\n {y('Sin resultados.')} Prueba con otro término o cambia la categoría.\n")
810
- pause()
811
- continue
812
-
813
- # Mostrar resultados navegables
814
- while True:
815
- result_items = _build_result_menu(results)
816
- n = len(results)
817
- sel = show_menu(
818
- f"Resultados — {n} encontrado{'s' if n != 1 else ''}",
819
- result_items,
820
- subtitle="Enter para ver acciones · instalar MCP · agregar profile · abrir URL",
821
- )
822
- if not sel or sel == "back":
823
- break
824
- _action_menu_item(results[int(sel)])
825
-
826
-
827
- def menu_scaffold() -> None:
828
- clr()
829
- _draw_header()
830
- print(f"\n {b('Crear nuevo profile Tier 2')}\n")
831
- print(textwrap.fill(
832
- "Un profile Tier 2 es un agente especializado para un stack tecnológico "
833
- "específico. Por ejemplo: un api-engineer para Django conoce los modelos, "
834
- "las migraciones y DRF — cosas que el agente genérico no sabe.",
835
- width=60, initial_indent=" ", subsequent_indent=" ",
836
- ))
837
- print()
838
- print(f" {d('Profiles disponibles: hono-drizzle · nextjs-admin · astro · expo · playwright-crawler')}")
839
- print(f" {d(' fastapi · express · rails · nestjs · django · go-gin · sveltekit · vuenuxt')}")
840
- print(f" {d(' laravel · wordpress')}\n")
841
-
842
- name = _ask_input("Nombre del stack nuevo (ej: django, laravel, gin)")
843
- if not name:
844
- return
845
- engineer = _ask_input("Nombre del agente (ej: api-engineer)", "api-engineer")
846
- desc = _ask_input("Descripción breve (Enter = generar automático)")
847
- details = _ask_input("Tecnologías del stack (ej: Django 4.2 + PostgreSQL + DRF)")
848
-
849
- args = ["--name", name, "--engineer", engineer]
850
- if desc:
851
- args += ["--description", desc]
852
- if details:
853
- args += ["--stack-details", details]
854
- print()
855
- run_script(SCRIPTS / "forge-scaffold-profile.py", *args)
856
- pause()
857
-
858
-
859
- def wiki_status() -> None:
860
- clr()
861
- _draw_header()
862
- print(f"\n {b('Wiki — estado actual')}\n")
863
-
864
- wiki_root = Path.cwd() / "docs" / "wiki"
865
- if not wiki_root.exists():
866
- print(f" {y('docs/wiki/ no existe.')} Ejecuta 'forge wiki ingest' para inicializarlo.")
867
- pause()
868
- return
869
-
870
- subdirs = ["raw", "concepts", "entities", "sources", "synthesis"]
871
- W_DIR = 14
872
- W_COUNT = 8
873
- W_MOD = 22
874
-
875
- header = (
876
- f" {b(_padded('Directorio', W_DIR))}"
877
- f" {b(_padded('Archivos', W_COUNT))}"
878
- f" {b('Última modificación')}"
879
- )
880
- print(header)
881
- print(f" {FG_MUTED}{'─' * (W_DIR + W_COUNT + W_MOD + 4)}{RESET}")
882
-
883
- for sub in subdirs:
884
- d_path = wiki_root / sub
885
- if d_path.exists():
886
- files = list(d_path.iterdir())
887
- count = len(files)
888
- if files:
889
- latest = max(f.stat().st_mtime for f in files)
890
- import datetime
891
- mod_str = datetime.datetime.fromtimestamp(latest).strftime("%Y-%m-%d %H:%M")
892
- else:
893
- mod_str = "—"
894
- else:
895
- count = 0
896
- mod_str = "—"
897
- print(
898
- f" {_padded(sub, W_DIR)}"
899
- f" {_padded(str(count), W_COUNT)}"
900
- f" {d(mod_str)}"
901
- )
902
-
903
- print(f"\n {b('Archivos de control:')}")
904
- for fname in ("index.md", "log.md"):
905
- fpath = wiki_root / fname
906
- mark = g("ok") if fpath.exists() else r("falta")
907
- print(f" docs/wiki/{fname} [{mark}]")
908
-
909
- pause()
910
-
911
-
912
- def wiki_ingest(source: Optional[str] = None) -> None:
913
- clr()
914
- _draw_header()
915
- print(f"\n {b('Wiki — ingestar fuente')}\n")
916
-
917
- wiki_root = Path.cwd() / "docs" / "wiki"
918
- if not wiki_root.exists():
919
- print(f" Inicializando estructura docs/wiki/ …")
920
- for sub in ["raw", "concepts", "entities", "sources", "synthesis"]:
921
- (wiki_root / sub).mkdir(parents=True, exist_ok=True)
922
- for fname, content in [
923
- ("index.md", "# Wiki Index\n\n<!-- generado por forge wiki ingest -->\n"),
924
- ("log.md", "# Wiki Log\n\n<!-- ingesta registrada aquí -->\n"),
925
- ]:
926
- fpath = wiki_root / fname
927
- if not fpath.exists():
928
- fpath.write_text(content, encoding="utf-8")
929
- print(f" {g('Estructura creada.')} docs/wiki/ inicializado con index.md y log.md.")
930
- print()
931
-
932
- if source:
933
- print(f" Fuente: {c(source)}")
934
- else:
935
- print(f" {d('No se especificó fuente. En el slash command puedes pasar URL, ruta o texto.')}")
936
-
937
- print()
938
- print(f" {b('Siguiente paso:')}")
939
- print(f" En Claude Code ejecuta el slash command:")
940
- print(f" {c('/wiki-ingest')}{' ' + source if source else ''}")
941
- print()
942
- print(f" El skill procesa la fuente y actualiza raw/, pages, index.md y log.md.")
943
- pause()
944
-
945
-
946
- def wiki_query(question: Optional[str] = None) -> None:
947
- clr()
948
- _draw_header()
949
- print(f"\n {b('Wiki — consultar')}\n")
950
-
951
- if question:
952
- print(f" Pregunta: {c(question)}")
953
- print()
954
-
955
- print(f" {b('Siguiente paso:')}")
956
- print(f" En Claude Code ejecuta el slash command:")
957
- if question:
958
- print(f" {c('/wiki-query')} {question}")
959
- else:
960
- print(f" {c('/wiki-query')} <tu pregunta>")
961
- print()
962
- print(f" El skill busca en docs/wiki/ y responde con citas a las páginas fuente.")
963
- pause()
964
-
965
-
966
- def wiki_lint() -> None:
967
- clr()
968
- _draw_header()
969
- print(f"\n {b('Wiki — health check')}\n")
970
- print(f" {b('Siguiente paso:')}")
971
- print(f" En Claude Code ejecuta el slash command:")
972
- print(f" {c('/wiki-lint')}")
973
- print()
974
- print(f" El skill verifica integridad del wiki: links, huérfanos, frontmatter, log.")
975
- print(f" Repara automáticamente lo que es seguro; lista el resto para revisión manual.")
976
- pause()
977
-
978
-
979
- def menu_wiki() -> None:
980
- items = [
981
- MenuItem(
982
- "status — resumen de la estructura del wiki", key="status",
983
- description=(
984
- "Muestra el estado actual de docs/wiki/: subdirectorios (raw, concepts, "
985
- "entities, sources, synthesis), cantidad de archivos en cada uno, "
986
- "última modificación, y si existen index.md y log.md."
987
- ),
988
- ),
989
- MenuItem(
990
- "ingest — indexar una fuente en el wiki", key="ingest",
991
- description=(
992
- "Inicializa docs/wiki/ si no existe (crea subdirectorios, index.md y log.md). "
993
- "Luego guía para ejecutar el slash command /wiki-ingest con la fuente "
994
- "(URL, ruta de archivo o texto libre). El skill AI hace el trabajo de ingesta."
995
- ),
996
- ),
997
- MenuItem(
998
- "query — consultar el wiki", key="query",
999
- description=(
1000
- "Guía para ejecutar /wiki-query con tu pregunta. "
1001
- "El skill busca en docs/wiki/ y responde citando las páginas fuente."
1002
- ),
1003
- ),
1004
- MenuItem(
1005
- "lint — verificar integridad del wiki", key="lint",
1006
- description=(
1007
- "Guía para ejecutar /wiki-lint. El skill verifica links rotos, "
1008
- "páginas huérfanas, frontmatter inválido y consistencia del log. "
1009
- "Repara automáticamente lo seguro y lista el resto."
1010
- ),
1011
- ),
1012
- MenuItem("", separator=True),
1013
- MenuItem("← Volver", key="back", description="Regresa al menú principal."),
1014
- ]
1015
- while True:
1016
- key = show_menu(
1017
- "Wiki — gestión del knowledge base",
1018
- items,
1019
- subtitle="docs/wiki/ · /wiki-ingest /wiki-query /wiki-lint",
1020
- )
1021
- if not key or key == "back":
1022
- return
1023
- if key == "status":
1024
- wiki_status()
1025
- elif key == "ingest":
1026
- clr()
1027
- _draw_header()
1028
- print()
1029
- src = _ask_input("Fuente a ingestar (URL, ruta o Enter para omitir)")
1030
- wiki_ingest(src if src else None)
1031
- elif key == "query":
1032
- clr()
1033
- _draw_header()
1034
- print()
1035
- q = _ask_input("Pregunta para el wiki (Enter para omitir)")
1036
- wiki_query(q if q else None)
1037
- elif key == "lint":
1038
- wiki_lint()
1039
-
1040
-
1041
- def menu_teardown() -> None:
1042
- items = [
1043
- MenuItem(
1044
- "Vista previa (dry-run)", key="dry",
1045
- description=(
1046
- "Muestra qué archivos serían eliminados sin borrar nada. "
1047
- "Siempre es recomendable ejecutar esto primero para verificar "
1048
- "que el teardown no toca archivos propios del proyecto."
1049
- ),
1050
- ),
1051
- MenuItem(
1052
- "Ejecutar teardown", key="confirm",
1053
- description=(
1054
- "Elimina los agentes instalados por forge de .claude/agents/, "
1055
- "preservando los agentes Tier 3 (propios del proyecto) y los "
1056
- "archivos de trabajo como CLAUDE.md y project.yaml. "
1057
- "Pedirá confirmación antes de proceder."
1058
- ),
1059
- ),
1060
- MenuItem("", separator=True),
1061
- MenuItem("← Volver", key="back", description="Regresa al menú principal."),
1062
- ]
1063
- key = show_menu(
1064
- "Teardown del proyecto",
1065
- items,
1066
- subtitle="Revierte la instalación de forge preservando el trabajo del proyecto",
1067
- )
1068
- if not key or key == "back":
1069
- return
1070
- if key == "dry":
1071
- run_script(SCRIPTS / "forge-teardown.py")
1072
- elif key == "confirm":
1073
- clr()
1074
- _draw_header()
1075
- print(f"\n {r(b('ATENCIÓN'))} — Esta operación elimina agentes instalados por forge.\n")
1076
- if _ask_yes_no("¿Confirmar teardown?", default=False):
1077
- run_script(SCRIPTS / "forge-teardown.py", "--confirm")
1078
- else:
1079
- print(f"\n Cancelado.")
1080
- pause()
1081
-
1082
- # ---------------------------------------------------------------------------
1083
- # Menú principal
1084
- # ---------------------------------------------------------------------------
1085
-
1086
- MAIN_ITEMS = [
1087
- MenuItem(
1088
- "Nuevo proyecto wizard interactivo", key="wizard",
1089
- description=(
1090
- "Genera project.yaml paso a paso eligiendo tipo de proyecto, "
1091
- "framework frontend/backend, base de datos, plataforma de deploy "
1092
- "y compliance. Al terminar instala los agentes automáticamente."
1093
- ),
1094
- ),
1095
- MenuItem(
1096
- "Inicializar agentes forge-init", key="init",
1097
- description=(
1098
- "Lee el project.yaml existente e instala los agentes de forge "
1099
- "en el runtime elegido. Para Claude Code también genera CLAUDE.md, "
1100
- ".claude/settings.json y slash commands /new-feature /deploy-check /review. "
1101
- "Por defecto no sobreescribe agentes ya existentes."
1102
- ),
1103
- ),
1104
- MenuItem(
1105
- "Auditar proyecto forge-audit", key="audit",
1106
- description=(
1107
- "Compara los agentes instalados contra los de forge para detectar "
1108
- "campos faltantes, versiones desactualizadas o agentes que ya no "
1109
- "están en el roster activo. Soporta salida JSON para CI/CD."
1110
- ),
1111
- ),
1112
- MenuItem(
1113
- "Wiki knowledge base del proyecto", key="wiki",
1114
- description=(
1115
- "Gestiona el wiki del proyecto en docs/wiki/. "
1116
- "Opciones: status (resumen de estructura), ingest (indexar fuentes), "
1117
- "query (consultar con citas) y lint (verificar integridad). "
1118
- "La ingesta y consulta usan los slash commands /wiki-ingest y /wiki-query de Claude Code."
1119
- ),
1120
- ),
1121
- MenuItem(
1122
- "Buscar templates frameworks · MCP · profiles", key="aitmpl",
1123
- description=(
1124
- "Catálogo curado de 40+ recursos: frameworks (forge, aider), "
1125
- "20 MCP servers (postgres, github, slack, playwright...), "
1126
- "profiles de stack y herramientas. Filtrable por categoría. "
1127
- "Funciona offline — sin dependencias de red."
1128
- ),
1129
- ),
1130
- MenuItem(
1131
- "Nuevo profile Tier 2 scaffold", key="scaffold",
1132
- description=(
1133
- "Crea el esqueleto de un agente especializado para un stack no cubierto "
1134
- "por los profiles actuales (django, laravel, gin, sveltekit, etc.). "
1135
- "Genera el .md con frontmatter correcto y todas las secciones obligatorias."
1136
- ),
1137
- ),
1138
- MenuItem(
1139
- "Teardown revertir instalación", key="teardown",
1140
- description=(
1141
- "Elimina los artefactos que forge instaló en el proyecto: agentes en "
1142
- ".claude/agents/, AGENTS.md, steering files de Kiro. "
1143
- "Preserva tu trabajo: CLAUDE.md, project.yaml y agentes Tier 3 propios."
1144
- ),
1145
- ),
1146
- MenuItem("", separator=True),
1147
- MenuItem(
1148
- "Salir", key="quit",
1149
- description="Cierra el CLI de forge.",
1150
- ),
1151
- ]
1152
-
1153
- ACTIONS = {
1154
- "wizard": menu_wizard,
1155
- "init": menu_init,
1156
- "audit": menu_audit,
1157
- "wiki": menu_wiki,
1158
- "aitmpl": menu_aitmpl,
1159
- "scaffold": menu_scaffold,
1160
- "teardown": menu_teardown,
1161
- }
1162
-
1163
-
1164
- def _dispatch_wiki_cli(args: list[str]) -> None:
1165
- """Dispatch `forge wiki <subcommand>` from CLI (non-interactive)."""
1166
- sub = args[0] if args else "status"
1167
- if sub == "status":
1168
- wiki_status()
1169
- elif sub == "ingest":
1170
- source_flag = next((a for a in args[1:] if not a.startswith("--")), None)
1171
- if not source_flag:
1172
- # look for --source <path>
1173
- for i, a in enumerate(args[1:], 1):
1174
- if a == "--source" and i + 1 < len(args):
1175
- source_flag = args[i + 1]
1176
- break
1177
- if a.startswith("--source="):
1178
- source_flag = a.split("=", 1)[1]
1179
- break
1180
- wiki_ingest(source_flag)
1181
- elif sub == "query":
1182
- question = " ".join(a for a in args[1:] if not a.startswith("--")) or None
1183
- wiki_query(question)
1184
- elif sub == "lint":
1185
- wiki_lint()
1186
- else:
1187
- print(f"forge wiki: subcomando desconocido '{sub}'. Usa: status | ingest | query | lint", file=sys.stderr)
1188
- sys.exit(1)
1189
-
1190
-
1191
- def main() -> None:
1192
- if "--help" in sys.argv or "-h" in sys.argv:
1193
- print(textwrap.dedent(f"""\
1194
- forge v{VERSION} — Framework de desarrollo con agentes IA
1195
-
1196
- Uso:
1197
- python3 .agentic/forge.py Abre el CLI interactivo
1198
- python3 .agentic/forge.py --help Muestra esta ayuda
1199
- python3 .agentic/forge.py --batch Muestra scripts para CI/no-interactivo
1200
-
1201
- Subcomandos:
1202
- python3 .agentic/forge.py wiki status Estado del wiki
1203
- python3 .agentic/forge.py wiki ingest [--source <ruta>] Ingestar fuente
1204
- python3 .agentic/forge.py wiki query "<pregunta>" Consultar el wiki
1205
- python3 .agentic/forge.py wiki lint Health check del wiki
1206
-
1207
- Scripts disponibles directamente:
1208
- scripts/forge-wizard.py Wizard de nuevo proyecto
1209
- scripts/forge-init.py Instala agentes
1210
- scripts/forge-audit.py Audita el proyecto
1211
- scripts/forge-scaffold-profile.py Crea un profile Tier 2
1212
- scripts/forge-teardown.py Revierte la instalación
1213
- scripts/aitmpl-search.py Busca templates y recursos de agentes IA
1214
- """))
1215
- return
1216
-
1217
- if "--batch" in sys.argv:
1218
- print(textwrap.dedent(f"""\
1219
- forge v{VERSION} — modo no-interactivo
1220
-
1221
- Usa los scripts directamente:
1222
- python3 .agentic/scripts/forge-wizard.py --mode=startup Crear project.yaml
1223
- python3 .agentic/scripts/forge-init.py --tool=claude-code Instalar agentes
1224
- python3 .agentic/scripts/forge-audit.py --json Auditar (JSON para CI)
1225
- python3 .agentic/scripts/forge-audit.py --json | jq '.summary.errors'
1226
- python3 .agentic/scripts/forge-teardown.py --confirm Teardown
1227
-
1228
- Wiki (no-interactivo):
1229
- python3 .agentic/forge.py wiki status
1230
- python3 .agentic/forge.py wiki ingest --source <ruta>
1231
- python3 .agentic/forge.py wiki query "<pregunta>"
1232
- python3 .agentic/forge.py wiki lint
1233
- """))
1234
- return
1235
-
1236
- # Subcomando wiki — funciona en modo non-TTY también
1237
- if len(sys.argv) > 1 and sys.argv[1] == "wiki":
1238
- _dispatch_wiki_cli(sys.argv[2:])
1239
- return
1240
-
1241
- if not IS_TTY:
1242
- print("forge: terminal interactivo requerido. Usar --help para ver opciones.", file=sys.stderr)
1243
- print("Tip: usa --batch para ver los scripts equivalentes para CI.", file=sys.stderr)
1244
- sys.exit(1)
1245
-
1246
- import shutil as _shutil
1247
- cols = _shutil.get_terminal_size(fallback=(80, 24)).columns
1248
- if cols < 58:
1249
- print(f"forge: terminal demasiado estrecha ({cols} cols, mínimo 58).", file=sys.stderr)
1250
- print(f" Amplía la ventana o usa --batch para ver scripts directos.", file=sys.stderr)
1251
- sys.exit(1)
1252
-
1253
- while True:
1254
- key = show_menu("¿Qué quieres hacer?", MAIN_ITEMS)
1255
- if key is None or key == "quit":
1256
- clr()
1257
- print(f"\n {d('forge — hasta luego.')}\n")
1258
- break
1259
- action = ACTIONS.get(key)
1260
- if action:
1261
- action()
1262
-
1263
-
1264
- if __name__ == "__main__":
1265
- main()