@saulwade/swl-ses 1.5.2 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -1,4 +1,4 @@
1
- # CLAUDE.md — @saulwade/swl-ses v1.5.2
1
+ # CLAUDE.md — @saulwade/swl-ses v1.6.0
2
2
 
3
3
  ## Reglas de máxima prioridad (aplican SIEMPRE, sin excepción)
4
4
 
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # swl-ses v1.5.2
1
+ # swl-ses v1.6.0
2
2
 
3
3
  > El paquete anterior `@saulwadeleon/swl-software-engineering-system` está deprecado. Migrar a `@saulwade/swl-ses` (npmjs.org canónico) o `@saul-wade/swl-ses` (mirror en GitHub Packages) — el CLI `swl-ses` no cambia.
4
4
 
@@ -6,7 +6,7 @@ Sistema de ingeniería de software auto-evolutivo **multi-runtime** con agentes
6
6
 
7
7
  Soporta 7 runtimes de IA: Claude Code, OpenClaude, OpenCode y Gemini CLI (soporte completo); Cursor, Codex CLI y GitHub Copilot (soporte parcial — reglas + MCP server o consolidación en archivo de instrucciones según el runtime). Incluye sistema de transformadores que adapta el formato canónico SWL al formato nativo de cada runtime, **multi-target install** (`--target=claude,cursor,codex` en una sola invocación), y **`swl-mcp-server` v1.0.0** con auth opt-in para que Cursor, Codex y otros clientes MCP consulten la memoria SWL (aprendizajes, instintos, sesiones).
8
8
 
9
- Cubre el SDLC completo: discovery, requisitos, arquitectura, UX/UI, frontend, backend, mobile, datos, testing, seguridad, CI/CD, observabilidad, releases, documentación, notificaciones y auto-evolución. Incluye sistema de notificaciones Telegram opt-in (hook saliente, bot bidireccional con 15 comandos, autostart cross-platform) y **auditoría profunda Nemesis** (loop iterativo Feynman + State Inconsistency hasta convergencia) con 8 tools ejecutables JSON-output para code-profiler, pentest-scanner, dep-doctor, bundle-tracker y más (ADR-0018, v1.4.1).
9
+ Cubre el SDLC completo: discovery, requisitos, arquitectura, UX/UI, frontend, backend, mobile, datos, testing, seguridad, CI/CD, observabilidad, releases, documentación, notificaciones y auto-evolución. Incluye sistema de notificaciones Telegram opt-in (hook saliente, bot bidireccional con 15 comandos, autostart cross-platform), **auditoría profunda Nemesis** (loop iterativo Feynman + State Inconsistency hasta convergencia, ahora con loop evaluator-optimizer opt-in vía `/swl:nemesis --remediar` desde v1.5.2 - ADR-0021) con 8 tools ejecutables JSON-output para code-profiler, pentest-scanner, dep-doctor, bundle-tracker y más (ADR-0018, v1.4.1), e **instalador/actualizador TUI custom** zero-deps con paneles, multi-select y barra de progreso por categoría (v1.6.0).
10
10
 
11
11
  ## Inventario
12
12
 
@@ -56,6 +56,23 @@ El setup requiere **dos comandos en orden**, con propósitos distintos:
56
56
  > y los hace disponibles en todos tus proyectos. Aun así, cada proyecto necesita su propio `init`
57
57
  > para obtener `.planning/` y `_userland/`.
58
58
 
59
+ ### Modo recomendado: TUI visual (v1.6.0+)
60
+
61
+ Desde v1.6.0, al ejecutar `install` o `update` sin flags desde una terminal
62
+ interactiva, swl-ses lanza un **TUI custom** con paneles, selectores con
63
+ flechas, multi-select con espacio y barra de progreso por categoría.
64
+
65
+ ```bash
66
+ # Lanza el TUI: Welcome → Menú → Wizard → Progreso → Resumen
67
+ npx -y @saulwade/swl-ses@latest install
68
+ npx -y @saulwade/swl-ses@latest update
69
+ ```
70
+
71
+ Opt-out con `--no-tui` para usar el asistido lineal clásico, o pasa cualquier
72
+ flag (`--target`, `--profile`, `--force`, etc.) y el CLI usa el flujo directo
73
+ sin prompts. Ver [`MANUAL_USO.md`](./MANUAL_USO.md) sección "Opción C — Modo
74
+ TUI visual" para capturas ASCII de cada pantalla.
75
+
59
76
  ### Opción 1: CLI vía npmjs (recomendada)
60
77
 
