@saulwade/swl-ses 1.3.2 → 1.3.3

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,190 +1,190 @@
1
- #!/usr/bin/env node
2
- /**
3
- * generar-skills-lock.js
4
- *
5
- * Genera `manifiestos/skills-lock.json` con SHA256 de cada `habilidades/*\/SKILL.md`.
6
- * Permite detectar drift silencioso: alguien edita un skill localmente sin
7
- * commit y nadie lo nota hasta que falla. El lock se regenera en cada
8
- * `/swl:release` y se valida en `/swl:salud`.
9
- *
10
- * Uso:
11
- * node scripts/generar-skills-lock.js # genera y escribe
12
- * node scripts/generar-skills-lock.js --check # compara, falla si hay drift
13
- * node scripts/generar-skills-lock.js --json # imprime resumen JSON
14
- *
15
- * Origen: análisis de temp/design.md-main/skills-lock.json (mayo 2026).
16
- *
17
- * Zero-dependencies. Compatible Windows (CRLF-safe), Node 18+.
18
- */
19
-
20
- 'use strict';
21
-
22
- const fs = require('fs');
23
- const path = require('path');
24
- const crypto = require('crypto');
25
-
26
- const ROOT = path.resolve(__dirname, '..');
27
-
28
- // Escritura atómica obligatoria (regla CLAUDE.md). Fallback defensivo.
29
- let atomicWriteSync;
30
- try {
31
- ({ atomicWriteSync } = require(path.join(ROOT, 'hooks', 'lib', 'atomic-write.js')));
32
- } catch {
33
- atomicWriteSync = (p, c, e) => fs.writeFileSync(p, c, e);
34
- }
35
- const HABILIDADES_DIR = path.join(ROOT, 'habilidades');
36
- const LOCK_FILE = path.join(ROOT, 'manifiestos', 'skills-lock.json');
37
- const LOCK_VERSION = 1;
38
-
39
- const CHECK = process.argv.includes('--check');
40
- const JSON_OUT = process.argv.includes('--json');
41
-
42
- function sha256(buffer) {
43
- return crypto.createHash('sha256').update(buffer).digest('hex');
44
- }
45
-
46
- function leerFrontmatter(contenido) {
47
- const m = contenido.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
48
- if (!m) return {};
49
- const fm = {};
50
- for (const linea of m[1].split(/\r?\n/)) {
51
- const kv = linea.match(/^([a-zA-Z_][a-zA-Z0-9_-]*):\s*(.*)$/);
52
- if (kv) fm[kv[1]] = kv[2].trim();
53
- }
54
- return fm;
55
- }
56
-
57
- function generarLock() {
58
- if (!fs.existsSync(HABILIDADES_DIR)) {
59
- console.error(`ERROR: directorio no encontrado: ${HABILIDADES_DIR}`);
60
- process.exit(2);
61
- }
62
-
63
- const skills = [];
64
- const dirs = fs
65
- .readdirSync(HABILIDADES_DIR, { withFileTypes: true })
66
- .filter((d) => d.isDirectory())
67
- .map((d) => d.name)
68
- .sort();
69
-
70
- for (const dir of dirs) {
71
- const skillFile = path.join(HABILIDADES_DIR, dir, 'SKILL.md');
72
- if (!fs.existsSync(skillFile)) {
73
- // Skill sin SKILL.md — anomalía pero no fatal
74
- skills.push({
75
- nombre: dir,
76
- path: `habilidades/${dir}/SKILL.md`,
77
- hash: null,
78
- bytes: 0,
79
- warning: 'skill-md-faltante',
80
- });
81
- continue;
82
- }
83
-
84
- const buffer = fs.readFileSync(skillFile);
85
- const contenido = buffer.toString('utf-8');
86
- const fm = leerFrontmatter(contenido);
87
-
88
- skills.push({
89
- nombre: fm.name || dir,
90
- path: `habilidades/${dir}/SKILL.md`,
91
- hash: `sha256:${sha256(buffer)}`,
92
- bytes: buffer.length,
93
- version: fm.version || null,
94
- });
95
- }
96
-
97
- // Hash agregado del lock completo (excluyendo timestamps)
98
- const datosCanonicos = JSON.stringify(
99
- skills.map((s) => ({ nombre: s.nombre, hash: s.hash })),
100
- null,
101
- 0
102
- );
103
- const lockHash = `sha256:${sha256(Buffer.from(datosCanonicos, 'utf-8'))}`;
104
-
105
- return {
106
- lockfileVersion: LOCK_VERSION,
107
- generatedAt: new Date().toISOString(),
108
- skillsCount: skills.length,
109
- lockHash,
110
- skills,
111
- };
112
- }
113
-
114
- function comparar(actual, anterior) {
115
- if (!anterior) return { drifted: [], faltantes: actual.skills, nuevos: [] };
116
-
117
- const anteriorMap = new Map(anterior.skills.map((s) => [s.nombre, s]));
118
- const actualMap = new Map(actual.skills.map((s) => [s.nombre, s]));
119
-
120
- const drifted = [];
121
- const nuevos = [];
122
- const faltantes = [];
123
-
124
- for (const s of actual.skills) {
125
- const prev = anteriorMap.get(s.nombre);
126
- if (!prev) {
127
- nuevos.push(s.nombre);
128
- } else if (prev.hash !== s.hash) {
129
- drifted.push({ nombre: s.nombre, antes: prev.hash, ahora: s.hash });
130
- }
131
- }
132
- for (const s of anterior.skills) {
133
- if (!actualMap.has(s.nombre)) {
134
- faltantes.push(s.nombre);
135
- }
136
- }
137
-
138
- return { drifted, faltantes, nuevos };
139
- }
140
-
141
- function main() {
142
- const actual = generarLock();
143
-
144
- if (CHECK) {
145
- if (!fs.existsSync(LOCK_FILE)) {
146
- console.error(`ERROR: ${LOCK_FILE} no existe. Ejecutar sin --check para generarlo.`);
147
- process.exit(1);
148
- }
149
- const anterior = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf-8'));
150
- const diff = comparar(actual, anterior);
151
- const total = diff.drifted.length + diff.faltantes.length + diff.nuevos.length;
152
-
153
- if (JSON_OUT) {
154
- console.log(JSON.stringify({ ok: total === 0, ...diff }, null, 2));
155
- } else if (total === 0) {
156
- console.log(`✓ skills-lock OK: ${actual.skillsCount} skills sin drift`);
157
- } else {
158
- console.log(`✗ skills-lock DRIFT detectado:`);
159
- if (diff.drifted.length) {
160
- console.log(` Modificados (${diff.drifted.length}):`);
161
- diff.drifted.forEach((d) => console.log(` - ${d.nombre}`));
162
- }
163
- if (diff.nuevos.length) {
164
- console.log(` Nuevos (${diff.nuevos.length}):`);
165
- diff.nuevos.forEach((n) => console.log(` - ${n}`));
166
- }
167
- if (diff.faltantes.length) {
168
- console.log(` Faltantes (${diff.faltantes.length}):`);
169
- diff.faltantes.forEach((n) => console.log(` - ${n}`));
170
- }
171
- console.log(`\n Para regenerar: node scripts/generar-skills-lock.js`);
172
- }
173
- process.exit(total === 0 ? 0 : 1);
174
- }
175
-
176
- // Generar
177
- atomicWriteSync(LOCK_FILE, JSON.stringify(actual, null, 2) + '\n', 'utf-8');
178
- if (JSON_OUT) {
179
- console.log(JSON.stringify({ ok: true, file: LOCK_FILE, count: actual.skillsCount }));
180
- } else {
181
- console.log(`✓ Generado ${LOCK_FILE}`);
182
- console.log(` ${actual.skillsCount} skills, lockHash=${actual.lockHash.slice(0, 23)}...`);
183
- }
184
- }
185
-
186
- if (require.main === module) {
187
- main();
188
- }
189
-
190
- module.exports = { generarLock, comparar };
1
+ #!/usr/bin/env node
2
+ /**
3
+ * generar-skills-lock.js
4
+ *
5
+ * Genera `manifiestos/skills-lock.json` con SHA256 de cada `habilidades/*\/SKILL.md`.
6
+ * Permite detectar drift silencioso: alguien edita un skill localmente sin
7
+ * commit y nadie lo nota hasta que falla. El lock se regenera en cada
8
+ * `/swl:release` y se valida en `/swl:salud`.
9
+ *
10
+ * Uso:
11
+ * node scripts/generar-skills-lock.js # genera y escribe
12
+ * node scripts/generar-skills-lock.js --check # compara, falla si hay drift
13
+ * node scripts/generar-skills-lock.js --json # imprime resumen JSON
14
+ *
15
+ * Origen: análisis de temp/design.md-main/skills-lock.json (mayo 2026).
16
+ *
17
+ * Zero-dependencies. Compatible Windows (CRLF-safe), Node 18+.
18
+ */
19
+
20
+ 'use strict';
21
+
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+ const crypto = require('crypto');
25
+
26
+ const ROOT = path.resolve(__dirname, '..');
27
+
28
+ // Escritura atómica obligatoria (regla CLAUDE.md). Fallback defensivo.
29
+ let atomicWriteSync;
30
+ try {
31
+ ({ atomicWriteSync } = require(path.join(ROOT, 'hooks', 'lib', 'atomic-write.js')));
32
+ } catch {
33
+ atomicWriteSync = (p, c, e) => fs.writeFileSync(p, c, e);
34
+ }
35
+ const HABILIDADES_DIR = path.join(ROOT, 'habilidades');
36
+ const LOCK_FILE = path.join(ROOT, 'manifiestos', 'skills-lock.json');
37
+ const LOCK_VERSION = 1;
38
+
39
+ const CHECK = process.argv.includes('--check');
40
+ const JSON_OUT = process.argv.includes('--json');
41
+
42
+ function sha256(buffer) {
43
+ return crypto.createHash('sha256').update(buffer).digest('hex');
44
+ }
45
+
46
+ function leerFrontmatter(contenido) {
47
+ const m = contenido.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
48
+ if (!m) return {};
49
+ const fm = {};
50
+ for (const linea of m[1].split(/\r?\n/)) {
51
+ const kv = linea.match(/^([a-zA-Z_][a-zA-Z0-9_-]*):\s*(.*)$/);
52
+ if (kv) fm[kv[1]] = kv[2].trim();
53
+ }
54
+ return fm;
55
+ }
56
+
57
+ function generarLock() {
58
+ if (!fs.existsSync(HABILIDADES_DIR)) {
59
+ console.error(`ERROR: directorio no encontrado: ${HABILIDADES_DIR}`);
60
+ process.exit(2);
61
+ }
62
+
63
+ const skills = [];
64
+ const dirs = fs
65
+ .readdirSync(HABILIDADES_DIR, { withFileTypes: true })
66
+ .filter((d) => d.isDirectory())
67
+ .map((d) => d.name)
68
+ .sort();
69
+
70
+ for (const dir of dirs) {
71
+ const skillFile = path.join(HABILIDADES_DIR, dir, 'SKILL.md');
72
+ if (!fs.existsSync(skillFile)) {
73
+ // Skill sin SKILL.md — anomalía pero no fatal
74
+ skills.push({
75
+ nombre: dir,
76
+ path: `habilidades/${dir}/SKILL.md`,
77
+ hash: null,
78
+ bytes: 0,
79
+ warning: 'skill-md-faltante',
80
+ });
81
+ continue;
82
+ }
83
+
84
+ const buffer = fs.readFileSync(skillFile);
85
+ const contenido = buffer.toString('utf-8');
86
+ const fm = leerFrontmatter(contenido);
87
+
88
+ skills.push({
89
+ nombre: fm.name || dir,
90
+ path: `habilidades/${dir}/SKILL.md`,
91
+ hash: `sha256:${sha256(buffer)}`,
92
+ bytes: buffer.length,
93
+ version: fm.version || null,
94
+ });
95
+ }
96
+
97
+ // Hash agregado del lock completo (excluyendo timestamps)
98
+ const datosCanonicos = JSON.stringify(
99
+ skills.map((s) => ({ nombre: s.nombre, hash: s.hash })),
100
+ null,
101
+ 0
102
+ );
103
+ const lockHash = `sha256:${sha256(Buffer.from(datosCanonicos, 'utf-8'))}`;
104
+
105
+ return {
106
+ lockfileVersion: LOCK_VERSION,
107
+ generatedAt: new Date().toISOString(),
108
+ skillsCount: skills.length,
109
+ lockHash,
110
+ skills,
111
+ };
112
+ }
113
+
114
+ function comparar(actual, anterior) {
115
+ if (!anterior) return { drifted: [], faltantes: actual.skills, nuevos: [] };
116
+
117
+ const anteriorMap = new Map(anterior.skills.map((s) => [s.nombre, s]));
118
+ const actualMap = new Map(actual.skills.map((s) => [s.nombre, s]));
119
+
120
+ const drifted = [];
121
+ const nuevos = [];
122
+ const faltantes = [];
123
+
124
+ for (const s of actual.skills) {
125
+ const prev = anteriorMap.get(s.nombre);
126
+ if (!prev) {
127
+ nuevos.push(s.nombre);
128
+ } else if (prev.hash !== s.hash) {
129
+ drifted.push({ nombre: s.nombre, antes: prev.hash, ahora: s.hash });
130
+ }
131
+ }
132
+ for (const s of anterior.skills) {
133
+ if (!actualMap.has(s.nombre)) {
134
+ faltantes.push(s.nombre);
135
+ }
136
+ }
137
+
138
+ return { drifted, faltantes, nuevos };
139
+ }
140
+
141
+ function main() {
142
+ const actual = generarLock();
143
+
144
+ if (CHECK) {
145
+ if (!fs.existsSync(LOCK_FILE)) {
146
+ console.error(`ERROR: ${LOCK_FILE} no existe. Ejecutar sin --check para generarlo.`);
147
+ process.exit(1);
148
+ }
149
+ const anterior = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf-8'));
150
+ const diff = comparar(actual, anterior);
151
+ const total = diff.drifted.length + diff.faltantes.length + diff.nuevos.length;
152
+
153
+ if (JSON_OUT) {
154
+ console.log(JSON.stringify({ ok: total === 0, ...diff }, null, 2));
155
+ } else if (total === 0) {
156
+ console.log(`✓ skills-lock OK: ${actual.skillsCount} skills sin drift`);
157
+ } else {
158
+ console.log(`✗ skills-lock DRIFT detectado:`);
159
+ if (diff.drifted.length) {
160
+ console.log(` Modificados (${diff.drifted.length}):`);
161
+ diff.drifted.forEach((d) => console.log(` - ${d.nombre}`));
162
+ }
163
+ if (diff.nuevos.length) {
164
+ console.log(` Nuevos (${diff.nuevos.length}):`);
165
+ diff.nuevos.forEach((n) => console.log(` - ${n}`));
166
+ }
167
+ if (diff.faltantes.length) {
168
+ console.log(` Faltantes (${diff.faltantes.length}):`);
169
+ diff.faltantes.forEach((n) => console.log(` - ${n}`));
170
+ }
171
+ console.log(`\n Para regenerar: node scripts/generar-skills-lock.js`);
172
+ }
173
+ process.exit(total === 0 ? 0 : 1);
174
+ }
175
+
176
+ // Generar
177
+ atomicWriteSync(LOCK_FILE, JSON.stringify(actual, null, 2) + '\n', 'utf-8');
178
+ if (JSON_OUT) {
179
+ console.log(JSON.stringify({ ok: true, file: LOCK_FILE, count: actual.skillsCount }));
180
+ } else {
181
+ console.log(`✓ Generado ${LOCK_FILE}`);
182
+ console.log(` ${actual.skillsCount} skills, lockHash=${actual.lockHash.slice(0, 23)}...`);
183
+ }
184
+ }
185
+
186
+ if (require.main === module) {
187
+ main();
188
+ }
189
+
190
+ module.exports = { generarLock, comparar, main };
@@ -159,3 +159,9 @@ function main() {
159
159
  }
