@saulwade/swl-ses 2.4.1 → 2.4.2
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 +2 -1
- package/README.md +1 -1
- package/habilidades/harness-claude-code/SKILL.md +2 -1
- package/habilidades/tdd-workflow/SKILL.md +3 -1
- package/manifiestos/canonical-hashes.json +328 -0
- package/manifiestos/skills-lock.json +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/reglas/arreglar-al-detectar.md +16 -0
- package/reglas/pruebas.md +7 -3
- package/scripts/actualizar.js +253 -254
- package/scripts/doctor.js +25 -17
- package/scripts/lib/detectar-runtime.js +58 -0
- package/scripts/lib/hooks-settings.js +40 -3
- package/scripts/tui/pantallas/inspect.js +175 -173
- package/scripts/tui/pantallas/uninstall-wizard.js +210 -208
- package/scripts/tui/pantallas/update-wizard.js +234 -232
- package/scripts/tui/pantallas/welcome.js +189 -187
|
@@ -151,6 +151,48 @@ const RUNTIMES = {
|
|
|
151
151
|
},
|
|
152
152
|
};
|
|
153
153
|
|
|
154
|
+
/**
|
|
155
|
+
* ¿El directorio dado es el dir GLOBAL de alguno de los runtimes conocidos?
|
|
156
|
+
*
|
|
157
|
+
* Un dir global jamás debe interpretarse como instalación "de proyecto" de
|
|
158
|
+
* otro runtime: con cwd en el HOME del usuario, todos los `runtime.local`
|
|
159
|
+
* relativos (.claude, .cursor, .codex, …) resuelven dentro del HOME y
|
|
160
|
+
* colisionan con los globales — el caso más dañino es OpenClaude, cuyo
|
|
161
|
+
* local `.claude` colisiona con el global de Claude Code.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} dir - Ruta absoluta o relativa a evaluar.
|
|
164
|
+
* @returns {boolean}
|
|
165
|
+
*/
|
|
166
|
+
function esDirGlobalDeAlgunRuntime(dir) {
|
|
167
|
+
const objetivo = path.resolve(dir);
|
|
168
|
+
for (const config of Object.values(RUNTIMES)) {
|
|
169
|
+
if (path.resolve(config.global) === objetivo) return true;
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Scopes REALES de un runtime: [global, proyecto] deduplicados y sin
|
|
176
|
+
* fantasmas. Con cwd en el HOME, el local relativo (.claude/.cursor/.codex)
|
|
177
|
+
* resuelve al dir GLOBAL propio (duplicado) o al de OTRO runtime (scope
|
|
178
|
+
* "proyecto" espurio — p.ej. el local de OpenClaude colisiona con el global
|
|
179
|
+
* de Claude Code, y un uninstall de ese "proyecto" borraría el global).
|
|
180
|
+
*
|
|
181
|
+
* Consumidores: doctor, actualizar, TUI (inspect/uninstall/update/welcome).
|
|
182
|
+
*
|
|
183
|
+
* @param {object} runtime - Config de RUNTIMES (con .global y .local).
|
|
184
|
+
* @returns {Array<{dir: string, esGlobal: boolean}>}
|
|
185
|
+
*/
|
|
186
|
+
function scopesReales(runtime) {
|
|
187
|
+
const globalAbs = path.resolve(runtime.global);
|
|
188
|
+
const localAbs = path.resolve(runtime.local);
|
|
189
|
+
const scopes = [{ dir: globalAbs, esGlobal: true }];
|
|
190
|
+
if (localAbs !== globalAbs && !esDirGlobalDeAlgunRuntime(localAbs)) {
|
|
191
|
+
scopes.push({ dir: localAbs, esGlobal: false });
|
|
192
|
+
}
|
|
193
|
+
return scopes;
|
|
194
|
+
}
|
|
195
|
+
|
|
154
196
|
/**
|
|
155
197
|
* Detecta qué runtimes están instalados en el sistema
|
|
156
198
|
*/
|
|
@@ -174,6 +216,13 @@ function detectarRuntimes() {
|
|
|
174
216
|
// Si este runtime comparte su directorio local con otro (ej: openclaude comparte .claude/ con claude),
|
|
175
217
|
// solo considerarlo instalado localmente si el estado de instalación declara explícitamente este target.
|
|
176
218
|
let localEfectivo = localExiste;
|
|
219
|
+
|
|
220
|
+
// Guard universal: el dir global de un runtime NUNCA es una instalación de
|
|
221
|
+
// proyecto. Con cwd en el HOME, `<cwd>/.claude` resuelve al GLOBAL de
|
|
222
|
+
// Claude Code y la detección lo trataba como "proyecto" de OpenClaude —
|
|
223
|
+
// produciendo falso "instalación duplicada", 47 hooks "faltantes" y un
|
|
224
|
+
// repair-loop (caso real doctor 2026-07-03).
|
|
225
|
+
if (esDirGlobalDeAlgunRuntime(localAbs)) localEfectivo = false;
|
|
177
226
|
const comparte = (localUsadoPor[localAbs] || []).length > 1;
|
|
178
227
|
if (comparte && localExiste && !globalExiste) {
|
|
179
228
|
// Revisar el estado de instalación para ver si el target coincide
|
|
@@ -293,6 +342,13 @@ function detectarInstalacionesDuales(runtimeId, opciones = {}) {
|
|
|
293
342
|
return { hayConflicto: false, global: false, local: false, globalDir, localDir };
|
|
294
343
|
}
|
|
295
344
|
|
|
345
|
+
// El dir global de CUALQUIER runtime no puede ser el lado "proyecto" del
|
|
346
|
+
// conflicto: con cwd en HOME, el local de OpenClaude (.claude) resuelve al
|
|
347
|
+
// global de Claude Code y se reportaba "duplicado global+proyecto" espurio.
|
|
348
|
+
if (esDirGlobalDeAlgunRuntime(localDir)) {
|
|
349
|
+
return { hayConflicto: false, global: Boolean(require('./estado').cargarEstado(globalDir)), local: false, globalDir, localDir };
|
|
350
|
+
}
|
|
351
|
+
|
|
296
352
|
const estadoGlobal = cargarEstado(globalDir);
|
|
297
353
|
const estadoLocal = cargarEstado(localDir);
|
|
298
354
|
|
|
@@ -337,6 +393,8 @@ function listarRuntimesInstalables() {
|
|
|
337
393
|
module.exports = {
|
|
338
394
|
RUNTIMES,
|
|
339
395
|
detectarRuntimes,
|
|
396
|
+
esDirGlobalDeAlgunRuntime,
|
|
397
|
+
scopesReales,
|
|
340
398
|
detectarInstalacionesDuales,
|
|
341
399
|
obtenerRuntime,
|
|
342
400
|
calcularRutas,
|
|
@@ -689,18 +689,55 @@ function registrarOptimizaciones(settingsPath) {
|
|
|
689
689
|
* hooks con formato válido. No valida contenido semántico, solo estructura.
|
|
690
690
|
*
|
|
691
691
|
* @param {string} settingsPath
|
|
692
|
+
* @param {object} [opts]
|
|
693
|
+
* @param {'claude'|'plano'} [opts.formatoHooks='claude'] - 'claude' valida el
|
|
694
|
+
* esquema de Claude Code ({matcher, hooks:[{type,command}]}, eventos
|
|
695
|
+
* PascalCase). 'plano' valida hooks.json de Cursor/Codex: entradas planas
|
|
696
|
+
* ({command, type?}) bajo eventos camelCase PROPIOS del runtime — los
|
|
697
|
+
* nombres de evento NO se validan contra whitelist porque cada runtime
|
|
698
|
+
* define su set y lo amplía por versión (afterFileEdit,
|
|
699
|
+
* beforeShellExecution, etc. son legítimos en Cursor; el esquema Claude
|
|
700
|
+
* los marcaba como error — caso real doctor 2026-07-03).
|
|
692
701
|
* @returns {{ ok: boolean, problemas: string[] }}
|
|
693
702
|
*/
|
|
694
|
-
function validarEstructuraSettings(settingsPath) {
|
|
703
|
+
function validarEstructuraSettings(settingsPath, opts = {}) {
|
|
704
|
+
const formatoHooks = opts.formatoHooks || 'claude';
|
|
695
705
|
const settings = leerSettings(settingsPath);
|
|
696
706
|
const problemas = [];
|
|
697
707
|
|
|
698
|
-
//
|
|
699
|
-
if (settings.hooks) {
|
|
708
|
+
// Formato plano (Cursor/Codex hooks.json): shape-only, sin whitelist de eventos.
|
|
709
|
+
if (settings.hooks && formatoHooks === 'plano') {
|
|
710
|
+
if (typeof settings.hooks !== 'object' || Array.isArray(settings.hooks)) {
|
|
711
|
+
problemas.push('hooks: debe ser un objeto { evento: [entradas] }');
|
|
712
|
+
} else {
|
|
713
|
+
for (const [evento, entradas] of Object.entries(settings.hooks)) {
|
|
714
|
+
if (!Array.isArray(entradas)) {
|
|
715
|
+
problemas.push(`hooks.${evento}: debe ser un array de entradas`);
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
for (let i = 0; i < entradas.length; i++) {
|
|
719
|
+
const entrada = entradas[i];
|
|
720
|
+
if (!entrada || typeof entrada !== 'object') {
|
|
721
|
+
problemas.push(`hooks.${evento}[${i}]: debe ser un objeto`);
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
if (typeof entrada.command !== 'string' || !entrada.command) {
|
|
725
|
+
problemas.push(`hooks.${evento}[${i}]: falta campo command (string)`);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Validar sección hooks (esquema Claude Code)
|
|
733
|
+
if (settings.hooks && formatoHooks === 'claude') {
|
|
700
734
|
const eventosValidos = new Set([
|
|
701
735
|
'PreToolUse', 'PostToolUse', 'UserPromptSubmit', 'Stop',
|
|
702
736
|
'SubagentStart', 'SubagentStop', 'PreCompact', 'PostCompact',
|
|
703
737
|
'Notification', 'SessionStart', 'TaskCompleted',
|
|
738
|
+
// Codex CLI usa la forma Claude en su hooks.json pero agrega este evento
|
|
739
|
+
// propio (ver transformadores/codex.js EVENTOS_VALIDOS).
|
|
740
|
+
'PermissionRequest',
|
|
704
741
|
]);
|
|
705
742
|
|
|
706
743
|
for (const [evento, entradas] of Object.entries(settings.hooks)) {
|
|
@@ -1,173 +1,175 @@
|
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (!
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
['
|
|
89
|
-
['
|
|
90
|
-
['
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
teclado.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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, scopesReales } = 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
|
+
// Dedup + guard anti-fantasma: desde HOME, el local relativo resuelve al
|
|
38
|
+
// global propio (duplicado) o al de OTRO runtime (p.ej. .claude de
|
|
39
|
+
// OpenClaude = global de Claude Code) — ver scopesReales().
|
|
40
|
+
const candidatos = scopesReales(runtime).map(({ dir, esGlobal }) => (
|
|
41
|
+
{ ruta: dir, scope: esGlobal ? 'global' : 'proyecto' }
|
|
42
|
+
));
|
|
43
|
+
for (const { ruta, scope } of candidatos) {
|
|
44
|
+
if (!fs.existsSync(ruta)) continue;
|
|
45
|
+
const estado = cargarEstado(ruta);
|
|
46
|
+
if (!estado) continue;
|
|
47
|
+
filas.push({
|
|
48
|
+
id: `${runtime.id}:${scope}`,
|
|
49
|
+
runtimeId: runtime.id,
|
|
50
|
+
runtimeNombre: runtime.nombre,
|
|
51
|
+
scope,
|
|
52
|
+
ruta,
|
|
53
|
+
version: estado.versionSistema || 'desconocida',
|
|
54
|
+
perfil: estado.perfil || '-',
|
|
55
|
+
instalado: estado.fechaInstalacion || estado.timestamp || '-',
|
|
56
|
+
componentes: estado.componentesInstalados || [],
|
|
57
|
+
archivos: estado.archivosInstalados || [],
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return filas;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function _agruparPorCategoria(archivos) {
|
|
65
|
+
const grupos = {};
|
|
66
|
+
for (const a of archivos) {
|
|
67
|
+
const cat = a.tipo || a.categoria || 'otros';
|
|
68
|
+
grupos[cat] = (grupos[cat] || 0) + 1;
|
|
69
|
+
}
|
|
70
|
+
return grupos;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function _pantallaDetalle(fila) {
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
if (!render.ES_TTY || !process.stdin.isTTY) {
|
|
76
|
+
resolve();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function _renderizar() {
|
|
81
|
+
render.limpiarPantalla();
|
|
82
|
+
const { cols, rows } = render.obtenerDimensiones();
|
|
83
|
+
|
|
84
|
+
render.escribirEn(2, 3, semantico.titulo(`${fila.runtimeNombre} — ${fila.scope}`));
|
|
85
|
+
render.escribirEn(3, 3, colores.dim(fila.ruta));
|
|
86
|
+
|
|
87
|
+
const datos = [
|
|
88
|
+
['Versión', colores.cyan('v' + fila.version)],
|
|
89
|
+
['Perfil', semantico.enfasis(fila.perfil)],
|
|
90
|
+
['Instalado', fila.instalado],
|
|
91
|
+
['Total componentes', String(fila.componentes.length)],
|
|
92
|
+
['Total archivos', String(fila.archivos.length)],
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
datos.forEach((d, i) => {
|
|
96
|
+
render.escribirEn(5 + i, 5,
|
|
97
|
+
render.rellenarDer(colores.dim(d[0] + ':'), 22) + d[1]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Conteos por categoría
|
|
101
|
+
const grupos = _agruparPorCategoria(fila.archivos);
|
|
102
|
+
const categorias = Object.entries(grupos).sort(([a], [b]) => a.localeCompare(b));
|
|
103
|
+
|
|
104
|
+
const filaCat = 5 + datos.length + 2;
|
|
105
|
+
render.escribirEn(filaCat, 3, semantico.enfasis('Archivos por categoría:'));
|
|
106
|
+
categorias.forEach(([cat, n], i) => {
|
|
107
|
+
const fila = filaCat + 1 + i;
|
|
108
|
+
if (fila >= rows - 3) return;
|
|
109
|
+
render.escribirEn(fila, 5,
|
|
110
|
+
render.rellenarDer(`${iconos.punto} ${cat}`, 22) +
|
|
111
|
+
colores.cyan(String(n).padStart(4)));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
render.dibujarPiePagina([
|
|
115
|
+
['Enter|Esc', 'volver al listado'],
|
|
116
|
+
]);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
render.iniciarModoTui();
|
|
120
|
+
_renderizar();
|
|
121
|
+
|
|
122
|
+
const onResize = () => _renderizar();
|
|
123
|
+
process.stdout.on('resize', onResize);
|
|
124
|
+
|
|
125
|
+
const teclado = teclas.crearTeclado();
|
|
126
|
+
function _finalizar() {
|
|
127
|
+
teclado.desactivar();
|
|
128
|
+
process.stdout.removeListener('resize', onResize);
|
|
129
|
+
render.salirModoTui();
|
|
130
|
+
resolve();
|
|
131
|
+
}
|
|
132
|
+
teclado.on('return', _finalizar);
|
|
133
|
+
teclado.on('escape', _finalizar);
|
|
134
|
+
teclado.activar();
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function ejecutarInspect() {
|
|
139
|
+
const instalaciones = _cargarDatosInstalaciones();
|
|
140
|
+
|
|
141
|
+
if (instalaciones.length === 0) {
|
|
142
|
+
return {
|
|
143
|
+
exito: false,
|
|
144
|
+
error: 'No se detectaron instalaciones SWL en este equipo.',
|
|
145
|
+
sugerencia: 'Ejecuta swl-ses install para crear una.',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Loop: seleccionar una instalación → ver detalle → volver al listado.
|
|
150
|
+
// Esc en el listado sale del Inspect.
|
|
151
|
+
while (true) {
|
|
152
|
+
const items = instalaciones.map(i => ({
|
|
153
|
+
valor: i.id,
|
|
154
|
+
etiqueta: `${i.runtimeNombre} (${i.scope}) — v${i.version} · perfil ${i.perfil}`,
|
|
155
|
+
descripcion: `${i.componentes.length} componentes · ${i.archivos.length} archivos · ${i.ruta}`,
|
|
156
|
+
}));
|
|
157
|
+
|
|
158
|
+
const elegida = await selectorUnico({
|
|
159
|
+
titulo: 'Inspect — Elige una instalación para ver detalle:',
|
|
160
|
+
items,
|
|
161
|
+
});
|
|
162
|
+
if (!elegida) break;
|
|
163
|
+
|
|
164
|
+
const fila = instalaciones.find(i => i.id === elegida);
|
|
165
|
+
if (fila) await _pantallaDetalle(fila);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { exito: true };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = {
|
|
172
|
+
ejecutarInspect,
|
|
173
|
+
_cargarDatosInstalaciones,
|
|
174
|
+
_agruparPorCategoria,
|
|
175
|
+
};
|