61
78
  ```bash
package/bin/swl-ses.js CHANGED
@@ -190,7 +190,7 @@ GESTIÓN DE COMPONENTES:
190
190
  agents remove <nombre> Remueve un agente individual
191
191
 
192
192
  OPCIONES DE INSTALL:
193
- --target <runtime> Runtime destino: claude|openclaude|copilot|opencode|codex|gemini (default: claude)
193
+ --target <runtime> Runtime destino: claude|openclaude|copilot|opencode|codex|gemini|cursor (default: claude)
194
194
  --profile <perfil> Perfil: core|backend-python|backend-node|frontend-react|frontend-angular|
195
195
  fullstack-python-angular|fullstack-node-react|mobile|devops|completo (default: core)
196
196
  --with <componentes> Incluir componentes adicionales (separados por coma)
@@ -200,10 +200,19 @@ OPCIONES DE INSTALL:
200
200
  --dry-run Mostrar plan sin aplicar cambios
201
201
  --force Sobreescribir archivos existentes sin confirmar
202
202
  --all-langs Instalar reglas de todos los lenguajes (omite detección automática de stack)
203
+ --with-mcp Configura el MCP server swl-memory automáticamente (v1.5.0+)
204
+ --tui Fuerza el modo TUI visual aunque pases otros flags (v1.6.0+)
205
+ --no-tui Desactiva el TUI; usa el asistido lineal clásico (v1.6.0+)
206
+ --verbose Expande el panel de log del Progreso del TUI de 8 a 24 líneas (v1.6.0+)
203
207
  --no-claudemd No modificar el CLAUDE.md del proyecto (solo aplica con --target claude).
204
208
  Por defecto el instalador crea o actualiza un bloque delimitado por
205
209
  <!-- SWL-BEGIN vX.Y.Z --> / <!-- SWL-END --> preservando el resto del archivo.
206
210
 
211
+ MODOS DE INVOCACIÓN (v1.6.0+):
212
+ TTY + sin flags → TUI visual completo (recomendado)
213
+ TTY + --no-tui → Asistido lineal clásico (preguntas en serie)
214
+ No-TTY o con flags → Modo directo sin prompts (CI/scripts)
215
+
207
216
  OPCIONES DE SKILLS/AGENTS:
208
217
  --skill <nombre> Nombre del skill a extraer del repo (con skills add)
209
218
  --agent <nombre> Nombre del agente a extraer del repo (con agents add)
@@ -215,11 +224,14 @@ OPCIONES GENERALES:
215
224
 
216
225
  EJEMPLOS:
217
226
  ${NOMBRE} init
218
- ${NOMBRE} install (modo asistido — recomendado para primera vez)
227
+ ${NOMBRE} install (TUI visual — recomendado para primera vez)
228
+ ${NOMBRE} install --no-tui (asistido lineal clásico)
219
229
  ${NOMBRE} install --target claude --profile backend-python
220
230
  ${NOMBRE} install --target openclaude --profile fullstack-python-angular
221
231
  ${NOMBRE} install --target gemini --profile core
222
232
  ${NOMBRE} install --target copilot --profile fullstack-node-react
233
+ ${NOMBRE} update (TUI visual con multi-select de runtimes)
234
+ ${NOMBRE} update --no-tui (preguntas lineales clásicas)
223
235
  ${NOMBRE} skills list --target claude
224
236
  ${NOMBRE} skills add https://github.com/user/repo --skill mi-skill
225
237
  ${NOMBRE} agents add ./path/local --agent mi-agente
@@ -312,14 +324,44 @@ function main() {
312
324
 
313
325
  const opciones = parsearOpciones(args.slice(1));
314
326
 
315
- // Modo asistido: `install` sin flags → flujo interactivo
316
- // (Si el usuario pasa cualquier flag relevante, se asume que sabe lo que hace.)
317
- if (comando === 'install') {
318
- const flagsIrrelevantesParaAsistido = ['verbose'];
327
+ // Modo asistido / TUI: `install` o `update` sin flags relevantes modo visual.
328
+ //
329
+ // Niveles de fallback:
330
+ // 1. TTY + sin flags (excepto --tui): lanzar TUI completo (scripts/tui/index.js)
331
+ // 2. TTY + --no-tui: lanzar install-asistido clásico (preguntarOpcion lineal)
332
+ // 3. No TTY (CI/pipe): flujo clásico con flags por defecto sin asistido
333
+ //
334
+ // El usuario puede forzar el TUI incluso con flags pasando --tui.
335
+ if (comando === 'install' || comando === 'update') {
336
+ const flagsIrrelevantesParaAsistido = ['verbose', 'tui'];
319
337
  const flagsEspecificados = Object.keys(opciones).filter(
320
338
  k => k !== '_args' && !flagsIrrelevantesParaAsistido.includes(k)
321
339
  );
322
- if (flagsEspecificados.length === 0) {
340
+ const stdinTty = !!process.stdin.isTTY;
341
+ const optoutTui = opciones['no-tui'] || opciones.no_tui;
342
+ const forzarTui = opciones.tui;
343
+ const sinFlags = flagsEspecificados.length === 0;
344
+
345
+ if ((sinFlags || forzarTui) && stdinTty && !optoutTui) {
346
+ try {
347
+ const { iniciarTui } = require('../scripts/tui');
348
+ iniciarTui({ operacionInicial: comando }).then(() => {
349
+ process.exit(0);
350
+ }).catch(err => {
351
+ console.error(`Error en TUI: ${err.message}`);
352
+ if (opciones.verbose) console.error(err.stack);
353
+ process.exit(1);
354
+ });
355
+ return;
356
+ } catch (err) {
357
+ console.error(`Error cargando TUI: ${err.message}`);
358
+ if (opciones.verbose) console.error(err.stack);
359
+ // Caer al asistido clásico como fallback
360
+ }
361
+ }
362
+
363
+ if (comando === 'install' && sinFlags && !forzarTui) {
364
+ // Fallback al asistido lineal (sin TUI) — preserva la UX de versiones previas
323
365
  try {
324
366
  const asistido = require('../scripts/comandos/install-asistido');
325
367
  asistido.main().catch(err => {
@@ -618,6 +618,17 @@ const PATRONES_ARCHIVO_SWL_EXCLUIDO = [
618
618
  /(?:^|[\\/])plantillas[\\/]/,
619
619
  /(?:^|[\\/])contextos[\\/]/,
620
620
  /(?:^|[\\/])instintos[\\/]/,
621
+ // Tests (cualquier convención): los comentarios JSDoc/docstring de un test
622
+ // describen el SUT y suelen contener palabras como "bug", "patrón", "fix"
623
+ // de forma narrativa-descriptiva, no como descubrimiento. Detectado cuando
624
+ // el hook capturó comentarios del propio test que valida un fix y los
625
+ // promovió a APRENDIZAJES.md como entradas truncadas.
626
+ /(?:^|[\\/])tests?[\\/]/,
627
+ /(?:^|[\\/])__tests__[\\/]/,
628
+ /(?:^|[\\/])spec[\\/]/,
629
+ /\.(?:test|spec)\.(?:js|ts|jsx|tsx|mjs|cjs)$/i,
630
+ /(?:^|[\\/])test_.*\.py$/,
631
+ /_test\.(?:py|go)$/i,
621
632
  // Todo .planning/ salvo wiki/ (que puede contener conocimiento del proyecto usuario).
622
633
  // En swl-ses .planning/ es meta del sistema; los aprendizajes se gestionan manualmente.
623
634
  /(?:^|[\\/])\.planning[\\/](?!wiki[\\/])/,
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "lockfileVersion": 1,
3
- "generatedAt": "2026-05-15T04:05:25.970Z",
4
- "skillsCount": 158,
5
- "lockHash": "sha256:e8aa8826b7a7285c7a0f182973b7565db338c7bb6a13707b6c518b2ac980b096",
3
+ "generatedAt": "2026-05-16T18:45:23.117Z",
4
+ "skillsCount": 162,
5
+ "lockHash": "sha256:20e7bde29e6c2ca22f9fa77c436ebc2a9085d6f45c0378478ba2d7717f00875c",
6
6
  "skills": [
7
7
  {
8
8
  "nombre": "accesibilidad-a11y",
@@ -343,9 +343,9 @@
343
343
  {
344
344
  "nombre": "discutir-fase",
345
345
  "path": "habilidades/discutir-fase/SKILL.md",
346
- "hash": "sha256:63963663eebf79a3966660e60ebdf797fccae4130faf4cae667d8b2e40cfd826",
347
- "bytes": 8370,
348
- "version": "\"1.0.0\""
346
+ "hash": "sha256:c3e77c9a40ee4fcec31d26a0be8e34b915c6ce1d704b02900456328669f4fad5",
347
+ "bytes": 11591,
348
+ "version": "\"1.1.0\""
349
349
  },
350
350
  {
351
351
  "nombre": "diseno-herramientas-agente",
@@ -396,6 +396,13 @@
396
396
  "bytes": 19364,
397
397
  "version": "\"1.1.0\""
398
398
  },
399
+ {
400
+ "nombre": "ejecutar-task-iterativo",
401
+ "path": "habilidades/ejecutar-task-iterativo/SKILL.md",
402
+ "hash": "sha256:ca8780fd72ad10dd21085c6cbbbd3baa0e625c0ba0834e1c3ec8e2b5934b88ba",
403
+ "bytes": 13131,
404
+ "version": "\"1.0.0\""
405
+ },
399
406
  {
400
407
  "nombre": "estilo-sin-ai-isms",
401
408
  "path": "habilidades/estilo-sin-ai-isms/SKILL.md",
@@ -651,9 +658,9 @@
651
658
  {
652
659
  "nombre": "meta-skills-estandar",
653
660
  "path": "habilidades/meta-skills-estandar/SKILL.md",
654
- "hash": "sha256:6c2861defb5f5c46c1ac851ecf2ec06958be714fd14d3f7f512ba916b43e3b7b",
655
- "bytes": 13101,
656
- "version": "\"1.0.0\""
661
+ "hash": "sha256:4908de280db0d143643fc96ae748519edc2142aa9ef35c415cd84074c274d613",
662
+ "bytes": 23181,
663
+ "version": "\"1.1.1\""
657
664
  },
658
665
  {
659
666
  "nombre": "microservicios",
@@ -690,6 +697,20 @@
690
697
  "bytes": 11807,
691
698
  "version": "\"1.0.1\""
692
699
  },
700
+ {
701
+ "nombre": "nemesis-evaluacion-json",
702
+ "path": "habilidades/nemesis-evaluacion-json/SKILL.md",
703
+ "hash": "sha256:e8c84252b6d6080101486b5acc021af14c9eeab6d25b71a0fc0f450177d5782d",
704
+ "bytes": 10610,
705
+ "version": null
706
+ },
707
+ {
708
+ "nombre": "nemesis-redistribuir",
709
+ "path": "habilidades/nemesis-redistribuir/SKILL.md",
710
+ "hash": "sha256:171919fe921f411b065644ab09a93bd32c68fa74f43669d83522ad237e4b0590",
711
+ "bytes": 13610,
712
+ "version": null
713
+ },
693
714
  {
694
715
  "nombre": "nestjs-experto",
695
716
  "path": "habilidades/nestjs-experto/SKILL.md",
@@ -721,9 +742,9 @@
721
742
  {
722
743
  "nombre": "node-experto",
723
744
  "path": "habilidades/node-experto/SKILL.md",
724
- "hash": "sha256:8002768234ffb90d057d2d626488015c68bc969556e026b8c66542c681f3b8e6",
725
- "bytes": 13538,
726
- "version": "\"1.0.1\""
745
+ "hash": "sha256:bf8ea36fadabf2b7a30590a2a1658ca39ddb15b4285c709db4379c631259f2b2",
746
+ "bytes": 19290,
747
+ "version": "\"1.0.2\""
727
748
  },
728
749
  {
729
750
  "nombre": "notificaciones-multicanal",
@@ -837,6 +858,13 @@
837
858
  "bytes": 21302,
838
859
  "version": "\"1.1.0\""
839
860
  },
861
+ {
862
+ "nombre": "protocolo-revision-swl",
863
+ "path": "habilidades/protocolo-revision-swl/SKILL.md",
864
+ "hash": "sha256:5ebbe37b828d70f7e5190f49116fd56e75280cc9a39e5c53302064e211437215",
865
+ "bytes": 15867,
866
+ "version": "\"1.0.1\""
867
+ },
840
868
  {
841
869
  "nombre": "rag-arquitectura",
842
870
  "path": "habilidades/rag-arquitectura/SKILL.md",
@@ -1001,9 +1029,9 @@
1001
1029
  {
1002
1030
  "nombre": "tdd-workflow",
1003
1031
  "path": "habilidades/tdd-workflow/SKILL.md",
1004
- "hash": "sha256:5f63f554a6a54e1a3b9d47f2497ae12176eb955418d2b0b138a168475dc1d820",
1005
- "bytes": 17424,
1006
- "version": "\"1.0.2\""
1032
+ "hash": "sha256:5bc34fef3315efa871c56210fc1942f430e1b5fb61dc54ab23891c352555e638",
1033
+ "bytes": 23870,
1034
+ "version": "\"1.0.4\""
1007
1035
  },
1008
1036
  {
1009
1037
  "nombre": "terraform-experto",
@@ -1078,9 +1106,9 @@
1078
1106
  {
1079
1107
  "nombre": "verificar-trabajo",
1080
1108
  "path": "habilidades/verificar-trabajo/SKILL.md",
1081
- "hash": "sha256:001fd34fbeefbe995c4b76fb55fa1a5277577f701b098998f189fd7d0c35e40e",
1082
- "bytes": 14619,
1083
- "version": "\"1.1.1\""
1109
+ "hash": "sha256:4aeab56aee56b7b481d8e2c59a4da505ad5bad6abff22e83e4e116c58fa1bbbf",
1110
+ "bytes": 21346,
1111
+ "version": "\"1.2.1\""
1084
1112
  },
1085
1113
  {
1086
1114
  "nombre": "web-fetcher-routing",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saulwade/swl-ses",
3
- "version": "1.5.2",
3
+ "version": "1.6.0",
4
4
  "description": "Sistema de ingenieria de software auto-evolutivo multi-runtime polyglot con 60 agentes, 162 habilidades, 44 comandos, 66 reglas y 41 hooks. Soporta 11 lenguajes y 7 runtimes: Claude Code, OpenClaude, OpenCode, Gemini CLI, Cursor, Codex CLI (soporte completo); GitHub Copilot (soporte parcial). 100% en espanol (Mexico). Multi-target install (--target CSV / --all-runtimes), autoconfig MCP en Cursor/Codex con --with-mcp, agentes Codex en TOML, hooks Cursor (17 eventos) y Codex (6 eventos). Gateway bidireccional con relay Telegram y auditoria profunda Nemesis con loop evaluator-optimizer opt-in (ADR-0021) y 8 tools ejecutables.",
5
5
  "bin": {
6
6
  "swl-ses": "bin/swl-ses.js",
@@ -28,12 +28,13 @@
28
28
  ],
29
29
  "scripts": {
30
30
  "postinstall": "echo '\n swl-software-engineering-system instalado.\n Ejecuta: npx swl-ses init\n'",
31
- "test": "node --test tests/lib/*.test.js tests/scripts/*.test.js tests/scripts/lib/*.test.js tests/hooks/*.test.js tests/gateway/*.test.js tests/bin/*.test.js tests/transformadores/*.test.js tests/mcp-server/*.test.js",
31
+ "test": "node --test tests/lib/*.test.js tests/scripts/*.test.js tests/scripts/lib/*.test.js tests/scripts/tui/*.test.js tests/hooks/*.test.js tests/gateway/*.test.js tests/bin/*.test.js tests/transformadores/*.test.js tests/mcp-server/*.test.js",
32
32
  "test:validate": "node scripts/validar.js",
33
33
  "test:manifest": "node scripts/validar-manifest.js",
34
+ "test:docs": "node scripts/verificar-docs-vs-codigo.js",
34
35
  "test:smoke": "node scripts/smoke-test.js",
35
36
  "test:aislamiento": "node scripts/validar-tests-aislamiento.js",
36
- "test:all": "npm test && node scripts/validar.js && node scripts/validar-manifest.js",
37
+ "test:all": "npm test && node scripts/validar.js && node scripts/validar-manifest.js && node scripts/verificar-docs-vs-codigo.js",
37
38
  "test:userland": "node scripts/validar-userland-vacio.js",
38
39
  "test:release": "npm run test:all && npm run test:userland && npm run test:smoke",
39
40
  "doctor": "node scripts/doctor.js",
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swl-ses",
3
- "version": "1.5.2",
3
+ "version": "1.6.0",
4
4
  "description": "Sistema de ingenieria de software auto-evolutivo multi-runtime polyglot. 60 agentes, 162 habilidades, 44 comandos, 66 reglas y 41 hooks. 62 librerias. 11 lenguajes. Soporta Claude Code, Copilot, OpenCode, Codex y Gemini CLI. Loop evaluator-optimizer en /swl:nemesis (ADR-0021).",
5
5
  "author": "Saul Wade Leon",
6
6
  "license": "MIT",
@@ -62,6 +62,7 @@
62
62
  "habilidades/doubt-driven-review",
63
63
  "habilidades/drift-detection",
64
64
  "habilidades/ejecutar-fase",
65
+ "habilidades/ejecutar-task-iterativo",
65
66
  "habilidades/estilo-sin-ai-isms",
66
67
  "habilidades/estructura-proyecto-claude",
67
68
  "habilidades/eval-framework",
@@ -127,6 +128,7 @@
127
128
  "habilidades/prevencion-sobreingenieria",
128
129
  "habilidades/privacy-memoria",
129
130
  "habilidades/prompt-engineering",
131
+ "habilidades/protocolo-revision-swl",
130
132
  "habilidades/rag-arquitectura",
131
133
  "habilidades/rails-experto",
132
134
  "habilidades/react-experto",
@@ -3,6 +3,22 @@
3
3
  /**
4
4
  * swl-ses uninstall
5
5
  * Desinstala componentes SWL del runtime especificado.
6
+ *
7
+ * Contrato adicional: opciones.onProgress(evento)
8
+ *
9
+ * Si se pasa una función onProgress, el desinstalador emite eventos
10
+ * estructurados en vez de imprimir cada archivo a stdout:
11
+ * { tipo: 'archivo-eliminado', componente, archivo, destino }
12
+ * { tipo: 'bloque-eliminado', archivo, etiqueta }
13
+ * { tipo: 'archivo-no-encontrado', archivo }
14
+ * { tipo: 'error', archivo, mensaje }
15
+ * { tipo: 'log', linea }
16
+ *
17
+ * Backward compat: si onProgress no se pasa, los console.log son idénticos
18
+ * a antes. El CLI clásico (uninstall sin TUI) no cambia.
19
+ *
20
+ * Devuelve { eliminados, noEncontrados, bloquesEliminados } para que el
21
+ * caller (TUI) pueda mostrar resumen sin re-parsear stdout.
6
22
  */
7
23
 
8
24
  const fs = require('fs');
@@ -15,23 +31,40 @@ const { eliminarBloque } = require('./lib/append-con-marcadores');
15
31
  function uninstall(opciones) {
16
32
  const target = opciones.target || 'claude';
17
33
  const esGlobal = opciones.global || false;
34
+ const onProgress = typeof opciones.onProgress === 'function' ? opciones.onProgress : null;
18
35
 
19
- console.log('swl-ses uninstall');
20
- console.log('='.repeat(40));
36
+ function _emitir(evento, mensajeFallback) {
37
+ if (onProgress) {
38
+ onProgress(evento);
39
+ } else if (mensajeFallback) {
40
+ console.log(mensajeFallback);
41
+ }
42
+ }
43
+
44
+ if (!onProgress) {
45
+ console.log('swl-ses uninstall');
46
+ console.log('='.repeat(40));
47
+ }
21
48
 
22
49
  const runtime = obtenerRuntime(target);
23
50
  const rutas = calcularRutas(target, { global: esGlobal });
24
51
 
25
52
  const estado = cargarEstado(rutas.base);
26
53
  if (!estado) {
54
+ if (onProgress) {
55
+ onProgress({ tipo: 'log', linea: `No se encontró instalación SWL en ${rutas.base}` });
56
+ return { eliminados: 0, noEncontrados: 0, bloquesEliminados: 0, error: 'instalacion-no-encontrada' };
57
+ }
27
58
  console.log(`No se encontró instalación SWL en ${rutas.base}`);
28
59
  process.exit(1);
29
60
  }
30
61
 
31
- console.log(`Target: ${target} (${runtime.nombre})`);
32
- console.log(`Ubicación: ${rutas.base}`);
33
- console.log(`Archivos a eliminar: ${estado.archivosInstalados.length}`);
34
- console.log('');
62
+ if (!onProgress) {
63
+ console.log(`Target: ${target} (${runtime.nombre})`);
64
+ console.log(`Ubicación: ${rutas.base}`);
65
+ console.log(`Archivos a eliminar: ${estado.archivosInstalados.length}`);
66
+ console.log('');
67
+ }
35
68
 
36
69
  let eliminados = 0;
37
70
  let noEncontrados = 0;
@@ -56,10 +89,17 @@ function uninstall(opciones) {
56
89
  if (accionInstall === 'creado') {
57
90
  try {
58
91
  fs.unlinkSync(archivo.destino);
59
- console.log(` - ${archivo.destino} (creado por swl-ses, eliminación completa)`);
92
+ _emitir(
93
+ { tipo: 'archivo-eliminado', componente: archivo.tipo, archivo: archivo.destino, motivo: 'creado-por-swl' },
94
+ ` - ${archivo.destino} (creado por swl-ses, eliminación completa)`
95
+ );
60
96
  eliminados++;
61
97
  } catch (err) {
62
- console.error(` ! Error eliminando ${archivo.destino}: ${err.message}`);
98
+ _emitir(
99
+ { tipo: 'error', archivo: archivo.destino, mensaje: err.message },
100
+ null
101
+ );
102
+ if (!onProgress) console.error(` ! Error eliminando ${archivo.destino}: ${err.message}`);
63
103
  }
64
104
  } else {
65
105
  try {
@@ -70,12 +110,19 @@ function uninstall(opciones) {
70
110
  'sin-cambios': 'sin bloque SWL presente (ya limpio)',
71
111
  'error': `error: ${r.detalle || 'desconocido'}`,
72
112
  }[r.accion] || r.accion;
73
- console.log(` ~ ${archivo.destino} (${etiqueta})`);
113
+ _emitir(
114
+ { tipo: 'bloque-eliminado', archivo: archivo.destino, etiqueta, accion: r.accion },
115
+ ` ~ ${archivo.destino} (${etiqueta})`
116
+ );
74
117
  if (r.accion === 'bloque-eliminado' || r.accion === 'archivo-eliminado') {
75
118
  bloquesEliminados++;
76
119
  }
77
120
  } catch (err) {
78
- console.error(` ! Error limpiando bloque en ${archivo.destino}: ${err.message}`);
121
+ _emitir(
122
+ { tipo: 'error', archivo: archivo.destino, mensaje: err.message },
123
+ null
124
+ );
125
+ if (!onProgress) console.error(` ! Error limpiando bloque en ${archivo.destino}: ${err.message}`);
79
126
  }
80
127
  }
81
128
  continue;
@@ -88,26 +135,47 @@ function uninstall(opciones) {
88
135
  } else {
89
136
  fs.unlinkSync(archivo.destino);
90
137
  }
91
- console.log(` - ${archivo.destino}`);
138
+ _emitir(
139
+ { tipo: 'archivo-eliminado', componente: archivo.tipo, archivo: archivo.destino },
140
+ ` - ${archivo.destino}`
141
+ );
92
142
  eliminados++;
93
143
  } catch (err) {
94
- console.error(` ! Error eliminando ${archivo.destino}: ${err.message}`);
144
+ _emitir(
145
+ { tipo: 'error', archivo: archivo.destino, mensaje: err.message },
146
+ null
147
+ );
148
+ if (!onProgress) console.error(` ! Error eliminando ${archivo.destino}: ${err.message}`);
95
149
  }
96
150
  }
97
151
 
98
152
  // Desregistrar hooks de settings.json
153
+ let hooksDesregistrados = 0;
154
+ let hooksPreservados = 0;
99
155
  if (runtime.soportaHooks) {
100
156
  const settingsPath = rutaSettings(runtime, rutas.base);
101
157
  try {
102
158
  const resultado = desregistrarHooks(settingsPath);
103
- if (resultado.eliminados > 0) {
104
- console.log(` - Hooks desregistrados de settings.json: ${resultado.eliminados}`);
159
+ hooksDesregistrados = resultado.eliminados || 0;
160
+ hooksPreservados = resultado.preservados || 0;
161
+ if (hooksDesregistrados > 0) {
162
+ _emitir(
163
+ { tipo: 'hooks-desregistrados', cuenta: hooksDesregistrados },
164
+ ` - Hooks desregistrados de settings.json: ${hooksDesregistrados}`
165
+ );
105
166
  }
106
- if (resultado.preservados > 0) {
107
- console.log(` (${resultado.preservados} hooks externos preservados)`);
167
+ if (hooksPreservados > 0) {
168
+ _emitir(
169
+ { tipo: 'hooks-preservados', cuenta: hooksPreservados },
170
+ ` (${hooksPreservados} hooks externos preservados)`
171
+ );
108
172
  }
109
173
  } catch (err) {
110
- console.error(` ! Error desregistrando hooks: ${err.message}`);
174
+ _emitir(
175
+ { tipo: 'error', mensaje: `Error desregistrando hooks: ${err.message}` },
176
+ null
177
+ );
178
+ if (!onProgress) console.error(` ! Error desregistrando hooks: ${err.message}`);
111
179
  }
112
180
  }
113
181
 
@@ -117,14 +185,27 @@ function uninstall(opciones) {
117
185
  fs.unlinkSync(estadoPath);
118
186
  }
119
187
 
120
- console.log('');
121
- console.log(`Eliminados: ${eliminados}`);
122
- if (bloquesEliminados > 0) {
123
- console.log(`Bloques SWL quitados (contenido de usuario preservado): ${bloquesEliminados}`);
188
+ if (!onProgress) {
189
+ console.log('');
190
+ console.log(`Eliminados: ${eliminados}`);
191
+ if (bloquesEliminados > 0) {
192
+ console.log(`Bloques SWL quitados (contenido de usuario preservado): ${bloquesEliminados}`);
193
+ }
194
+ console.log(`No encontrados: ${noEncontrados}`);
195
+ console.log('');
196
+ console.log('Nota: .planning/ y _userland/ no se eliminan (contienen datos del proyecto).');
124
197
  }
125
- console.log(`No encontrados: ${noEncontrados}`);
126
- console.log('');
127
- console.log('Nota: .planning/ y _userland/ no se eliminan (contienen datos del proyecto).');
198
+
199
+ return {
200
+ eliminados,
201
+ noEncontrados,
202
+ bloquesEliminados,
203
+ hooksDesregistrados,
204
+ hooksPreservados,
205
+ target,
206
+ scope: esGlobal ? 'global' : 'proyecto',
207
+ ubicacion: rutas.base,
208
+ };
128
209
  }
129
210
 
130
211
  module.exports = uninstall;
@@ -42,6 +42,24 @@ const { actualizarGitignore, entradasParaRuntime, limpiarTracked, leerManifest,
42
42
  const RAIZ_PKG = path.resolve(__dirname, '..');
43
43
  const VERSION = require('../package.json').version;
44
44
 
45
+ /**
46
+ * Contrato adicional: `opciones.onProgress(evento)`
47
+ *
48
+ * Si se pasa una función `opciones.onProgress`, el instalador deja de imprimir
49
+ * cada archivo copiado/backup/symlink a stdout y en su lugar emite eventos
50
+ * estructurados que el caller (típicamente la TUI) puede consumir para mostrar
51
+ * barra de progreso + contadores por categoría.
52
+ *
53
+ * Eventos emitidos:
54
+ * { tipo: 'archivo-copiado', componente: 'agentes'|'habilidades'|... , archivo: 'nombre.md' }
55
+ * { tipo: 'backup', componente: <tipo>, archivo, rutaBackup }
56
+ * { tipo: 'symlink', componente: <tipo>, archivo }
57
+ * { tipo: 'colision', componente: <tipo>, archivo }
58
+ * { tipo: 'log', linea: 'mensaje libre' } — para mensajes informativos
59
+ *
60
+ * Backward compat: si onProgress NO se pasa, los console.log siguen iguales.
61
+ * El comportamiento sin TUI no cambia.
62
+ */
45
63
  async function install(opciones) {
46
64
  // Leer manifest .swl-ses si existe — sus valores son defaults que el CLI puede sobreescribir
47
65
  const manifest = leerManifest(process.cwd());
@@ -990,7 +1008,15 @@ function instalarArchivo(archivo, rutas, runtime, opciones = {}) {
990
1008
 
991
1009
  // Verificar colisión
992
1010
  if (fs.existsSync(destino) && !opciones.force) {
993
- console.log(` - Colisión: ${nombreArchivo} (usa --force para sobreescribir)`);
1011
+ if (typeof opciones.onProgress === 'function') {
1012
+ opciones.onProgress({
1013
+ tipo: 'colision',
1014
+ componente: archivo.tipo,
1015
+ archivo: nombreArchivo,
1016
+ });
1017
+ } else {
1018
+ console.log(` - Colisión: ${nombreArchivo} (usa --force para sobreescribir)`);
1019
+ }
994
1020
  return { instalado: false, colision: true };
995
1021
  }
996
1022
 
@@ -998,7 +1024,16 @@ function instalarArchivo(archivo, rutas, runtime, opciones = {}) {
998
1024
  if (fs.existsSync(destino) && opciones.force && opciones.versionAnterior) {
999
1025
  const backup = crearBackup(process.cwd(), destino, opciones.versionAnterior);
1000
1026
  if (backup.respaldado) {
1001
- console.log(` ↩ Backup: ${nombreArchivo} ${path.relative(process.cwd(), backup.rutaBackup)}`);
1027
+ if (typeof opciones.onProgress === 'function') {
1028
+ opciones.onProgress({
1029
+ tipo: 'backup',
1030
+ componente: archivo.tipo,
1031
+ archivo: nombreArchivo,
1032
+ rutaBackup: backup.rutaBackup,
1033
+ });
1034
+ } else {
1035
+ console.log(` ↩ Backup: ${nombreArchivo} → ${path.relative(process.cwd(), backup.rutaBackup)}`);
1036
+ }
1002
1037
  if (opciones.estado) {
1003
1038
  registrarBackup(opciones.estado, {
1004
1039
  rutaOriginal: destino,
@@ -1027,7 +1062,15 @@ function instalarArchivo(archivo, rutas, runtime, opciones = {}) {
1027
1062
  }
1028
1063
  const origenAbsoluto = path.resolve(archivo.origen);
1029
1064
  fs.symlinkSync(origenAbsoluto, destino, stat.isDirectory() ? 'junction' : 'file');
1030
- console.log(` ~ ${path.relative(rutas.base, destino)} (symlink)`);
1065
+ if (typeof opciones.onProgress === 'function') {
1066
+ opciones.onProgress({
1067
+ tipo: 'symlink',
1068
+ componente: archivo.tipo,
1069
+ archivo: nombreArchivo,
1070
+ });
1071
+ } else {
1072
+ console.log(` ~ ${path.relative(rutas.base, destino)} (symlink)`);
1073
+ }
1031
1074
  return { instalado: true, destino };
1032
1075
  } catch (symlinkErr) {
1033
1076
  // Fallback a copy si symlink falla (permisos, filesystem)
@@ -1045,7 +1088,15 @@ function instalarArchivo(archivo, rutas, runtime, opciones = {}) {
1045
1088
  } else {
1046
1089
  fs.copyFileSync(archivo.origen, destino);
1047
1090
  }
1048
- console.log(` + ${path.relative(rutas.base, destino)}`);
1091
+ if (typeof opciones.onProgress === 'function') {
1092
+ opciones.onProgress({
1093
+ tipo: 'archivo-copiado',
1094
+ componente: archivo.tipo,
1095
+ archivo: nombreArchivo,
1096
+ });
1097
+ } else {
1098
+ console.log(` + ${path.relative(rutas.base, destino)}`);
1099
+ }
1049
1100
  return { instalado: true, destino };
1050
1101
  } else {
1051
1102
  return { instalado: false, razon: 'origen no existe' };
@@ -45,6 +45,9 @@ const BOOLEANAS = [
45
45
  // ADR-0019 — Codex/Cursor + MCP autoconfig opt-in
46
46
  'with-mcp', 'no-mcp',
47
47
  'all-runtimes',
48
+ // TUI opt-out (Fase 4 instalador visual). Default: si stdin es TTY y no
49
+ // hay flags, lanza el TUI. Con --no-tui cae al install-asistido clásico.
50
+ 'no-tui', 'tui',
48
51
  ];
49
52
 
50
53
  /**