@saulwade/swl-ses 1.2.2 → 1.3.1
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 +199 -255
- package/README.md +4 -4
- package/comandos/swl/claudemd.md +136 -0
- package/comandos/swl/salud.md +29 -0
- package/habilidades/nextjs-experto/SKILL.md +5 -5
- package/habilidades/nextjs-testing/SKILL.md +26 -1
- package/habilidades/nuevo-proyecto/SKILL.md +82 -2
- package/habilidades/planear-fase/SKILL.md +24 -4
- package/habilidades/react-experto/SKILL.md +25 -4
- package/habilidades/swl-claudemd/SKILL.md +220 -0
- package/habilidades/tdd-workflow/SKILL.md +8 -1
- package/hooks/claudemd-bloat-detector.js +161 -0
- package/hooks/lib/privacy-filter.js +42 -4
- package/manifiestos/hooks-config.json +9 -0
- package/manifiestos/modulos.json +21 -2
- package/manifiestos/skills-lock.json +68 -40
- package/package.json +87 -87
- package/plugin.json +343 -343
- package/reglas/seguridad-agentes.md +12 -0
- package/scripts/auditar-claudemd.js +297 -0
- package/scripts/instalador.js +4 -0
- package/scripts/lib/detectar-stack-detallado.js +307 -0
- package/scripts/lib/transformadores/claude.js +200 -124
- package/scripts/publicar.js +166 -12
|
@@ -1,124 +1,200 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const TransformadorBase = require('./base');
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const TransformadorBase = require('./base');
|
|
4
|
+
const { detectarStackDetallado } = require('../detectar-stack-detallado');
|
|
5
|
+
|
|
6
|
+
class TransformadorClaude extends TransformadorBase {
|
|
7
|
+
transformarAgente(contenidoMd, metadatos) {
|
|
8
|
+
// Pass-through: Claude soporta el formato SWL directamente
|
|
9
|
+
return {
|
|
10
|
+
contenido: contenidoMd,
|
|
11
|
+
rutaRelativa: `agents/${metadatos.nombreArchivo}`,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
transformarSkill(dirSkill, metadatos) {
|
|
16
|
+
// Pass-through: copiar directorio completo
|
|
17
|
+
return {
|
|
18
|
+
tipo: 'directorio',
|
|
19
|
+
rutaRelativa: `skills/${metadatos.nombreDirectorio}`,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
transformarRegla(contenidoMd, metadatos) {
|
|
24
|
+
return {
|
|
25
|
+
contenido: contenidoMd,
|
|
26
|
+
rutaRelativa: `rules/${metadatos.nombreArchivo}`,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
transformarComando(contenidoMd, metadatos) {
|
|
31
|
+
return {
|
|
32
|
+
contenido: contenidoMd,
|
|
33
|
+
rutaRelativa: `commands/swl/${metadatos.nombreArchivo}`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Genera el bloque SWL para insertar en el CLAUDE.md del proyecto destino.
|
|
39
|
+
*
|
|
40
|
+
* A diferencia de los otros transformadores (codex/gemini/copilot/opencode)
|
|
41
|
+
* que generan el archivo completo de instrucciones desde cero, Claude usa
|
|
42
|
+
* un CLAUDE.md que el usuario mantiene y que puede tener contenido propio
|
|
43
|
+
* (reglas del proyecto, convenciones, notas). Por eso:
|
|
44
|
+
*
|
|
45
|
+
* - El contenido devuelto es un BLOQUE (no archivo completo)
|
|
46
|
+
* - Se devuelve `merge: { tipo: 'marcadores', ... }` para indicar al
|
|
47
|
+
* caller que debe usar `append-con-marcadores` en lugar de writeFileSync
|
|
48
|
+
* - Los marcadores `<!-- SWL-BEGIN vX.Y.Z -->` / `<!-- SWL-END -->`
|
|
49
|
+
* permiten que la siguiente actualización reemplace solo este bloque
|
|
50
|
+
* sin tocar el contenido del usuario
|
|
51
|
+
*
|
|
52
|
+
* Aplicación de ADR-0016 (best practices Anthropic "The CLAUDE.md file"):
|
|
53
|
+
* - Detecta stack del proyecto destino (lenguaje, framework, ORM,
|
|
54
|
+
* package manager, comandos disponibles)
|
|
55
|
+
* - Genera secciones canónicas: Stack / Comandos del proyecto / Sistema SWL
|
|
56
|
+
* - Documenta jerarquía project / user-level (`~/.claude/CLAUDE.md`)
|
|
57
|
+
* - Documenta patrón "ask Claude to save to memory" del video oficial
|
|
58
|
+
*/
|
|
59
|
+
generarArchivoInstrucciones(contexto) {
|
|
60
|
+
const fecha = new Date().toISOString().split('T')[0];
|
|
61
|
+
const stack = contexto.dirProyecto
|
|
62
|
+
? this._detectarStackSeguro(contexto.dirProyecto)
|
|
63
|
+
: null;
|
|
64
|
+
|
|
65
|
+
const lineas = [
|
|
66
|
+
`> Gestionado por swl-ses v${contexto.version} — no editar entre estos marcadores.`,
|
|
67
|
+
`> Regenerado en ${fecha} con perfil \`${contexto.perfil || 'default'}\`.`,
|
|
68
|
+
`> Para excluir este bloque en futuras actualizaciones: \`npx swl-ses@latest install --no-claudemd\`.`,
|
|
69
|
+
'',
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
// ─── Stack del proyecto detectado (best practice Anthropic #2) ─────────
|
|
73
|
+
if (stack && stack.detectado) {
|
|
74
|
+
lineas.push('## Stack del proyecto');
|
|
75
|
+
lineas.push('');
|
|
76
|
+
const partes = [];
|
|
77
|
+
if (stack.lenguajePrincipal) {
|
|
78
|
+
partes.push(`**Lenguaje**: ${stack.lenguajePrincipal.lenguaje}` +
|
|
79
|
+
(stack.lenguajePrincipal.runtime ? ` (${stack.lenguajePrincipal.runtime})` : ''));
|
|
80
|
+
}
|
|
81
|
+
if (stack.framework) partes.push(`**Framework**: ${stack.framework}`);
|
|
82
|
+
if (stack.orm) partes.push(`**ORM/BD**: ${stack.orm}`);
|
|
83
|
+
if (stack.packageManager) partes.push(`**Package manager**: ${stack.packageManager}`);
|
|
84
|
+
lineas.push('- ' + partes.join('\n- '));
|
|
85
|
+
lineas.push('');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── Comandos del proyecto (best practice Anthropic #3) ────────────────
|
|
89
|
+
if (stack && stack.comandos && stack.comandos.length > 0) {
|
|
90
|
+
lineas.push('## Comandos del proyecto');
|
|
91
|
+
lineas.push('');
|
|
92
|
+
lineas.push('| Comando | Propósito |');
|
|
93
|
+
lineas.push('|---------|-----------|');
|
|
94
|
+
for (const c of stack.comandos) {
|
|
95
|
+
lineas.push(`| \`${c.comando}\` | ${c.proposito} |`);
|
|
96
|
+
}
|
|
97
|
+
lineas.push('');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─── Sistema SWL (siempre presente) ────────────────────────────────────
|
|
101
|
+
lineas.push('## Sistema SWL — uso obligatorio');
|
|
102
|
+
lineas.push('');
|
|
103
|
+
lineas.push(`Este proyecto usa swl-ses v${contexto.version} (sistema de ingeniería de software auto-evolutivo multi-runtime).`);
|
|
104
|
+
lineas.push(`Componentes instalados: ${contexto.conteos?.agentes || 0} agentes, ${contexto.conteos?.skills || 0} skills, ${contexto.conteos?.comandos || 0} comandos, ${contexto.conteos?.reglas || 0} reglas.`);
|
|
105
|
+
lineas.push('');
|
|
106
|
+
|
|
107
|
+
lineas.push('### Reglas de máxima prioridad');
|
|
108
|
+
lineas.push('');
|
|
109
|
+
lineas.push('1. **Idioma: español de México** — respuestas, código comentado, commits, PRs y docs. Evitar anglicismos innecesarios.');
|
|
110
|
+
lineas.push('2. **Usar el sistema SWL** — invocar agentes especializados (`orquestador-swl`, `implementador-swl`, `depurador-swl`, etc.) en lugar de hacer trabajo directo. Cargar skills con `Skill("nombre")` antes de implementar. Usar comandos `/swl:*` cuando apliquen.');
|
|
111
|
+
lineas.push('3. **Investigar antes de editar** — leer el archivo completo y entender el contexto antes de modificar código.');
|
|
112
|
+
lineas.push('4. **Lectura de documentos Office/Jupyter** — usar `python scripts/vendor/markitdown/cli.py <archivo>` para `.docx`, `.xlsx`, `.pptx`, `.ipynb`. El Read tool no soporta esos formatos.');
|
|
113
|
+
lineas.push('');
|
|
114
|
+
|
|
115
|
+
lineas.push('### Dónde están los componentes');
|
|
116
|
+
lineas.push('');
|
|
117
|
+
lineas.push('- Agentes: `.claude/agents/*.md`');
|
|
118
|
+
lineas.push('- Skills: `.claude/skills/*/SKILL.md` (invocar con `Skill("nombre")`)');
|
|
119
|
+
lineas.push('- Comandos: `.claude/commands/swl/*.md` (invocar con `/swl:nombre`)');
|
|
120
|
+
lineas.push('- Reglas: `.claude/rules/*.md` (se cargan automáticamente)');
|
|
121
|
+
lineas.push('- Hooks: registrados en `.claude/settings.json`');
|
|
122
|
+
lineas.push('');
|
|
123
|
+
|
|
124
|
+
lineas.push('### Flujos recomendados');
|
|
125
|
+
lineas.push('');
|
|
126
|
+
lineas.push('- **Feature completa**: `/swl:discutir-fase N` → `/swl:planear-fase N` → `/swl:ejecutar-fase N` → `/swl:verificar`');
|
|
127
|
+
lineas.push('- **Debugging**: delegar a `depurador-swl` con método científico (reproducir → aislar → hipótesis → fix → verificar)');
|
|
128
|
+
lineas.push('- **Calidad**: `/swl:revisar` (código) + `revisor-seguridad-swl` antes de merge a main');
|
|
129
|
+
lineas.push('- **Aprendizaje continuo**: `/swl:aprender` al final de fase, `/swl:evolucionar` ante fallos recurrentes de agentes');
|
|
130
|
+
lineas.push('');
|
|
131
|
+
|
|
132
|
+
lineas.push('### Consulta del sistema');
|
|
133
|
+
lineas.push('');
|
|
134
|
+
lineas.push('- `/swl:ayuda` — catálogo completo de comandos');
|
|
135
|
+
lineas.push('- `/swl:salud` — diagnóstico de integridad (incluye calidad de CLAUDE.md)');
|
|
136
|
+
lineas.push('- `/swl:metricas` — tokens, costo y modelos usados en la sesión');
|
|
137
|
+
lineas.push('- `/swl:skill-search <keyword>` — buscar skills relevantes');
|
|
138
|
+
lineas.push('- `/swl:claudemd audit` — auditar este CLAUDE.md (líneas, bullets gigantes, secciones canónicas)');
|
|
139
|
+
lineas.push('');
|
|
140
|
+
|
|
141
|
+
lineas.push('### Convenciones de versionado y commits');
|
|
142
|
+
lineas.push('');
|
|
143
|
+
lineas.push('- Versionado semántico estricto (SemVer)');
|
|
144
|
+
lineas.push('- Mensajes de commit en español, formato `<tipo>(<scope>): <descripción en imperativo>`');
|
|
145
|
+
lineas.push('- Commits atómicos (un cambio lógico = un commit)');
|
|
146
|
+
lineas.push('- Antes de release: `/swl:release` y gate `node scripts/verificar-release.js`');
|
|
147
|
+
lineas.push('');
|
|
148
|
+
|
|
149
|
+
// ─── Jerarquía project / user-level (best practice Anthropic) ─────────
|
|
150
|
+
lineas.push('### Jerarquía de CLAUDE.md (recomendada)');
|
|
151
|
+
lineas.push('');
|
|
152
|
+
lineas.push('- **Project-level** (este archivo, `./CLAUDE.md`) — convenciones del proyecto, compartido vía VCS con el equipo.');
|
|
153
|
+
lineas.push('- **User-level** (`~/.claude/CLAUDE.md`) — preferencias personales transversales (estilo de comentarios, lenguaje preferido, atajos personales). NO se commitea.');
|
|
154
|
+
lineas.push('- Las **reglas globales** del usuario en `~/.claude/rules/*.md` se cargan automáticamente para todos los proyectos.');
|
|
155
|
+
lineas.push('- Si no tienes user-level, créalo con `/swl:claudemd init-user`.');
|
|
156
|
+
lineas.push('');
|
|
157
|
+
|
|
158
|
+
// ─── Patrón "ask Claude to save to memory" del video oficial ──────────
|
|
159
|
+
lineas.push('### Cómo evolucionar este archivo');
|
|
160
|
+
lineas.push('');
|
|
161
|
+
lineas.push('1. **Empieza mínimo**. No adelantes reglas. Agrega solo lo que tengas que corregir más de una vez.');
|
|
162
|
+
lineas.push('2. **Cuando corrijas a Claude**, pide explícitamente: *"guarda esto en memoria"* o *"agrega esto a CLAUDE.md"*. Esa es la forma natural de que el archivo crezca con valor.');
|
|
163
|
+
lineas.push('3. **Usa `@filepath`** para referenciar docs sin duplicar contenido (ej. `@docs/api.md`, `@.planning/PROYECTO.md`).');
|
|
164
|
+
lineas.push('4. **Si crece mucho**, ejecuta `/swl:claudemd refactor` para extraer secciones a archivos `@`-referenciados.');
|
|
165
|
+
lineas.push('');
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
contenido: lineas.join('\n'),
|
|
169
|
+
rutaRelativa: 'CLAUDE.md',
|
|
170
|
+
dirBase: '.',
|
|
171
|
+
merge: {
|
|
172
|
+
tipo: 'marcadores',
|
|
173
|
+
beginTag: `<!-- SWL-BEGIN v${contexto.version} — bloque gestionado por swl-ses -->`,
|
|
174
|
+
endTag: '<!-- SWL-END -->',
|
|
175
|
+
beginPrefix: '<!-- SWL-BEGIN',
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
transformarHooks(hooksConfig, opciones) {
|
|
181
|
+
// Los hooks de Claude se manejan via hooks-settings.js existente
|
|
182
|
+
return { formato: 'claude-settings', config: hooksConfig };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Wrapper defensivo: si la detección de stack falla por cualquier razón
|
|
187
|
+
* (permisos, archivo corrupto, etc.), se ignora y se genera el bloque
|
|
188
|
+
* sin secciones de stack. NUNCA romper el installer.
|
|
189
|
+
*/
|
|
190
|
+
_detectarStackSeguro(dirProyecto) {
|
|
191
|
+
try {
|
|
192
|
+
return detectarStackDetallado(dirProyecto);
|
|
193
|
+
} catch (e) {
|
|
194
|
+
// Silencioso: stack no detectado, bloque base se genera igualmente
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = TransformadorClaude;
|
package/scripts/publicar.js
CHANGED
|
@@ -12,10 +12,11 @@
|
|
|
12
12
|
|
|
13
13
|
'use strict';
|
|
14
14
|
|
|
15
|
-
const { execFileSync } = require('child_process');
|
|
15
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
16
16
|
const fs = require('fs');
|
|
17
17
|
const path = require('path');
|
|
18
18
|
const os = require('os');
|
|
19
|
+
const readline = require('readline');
|
|
19
20
|
|
|
20
21
|
const {
|
|
21
22
|
NOMBRE_GITHUB_MIRROR: GITHUB_NAME,
|
|
@@ -58,6 +59,159 @@ function npmExec(args, opts = {}) {
|
|
|
58
59
|
return execFileSync(bin, args, { ...defaults, ...opts });
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Variante de npmExec que captura stderr para detección posterior de errores
|
|
64
|
+
* estructurados (ej: EOTP). stdout sigue heredado (live-streaming visible al
|
|
65
|
+
* usuario) y stderr se pipea a un buffer + se ecoa a process.stderr al final.
|
|
66
|
+
*
|
|
67
|
+
* Retorna { status, stderr } sin lanzar — el caller inspecciona el status.
|
|
68
|
+
*/
|
|
69
|
+
function npmSpawnCaptureStderr(args, opts = {}) {
|
|
70
|
+
const defaults = { cwd: ROOT, timeout: 300_000, env: process.env };
|
|
71
|
+
const merged = { ...defaults, ...opts, stdio: ['inherit', 'inherit', 'pipe'] };
|
|
72
|
+
let res;
|
|
73
|
+
if (NPM_CLI_JS) {
|
|
74
|
+
res = spawnSync(process.execPath, [NPM_CLI_JS, ...args], merged);
|
|
75
|
+
} else {
|
|
76
|
+
const bin = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
77
|
+
res = spawnSync(bin, args, merged);
|
|
78
|
+
}
|
|
79
|
+
const stderr = res.stderr ? String(res.stderr) : '';
|
|
80
|
+
// Ecoar stderr al usuario para que vea el diagnóstico de npm aunque se capturó.
|
|
81
|
+
if (stderr) process.stderr.write(stderr);
|
|
82
|
+
return { status: res.status, signal: res.signal, stderr, error: res.error };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Detecta si un blob de stderr indica que npm requiere una OTP de 2FA.
|
|
87
|
+
* Match defensivo: cubre tanto el código de error explícito (`EOTP`) como el
|
|
88
|
+
* mensaje canónico ("requires a one-time password"), porque npm ha cambiado
|
|
89
|
+
* el formato del mensaje entre versiones (npm 8 vs 10+).
|
|
90
|
+
*
|
|
91
|
+
* Pura — testeable sin dependencias. Exportada para tests.
|
|
92
|
+
*/
|
|
93
|
+
function esErrorOTP(stderr) {
|
|
94
|
+
if (!stderr) return false;
|
|
95
|
+
const blob = String(stderr);
|
|
96
|
+
return /\bEOTP\b/.test(blob) || /one-time password/i.test(blob);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Solicita una OTP al usuario via stdin (readline síncrono).
|
|
101
|
+
* Retorna la OTP normalizada (string de 6 dígitos) o null si:
|
|
102
|
+
* - stdin no es TTY (CI, pipe) → no se puede pedir interactivamente
|
|
103
|
+
* - el usuario cancela con Ctrl+C / línea vacía
|
|
104
|
+
* - el formato no es válido (no 6 dígitos)
|
|
105
|
+
*
|
|
106
|
+
* El caller debe manejar el caso null como "fallback a guía manual".
|
|
107
|
+
*/
|
|
108
|
+
function solicitarOTPInteractiva() {
|
|
109
|
+
if (!process.stdin.isTTY) {
|
|
110
|
+
process.stderr.write('[publicar] stdin no es TTY: no se puede pedir OTP interactivamente.\n');
|
|
111
|
+
process.stderr.write('[publicar] Configurar NPM_CONFIG_OTP=<otp> antes de invocar este script.\n');
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
116
|
+
// questionSync vía promesa NO funciona aquí — el script es síncrono.
|
|
117
|
+
// Usamos un truco: readline es async-only, así que dejamos al usuario
|
|
118
|
+
// el manejo bloqueante leyendo línea por línea con readSync vía read-int.
|
|
119
|
+
// Pero readline no expone modo síncrono. Alternativa: leer de stdin con
|
|
120
|
+
// fd 0 + readSync. En Windows con TTY funciona; en Linux idem.
|
|
121
|
+
try {
|
|
122
|
+
process.stderr.write('\nNPM requiere OTP (2FA). Ingresa el código de 6 dígitos de tu autenticador: ');
|
|
123
|
+
const otp = leerLineaStdinSync().trim();
|
|
124
|
+
rl.close();
|
|
125
|
+
if (!/^\d{6}$/.test(otp)) {
|
|
126
|
+
process.stderr.write(`\n[publicar] OTP inválida: se esperan 6 dígitos, se recibió: "${otp.slice(0, 20)}"\n`);
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return otp;
|
|
130
|
+
} catch (err) {
|
|
131
|
+
rl.close();
|
|
132
|
+
process.stderr.write(`\n[publicar] error leyendo OTP: ${String(err.message).slice(0, 120)}\n`);
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Lectura síncrona de una línea de stdin. Necesario porque readline solo
|
|
139
|
+
* expone API asíncrona, pero todo el flujo de publicar.js es síncrono.
|
|
140
|
+
* Lee byte por byte hasta encontrar newline o EOF.
|
|
141
|
+
*/
|
|
142
|
+
function leerLineaStdinSync() {
|
|
143
|
+
const BUFFER_SIZE = 256;
|
|
144
|
+
const buf = Buffer.alloc(BUFFER_SIZE);
|
|
145
|
+
let line = '';
|
|
146
|
+
while (true) {
|
|
147
|
+
let bytesRead;
|
|
148
|
+
try {
|
|
149
|
+
bytesRead = fs.readSync(0, buf, 0, BUFFER_SIZE, null);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
// EAGAIN en stdin no-bloqueante: poco común en TTY, pero defensivo.
|
|
152
|
+
if (err.code === 'EAGAIN') continue;
|
|
153
|
+
throw err;
|
|
154
|
+
}
|
|
155
|
+
if (bytesRead === 0) break; // EOF
|
|
156
|
+
const chunk = buf.slice(0, bytesRead).toString('utf-8');
|
|
157
|
+
const nlIdx = chunk.indexOf('\n');
|
|
158
|
+
if (nlIdx >= 0) {
|
|
159
|
+
line += chunk.slice(0, nlIdx);
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
line += chunk;
|
|
163
|
+
}
|
|
164
|
+
return line.replace(/\r$/, ''); // Windows envía \r\n
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Ejecuta un `npm publish` con soporte automático de OTP (2FA).
|
|
169
|
+
*
|
|
170
|
+
* Flujo:
|
|
171
|
+
* 1. Si NPM_CONFIG_OTP está definida en el entorno, se usa directamente
|
|
172
|
+
* (npm la lee automáticamente — no hay que pasarla como flag).
|
|
173
|
+
* 2. Si el publish falla con EOTP/one-time password, se intenta solicitar
|
|
174
|
+
* la OTP interactivamente y reintentar con --ignore-scripts (los
|
|
175
|
+
* scripts pre-publish ya pasaron en el primer intento).
|
|
176
|
+
* 3. Si no se puede obtener OTP (no TTY o usuario cancela), reporta el
|
|
177
|
+
* error y retorna false.
|
|
178
|
+
*
|
|
179
|
+
* Retorna: { ok: boolean, stderr: string }
|
|
180
|
+
*
|
|
181
|
+
* Exportada para tests.
|
|
182
|
+
*/
|
|
183
|
+
function ejecutarPublishConOTP(args, opts = {}) {
|
|
184
|
+
// Intento 1: con OTP de env si existe, sin ella si no.
|
|
185
|
+
const intento1 = npmSpawnCaptureStderr(args, opts);
|
|
186
|
+
if (intento1.status === 0) {
|
|
187
|
+
return { ok: true, stderr: intento1.stderr };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Si NO es EOTP, no podemos hacer nada — propagamos el fallo.
|
|
191
|
+
if (!esErrorOTP(intento1.stderr)) {
|
|
192
|
+
return { ok: false, stderr: intento1.stderr };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Es EOTP. Intentar solicitar OTP interactivamente.
|
|
196
|
+
process.stderr.write('\n[publicar] npm rechazó el publish con EOTP (2FA requerida).\n');
|
|
197
|
+
const otp = solicitarOTPInteractiva();
|
|
198
|
+
if (!otp) {
|
|
199
|
+
process.stderr.write('[publicar] No se obtuvo OTP. Aborta. Reintentar con:\n');
|
|
200
|
+
process.stderr.write(` NPM_CONFIG_OTP=<otp> node scripts/publicar.js ${process.argv.slice(2).join(' ')}\n`);
|
|
201
|
+
return { ok: false, stderr: intento1.stderr };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Reintento con OTP via env + --ignore-scripts (los pre-publish ya pasaron).
|
|
205
|
+
process.stderr.write('\n[publicar] Reintentando publish con OTP recibida...\n');
|
|
206
|
+
const envConOTP = { ...(opts.env || process.env), NPM_CONFIG_OTP: otp };
|
|
207
|
+
const argsConIgnore = args.includes('--ignore-scripts') ? args : [...args, '--ignore-scripts'];
|
|
208
|
+
const intento2 = npmSpawnCaptureStderr(argsConIgnore, { ...opts, env: envConOTP });
|
|
209
|
+
if (intento2.status === 0) {
|
|
210
|
+
return { ok: true, stderr: intento2.stderr };
|
|
211
|
+
}
|
|
212
|
+
return { ok: false, stderr: intento2.stderr };
|
|
213
|
+
}
|
|
214
|
+
|
|
61
215
|
function leerPkg() {
|
|
62
216
|
return JSON.parse(fs.readFileSync(PKG_PATH, 'utf-8'));
|
|
63
217
|
}
|
|
@@ -245,14 +399,13 @@ function publicarNpmjs(pkg, dryRun) {
|
|
|
245
399
|
|
|
246
400
|
const args = ['publish', `--registry=${NPMJS_REGISTRY}`, '--access', 'public'];
|
|
247
401
|
if (dryRun) args.push('--dry-run');
|
|
248
|
-
|
|
249
|
-
|
|
402
|
+
const resultado = ejecutarPublishConOTP(args);
|
|
403
|
+
if (resultado.ok) {
|
|
250
404
|
console.log(`${dryRun ? '[DRY-RUN] ' : ''}OK: ${pkg.name}@${pkg.version} ${dryRun ? 'se publicaría' : 'publicado'} en npmjs`);
|
|
251
405
|
return true;
|
|
252
|
-
} catch (err) {
|
|
253
|
-
console.error(`ERROR publicando en npmjs: ${err.message}`);
|
|
254
|
-
return false;
|
|
255
406
|
}
|
|
407
|
+
console.error(`ERROR publicando en npmjs (ver stderr arriba para diagnóstico de npm).`);
|
|
408
|
+
return false;
|
|
256
409
|
}
|
|
257
410
|
|
|
258
411
|
function publicarGitHub(pkg, dryRun) {
|
|
@@ -272,17 +425,16 @@ function publicarGitHub(pkg, dryRun) {
|
|
|
272
425
|
|
|
273
426
|
const args = ['publish', `--registry=${GITHUB_REGISTRY}`];
|
|
274
427
|
if (dryRun) args.push('--dry-run');
|
|
275
|
-
|
|
276
|
-
|
|
428
|
+
const resultado = ejecutarPublishConOTP(args, { cwd: tmpDir });
|
|
429
|
+
if (resultado.ok) {
|
|
277
430
|
console.log(`${dryRun ? '[DRY-RUN] ' : ''}OK: ${GITHUB_NAME}@${pkg.version} ${dryRun ? 'se publicaría' : 'publicado'} en GitHub Packages`);
|
|
278
431
|
if (dryRun) console.log(`Directorio temporal: ${tmpDir}`);
|
|
279
432
|
limpiar(tmpDir);
|
|
280
433
|
return true;
|
|
281
|
-
} catch (err) {
|
|
282
|
-
console.error(`ERROR publicando en GitHub Packages: ${err.message}`);
|
|
283
|
-
limpiar(tmpDir);
|
|
284
|
-
return false;
|
|
285
434
|
}
|
|
435
|
+
console.error(`ERROR publicando en GitHub Packages (ver stderr arriba para diagnóstico de npm).`);
|
|
436
|
+
limpiar(tmpDir);
|
|
437
|
+
return false;
|
|
286
438
|
}
|
|
287
439
|
|
|
288
440
|
function parsearArgs(argv) {
|
|
@@ -350,6 +502,8 @@ if (require.main === module) {
|
|
|
350
502
|
prepararDirectorioTemporal,
|
|
351
503
|
copiarDir,
|
|
352
504
|
limpiar,
|
|
505
|
+
esErrorOTP,
|
|
506
|
+
ejecutarPublishConOTP,
|
|
353
507
|
GITHUB_NAME,
|
|
354
508
|
GITHUB_REGISTRY,
|
|
355
509
|
NPMJS_REGISTRY,
|