@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.
@@ -1,124 +1,200 @@
1
- 'use strict';
2
-
3
- const TransformadorBase = require('./base');
4
-
5
- class TransformadorClaude extends TransformadorBase {
6
- transformarAgente(contenidoMd, metadatos) {
7
- // Pass-through: Claude soporta el formato SWL directamente
8
- return {
9
- contenido: contenidoMd,
10
- rutaRelativa: `agents/${metadatos.nombreArchivo}`,
11
- };
12
- }
13
-
14
- transformarSkill(dirSkill, metadatos) {
15
- // Pass-through: copiar directorio completo
16
- return {
17
- tipo: 'directorio',
18
- rutaRelativa: `skills/${metadatos.nombreDirectorio}`,
19
- };
20
- }
21
-
22
- transformarRegla(contenidoMd, metadatos) {
23
- return {
24
- contenido: contenidoMd,
25
- rutaRelativa: `rules/${metadatos.nombreArchivo}`,
26
- };
27
- }
28
-
29
- transformarComando(contenidoMd, metadatos) {
30
- return {
31
- contenido: contenidoMd,
32
- rutaRelativa: `commands/swl/${metadatos.nombreArchivo}`,
33
- };
34
- }
35
-
36
- /**
37
- * Genera el bloque SWL para insertar en el CLAUDE.md del proyecto destino.
38
- *
39
- * A diferencia de los otros transformadores (codex/gemini/copilot/opencode)
40
- * que generan el archivo completo de instrucciones desde cero, Claude usa
41
- * un CLAUDE.md que el usuario mantiene y que puede tener contenido propio
42
- * (reglas del proyecto, convenciones, notas). Por eso:
43
- *
44
- * - El contenido devuelto es un BLOQUE (no archivo completo)
45
- * - Se devuelve `merge: { tipo: 'marcadores', ... }` para indicar al
46
- * caller que debe usar `append-con-marcadores` en lugar de writeFileSync
47
- * - Los marcadores `<!-- SWL-BEGIN vX.Y.Z -->` / `<!-- SWL-END -->`
48
- * permiten que la siguiente actualización reemplace solo este bloque
49
- * sin tocar el contenido del usuario
50
- *
51
- * El bloque es CONCISO a propósito: apunta a `.claude/` y a los comandos
52
- * `/swl:*` en lugar de replicar el contenido completo del sistema. Objetivo:
53
- * que Claude sepa que el sistema SWL está instalado y debe priorizarlo,
54
- * sin inflar el CLAUDE.md del proyecto.
55
- */
56
- generarArchivoInstrucciones(contexto) {
57
- const fecha = new Date().toISOString().split('T')[0];
58
- const lineas = [
59
- `> Gestionado por swl-ses v${contexto.version} — no editar entre estos marcadores.`,
60
- `> Regenerado en ${fecha} con perfil \`${contexto.perfil || 'default'}\`.`,
61
- `> Para excluir este bloque en futuras actualizaciones: \`npx swl-ses@latest install --no-claudemd\`.`,
62
- '',
63
- '## Sistema SWL — uso obligatorio',
64
- '',
65
- `Este proyecto usa swl-ses v${contexto.version} (sistema de ingeniería de software auto-evolutivo multi-runtime).`,
66
- `Componentes instalados: ${contexto.conteos?.agentes || 0} agentes, ${contexto.conteos?.skills || 0} skills, ${contexto.conteos?.comandos || 0} comandos, ${contexto.conteos?.reglas || 0} reglas.`,
67
- '',
68
- '### Reglas de máxima prioridad',
69
- '',
70
- '1. **Idioma: español de México** — respuestas, código comentado, commits, PRs y docs. Evitar anglicismos innecesarios.',
71
- '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.',
72
- '3. **Investigar antes de editar** leer el archivo completo y entender el contexto antes de modificar código.',
73
- '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.',
74
- '',
75
- '### Dónde están los componentes',
76
- '',
77
- '- Agentes: `.claude/agents/*.md`',
78
- '- Skills: `.claude/skills/*/SKILL.md` (invocar con `Skill("nombre")`)',
79
- '- Comandos: `.claude/commands/swl/*.md` (invocar con `/swl:nombre`)',
80
- '- Reglas: `.claude/rules/*.md` (se cargan automáticamente)',
81
- '- Hooks: registrados en `.claude/settings.json`',
82
- '',
83
- '### Flujos recomendados',
84
- '',
85
- '- **Feature completa**: `/swl:discutir-fase N` → `/swl:planear-fase N` → `/swl:ejecutar-fase N` → `/swl:verificar`',
86
- '- **Debugging**: delegar a `depurador-swl` con método científico (reproducir → aislar → hipótesis → fix → verificar)',
87
- '- **Calidad**: `/swl:revisar` (código) + `revisor-seguridad-swl` antes de merge a main',
88
- '- **Aprendizaje continuo**: `/swl:aprender` al final de fase, `/swl:evolucionar` ante fallos recurrentes de agentes',
89
- '',
90
- '### Consulta del sistema',
91
- '',
92
- '- `/swl:ayuda` catálogo completo de comandos',
93
- '- `/swl:salud` — diagnóstico de integridad',
94
- '- `/swl:metricas` tokens, costo y modelos usados en la sesión',
95
- '- `/swl:skill-search <keyword>` buscar skills relevantes',
96
- '',
97
- '### Convenciones de versionado y commits',
98
- '',
99
- '- Versionado semántico estricto (SemVer)',
100
- '- Mensajes de commit en español, formato `<tipo>(<scope>): <descripción en imperativo>`',
101
- '- Commits atómicos (un cambio lógico = un commit)',
102
- '- Antes de release: `/swl:release` y gate `node scripts/verificar-release.js`',
103
- ];
104
-
105
- return {
106
- contenido: lineas.join('\n'),
107
- rutaRelativa: 'CLAUDE.md',
108
- dirBase: '.',
109
- merge: {
110
- tipo: 'marcadores',
111
- beginTag: `<!-- SWL-BEGIN v${contexto.version}bloque gestionado por swl-ses -->`,
112
- endTag: '<!-- SWL-END -->',
113
- beginPrefix: '<!-- SWL-BEGIN',
114
- },
115
- };
116
- }
117
-
118
- transformarHooks(hooksConfig, opciones) {
119
- // Los hooks de Claude se manejan via hooks-settings.js existente
120
- return { formato: 'claude-settings', config: hooksConfig };
121
- }
122
- }
123
-
124
- module.exports = TransformadorClaude;
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;
@@ -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
- try {
249
- npmExec(args);
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
- try {
276
- npmExec(args, { cwd: tmpDir });
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,