@saulwade/swl-ses 1.5.1 → 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.
Files changed (155) hide show
  1. package/CLAUDE.md +225 -209
  2. package/README.md +578 -561
  3. package/agentes/arquitecto-swl.md +33 -1
  4. package/agentes/nemesis-auditor-swl.md +59 -19
  5. package/bin/swl-mcp-server.js +214 -214
  6. package/bin/swl-ses.js +49 -7
  7. package/comandos/swl/.evolved.json +22 -22
  8. package/comandos/swl/contribuir.md +233 -233
  9. package/comandos/swl/nemesis.md +230 -56
  10. package/gateway/lib/event-channel.js +191 -191
  11. package/habilidades/backend-production-resilience/SKILL.md +288 -288
  12. package/habilidades/benchmark-memoria/SKILL.md +186 -186
  13. package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
  14. package/habilidades/doubt-driven-review/SKILL.md +171 -171
  15. package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
  16. package/habilidades/ejecutar-task-iterativo/SKILL.md +278 -278
  17. package/habilidades/eval-framework/SKILL.md +212 -212
  18. package/habilidades/feynman-auditor-swl/SKILL.md +123 -123
  19. package/habilidades/feynman-auditor-swl/recursos/preguntas-language-agnostic.md +108 -108
  20. package/habilidades/harness-claude-code/SKILL.md +299 -299
  21. package/habilidades/infra-github-actions/SKILL.md +166 -166
  22. package/habilidades/legacy-code-rescue/SKILL.md +267 -267
  23. package/habilidades/manejo-errores/.evolved.json +8 -8
  24. package/habilidades/meta-skills-estandar/SKILL.md +207 -4
  25. package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
  26. package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
  27. package/habilidades/nemesis-evaluacion-json/SKILL.md +266 -0
  28. package/habilidades/nemesis-redistribuir/SKILL.md +341 -0
  29. package/habilidades/node-experto/SKILL.md +94 -4
  30. package/habilidades/patrones-python/SKILL.md +229 -229
  31. package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
  32. package/habilidades/planear-fase/SKILL.md +319 -319
  33. package/habilidades/protocolo-revision-swl/SKILL.md +350 -276
  34. package/habilidades/release-semver/.evolved.json +8 -8
  35. package/habilidades/state-inconsistency-auditor-swl/SKILL.md +166 -166
  36. package/habilidades/state-inconsistency-auditor-swl/recursos/coupled-state-patterns.md +147 -147
  37. package/habilidades/tdd-workflow/SKILL.md +121 -4
  38. package/habilidades/testing-python/SKILL.md +340 -340
  39. package/habilidades/web-fetcher-routing/SKILL.md +75 -75
  40. package/hooks/check-update.js +31 -3
  41. package/hooks/claudemd-bloat-detector.js +161 -161
  42. package/hooks/extraccion-aprendizajes.js +11 -0
  43. package/hooks/lib/agent-routing.js +107 -107
  44. package/hooks/lib/auto-consolidator.js +335 -335
  45. package/hooks/lib/error-classifier.js +308 -308
  46. package/hooks/lib/merkle-audit.js +96 -96
  47. package/hooks/lib/provenance-tracker.js +191 -191
  48. package/hooks/lib/rate-limit-tracker.js +253 -253
  49. package/hooks/lib/resource-quota.js +122 -122
  50. package/hooks/lib/retry-jitter.js +165 -165
  51. package/hooks/lib/security-net.js +201 -201
  52. package/hooks/lib/skill-auditor.js +588 -588
  53. package/hooks/lib/sync-status.js +228 -228
  54. package/hooks/lib/taint-tracker.js +107 -107
  55. package/hooks/lib/text-similarity.js +241 -241
  56. package/hooks/lib/toon-compressor.js +245 -245
  57. package/hooks/registro-turnos.js +209 -209
  58. package/hooks/sugerir-regenerar-inventario.js +170 -170
  59. package/hooks/validar-formato-post-subagente.js +140 -140
  60. package/hooks/validar-memoria-hook.js +218 -218
  61. package/instintos/prompt-appendices.yaml +57 -57
  62. package/manifiestos/agent-output-schemas.json +57 -57
  63. package/manifiestos/modulos.json +1324 -1321
  64. package/manifiestos/skills-lock.json +1142 -1114
  65. package/package.json +5 -4
  66. package/plantillas/auditor-veto-template.md +105 -105
  67. package/plantillas/github-workflows/README.md +47 -47
  68. package/plantillas/github-workflows/release-please.yml +44 -44
  69. package/plantillas/github-workflows/swl-ci.yml +107 -107
  70. package/plantillas/github-workflows/swl-security.yml +51 -51
  71. package/plugin.json +355 -351
  72. package/reglas/analisis-previo-tareas-grandes.md +172 -172
  73. package/reglas/arreglar-al-detectar.md +147 -147
  74. package/reglas/fragmentos-compartidos.md +152 -152
  75. package/reglas/harness-claude-code.md +213 -213
  76. package/reglas/registro-componentes-nuevos.md +192 -0
  77. package/reglas/usar-context7.md +226 -226
  78. package/schemas/diary-entry.schema.json +80 -80
  79. package/scripts/actualizar.js +110 -1
  80. package/scripts/audit-tools/audit-history.js +330 -330
  81. package/scripts/audit-tools/bundle-tracker.js +290 -290
  82. package/scripts/audit-tools/canary-monitor.js +352 -352
  83. package/scripts/audit-tools/code-profiler.js +605 -605
  84. package/scripts/audit-tools/dep-doctor.js +320 -320
  85. package/scripts/audit-tools/env-validator.js +206 -206
  86. package/scripts/audit-tools/lib/fs-walk.js +48 -48
  87. package/scripts/audit-tools/lib/output.js +23 -23
  88. package/scripts/audit-tools/migration-checker.js +392 -392
  89. package/scripts/audit-tools/pentest-scanner.js +1436 -1436
  90. package/scripts/benchmark-memoria.js +167 -167
  91. package/scripts/configurar-branch-protection.js +418 -418
  92. package/scripts/derivar-feature-list.js +489 -489
  93. package/scripts/desinstalar.js +105 -24
  94. package/scripts/detectar-aprendizajes-duplicados.js +151 -151
  95. package/scripts/doctor.js +27 -0
  96. package/scripts/field-report.js +199 -199
  97. package/scripts/generar-checklists-consolidados.js +273 -273
  98. package/scripts/generar-inventario.js +420 -420
  99. package/scripts/generar-matriz-lenguajes.js +271 -271
  100. package/scripts/instalador.js +55 -4
  101. package/scripts/lib/artefactos-python.js +43 -43
  102. package/scripts/lib/benchmark-metrics.js +160 -160
  103. package/scripts/lib/budget-enforcer.js +252 -252
  104. package/scripts/lib/configurar-ci.js +380 -380
  105. package/scripts/lib/contadores-inventario.js +217 -217
  106. package/scripts/lib/detectar-stack-detallado.js +307 -307
  107. package/scripts/lib/diary-entry.js +234 -234
  108. package/scripts/lib/eval-metrics-store.js +218 -218
  109. package/scripts/lib/eval-quality.js +171 -171
  110. package/scripts/lib/eval-schemas.js +144 -144
  111. package/scripts/lib/eval-self-correct.js +106 -106
  112. package/scripts/lib/eval-validator.js +185 -185
  113. package/scripts/lib/expandir-targets.js +71 -71
  114. package/scripts/lib/jaccard-similarity.js +98 -98
  115. package/scripts/lib/longmemeval-runner.js +125 -125
  116. package/scripts/lib/mcp_config.py +127 -0
  117. package/scripts/lib/npm-version.js +261 -261
  118. package/scripts/lib/paquetes-conocidos.js +50 -50
  119. package/scripts/lib/parsear-opciones.js +3 -0
  120. package/scripts/lib/prompt-builder.js +264 -264
  121. package/scripts/lib/rrf-fusion.js +175 -175
  122. package/scripts/lib/scoring-instintos.js +277 -277
  123. package/scripts/lib/semantic-search.js +252 -252
  124. package/scripts/lib/toml-merge.js +204 -204
  125. package/scripts/lib/transformadores/codex.js +375 -375
  126. package/scripts/lib/transformadores/cursor.js +359 -359
  127. package/scripts/lib/ui.js +148 -22
  128. package/scripts/limpiar-artefactos-python.js +131 -131
  129. package/scripts/mcp-orchestrator.py +8 -18
  130. package/scripts/mcp-pool-manager.py +12 -23
  131. package/scripts/mcp-server/README.md +170 -170
  132. package/scripts/mcp-server/auth.js +105 -105
  133. package/scripts/mcp-server/cache.js +106 -106
  134. package/scripts/mcp-server/telemetry.js +78 -78
  135. package/scripts/migrar-csv-a-array.js +168 -168
  136. package/scripts/migrar-fase-dominio.js +201 -201
  137. package/scripts/publicar.js +511 -511
  138. package/scripts/run-eval.js +141 -141
  139. package/scripts/tui/componentes/selector-multi.js +189 -0
  140. package/scripts/tui/componentes/selector-unico.js +158 -0
  141. package/scripts/tui/ejecutores.js +375 -0
  142. package/scripts/tui/index.js +162 -0
  143. package/scripts/tui/lib/colores.js +129 -0
  144. package/scripts/tui/lib/render.js +264 -0
  145. package/scripts/tui/lib/teclas.js +113 -0
  146. package/scripts/tui/pantallas/inspect.js +173 -0
  147. package/scripts/tui/pantallas/install-wizard.js +334 -0
  148. package/scripts/tui/pantallas/menu-principal.js +52 -0
  149. package/scripts/tui/pantallas/progreso.js +274 -0
  150. package/scripts/tui/pantallas/resumen.js +132 -0
  151. package/scripts/tui/pantallas/uninstall-wizard.js +208 -0
  152. package/scripts/tui/pantallas/update-wizard.js +232 -0
  153. package/scripts/tui/pantallas/welcome.js +187 -0
  154. package/scripts/validar-userland-vacio.js +110 -110
  155. package/scripts/verificar-docs-vs-codigo.js +425 -0
