@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.
@@ -1,254 +1,253 @@
1
- 'use strict';
2
-
3
- /**
4
- * swl-ses update
5
- * Actualiza componentes instalados preservando _userland/.
6
- * Interactivo: muestra versiones, pide confirmación, muestra progreso.
7
- * No-interactivo con --force o sin TTY.
8
- *
9
- * Modo --check (read-only):
10
- * Solo muestra el estado actual de versiones (local vs npm vs runtimes instalados).
11
- * No pide confirmación. No actualiza nada. Util para el autor o usuarios que
12
- * quieren verificar si están al dia sin esperar al hook automatico.
13
- */
14
-
15
- const fs = require('fs');
16
- const os = require('os');
17
- const path = require('path');
18
- const { detectarRuntimes } = require('./lib/detectar-runtime');
19
- const { cargarEstado } = require('./lib/estado');
20
- const {
21
- color, icono, encabezado, formatearPaso, formatearAdvertencia,
22
- spinner, preguntarSiNo, lineaSeparadora, ES_TTY,
23
- } = require('./lib/ui');
24
- const { versionRemotaParalela, compararSemver } = require('./lib/npm-version');
25
-
26
- const VERSION_LOCAL = require('../package.json').version;
27
-
28
- /**
29
- * Consulta la versión más reciente publicada en npm. Si la consulta falla
30
- * (sin red, timeout), retorna la versión local como fallback razonable.
31
- */
32
- async function obtenerVersionRemota() {
33
- const r = await versionRemotaParalela({ timeoutMs: 10000 });
34
- return r.version || VERSION_LOCAL;
35
- }
36
-
37
- /**
38
- * Modo --check: muestra el estado actual sin actualizar. Read-only.
39
- *
40
- * Salida:
41
- * - Version del paquete que esta ejecutando este CLI (VERSION_LOCAL).
42
- * - Version mas reciente publicada en npm.
43
- * - Comparacion semver: al dia / adelantado / atras.
44
- * - Estado del hook check-update.js: ultima verificacion automatica.
45
- * - Tabla por runtime instalado (si los detecta) con su versionSistema.
46
- */
47
- async function checkStatus() {
48
- console.log(encabezado('swl-ses update --check', VERSION_LOCAL));
49
-
50
- // Resolver version remota con timeout corto
51
- const remota = await obtenerVersionRemota();
52
- const cmp = compararSemver(VERSION_LOCAL, remota);
53
-
54
- let estadoVersion;
55
- if (cmp === null) {
56
- estadoVersion = color.amarillo('Comparacion no disponible (formato inesperado)');
57
- } else if (cmp === 0) {
58
- estadoVersion = color.verde('Al dia');
59
- } else if (cmp < 0) {
60
- estadoVersion = color.amarillo(`Nueva version disponible (${remota})`);
61
- } else {
62
- estadoVersion = color.dim(`Local mas reciente que npm (${VERSION_LOCAL} > ${remota}, publicacion pendiente)`);
63
- }
64
-
65
- console.log(` Paquete ejecutado: ${color.negrita('v' + VERSION_LOCAL)}`);
66
- console.log(` Version en npm: ${color.negrita('v' + remota)}`);
67
- console.log(` Estado: ${estadoVersion}`);
68
- console.log('');
69
-
70
- // Estado del hook automatico (check-update.js).
71
- // SWL_UPDATE_FLAG_PATH override solo para tests (evita race con flag único).
72
- const flagPath = process.env.SWL_UPDATE_FLAG_PATH || path.join(os.tmpdir(), 'swl-ses-update-check.json');
73
- try {
74
- const flag = JSON.parse(fs.readFileSync(flagPath, 'utf8'));
75
- const horas = ((Date.now() - flag.timestamp) / 3600000).toFixed(1);
76
- console.log(` ${color.negrita('Hook automatico (check-update.js):')}`);
77
- console.log(` Ultima verificacion: hace ${horas}h`);
78
- console.log(` Local registrada: ${flag.local}`);
79
- console.log(` Remota registrada: ${flag.remota || color.dim('(no resuelta)')}`);
80
- console.log(` hayNueva: ${flag.hayNueva ? color.amarillo('si') : color.verde('no')}`);
81
- console.log(` Flag: ${flagPath}`);
82
- } catch {
83
- console.log(` ${color.negrita('Hook automatico (check-update.js):')}`);
84
- console.log(` ${color.dim('Sin flag de throttle — el hook nunca se ha ejecutado en este equipo,')}`);
85
- console.log(` ${color.dim('o el archivo temp/swl-ses-update-check.json fue removido.')}`);
86
- }
87
- console.log('');
88
-
89
- // Estado por runtime instalado
90
- const runtimes = detectarRuntimes();
91
- if (runtimes.length === 0) {
92
- console.log(formatearAdvertencia('Runtimes', 'No se detectaron runtimes con instalacion SWL'));
93
- console.log('');
94
- return;
95
- }
96
-
97
- console.log(` ${color.negrita('Runtimes detectados:')}`);
98
- let totalRuntimes = 0;
99
- for (const runtime of runtimes) {
100
- const dirs = [runtime.global, path.resolve(runtime.local)];
101
- for (const dir of dirs) {
102
- if (!fs.existsSync(dir)) continue;
103
- const estado = cargarEstado(dir);
104
- if (!estado) continue;
105
-
106
- const esGlobal = dir === runtime.global;
107
- const ubicacion = esGlobal ? 'global' : 'proyecto';
108
- const v = estado.versionSistema || 'desconocida';
109
-
110
- const cmpRuntime = compararSemver(v, remota);
111
- let etiqueta;
112
- if (cmpRuntime === null) {
113
- etiqueta = color.dim('(version no reconocida)');
114
- } else if (cmpRuntime === 0) {
115
- etiqueta = color.verde('al dia');
116
- } else if (cmpRuntime < 0) {
117
- etiqueta = color.amarillo(`atrasada (npm: ${remota})`);
118
- } else {
119
- etiqueta = color.dim('adelantada');
120
- }
121
-
122
- console.log(` ${runtime.nombre} (${ubicacion}): v${v} ${etiqueta}`);
123
- totalRuntimes++;
124
- }
125
- }
126
- if (totalRuntimes === 0) {
127
- console.log(` ${color.dim('(ninguno con .swl-install-state.json valido)')}`);
128
- }
129
- console.log('');
130
- console.log(` ${color.dim('Modo --check es read-only. Para actualizar:')} npx -y @saulwade/swl-ses@latest update`);
131
- console.log('');
132
- }
133
-
134
- async function update(opciones) {
135
- // Modo read-only: solo reportar estado, no tocar nada
136
- if (opciones.check) {
137
- await checkStatus();
138
- return;
139
- }
140
-
141
- const force = opciones.force || !ES_TTY;
142
-
143
- // Obtener la versión más reciente del registro npm
144
- const VERSION = await obtenerVersionRemota();
145
-
146
- console.log(encabezado('swl-ses update', VERSION_LOCAL));
147
-
148
- // 1. Detectar runtimes
149
- const runtimes = detectarRuntimes();
150
-
151
- if (runtimes.length === 0) {
152
- console.log(formatearAdvertencia('Runtimes', 'No se detectaron runtimes con instalación SWL'));
153
- console.log(`\n Ejecuta primero: npx @saulwade/swl-ses@latest install --target claude`);
154
- process.exit(1);
155
- }
156
-
157
- let actualizados = 0;
158
- let omitidos = 0;
159
-
160
- // 2. Iterar runtimes
161
- for (const runtime of runtimes) {
162
- const dirs = [runtime.global, path.resolve(runtime.local)];
163
- for (const dir of dirs) {
164
- if (!fs.existsSync(dir)) continue;
165
-
166
- const estado = cargarEstado(dir);
167
- if (!estado) continue;
168
-
169
- const versionInstalada = estado.versionSistema || 'desconocida';
170
- const esGlobal = dir === runtime.global;
171
- const ubicacion = esGlobal ? 'global' : 'proyecto';
172
-
173
- const necesitaActualizar = versionInstalada !== VERSION;
174
- const paqueteDesactualizado = VERSION_LOCAL !== VERSION;
175
-
176
- console.log(` ${color.negrita(`Runtime: ${runtime.nombre}`)} ${color.dim(`(${ubicacion})`)}`);
177
- console.log(` Versión instalada: ${necesitaActualizar ? color.amarillo(versionInstalada) : color.verde(versionInstalada)}`);
178
- console.log(` Versión disponible: ${color.verde(VERSION)}${paqueteDesactualizado ? color.dim(` (ejecutando con v${VERSION_LOCAL})`) : ''}`);
179
- console.log(` Perfil: ${estado.perfil}`);
180
- console.log(` Componentes: ${estado.componentesInstalados.length}`);
181
- console.log(` Archivos: ${estado.archivosInstalados.length}`);
182
- console.log('');
183
-
184
- // Si la versión disponible es más reciente pero el paquete ejecutándose es viejo,
185
- // indicar al usuario que use @latest
186
- if (paqueteDesactualizado && necesitaActualizar) {
187
- console.log(formatearAdvertencia(runtime.nombre,
188
- `Este paquete es v${VERSION_LOCAL} pero la v${VERSION} está disponible.\n` +
189
- ` Para actualizar, ejecuta: npx @saulwade/swl-ses@latest update --force`));
190
- console.log('');
191
- omitidos++;
192
- continue;
193
- }
194
-
195
- // Verificar si ya está actualizado
196
- if (!necesitaActualizar && !force) {
197
- console.log(formatearPaso(runtime.nombre, 'Ya está en la versión más reciente'));
198
- console.log('');
199
- omitidos++;
200
- continue;
201
- }
202
-
203
- // Confirmar
204
- if (!force) {
205
- const confirmar = await preguntarSiNo(
206
- !necesitaActualizar
207
- ? `¿Re-instalar ${runtime.nombre} v${VERSION}?`
208
- : `¿Actualizar ${runtime.nombre} de ${versionInstalada} a ${VERSION}?`
209
- );
210
- console.log('');
211
-
212
- if (!confirmar) {
213
- console.log(formatearAdvertencia(runtime.nombre, 'Actualización cancelada por el usuario'));
214
- console.log('');
215
- omitidos++;
216
- continue;
217
- }
218
- }
219
-
220
- // Ejecutar actualización con spinner
221
- const sp = spinner(`Actualizando ${runtime.nombre}...`);
222
-
223
- try {
224
- const instalador = require('./instalador');
225
- await instalador({
226
- target: runtime.id,
227
- profile: estado.perfil,
228
- global: estado.global,
229
- force: true,
230
- });
231
-
232
- sp.exito(`${runtime.nombre} actualizado a v${VERSION} (${estado.archivosInstalados.length} archivos)`);
233
- actualizados++;
234
- } catch (err) {
235
- sp.fallo(`Error actualizando ${runtime.nombre}: ${err.message}`);
236
- }
237
-
238
- console.log('');
239
- }
240
- }
241
-
242
- // 3. Reporte final
243
- console.log(` ${lineaSeparadora()}`);
244
- if (actualizados > 0) {
245
- console.log(formatearPaso('Resultado', `${actualizados} runtime(s) actualizado(s)${omitidos > 0 ? `, ${omitidos} omitido(s)` : ''}`));
246
- } else if (omitidos > 0) {
247
- console.log(formatearPaso('Resultado', 'Todo está actualizado'));
248
- } else {
249
- console.log(formatearAdvertencia('Resultado', 'Ningún runtime fue actualizado'));
250
- }
251
- console.log('');
252
- }
253
-
254
- module.exports = update;
1
+ 'use strict';
2
+
3
+ /**
4
+ * swl-ses update
5
+ * Actualiza componentes instalados preservando _userland/.
6
+ * Interactivo: muestra versiones, pide confirmación, muestra progreso.
7
+ * No-interactivo con --force o sin TTY.
8
+ *
9
+ * Modo --check (read-only):
10
+ * Solo muestra el estado actual de versiones (local vs npm vs runtimes instalados).
11
+ * No pide confirmación. No actualiza nada. Util para el autor o usuarios que
12
+ * quieren verificar si están al dia sin esperar al hook automatico.
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const os = require('os');
17
+ const path = require('path');
18
+ const { detectarRuntimes, scopesReales } = require('./lib/detectar-runtime');
19
+ const { cargarEstado } = require('./lib/estado');
20
+ const {
21
+ color, icono, encabezado, formatearPaso, formatearAdvertencia,
22
+ spinner, preguntarSiNo, lineaSeparadora, ES_TTY,
23
+ } = require('./lib/ui');
24
+ const { versionRemotaParalela, compararSemver } = require('./lib/npm-version');
25
+
26
+ const VERSION_LOCAL = require('../package.json').version;
27
+
28
+ /**
29
+ * Consulta la versión más reciente publicada en npm. Si la consulta falla
30
+ * (sin red, timeout), retorna la versión local como fallback razonable.
31
+ */
32
+ async function obtenerVersionRemota() {
33
+ const r = await versionRemotaParalela({ timeoutMs: 10000 });
34
+ return r.version || VERSION_LOCAL;
35
+ }
36
+
37
+ /**
38
+ * Modo --check: muestra el estado actual sin actualizar. Read-only.
39
+ *
40
+ * Salida:
41
+ * - Version del paquete que esta ejecutando este CLI (VERSION_LOCAL).
42
+ * - Version mas reciente publicada en npm.
43
+ * - Comparacion semver: al dia / adelantado / atras.
44
+ * - Estado del hook check-update.js: ultima verificacion automatica.
45
+ * - Tabla por runtime instalado (si los detecta) con su versionSistema.
46
+ */
47
+ async function checkStatus() {
48
+ console.log(encabezado('swl-ses update --check', VERSION_LOCAL));
49
+
50
+ // Resolver version remota con timeout corto
51
+ const remota = await obtenerVersionRemota();
52
+ const cmp = compararSemver(VERSION_LOCAL, remota);
53
+
54
+ let estadoVersion;
55
+ if (cmp === null) {
56
+ estadoVersion = color.amarillo('Comparacion no disponible (formato inesperado)');
57
+ } else if (cmp === 0) {
58
+ estadoVersion = color.verde('Al dia');
59
+ } else if (cmp < 0) {
60
+ estadoVersion = color.amarillo(`Nueva version disponible (${remota})`);
61
+ } else {
62
+ estadoVersion = color.dim(`Local mas reciente que npm (${VERSION_LOCAL} > ${remota}, publicacion pendiente)`);
63
+ }
64
+
65
+ console.log(` Paquete ejecutado: ${color.negrita('v' + VERSION_LOCAL)}`);
66
+ console.log(` Version en npm: ${color.negrita('v' + remota)}`);
67
+ console.log(` Estado: ${estadoVersion}`);
68
+ console.log('');
69
+
70
+ // Estado del hook automatico (check-update.js).
71
+ // SWL_UPDATE_FLAG_PATH override solo para tests (evita race con flag único).
72
+ const flagPath = process.env.SWL_UPDATE_FLAG_PATH || path.join(os.tmpdir(), 'swl-ses-update-check.json');
73
+ try {
74
+ const flag = JSON.parse(fs.readFileSync(flagPath, 'utf8'));
75
+ const horas = ((Date.now() - flag.timestamp) / 3600000).toFixed(1);
76
+ console.log(` ${color.negrita('Hook automatico (check-update.js):')}`);
77
+ console.log(` Ultima verificacion: hace ${horas}h`);
78
+ console.log(` Local registrada: ${flag.local}`);
79
+ console.log(` Remota registrada: ${flag.remota || color.dim('(no resuelta)')}`);
80
+ console.log(` hayNueva: ${flag.hayNueva ? color.amarillo('si') : color.verde('no')}`);
81
+ console.log(` Flag: ${flagPath}`);
82
+ } catch {
83
+ console.log(` ${color.negrita('Hook automatico (check-update.js):')}`);
84
+ console.log(` ${color.dim('Sin flag de throttle — el hook nunca se ha ejecutado en este equipo,')}`);
85
+ console.log(` ${color.dim('o el archivo temp/swl-ses-update-check.json fue removido.')}`);
86
+ }
87
+ console.log('');
88
+
89
+ // Estado por runtime instalado
90
+ const runtimes = detectarRuntimes();
91
+ if (runtimes.length === 0) {
92
+ console.log(formatearAdvertencia('Runtimes', 'No se detectaron runtimes con instalacion SWL'));
93
+ console.log('');
94
+ return;
95
+ }
96
+
97
+ console.log(` ${color.negrita('Runtimes detectados:')}`);
98
+ let totalRuntimes = 0;
99
+ for (const runtime of runtimes) {
100
+ // Dedup + guard anti-fantasma (ver scopesReales): desde HOME el local
101
+ // colapsa con el global propio o con el de otro runtime.
102
+ for (const { dir, esGlobal } of scopesReales(runtime)) {
103
+ if (!fs.existsSync(dir)) continue;
104
+ const estado = cargarEstado(dir);
105
+ if (!estado) continue;
106
+
107
+ const ubicacion = esGlobal ? 'global' : 'proyecto';
108
+ const v = estado.versionSistema || 'desconocida';
109
+
110
+ const cmpRuntime = compararSemver(v, remota);
111
+ let etiqueta;
112
+ if (cmpRuntime === null) {
113
+ etiqueta = color.dim('(version no reconocida)');
114
+ } else if (cmpRuntime === 0) {
115
+ etiqueta = color.verde('al dia');
116
+ } else if (cmpRuntime < 0) {
117
+ etiqueta = color.amarillo(`atrasada (npm: ${remota})`);
118
+ } else {
119
+ etiqueta = color.dim('adelantada');
120
+ }
121
+
122
+ console.log(` ${runtime.nombre} (${ubicacion}): v${v} ${etiqueta}`);
123
+ totalRuntimes++;
124
+ }
125
+ }
126
+ if (totalRuntimes === 0) {
127
+ console.log(` ${color.dim('(ninguno con .swl-install-state.json valido)')}`);
128
+ }
129
+ console.log('');
130
+ console.log(` ${color.dim('Modo --check es read-only. Para actualizar:')} npx -y @saulwade/swl-ses@latest update`);
131
+ console.log('');
132
+ }
133
+
134
+ async function update(opciones) {
135
+ // Modo read-only: solo reportar estado, no tocar nada
136
+ if (opciones.check) {
137
+ await checkStatus();
138
+ return;
139
+ }
140
+
141
+ const force = opciones.force || !ES_TTY;
142
+
143
+ // Obtener la versión más reciente del registro npm
144
+ const VERSION = await obtenerVersionRemota();
145
+
146
+ console.log(encabezado('swl-ses update', VERSION_LOCAL));
147
+
148
+ // 1. Detectar runtimes
149
+ const runtimes = detectarRuntimes();
150
+
151
+ if (runtimes.length === 0) {
152
+ console.log(formatearAdvertencia('Runtimes', 'No se detectaron runtimes con instalación SWL'));
153
+ console.log(`\n Ejecuta primero: npx @saulwade/swl-ses@latest install --target claude`);
154
+ process.exit(1);
155
+ }
156
+
157
+ let actualizados = 0;
158
+ let omitidos = 0;
159
+
160
+ // 2. Iterar runtimes
161
+ for (const runtime of runtimes) {
162
+ // Dedup + guard anti-fantasma (ver scopesReales).
163
+ for (const { dir, esGlobal } of scopesReales(runtime)) {
164
+ if (!fs.existsSync(dir)) continue;
165
+
166
+ const estado = cargarEstado(dir);
167
+ if (!estado) continue;
168
+
169
+ const versionInstalada = estado.versionSistema || 'desconocida';
170
+ const ubicacion = esGlobal ? 'global' : 'proyecto';
171
+
172
+ const necesitaActualizar = versionInstalada !== VERSION;
173
+ const paqueteDesactualizado = VERSION_LOCAL !== VERSION;
174
+
175
+ console.log(` ${color.negrita(`Runtime: ${runtime.nombre}`)} ${color.dim(`(${ubicacion})`)}`);
176
+ console.log(` Versión instalada: ${necesitaActualizar ? color.amarillo(versionInstalada) : color.verde(versionInstalada)}`);
177
+ console.log(` Versión disponible: ${color.verde(VERSION)}${paqueteDesactualizado ? color.dim(` (ejecutando con v${VERSION_LOCAL})`) : ''}`);
178
+ console.log(` Perfil: ${estado.perfil}`);
179
+ console.log(` Componentes: ${estado.componentesInstalados.length}`);
180
+ console.log(` Archivos: ${estado.archivosInstalados.length}`);
181
+ console.log('');
182
+
183
+ // Si la versión disponible es más reciente pero el paquete ejecutándose es viejo,
184
+ // indicar al usuario que use @latest
185
+ if (paqueteDesactualizado && necesitaActualizar) {
186
+ console.log(formatearAdvertencia(runtime.nombre,
187
+ `Este paquete es v${VERSION_LOCAL} pero la v${VERSION} está disponible.\n` +
188
+ ` Para actualizar, ejecuta: npx @saulwade/swl-ses@latest update --force`));
189
+ console.log('');
190
+ omitidos++;
191
+ continue;
192
+ }
193
+
194
+ // Verificar si ya está actualizado
195
+ if (!necesitaActualizar && !force) {
196
+ console.log(formatearPaso(runtime.nombre, 'Ya está en la versión más reciente'));
197
+ console.log('');
198
+ omitidos++;
199
+ continue;
200
+ }
201
+
202
+ // Confirmar
203
+ if (!force) {
204
+ const confirmar = await preguntarSiNo(
205
+ !necesitaActualizar
206
+ ? `¿Re-instalar ${runtime.nombre} v${VERSION}?`
207
+ : `¿Actualizar ${runtime.nombre} de ${versionInstalada} a ${VERSION}?`
208
+ );
209
+ console.log('');
210
+
211
+ if (!confirmar) {
212
+ console.log(formatearAdvertencia(runtime.nombre, 'Actualización cancelada por el usuario'));
213
+ console.log('');
214
+ omitidos++;
215
+ continue;
216
+ }
217
+ }
218
+
219
+ // Ejecutar actualización con spinner
220
+ const sp = spinner(`Actualizando ${runtime.nombre}...`);
221
+
222
+ try {
223
+ const instalador = require('./instalador');
224
+ await instalador({
225
+ target: runtime.id,
226
+ profile: estado.perfil,
227
+ global: estado.global,
228
+ force: true,
229
+ });
230
+
231
+ sp.exito(`${runtime.nombre} actualizado a v${VERSION} (${estado.archivosInstalados.length} archivos)`);
232
+ actualizados++;
233
+ } catch (err) {
234
+ sp.fallo(`Error actualizando ${runtime.nombre}: ${err.message}`);
235
+ }
236
+
237
+ console.log('');
238
+ }
239
+ }
240
+
241
+ // 3. Reporte final
242
+ console.log(` ${lineaSeparadora()}`);
243
+ if (actualizados > 0) {
244
+ console.log(formatearPaso('Resultado', `${actualizados} runtime(s) actualizado(s)${omitidos > 0 ? `, ${omitidos} omitido(s)` : ''}`));
245
+ } else if (omitidos > 0) {
246
+ console.log(formatearPaso('Resultado', 'Todo está actualizado'));
247
+ } else {
248
+ console.log(formatearAdvertencia('Resultado', 'Ningún runtime fue actualizado'));
249
+ }
250
+ console.log('');
251
+ }
252
+
253
+ module.exports = update;
package/scripts/doctor.js CHANGED
@@ -10,7 +10,7 @@
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
12
 
