refacil-sdd-ai 3.0.2 → 3.1.0

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/bin/cli.js CHANGED
@@ -1,1407 +1,400 @@
1
- #!/usr/bin/env node
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const {
6
- syncCompactGuidance,
7
- removeCompactGuidance,
8
- } = require('../lib/compact-guidance');
9
- const compactBash = require('../lib/compact/bash');
10
- const compactTelemetry = require('../lib/compact/telemetry');
11
- const busBroker = require('../lib/bus/broker');
12
- const busSpawn = require('../lib/bus/spawn');
13
- const busClient = require('../lib/bus/client');
14
- const busWatch = require('../lib/bus/watch');
15
- const busPresenter = require('../lib/bus/presenter');
16
-
17
- const SKILLS = [
18
- 'setup',
19
- 'prereqs',
20
- 'guide',
21
- 'explore',
22
- 'propose',
23
- 'apply',
24
- 'test',
25
- 'verify',
26
- 'review',
27
- 'archive',
28
- 'bug',
29
- 'up-code',
30
- // refacil-bus (agent chat room)
31
- 'join',
32
- 'say',
33
- 'ask',
34
- 'reply',
35
- 'inbox',
36
- 'attend',
37
- ];
38
-
39
- // Sub-agentes instalados en .claude/agents/ y .cursor/agents/.
40
- // Fuente: refacil-sdd-ai/agents/<name>.md (un solo archivo, frontmatter estilo Claude Code).
41
- // El installer lo copia verbatim a Claude y transforma el frontmatter para Cursor.
42
- const AGENTS = [
43
- 'auditor',
44
- 'investigator',
45
- 'validator',
46
- ];
47
-
48
- const packageRoot = path.resolve(__dirname, '..');
49
- const projectRoot = process.cwd();
50
-
51
- function copyDir(src, dest) {
52
- fs.mkdirSync(dest, { recursive: true });
53
- const entries = fs.readdirSync(src, { withFileTypes: true });
54
- for (const entry of entries) {
55
- const srcPath = path.join(src, entry.name);
56
- const destPath = path.join(dest, entry.name);
57
- if (entry.isDirectory()) {
58
- copyDir(srcPath, destPath);
59
- } else {
60
- fs.copyFileSync(srcPath, destPath);
61
- }
62
- }
63
- }
64
-
65
- function installSkills() {
66
- let installed = 0;
67
-
68
- for (const skill of SKILLS) {
69
- const srcDir = path.join(packageRoot, 'skills', skill);
70
- if (!fs.existsSync(srcDir)) continue;
71
-
72
- // Copy to .claude/skills/
73
- const claudeDest = path.join(projectRoot, '.claude', 'skills', `refacil-${skill}`);
74
- copyDir(srcDir, claudeDest);
75
-
76
- // Copy to .cursor/skills/
77
- const cursorDest = path.join(projectRoot, '.cursor', 'skills', `refacil-${skill}`);
78
- copyDir(srcDir, cursorDest);
79
-
80
- installed++;
81
- }
82
-
83
- return installed;
84
- }
85
-
86
- // Transforma el frontmatter de un sub-agente Claude Code al formato Cursor.
87
- // Claude Code: `tools:` (allowlist granular), `model: sonnet|opus|haiku`
88
- // Cursor: `readonly: true|false` (booleano), `model: inherit` (default).
89
- //
90
- // Reglas:
91
- // - Si tools NO incluye Edit ni Write → readonly: true (reviewer-style, read-only).
92
- // - Si tools SI incluye Edit o Write → readonly: false.
93
- // - model: sonnet|opus|haiku → model: inherit (Cursor decide).
94
- // - model: <id explicito tipo claude-sonnet-4-6> → se mantiene.
95
- // - Body (todo despues del segundo ---) se preserva verbatim.
96
- function transformFrontmatterForCursor(content) {
97
- const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
98
- if (!match) return content; // sin frontmatter reconocible, copiamos tal cual
99
-
100
- const [, frontmatterRaw, body] = match;
101
- const lines = frontmatterRaw.split('\n');
102
- const out = [];
103
- let toolsLine = null;
104
- let hasReadonly = false;
105
-
106
- for (const line of lines) {
107
- if (line.startsWith('tools:')) {
108
- toolsLine = line;
109
- continue; // se omite: Cursor no lo entiende
110
- }
111
- if (line.startsWith('readonly:')) {
112
- hasReadonly = true;
113
- out.push(line);
114
- continue;
115
- }
116
- if (line.startsWith('model:')) {
117
- const value = line.slice('model:'.length).trim();
118
- if (value === 'sonnet' || value === 'opus' || value === 'haiku') {
119
- out.push('model: inherit');
120
- } else {
121
- out.push(line);
122
- }
123
- continue;
124
- }
125
- out.push(line);
126
- }
127
-
128
- // Si el agente declaraba tools explicito y no tenia readonly,
129
- // inferimos readonly a partir de tools.
130
- if (toolsLine && !hasReadonly) {
131
- const toolsList = toolsLine.slice('tools:'.length).trim();
132
- const canWrite = /\b(Edit|Write|NotebookEdit)\b/.test(toolsList);
133
- out.push(`readonly: ${canWrite ? 'false' : 'true'}`);
134
- }
135
-
136
- return `---\n${out.join('\n')}\n---\n${body}`;
137
- }
138
-
139
- function installAgents() {
140
- let installed = 0;
141
-
142
- const claudeDir = path.join(projectRoot, '.claude', 'agents');
143
- const cursorDir = path.join(projectRoot, '.cursor', 'agents');
144
- fs.mkdirSync(claudeDir, { recursive: true });
145
- fs.mkdirSync(cursorDir, { recursive: true });
146
-
147
- for (const agent of AGENTS) {
148
- const srcFile = path.join(packageRoot, 'agents', `${agent}.md`);
149
- if (!fs.existsSync(srcFile)) continue;
150
-
151
- const content = fs.readFileSync(srcFile, 'utf8');
152
-
153
- // Claude Code: copia verbatim
154
- const claudeDest = path.join(claudeDir, `refacil-${agent}.md`);
155
- fs.writeFileSync(claudeDest, content);
156
-
157
- // Cursor: transforma frontmatter
158
- const cursorDest = path.join(cursorDir, `refacil-${agent}.md`);
159
- fs.writeFileSync(cursorDest, transformFrontmatterForCursor(content));
160
-
161
- installed++;
162
- }
163
-
164
- return installed;
165
- }
166
-
167
- const SDD_SECTION_MARKER = '## Metodologia SDD-AI (Refacil)';
168
-
169
- function extractSddSection(templateContent) {
170
- const idx = templateContent.indexOf(SDD_SECTION_MARKER);
171
- if (idx === -1) return templateContent;
172
- return templateContent.substring(idx);
173
- }
174
-
175
- function readMethodologyGuide() {
176
- return fs.readFileSync(
177
- path.join(packageRoot, 'templates', 'methodology-guide.md'),
178
- 'utf8',
179
- );
180
- }
181
-
182
- function writeGuideFile(destPath, header, label) {
183
- const guide = readMethodologyGuide();
184
- const content = `# ${header}\n\n${guide}`;
185
-
186
- if (fs.existsSync(destPath)) {
187
- const existing = fs.readFileSync(destPath, 'utf8');
188
- if (existing.includes(SDD_SECTION_MARKER)) {
189
- console.log(` ${label} ya tiene la seccion SDD-AI. Sin cambios.`);
190
- return false;
191
- }
192
- const sddSection = extractSddSection(guide);
193
- fs.writeFileSync(destPath, existing.trimEnd() + '\n\n' + sddSection + '\n');
194
- console.log(` ${label} existente — seccion SDD-AI agregada al final.`);
195
- return true;
196
- }
197
-
198
- fs.writeFileSync(destPath, content);
199
- return true;
200
- }
201
-
202
- function createClaudeMd() {
203
- return writeGuideFile(
204
- path.join(projectRoot, 'CLAUDE.md'),
205
- 'CLAUDE.md',
206
- 'CLAUDE.md',
207
- );
208
- }
209
-
210
- function createCursorRules() {
211
- return writeGuideFile(
212
- path.join(projectRoot, '.cursorrules'),
213
- 'Cursor Rules',
214
- '.cursorrules',
215
- );
216
- }
217
-
218
- const REPO_VERSION_FILES = ['.claude/.sdd-version', '.cursor/.sdd-version'];
219
-
220
- function readRepoVersion(rootDir) {
221
- for (const rel of REPO_VERSION_FILES) {
222
- const p = path.join(rootDir, rel);
223
- try {
224
- const raw = fs.readFileSync(p, 'utf8').trim();
225
- if (raw) return raw;
226
- } catch (_) {
227
- // continuar al siguiente
228
- }
229
- }
230
- return null;
231
- }
232
-
233
- function writeRepoVersion(rootDir, version) {
234
- for (const rel of REPO_VERSION_FILES) {
235
- const p = path.join(rootDir, rel);
236
- const parent = path.dirname(p);
237
- if (!fs.existsSync(parent)) continue;
238
- try {
239
- fs.writeFileSync(p, String(version) + '\n');
240
- } catch (_) {
241
- // tolerante
242
- }
243
- }
244
- }
245
-
246
- function getPackageVersion() {
247
- try {
248
- return require(path.join(packageRoot, 'package.json')).version;
249
- } catch (_) {
250
- return null;
251
- }
252
- }
253
-
254
- function removeSkills() {
255
- let removed = 0;
256
- for (const skill of SKILLS) {
257
- const claudeDir = path.join(projectRoot, '.claude', 'skills', `refacil-${skill}`);
258
- const cursorDir = path.join(projectRoot, '.cursor', 'skills', `refacil-${skill}`);
259
-
260
- if (fs.existsSync(claudeDir)) {
261
- fs.rmSync(claudeDir, { recursive: true });
262
- removed++;
263
- }
264
- if (fs.existsSync(cursorDir)) {
265
- fs.rmSync(cursorDir, { recursive: true });
266
- }
267
- }
268
- return removed;
269
- }
270
-
271
- function checkClaudeCodeVersion() {
272
- const { execSync } = require('child_process');
273
- try {
274
- const output = execSync('claude --version 2>&1', {
275
- encoding: 'utf8',
276
- timeout: 5000,
277
- stdio: ['pipe', 'pipe', 'pipe'],
278
- }).trim();
279
- const match = output.match(/(\d+)\.(\d+)\.(\d+)/);
280
- if (!match) return { ok: null, version: null };
281
- const maj = Number(match[1]);
282
- const min = Number(match[2]);
283
- const patch = Number(match[3]);
284
- const ok =
285
- maj > 2 ||
286
- (maj === 2 && min > 1) ||
287
- (maj === 2 && min === 1 && patch >= 89);
288
- return { ok, version: `${maj}.${min}.${patch}` };
289
- } catch (_) {
290
- return { ok: null, version: null };
291
- }
292
- }
293
-
294
- function checkNodeVersion() {
295
- const version = process.version; // e.g. v20.19.5
296
- const major = parseInt(version.split('.')[0].replace('v', ''));
297
- const minor = parseInt(version.split('.')[1]);
298
-
299
- if (major < 20 || (major === 20 && minor < 19)) {
300
- console.log(`\n ADVERTENCIA: Node.js ${version} detectado.`);
301
- console.log(' OpenSpec requiere Node.js >= 20.19.0.');
302
- console.log(' Las skills se instalaran pero /refacil:setup podria fallar al instalar OpenSpec.\n');
303
- return false;
304
- }
305
- return true;
306
- }
307
-
308
- // --- Hook installation ---
309
-
310
- function installHook() {
311
- const settingsDir = path.join(projectRoot, '.claude');
312
- const settingsPath = path.join(settingsDir, 'settings.json');
313
- let settings = {};
314
-
315
- if (fs.existsSync(settingsPath)) {
316
- settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
317
- }
318
-
319
- if (!settings.hooks) settings.hooks = {};
320
- if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
321
-
322
- let changed = false;
323
-
324
- // SessionStart: check-update
325
- const hasUpdateHook = settings.hooks.SessionStart.some(h => h._sdd === true);
326
- if (!hasUpdateHook) {
327
- settings.hooks.SessionStart.push({
328
- _sdd: true,
329
- matcher: '',
330
- hooks: [
331
- {
332
- type: 'command',
333
- command: 'refacil-sdd-ai check-update',
334
- },
335
- ],
336
- });
337
- changed = true;
338
- }
339
-
340
- // PreToolUse
341
- if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
342
-
343
- // compact-bash (must run BEFORE check-review so rewrite is visible to subsequent hooks)
344
- const hasCompactHook = settings.hooks.PreToolUse.some(
345
- (h) => h._sdd_compact === true,
346
- );
347
- if (!hasCompactHook) {
348
- settings.hooks.PreToolUse.unshift({
349
- _sdd_compact: true,
350
- matcher: 'Bash',
351
- hooks: [
352
- {
353
- type: 'command',
354
- command: 'refacil-sdd-ai compact-bash',
355
- },
356
- ],
357
- });
358
- changed = true;
359
- }
360
-
361
- // check-review
362
- const hasReviewHook = settings.hooks.PreToolUse.some(
363
- (h) => h._sdd_review === true,
364
- );
365
- if (!hasReviewHook) {
366
- settings.hooks.PreToolUse.push({
367
- _sdd_review: true,
368
- matcher: 'Bash',
369
- hooks: [
370
- {
371
- type: 'command',
372
- command: 'refacil-sdd-ai check-review',
373
- },
374
- ],
375
- });
376
- changed = true;
377
- }
378
-
379
- if (!changed) {
380
- console.log(' Hooks SDD-AI ya configurados.');
381
- return false;
382
- }
383
-
384
- fs.mkdirSync(settingsDir, { recursive: true });
385
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
386
- return true;
387
- }
388
-
389
- function uninstallHook() {
390
- const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
391
- if (!fs.existsSync(settingsPath)) return false;
392
-
393
- let settings;
394
- try {
395
- settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
396
- } catch (_) {
397
- console.log(' No se pudieron remover hooks: .claude/settings.json invalido.');
398
- return false;
399
- }
400
-
401
- if (!settings.hooks) return false;
402
-
403
- let changed = false;
404
-
405
- if (Array.isArray(settings.hooks.SessionStart)) {
406
- const original = settings.hooks.SessionStart.length;
407
- settings.hooks.SessionStart = settings.hooks.SessionStart.filter(
408
- (h) => h._sdd !== true,
409
- );
410
- if (settings.hooks.SessionStart.length !== original) changed = true;
411
- if (settings.hooks.SessionStart.length === 0) delete settings.hooks.SessionStart;
412
- }
413
-
414
- if (Array.isArray(settings.hooks.PreToolUse)) {
415
- const original = settings.hooks.PreToolUse.length;
416
- settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
417
- (h) => h._sdd_review !== true && h._sdd_compact !== true,
418
- );
419
- if (settings.hooks.PreToolUse.length !== original) changed = true;
420
- if (settings.hooks.PreToolUse.length === 0) delete settings.hooks.PreToolUse;
421
- }
422
-
423
- if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
424
-
425
- if (!changed) return false;
426
-
427
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
428
- return true;
429
- }
430
-
431
- // --- Check update ---
432
-
433
- function repoIsInitialized() {
434
- return (
435
- fs.existsSync(path.join(projectRoot, '.claude', 'skills')) ||
436
- fs.existsSync(path.join(projectRoot, '.cursor', 'skills'))
437
- );
438
- }
439
-
440
- function syncRepoSkillsIfStale(globalVersion) {
441
- // Si el paquete global ya está en globalVersion pero este repo tiene skills
442
- // de una versión anterior (porque otro repo disparó el auto-upgrade), hay
443
- // que re-copiarlas aquí. Idem si nunca se marcó la versión en este repo.
444
- if (!repoIsInitialized()) return null;
445
- const repoVersion = readRepoVersion(projectRoot);
446
- if (repoVersion === globalVersion) return null;
447
-
448
- const { execSync } = require('child_process');
449
- const localCli = path.join(packageRoot, 'bin', 'cli.js');
450
- try {
451
- execSync(`"${process.execPath}" "${localCli}" update`, {
452
- encoding: 'utf8',
453
- timeout: 30000,
454
- stdio: ['pipe', 'pipe', 'pipe'],
455
- });
456
- writeRepoVersion(projectRoot, globalVersion);
457
- return { from: repoVersion, to: globalVersion };
458
- } catch (_) {
459
- return { from: repoVersion, to: globalVersion, failed: true };
460
- }
461
- }
462
-
463
- function checkUpdate() {
464
- const { execSync } = require('child_process');
465
- const localVersion = getPackageVersion();
466
-
467
- // Always ensure AGENTS.md has current compact-guidance block (silent unless error)
468
- try {
469
- syncCompactGuidance(projectRoot, packageRoot);
470
- } catch (err) {
471
- process.stderr.write(
472
- `[refacil-sdd-ai] No se pudo sincronizar compact-guidance: ${err.message}\n`,
473
- );
474
- }
475
-
476
- // Paso 1: sincronizar skills del repo actual si quedaron desfasadas respecto
477
- // al paquete global (pasa cuando otro repo ya disparó el auto-upgrade).
478
- const syncResult = syncRepoSkillsIfStale(localVersion);
479
- if (syncResult && !syncResult.failed) {
480
- const fromLabel = syncResult.from ? `v${syncResult.from}` : 'version desconocida';
481
- console.log(
482
- `[refacil-sdd-ai] Skills de este repo sincronizadas (${fromLabel} -> v${syncResult.to}). ` +
483
- 'Reinicia la sesion de Claude Code o Cursor para detectar los cambios.',
484
- );
485
- } else if (syncResult && syncResult.failed) {
486
- console.log(
487
- `[refacil-sdd-ai] Skills de este repo estan desactualizadas respecto al paquete global (v${syncResult.to}) ` +
488
- 'pero la sincronizacion automatica fallo. Ejecuta manualmente: refacil-sdd-ai update',
489
- );
490
- }
491
-
492
- // Paso 2: chequear si hay version nueva en npm y, si la hay, actualizar
493
- // global + copiar skills al repo actual.
494
- try {
495
- const latest = execSync('npm view refacil-sdd-ai version', {
496
- encoding: 'utf8',
497
- timeout: 10000,
498
- stdio: ['pipe', 'pipe', 'pipe'],
499
- }).trim();
500
-
501
- if (!latest || latest === localVersion) return;
502
-
503
- try {
504
- execSync('npm update -g refacil-sdd-ai', {
505
- encoding: 'utf8',
506
- timeout: 60000,
507
- stdio: ['pipe', 'pipe', 'pipe'],
508
- });
509
- execSync('refacil-sdd-ai update', {
510
- encoding: 'utf8',
511
- timeout: 30000,
512
- stdio: ['pipe', 'pipe', 'pipe'],
513
- });
514
- writeRepoVersion(projectRoot, latest);
515
- console.log(
516
- `[refacil-sdd-ai] La metodologia SDD-AI se actualizo automaticamente de v${localVersion} a v${latest}. Skills y hooks sincronizados.`,
517
- );
518
- } catch (_) {
519
- console.log(
520
- `[refacil-sdd-ai] Hay una nueva version disponible (v${localVersion} -> v${latest}) pero la actualizacion automatica fallo. ` +
521
- `Informa al usuario que ejecute manualmente: npm update -g refacil-sdd-ai && refacil-sdd-ai update`,
522
- );
523
- }
524
- } catch (_) {
525
- // Silent on error (no internet, registry unreachable, etc.)
526
- }
527
- }
528
-
529
- // --- Check review (PreToolUse hook) ---
530
-
531
- function checkReview() {
532
- // Read stdin (JSON from PreToolUse hook)
533
- let input;
534
- try {
535
- const stdin = fs.readFileSync(0, 'utf8');
536
- input = JSON.parse(stdin);
537
- } catch (_) {
538
- // If no stdin or invalid JSON, allow (not called from hook context)
539
- return;
540
- }
541
-
542
- // Only block git push commands
543
- const command = (input.tool_input && input.tool_input.command) || '';
544
- if (!command.match(/git\s+push/)) return;
545
-
546
- // Find active change in openspec/changes/ (exclude archive/)
547
- const changesDir = path.join(projectRoot, 'openspec', 'changes');
548
- if (!fs.existsSync(changesDir)) return; // No openspec, allow
549
-
550
- const entries = fs.readdirSync(changesDir, { withFileTypes: true });
551
- const activeChanges = entries.filter(
552
- (e) => e.isDirectory() && e.name !== 'archive',
553
- );
554
-
555
- if (activeChanges.length === 0) return; // No active changes, allow
556
-
557
- // Check if any active change is missing .review-passed
558
- const missing = activeChanges.filter(
559
- (e) => !fs.existsSync(path.join(changesDir, e.name, '.review-passed')),
560
- );
561
-
562
- if (missing.length > 0) {
563
- const names = missing.map((e) => e.name).join(', ');
564
- const reason =
565
- missing.length === 1
566
- ? `[refacil-sdd-ai] Review pendiente para: ${names}. ` +
567
- 'Deten el push y ejecuta /refacil:review sobre ese cambio antes de subir codigo. ' +
568
- 'Si el review aprueba, reintenta el git push. ' +
569
- 'Si el review requiere correcciones, informa los hallazgos al usuario y NO reintentar el push.'
570
- : `[refacil-sdd-ai] Hay multiples cambios sin review aprobado: ${names}. ` +
571
- 'Deten el push y pide al usuario seleccionar explicitamente que cambio quiere subir. ' +
572
- 'Luego ejecuta /refacil:review <nombre-cambio> para ese cambio especifico y reintenta el push. ' +
573
- 'No ejecutes review automatico sin seleccion cuando hay mas de un cambio pendiente.';
574
- console.log(
575
- JSON.stringify({
576
- decision: 'block',
577
- reason,
578
- }),
579
- );
580
- }
581
- }
582
-
583
- // --- Commands ---
584
-
585
- function init() {
586
- console.log('\n refacil-sdd-ai: Inicializando metodologia SDD-AI...\n');
587
-
588
- // Check Node version
589
- const nodeOk = checkNodeVersion();
590
- if (nodeOk) {
591
- console.log(` Node.js ${process.version} OK`);
592
- }
593
-
594
- // Check Claude Code version (for compact-bash hook)
595
- const claudeCheck = checkClaudeCodeVersion();
596
- if (claudeCheck.ok === true) {
597
- console.log(` Claude Code ${claudeCheck.version} OK`);
598
- } else if (claudeCheck.ok === false) {
599
- console.log(`\n ADVERTENCIA: Claude Code ${claudeCheck.version} detectado.`);
600
- console.log(' El hook compact-bash requiere Claude Code >= 2.1.89 para rewrite silencioso.');
601
- console.log(' Con version inferior se instala igual pero el rewrite no tendra efecto.');
602
- console.log(' Actualiza con: npm install -g @anthropic-ai/claude-code\n');
603
- }
604
- // ok === null: claude no esta en PATH, silencioso
605
-
606
- // Install skills
607
- const count = installSkills();
608
- console.log(` ${count} skills instaladas en .claude/skills/ y .cursor/skills/`);
609
-
610
- // Install sub-agents
611
- const agentsCount = installAgents();
612
- if (agentsCount > 0) {
613
- console.log(` ${agentsCount} sub-agentes instalados en .claude/agents/ y .cursor/agents/`);
614
- }
615
-
616
- writeRepoVersion(projectRoot, getPackageVersion());
617
-
618
- // Create or update CLAUDE.md
619
- if (createClaudeMd()) {
620
- console.log(' CLAUDE.md OK');
621
- }
622
-
623
- // Create or update .cursorrules
624
- if (createCursorRules()) {
625
- console.log(' .cursorrules OK');
626
- }
627
-
628
- // Install SessionStart hook for version check
629
- if (installHook()) {
630
- console.log(' Hook check-update agregado a .claude/settings.json');
631
- }
632
-
633
- // Sync compact-guidance block in AGENTS.md (if it exists)
634
- try {
635
- const result = syncCompactGuidance(projectRoot, packageRoot);
636
- if (result.status === 'appended') {
637
- console.log(' Bloque compact-guidance agregado a AGENTS.md');
638
- } else if (result.status === 'replaced') {
639
- console.log(' Bloque compact-guidance actualizado en AGENTS.md');
640
- }
641
- } catch (err) {
642
- console.error(` Advertencia: no se pudo sincronizar compact-guidance: ${err.message}`);
643
- }
644
-
645
- console.log('\n Siguientes pasos:\n');
646
- console.log(' 1. REINICIA tu sesion de Claude Code o Cursor');
647
- console.log(' (las skills nuevas no se detectan hasta reiniciar)\n');
648
- console.log(' 2. Ejecuta: /refacil:setup');
649
- console.log(' (instala OpenSpec y genera AGENTS.md para tu proyecto)\n');
650
- console.log(' Nota: /refacil:setup tambien instalara los comandos de OpenSpec (opsx:*).');
651
- console.log(' Los comandos refacil:* y opsx:* coexisten sin conflicto.');
652
- console.log(' El equipo debe usar los refacil:* como interfaz principal.\n');
653
- }
654
-
655
- function update() {
656
- console.log('\n refacil-sdd-ai: Actualizando skills...\n');
657
- const count = installSkills();
658
- console.log(` ${count} skills actualizadas en .claude/skills/ y .cursor/skills/`);
659
-
660
- const agentsCount = installAgents();
661
- if (agentsCount > 0) {
662
- console.log(` ${agentsCount} sub-agentes actualizados en .claude/agents/ y .cursor/agents/`);
663
- }
664
-
665
- writeRepoVersion(projectRoot, getPackageVersion());
666
-
667
- // Ensure hook is installed (for users updating from versions without hook)
668
- if (installHook()) {
669
- console.log(' Hook check-update agregado a .claude/settings.json');
670
- }
671
-
672
- // Sync compact-guidance block in AGENTS.md
673
- try {
674
- const result = syncCompactGuidance(projectRoot, packageRoot);
675
- if (result.status === 'appended') {
676
- console.log(' Bloque compact-guidance agregado a AGENTS.md');
677
- } else if (result.status === 'replaced') {
678
- console.log(' Bloque compact-guidance actualizado en AGENTS.md');
679
- }
680
- } catch (err) {
681
- console.error(` Advertencia: no se pudo sincronizar compact-guidance: ${err.message}`);
682
- }
683
-
684
- console.log('\n REINICIA tu sesion de Claude Code o Cursor para aplicar los cambios.\n');
685
- }
686
-
687
- function clean() {
688
- console.log('\n refacil-sdd-ai: Eliminando skills...\n');
689
- const count = removeSkills();
690
- console.log(` ${count} skills eliminadas de .claude/skills/ y .cursor/skills/`);
691
- if (uninstallHook()) {
692
- console.log(' Hooks SDD-AI removidos de .claude/settings.json');
693
- } else {
694
- console.log(' No se encontraron hooks SDD-AI para remover.');
695
- }
696
-
697
- // Remove compact-guidance block from AGENTS.md if present
698
- try {
699
- const result = removeCompactGuidance(projectRoot);
700
- if (result.status === 'removed') {
701
- console.log(' Bloque compact-guidance removido de AGENTS.md');
702
- }
703
- } catch (err) {
704
- console.error(` Advertencia: no se pudo limpiar compact-guidance: ${err.message}`);
705
- }
706
-
707
- console.log(' AGENTS.md, CLAUDE.md y .cursorrules no fueron eliminados.');
708
- console.log('\n Nota: Los comandos opsx:* de OpenSpec no se eliminan.');
709
- console.log(' Para eliminar OpenSpec: rm -rf openspec/ .claude/commands/opsx .cursor/commands/opsx\n');
710
- }
711
-
712
- // --- Compact subcommands (stats / enable / disable / clear-log) ---
713
-
714
- function handleCompactSubcommand(sub) {
715
- switch (sub) {
716
- case 'stats':
717
- showCompactStats();
718
- break;
719
- case 'disable':
720
- compactTelemetry.disable();
721
- console.log(' compact-bash deshabilitado. Reactiva con: refacil-sdd-ai compact enable');
722
- break;
723
- case 'enable':
724
- compactTelemetry.enable();
725
- console.log(' compact-bash habilitado.');
726
- break;
727
- case 'clear-log':
728
- compactTelemetry.clearLog();
729
- console.log(' compact.log limpiado.');
730
- break;
731
- default:
732
- console.log('Uso: refacil-sdd-ai compact <stats|disable|enable|clear-log>');
733
- }
734
- }
735
-
736
- function showCompactStats() {
737
- const s = compactTelemetry.stats();
738
- if (s.totalEvents === 0) {
739
- console.log('\n No hay eventos registrados todavia. Ejecuta comandos Bash para generar telemetria de compactacion.\n');
740
- return;
741
- }
742
-
743
- const sortedRewrites = Object.entries(s.byRule)
744
- .filter(([, data]) => data.rewriteCount > 0)
745
- .sort((a, b) => b[1].rewriteSaved - a[1].rewriteSaved);
746
- const sortedAlreadyCompact = Object.entries(s.byRule)
747
- .filter(([, data]) => data.alreadyCompactCount > 0)
748
- .sort((a, b) => b[1].alreadyCompactPotential - a[1].alreadyCompactPotential);
749
-
750
- console.log(`\n compact-bash stats\n`);
751
- console.log(` Rewrites por hook: ${s.totalRewrites}`);
752
- console.log(` Comandos ya compactos detectados (skill/agente): ${s.totalAlreadyCompact}\n`);
753
-
754
- if (sortedRewrites.length > 0) {
755
- console.log(' Ahorro aplicado por hook (rewrite):');
756
- for (const [id, data] of sortedRewrites) {
757
- const kTokens = (data.rewriteSaved / 1000).toFixed(1);
758
- console.log(
759
- ` ${id.padEnd(18)} ${String(data.rewriteCount).padStart(6)} rewrites ~${kTokens.padStart(7)}k tokens`,
760
- );
761
- }
762
- const totalHookK = (s.totalSaved / 1000).toFixed(1);
763
- const hookUsd = ((s.totalSaved / 1_000_000) * 3).toFixed(2);
764
- console.log(` ${'-'.repeat(62)}`);
765
- console.log(
766
- ` ${'Total hook'.padEnd(18)} ${String(s.totalRewrites).padStart(6)} rewrites ~${totalHookK.padStart(7)}k tokens (~$${hookUsd} USD)`,
767
- );
768
- console.log('');
769
- }
770
-
771
- if (sortedAlreadyCompact.length > 0) {
772
- console.log(' Ahorro potencial ya capturado por skill/agente (sin rewrite):');
773
- for (const [id, data] of sortedAlreadyCompact) {
774
- const kTokens = (data.alreadyCompactPotential / 1000).toFixed(1);
775
- console.log(
776
- ` ${id.padEnd(18)} ${String(data.alreadyCompactCount).padStart(6)} eventos ~${kTokens.padStart(7)}k tokens potenciales`,
777
- );
778
- }
779
- const totalAgentK = (s.totalAlreadyCompactPotential / 1000).toFixed(1);
780
- const agentUsd = ((s.totalAlreadyCompactPotential / 1_000_000) * 3).toFixed(2);
781
- console.log(` ${'-'.repeat(62)}`);
782
- console.log(
783
- ` ${'Total skill'.padEnd(18)} ${String(s.totalAlreadyCompact).padStart(6)} eventos ~${totalAgentK.padStart(7)}k tokens (~$${agentUsd} USD)`,
784
- );
785
- console.log('');
786
- }
787
-
788
- const totalK = (s.totalObservedPotential / 1000).toFixed(1);
789
- const totalUsd = ((s.totalObservedPotential / 1_000_000) * 3).toFixed(2);
790
- console.log(` ${'-'.repeat(62)}`);
791
- console.log(
792
- ` ${'Total observado'.padEnd(18)} ${String(s.totalEvents).padStart(6)} eventos ~${totalK.padStart(7)}k tokens (~$${totalUsd} USD, Sonnet input)`,
793
- );
794
- console.log(`\n Log: ${compactTelemetry.LOG_PATH}`);
795
- if (compactTelemetry.isDisabled()) {
796
- console.log(' Estado: DESHABILITADO (no se registran nuevos eventos)');
797
- }
798
- console.log('');
799
- }
800
-
801
- // --- Bus subcommands (refacil-bus: broker core — fase 1) ---
802
-
803
- async function busStart() {
804
- try {
805
- const { info, started } = await busSpawn.ensureBroker(packageRoot);
806
- if (started) {
807
- console.log(` refacil-bus broker iniciado en 127.0.0.1:${info.port} (pid ${info.pid}).`);
808
- } else {
809
- console.log(` refacil-bus broker ya estaba activo en 127.0.0.1:${info.port} (pid ${info.pid}).`);
810
- }
811
- } catch (err) {
812
- console.error(` No se pudo iniciar el broker: ${err.message}`);
813
- process.exit(1);
814
- }
815
- }
816
-
817
- function busStop() {
818
- const result = busSpawn.stopBroker();
819
- if (result.stopped) {
820
- console.log(` refacil-bus broker detenido (pid ${result.info.pid}).`);
821
- } else if (result.reason === 'no-info') {
822
- console.log(' refacil-bus broker no está corriendo.');
823
- } else if (result.reason === 'not-alive') {
824
- console.log(' refacil-bus broker no estaba vivo; info obsoleta limpiada.');
825
- } else {
826
- console.error(` No se pudo detener el broker: ${result.reason}`);
827
- process.exit(1);
828
- }
829
- }
830
-
831
- async function busStatus() {
832
- const status = await busSpawn.isBrokerAlive();
833
- if (!status.alive) {
834
- console.log(' refacil-bus broker: INACTIVO');
835
- if (status.staleInfo) {
836
- console.log(` (info obsoleta encontrada: pid ${status.staleInfo.pid}, puerto ${status.staleInfo.port})`);
837
- }
838
- return;
839
- }
840
- const info = status.info;
841
- const uptimeMs = Date.now() - new Date(info.startedAt).getTime();
842
- const uptimeMin = Math.floor(uptimeMs / 60000);
843
- console.log(' refacil-bus broker: ACTIVO');
844
- console.log(` host: 127.0.0.1`);
845
- console.log(` puerto: ${info.port}`);
846
- console.log(` pid: ${info.pid}`);
847
- console.log(` iniciado: ${info.startedAt}`);
848
- console.log(` uptime: ${uptimeMin} min`);
849
- console.log(` info: ${busBroker.BUS_INFO_PATH}`);
850
- }
851
-
852
- function busServe() {
853
- // Invocado por spawn detached — ejecuta el broker en foreground.
854
- busBroker.start().catch((err) => {
855
- process.stderr.write(`Error arrancando broker: ${err.message}\n`);
856
- process.exit(1);
857
- });
858
- }
859
-
860
- function parseBusArgs(argv) {
861
- const args = {};
862
- for (let i = 0; i < argv.length; i++) {
863
- const token = argv[i];
864
- if (!token || !token.startsWith('--')) continue;
865
- const key = token.slice(2);
866
- const next = argv[i + 1];
867
- if (next === undefined || next.startsWith('--')) {
868
- args[key] = true;
869
- } else {
870
- args[key] = next;
871
- i++;
872
- }
873
- }
874
- return args;
875
- }
876
-
877
- function defaultSessionName() {
878
- return path.basename(process.cwd()) || 'sesion';
879
- }
880
-
881
- async function connectOrDie() {
882
- try {
883
- const { info } = await busSpawn.ensureBroker(packageRoot);
884
- const ws = await busClient.connect(info.port);
885
- return { ws, info };
886
- } catch (err) {
887
- console.error(` No se pudo conectar al bus: ${err.message}`);
888
- process.exit(1);
889
- }
890
- }
891
-
892
- function formatMessage(m) {
893
- const target = m.to ? ` → @${m.to}` : '';
894
- return ` [${m.ts}] ${m.from}${target} (${m.kind}): ${m.text}`;
895
- }
896
-
897
- async function busJoin(args) {
898
- const session = args.session || defaultSessionName();
899
- const room = args.room;
900
- const repo = args.repo || process.cwd();
901
- let intro = args.intro;
902
- if (!intro) {
903
- try {
904
- intro = busPresenter.buildIntro({ repoDir: repo, session });
905
- } catch (_) {
906
- intro = `${session} se unió a la sala`;
907
- }
908
- }
909
- if (!room) {
910
- console.error(' Uso: refacil-sdd-ai bus join --room <sala> [--session <s>] [--intro "..."]');
911
- process.exit(1);
912
- }
913
- const { ws } = await connectOrDie();
914
- const reply = await busClient.sendAndWait(
915
- ws,
916
- 'join',
917
- { session, room, repo, intro },
918
- (d) => d.type === 'system' && d.event === 'joined',
919
- 3000,
920
- );
921
- busClient.close(ws);
922
- if (!reply) {
923
- console.error(' Timeout uniéndose a la sala.');
924
- process.exit(1);
925
- }
926
- const members = (reply.detail && reply.detail.members) || [];
927
- console.log(` Unido a la sala "${room}" como "${session}".`);
928
- console.log(` Miembros actuales: ${members.join(', ') || '(solo tú)'}`);
929
- console.log(` Para consultarte: /refacil:ask @${session} "..."`);
930
- }
931
-
932
- async function busLeave(args) {
933
- const session = args.session || defaultSessionName();
934
- const { ws } = await connectOrDie();
935
- const reply = await busClient.sendAndWait(
936
- ws,
937
- 'leave',
938
- { session },
939
- (d) => d.type === 'system' && (d.event === 'left' || d.event === 'error'),
940
- 3000,
941
- );
942
- busClient.close(ws);
943
- if (reply && reply.event === 'left') {
944
- console.log(` "${session}" salió de la sala.`);
945
- } else {
946
- console.log(` "${session}" no estaba en ninguna sala.`);
947
- }
948
- }
949
-
950
- async function busSay(args) {
951
- const session = args.session || defaultSessionName();
952
- const text = args.text;
953
- if (!text) {
954
- console.error(' Uso: refacil-sdd-ai bus say --text "..." [--session <s>]');
955
- process.exit(1);
956
- }
957
- const { ws } = await connectOrDie();
958
- const reply = await busClient.sendAndWait(
959
- ws,
960
- 'say',
961
- { session, text },
962
- (d) => d.type === 'system' && (d.event === 'sent' || d.event === 'error'),
963
- 3000,
964
- );
965
- busClient.close(ws);
966
- if (reply && reply.event === 'sent') {
967
- console.log(` Mensaje enviado (id ${reply.detail.id}).`);
968
- } else {
969
- const detail = (reply && reply.detail) || 'sin respuesta';
970
- console.error(` No se pudo enviar: ${detail}`);
971
- process.exit(1);
972
- }
973
- }
974
-
975
- async function busAsk(args) {
976
- const session = args.session || defaultSessionName();
977
- const to = args.to;
978
- const text = args.text;
979
- const waitSec = args.wait ? parseInt(args.wait, 10) : 0;
980
- if (!to || !text) {
981
- console.error(' Uso: refacil-sdd-ai bus ask --to <name> --text "..." [--wait N] [--session <s>]');
982
- process.exit(1);
983
- }
984
- const { ws } = await connectOrDie();
985
- const ack = await busClient.sendAndWait(
986
- ws,
987
- 'ask',
988
- { session, to: to.replace(/^@/, ''), text },
989
- (d) => d.type === 'system' && (d.event === 'sent' || d.event === 'error'),
990
- 3000,
991
- );
992
- if (!ack || ack.event !== 'sent') {
993
- busClient.close(ws);
994
- const detail = (ack && ack.detail) || 'sin respuesta';
995
- console.error(` No se pudo enviar la pregunta: ${detail}`);
996
- process.exit(1);
997
- }
998
- const correlationId = ack.detail.correlationId;
999
- console.log(` Pregunta enviada a @${to.replace(/^@/, '')} (correlationId ${correlationId}).`);
1000
-
1001
- if (waitSec > 0) {
1002
- console.log(` Esperando respuesta hasta ${waitSec}s...`);
1003
- const resp = await busClient.sendAndWait(
1004
- ws,
1005
- 'ping',
1006
- {},
1007
- (d) => d.type === 'msg' && d.kind === 'reply' && d.correlationId === correlationId,
1008
- waitSec * 1000,
1009
- );
1010
- busClient.close(ws);
1011
- if (!resp) {
1012
- console.log(` Sin respuesta en ${waitSec}s. Usa /refacil:inbox más tarde para recuperarla.`);
1013
- return;
1014
- }
1015
- console.log(` Respuesta de @${resp.from}:`);
1016
- console.log(` ${resp.text}`);
1017
- } else {
1018
- busClient.close(ws);
1019
- console.log(' Usa /refacil:inbox para ver respuestas.');
1020
- }
1021
- }
1022
-
1023
- async function busReply(args) {
1024
- const session = args.session || defaultSessionName();
1025
- const text = args.text;
1026
- const correlationId = args.correlation || null;
1027
- const to = args.to ? args.to.replace(/^@/, '') : null;
1028
- if (!text) {
1029
- console.error(' Uso: refacil-sdd-ai bus reply --text "..." [--to <name>] [--correlation <id>]');
1030
- process.exit(1);
1031
- }
1032
- const { ws } = await connectOrDie();
1033
- const reply = await busClient.sendAndWait(
1034
- ws,
1035
- 'reply',
1036
- { session, text, to, correlationId },
1037
- (d) => d.type === 'system' && (d.event === 'sent' || d.event === 'error'),
1038
- 3000,
1039
- );
1040
- busClient.close(ws);
1041
- if (reply && reply.event === 'sent') {
1042
- console.log(` Respuesta enviada (id ${reply.detail.id}).`);
1043
- } else {
1044
- const detail = (reply && reply.detail) || 'sin respuesta';
1045
- console.error(` No se pudo responder: ${detail}`);
1046
- process.exit(1);
1047
- }
1048
- }
1049
-
1050
- async function busHistory(args) {
1051
- const session = args.session || defaultSessionName();
1052
- const n = args.n ? parseInt(args.n, 10) : 20;
1053
- const { ws } = await connectOrDie();
1054
- const reply = await busClient.sendAndWait(
1055
- ws,
1056
- 'history',
1057
- { session, n },
1058
- (d) => d.type === 'history',
1059
- 3000,
1060
- );
1061
- busClient.close(ws);
1062
- if (!reply) {
1063
- console.log(' Sin historial.');
1064
- return;
1065
- }
1066
- const msgs = reply.messages || [];
1067
- if (msgs.length === 0) {
1068
- console.log(' Sin historial.');
1069
- return;
1070
- }
1071
- console.log(` Últimos ${msgs.length} mensajes:`);
1072
- for (const m of msgs) console.log(formatMessage(m));
1073
- }
1074
-
1075
- async function busInbox(args) {
1076
- const session = args.session || defaultSessionName();
1077
- const { ws } = await connectOrDie();
1078
- const reply = await busClient.sendAndWait(
1079
- ws,
1080
- 'inbox',
1081
- { session },
1082
- (d) => d.type === 'inbox',
1083
- 3000,
1084
- );
1085
- busClient.close(ws);
1086
- if (!reply) {
1087
- console.log(' Sin respuesta del broker.');
1088
- return;
1089
- }
1090
- const msgs = reply.messages || [];
1091
- if (msgs.length === 0) {
1092
- console.log(' Sin mensajes nuevos.');
1093
- return;
1094
- }
1095
- console.log(` ${msgs.length} mensaje(s) nuevo(s):`);
1096
- for (const m of msgs) console.log(formatMessage(m));
1097
- }
1098
-
1099
- function findFirstUnansweredAsk(messages, session) {
1100
- const asks = messages.filter((m) => m.kind === 'ask' && m.to === session);
1101
- for (const ask of asks) {
1102
- const hasReply = messages.some(
1103
- (m) => m.kind === 'reply' && m.correlationId === ask.correlationId,
1104
- );
1105
- if (!hasReply) return ask;
1106
- }
1107
- return null;
1108
- }
1109
-
1110
- function printAttendQuestion(msg) {
1111
- console.log(' Pregunta recibida del bus:');
1112
- console.log(` de: @${msg.from}`);
1113
- console.log(` correlationId: ${msg.correlationId || '(sin id)'}`);
1114
- console.log(` texto: ${msg.text}`);
1115
- console.log('');
1116
- console.log(' Responde con: /refacil:reply "<respuesta>"');
1117
- console.log(' Luego vuelve a ejecutar /refacil:attend para seguir escuchando.');
1118
- }
1119
-
1120
- async function busAttend(args) {
1121
- const session = args.session || defaultSessionName();
1122
- const timeoutSec = args.timeout ? parseInt(args.timeout, 10) : 540;
1123
- const { ws } = await connectOrDie();
1124
-
1125
- // 1) Revisar preguntas pendientes en el historial antes de suscribirse al push.
1126
- const hist = await busClient.sendAndWait(
1127
- ws,
1128
- 'history',
1129
- { session, n: 50 },
1130
- (d) => d.type === 'history',
1131
- 3000,
1132
- );
1133
- if (hist) {
1134
- const pending = findFirstUnansweredAsk(hist.messages || [], session);
1135
- if (pending) {
1136
- busClient.close(ws);
1137
- printAttendQuestion(pending);
1138
- return;
1139
- }
1140
- }
1141
-
1142
- // 2) Suscribirse y esperar push de ask dirigido a esta sesión.
1143
- const result = await new Promise((resolve) => {
1144
- let done = false;
1145
- const finish = (v) => {
1146
- if (done) return;
1147
- done = true;
1148
- clearTimeout(timer);
1149
- ws.removeListener('message', onMessage);
1150
- resolve(v);
1151
- };
1152
- const onMessage = (raw) => {
1153
- let data;
1154
- try { data = JSON.parse(raw.toString()); } catch (_) { return; }
1155
- if (data.type === 'msg' && data.kind === 'ask' && data.to === session) {
1156
- finish({ kind: 'message', msg: data });
1157
- }
1158
- };
1159
- ws.on('message', onMessage);
1160
- const timer = setTimeout(() => finish({ kind: 'timeout' }), timeoutSec * 1000);
1161
- busClient.send(ws, 'attend', { session });
1162
- });
1163
-
1164
- busClient.close(ws);
1165
-
1166
- if (result.kind === 'message') {
1167
- printAttendQuestion(result.msg);
1168
- } else {
1169
- console.log(` Sin preguntas en ${timeoutSec}s. Re-ejecuta /refacil:attend para seguir escuchando.`);
1170
- }
1171
- }
1172
-
1173
- function readPersistedSessions() {
1174
- try {
1175
- return JSON.parse(fs.readFileSync(busBroker.SESSIONS_PATH, 'utf8'));
1176
- } catch (_) {
1177
- return {};
1178
- }
1179
- }
1180
-
1181
- async function busWatchCmd(positional, args) {
1182
- const session = args.session || positional || null;
1183
- let room = args.room || null;
1184
- if (session && !room) {
1185
- const persisted = readPersistedSessions();
1186
- if (persisted[session] && persisted[session].room) {
1187
- room = persisted[session].room;
1188
- }
1189
- }
1190
- if (!session && !room) {
1191
- console.error(' Uso: refacil-sdd-ai bus watch <session> [--room <sala>]');
1192
- process.exit(1);
1193
- }
1194
- try {
1195
- const { info } = await busSpawn.ensureBroker(packageRoot);
1196
- await busWatch.start({ session, room, port: info.port });
1197
- } catch (err) {
1198
- console.error(` No se pudo iniciar el watch: ${err.message}`);
1199
- process.exit(1);
1200
- }
1201
- }
1202
-
1203
- function openInBrowser(url) {
1204
- const { spawn } = require('child_process');
1205
- const platform = process.platform;
1206
- let cmd;
1207
- let args;
1208
- if (platform === 'win32') {
1209
- cmd = 'cmd';
1210
- args = ['/c', 'start', '""', url];
1211
- } else if (platform === 'darwin') {
1212
- cmd = 'open';
1213
- args = [url];
1214
- } else {
1215
- cmd = 'xdg-open';
1216
- args = [url];
1217
- }
1218
- try {
1219
- spawn(cmd, args, { detached: true, stdio: 'ignore', windowsHide: true }).unref();
1220
- return true;
1221
- } catch (_) {
1222
- return false;
1223
- }
1224
- }
1225
-
1226
- async function busView() {
1227
- try {
1228
- const { info } = await busSpawn.ensureBroker(packageRoot);
1229
- const url = `http://127.0.0.1:${info.port}/`;
1230
- console.log(` refacil-bus view disponible en: ${url}`);
1231
- const opened = openInBrowser(url);
1232
- if (!opened) {
1233
- console.log(' (no se pudo abrir el navegador automáticamente, abre la URL manualmente)');
1234
- }
1235
- } catch (err) {
1236
- console.error(` No se pudo iniciar la vista: ${err.message}`);
1237
- process.exit(1);
1238
- }
1239
- }
1240
-
1241
- async function busRooms() {
1242
- const { ws } = await connectOrDie();
1243
- const reply = await busClient.sendAndWait(
1244
- ws,
1245
- 'status',
1246
- {},
1247
- (d) => d.type === 'system' && d.event === 'status',
1248
- 3000,
1249
- );
1250
- busClient.close(ws);
1251
- if (!reply) {
1252
- console.log(' Sin respuesta del broker.');
1253
- return;
1254
- }
1255
- const rooms = (reply.detail && reply.detail.rooms) || {};
1256
- const names = Object.keys(rooms);
1257
- if (names.length === 0) {
1258
- console.log(' No hay salas activas.');
1259
- return;
1260
- }
1261
- console.log(' Salas activas:');
1262
- for (const name of names) {
1263
- const members = rooms[name] || [];
1264
- console.log(` ${name} (${members.length}): ${members.join(', ')}`);
1265
- }
1266
- }
1267
-
1268
- function handleBusSubcommand(sub) {
1269
- const rest = process.argv.slice(4);
1270
- const positional = rest.length > 0 && !rest[0].startsWith('--') ? rest[0] : null;
1271
- const args = parseBusArgs(rest);
1272
- switch (sub) {
1273
- case 'start':
1274
- busStart();
1275
- break;
1276
- case 'stop':
1277
- busStop();
1278
- break;
1279
- case 'status':
1280
- busStatus();
1281
- break;
1282
- case 'serve':
1283
- busServe();
1284
- break;
1285
- case 'join':
1286
- busJoin(args);
1287
- break;
1288
- case 'leave':
1289
- busLeave(args);
1290
- break;
1291
- case 'say':
1292
- busSay(args);
1293
- break;
1294
- case 'ask':
1295
- busAsk(args);
1296
- break;
1297
- case 'reply':
1298
- busReply(args);
1299
- break;
1300
- case 'history':
1301
- busHistory(args);
1302
- break;
1303
- case 'inbox':
1304
- busInbox(args);
1305
- break;
1306
- case 'rooms':
1307
- busRooms();
1308
- break;
1309
- case 'watch':
1310
- busWatchCmd(positional, args);
1311
- break;
1312
- case 'attend':
1313
- busAttend(args);
1314
- break;
1315
- case 'view':
1316
- busView();
1317
- break;
1318
- default:
1319
- console.log('Uso: refacil-sdd-ai bus <start|stop|status|serve|join|leave|say|ask|reply|history|inbox|rooms|watch|attend|view>');
1320
- }
1321
- }
1322
-
1323
- function help() {
1324
- console.log(`
1325
- refacil-sdd-ai — Metodologia SDD-AI con OpenSpec
1326
-
1327
- Comandos:
1328
- init Instala skills en .claude/ y .cursor/, crea CLAUDE.md y .cursorrules
1329
- update Re-copia skills (para actualizar a nueva version del paquete)
1330
- check-update Verifica si hay una version mas reciente en npm y sincroniza compact-guidance en AGENTS.md
1331
- check-review Verifica que el review se haya completado (usado por hook PreToolUse)
1332
- compact-bash Reescribe comandos Bash bare para reducir tokens (usado por hook PreToolUse)
1333
- compact Subcomandos del hook compact-bash:
1334
- compact stats - Estadisticas completas (hook + ya-compacto) y ahorro estimado
1335
- compact disable - Desactiva el rewrite temporalmente
1336
- compact enable - Re-activa el rewrite
1337
- compact clear-log - Borra el log historico
1338
- bus Subcomandos del chat room entre agentes (refacil-bus):
1339
- bus start - Arranca el broker local (auto-spawn detached)
1340
- bus stop - Detiene el broker
1341
- bus status - Muestra puerto, pid, uptime del broker
1342
- bus serve - (interno) Ejecuta el broker en foreground
1343
- bus join --room <sala> [--session <s>] [--intro "..."]
1344
- bus leave [--session <s>]
1345
- bus say --text "..." [--session <s>]
1346
- bus ask --to <name> --text "..." [--wait N] [--session <s>]
1347
- bus reply --text "..." [--correlation <id>] [--to <name>]
1348
- bus history [--n N] [--session <s>]
1349
- bus inbox [--session <s>]
1350
- bus rooms
1351
- bus watch <session> [--room <sala>] (panel en vivo, sin tokens)
1352
- bus attend [--timeout N] (escucha preguntas dirigidas)
1353
- bus view (abre la UI web en el navegador)
1354
- clean Elimina skills y remueve hooks SDD-AI de .claude/settings.json
1355
- help Muestra esta ayuda
1356
-
1357
- Flujo completo:
1358
- 1. npm install -g refacil-sdd-ai
1359
- 2. refacil-sdd-ai init
1360
- 3. REINICIAR sesion de Claude Code o Cursor
1361
- 4. Ejecutar: /refacil:setup (instala OpenSpec + genera AGENTS.md)
1362
-
1363
- Requisitos:
1364
- - Node.js >= 20.19.0 (requerido por OpenSpec)
1365
- - Claude Code >= 2.1.89 (requerido por compact-bash para rewrite silencioso) o Cursor
1366
- `);
1367
- }
1368
-
1369
- // --- Main ---
1370
-
1371
- const command = process.argv[2] || 'help';
1372
-
1373
- switch (command) {
1374
- case 'init':
1375
- init();
1376
- break;
1377
- case 'update':
1378
- update();
1379
- break;
1380
- case 'check-update':
1381
- checkUpdate();
1382
- break;
1383
- case 'check-review':
1384
- checkReview();
1385
- break;
1386
- case 'compact-bash':
1387
- compactBash.run();
1388
- break;
1389
- case 'compact':
1390
- handleCompactSubcommand(process.argv[3]);
1391
- break;
1392
- case 'bus':
1393
- handleBusSubcommand(process.argv[3]);
1394
- break;
1395
- case 'clean':
1396
- clean();
1397
- break;
1398
- case 'help':
1399
- case '--help':
1400
- case '-h':
1401
- help();
1402
- break;
1403
- default:
1404
- console.error(` Comando desconocido: ${command}`);
1405
- help();
1406
- process.exit(1);
1407
- }
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const {
8
+ syncCompactGuidance,
9
+ removeCompactGuidance,
10
+ } = require('../lib/compact-guidance');
11
+ const compactBash = require('../lib/compact/bash');
12
+ const {
13
+ installSkills,
14
+ installAgents,
15
+ removeSkills,
16
+ createClaudeMd,
17
+ createCursorRules,
18
+ readRepoVersion,
19
+ writeRepoVersion,
20
+ getPackageVersion,
21
+ checkNodeVersion,
22
+ checkClaudeCodeVersion,
23
+ } = require('../lib/installer');
24
+ const { installHooks, uninstallHooks } = require('../lib/hooks');
25
+ const { handleCompact } = require('../lib/commands/compact');
26
+ const { handleBus } = require('../lib/commands/bus');
27
+ const { syncIgnoreFiles } = require('../lib/ignore-files');
28
+
29
+ const packageRoot = path.resolve(__dirname, '..');
30
+ const projectRoot = process.cwd();
31
+
32
+ // --- Check update (SessionStart hook) ---
33
+
34
+ function repoIsInitialized() {
35
+ return (
36
+ fs.existsSync(path.join(projectRoot, '.claude', 'skills')) ||
37
+ fs.existsSync(path.join(projectRoot, '.cursor', 'skills'))
38
+ );
39
+ }
40
+
41
+ function syncRepoSkillsIfStale(globalVersion) {
42
+ if (!repoIsInitialized()) return null;
43
+ const repoVersion = readRepoVersion(projectRoot);
44
+ if (repoVersion === globalVersion) return null;
45
+
46
+ const { execSync } = require('child_process');
47
+ const localCli = path.join(packageRoot, 'bin', 'cli.js');
48
+ try {
49
+ execSync(`"${process.execPath}" "${localCli}" update`, {
50
+ encoding: 'utf8',
51
+ timeout: 30000,
52
+ stdio: ['pipe', 'pipe', 'pipe'],
53
+ });
54
+ writeRepoVersion(projectRoot, globalVersion);
55
+ return { from: repoVersion, to: globalVersion };
56
+ } catch (_) {
57
+ return { from: repoVersion, to: globalVersion, failed: true };
58
+ }
59
+ }
60
+
61
+ function checkUpdate() {
62
+ const { execSync } = require('child_process');
63
+ const localVersion = getPackageVersion(packageRoot);
64
+
65
+ try {
66
+ syncCompactGuidance(projectRoot, packageRoot);
67
+ } catch (err) {
68
+ process.stderr.write(
69
+ `[refacil-sdd-ai] No se pudo sincronizar compact-guidance: ${err.message}\n`,
70
+ );
71
+ }
72
+
73
+ const syncResult = syncRepoSkillsIfStale(localVersion);
74
+ if (syncResult && !syncResult.failed) {
75
+ const fromLabel = syncResult.from ? `v${syncResult.from}` : 'version desconocida';
76
+ console.log(
77
+ `[refacil-sdd-ai] Skills de este repo sincronizadas (${fromLabel} -> v${syncResult.to}). ` +
78
+ 'Reinicia la sesion de Claude Code o Cursor para detectar los cambios.',
79
+ );
80
+ } else if (syncResult && syncResult.failed) {
81
+ console.log(
82
+ `[refacil-sdd-ai] Skills de este repo estan desactualizadas respecto al paquete global (v${syncResult.to}) ` +
83
+ 'pero la sincronizacion automatica fallo. Ejecuta manualmente: refacil-sdd-ai update',
84
+ );
85
+ }
86
+
87
+ try {
88
+ const latest = execSync('npm view refacil-sdd-ai version', {
89
+ encoding: 'utf8',
90
+ timeout: 10000,
91
+ stdio: ['pipe', 'pipe', 'pipe'],
92
+ }).trim();
93
+
94
+ if (!latest || latest === localVersion) return;
95
+
96
+ try {
97
+ execSync('npm update -g refacil-sdd-ai', {
98
+ encoding: 'utf8',
99
+ timeout: 60000,
100
+ stdio: ['pipe', 'pipe', 'pipe'],
101
+ });
102
+ execSync('refacil-sdd-ai update', {
103
+ encoding: 'utf8',
104
+ timeout: 30000,
105
+ stdio: ['pipe', 'pipe', 'pipe'],
106
+ });
107
+ writeRepoVersion(projectRoot, latest);
108
+ console.log(
109
+ `[refacil-sdd-ai] La metodologia SDD-AI se actualizo automaticamente de v${localVersion} a v${latest}. Skills y hooks sincronizados.`,
110
+ );
111
+ } catch (_) {
112
+ console.log(
113
+ `[refacil-sdd-ai] Hay una nueva version disponible (v${localVersion} -> v${latest}) pero la actualizacion automatica fallo. ` +
114
+ `Informa al usuario que ejecute manualmente: npm update -g refacil-sdd-ai && refacil-sdd-ai update`,
115
+ );
116
+ }
117
+ } catch (_) {
118
+ // Silent: sin internet o registry no disponible
119
+ }
120
+ }
121
+
122
+ // --- Check review (PreToolUse hook) ---
123
+
124
+ function checkReview() {
125
+ let input;
126
+ try {
127
+ const stdin = fs.readFileSync(0, 'utf8');
128
+ input = JSON.parse(stdin);
129
+ } catch (_) {
130
+ return;
131
+ }
132
+
133
+ const command = (input.tool_input && input.tool_input.command) || '';
134
+ if (!command.match(/git\s+push/)) return;
135
+
136
+ const changesDir = path.join(projectRoot, 'openspec', 'changes');
137
+ if (!fs.existsSync(changesDir)) return;
138
+
139
+ const entries = fs.readdirSync(changesDir, { withFileTypes: true });
140
+ const activeChanges = entries.filter(
141
+ (e) => e.isDirectory() && e.name !== 'archive',
142
+ );
143
+
144
+ if (activeChanges.length === 0) return;
145
+
146
+ const missing = activeChanges.filter(
147
+ (e) => !fs.existsSync(path.join(changesDir, e.name, '.review-passed')),
148
+ );
149
+
150
+ if (missing.length > 0) {
151
+ const names = missing.map((e) => e.name).join(', ');
152
+ const reason =
153
+ missing.length === 1
154
+ ? `[refacil-sdd-ai] Review pendiente para: ${names}. ` +
155
+ 'Deten el push y ejecuta /refacil:review sobre ese cambio antes de subir codigo. ' +
156
+ 'Si el review aprueba, reintenta el git push. ' +
157
+ 'Si el review requiere correcciones, informa los hallazgos al usuario y NO reintentar el push.'
158
+ : `[refacil-sdd-ai] Hay multiples cambios sin review aprobado: ${names}. ` +
159
+ 'Deten el push y pide al usuario seleccionar explicitamente que cambio quiere subir. ' +
160
+ 'Luego ejecuta /refacil:review <nombre-cambio> para ese cambio especifico y reintenta el push. ' +
161
+ 'No ejecutes review automatico sin seleccion cuando hay mas de un cambio pendiente.';
162
+ console.log(JSON.stringify({ decision: 'block', reason }));
163
+ }
164
+ }
165
+
166
+ // --- High-level commands ---
167
+
168
+ function init() {
169
+ console.log('\n refacil-sdd-ai: Inicializando metodologia SDD-AI...\n');
170
+
171
+ const nodeOk = checkNodeVersion();
172
+ if (nodeOk) console.log(` Node.js ${process.version} OK`);
173
+
174
+ const claudeCheck = checkClaudeCodeVersion();
175
+ if (claudeCheck.ok === true) {
176
+ console.log(` Claude Code ${claudeCheck.version} OK`);
177
+ } else if (claudeCheck.ok === false) {
178
+ console.log(`\n ADVERTENCIA: Claude Code ${claudeCheck.version} detectado.`);
179
+ console.log(' El hook compact-bash requiere Claude Code >= 2.1.89 para rewrite silencioso.');
180
+ console.log(' Con version inferior se instala igual pero el rewrite no tendra efecto.');
181
+ console.log(' Actualiza con: npm install -g @anthropic-ai/claude-code\n');
182
+ }
183
+
184
+ const count = installSkills(packageRoot, projectRoot);
185
+ console.log(` ${count} skills instaladas en .claude/skills/ y .cursor/skills/`);
186
+
187
+ const agentsCount = installAgents(packageRoot, projectRoot);
188
+ if (agentsCount > 0) {
189
+ console.log(` ${agentsCount} sub-agentes instalados en .claude/agents/ y .cursor/agents/`);
190
+ }
191
+
192
+ writeRepoVersion(projectRoot, getPackageVersion(packageRoot));
193
+
194
+ if (createClaudeMd(packageRoot, projectRoot)) console.log(' CLAUDE.md OK');
195
+ if (createCursorRules(packageRoot, projectRoot)) console.log(' .cursorrules OK');
196
+
197
+ if (installHooks('.claude', projectRoot)) {
198
+ console.log(' Hook check-update agregado a .claude/settings.json');
199
+ }
200
+ if (installHooks('.cursor', projectRoot)) {
201
+ console.log(' Hook check-update agregado a .cursor/settings.json');
202
+ }
203
+
204
+ try {
205
+ const ignoreResult = syncIgnoreFiles(projectRoot);
206
+ const s = ignoreResult.claude;
207
+ if (s.status === 'created') {
208
+ console.log(' .claudeignore y .cursorignore creados');
209
+ } else if (s.status === 'updated') {
210
+ console.log(` .claudeignore y .cursorignore actualizados (${s.added} entradas agregadas)`);
211
+ } else {
212
+ console.log(' .claudeignore y .cursorignore ya están al día');
213
+ }
214
+ } catch (err) {
215
+ console.error(` Advertencia: no se pudo sincronizar ignore files: ${err.message}`);
216
+ }
217
+
218
+ try {
219
+ const result = syncCompactGuidance(projectRoot, packageRoot);
220
+ if (result.status === 'appended') {
221
+ console.log(' Bloque compact-guidance agregado a AGENTS.md');
222
+ } else if (result.status === 'replaced') {
223
+ console.log(' Bloque compact-guidance actualizado en AGENTS.md');
224
+ }
225
+ } catch (err) {
226
+ console.error(` Advertencia: no se pudo sincronizar compact-guidance: ${err.message}`);
227
+ }
228
+
229
+ console.log('\n Siguientes pasos:\n');
230
+ console.log(' 1. REINICIA tu sesion de Claude Code o Cursor');
231
+ console.log(' (las skills nuevas no se detectan hasta reiniciar)\n');
232
+ console.log(' 2. Ejecuta: /refacil:setup');
233
+ console.log(' (instala OpenSpec y genera AGENTS.md para tu proyecto)\n');
234
+ console.log(' Nota: /refacil:setup tambien instalara los comandos de OpenSpec (opsx:*).');
235
+ console.log(' Los comandos refacil:* y opsx:* coexisten sin conflicto.');
236
+ console.log(' El equipo debe usar los refacil:* como interfaz principal.\n');
237
+ }
238
+
239
+ function update() {
240
+ console.log('\n refacil-sdd-ai: Actualizando skills...\n');
241
+
242
+ const count = installSkills(packageRoot, projectRoot);
243
+ console.log(` ${count} skills actualizadas en .claude/skills/ y .cursor/skills/`);
244
+
245
+ const agentsCount = installAgents(packageRoot, projectRoot);
246
+ if (agentsCount > 0) {
247
+ console.log(` ${agentsCount} sub-agentes actualizados en .claude/agents/ y .cursor/agents/`);
248
+ }
249
+
250
+ writeRepoVersion(projectRoot, getPackageVersion(packageRoot));
251
+
252
+ if (installHooks('.claude', projectRoot)) {
253
+ console.log(' Hook check-update agregado a .claude/settings.json');
254
+ }
255
+ if (installHooks('.cursor', projectRoot)) {
256
+ console.log(' Hook check-update agregado a .cursor/settings.json');
257
+ }
258
+
259
+ try {
260
+ const ignoreResult = syncIgnoreFiles(projectRoot);
261
+ const s = ignoreResult.claude;
262
+ if (s.status === 'created') {
263
+ console.log(' .claudeignore y .cursorignore creados');
264
+ } else if (s.status === 'updated') {
265
+ console.log(` .claudeignore y .cursorignore actualizados (${s.added} entradas agregadas)`);
266
+ } else {
267
+ console.log(' .claudeignore y .cursorignore ya están al día');
268
+ }
269
+ } catch (err) {
270
+ console.error(` Advertencia: no se pudo sincronizar ignore files: ${err.message}`);
271
+ }
272
+
273
+ try {
274
+ const result = syncCompactGuidance(projectRoot, packageRoot);
275
+ if (result.status === 'appended') {
276
+ console.log(' Bloque compact-guidance agregado a AGENTS.md');
277
+ } else if (result.status === 'replaced') {
278
+ console.log(' Bloque compact-guidance actualizado en AGENTS.md');
279
+ }
280
+ } catch (err) {
281
+ console.error(` Advertencia: no se pudo sincronizar compact-guidance: ${err.message}`);
282
+ }
283
+
284
+ console.log('\n REINICIA tu sesion de Claude Code o Cursor para aplicar los cambios.\n');
285
+ }
286
+
287
+ function clean() {
288
+ console.log('\n refacil-sdd-ai: Eliminando skills...\n');
289
+
290
+ const count = removeSkills(projectRoot);
291
+ console.log(` ${count} skills eliminadas de .claude/skills/ y .cursor/skills/`);
292
+
293
+ if (uninstallHooks('.claude', projectRoot)) {
294
+ console.log(' Hooks SDD-AI removidos de .claude/settings.json');
295
+ } else {
296
+ console.log(' No se encontraron hooks SDD-AI para remover en .claude/settings.json.');
297
+ }
298
+ if (uninstallHooks('.cursor', projectRoot)) {
299
+ console.log(' Hooks SDD-AI removidos de .cursor/settings.json');
300
+ }
301
+
302
+ try {
303
+ const result = removeCompactGuidance(projectRoot);
304
+ if (result.status === 'removed') {
305
+ console.log(' Bloque compact-guidance removido de AGENTS.md');
306
+ }
307
+ } catch (err) {
308
+ console.error(` Advertencia: no se pudo limpiar compact-guidance: ${err.message}`);
309
+ }
310
+
311
+ console.log(' AGENTS.md, CLAUDE.md y .cursorrules no fueron eliminados.');
312
+ console.log('\n Nota: Los comandos opsx:* de OpenSpec no se eliminan.');
313
+ console.log(' Para eliminar OpenSpec: rm -rf openspec/ .claude/commands/opsx .cursor/commands/opsx\n');
314
+ }
315
+
316
+ function help() {
317
+ console.log(`
318
+ refacil-sdd-ai — Metodologia SDD-AI con OpenSpec
319
+
320
+ Comandos:
321
+ init Instala skills en .claude/ y .cursor/, crea CLAUDE.md y .cursorrules
322
+ update Re-copia skills (para actualizar a nueva version del paquete)
323
+ check-update Verifica si hay una version mas reciente en npm y sincroniza compact-guidance en AGENTS.md
324
+ check-review Verifica que el review se haya completado (usado por hook PreToolUse)
325
+ compact-bash Reescribe comandos Bash bare para reducir tokens (usado por hook PreToolUse)
326
+ compact Subcomandos del hook compact-bash:
327
+ compact stats - Estadisticas completas (hook + ya-compacto) y ahorro estimado
328
+ compact disable - Desactiva el rewrite temporalmente
329
+ compact enable - Re-activa el rewrite
330
+ compact clear-log - Borra el log historico
331
+ bus Subcomandos del chat room entre agentes (refacil-bus):
332
+ bus start - Arranca el broker local (auto-spawn detached)
333
+ bus stop - Detiene el broker
334
+ bus status - Muestra puerto, pid, uptime del broker
335
+ bus serve - (interno) Ejecuta el broker en foreground
336
+ bus join --room <sala> [--session <s>] [--intro "..."]
337
+ bus leave [--session <s>]
338
+ bus say --text "..." [--session <s>]
339
+ bus ask --to <name> --text "..." [--wait N] [--session <s>]
340
+ bus reply --text "..." [--correlation <id>] [--to <name>]
341
+ bus history [--n N] [--session <s>]
342
+ bus inbox [--session <s>]
343
+ bus rooms
344
+ bus watch <session> [--room <sala>] (panel en vivo, sin tokens)
345
+ bus attend [--timeout N] (escucha preguntas dirigidas)
346
+ bus view (abre la UI web en el navegador)
347
+ clean Elimina skills y remueve hooks SDD-AI de .claude/settings.json y .cursor/settings.json
348
+ help Muestra esta ayuda
349
+
350
+ Flujo completo:
351
+ 1. npm install -g refacil-sdd-ai
352
+ 2. refacil-sdd-ai init
353
+ 3. REINICIAR sesion de Claude Code o Cursor
354
+ 4. Ejecutar: /refacil:setup (instala OpenSpec + genera AGENTS.md)
355
+
356
+ Requisitos:
357
+ - Node.js >= 20.19.0 (requerido por OpenSpec)
358
+ - Claude Code >= 2.1.89 (requerido por compact-bash para rewrite silencioso) o Cursor
359
+ `);
360
+ }
361
+
362
+ // --- Main ---
363
+
364
+ const command = process.argv[2] || 'help';
365
+
366
+ switch (command) {
367
+ case 'init':
368
+ init();
369
+ break;
370
+ case 'update':
371
+ update();
372
+ break;
373
+ case 'check-update':
374
+ checkUpdate();
375
+ break;
376
+ case 'check-review':
377
+ checkReview();
378
+ break;
379
+ case 'compact-bash':
380
+ compactBash.run();
381
+ break;
382
+ case 'compact':
383
+ handleCompact(process.argv[3]);
384
+ break;
385
+ case 'bus':
386
+ handleBus(process.argv[3], process.argv.slice(4), packageRoot);
387
+ break;
388
+ case 'clean':
389
+ clean();
390
+ break;
391
+ case 'help':
392
+ case '--help':
393
+ case '-h':
394
+ help();
395
+ break;
396
+ default:
397
+ console.error(` Comando desconocido: ${command}`);
398
+ help();
399
+ process.exit(1);
400
+ }