@saulwade/swl-ses 2.3.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 +242 -240
- package/README.md +597 -597
- package/agentes/_intent-spec.md +73 -73
- package/agentes/_propose-step.md +90 -90
- package/agentes/accesibilidad-wcag-swl.md +690 -690
- package/agentes/arquitecto-swl.md +267 -267
- package/agentes/auto-evolucion-swl.md +908 -908
- package/agentes/backend-api-swl.md +472 -472
- package/agentes/backend-csharp-swl.md +420 -420
- package/agentes/backend-go-swl.md +390 -390
- package/agentes/backend-java-swl.md +281 -281
- package/agentes/backend-node-swl.md +479 -479
- package/agentes/backend-python-swl.md +605 -605
- package/agentes/backend-rust-swl.md +364 -364
- package/agentes/backend-workers-swl.md +482 -482
- package/agentes/cloud-infra-swl.md +509 -509
- package/agentes/consolidador-swl.md +541 -541
- package/agentes/datos-swl.md +605 -605
- package/agentes/depurador-swl.md +352 -352
- package/agentes/devops-ci-swl.md +400 -400
- package/agentes/disenador-ui-swl.md +569 -569
- package/agentes/documentador-swl.md +345 -345
- package/agentes/frontend-angular-swl.md +621 -621
- package/agentes/frontend-css-swl.md +716 -716
- package/agentes/frontend-react-swl.md +692 -692
- package/agentes/frontend-swl.md +496 -496
- package/agentes/frontend-tailwind-swl.md +826 -826
- package/agentes/gh-fix-ci-swl.md +275 -275
- package/agentes/implementador-swl.md +328 -328
- package/agentes/investigador-swl.md +432 -432
- package/agentes/investigador-ux-swl.md +505 -505
- package/agentes/llm-apps-swl.md +278 -278
- package/agentes/migrador-swl.md +442 -442
- package/agentes/mobile-android-swl.md +511 -511
- package/agentes/mobile-cross-swl.md +541 -541
- package/agentes/mobile-ios-swl.md +502 -502
- package/agentes/mobile-testing-swl.md +302 -302
- package/agentes/nemesis-auditor-swl.md +285 -285
- package/agentes/notificador-swl.md +918 -918
- package/agentes/observabilidad-swl.md +438 -438
- package/agentes/orquestador-swl.md +1053 -1026
- package/agentes/pagos-swl.md +310 -310
- package/agentes/perfilador-usuario-swl.md +321 -321
- package/agentes/planificador-swl.md +399 -399
- package/agentes/producto-prd-swl.md +589 -589
- package/agentes/red-team-swl.md +218 -218
- package/agentes/release-manager-swl.md +590 -590
- package/agentes/rendimiento-swl.md +713 -713
- package/agentes/resolutor-build-swl.md +245 -245
- package/agentes/revisor-angular-swl.md +278 -278
- package/agentes/revisor-codigo-swl.md +538 -505
- package/agentes/revisor-csharp-swl.md +264 -264
- package/agentes/revisor-go-swl.md +259 -259
- package/agentes/revisor-java-swl.md +257 -257
- package/agentes/revisor-kotlin-swl.md +273 -273
- package/agentes/revisor-nextjs-swl.md +281 -281
- package/agentes/revisor-php-swl.md +271 -271
- package/agentes/revisor-react-swl.md +278 -278
- package/agentes/revisor-rust-swl.md +346 -346
- package/agentes/revisor-seguridad-swl.md +399 -399
- package/agentes/revisor-swift-swl.md +268 -268
- package/agentes/revisor-typescript-swl.md +346 -346
- package/agentes/sre-swl.md +291 -291
- package/agentes/tdd-qa-swl.md +393 -393
- package/comandos/swl/contexto.md +0 -2
- package/comandos/swl/deuda-codigo.md +97 -0
- package/habilidades/angular-moderno/SKILL.md +5 -1
- package/habilidades/contenedores-docker/SKILL.md +3 -1
- package/habilidades/ejecutar-task-iterativo/SKILL.md +278 -278
- package/habilidades/harness-claude-code/SKILL.md +5 -4
- package/habilidades/harness-claude-code/recursos/disciplina-harness-regla.md +6 -5
- package/habilidades/legacy-code-rescue/SKILL.md +2 -1
- package/habilidades/meta-skills-estandar/SKILL.md +1 -1
- package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +2 -2
- package/habilidades/postgresql-experto/SKILL.md +3 -1
- package/habilidades/prevencion-sobreingenieria/SKILL.md +70 -92
- package/habilidades/prevencion-sobreingenieria/recursos/soluciones-nativas.md +166 -0
- package/habilidades/prevencion-sobreingenieria/recursos/variables-residuales-post-refactor.md +85 -0
- package/habilidades/tdd-workflow/SKILL.md +3 -1
- package/hooks/audit-trail.js +4 -4
- package/hooks/calidad-pre-commit.js +15 -11
- package/hooks/captura-acciones-post.js +3 -2
- package/hooks/captura-acciones-session.js +3 -2
- package/hooks/contexto-iteracion.js +2 -1
- package/hooks/contexto-subagente.js +68 -0
- package/hooks/guardrail-modelo.js +13 -3
- package/hooks/inyeccion-contexto.js +12 -12
- package/hooks/registro-turnos.js +5 -4
- package/hooks/spec-gate.js +2 -1
- package/hooks/sugerir-contribuir.js +2 -1
- package/hooks/sugerir-regenerar-inventario.js +3 -2
- package/hooks/tdd-gate.js +2 -1
- package/llms.txt +3 -3
- package/manifiestos/canonical-hashes.json +995 -10
- package/manifiestos/hook-profiles.json +0 -2
- package/manifiestos/hooks-config.json +469 -477
- package/manifiestos/invariantes-criticos.json +30 -0
- package/manifiestos/modulos.json +1390 -1390
- package/manifiestos/skills-lock.json +24 -24
- package/package.json +93 -93
- package/plugin.json +369 -371
- package/reglas/arreglar-al-detectar.md +16 -0
- package/reglas/pruebas.md +7 -3
- package/schemas/agent-frontmatter.schema.json +3 -1
- package/scripts/actualizar.js +253 -254
- package/scripts/doctor.js +25 -17
- package/scripts/instalador.js +4 -1
- package/scripts/lib/detectar-runtime.js +58 -0
- package/scripts/lib/expandir-targets.js +71 -71
- package/scripts/lib/hooks-settings.js +40 -3
- package/scripts/lib/preservar-usuario.js +39 -5
- package/scripts/lib/toml-merge.js +204 -204
- package/scripts/mcp-server/auth.js +105 -105
- package/scripts/mcp-server/cache.js +106 -106
- 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
- package/scripts/validar.js +1 -1
- package/scripts/verificar-release.js +51 -0
- package/hooks/lib/context-compressor.js +0 -657
- package/hooks/linea-estado.js +0 -328
- package/hooks/monitor-contexto.js +0 -373
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
|
-
|
|
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:
|
|
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
|
|
263
|
-
|
|
264
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/scripts/instalador.js
CHANGED
|
@@ -26,6 +26,7 @@ const { obtenerTransformador } = require('./lib/transformadores');
|
|
|
26
26
|
const {
|
|
27
27
|
esProtegido,
|
|
28
28
|
crearBackup,
|
|
29
|
+
rutaBackupSegura,
|
|
29
30
|
registrarBackup,
|
|
30
31
|
limpiarBackups,
|
|
31
32
|
mergeInstintos,
|
|
@@ -1245,7 +1246,9 @@ function instalarArchivo(archivo, rutas, runtime, opciones = {}) {
|
|
|
1245
1246
|
// ALTO (nemesis O6): respaldar el DIRECTORIO COMPLETO (recursos incluidos)
|
|
1246
1247
|
// antes de sobrescribir — el usuario pudo modificar recursos/ aunque no
|
|
1247
1248
|
// tocara el SKILL.md. Overwrite controlado en-bloque (no fall-through).
|
|
1248
|
-
|
|
1249
|
+
// rutaBackupSegura contiene el backup dentro del dir versionado incluso
|
|
1250
|
+
// cuando destino está fuera del cwd (install --global — bug 2026-07-03).
|
|
1251
|
+
const backupDir = rutaBackupSegura(process.cwd(), versionBackup, destino);
|
|
1249
1252
|
let backupOk = false;
|
|
1250
1253
|
try { copiarDirectorio(destino, backupDir); backupOk = true; } catch (e) { console.log(` ⚠ Backup de directorio falló (${e.message})`); }
|
|
1251
1254
|
if (!backupOk) {
|
|
@@ -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,
|
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Expansión de --target (CSV) y --all-runtimes para multi-target install.
|
|
5
|
-
*
|
|
6
|
-
* ADR-0019 Sub-fase 2.5.
|
|
7
|
-
*
|
|
8
|
-
* Extraído de bin/swl-ses.js para permitir tests unitarios. NUNCA debe tener
|
|
9
|
-
* side effects fuera de logging — solo manipula la lista de strings.
|
|
10
|
-
*
|
|
11
|
-
* @module scripts/lib/expandir-targets
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
const { listarRuntimesInstalables, RUNTIMES } = require('./detectar-runtime');
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Expande las opciones del CLI a un array de target IDs.
|
|
18
|
-
*
|
|
19
|
-
* Reglas:
|
|
20
|
-
* - `all_runtimes` tiene prioridad sobre `target`. Si ambos vienen, se loggea
|
|
21
|
-
* aviso (vía `logger.warn`) y se usa `all_runtimes`.
|
|
22
|
-
* - `target='a,b,c'` → ['a','b','c'].
|
|
23
|
-
* - `target='claude'` → ['claude'].
|
|
24
|
-
* - Sin nada → ['claude'] (default histórico, backward-compat).
|
|
25
|
-
* - Duplicados se deduplican preservando el primer orden.
|
|
26
|
-
* - Targets desconocidos se omiten con aviso (logger.warn).
|
|
27
|
-
*
|
|
28
|
-
* @param {object} opciones - Objeto con `target` (string|undefined) y `all_runtimes` (bool).
|
|
29
|
-
* @param {object} [logger] - Sustituible para tests. Default console.
|
|
30
|
-
* @returns {{ targets: string[], omitidos: string[], errores: string[] }}
|
|
31
|
-
*/
|
|
32
|
-
function expandirTargets(opciones, logger) {
|
|
33
|
-
const log = logger || console;
|
|
34
|
-
const omitidos = [];
|
|
35
|
-
const errores = [];
|
|
36
|
-
|
|
37
|
-
let candidatos;
|
|
38
|
-
if (opciones.all_runtimes) {
|
|
39
|
-
if (opciones.target && typeof opciones.target === 'string') {
|
|
40
|
-
log.warn && log.warn('[expandir-targets] --all-runtimes tiene prioridad sobre --target; se ignora --target.');
|
|
41
|
-
}
|
|
42
|
-
candidatos = listarRuntimesInstalables();
|
|
43
|
-
} else if (typeof opciones.target === 'string' && opciones.target.includes(',')) {
|
|
44
|
-
candidatos = opciones.target.split(',').map(s => s.trim()).filter(Boolean);
|
|
45
|
-
} else if (typeof opciones.target === 'string' && opciones.target.length > 0) {
|
|
46
|
-
candidatos = [opciones.target.trim()];
|
|
47
|
-
} else {
|
|
48
|
-
candidatos = ['claude']; // backward-compat
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const visto = new Set();
|
|
52
|
-
const targets = [];
|
|
53
|
-
for (const t of candidatos) {
|
|
54
|
-
if (visto.has(t)) continue;
|
|
55
|
-
visto.add(t);
|
|
56
|
-
if (!RUNTIMES[t]) {
|
|
57
|
-
log.warn && log.warn(`[expandir-targets] Target desconocido omitido: "${t}". Disponibles: ${Object.keys(RUNTIMES).join(', ')}`);
|
|
58
|
-
omitidos.push(t);
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
targets.push(t);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (targets.length === 0) {
|
|
65
|
-
errores.push('Ningún target válido tras expansión.');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return { targets, omitidos, errores };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
module.exports = { expandirTargets };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Expansión de --target (CSV) y --all-runtimes para multi-target install.
|
|
5
|
+
*
|
|
6
|
+
* ADR-0019 Sub-fase 2.5.
|
|
7
|
+
*
|
|
8
|
+
* Extraído de bin/swl-ses.js para permitir tests unitarios. NUNCA debe tener
|
|
9
|
+
* side effects fuera de logging — solo manipula la lista de strings.
|
|
10
|
+
*
|
|
11
|
+
* @module scripts/lib/expandir-targets
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { listarRuntimesInstalables, RUNTIMES } = require('./detectar-runtime');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Expande las opciones del CLI a un array de target IDs.
|
|
18
|
+
*
|
|
19
|
+
* Reglas:
|
|
20
|
+
* - `all_runtimes` tiene prioridad sobre `target`. Si ambos vienen, se loggea
|
|
21
|
+
* aviso (vía `logger.warn`) y se usa `all_runtimes`.
|
|
22
|
+
* - `target='a,b,c'` → ['a','b','c'].
|
|
23
|
+
* - `target='claude'` → ['claude'].
|
|
24
|
+
* - Sin nada → ['claude'] (default histórico, backward-compat).
|
|
25
|
+
* - Duplicados se deduplican preservando el primer orden.
|
|
26
|
+
* - Targets desconocidos se omiten con aviso (logger.warn).
|
|
27
|
+
*
|
|
28
|
+
* @param {object} opciones - Objeto con `target` (string|undefined) y `all_runtimes` (bool).
|
|
29
|
+
* @param {object} [logger] - Sustituible para tests. Default console.
|
|
30
|
+
* @returns {{ targets: string[], omitidos: string[], errores: string[] }}
|
|
31
|
+
*/
|
|
32
|
+
function expandirTargets(opciones, logger) {
|
|
33
|
+
const log = logger || console;
|
|
34
|
+
const omitidos = [];
|
|
35
|
+
const errores = [];
|
|
36
|
+
|
|
37
|
+
let candidatos;
|
|
38
|
+
if (opciones.all_runtimes) {
|
|
39
|
+
if (opciones.target && typeof opciones.target === 'string') {
|
|
40
|
+
log.warn && log.warn('[expandir-targets] --all-runtimes tiene prioridad sobre --target; se ignora --target.');
|
|
41
|
+
}
|
|
42
|
+
candidatos = listarRuntimesInstalables();
|
|
43
|
+
} else if (typeof opciones.target === 'string' && opciones.target.includes(',')) {
|
|
44
|
+
candidatos = opciones.target.split(',').map(s => s.trim()).filter(Boolean);
|
|
45
|
+
} else if (typeof opciones.target === 'string' && opciones.target.length > 0) {
|
|
46
|
+
candidatos = [opciones.target.trim()];
|
|
47
|
+
} else {
|
|
48
|
+
candidatos = ['claude']; // backward-compat
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const visto = new Set();
|
|
52
|
+
const targets = [];
|
|
53
|
+
for (const t of candidatos) {
|
|
54
|
+
if (visto.has(t)) continue;
|
|
55
|
+
visto.add(t);
|
|
56
|
+
if (!RUNTIMES[t]) {
|
|
57
|
+
log.warn && log.warn(`[expandir-targets] Target desconocido omitido: "${t}". Disponibles: ${Object.keys(RUNTIMES).join(', ')}`);
|
|
58
|
+
omitidos.push(t);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
targets.push(t);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (targets.length === 0) {
|
|
65
|
+
errores.push('Ningún target válido tras expansión.');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { targets, omitidos, errores };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { expandirTargets };
|
|
@@ -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)) {
|
|
@@ -34,7 +34,7 @@ const path = require('path');
|
|
|
34
34
|
* - /swl:evolucionar → skills auto-generados en habilidades/
|
|
35
35
|
* - /swl:crear-skill → skills nuevos en habilidades/
|
|
36
36
|
* - session-store → .planning/sessions/*.json
|
|
37
|
-
* -
|
|
37
|
+
* - /swl:compactar → .planning/COMPACTACION.md
|
|
38
38
|
* - cron-store → .planning/cron/
|
|
39
39
|
* - gateway → .planning/gateway-sessions.json
|
|
40
40
|
* - agent-comms → .planning/comms/
|
|
@@ -181,6 +181,41 @@ function dirBackup(baseDir, version) {
|
|
|
181
181
|
return path.join(baseDir, '.planning', 'backups', `v${version}`);
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Subruta segura de un archivo DENTRO del directorio de backups.
|
|
186
|
+
*
|
|
187
|
+
* Un destino fuera de baseDir (install --global: ~/.claude, ~/.openclaude)
|
|
188
|
+
* produce un path.relative con segmentos '..' que ESCAPAN del directorio
|
|
189
|
+
* versionado: `.planning/backups/v2.3.1/../../.claude/...` colapsa a
|
|
190
|
+
* `.planning/.claude/...` (bug 2026-07-03: 1147 backups globales aterrizaron
|
|
191
|
+
* fuera del dir de backups y aparecieron como archivos nuevos en el repo).
|
|
192
|
+
* Se neutralizan '..', '.' y dos puntos de unidad preservando la estructura.
|
|
193
|
+
*
|
|
194
|
+
* @param {string} baseDir - Raíz del proyecto.
|
|
195
|
+
* @param {string} rutaAbsoluta - Ruta absoluta del archivo a respaldar.
|
|
196
|
+
* @returns {string} Subruta relativa contenida (sin '..').
|
|
197
|
+
*/
|
|
198
|
+
function subrutaBackupSegura(baseDir, rutaAbsoluta) {
|
|
199
|
+
return path.relative(baseDir, rutaAbsoluta)
|
|
200
|
+
.split(/[\\/]/)
|
|
201
|
+
.filter(s => s && s !== '..' && s !== '.')
|
|
202
|
+
.map(s => s.replace(/:/g, ''))
|
|
203
|
+
.join(path.sep);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Ruta completa de backup para un archivo, garantizada dentro del
|
|
208
|
+
* directorio versionado de backups.
|
|
209
|
+
*
|
|
210
|
+
* @param {string} baseDir - Raíz del proyecto.
|
|
211
|
+
* @param {string} versionAnterior - Versión que se está sobreescribiendo.
|
|
212
|
+
* @param {string} rutaAbsoluta - Ruta absoluta del archivo a respaldar.
|
|
213
|
+
* @returns {string} Ruta absoluta del backup.
|
|
214
|
+
*/
|
|
215
|
+
function rutaBackupSegura(baseDir, versionAnterior, rutaAbsoluta) {
|
|
216
|
+
return path.join(dirBackup(baseDir, versionAnterior), subrutaBackupSegura(baseDir, rutaAbsoluta));
|
|
217
|
+
}
|
|
218
|
+
|
|
184
219
|
/**
|
|
185
220
|
* Crea un backup de un archivo antes de sobreescribirlo.
|
|
186
221
|
*
|
|
@@ -195,10 +230,8 @@ function crearBackup(baseDir, rutaAbsoluta, versionAnterior) {
|
|
|
195
230
|
}
|
|
196
231
|
|
|
197
232
|
try {
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
const rutaBackup = path.join(backupDir, rutaRelativa);
|
|
201
|
-
const dirPadre = path.dirname(rutaBackup);
|
|
233
|
+
const rutaBackup = rutaBackupSegura(baseDir, versionAnterior, rutaAbsoluta);
|
|
234
|
+
const dirPadre = path.dirname(rutaBackup);
|
|
202
235
|
|
|
203
236
|
if (!fs.existsSync(dirPadre)) {
|
|
204
237
|
fs.mkdirSync(dirPadre, { recursive: true });
|
|
@@ -573,6 +606,7 @@ module.exports = {
|
|
|
573
606
|
|
|
574
607
|
// Nivel 2: Backup
|
|
575
608
|
crearBackup,
|
|
609
|
+
rutaBackupSegura,
|
|
576
610
|
registrarBackup,
|
|
577
611
|
listarBackups,
|
|
578
612
|
restaurarBackup,
|