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/README.md +402 -392
- package/bin/cli.js +81 -1217
- package/lib/commands/bus.js +499 -0
- package/lib/commands/compact.js +92 -0
- package/lib/hooks.js +107 -0
- package/lib/ignore-files.js +88 -0
- package/lib/installer.js +265 -0
- package/package.json +6 -2
- package/refacil-bus-diagrams.md +314 -0
- package/skills/archive/SKILL.md +191 -167
- package/skills/guide/SKILL.md +4 -2
- package/skills/setup/SKILL.md +123 -91
- package/skills/update/SKILL.md +81 -0
- package/templates/methodology-guide.md +52 -45
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
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
|
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;
|
|
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;
|
|
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
|
-
// ---
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
756
|
-
if (installHook()) {
|
|
201
|
+
if (installHooks('.claude', projectRoot)) {
|
|
757
202
|
console.log(' Hook check-update agregado a .claude/settings.json');
|
|
758
203
|
}
|
|
759
|
-
if (
|
|
204
|
+
if (installHooks('.cursor', projectRoot)) {
|
|
760
205
|
console.log(' Hook check-update agregado a .cursor/settings.json');
|
|
761
206
|
}
|
|
762
207
|
|
|
763
|
-
|
|
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
|
-
|
|
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
|
-
|
|
798
|
-
|
|
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 (
|
|
262
|
+
if (installHooks('.cursor', projectRoot)) {
|
|
802
263
|
console.log(' Hook check-update agregado a .cursor/settings.json');
|
|
803
264
|
}
|
|
804
265
|
|
|
805
|
-
|
|
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
|
-
|
|
296
|
+
|
|
297
|
+
const count = removeSkills(projectRoot);
|
|
823
298
|
console.log(` ${count} skills eliminadas de .claude/skills/ y .cursor/skills/`);
|
|
824
|
-
|
|
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 (
|
|
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
|
-
|
|
390
|
+
handleCompact(process.argv[3]);
|
|
1527
391
|
break;
|
|
1528
392
|
case 'bus':
|
|
1529
|
-
|
|
393
|
+
handleBus(process.argv[3], process.argv.slice(4), packageRoot);
|
|
1530
394
|
break;
|
|
1531
395
|
case 'clean':
|
|
1532
396
|
clean();
|