refacil-sdd-ai 3.0.3 → 4.0.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,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ 'use strict';
4
+
3
5
  const fs = require('fs');
4
6
  const path = require('path');
5
7
  const {
@@ -7,555 +9,27 @@ const {
7
9
  removeCompactGuidance,
8
10
  } = require('../lib/compact-guidance');
9
11
  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
- ];
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');
47
28
 
48
29
  const packageRoot = path.resolve(__dirname, '..');
49
30
  const projectRoot = process.cwd();
50
31
 
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
- // --- Cursor hook installation (mismo formato, mismos marcadores) ---
432
-
433
- function installCursorHook() {
434
- const settingsDir = path.join(projectRoot, '.cursor');
435
- const settingsPath = path.join(settingsDir, 'settings.json');
436
- let settings = {};
437
-
438
- if (fs.existsSync(settingsPath)) {
439
- try {
440
- settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
441
- } catch (_) {
442
- settings = {};
443
- }
444
- }
445
-
446
- if (!settings.hooks) settings.hooks = {};
447
- if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
448
-
449
- let changed = false;
450
-
451
- // SessionStart: check-update
452
- const hasUpdateHook = settings.hooks.SessionStart.some(h => h._sdd === true);
453
- if (!hasUpdateHook) {
454
- settings.hooks.SessionStart.push({
455
- _sdd: true,
456
- matcher: '',
457
- hooks: [
458
- {
459
- type: 'command',
460
- command: 'refacil-sdd-ai check-update',
461
- },
462
- ],
463
- });
464
- changed = true;
465
- }
466
-
467
- // PreToolUse
468
- if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
469
-
470
- // compact-bash (must run BEFORE check-review so rewrite is visible to subsequent hooks)
471
- const hasCompactHook = settings.hooks.PreToolUse.some(
472
- (h) => h._sdd_compact === true,
473
- );
474
- if (!hasCompactHook) {
475
- settings.hooks.PreToolUse.unshift({
476
- _sdd_compact: true,
477
- matcher: 'Bash',
478
- hooks: [
479
- {
480
- type: 'command',
481
- command: 'refacil-sdd-ai compact-bash',
482
- },
483
- ],
484
- });
485
- changed = true;
486
- }
487
-
488
- // check-review
489
- const hasReviewHook = settings.hooks.PreToolUse.some(
490
- (h) => h._sdd_review === true,
491
- );
492
- if (!hasReviewHook) {
493
- settings.hooks.PreToolUse.push({
494
- _sdd_review: true,
495
- matcher: 'Bash',
496
- hooks: [
497
- {
498
- type: 'command',
499
- command: 'refacil-sdd-ai check-review',
500
- },
501
- ],
502
- });
503
- changed = true;
504
- }
505
-
506
- if (!changed) {
507
- console.log(' Hooks SDD-AI ya configurados en Cursor.');
508
- return false;
509
- }
510
-
511
- fs.mkdirSync(settingsDir, { recursive: true });
512
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
513
- return true;
514
- }
515
-
516
- function uninstallCursorHook() {
517
- const settingsPath = path.join(projectRoot, '.cursor', 'settings.json');
518
- if (!fs.existsSync(settingsPath)) return false;
519
-
520
- let settings;
521
- try {
522
- settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
523
- } catch (_) {
524
- console.log(' No se pudieron remover hooks: .cursor/settings.json invalido.');
525
- return false;
526
- }
527
-
528
- if (!settings.hooks) return false;
529
-
530
- let changed = false;
531
-
532
- if (Array.isArray(settings.hooks.SessionStart)) {
533
- const original = settings.hooks.SessionStart.length;
534
- settings.hooks.SessionStart = settings.hooks.SessionStart.filter(
535
- (h) => h._sdd !== true,
536
- );
537
- if (settings.hooks.SessionStart.length !== original) changed = true;
538
- if (settings.hooks.SessionStart.length === 0) delete settings.hooks.SessionStart;
539
- }
540
-
541
- if (Array.isArray(settings.hooks.PreToolUse)) {
542
- const original = settings.hooks.PreToolUse.length;
543
- settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
544
- (h) => h._sdd_review !== true && h._sdd_compact !== true,
545
- );
546
- if (settings.hooks.PreToolUse.length !== original) changed = true;
547
- if (settings.hooks.PreToolUse.length === 0) delete settings.hooks.PreToolUse;
548
- }
549
-
550
- if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
551
-
552
- if (!changed) return false;
553
-
554
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
555
- return true;
556
- }
557
-
558
- // --- Check update ---
32
+ // --- Check update (SessionStart hook) ---
559
33
 