13
- const { detectarRuntimes, detectarInstalacionesDuales } = require('./lib/detectar-runtime');
13
+ const { detectarRuntimes, detectarInstalacionesDuales, esDirGlobalDeAlgunRuntime, scopesReales } = require('./lib/detectar-runtime');
14
14
  const { cargarEstado, verificarIntegridad } = require('./lib/estado');
15
15
  const { verificarHooks, repararHooks, rutaSettings, cargarHooksConfig, desregistrarHooks, esHookSWL, verificarOptimizaciones, registrarOptimizaciones, validarEstructuraSettings } = require('./lib/hooks-settings');
16
16
  const {
@@ -106,7 +106,10 @@ async function doctor(opciones = {}) {
106
106
  try {
107
107
  const runtimesParaDrift = detectarRuntimes();
108
108
  for (const runtime of runtimesParaDrift) {
109
- for (const dir of [runtime.global, path.resolve(runtime.local)]) {
109
+ // Dedup + guard anti-fantasma: desde HOME, global y local colapsan al
110
+ // mismo dir (reporte doble) o el local resuelve al GLOBAL de otro
111
+ // runtime (drift "proyecto" espurio). Caso real doctor 2026-07-03.
112
+ for (const { dir, esGlobal } of scopesReales(runtime)) {
110
113
  if (!fs.existsSync(dir)) continue;
111
114
  const estado = cargarEstado(dir);
112
115
  if (!estado || !estado.versionSistema) continue;
@@ -115,7 +118,7 @@ async function doctor(opciones = {}) {
115
118
  driftsDetectados.push({
116
119
  runtime: runtime.nombre,
117
120
  runtimeId: runtime.id,
118
- scope: dir === runtime.global ? 'global' : 'proyecto',
121
+ scope: esGlobal ? 'global' : 'proyecto',
119
122
  dir,
120
123
  versionInstalada: estado.versionSistema,
121
124
  perfil: estado.perfil || 'desconocido',
@@ -259,13 +262,10 @@ async function doctor(opciones = {}) {
259
262
 
260
263
  // 5. Verificar estado de instalación por runtime
261
264
  for (const runtime of runtimes) {
262
- // Deduplicar: cuando global y local resuelven al mismo path (ej: running desde home dir)
263
- const dirsRaw = [runtime.global, path.resolve(runtime.local)];
264
- const dirs = [...new Set(dirsRaw)];
265
- for (const dir of dirs) {
265
+ // Deduplicar + descartar locales que resuelven al global de otro runtime
266
+ // (ej: doctor corrido desde el home dir)
267
+ for (const { dir, esGlobal } of scopesReales(runtime)) {
266
268
  if (!fs.existsSync(dir)) continue;
267
-
268
- const esGlobal = dir === runtime.global;
269
269
  const estado = cargarEstado(dir);
270
270
  if (estado) {
271
271
  const { integro, problemas } = verificarIntegridad(estado);
@@ -379,11 +379,7 @@ async function doctor(opciones = {}) {
379
379
  // Sub-fase 12 v1.5.0: deduplicar paths cuando cwd === homedir (runtime.local
380
380
  // y runtime.global colapsan al mismo directorio) — evita reporte doble.
381
381
  for (const runtime of runtimes) {
382
- const dirsUnicos = Array.from(new Set([
383
- path.resolve(runtime.global),
384
- path.resolve(runtime.local),
385
- ]));
386
- for (const dir of dirsUnicos) {
382
+ for (const { dir } of scopesReales(runtime)) {
387
383
  if (!fs.existsSync(dir)) continue;
388
384
  const estado = cargarEstado(dir);
389
385
  if (!estado || !estado.perfil) continue;
@@ -559,12 +555,20 @@ async function doctor(opciones = {}) {
559
555
  for (const runtime of runtimes) {
560
556
  const dir = path.resolve(runtime.local);
561
557
  if (!fs.existsSync(dir)) continue;
558
+ // Local que resuelve al global de OTRO runtime = scope fantasma (ver scopesReales)
559
+ if (esDirGlobalDeAlgunRuntime(dir) && dir !== path.resolve(runtime.global)) continue;
562
560
  const estado = cargarEstado(dir);
563
561
  if (!estado) continue;
564
562
 
565
563
  const settingsPath = rutaSettings(runtime, dir);
566
564
  if (fs.existsSync(settingsPath)) {
567
- const structResult = validarEstructuraSettings(settingsPath);
565
+ // Cursor usa hooks.json con eventos camelCase propios y entradas PLANAS
566
+ // ({command, type}) — el esquema Claude produce falsos errores
567
+ // ("afterFileEdit: evento no reconocido", "[0].hooks debe ser array").
568
+ // Codex también usa hooks.json pero con FORMA Claude ({matcher,
569
+ // hooks:[...]}, eventos PascalCase) — ver transformadores/codex.js.
570
+ const formatoHooks = runtime.id === 'cursor' ? 'plano' : 'claude';
571
+ const structResult = validarEstructuraSettings(settingsPath, { formatoHooks });
568
572
  if (structResult.ok) {
569
573
  console.log(formatearPaso(`Estructura settings ${runtime.nombre}`, 'Formato válido'));
570
574
  ok++;
@@ -624,8 +628,7 @@ async function doctor(opciones = {}) {
624
628
 
625
629
  // 8. Verificar componentes externos
626
630
  for (const runtime of runtimes) {
627
- const dirs = [runtime.global, path.resolve(runtime.local)];
628
- for (const dir of dirs) {
631
+ for (const { dir } of scopesReales(runtime)) {
629
632
  if (!fs.existsSync(dir)) continue;
630
633
  const estado = cargarEstado(dir);
631
634
  if (estado && estado.componentesExternos && estado.componentesExternos.length > 0) {
@@ -712,6 +715,11 @@ async function doctor(opciones = {}) {
712
715
  } else {
713
716
  console.log(` ${reparados} reparado(s), ${color.rojo(`${fallos} fallo(s)`)}.`);
714
717
  }
718
+ // Las reparaciones no se re-verifican en esta corrida: honestidad sobre
719
+ // el alcance (caso repair-loop 2026-07-03 — un falso positivo del check
720
+ // se "reparaba" y reaparecía indefinidamente).
721
+ console.log(` ${color.dim('Re-ejecuta `doctor` para confirmar. Si el MISMO problema reaparece tras reparar,')}`);
722
+ console.log(` ${color.dim('es un falso positivo del check (repórtalo) — no falta de reparación.')}`);
715
723
  } else {
716
724
  console.log(` ${color.dim('Reparación cancelada. Usa --force para reparar sin confirmar.')}`);
717
725
  }