160
160
 
161
161
  if (require.main === module) main();
162
+
163
+ // Exporta main para que el wrapper del bin CLI
164
+ // (`scripts/cli/inbox-tmux-inject.js`) pueda invocarla sin depender del
165
+ // guard `require.main === module`, que solo es true cuando el script se
166
+ // ejecuta como punto de entrada directo (no cuando es require()d).
167
+ module.exports = { main };
@@ -187,13 +187,15 @@ function buscar(cwd, query) {
187
187
  // Exports
188
188
  // ---------------------------------------------------------------------------
189
189
 
190
- module.exports = { descubrir, buscar, cargarIndice, escanear };
191
-
192
190
  // ---------------------------------------------------------------------------
193
191
  // CLI directo
194
192
  // ---------------------------------------------------------------------------
195
193
 
196
- if (require.main === module) {
194
+ // Extraído a función nombrada para que el wrapper del bin CLI
195
+ // (`scripts/cli/skill-discovery.js`) pueda invocarla sin depender del guard
196
+ // `require.main === module`, que solo se cumple cuando el script se ejecuta
197
+ // como punto de entrada directo (no cuando es require()d desde otro módulo).
198
+ function main() {
197
199
  const cwd = process.cwd();
198
200
  const args = process.argv.slice(2);
199
201
 
@@ -232,3 +234,9 @@ if (require.main === module) {
232
234
  process.stdout.write(`Uso: Skill("nombre-de-habilidad")\n\n`);
233
235
  }
234
236
  }
237
+
238
+ if (require.main === module) {
239
+ main();
240
+ }
241
+
242
+ module.exports = { descubrir, buscar, cargarIndice, escanear, main };
@@ -286,4 +286,4 @@ function main() {
286
286
 
287
287
  if (require.main === module) main();
288
288
 
289
- module.exports = { verificarArchivo, extraerFrontmatter, leerCampo, obtenerVersionEnHEAD };
289
+ module.exports = { verificarArchivo, extraerFrontmatter, leerCampo, obtenerVersionEnHEAD, main };