@saulwade/swl-ses 1.5.0 → 1.5.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.
Files changed (134) hide show
  1. package/CLAUDE.md +19 -2
  2. package/README.md +561 -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/comandos/swl/.evolved.json +22 -22
  7. package/comandos/swl/contribuir.md +233 -233
  8. package/comandos/swl/nemesis.md +230 -56
  9. package/gateway/lib/event-channel.js +191 -191
  10. package/habilidades/backend-production-resilience/SKILL.md +288 -288
  11. package/habilidades/benchmark-memoria/SKILL.md +186 -186
  12. package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
  13. package/habilidades/doubt-driven-review/SKILL.md +171 -171
  14. package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
  15. package/habilidades/ejecutar-task-iterativo/SKILL.md +278 -278
  16. package/habilidades/eval-framework/SKILL.md +212 -212
  17. package/habilidades/feynman-auditor-swl/SKILL.md +123 -123
  18. package/habilidades/feynman-auditor-swl/recursos/preguntas-language-agnostic.md +108 -108
  19. package/habilidades/harness-claude-code/SKILL.md +299 -299
  20. package/habilidades/infra-github-actions/SKILL.md +166 -166
  21. package/habilidades/legacy-code-rescue/SKILL.md +267 -267
  22. package/habilidades/manejo-errores/.evolved.json +8 -8
  23. package/habilidades/meta-skills-estandar/SKILL.md +225 -1
  24. package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
  25. package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
  26. package/habilidades/nemesis-evaluacion-json/SKILL.md +266 -0
  27. package/habilidades/nemesis-redistribuir/SKILL.md +341 -0
  28. package/habilidades/node-experto/SKILL.md +105 -4
  29. package/habilidades/patrones-python/SKILL.md +229 -229
  30. package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
  31. package/habilidades/planear-fase/SKILL.md +319 -319
  32. package/habilidades/protocolo-revision-swl/SKILL.md +350 -276
  33. package/habilidades/release-semver/.evolved.json +8 -8
  34. package/habilidades/state-inconsistency-auditor-swl/SKILL.md +166 -166
  35. package/habilidades/state-inconsistency-auditor-swl/recursos/coupled-state-patterns.md +147 -147
  36. package/habilidades/tdd-workflow/SKILL.md +150 -4
  37. package/habilidades/testing-python/SKILL.md +340 -340
  38. package/habilidades/verificar-trabajo/SKILL.md +8 -3
  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/lib/agent-routing.js +107 -107
  43. package/hooks/lib/auto-consolidator.js +335 -335
  44. package/hooks/lib/error-classifier.js +308 -308
  45. package/hooks/lib/merkle-audit.js +96 -96
  46. package/hooks/lib/provenance-tracker.js +191 -191
  47. package/hooks/lib/rate-limit-tracker.js +253 -253
  48. package/hooks/lib/resource-quota.js +122 -122
  49. package/hooks/lib/retry-jitter.js +165 -165
  50. package/hooks/lib/security-net.js +201 -201
  51. package/hooks/lib/skill-auditor.js +588 -588
  52. package/hooks/lib/sync-status.js +228 -228
  53. package/hooks/lib/taint-tracker.js +107 -107
  54. package/hooks/lib/text-similarity.js +241 -241
  55. package/hooks/lib/toon-compressor.js +245 -245
  56. package/hooks/registro-turnos.js +209 -209
  57. package/hooks/sugerir-regenerar-inventario.js +170 -170
  58. package/hooks/validar-formato-post-subagente.js +140 -140
  59. package/hooks/validar-memoria-hook.js +218 -218
  60. package/instintos/prompt-appendices.yaml +57 -57
  61. package/manifiestos/agent-output-schemas.json +57 -57
  62. package/manifiestos/modulos.json +1324 -1321
  63. package/manifiestos/skills-lock.json +1114 -1114
  64. package/package.json +2 -2
  65. package/plantillas/auditor-veto-template.md +105 -105
  66. package/plantillas/github-workflows/README.md +47 -47
  67. package/plantillas/github-workflows/release-please.yml +44 -44
  68. package/plantillas/github-workflows/swl-ci.yml +107 -107
  69. package/plantillas/github-workflows/swl-security.yml +51 -51
  70. package/plugin.json +353 -351
  71. package/reglas/analisis-previo-tareas-grandes.md +172 -172
  72. package/reglas/arreglar-al-detectar.md +147 -147
  73. package/reglas/fragmentos-compartidos.md +152 -152
  74. package/reglas/harness-claude-code.md +213 -213
  75. package/reglas/registro-componentes-nuevos.md +192 -0
  76. package/reglas/usar-context7.md +226 -226
  77. package/schemas/diary-entry.schema.json +80 -80
  78. package/scripts/actualizar.js +110 -1
  79. package/scripts/audit-tools/audit-history.js +330 -330
  80. package/scripts/audit-tools/bundle-tracker.js +290 -290
  81. package/scripts/audit-tools/canary-monitor.js +352 -352
  82. package/scripts/audit-tools/code-profiler.js +605 -605
  83. package/scripts/audit-tools/dep-doctor.js +320 -320
  84. package/scripts/audit-tools/env-validator.js +206 -206
  85. package/scripts/audit-tools/lib/fs-walk.js +48 -48
  86. package/scripts/audit-tools/lib/output.js +23 -23
  87. package/scripts/audit-tools/migration-checker.js +392 -392
  88. package/scripts/audit-tools/pentest-scanner.js +1436 -1436
  89. package/scripts/benchmark-memoria.js +167 -167
  90. package/scripts/configurar-branch-protection.js +418 -418
  91. package/scripts/derivar-feature-list.js +489 -489
  92. package/scripts/detectar-aprendizajes-duplicados.js +151 -151
  93. package/scripts/doctor.js +58 -4
  94. package/scripts/field-report.js +199 -199
  95. package/scripts/generar-checklists-consolidados.js +273 -273
  96. package/scripts/generar-inventario.js +420 -420
  97. package/scripts/generar-matriz-lenguajes.js +271 -271
  98. package/scripts/lib/artefactos-python.js +43 -43
  99. package/scripts/lib/benchmark-metrics.js +160 -160
  100. package/scripts/lib/budget-enforcer.js +252 -252
  101. package/scripts/lib/configurar-ci.js +380 -380
  102. package/scripts/lib/contadores-inventario.js +217 -217
  103. package/scripts/lib/detectar-stack-detallado.js +307 -307
  104. package/scripts/lib/diary-entry.js +234 -234
  105. package/scripts/lib/eval-metrics-store.js +218 -218
  106. package/scripts/lib/eval-quality.js +171 -171
  107. package/scripts/lib/eval-schemas.js +144 -144
  108. package/scripts/lib/eval-self-correct.js +106 -106
  109. package/scripts/lib/eval-validator.js +185 -185
  110. package/scripts/lib/expandir-targets.js +71 -71
  111. package/scripts/lib/jaccard-similarity.js +98 -98
  112. package/scripts/lib/longmemeval-runner.js +125 -125
  113. package/scripts/lib/mcp_config.py +127 -0
  114. package/scripts/lib/npm-version.js +261 -261
  115. package/scripts/lib/paquetes-conocidos.js +50 -50
  116. package/scripts/lib/prompt-builder.js +264 -264
  117. package/scripts/lib/rrf-fusion.js +175 -175
  118. package/scripts/lib/scoring-instintos.js +277 -277
  119. package/scripts/lib/semantic-search.js +252 -252
  120. package/scripts/lib/toml-merge.js +204 -204
  121. package/scripts/lib/transformadores/codex.js +375 -375
  122. package/scripts/lib/transformadores/cursor.js +359 -359
  123. package/scripts/limpiar-artefactos-python.js +131 -131
  124. package/scripts/mcp-orchestrator.py +8 -18
  125. package/scripts/mcp-pool-manager.py +12 -23
  126. package/scripts/mcp-server/README.md +170 -170
  127. package/scripts/mcp-server/auth.js +105 -105
  128. package/scripts/mcp-server/cache.js +106 -106
  129. package/scripts/mcp-server/telemetry.js +78 -78
  130. package/scripts/migrar-csv-a-array.js +168 -168
  131. package/scripts/migrar-fase-dominio.js +201 -201
  132. package/scripts/publicar.js +511 -511
  133. package/scripts/run-eval.js +141 -141
  134. package/scripts/validar-userland-vacio.js +110 -110
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env python3
2
+ """mcp_config.py — Carga unificada de configuracion MCP con deep merge.
3
+
4
+ Replica la semantica de fusion de Claude Code: combina por clave los archivos
5
+ mcp-servers.json -> .claude/settings.json -> .claude/settings.local.json
6
+ (el ultimo gana por clave, env se mergea por sub-clave). Asi un equipo puede
7
+ mantener `command` y `args` en settings.json versionado y override solo el
8
+ `env` (con credenciales por maquina) en settings.local.json gitignored.
9
+
10
+ Uso programatico:
11
+ from mcp_config import cargar_config_mcp
12
+ servers = cargar_config_mcp(Path('.'))
13
+
14
+ Uso CLI (para tests y debugging):
15
+ python scripts/lib/mcp_config.py [cwd]
16
+ -> imprime json con el mapa de servidores fusionado
17
+
18
+ Origen: fix bug donde mcp-orchestrator.py / mcp-pool-manager.py hacian
19
+ first-wins y rompian /swl:mcp-status cuando settings.local.json contenia
20
+ solo overrides parciales. Coherente con la regla del repo en CLAUDE.md
21
+ seccion "Code style": settings.local.json sobrescribe por key, no reemplaza.
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ import sys
27
+ from pathlib import Path
28
+ from typing import Any
29
+
30
+ # Orden de capas: base -> override. La ultima capa gana por clave al hacer
31
+ # merge, igual que el deep merge interno de Claude Code.
32
+ CAPAS_DEFAULT = (
33
+ 'mcp-servers.json',
34
+ '.claude/settings.json',
35
+ '.claude/settings.local.json',
36
+ )
37
+
38
+
39
+ def _leer_servers(ruta: Path) -> dict:
40
+ """Lee un archivo JSON y devuelve su seccion mcpServers, o {} si falla.
41
+
42
+ Tolera archivos inexistentes, JSON invalido y archivos sin la clave
43
+ mcpServers. NUNCA levanta — la fusion debe ser robusta a capas faltantes.
44
+ """
45
+ if not ruta.exists():
46
+ return {}
47
+ try:
48
+ data = json.loads(ruta.read_text(encoding='utf-8'))
49
+ except (OSError, json.JSONDecodeError):
50
+ return {}
51
+ servers = data.get('mcpServers')
52
+ if not isinstance(servers, dict):
53
+ return {}
54
+ return servers
55
+
56
+
57
+ def _fusionar_servidor(base: dict, override: dict) -> dict:
58
+ """Combina dos definiciones de servidor.
59
+
60
+ Reglas:
61
+ - Claves escalares de `override` reemplazan a `base` (command, cwd, url).
62
+ - `env` se mergea por sub-clave: el override anade o sobrescribe variables
63
+ sin perder las que solo existen en `base`.
64
+ - `args` reemplaza completamente si el override lo define (lista entera,
65
+ no merge — para no producir listas ambiguas).
66
+ - Cualquier otro dict anidado se mergea superficial (override gana).
67
+ """
68
+ if not isinstance(base, dict) or not isinstance(override, dict):
69
+ return override if isinstance(override, dict) else base
70
+
71
+ resultado: dict[str, Any] = dict(base)
72
+
73
+ for clave, valor in override.items():
74
+ if clave == 'env' and isinstance(valor, dict):
75
+ env_base = resultado.get('env') if isinstance(resultado.get('env'), dict) else {}
76
+ resultado['env'] = {**env_base, **valor}
77
+ elif clave == 'args':
78
+ # args reemplaza, no mergea: una lista parcial seria peligrosa.
79
+ resultado['args'] = valor
80
+ elif isinstance(valor, dict) and isinstance(resultado.get(clave), dict):
81
+ resultado[clave] = {**resultado[clave], **valor}
82
+ else:
83
+ resultado[clave] = valor
84
+
85
+ return resultado
86
+
87
+
88
+ def cargar_config_mcp(cwd: Path, config_path: str | None = None) -> dict:
89
+ """Devuelve el mapa de servidores MCP fusionado siguiendo CAPAS_DEFAULT.
90
+
91
+ Si `config_path` se especifica, solo se lee ese archivo (modo override
92
+ completo para CI o tests). De lo contrario se aplican las tres capas.
93
+
94
+ Garantia: el dict devuelto contiene TODOS los servidores definidos en
95
+ cualquier capa. Las capas mas profundas refinan (no reemplazan) la
96
+ definicion de cada servidor por clave.
97
+ """
98
+ if config_path:
99
+ return _leer_servers(Path(config_path))
100
+
101
+ fusionado: dict[str, dict] = {}
102
+ for nombre_capa in CAPAS_DEFAULT:
103
+ capa = _leer_servers(cwd / nombre_capa)
104
+ for nombre_servidor, cfg in capa.items():
105
+ if nombre_servidor in fusionado:
106
+ fusionado[nombre_servidor] = _fusionar_servidor(
107
+ fusionado[nombre_servidor], cfg
108
+ )
109
+ else:
110
+ fusionado[nombre_servidor] = cfg if isinstance(cfg, dict) else {}
111
+ return fusionado
112
+
113
+
114
+ def _main(argv: list[str]) -> int:
115
+ """Entry point CLI: imprime el JSON fusionado para uso en tests."""
116
+ cwd = Path(argv[0]) if argv else Path('.')
117
+ if not cwd.is_dir():
118
+ sys.stderr.write(f'[mcp_config] CWD no es directorio: {cwd}\n')
119
+ return 1
120
+ resultado = cargar_config_mcp(cwd)
121
+ sys.stdout.write(json.dumps(resultado, indent=2, ensure_ascii=False))
122
+ sys.stdout.write('\n')
123
+ return 0
124
+
125
+
126
+ if __name__ == '__main__':
127
+ sys.exit(_main(sys.argv[1:]))
@@ -1,261 +1,261 @@
1
- 'use strict';
2
-
3
- /**
4
- * npm-version.js — utilidades compartidas para consultar versiones en npm.
5
- *
6
- * Lib usada por scripts/check-update.js, hooks/check-update.js,
7
- * scripts/doctor.js y scripts/actualizar.js. Consolida tres preocupaciones
8
- * que estaban duplicadas en los 4 archivos:
9
- * 1. Aumento de PATH (necesario en git-bash Windows donde el PATH
10
- * heredado no incluye node/npm).
11
- * 2. Consulta paralela a npm view de los nombres conocidos del paquete.
12
- * 3. Comparación robusta de versiones SemVer con soporte para
13
- * pre-release/build metadata (5.7.5-beta.1+abc).
14
- *
15
- * Por seguridad usa execFile (sin shell) en lugar de execSync con shell:true.
16
- */
17
-
18
- const fs = require('fs');
19
- const path = require('path');
20
- const os = require('os');
21
- const { execFile, execFileSync } = require('child_process');
22
-
23
- const { NOMBRES_DETECCION } = require('./paquetes-conocidos');
24
-
25
- const TIMEOUT_DEFAULT_MS = 10_000;
26
-
27
- /**
28
- * Resuelve la ruta absoluta a `npm-cli.js` que viene con la instalación
29
- * de Node. En Windows `npm.cmd` y en POSIX `npm` son shims que terminan
30
- * invocando este JS con el binario de Node. Llamamos directamente a Node
31
- * con el script para evitar:
32
- * 1. La diferencia .cmd vs binario nativo entre Windows y POSIX.
33
- * 2. El requerimiento de `shell: true` en Node 20+ para ejecutar .cmd
34
- * (que dispara DEP0190 cuando hay args, aunque estén validados).
35
- *
36
- * Si no se encuentra (ej. instalación no estándar), devuelve null y
37
- * caemos al fallback con shell.
38
- */
39
- function localizarNpmCli() {
40
- const dir = path.dirname(process.execPath);
41
- const candidatos = [
42
- path.join(dir, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
43
- path.join(dir, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
44
- path.join(os.homedir(), 'AppData', 'Roaming', 'npm', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
45
- ];
46
- for (const c of candidatos) {
47
- try {
48
- if (fs.existsSync(c)) return c;
49
- } catch { /* continuar */ }
50
- }
51
- return null;
52
- }
53
-
54
- const NPM_CLI_JS = localizarNpmCli();
55
-
56
- if (!NPM_CLI_JS) {
57
- // Si no encontramos npm-cli.js (instalación de Node atípica), emitimos
58
- // un aviso pero NO caemos a `shell: true` — eso reintroduciría DEP0190
59
- // y exigiría confiar en el regex como única defensa contra inyección.
60
- // En su lugar, los callers reciben null/error y deciden cómo continuar.
61
- process.stderr.write(
62
- `[npm-version] aviso: no se encontró npm-cli.js junto a node ` +
63
- `(${process.execPath}). Las consultas a npm view fallarán hasta que ` +
64
- `node esté instalado con npm bundled (instalación oficial).\n`
65
- );
66
- }
67
-
68
- /**
69
- * Whitelist de nombres de paquete: scope opcional + nombre alfanumérico
70
- * con guiones/puntos/underscore. Rechaza espacios, comillas, $, |, ;, etc.
71
- * Defensa en profundidad — actualmente los nombres son constantes del
72
- * código, pero esta validación protege ante regresiones futuras donde
73
- * el nombre venga de variable de entorno o input externo.
74
- */
75
- const REGEX_NOMBRE_PAQUETE_SEGURO = /^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
76
-
77
- function asegurarNombrePaqueteSeguro(pkg) {
78
- if (typeof pkg !== 'string' || !REGEX_NOMBRE_PAQUETE_SEGURO.test(pkg)) {
79
- throw new Error(`nombre de paquete inválido: ${JSON.stringify(pkg)}`);
80
- }
81
- }
82
-
83
- /**
84
- * Construye [bin, args] para invocar npm de forma segura:
85
- * - Si encontramos npm-cli.js, usa el binario de Node directamente
86
- * (cross-platform, sin shell, sin DEP0190).
87
- * - Si no se encontró, lanza error explícito en lugar de caer a shell.
88
- * Eliminar el fallback `shell: true` cierra la última superficie de
89
- * ataque por inyección si en el futuro un nombre de paquete se
90
- * parametriza desde input externo (defensa en profundidad).
91
- */
92
- function comandoNpm(args) {
93
- if (!NPM_CLI_JS) {
94
- throw new Error('npm-cli.js no localizado: instala Node con npm bundled');
95
- }
96
- return { bin: process.execPath, argsList: [NPM_CLI_JS, ...args], opts: {} };
97
- }
98
-
99
- /**
100
- * Aumenta PATH con ubicaciones típicas de Node/npm en Windows.
101
- * Devuelve un PATH listo para inyectar en el env de un subproceso.
102
- */
103
- function pathAumentado() {
104
- const extras = [
105
- '/c/Program Files/nodejs',
106
- '/c/Program Files (x86)/nodejs',
107
- 'C:\\Program Files\\nodejs',
108
- 'C:\\Program Files (x86)\\nodejs',
109
- path.join(os.homedir(), 'AppData', 'Roaming', 'npm'),
110
- ];
111
- const sep = process.platform === 'win32' ? ';' : ':';
112
- return extras.join(sep) + sep + (process.env.PATH || '');
113
- }
114
-
115
- /**
116
- * Parsea una versión SemVer aceptando suffixes pre-release/build metadata.
117
- * Devuelve null si la cadena no es semver válida.
118
- */
119
- function parseSemver(v) {
120
- const m = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(String(v));
121
- if (!m) return null;
122
- return [Number(m[1]), Number(m[2]), Number(m[3])];
123
- }
124
-
125
- /**
126
- * Compara dos cadenas semver. Retorna 1, 0, -1 o null si alguna no parsea.
127
- */
128
- function compararSemver(a, b) {
129
- const pa = parseSemver(a);
130
- const pb = parseSemver(b);
131
- if (!pa || !pb) return null;
132
- for (let i = 0; i < 3; i++) {
133
- if (pa[i] !== pb[i]) return pa[i] > pb[i] ? 1 : -1;
134
- }
135
- return 0;
136
- }
137
-
138
- /**
139
- * Versión asíncrona: ejecuta `npm view <pkg> version` sin shell.
140
- * Usa execFile para evitar inyección si el nombre del paquete viniera
141
- * de input no confiable (defensa en profundidad — actualmente los
142
- * nombres son constantes literales).
143
- */
144
- function consultarUna(pkg, { timeoutMs = TIMEOUT_DEFAULT_MS, env = process.env } = {}) {
145
- return new Promise((resolve) => {
146
- try {
147
- asegurarNombrePaqueteSeguro(pkg);
148
- } catch (e) {
149
- resolve({ paquete: pkg, version: null, error: e.message });
150
- return;
151
- }
152
- const { bin, argsList, opts } = comandoNpm(['view', pkg, 'version']);
153
- execFile(bin, argsList, {
154
- timeout: timeoutMs,
155
- encoding: 'utf-8',
156
- env,
157
- ...opts,
158
- }, (err, stdout) => {
159
- if (err) {
160
- resolve({ paquete: pkg, version: null, error: String(err.message).slice(0, 120) });
161
- return;
162
- }
163
- const ver = String(stdout).trim();
164
- if (/^\d+\.\d+\.\d+/.test(ver)) {
165
- resolve({ paquete: pkg, version: ver });
166
- } else {
167
- resolve({ paquete: pkg, version: null, error: `formato inválido: ${ver.slice(0, 60)}` });
168
- }
169
- });
170
- });
171
- }
172
-
173
- /**
174
- * Consulta en paralelo la versión publicada de los nombres conocidos.
175
- * Devuelve la primera versión válida encontrada (preferencia por orden
176
- * en NOMBRES_DETECCION: canónico npmjs > mirror GH > legacy).
177
- *
178
- * Diseño:
179
- * - Ejecuta las consultas en paralelo con Promise.allSettled para que
180
- * el peor caso sea max(timeout) en vez de N × timeout en serie.
181
- * - Conserva orden de prioridad post-resolución para devolver el
182
- * resultado más canónico cuando varios responden con éxito.
183
- *
184
- * @returns {{paquete: string|null, version: string|null, intentos: Array}}
185
- */
186
- async function versionRemotaParalela({ timeoutMs = TIMEOUT_DEFAULT_MS, paquetes = NOMBRES_DETECCION } = {}) {
187
- const env = { ...process.env, PATH: pathAumentado() };
188
- const promesas = paquetes.map(pkg => consultarUna(pkg, { timeoutMs, env }));
189
- const settled = await Promise.allSettled(promesas);
190
-
191
- // Registrar TODOS los resultados — éxitos y fallos — para que el
192
- // diagnóstico downstream pueda mostrar cuántos paquetes respondieron.
193
- // Antes este bloque omitía los éxitos posteriores al primer 'mejor',
194
- // dejando intentos vacío cuando 2-3 paquetes resolvían correctamente.
195
- const intentos = [];
196
- let mejor = null;
197
-
198
- for (let i = 0; i < paquetes.length; i++) {
199
- const r = settled[i];
200
- if (r.status === 'fulfilled') {
201
- const { paquete, version, error } = r.value;
202
- if (version) {
203
- if (!mejor) mejor = { paquete, version };
204
- intentos.push({ paquete, resultado: 'ok', version });
205
- } else {
206
- intentos.push({ paquete, resultado: 'error', error: error || 'sin versión' });
207
- }
208
- } else {
209
- intentos.push({ paquete: paquetes[i], resultado: 'rejected', error: String(r.reason).slice(0, 120) });
210
- }
211
- }
212
-
213
- if (mejor) return { ...mejor, intentos };
214
- return { paquete: null, version: null, intentos };
215
- }
216
-
217
- /**
218
- * Versión síncrona-bloqueante de la consulta. Útil para scripts CLI
219
- * donde el flujo de control síncrono es más simple. Acepta también el
220
- * orden de prioridad sin paralelizar.
221
- *
222
- * Implementación: usa deasync vía execFileSync con timeout corto por
223
- * paquete (no paraleliza, pero sigue siendo más rápido que el patrón
224
- * anterior gracias al timeout reducido por intento).
225
- */
226
- function versionRemotaSync({ timeoutMs = TIMEOUT_DEFAULT_MS, paquetes = NOMBRES_DETECCION } = {}) {
227
- const env = { ...process.env, PATH: pathAumentado() };
228
- const intentos = [];
229
-
230
- for (const pkg of paquetes) {
231
- try {
232
- asegurarNombrePaqueteSeguro(pkg);
233
- const { bin, argsList, opts } = comandoNpm(['view', pkg, 'version']);
234
- const result = execFileSync(bin, argsList, {
235
- timeout: timeoutMs,
236
- encoding: 'utf-8',
237
- env,
238
- stdio: ['pipe', 'pipe', 'pipe'],
239
- ...opts,
240
- });
241
- const ver = String(result).trim();
242
- if (/^\d+\.\d+\.\d+/.test(ver)) {
243
- return { paquete: pkg, version: ver, intentos };
244
- }
245
- intentos.push({ paquete: pkg, resultado: 'formato inválido', raw: ver.slice(0, 80) });
246
- } catch (err) {
247
- intentos.push({ paquete: pkg, resultado: 'error', error: String(err.message).slice(0, 120) });
248
- }
249
- }
250
- return { paquete: null, version: null, intentos };
251
- }
252
-
253
- module.exports = {
254
- pathAumentado,
255
- parseSemver,
256
- compararSemver,
257
- consultarUna,
258
- versionRemotaParalela,
259
- versionRemotaSync,
260
- TIMEOUT_DEFAULT_MS,
261
- };
1
+ 'use strict';
2
+
3
+ /**
4
+ * npm-version.js — utilidades compartidas para consultar versiones en npm.
5
+ *
6
+ * Lib usada por scripts/check-update.js, hooks/check-update.js,
7
+ * scripts/doctor.js y scripts/actualizar.js. Consolida tres preocupaciones
8
+ * que estaban duplicadas en los 4 archivos:
9
+ * 1. Aumento de PATH (necesario en git-bash Windows donde el PATH
10
+ * heredado no incluye node/npm).
11
+ * 2. Consulta paralela a npm view de los nombres conocidos del paquete.
12
+ * 3. Comparación robusta de versiones SemVer con soporte para
13
+ * pre-release/build metadata (5.7.5-beta.1+abc).
14
+ *
15
+ * Por seguridad usa execFile (sin shell) en lugar de execSync con shell:true.
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const os = require('os');
21
+ const { execFile, execFileSync } = require('child_process');
22
+
23
+ const { NOMBRES_DETECCION } = require('./paquetes-conocidos');
24
+
25
+ const TIMEOUT_DEFAULT_MS = 10_000;
26
+
27
+ /**
28
+ * Resuelve la ruta absoluta a `npm-cli.js` que viene con la instalación
29
+ * de Node. En Windows `npm.cmd` y en POSIX `npm` son shims que terminan
30
+ * invocando este JS con el binario de Node. Llamamos directamente a Node
31
+ * con el script para evitar:
32
+ * 1. La diferencia .cmd vs binario nativo entre Windows y POSIX.
33
+ * 2. El requerimiento de `shell: true` en Node 20+ para ejecutar .cmd
34
+ * (que dispara DEP0190 cuando hay args, aunque estén validados).
35
+ *
36
+ * Si no se encuentra (ej. instalación no estándar), devuelve null y
37
+ * caemos al fallback con shell.
38
+ */
39
+ function localizarNpmCli() {
40
+ const dir = path.dirname(process.execPath);
41
+ const candidatos = [
42
+ path.join(dir, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
43
+ path.join(dir, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
44
+ path.join(os.homedir(), 'AppData', 'Roaming', 'npm', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
45
+ ];
46
+ for (const c of candidatos) {
47
+ try {
48
+ if (fs.existsSync(c)) return c;
49
+ } catch { /* continuar */ }
50
+ }
51
+ return null;
52
+ }
53
+
54
+ const NPM_CLI_JS = localizarNpmCli();
55
+
56
+ if (!NPM_CLI_JS) {
57
+ // Si no encontramos npm-cli.js (instalación de Node atípica), emitimos
58
+ // un aviso pero NO caemos a `shell: true` — eso reintroduciría DEP0190
59
+ // y exigiría confiar en el regex como única defensa contra inyección.
60
+ // En su lugar, los callers reciben null/error y deciden cómo continuar.
61
+ process.stderr.write(
62
+ `[npm-version] aviso: no se encontró npm-cli.js junto a node ` +
63
+ `(${process.execPath}). Las consultas a npm view fallarán hasta que ` +
64
+ `node esté instalado con npm bundled (instalación oficial).\n`
65
+ );
66
+ }
67
+
68
+ /**
69
+ * Whitelist de nombres de paquete: scope opcional + nombre alfanumérico
70
+ * con guiones/puntos/underscore. Rechaza espacios, comillas, $, |, ;, etc.
71
+ * Defensa en profundidad — actualmente los nombres son constantes del
72
+ * código, pero esta validación protege ante regresiones futuras donde
73
+ * el nombre venga de variable de entorno o input externo.
74
+ */
75
+ const REGEX_NOMBRE_PAQUETE_SEGURO = /^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
76
+
77
+ function asegurarNombrePaqueteSeguro(pkg) {
78
+ if (typeof pkg !== 'string' || !REGEX_NOMBRE_PAQUETE_SEGURO.test(pkg)) {
79
+ throw new Error(`nombre de paquete inválido: ${JSON.stringify(pkg)}`);
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Construye [bin, args] para invocar npm de forma segura:
85
+ * - Si encontramos npm-cli.js, usa el binario de Node directamente
86
+ * (cross-platform, sin shell, sin DEP0190).
87
+ * - Si no se encontró, lanza error explícito en lugar de caer a shell.
88
+ * Eliminar el fallback `shell: true` cierra la última superficie de
89
+ * ataque por inyección si en el futuro un nombre de paquete se
90
+ * parametriza desde input externo (defensa en profundidad).
91
+ */
92
+ function comandoNpm(args) {
93
+ if (!NPM_CLI_JS) {
94
+ throw new Error('npm-cli.js no localizado: instala Node con npm bundled');
95
+ }
96
+ return { bin: process.execPath, argsList: [NPM_CLI_JS, ...args], opts: {} };
97
+ }
98
+
99
+ /**
100
+ * Aumenta PATH con ubicaciones típicas de Node/npm en Windows.
101
+ * Devuelve un PATH listo para inyectar en el env de un subproceso.
102
+ */
103
+ function pathAumentado() {
104
+ const extras = [
105
+ '/c/Program Files/nodejs',
106
+ '/c/Program Files (x86)/nodejs',
107
+ 'C:\\Program Files\\nodejs',
108
+ 'C:\\Program Files (x86)\\nodejs',
109
+ path.join(os.homedir(), 'AppData', 'Roaming', 'npm'),
110
+ ];
111
+ const sep = process.platform === 'win32' ? ';' : ':';
112
+ return extras.join(sep) + sep + (process.env.PATH || '');
113
+ }
114
+
115
+ /**
116
+ * Parsea una versión SemVer aceptando suffixes pre-release/build metadata.
117
+ * Devuelve null si la cadena no es semver válida.
118
+ */
119
+ function parseSemver(v) {
120
+ const m = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(String(v));
121
+ if (!m) return null;
122
+ return [Number(m[1]), Number(m[2]), Number(m[3])];
123
+ }
124
+
125
+ /**
126
+ * Compara dos cadenas semver. Retorna 1, 0, -1 o null si alguna no parsea.
127
+ */
128
+ function compararSemver(a, b) {
129
+ const pa = parseSemver(a);
130
+ const pb = parseSemver(b);
131
+ if (!pa || !pb) return null;
132
+ for (let i = 0; i < 3; i++) {
133
+ if (pa[i] !== pb[i]) return pa[i] > pb[i] ? 1 : -1;
134
+ }
135
+ return 0;
136
+ }
137
+
138
+ /**
139
+ * Versión asíncrona: ejecuta `npm view <pkg> version` sin shell.
140
+ * Usa execFile para evitar inyección si el nombre del paquete viniera
141
+ * de input no confiable (defensa en profundidad — actualmente los
142
+ * nombres son constantes literales).
143
+ */
144
+ function consultarUna(pkg, { timeoutMs = TIMEOUT_DEFAULT_MS, env = process.env } = {}) {
145
+ return new Promise((resolve) => {
146
+ try {
147
+ asegurarNombrePaqueteSeguro(pkg);
148
+ } catch (e) {
149
+ resolve({ paquete: pkg, version: null, error: e.message });
150
+ return;
151
+ }
152
+ const { bin, argsList, opts } = comandoNpm(['view', pkg, 'version']);
153
+ execFile(bin, argsList, {
154
+ timeout: timeoutMs,
155
+ encoding: 'utf-8',
156
+ env,
157
+ ...opts,
158
+ }, (err, stdout) => {
159
+ if (err) {
160
+ resolve({ paquete: pkg, version: null, error: String(err.message).slice(0, 120) });
161
+ return;
162
+ }
163
+ const ver = String(stdout).trim();
164
+ if (/^\d+\.\d+\.\d+/.test(ver)) {
165
+ resolve({ paquete: pkg, version: ver });
166
+ } else {
167
+ resolve({ paquete: pkg, version: null, error: `formato inválido: ${ver.slice(0, 60)}` });
168
+ }
169
+ });
170
+ });
171
+ }
172
+
173
+ /**
174
+ * Consulta en paralelo la versión publicada de los nombres conocidos.
175
+ * Devuelve la primera versión válida encontrada (preferencia por orden
176
+ * en NOMBRES_DETECCION: canónico npmjs > mirror GH > legacy).
177
+ *
178
+ * Diseño:
179
+ * - Ejecuta las consultas en paralelo con Promise.allSettled para que
180
+ * el peor caso sea max(timeout) en vez de N × timeout en serie.
181
+ * - Conserva orden de prioridad post-resolución para devolver el
182
+ * resultado más canónico cuando varios responden con éxito.
183
+ *
184
+ * @returns {{paquete: string|null, version: string|null, intentos: Array}}
185
+ */
186
+ async function versionRemotaParalela({ timeoutMs = TIMEOUT_DEFAULT_MS, paquetes = NOMBRES_DETECCION } = {}) {
187
+ const env = { ...process.env, PATH: pathAumentado() };
188
+ const promesas = paquetes.map(pkg => consultarUna(pkg, { timeoutMs, env }));
189
+ const settled = await Promise.allSettled(promesas);
190
+
191
+ // Registrar TODOS los resultados — éxitos y fallos — para que el
192
+ // diagnóstico downstream pueda mostrar cuántos paquetes respondieron.
193
+ // Antes este bloque omitía los éxitos posteriores al primer 'mejor',
194
+ // dejando intentos vacío cuando 2-3 paquetes resolvían correctamente.
195
+ const intentos = [];
196
+ let mejor = null;
197
+
198
+ for (let i = 0; i < paquetes.length; i++) {
199
+ const r = settled[i];
200
+ if (r.status === 'fulfilled') {
201
+ const { paquete, version, error } = r.value;
202
+ if (version) {
203
+ if (!mejor) mejor = { paquete, version };
204
+ intentos.push({ paquete, resultado: 'ok', version });
205
+ } else {
206
+ intentos.push({ paquete, resultado: 'error', error: error || 'sin versión' });
207
+ }
208
+ } else {
209
+ intentos.push({ paquete: paquetes[i], resultado: 'rejected', error: String(r.reason).slice(0, 120) });
210
+ }
211
+ }
212
+
213
+ if (mejor) return { ...mejor, intentos };
214
+ return { paquete: null, version: null, intentos };
215
+ }
216
+
217
+ /**
218
+ * Versión síncrona-bloqueante de la consulta. Útil para scripts CLI
219
+ * donde el flujo de control síncrono es más simple. Acepta también el
220
+ * orden de prioridad sin paralelizar.
221
+ *
222
+ * Implementación: usa deasync vía execFileSync con timeout corto por
223
+ * paquete (no paraleliza, pero sigue siendo más rápido que el patrón
224
+ * anterior gracias al timeout reducido por intento).
225
+ */
226
+ function versionRemotaSync({ timeoutMs = TIMEOUT_DEFAULT_MS, paquetes = NOMBRES_DETECCION } = {}) {
227
+ const env = { ...process.env, PATH: pathAumentado() };
228
+ const intentos = [];
229
+
230
+ for (const pkg of paquetes) {
231
+ try {
232
+ asegurarNombrePaqueteSeguro(pkg);
233
+ const { bin, argsList, opts } = comandoNpm(['view', pkg, 'version']);
234
+ const result = execFileSync(bin, argsList, {
235
+ timeout: timeoutMs,
236
+ encoding: 'utf-8',
237
+ env,
238
+ stdio: ['pipe', 'pipe', 'pipe'],
239
+ ...opts,
240
+ });
241
+ const ver = String(result).trim();
242
+ if (/^\d+\.\d+\.\d+/.test(ver)) {
243
+ return { paquete: pkg, version: ver, intentos };
244
+ }
245
+ intentos.push({ paquete: pkg, resultado: 'formato inválido', raw: ver.slice(0, 80) });
246
+ } catch (err) {
247
+ intentos.push({ paquete: pkg, resultado: 'error', error: String(err.message).slice(0, 120) });
248
+ }
249
+ }
250
+ return { paquete: null, version: null, intentos };
251
+ }
252
+
253
+ module.exports = {
254
+ pathAumentado,
255
+ parseSemver,
256
+ compararSemver,
257
+ consultarUna,
258
+ versionRemotaParalela,
259
+ versionRemotaSync,
260
+ TIMEOUT_DEFAULT_MS,
261
+ };