560
34
  function repoIsInitialized() {
561
35
  return (
@@ -565,9 +39,6 @@ function repoIsInitialized() {
565
39
  }
566
40
 
567
41
  function syncRepoSkillsIfStale(globalVersion) {
568
- // Si el paquete global ya está en globalVersion pero este repo tiene skills
569
- // de una versión anterior (porque otro repo disparó el auto-upgrade), hay
570
- // que re-copiarlas aquí. Idem si nunca se marcó la versión en este repo.
571
42
  if (!repoIsInitialized()) return null;
572
43
  const repoVersion = readRepoVersion(projectRoot);
573
44
  if (repoVersion === globalVersion) return null;
@@ -589,9 +60,8 @@ function syncRepoSkillsIfStale(globalVersion) {
589
60
 
590
61
  function checkUpdate() {
591
62
  const { execSync } = require('child_process');
592
- const localVersion = getPackageVersion();
63
+ const localVersion = getPackageVersion(packageRoot);
593
64
 
594
- // Always ensure AGENTS.md has current compact-guidance block (silent unless error)
595
65
  try {
596
66
  syncCompactGuidance(projectRoot, packageRoot);
597
67
  } catch (err) {
@@ -600,8 +70,6 @@ function checkUpdate() {
600
70
  );
601
71
  }
602
72
 
603
- // Paso 1: sincronizar skills del repo actual si quedaron desfasadas respecto
604
- // al paquete global (pasa cuando otro repo ya disparó el auto-upgrade).
605
73
  const syncResult = syncRepoSkillsIfStale(localVersion);
606
74
  if (syncResult && !syncResult.failed) {
607
75
  const fromLabel = syncResult.from ? `v${syncResult.from}` : 'version desconocida';
@@ -609,6 +77,10 @@ function checkUpdate() {
609
77
  `[refacil-sdd-ai] Skills de este repo sincronizadas (${fromLabel} -> v${syncResult.to}). ` +
610
78
  'Reinicia la sesion de Claude Code o Cursor para detectar los cambios.',
611
79
  );
80
+ console.log(
81
+ '[refacil-sdd-ai] La metodologia fue actualizada. ' +
82
+ 'Pregunta al usuario si desea aplicar las migraciones pendientes y, si confirma, ejecuta /refacil:update.',
83
+ );
612
84
  } else if (syncResult && syncResult.failed) {
613
85
  console.log(
614
86
  `[refacil-sdd-ai] Skills de este repo estan desactualizadas respecto al paquete global (v${syncResult.to}) ` +
@@ -616,8 +88,6 @@ function checkUpdate() {
616
88
  );
617
89
  }
618
90
 
619
- // Paso 2: chequear si hay version nueva en npm y, si la hay, actualizar
620
- // global + copiar skills al repo actual.
621
91
  try {
622
92
  const latest = execSync('npm view refacil-sdd-ai version', {
623
93
  encoding: 'utf8',
@@ -649,39 +119,34 @@ function checkUpdate() {
649
119
  );
650
120
  }
651
121
  } catch (_) {
652
- // Silent on error (no internet, registry unreachable, etc.)
122
+ // Silent: sin internet o registry no disponible
653
123
  }
654
124
  }
655
125
 
656
126
  // --- Check review (PreToolUse hook) ---
657
127
 
658
128
  function checkReview() {
659
- // Read stdin (JSON from PreToolUse hook)
660
129
  let input;
661
130
  try {
662
131
  const stdin = fs.readFileSync(0, 'utf8');
663
132
  input = JSON.parse(stdin);
664
133
  } catch (_) {
665
- // If no stdin or invalid JSON, allow (not called from hook context)
666
134
  return;
667
135
  }
668
136
 
669
- // Only block git push commands
670
137
  const command = (input.tool_input && input.tool_input.command) || '';
671
138
  if (!command.match(/git\s+push/)) return;
672
139
 
673
- // Find active change in openspec/changes/ (exclude archive/)
674
140
  const changesDir = path.join(projectRoot, 'openspec', 'changes');
675
- if (!fs.existsSync(changesDir)) return; // No openspec, allow
141
+ if (!fs.existsSync(changesDir)) return;
676
142
 
677
143
  const entries = fs.readdirSync(changesDir, { withFileTypes: true });
678
144
  const activeChanges = entries.filter(
679
145
  (e) => e.isDirectory() && e.name !== 'archive',
680
146
  );
681
147
 
682
- if (activeChanges.length === 0) return; // No active changes, allow
148
+ if (activeChanges.length === 0) return;
683
149
 
684
- // Check if any active change is missing .review-passed
685
150
  const missing = activeChanges.filter(
686
151
  (e) => !fs.existsSync(path.join(changesDir, e.name, '.review-passed')),
687
152
  );
@@ -698,27 +163,18 @@ function checkReview() {
698
163
  'Deten el push y pide al usuario seleccionar explicitamente que cambio quiere subir. ' +
699
164
  'Luego ejecuta /refacil:review <nombre-cambio> para ese cambio especifico y reintenta el push. ' +
700
165
  'No ejecutes review automatico sin seleccion cuando hay mas de un cambio pendiente.';
701
- console.log(
702
- JSON.stringify({
703
- decision: 'block',
704
- reason,
705
- }),
706
- );
166
+ console.log(JSON.stringify({ decision: 'block', reason }));
707
167
  }
708
168
  }
709
169
 
710
- // --- Commands ---
170
+ // --- High-level commands ---
711
171
 
712
172
  function init() {
713
173
  console.log('\n refacil-sdd-ai: Inicializando metodologia SDD-AI...\n');
714
174
 
715
- // Check Node version
716
175
  const nodeOk = checkNodeVersion();
717
- if (nodeOk) {
718
- console.log(` Node.js ${process.version} OK`);
719
- }
176
+ if (nodeOk) console.log(` Node.js ${process.version} OK`);
720
177
 
721
- // Check Claude Code version (for compact-bash hook)
722
178
  const claudeCheck = checkClaudeCodeVersion();
723
179
  if (claudeCheck.ok === true) {
724
180
  console.log(` Claude Code ${claudeCheck.version} OK`);
@@ -728,39 +184,41 @@ function init() {
728
184
  console.log(' Con version inferior se instala igual pero el rewrite no tendra efecto.');
729
185
  console.log(' Actualiza con: npm install -g @anthropic-ai/claude-code\n');
730
186
  }
731
- // ok === null: claude no esta en PATH, silencioso
732
187
 
733
- // Install skills
734
- const count = installSkills();
188
+ const count = installSkills(packageRoot, projectRoot);
735
189
  console.log(` ${count} skills instaladas en .claude/skills/ y .cursor/skills/`);
736
190
 
737
- // Install sub-agents
738
- const agentsCount = installAgents();
191
+ const agentsCount = installAgents(packageRoot, projectRoot);
739
192
  if (agentsCount > 0) {
740
193
  console.log(` ${agentsCount} sub-agentes instalados en .claude/agents/ y .cursor/agents/`);
741
194
  }
742
195
 
743
- writeRepoVersion(projectRoot, getPackageVersion());
744
-
745
- // Create or update CLAUDE.md
746
- if (createClaudeMd()) {
747
- console.log(' CLAUDE.md OK');
748
- }
196
+ writeRepoVersion(projectRoot, getPackageVersion(packageRoot));
749
197
 
750
- // Create or update .cursorrules
751
- if (createCursorRules()) {
752
- console.log(' .cursorrules OK');
753
- }
198
+ if (createClaudeMd(packageRoot, projectRoot)) console.log(' CLAUDE.md OK');
199
+ if (createCursorRules(packageRoot, projectRoot)) console.log(' .cursorrules OK');
754
200
 
755
- // Install SessionStart hook for version check
756
- if (installHook()) {
201
+ if (installHooks('.claude', projectRoot)) {
757
202
  console.log(' Hook check-update agregado a .claude/settings.json');
758
203
  }
759
- if (installCursorHook()) {
204
+ if (installHooks('.cursor', projectRoot)) {
760
205
  console.log(' Hook check-update agregado a .cursor/settings.json');
761
206
  }
762
207
 
763
- // Sync compact-guidance block in AGENTS.md (if it exists)
208
+ try {
209
+ const ignoreResult = syncIgnoreFiles(projectRoot);
210
+ const s = ignoreResult.claude;
211
+ if (s.status === 'created') {
212
+ console.log(' .claudeignore y .cursorignore creados');
213
+ } else if (s.status === 'updated') {
214
+ console.log(` .claudeignore y .cursorignore actualizados (${s.added} entradas agregadas)`);
215
+ } else {
216
+ console.log(' .claudeignore y .cursorignore ya están al día');
217
+ }
218
+ } catch (err) {
219
+ console.error(` Advertencia: no se pudo sincronizar ignore files: ${err.message}`);
220
+ }
221
+
764
222
  try {
765
223
  const result = syncCompactGuidance(projectRoot, packageRoot);
766
224
  if (result.status === 'appended') {
@@ -784,25 +242,41 @@ function init() {
784
242
 
785
243
  function update() {
786
244
  console.log('\n refacil-sdd-ai: Actualizando skills...\n');
787
- const count = installSkills();
245
+
246
+ const count = installSkills(packageRoot, projectRoot);
788
247
  console.log(` ${count} skills actualizadas en .claude/skills/ y .cursor/skills/`);
789
248
 
790
- const agentsCount = installAgents();
249
+ const agentsCount = installAgents(packageRoot, projectRoot);
791
250
  if (agentsCount > 0) {
792
251
  console.log(` ${agentsCount} sub-agentes actualizados en .claude/agents/ y .cursor/agents/`);
793
252
  }
794
253
 
795
- writeRepoVersion(projectRoot, getPackageVersion());
254
+ writeRepoVersion(projectRoot, getPackageVersion(packageRoot));
796
255
 
797
- // Ensure hook is installed (for users updating from versions without hook)
798
- if (installHook()) {
256
+ createClaudeMd(packageRoot, projectRoot);
257
+ createCursorRules(packageRoot, projectRoot);
258
+
259
+ if (installHooks('.claude', projectRoot)) {
799
260
  console.log(' Hook check-update agregado a .claude/settings.json');
800
261
  }
801
- if (installCursorHook()) {
262
+ if (installHooks('.cursor', projectRoot)) {
802
263
  console.log(' Hook check-update agregado a .cursor/settings.json');
803
264
  }
804
265
 
805
- // Sync compact-guidance block in AGENTS.md
266
+ try {
267
+ const ignoreResult = syncIgnoreFiles(projectRoot);
268
+ const s = ignoreResult.claude;
269
+ if (s.status === 'created') {
270
+ console.log(' .claudeignore y .cursorignore creados');
271
+ } else if (s.status === 'updated') {
272
+ console.log(` .claudeignore y .cursorignore actualizados (${s.added} entradas agregadas)`);
273
+ } else {
274
+ console.log(' .claudeignore y .cursorignore ya están al día');
275
+ }
276
+ } catch (err) {
277
+ console.error(` Advertencia: no se pudo sincronizar ignore files: ${err.message}`);
278
+ }
279
+
806
280
  try {
807
281
  const result = syncCompactGuidance(projectRoot, packageRoot);
808
282
  if (result.status === 'appended') {
@@ -819,18 +293,19 @@ function update() {
819
293
 
820
294
  function clean() {
821
295
  console.log('\n refacil-sdd-ai: Eliminando skills...\n');
822
- const count = removeSkills();
296
+
297
+ const count = removeSkills(projectRoot);
823
298
  console.log(` ${count} skills eliminadas de .claude/skills/ y .cursor/skills/`);
824
- if (uninstallHook()) {
299
+
300
+ if (uninstallHooks('.claude', projectRoot)) {
825
301
  console.log(' Hooks SDD-AI removidos de .claude/settings.json');
826
302
  } else {
827
303
  console.log(' No se encontraron hooks SDD-AI para remover en .claude/settings.json.');
828
304
  }
829
- if (uninstallCursorHook()) {
305
+ if (uninstallHooks('.cursor', projectRoot)) {
830
306
  console.log(' Hooks SDD-AI removidos de .cursor/settings.json');
831
307
  }
832
308
 
833
- // Remove compact-guidance block from AGENTS.md if present
834
309
  try {
835
310
  const result = removeCompactGuidance(projectRoot);
836
311
  if (result.status === 'removed') {
@@ -845,617 +320,6 @@ function clean() {
845
320
  console.log(' Para eliminar OpenSpec: rm -rf openspec/ .claude/commands/opsx .cursor/commands/opsx\n');
846
321
  }
847
322
 
848
- // --- Compact subcommands (stats / enable / disable / clear-log) ---
849
-
850
- function handleCompactSubcommand(sub) {
851
- switch (sub) {
852
- case 'stats':
853
- showCompactStats();
854
- break;
855
- case 'disable':
856
- compactTelemetry.disable();
857
- console.log(' compact-bash deshabilitado. Reactiva con: refacil-sdd-ai compact enable');
858
- break;
859
- case 'enable':
860
- compactTelemetry.enable();
861
- console.log(' compact-bash habilitado.');
862
- break;
863
- case 'clear-log':
864
- compactTelemetry.clearLog();
865
- console.log(' compact.log limpiado.');
866
- break;
867
- default:
868
- console.log('Uso: refacil-sdd-ai compact <stats|disable|enable|clear-log>');
869
- }
870
- }
871
-
872
- function showCompactStats() {
873
- const s = compactTelemetry.stats();
874
- if (s.totalEvents === 0) {
875
- console.log('\n No hay eventos registrados todavia. Ejecuta comandos Bash para generar telemetria de compactacion.\n');
876
- return;
877
- }
878
-
879
- const sortedRewrites = Object.entries(s.byRule)
880
- .filter(([, data]) => data.rewriteCount > 0)
881
- .sort((a, b) => b[1].rewriteSaved - a[1].rewriteSaved);
882
- const sortedAlreadyCompact = Object.entries(s.byRule)
883
- .filter(([, data]) => data.alreadyCompactCount > 0)
884
- .sort((a, b) => b[1].alreadyCompactPotential - a[1].alreadyCompactPotential);
885
-
886
- console.log(`\n compact-bash stats\n`);
887
- console.log(` Rewrites por hook: ${s.totalRewrites}`);
888
- console.log(` Comandos ya compactos detectados (skill/agente): ${s.totalAlreadyCompact}\n`);
889
-
890
- if (sortedRewrites.length > 0) {
891
- console.log(' Ahorro aplicado por hook (rewrite):');
892
- for (const [id, data] of sortedRewrites) {
893
- const kTokens = (data.rewriteSaved / 1000).toFixed(1);
894
- console.log(
895
- ` ${id.padEnd(18)} ${String(data.rewriteCount).padStart(6)} rewrites ~${kTokens.padStart(7)}k tokens`,
896
- );
897
- }
898
- const totalHookK = (s.totalSaved / 1000).toFixed(1);
899
- const hookUsd = ((s.totalSaved / 1_000_000) * 3).toFixed(2);
900
- console.log(` ${'-'.repeat(62)}`);
901
- console.log(
902
- ` ${'Total hook'.padEnd(18)} ${String(s.totalRewrites).padStart(6)} rewrites ~${totalHookK.padStart(7)}k tokens (~$${hookUsd} USD)`,
903
- );
904
- console.log('');
905
- }
906
-
907
- if (sortedAlreadyCompact.length > 0) {
908
- console.log(' Ahorro potencial ya capturado por skill/agente (sin rewrite):');
909
- for (const [id, data] of sortedAlreadyCompact) {
910
- const kTokens = (data.alreadyCompactPotential / 1000).toFixed(1);
911
- console.log(
912
- ` ${id.padEnd(18)} ${String(data.alreadyCompactCount).padStart(6)} eventos ~${kTokens.padStart(7)}k tokens potenciales`,
913
- );
914
- }
915
- const totalAgentK = (s.totalAlreadyCompactPotential / 1000).toFixed(1);
916
- const agentUsd = ((s.totalAlreadyCompactPotential / 1_000_000) * 3).toFixed(2);
917
- console.log(` ${'-'.repeat(62)}`);
918
- console.log(
919
- ` ${'Total skill'.padEnd(18)} ${String(s.totalAlreadyCompact).padStart(6)} eventos ~${totalAgentK.padStart(7)}k tokens (~$${agentUsd} USD)`,
920
- );
921
- console.log('');
922
- }
923
-
924
- const totalK = (s.totalObservedPotential / 1000).toFixed(1);
925
- const totalUsd = ((s.totalObservedPotential / 1_000_000) * 3).toFixed(2);
926
- console.log(` ${'-'.repeat(62)}`);
927
- console.log(
928
- ` ${'Total observado'.padEnd(18)} ${String(s.totalEvents).padStart(6)} eventos ~${totalK.padStart(7)}k tokens (~$${totalUsd} USD, Sonnet input)`,
929
- );
930
- console.log(`\n Log: ${compactTelemetry.LOG_PATH}`);
931
- if (compactTelemetry.isDisabled()) {
932
- console.log(' Estado: DESHABILITADO (no se registran nuevos eventos)');
933
- }
934
- console.log('');
935
- }
936
-
937
- // --- Bus subcommands (refacil-bus: broker core — fase 1) ---
938
-
939
- async function busStart() {
940
- try {
941
- const { info, started } = await busSpawn.ensureBroker(packageRoot);
942
- if (started) {
943
- console.log(` refacil-bus broker iniciado en 127.0.0.1:${info.port} (pid ${info.pid}).`);
944
- } else {
945
- console.log(` refacil-bus broker ya estaba activo en 127.0.0.1:${info.port} (pid ${info.pid}).`);
946
- }
947
- } catch (err) {
948
- console.error(` No se pudo iniciar el broker: ${err.message}`);
949
- process.exit(1);
950
- }
951
- }
952
-
953
- function busStop() {
954
- const result = busSpawn.stopBroker();
955
- if (result.stopped) {
956
- console.log(` refacil-bus broker detenido (pid ${result.info.pid}).`);
957
- } else if (result.reason === 'no-info') {
958
- console.log(' refacil-bus broker no está corriendo.');
959
- } else if (result.reason === 'not-alive') {
960
- console.log(' refacil-bus broker no estaba vivo; info obsoleta limpiada.');
961
- } else {
962
- console.error(` No se pudo detener el broker: ${result.reason}`);
963
- process.exit(1);
964
- }
965
- }
966
-
967
- async function busStatus() {
968
- const status = await busSpawn.isBrokerAlive();
969
- if (!status.alive) {
970
- console.log(' refacil-bus broker: INACTIVO');
971
- if (status.staleInfo) {
972
- console.log(` (info obsoleta encontrada: pid ${status.staleInfo.pid}, puerto ${status.staleInfo.port})`);
973
- }
974
- return;
975
- }
976
- const info = status.info;
977
- const uptimeMs = Date.now() - new Date(info.startedAt).getTime();
978
- const uptimeMin = Math.floor(uptimeMs / 60000);
979
- console.log(' refacil-bus broker: ACTIVO');
980
- console.log(` host: 127.0.0.1`);
981
- console.log(` puerto: ${info.port}`);
982
- console.log(` pid: ${info.pid}`);
983
- console.log(` iniciado: ${info.startedAt}`);
984
- console.log(` uptime: ${uptimeMin} min`);
985
- console.log(` info: ${busBroker.BUS_INFO_PATH}`);
986
- }
987
-
988
- function busServe() {
989
- // Invocado por spawn detached — ejecuta el broker en foreground.
990
- busBroker.start().catch((err) => {
991
- process.stderr.write(`Error arrancando broker: ${err.message}\n`);
992
- process.exit(1);
993
- });
994
- }
995
-
996
- function parseBusArgs(argv) {
997
- const args = {};
998
- for (let i = 0; i < argv.length; i++) {
999
- const token = argv[i];
1000
- if (!token || !token.startsWith('--')) continue;
1001
- const key = token.slice(2);
1002
- const next = argv[i + 1];
1003
- if (next === undefined || next.startsWith('--')) {
1004
- args[key] = true;
1005
- } else {
1006
- args[key] = next;
1007
- i++;
1008
- }
1009
- }
1010
- return args;
1011
- }
1012
-
1013
- function defaultSessionName() {
1014
- return path.basename(process.cwd()) || 'sesion';
1015
- }
1016
-
1017
- async function connectOrDie() {
1018
- try {
1019
- const { info } = await busSpawn.ensureBroker(packageRoot);
1020
- const ws = await busClient.connect(info.port);
1021
- return { ws, info };
1022
- } catch (err) {
1023
- console.error(` No se pudo conectar al bus: ${err.message}`);
1024
- process.exit(1);
1025
- }
1026
- }
1027
-
1028
- function formatMessage(m) {
1029
- const target = m.to ? ` → @${m.to}` : '';
1030
- return ` [${m.ts}] ${m.from}${target} (${m.kind}): ${m.text}`;
1031
- }
1032
-
1033
- async function busJoin(args) {
1034
- const session = args.session || defaultSessionName();
1035
- const room = args.room;
1036
- const repo = args.repo || process.cwd();
1037
- let intro = args.intro;
1038
- if (!intro) {
1039
- try {
1040
- intro = busPresenter.buildIntro({ repoDir: repo, session });
1041
- } catch (_) {
1042
- intro = `${session} se unió a la sala`;
1043
- }
1044
- }
1045
- if (!room) {
1046
- console.error(' Uso: refacil-sdd-ai bus join --room <sala> [--session <s>] [--intro "..."]');
1047
- process.exit(1);
1048
- }
1049
- const { ws } = await connectOrDie();
1050
- const reply = await busClient.sendAndWait(
1051
- ws,
1052
- 'join',
1053
- { session, room, repo, intro },
1054
- (d) => d.type === 'system' && d.event === 'joined',
1055
- 3000,
1056
- );
1057
- busClient.close(ws);
1058
- if (!reply) {
1059
- console.error(' Timeout uniéndose a la sala.');
1060
- process.exit(1);
1061
- }
1062
- const members = (reply.detail && reply.detail.members) || [];
1063
- console.log(` Unido a la sala "${room}" como "${session}".`);
1064
- console.log(` Miembros actuales: ${members.join(', ') || '(solo tú)'}`);
1065
- console.log(` Para consultarte: /refacil:ask @${session} "..."`);
1066
- }
1067
-
1068
- async function busLeave(args) {
1069
- const session = args.session || defaultSessionName();
1070
- const { ws } = await connectOrDie();
1071
- const reply = await busClient.sendAndWait(
1072
- ws,
1073
- 'leave',
1074
- { session },
1075
- (d) => d.type === 'system' && (d.event === 'left' || d.event === 'error'),
1076
- 3000,
1077
- );
1078
- busClient.close(ws);
1079
- if (reply && reply.event === 'left') {
1080
- console.log(` "${session}" salió de la sala.`);
1081
- } else {
1082
- console.log(` "${session}" no estaba en ninguna sala.`);
1083
- }
1084
- }
1085
-
1086
- async function busSay(args) {
1087
- const session = args.session || defaultSessionName();
1088
- const text = args.text;
1089
- if (!text) {
1090
- console.error(' Uso: refacil-sdd-ai bus say --text "..." [--session <s>]');
1091
- process.exit(1);
1092
- }
1093
- const { ws } = await connectOrDie();
1094
- const reply = await busClient.sendAndWait(
1095
- ws,
1096
- 'say',
1097
- { session, text },
1098
- (d) => d.type === 'system' && (d.event === 'sent' || d.event === 'error'),
1099
- 3000,
1100
- );
1101
- busClient.close(ws);
1102
- if (reply && reply.event === 'sent') {
1103
- console.log(` Mensaje enviado (id ${reply.detail.id}).`);
1104
- } else {
1105
- const detail = (reply && reply.detail) || 'sin respuesta';
1106
- console.error(` No se pudo enviar: ${detail}`);
1107
- process.exit(1);
1108
- }
1109
- }
1110
-
1111
- async function busAsk(args) {
1112
- const session = args.session || defaultSessionName();
1113
- const to = args.to;
1114
- const text = args.text;
1115
- const waitSec = args.wait ? parseInt(args.wait, 10) : 0;
1116
- if (!to || !text) {
1117
- console.error(' Uso: refacil-sdd-ai bus ask --to <name> --text "..." [--wait N] [--session <s>]');
1118
- process.exit(1);
1119
- }
1120
- const { ws } = await connectOrDie();
1121
- const ack = await busClient.sendAndWait(
1122
- ws,
1123
- 'ask',
1124
- { session, to: to.replace(/^@/, ''), text },
1125
- (d) => d.type === 'system' && (d.event === 'sent' || d.event === 'error'),
1126
- 3000,
1127
- );
1128
- if (!ack || ack.event !== 'sent') {
1129
- busClient.close(ws);
1130
- const detail = (ack && ack.detail) || 'sin respuesta';
1131
- console.error(` No se pudo enviar la pregunta: ${detail}`);
1132
- process.exit(1);
1133
- }
1134
- const correlationId = ack.detail.correlationId;
1135
- console.log(` Pregunta enviada a @${to.replace(/^@/, '')} (correlationId ${correlationId}).`);
1136
-
1137
- if (waitSec > 0) {
1138
- console.log(` Esperando respuesta hasta ${waitSec}s...`);
1139
- const resp = await busClient.sendAndWait(
1140
- ws,
1141
- 'ping',
1142
- {},
1143
- (d) => d.type === 'msg' && d.kind === 'reply' && d.correlationId === correlationId,
1144
- waitSec * 1000,
1145
- );
1146
- busClient.close(ws);
1147
- if (!resp) {
1148
- console.log(` Sin respuesta en ${waitSec}s. Usa /refacil:inbox más tarde para recuperarla.`);
1149
- return;
1150
- }
1151
- console.log(` Respuesta de @${resp.from}:`);
1152
- console.log(` ${resp.text}`);
1153
- } else {
1154
- busClient.close(ws);
1155
- console.log(' Usa /refacil:inbox para ver respuestas.');
1156
- }
1157
- }
1158
-
1159
- async function busReply(args) {
1160
- const session = args.session || defaultSessionName();
1161
- const text = args.text;
1162
- const correlationId = args.correlation || null;
1163
- const to = args.to ? args.to.replace(/^@/, '') : null;
1164
- if (!text) {
1165
- console.error(' Uso: refacil-sdd-ai bus reply --text "..." [--to <name>] [--correlation <id>]');
1166
- process.exit(1);
1167
- }
1168
- const { ws } = await connectOrDie();
1169
- const reply = await busClient.sendAndWait(
1170
- ws,
1171
- 'reply',
1172
- { session, text, to, correlationId },
1173
- (d) => d.type === 'system' && (d.event === 'sent' || d.event === 'error'),
1174
- 3000,
1175
- );
1176
- busClient.close(ws);
1177
- if (reply && reply.event === 'sent') {
1178
- console.log(` Respuesta enviada (id ${reply.detail.id}).`);
1179
- } else {
1180
- const detail = (reply && reply.detail) || 'sin respuesta';
1181
- console.error(` No se pudo responder: ${detail}`);
1182
- process.exit(1);
1183
- }
1184
- }
1185
-
1186
- async function busHistory(args) {
1187
- const session = args.session || defaultSessionName();
1188
- const n = args.n ? parseInt(args.n, 10) : 20;
1189
- const { ws } = await connectOrDie();
1190
- const reply = await busClient.sendAndWait(
1191
- ws,
1192
- 'history',
1193
- { session, n },
1194
- (d) => d.type === 'history',
1195
- 3000,
1196
- );
1197
- busClient.close(ws);
1198
- if (!reply) {
1199
- console.log(' Sin historial.');
1200
- return;
1201
- }
1202
- const msgs = reply.messages || [];
1203
- if (msgs.length === 0) {
1204
- console.log(' Sin historial.');
1205
- return;
1206
- }
1207
- console.log(` Últimos ${msgs.length} mensajes:`);
1208
- for (const m of msgs) console.log(formatMessage(m));
1209
- }
1210
-
1211
- async function busInbox(args) {
1212
- const session = args.session || defaultSessionName();
1213
- const { ws } = await connectOrDie();
1214
- const reply = await busClient.sendAndWait(
1215
- ws,
1216
- 'inbox',
1217
- { session },
1218
- (d) => d.type === 'inbox',
1219
- 3000,
1220
- );
1221
- busClient.close(ws);
1222
- if (!reply) {
1223
- console.log(' Sin respuesta del broker.');
1224
- return;
1225
- }
1226
- const msgs = reply.messages || [];
1227
- if (msgs.length === 0) {
1228
- console.log(' Sin mensajes nuevos.');
1229
- return;
1230
- }
1231
- console.log(` ${msgs.length} mensaje(s) nuevo(s):`);
1232
- for (const m of msgs) console.log(formatMessage(m));
1233
- }
1234
-
1235
- function findFirstUnansweredAsk(messages, session) {
1236
- const asks = messages.filter((m) => m.kind === 'ask' && m.to === session);
1237
- for (const ask of asks) {
1238
- const hasReply = messages.some(
1239
- (m) => m.kind === 'reply' && m.correlationId === ask.correlationId,
1240
- );
1241
- if (!hasReply) return ask;
1242
- }
1243
- return null;
1244
- }
1245
-
1246
- function printAttendQuestion(msg) {
1247
- console.log(' Pregunta recibida del bus:');
1248
- console.log(` de: @${msg.from}`);
1249
- console.log(` correlationId: ${msg.correlationId || '(sin id)'}`);
1250
- console.log(` texto: ${msg.text}`);
1251
- console.log('');
1252
- console.log(' Responde con: /refacil:reply "<respuesta>"');
1253
- console.log(' Luego vuelve a ejecutar /refacil:attend para seguir escuchando.');
1254
- }
1255
-
1256
- async function busAttend(args) {
1257
- const session = args.session || defaultSessionName();
1258
- const timeoutSec = args.timeout ? parseInt(args.timeout, 10) : 540;
1259
- const { ws } = await connectOrDie();
1260
-
1261
- // 1) Revisar preguntas pendientes en el historial antes de suscribirse al push.
1262
- const hist = await busClient.sendAndWait(
1263
- ws,
1264
- 'history',
1265
- { session, n: 50 },
1266
- (d) => d.type === 'history',
1267
- 3000,
1268
- );
1269
- if (hist) {
1270
- const pending = findFirstUnansweredAsk(hist.messages || [], session);
1271
- if (pending) {
1272
- busClient.close(ws);
1273
- printAttendQuestion(pending);
1274
- return;
1275
- }
1276
- }
1277
-
1278
- // 2) Suscribirse y esperar push de ask dirigido a esta sesión.
1279
- const result = await new Promise((resolve) => {
1280
- let done = false;
1281
- const finish = (v) => {
1282
- if (done) return;
1283
- done = true;
1284
- clearTimeout(timer);
1285
- ws.removeListener('message', onMessage);
1286
- resolve(v);
1287
- };
1288
- const onMessage = (raw) => {
1289
- let data;
1290
- try { data = JSON.parse(raw.toString()); } catch (_) { return; }
1291
- if (data.type === 'msg' && data.kind === 'ask' && data.to === session) {
1292
- finish({ kind: 'message', msg: data });
1293
- }
1294
- };
1295
- ws.on('message', onMessage);
1296
- const timer = setTimeout(() => finish({ kind: 'timeout' }), timeoutSec * 1000);
1297
- busClient.send(ws, 'attend', { session });
1298
- });
1299
-
1300
- busClient.close(ws);
1301
-
1302
- if (result.kind === 'message') {
1303
- printAttendQuestion(result.msg);
1304
- } else {
1305
- console.log(` Sin preguntas en ${timeoutSec}s. Re-ejecuta /refacil:attend para seguir escuchando.`);
1306
- }
1307
- }
1308
-
1309
- function readPersistedSessions() {
1310
- try {
1311
- return JSON.parse(fs.readFileSync(busBroker.SESSIONS_PATH, 'utf8'));
1312
- } catch (_) {
1313
- return {};
1314
- }
1315
- }
1316
-
1317
- async function busWatchCmd(positional, args) {
1318
- const session = args.session || positional || null;
1319
- let room = args.room || null;
1320
- if (session && !room) {
1321
- const persisted = readPersistedSessions();
1322
- if (persisted[session] && persisted[session].room) {
1323
- room = persisted[session].room;
1324
- }
1325
- }
1326
- if (!session && !room) {
1327
- console.error(' Uso: refacil-sdd-ai bus watch <session> [--room <sala>]');
1328
- process.exit(1);
1329
- }
1330
- try {
1331
- const { info } = await busSpawn.ensureBroker(packageRoot);
1332
- await busWatch.start({ session, room, port: info.port });
1333
- } catch (err) {
1334
- console.error(` No se pudo iniciar el watch: ${err.message}`);
1335
- process.exit(1);
1336
- }
1337
- }
1338
-
1339
- function openInBrowser(url) {
1340
- const { spawn } = require('child_process');
1341
- const platform = process.platform;
1342
- let cmd;
1343
- let args;
1344
- if (platform === 'win32') {
1345
- cmd = 'cmd';
1346
- args = ['/c', 'start', '""', url];
1347
- } else if (platform === 'darwin') {
1348
- cmd = 'open';
1349
- args = [url];
1350
- } else {
1351
- cmd = 'xdg-open';
1352
- args = [url];
1353
- }
1354
- try {
1355
- spawn(cmd, args, { detached: true, stdio: 'ignore', windowsHide: true }).unref();
1356
- return true;
1357
- } catch (_) {
1358
- return false;
1359
- }
1360
- }
1361
-
1362
- async function busView() {
1363
- try {
1364
- const { info } = await busSpawn.ensureBroker(packageRoot);
1365
- const url = `http://127.0.0.1:${info.port}/`;
1366
- console.log(` refacil-bus view disponible en: ${url}`);
1367
- const opened = openInBrowser(url);
1368
- if (!opened) {
1369
- console.log(' (no se pudo abrir el navegador automáticamente, abre la URL manualmente)');
1370
- }
1371
- } catch (err) {
1372
- console.error(` No se pudo iniciar la vista: ${err.message}`);
1373
- process.exit(1);
1374
- }
1375
- }
1376
-
1377
- async function busRooms() {
1378
- const { ws } = await connectOrDie();
1379
- const reply = await busClient.sendAndWait(
1380
- ws,
1381
- 'status',
1382
- {},
1383
- (d) => d.type === 'system' && d.event === 'status',
1384
- 3000,
1385
- );
1386
- busClient.close(ws);
1387
- if (!reply) {
1388
- console.log(' Sin respuesta del broker.');
1389
- return;
1390
- }
1391
- const rooms = (reply.detail && reply.detail.rooms) || {};
1392
- const names = Object.keys(rooms);
1393
- if (names.length === 0) {
1394
- console.log(' No hay salas activas.');
1395
- return;
1396
- }
1397
- console.log(' Salas activas:');
1398
- for (const name of names) {
1399
- const members = rooms[name] || [];
1400
- console.log(` ${name} (${members.length}): ${members.join(', ')}`);
1401
- }
1402
- }
1403
-
1404
- function handleBusSubcommand(sub) {
1405
- const rest = process.argv.slice(4);
1406
- const positional = rest.length > 0 && !rest[0].startsWith('--') ? rest[0] : null;
1407
- const args = parseBusArgs(rest);
1408
- switch (sub) {
1409
- case 'start':
1410
- busStart();
1411
- break;
1412
- case 'stop':
1413
- busStop();
1414
- break;
1415
- case 'status':
1416
- busStatus();
1417
- break;
1418
- case 'serve':
1419
- busServe();
1420
- break;
1421
- case 'join':
1422
- busJoin(args);
1423
- break;
1424
- case 'leave':
1425
- busLeave(args);
1426
- break;
1427
- case 'say':
1428
- busSay(args);
1429
- break;
1430
- case 'ask':
1431
- busAsk(args);
1432
- break;
1433
- case 'reply':
1434
- busReply(args);
1435
- break;
1436
- case 'history':
1437
- busHistory(args);
1438
- break;
1439
- case 'inbox':
1440
- busInbox(args);
1441
- break;
1442
- case 'rooms':
1443
- busRooms();
1444
- break;
1445
- case 'watch':
1446
- busWatchCmd(positional, args);
1447
- break;
1448
- case 'attend':
1449
- busAttend(args);
1450
- break;
1451
- case 'view':
1452
- busView();
1453
- break;
1454
- default:
1455
- console.log('Uso: refacil-sdd-ai bus <start|stop|status|serve|join|leave|say|ask|reply|history|inbox|rooms|watch|attend|view>');
1456
- }
1457
- }
1458
-
1459
323
  function help() {
1460
324
  console.log(`
1461
325
  refacil-sdd-ai — Metodologia SDD-AI con OpenSpec
@@ -1523,10 +387,10 @@ switch (command) {
1523
387
  compactBash.run();
1524
388
  break;
1525
389
  case 'compact':
1526
- handleCompactSubcommand(process.argv[3]);
390
+ handleCompact(process.argv[3]);
1527
391
  break;
1528
392
  case 'bus':
1529
- handleBusSubcommand(process.argv[3]);
393
+ handleBus(process.argv[3], process.argv.slice(4), packageRoot);
1530
394
  break;
1531
395
  case 'clean':
1532
396
  clean();