@@ -0,0 +1,264 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Primitivas de render para la TUI custom de swl-ses.
5
+ * Zero-deps. Solo escape codes ANSI estándar + readline para keypress.
6
+ *
7
+ * Modelo:
8
+ * - `iniciarModoTui()` entra al alternate screen buffer, oculta cursor.
9
+ * - `salirModoTui()` restaura el buffer principal y muestra el cursor.
10
+ * - Cada pantalla llama `limpiarPantalla()` y dibuja desde (1,1).
11
+ * - Las coordenadas son 1-based (compatibilidad con escape codes ANSI).
12
+ *
13
+ * Diseño defensivo:
14
+ * - Si stdout no es TTY (CI, pipe), todas las funciones de cursor son no-op
15
+ * y los dibujos se reducen a console.log lineal.
16
+ * - Capturar SIGINT y exit para garantizar salirModoTui() siempre.
17
+ */
18
+
19
+ const { colores, borde, iconos } = require('./colores');
20
+
21
+ const ES_TTY = !!process.stdout.isTTY;
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Control del terminal
25
+ // ---------------------------------------------------------------------------
26
+
27
+ let modoTuiActivo = false;
28
+ let limpiezaRegistrada = false;
29
+
30
+ function iniciarModoTui() {
31
+ if (modoTuiActivo || !ES_TTY) return;
32
+ modoTuiActivo = true;
33
+
34
+ // Alternate screen buffer: preserva el contenido del terminal pre-TUI
35
+ process.stdout.write('\x1b[?1049h');
36
+ // Ocultar cursor
37
+ process.stdout.write('\x1b[?25l');
38
+ // Limpiar
39
+ process.stdout.write('\x1b[2J\x1b[H');
40
+
41
+ // Registrar limpieza al salir (idempotente)
42
+ if (!limpiezaRegistrada) {
43
+ limpiezaRegistrada = true;
44
+ const limpieza = () => salirModoTui();
45
+ process.once('exit', limpieza);
46
+ process.once('SIGINT', () => { salirModoTui(); process.exit(130); });
47
+ process.once('SIGTERM', () => { salirModoTui(); process.exit(143); });
48
+ }
49
+ }
50
+
51
+ function salirModoTui() {
52
+ if (!modoTuiActivo || !ES_TTY) return;
53
+ modoTuiActivo = false;
54
+
55
+ // Mostrar cursor
56
+ process.stdout.write('\x1b[?25h');
57
+ // Salir del alternate screen buffer (restaura contenido pre-TUI)
58
+ process.stdout.write('\x1b[?1049l');
59
+ }
60
+
61
+ function estaActivo() {
62
+ return modoTuiActivo;
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Movimiento de cursor
67
+ // ---------------------------------------------------------------------------
68
+
69
+ function moverCursor(fila, col) {
70
+ if (!ES_TTY) return;
71
+ process.stdout.write(`\x1b[${fila};${col}H`);
72
+ }
73
+
74
+ function limpiarPantalla() {
75
+ if (!ES_TTY) return;
76
+ process.stdout.write('\x1b[2J\x1b[H');
77
+ }
78
+
79
+ function limpiarLinea() {
80
+ if (!ES_TTY) return;
81
+ process.stdout.write('\x1b[2K\r');
82
+ }
83
+
84
+ function obtenerDimensiones() {
85
+ return {
86
+ cols: process.stdout.columns || 80,
87
+ rows: process.stdout.rows || 24,
88
+ };
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Escritura posicionada
93
+ // ---------------------------------------------------------------------------
94
+
95
+ /**
96
+ * Escribe texto en una posición específica de la pantalla. El texto
97
+ * puede contener escape codes ANSI (colores) — no se cuentan en la longitud.
98
+ */
99
+ function escribirEn(fila, col, texto) {
100
+ moverCursor(fila, col);
101
+ process.stdout.write(texto);
102
+ }
103
+
104
+ /**
105
+ * Cuenta la longitud visual de un string ignorando códigos ANSI.
106
+ * Útil para centrar/alinear texto con color sin distorsionar el cálculo.
107
+ */
108
+ function anchoVisual(texto) {
109
+ return String(texto).replace(/\x1b\[[0-9;]*m/g, '').length;
110
+ }
111
+
112
+ /**
113
+ * Recorta un string a `maxAncho` caracteres visuales preservando códigos ANSI.
114
+ * Si excede, agrega '…' al final.
115
+ */
116
+ function recortar(texto, maxAncho) {
117
+ const s = String(texto);
118
+ if (anchoVisual(s) <= maxAncho) return s;
119
+
120
+ // Conservar códigos ANSI mientras se cuentan solo caracteres visibles
121
+ let resultado = '';
122
+ let ancho = 0;
123
+ let i = 0;
124
+ while (i < s.length && ancho < maxAncho - 1) {
125
+ if (s[i] === '\x1b' && s[i + 1] === '[') {
126
+ // copiar el escape code completo sin contar
127
+ const finCode = s.indexOf('m', i);
128
+ if (finCode === -1) break;
129
+ resultado += s.slice(i, finCode + 1);
130
+ i = finCode + 1;
131
+ } else {
132
+ resultado += s[i];
133
+ ancho++;
134
+ i++;
135
+ }
136
+ }
137
+ return resultado + '…\x1b[0m';
138
+ }
139
+
140
+ /**
141
+ * Rellena un string a `ancho` caracteres visuales con espacios a la derecha.
142
+ */
143
+ function rellenarDer(texto, ancho) {
144
+ const diff = ancho - anchoVisual(texto);
145
+ return diff > 0 ? String(texto) + ' '.repeat(diff) : recortar(texto, ancho);
146
+ }
147
+
148
+ /**
149
+ * Centra un string en `ancho` caracteres visuales.
150
+ */
151
+ function centrar(texto, ancho) {
152
+ const sobra = ancho - anchoVisual(texto);
153
+ if (sobra <= 0) return recortar(texto, ancho);
154
+ const izq = Math.floor(sobra / 2);
155
+ const der = sobra - izq;
156
+ return ' '.repeat(izq) + texto + ' '.repeat(der);
157
+ }
158
+
159
+ // ---------------------------------------------------------------------------
160
+ // Cajas y paneles
161
+ // ---------------------------------------------------------------------------
162
+
163
+ /**
164
+ * Dibuja una caja con borde rounded en (fila, col) con ancho y alto dados.
165
+ * Opciones:
166
+ * - titulo: string opcional centrado en la línea superior
167
+ * - color: función de color a aplicar al borde
168
+ * - doble: usar borde doble (═║) en lugar de rounded (─│)
169
+ * - resaltado: invertir colores del borde (para indicar foco)
170
+ */
171
+ function dibujarCaja(fila, col, ancho, alto, opts = {}) {
172
+ if (!ES_TTY) return;
173
+ if (ancho < 2 || alto < 2) return;
174
+
175
+ const c = opts.color || colores.gris;
176
+ const b = opts.doble ? {
177
+ esqSI: borde.dbEsqSupIzq, esqSD: borde.dbEsqSupDer,
178
+ esqII: borde.dbEsqInfIzq, esqID: borde.dbEsqInfDer,
179
+ h: borde.dbHorizontal, v: borde.dbVertical,
180
+ } : {
181
+ esqSI: borde.esquinaSupIzq, esqSD: borde.esquinaSupDer,
182
+ esqII: borde.esquinaInfIzq, esqID: borde.esquinaInfDer,
183
+ h: borde.horizontal, v: borde.vertical,
184
+ };
185
+
186
+ // Línea superior con título opcional
187
+ let lineaSup;
188
+ if (opts.titulo) {
189
+ const tituloRecortado = recortar(opts.titulo, ancho - 4);
190
+ const anchoTitulo = anchoVisual(tituloRecortado);
191
+ const izq = b.h.repeat(2);
192
+ const der = b.h.repeat(Math.max(0, ancho - 4 - anchoTitulo));
193
+ lineaSup = c(b.esqSI + izq) + ' ' + tituloRecortado + ' ' + c(der + b.esqSD);
194
+ } else {
195
+ lineaSup = c(b.esqSI + b.h.repeat(ancho - 2) + b.esqSD);
196
+ }
197
+ escribirEn(fila, col, lineaSup);
198
+
199
+ // Laterales
200
+ for (let f = fila + 1; f < fila + alto - 1; f++) {
201
+ escribirEn(f, col, c(b.v));
202
+ escribirEn(f, col + ancho - 1, c(b.v));
203
+ }
204
+
205
+ // Línea inferior
206
+ escribirEn(fila + alto - 1, col, c(b.esqII + b.h.repeat(ancho - 2) + b.esqID));
207
+ }
208
+
209
+ /**
210
+ * Dibuja una barra horizontal de progreso en (fila, col) con ancho dado.
211
+ * Porcentaje entre 0 y 1. Estilo: [████░░░░░░] 42%
212
+ */
213
+ function dibujarBarra(fila, col, ancho, porcentaje, opts = {}) {
214
+ const pct = Math.max(0, Math.min(1, porcentaje));
215
+ const lleno = opts.charLleno || '█';
216
+ const vacio = opts.charVacio || '░';
217
+ const colorLleno = opts.colorLleno || colores.verde;
218
+ const colorVacio = opts.colorVacio || colores.dim;
219
+
220
+ const anchoBarra = ancho - 7; // reservar espacio para " 100%"
221
+ const llenoN = Math.round(pct * anchoBarra);
222
+ const vacioN = anchoBarra - llenoN;
223
+
224
+ const barra = colorLleno(lleno.repeat(llenoN)) + colorVacio(vacio.repeat(vacioN));
225
+ const pctStr = String(Math.round(pct * 100)).padStart(3) + '%';
226
+
227
+ escribirEn(fila, col, barra + ' ' + colores.dim(pctStr));
228
+ }
229
+
230
+ /**
231
+ * Dibuja una línea de pie (footer hint) con atajos de teclado.
232
+ * Ejemplo: "↑↓ navegar · Enter elegir · Esc salir"
233
+ */
234
+ function dibujarPiePagina(atajos) {
235
+ const { rows, cols } = obtenerDimensiones();
236
+ const sep = colores.dim(' · ');
237
+ const linea = atajos
238
+ .map(([tecla, accion]) => `${colores.cyan(tecla)} ${colores.dim(accion)}`)
239
+ .join(sep);
240
+ escribirEn(rows, 1, ' ' + recortar(linea, cols - 2));
241
+ }
242
+
243
+ // ---------------------------------------------------------------------------
244
+ // Exports
245
+ // ---------------------------------------------------------------------------
246
+
247
+ module.exports = {
248
+ ES_TTY,
249
+ iniciarModoTui,
250
+ salirModoTui,
251
+ estaActivo,
252
+ moverCursor,
253
+ limpiarPantalla,
254
+ limpiarLinea,
255
+ obtenerDimensiones,
256
+ escribirEn,
257
+ anchoVisual,
258
+ recortar,
259
+ rellenarDer,
260
+ centrar,
261
+ dibujarCaja,
262
+ dibujarBarra,
263
+ dibujarPiePagina,
264
+ };
@@ -0,0 +1,113 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Handler unificado de keypress para la TUI de swl-ses.
5
+ * Zero-deps. Usa readline.emitKeypressEvents para parsear secuencias ANSI.
6
+ *
7
+ * Modelo de uso:
8
+ *
9
+ * const teclado = crearTeclado();
10
+ * teclado.on('up', () => cursor--);
11
+ * teclado.on('down', () => cursor++);
12
+ * teclado.on('return', () => resolver());
13
+ * teclado.on('escape', () => cancelar());
14
+ * teclado.activar();
15
+ * // ... eventualmente
16
+ * teclado.desactivar();
17
+ *
18
+ * Maneja:
19
+ * - Flechas (up/down/left/right)
20
+ * - vim-keys (h/j/k/l) opcional
21
+ * - Espacio, return, escape, tab
22
+ * - Ctrl+C (re-emite como 'escape' por default; se puede sobrescribir)
23
+ * - Teclas alfanuméricas como caracteres (con `keypress` evento)
24
+ */
25
+
26
+ const readline = require('readline');
27
+
28
+ function crearTeclado(opciones = {}) {
29
+ const aliasVim = opciones.vim !== false; // por default soporta h/j/k/l
30
+ const handlers = new Map();
31
+ let activo = false;
32
+ let modoAnterior = null;
33
+
34
+ function _resolverNombre(key, str) {
35
+ if (!key) return null;
36
+ if (key.ctrl && key.name === 'c') return 'escape'; // Ctrl+C → escape
37
+ if (key.name) return key.name;
38
+ return null;
39
+ }
40
+
41
+ function onKeypress(str, key) {
42
+ if (!activo) return;
43
+
44
+ const nombre = _resolverNombre(key, str);
45
+ if (!nombre) return;
46
+
47
+ // Alias vim
48
+ let normalizado = nombre;
49
+ if (aliasVim) {
50
+ if (nombre === 'k') normalizado = 'up';
51
+ else if (nombre === 'j') normalizado = 'down';
52
+ else if (nombre === 'h') normalizado = 'left';
53
+ else if (nombre === 'l') normalizado = 'right';
54
+ }
55
+
56
+ // Disparar handlers exactos
57
+ if (handlers.has(normalizado)) {
58
+ handlers.get(normalizado).forEach(fn => fn(key, str));
59
+ }
60
+
61
+ // Handler genérico para cualquier tecla
62
+ if (handlers.has('*')) {
63
+ handlers.get('*').forEach(fn => fn(nombre, str, key));
64
+ }
65
+ }
66
+
67
+ function activar() {
68
+ if (activo) return;
69
+ if (!process.stdin.isTTY) {
70
+ // No-TTY: no podemos leer teclas. El llamador debe manejar este caso.
71
+ return;
72
+ }
73
+ activo = true;
74
+ readline.emitKeypressEvents(process.stdin);
75
+ modoAnterior = process.stdin.isRaw;
76
+ process.stdin.setRawMode(true);
77
+ process.stdin.resume();
78
+ process.stdin.on('keypress', onKeypress);
79
+ }
80
+
81
+ function desactivar() {
82
+ if (!activo) return;
83
+ activo = false;
84
+ if (process.stdin.isTTY) {
85
+ process.stdin.removeListener('keypress', onKeypress);
86
+ process.stdin.setRawMode(modoAnterior || false);
87
+ process.stdin.pause();
88
+ }
89
+ }
90
+
91
+ function on(tecla, handler) {
92
+ if (!handlers.has(tecla)) handlers.set(tecla, []);
93
+ handlers.get(tecla).push(handler);
94
+ return api;
95
+ }
96
+
97
+ function off(tecla, handler) {
98
+ if (!handlers.has(tecla)) return api;
99
+ if (!handler) {
100
+ handlers.delete(tecla);
101
+ return api;
102
+ }
103
+ const arr = handlers.get(tecla).filter(h => h !== handler);
104
+ if (arr.length === 0) handlers.delete(tecla);
105
+ else handlers.set(tecla, arr);
106
+ return api;
107
+ }
108
+
109
+ const api = { activar, desactivar, on, off, estaActivo: () => activo };
110
+ return api;
111
+ }
112
+
113
+ module.exports = { crearTeclado };
@@ -0,0 +1,173 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Pantalla Inspect — navegador read-only de componentes instalados.
5
+ *
6
+ * Flujo:
7
+ * 1. Lista de instalaciones detectadas (runtime + scope + perfil + version)
8
+ * 2. Al seleccionar una, muestra detalles:
9
+ * - Componentes por categoría (con conteos)
10
+ * - Última actualización
11
+ * - Lista de hooks registrados
12
+ * - Lista de skills disponibles
13
+ *
14
+ * No modifica nada. Solo lee de los archivos de estado.
15
+ */
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+
20
+ const { selectorUnico } = require('../componentes/selector-unico');
21
+ const render = require('../lib/render');
22
+ const teclas = require('../lib/teclas');
23
+ const { colores, semantico, iconos } = require('../lib/colores');
24
+
25
+ function _cargarDatosInstalaciones() {
26
+ let detectarRuntimes, cargarEstado;
27
+ try {
28
+ ({ detectarRuntimes } = require('../../lib/detectar-runtime'));
29
+ ({ cargarEstado } = require('../../lib/estado'));
30
+ } catch (_) {
31
+ return [];
32
+ }
33
+
34
+ const runtimes = detectarRuntimes();
35
+ const filas = [];
36
+ for (const runtime of runtimes) {
37
+ const candidatos = [
38
+ { ruta: runtime.global, scope: 'global' },
39
+ { ruta: path.resolve(runtime.local), scope: 'proyecto' },
40
+ ];
41
+ for (const { ruta, scope } of candidatos) {
42
+ if (!fs.existsSync(ruta)) continue;
43
+ const estado = cargarEstado(ruta);
44
+ if (!estado) continue;
45
+ filas.push({
46
+ id: `${runtime.id}:${scope}`,
47
+ runtimeId: runtime.id,
48
+ runtimeNombre: runtime.nombre,
49
+ scope,
50
+ ruta,
51
+ version: estado.versionSistema || 'desconocida',
52
+ perfil: estado.perfil || '-',
53
+ instalado: estado.fechaInstalacion || estado.timestamp || '-',
54
+ componentes: estado.componentesInstalados || [],
55
+ archivos: estado.archivosInstalados || [],
56
+ });
57
+ }
58
+ }
59
+ return filas;
60
+ }
61
+
62
+ function _agruparPorCategoria(archivos) {
63
+ const grupos = {};
64
+ for (const a of archivos) {
65
+ const cat = a.tipo || a.categoria || 'otros';
66
+ grupos[cat] = (grupos[cat] || 0) + 1;
67
+ }
68
+ return grupos;
69
+ }
70
+
71
+ function _pantallaDetalle(fila) {
72
+ return new Promise((resolve) => {
73
+ if (!render.ES_TTY || !process.stdin.isTTY) {
74
+ resolve();
75
+ return;
76
+ }
77
+
78
+ function _renderizar() {
79
+ render.limpiarPantalla();
80
+ const { cols, rows } = render.obtenerDimensiones();
81
+
82
+ render.escribirEn(2, 3, semantico.titulo(`${fila.runtimeNombre} — ${fila.scope}`));
83
+ render.escribirEn(3, 3, colores.dim(fila.ruta));
84
+
85
+ const datos = [
86
+ ['Versión', colores.cyan('v' + fila.version)],
87
+ ['Perfil', semantico.enfasis(fila.perfil)],
88
+ ['Instalado', fila.instalado],
89
+ ['Total componentes', String(fila.componentes.length)],
90
+ ['Total archivos', String(fila.archivos.length)],
91
+ ];
92
+
93
+ datos.forEach((d, i) => {
94
+ render.escribirEn(5 + i, 5,
95
+ render.rellenarDer(colores.dim(d[0] + ':'), 22) + d[1]);
96
+ });
97
+
98
+ // Conteos por categoría
99
+ const grupos = _agruparPorCategoria(fila.archivos);
100
+ const categorias = Object.entries(grupos).sort(([a], [b]) => a.localeCompare(b));
101
+
102
+ const filaCat = 5 + datos.length + 2;
103
+ render.escribirEn(filaCat, 3, semantico.enfasis('Archivos por categoría:'));
104
+ categorias.forEach(([cat, n], i) => {
105
+ const fila = filaCat + 1 + i;
106
+ if (fila >= rows - 3) return;
107
+ render.escribirEn(fila, 5,
108
+ render.rellenarDer(`${iconos.punto} ${cat}`, 22) +
109
+ colores.cyan(String(n).padStart(4)));
110
+ });
111
+
112
+ render.dibujarPiePagina([
113
+ ['Enter|Esc', 'volver al listado'],
114
+ ]);
115
+ }
116
+
117
+ render.iniciarModoTui();
118
+ _renderizar();
119
+
120
+ const onResize = () => _renderizar();
121
+ process.stdout.on('resize', onResize);
122
+
123
+ const teclado = teclas.crearTeclado();
124
+ function _finalizar() {
125
+ teclado.desactivar();
126
+ process.stdout.removeListener('resize', onResize);
127
+ render.salirModoTui();
128
+ resolve();
129
+ }
130
+ teclado.on('return', _finalizar);
131
+ teclado.on('escape', _finalizar);
132
+ teclado.activar();
133
+ });
134
+ }
135
+
136
+ async function ejecutarInspect() {
137
+ const instalaciones = _cargarDatosInstalaciones();
138
+
139
+ if (instalaciones.length === 0) {
140
+ return {
141
+ exito: false,
142
+ error: 'No se detectaron instalaciones SWL en este equipo.',
143
+ sugerencia: 'Ejecuta swl-ses install para crear una.',
144
+ };
145
+ }
146
+
147
+ // Loop: seleccionar una instalación → ver detalle → volver al listado.
148
+ // Esc en el listado sale del Inspect.
149
+ while (true) {
150
+ const items = instalaciones.map(i => ({
151
+ valor: i.id,
152
+ etiqueta: `${i.runtimeNombre} (${i.scope}) — v${i.version} · perfil ${i.perfil}`,
153
+ descripcion: `${i.componentes.length} componentes · ${i.archivos.length} archivos · ${i.ruta}`,
154
+ }));
155
+
156
+ const elegida = await selectorUnico({
157
+ titulo: 'Inspect — Elige una instalación para ver detalle:',
158
+ items,
159
+ });
160
+ if (!elegida) break;
161
+
162
+ const fila = instalaciones.find(i => i.id === elegida);
163
+ if (fila) await _pantallaDetalle(fila);
164
+ }
165
+
166
+ return { exito: true };
167
+ }
168
+
169
+ module.exports = {
170
+ ejecutarInspect,
171
+ _cargarDatosInstalaciones,
172
+ _agruparPorCategoria,
173
+ };