refacil-sdd-ai 4.4.1 → 4.5.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/agents/validator.md +1 -1
- package/bin/cli.js +225 -37
- package/lib/hooks.js +43 -7
- package/lib/ignore-files.js +2 -1
- package/lib/installer.js +179 -15
- package/lib/opencode-plugin/index.js +262 -0
- package/package.json +7 -3
package/agents/validator.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: refacil-validator
|
|
3
|
-
description: Validates implementation against SDD specs (CA/CR) and tests. Delegated by /refacil:verify — do not invoke directly.
|
|
3
|
+
description: Validates implementation against SDD specs (CA/CR) and tests. Delegated by /refacil:verify — do not invoke directly. Never modifies files.
|
|
4
4
|
tools: Read, Grep, Glob, Bash
|
|
5
5
|
model: sonnet
|
|
6
6
|
---
|
package/bin/cli.js
CHANGED
|
@@ -12,7 +12,9 @@ const compactBash = require('../lib/compact/bash');
|
|
|
12
12
|
const {
|
|
13
13
|
installSkills,
|
|
14
14
|
installAgents,
|
|
15
|
+
installOpenCodeJson,
|
|
15
16
|
removeSkills,
|
|
17
|
+
removeOpenCodeArtifacts,
|
|
16
18
|
removeOpenspecLegacyAssets,
|
|
17
19
|
createClaudeMd,
|
|
18
20
|
createCursorRules,
|
|
@@ -22,7 +24,7 @@ const {
|
|
|
22
24
|
checkNodeVersion,
|
|
23
25
|
checkClaudeCodeVersion,
|
|
24
26
|
} = require('../lib/installer');
|
|
25
|
-
const { installHooks, uninstallHooks, cleanLegacySettingsHooks } = require('../lib/hooks');
|
|
27
|
+
const { installHooks, uninstallHooks, cleanLegacySettingsHooks, installOpenCodePlugin, uninstallOpenCodePlugin } = require('../lib/hooks');
|
|
26
28
|
const { handleCompact } = require('../lib/commands/compact');
|
|
27
29
|
const { handleBus } = require('../lib/commands/bus');
|
|
28
30
|
const { handleSdd, autoMigrateOpenspec, findProjectRoot } = require('../lib/commands/sdd');
|
|
@@ -119,10 +121,108 @@ function notifyUpdate() {
|
|
|
119
121
|
function repoIsInitialized() {
|
|
120
122
|
return (
|
|
121
123
|
fs.existsSync(path.join(projectRoot, '.claude', 'skills')) ||
|
|
122
|
-
fs.existsSync(path.join(projectRoot, '.cursor', 'skills'))
|
|
124
|
+
fs.existsSync(path.join(projectRoot, '.cursor', 'skills')) ||
|
|
125
|
+
fs.existsSync(path.join(projectRoot, '.opencode', 'skills'))
|
|
123
126
|
);
|
|
124
127
|
}
|
|
125
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Inline readline-based multi-select for IDE targets.
|
|
131
|
+
* Shows a checklist of available IDEs and returns the selected ones.
|
|
132
|
+
* Falls back to all IDEs if stdin is not a TTY.
|
|
133
|
+
* @param {Array<{label: string, value: string, selected: boolean}>} options
|
|
134
|
+
* @returns {Promise<string[]>} selected values
|
|
135
|
+
*/
|
|
136
|
+
function readlineMultiSelect(options) {
|
|
137
|
+
return new Promise((resolve) => {
|
|
138
|
+
const readline = require('readline');
|
|
139
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
140
|
+
|
|
141
|
+
const selected = options.map((o) => o.selected);
|
|
142
|
+
let renderCount = 0;
|
|
143
|
+
|
|
144
|
+
function render() {
|
|
145
|
+
if (renderCount > 0) {
|
|
146
|
+
process.stdout.write(`\x1B[${options.length + 7}A\x1B[J`);
|
|
147
|
+
}
|
|
148
|
+
renderCount++;
|
|
149
|
+
console.log('\n Select IDEs to install refacil-sdd-ai into:');
|
|
150
|
+
console.log(' (space = toggle, enter = confirm, a = toggle all)\n');
|
|
151
|
+
for (let i = 0; i < options.length; i++) {
|
|
152
|
+
const check = selected[i] ? '[x]' : '[ ]';
|
|
153
|
+
console.log(` ${check} ${options[i].label}`);
|
|
154
|
+
}
|
|
155
|
+
console.log('\n Enter numbers to toggle (e.g. 1,2,3) or "a" for all, then Enter to confirm:');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
render();
|
|
159
|
+
|
|
160
|
+
rl.on('line', (line) => {
|
|
161
|
+
const input = line.trim().toLowerCase();
|
|
162
|
+
if (input === '') {
|
|
163
|
+
rl.close();
|
|
164
|
+
resolve(options.filter((_, i) => selected[i]).map((o) => o.value));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (input === 'a') {
|
|
168
|
+
const anySelected = selected.some(Boolean);
|
|
169
|
+
for (let i = 0; i < selected.length; i++) selected[i] = !anySelected;
|
|
170
|
+
} else {
|
|
171
|
+
const nums = input.split(/[\s,]+/).map(Number).filter((n) => !isNaN(n) && n >= 1 && n <= options.length);
|
|
172
|
+
for (const n of nums) selected[n - 1] = !selected[n - 1];
|
|
173
|
+
}
|
|
174
|
+
render();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
rl.on('close', () => {
|
|
178
|
+
resolve(options.filter((_, i) => selected[i]).map((o) => o.value));
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Show interactive IDE selector if TTY and --all not in args.
|
|
185
|
+
* Pre-selects IDEs by folder presence.
|
|
186
|
+
* Returns selected IDE dirs (e.g. ['.claude', '.cursor', '.opencode']).
|
|
187
|
+
*/
|
|
188
|
+
async function selectIDEs() {
|
|
189
|
+
const allFlag = process.argv.includes('--all');
|
|
190
|
+
const allIDEs = ['.claude', '.cursor', '.opencode'];
|
|
191
|
+
|
|
192
|
+
// --all or non-TTY: install all three
|
|
193
|
+
if (allFlag || !process.stdout.isTTY) {
|
|
194
|
+
return allIDEs;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const options = [
|
|
198
|
+
{ label: 'Claude Code (.claude/)', value: '.claude', selected: fs.existsSync(path.join(projectRoot, '.claude')) },
|
|
199
|
+
{ label: 'Cursor (.cursor/)', value: '.cursor', selected: fs.existsSync(path.join(projectRoot, '.cursor')) },
|
|
200
|
+
{ label: 'OpenCode (.opencode/)', value: '.opencode', selected: fs.existsSync(path.join(projectRoot, '.opencode')) },
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
// Try @clack/prompts first, fall back to inline readline
|
|
204
|
+
let selected;
|
|
205
|
+
try {
|
|
206
|
+
// @clack/prompts is an optional peer dep — try to load it without crashing if absent
|
|
207
|
+
const clack = require('@clack/prompts');
|
|
208
|
+
const result = await clack.multiselect({
|
|
209
|
+
message: 'Select IDEs to install refacil-sdd-ai into:',
|
|
210
|
+
options: options.map((o) => ({ label: o.label, value: o.value, selected: o.selected })),
|
|
211
|
+
required: false,
|
|
212
|
+
});
|
|
213
|
+
if (clack.isCancel(result)) {
|
|
214
|
+
console.log('\n Installation cancelled.\n');
|
|
215
|
+
process.exit(0);
|
|
216
|
+
}
|
|
217
|
+
selected = result;
|
|
218
|
+
} catch (_) {
|
|
219
|
+
// @clack/prompts not available — use inline readline fallback
|
|
220
|
+
selected = await readlineMultiSelect(options);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return selected;
|
|
224
|
+
}
|
|
225
|
+
|
|
126
226
|
function semverGt(a, b) {
|
|
127
227
|
if (!a || !b) return false;
|
|
128
228
|
const pa = a.split('.').map(Number);
|
|
@@ -297,7 +397,7 @@ function checkReview() {
|
|
|
297
397
|
|
|
298
398
|
// --- High-level commands ---
|
|
299
399
|
|
|
300
|
-
function init() {
|
|
400
|
+
async function init() {
|
|
301
401
|
console.log('\n refacil-sdd-ai: Initializing SDD-AI methodology...\n');
|
|
302
402
|
|
|
303
403
|
const nodeOk = checkNodeVersion();
|
|
@@ -313,35 +413,67 @@ function init() {
|
|
|
313
413
|
console.log(' Update with: npm install -g @anthropic-ai/claude-code\n');
|
|
314
414
|
}
|
|
315
415
|
|
|
316
|
-
|
|
317
|
-
|
|
416
|
+
// Select target IDEs (interactive selector or --all / non-TTY)
|
|
417
|
+
const selectedIDEs = await selectIDEs();
|
|
418
|
+
|
|
419
|
+
if (selectedIDEs.length === 0) {
|
|
420
|
+
console.log('\n No IDEs selected. Nothing installed.\n');
|
|
421
|
+
console.log(' Re-run with: refacil-sdd-ai init --all to install for all IDEs');
|
|
422
|
+
process.exit(0);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const installClaude = selectedIDEs.includes('.claude');
|
|
427
|
+
const installCursor = selectedIDEs.includes('.cursor');
|
|
428
|
+
const installOpenCode = selectedIDEs.includes('.opencode');
|
|
318
429
|
|
|
319
|
-
const
|
|
430
|
+
const count = installSkills(packageRoot, projectRoot, selectedIDEs);
|
|
431
|
+
const ideList = selectedIDEs.map((d) => `${d}/skills/`).join(', ');
|
|
432
|
+
console.log(` ${count} skills installed in ${ideList}`);
|
|
433
|
+
|
|
434
|
+
const agentsCount = installAgents(packageRoot, projectRoot, selectedIDEs);
|
|
320
435
|
if (agentsCount > 0) {
|
|
321
|
-
|
|
436
|
+
const agentList = selectedIDEs.map((d) => `${d}/agents/`).join(', ');
|
|
437
|
+
console.log(` ${agentsCount} sub-agents installed in ${agentList}`);
|
|
322
438
|
}
|
|
323
439
|
|
|
324
440
|
writeRepoVersion(projectRoot, getPackageVersion(packageRoot));
|
|
325
441
|
|
|
326
|
-
if (
|
|
327
|
-
|
|
442
|
+
if (installClaude) {
|
|
443
|
+
if (createClaudeMd(packageRoot, projectRoot)) console.log(' CLAUDE.md OK');
|
|
444
|
+
if (installHooks('.claude', projectRoot)) {
|
|
445
|
+
console.log(' Hook check-update added to .claude/settings.json');
|
|
446
|
+
}
|
|
447
|
+
}
|
|
328
448
|
|
|
329
|
-
if (
|
|
330
|
-
console.log('
|
|
449
|
+
if (installCursor) {
|
|
450
|
+
if (createCursorRules(packageRoot, projectRoot)) console.log(' .cursorrules OK');
|
|
451
|
+
if (installHooks('.cursor', projectRoot)) {
|
|
452
|
+
console.log(' Hook check-update added to .cursor/hooks.json');
|
|
453
|
+
}
|
|
331
454
|
}
|
|
332
|
-
|
|
333
|
-
|
|
455
|
+
|
|
456
|
+
if (installOpenCode) {
|
|
457
|
+
try {
|
|
458
|
+
installOpenCodeJson(projectRoot);
|
|
459
|
+
console.log(' .opencode/opencode.json created/updated');
|
|
460
|
+
} catch (err) {
|
|
461
|
+
console.error(` Warning: could not create opencode.json: ${err.message}`);
|
|
462
|
+
}
|
|
463
|
+
if (installHooks('.opencode', projectRoot)) {
|
|
464
|
+
console.log(' OpenCode plugin installed to .opencode/plugins/refacil-hooks.js');
|
|
465
|
+
}
|
|
334
466
|
}
|
|
335
467
|
|
|
336
468
|
try {
|
|
337
469
|
const ignoreResult = syncIgnoreFiles(projectRoot);
|
|
338
470
|
const s = ignoreResult.claude;
|
|
339
471
|
if (s.status === 'created') {
|
|
340
|
-
console.log(' .claudeignore and .
|
|
472
|
+
console.log(' .claudeignore, .cursorignore and .opencodeignore created');
|
|
341
473
|
} else if (s.status === 'updated') {
|
|
342
|
-
console.log(` .claudeignore and .
|
|
474
|
+
console.log(` .claudeignore, .cursorignore and .opencodeignore updated (${s.added} entries added)`);
|
|
343
475
|
} else {
|
|
344
|
-
console.log(' .claudeignore and .
|
|
476
|
+
console.log(' .claudeignore, .cursorignore and .opencodeignore are up to date');
|
|
345
477
|
}
|
|
346
478
|
} catch (err) {
|
|
347
479
|
console.error(` Warning: could not sync ignore files: ${err.message}`);
|
|
@@ -359,7 +491,7 @@ function init() {
|
|
|
359
491
|
}
|
|
360
492
|
|
|
361
493
|
console.log('\n Next steps:\n');
|
|
362
|
-
console.log(' 1. RESTART your
|
|
494
|
+
console.log(' 1. RESTART your IDE session');
|
|
363
495
|
console.log(' (new skills are not detected until the session is restarted)\n');
|
|
364
496
|
console.log(' 2. Run: /refacil:setup');
|
|
365
497
|
console.log(' (generates AGENTS.md for your project)\n');
|
|
@@ -368,12 +500,29 @@ function init() {
|
|
|
368
500
|
function update() {
|
|
369
501
|
console.log('\n refacil-sdd-ai: Updating skills...\n');
|
|
370
502
|
|
|
371
|
-
|
|
372
|
-
|
|
503
|
+
// Detect installed IDEs by folder presence
|
|
504
|
+
const hasClaudeDir = fs.existsSync(path.join(projectRoot, '.claude'));
|
|
505
|
+
const hasCursorDir = fs.existsSync(path.join(projectRoot, '.cursor'));
|
|
506
|
+
const hasOpenCodeDir = fs.existsSync(path.join(projectRoot, '.opencode'));
|
|
507
|
+
|
|
508
|
+
const detectedIDEs = [
|
|
509
|
+
hasClaudeDir && '.claude',
|
|
510
|
+
hasCursorDir && '.cursor',
|
|
511
|
+
hasOpenCodeDir && '.opencode',
|
|
512
|
+
].filter(Boolean);
|
|
513
|
+
|
|
514
|
+
const count = installSkills(packageRoot, projectRoot, detectedIDEs);
|
|
515
|
+
const installedDirs = detectedIDEs.map((d) => `${d}/skills/`);
|
|
516
|
+
console.log(` ${count} skills updated in ${installedDirs.join(', ') || '(none detected)'}`);
|
|
373
517
|
|
|
374
|
-
const agentsCount = installAgents(packageRoot, projectRoot);
|
|
518
|
+
const agentsCount = installAgents(packageRoot, projectRoot, detectedIDEs);
|
|
375
519
|
if (agentsCount > 0) {
|
|
376
|
-
|
|
520
|
+
const agentDirs = [
|
|
521
|
+
hasClaudeDir && '.claude/agents/',
|
|
522
|
+
hasCursorDir && '.cursor/agents/',
|
|
523
|
+
hasOpenCodeDir && '.opencode/agents/',
|
|
524
|
+
].filter(Boolean);
|
|
525
|
+
console.log(` ${agentsCount} sub-agents updated in ${agentDirs.join(', ')}`);
|
|
377
526
|
}
|
|
378
527
|
|
|
379
528
|
try {
|
|
@@ -387,25 +536,40 @@ function update() {
|
|
|
387
536
|
|
|
388
537
|
writeRepoVersion(projectRoot, getPackageVersion(packageRoot));
|
|
389
538
|
|
|
390
|
-
|
|
391
|
-
|
|
539
|
+
if (hasClaudeDir) {
|
|
540
|
+
createClaudeMd(packageRoot, projectRoot);
|
|
541
|
+
if (installHooks('.claude', projectRoot)) {
|
|
542
|
+
console.log(' Hook check-update added to .claude/settings.json');
|
|
543
|
+
}
|
|
544
|
+
}
|
|
392
545
|
|
|
393
|
-
if (
|
|
394
|
-
|
|
546
|
+
if (hasCursorDir) {
|
|
547
|
+
createCursorRules(packageRoot, projectRoot);
|
|
548
|
+
if (installHooks('.cursor', projectRoot)) {
|
|
549
|
+
console.log(' Hook check-update added to .cursor/hooks.json');
|
|
550
|
+
}
|
|
395
551
|
}
|
|
396
|
-
|
|
397
|
-
|
|
552
|
+
|
|
553
|
+
if (hasOpenCodeDir) {
|
|
554
|
+
try {
|
|
555
|
+
installOpenCodeJson(projectRoot);
|
|
556
|
+
} catch (err) {
|
|
557
|
+
console.error(` Warning: could not update opencode.json: ${err.message}`);
|
|
558
|
+
}
|
|
559
|
+
if (installHooks('.opencode', projectRoot)) {
|
|
560
|
+
console.log(' OpenCode plugin updated at .opencode/plugins/refacil-hooks.js');
|
|
561
|
+
}
|
|
398
562
|
}
|
|
399
563
|
|
|
400
564
|
try {
|
|
401
565
|
const ignoreResult = syncIgnoreFiles(projectRoot);
|
|
402
566
|
const s = ignoreResult.claude;
|
|
403
567
|
if (s.status === 'created') {
|
|
404
|
-
console.log(' .claudeignore and .
|
|
568
|
+
console.log(' .claudeignore, .cursorignore and .opencodeignore created');
|
|
405
569
|
} else if (s.status === 'updated') {
|
|
406
|
-
console.log(` .claudeignore and .
|
|
570
|
+
console.log(` .claudeignore, .cursorignore and .opencodeignore updated (${s.added} entries added)`);
|
|
407
571
|
} else {
|
|
408
|
-
console.log(' .claudeignore and .
|
|
572
|
+
console.log(' .claudeignore, .cursorignore and .opencodeignore are up to date');
|
|
409
573
|
}
|
|
410
574
|
} catch (err) {
|
|
411
575
|
console.error(` Warning: could not sync ignore files: ${err.message}`);
|
|
@@ -422,7 +586,7 @@ function update() {
|
|
|
422
586
|
console.error(` Warning: could not sync compact-guidance: ${err.message}`);
|
|
423
587
|
}
|
|
424
588
|
|
|
425
|
-
console.log('\n RESTART your
|
|
589
|
+
console.log('\n RESTART your IDE session to apply the changes.\n');
|
|
426
590
|
}
|
|
427
591
|
|
|
428
592
|
function clean() {
|
|
@@ -440,6 +604,19 @@ function clean() {
|
|
|
440
604
|
console.log(' SDD-AI hooks removed from .cursor/settings.json');
|
|
441
605
|
}
|
|
442
606
|
|
|
607
|
+
// Clean OpenCode artifacts if .opencode/ directory is present
|
|
608
|
+
if (fs.existsSync(path.join(projectRoot, '.opencode'))) {
|
|
609
|
+
try {
|
|
610
|
+
removeOpenCodeArtifacts(projectRoot);
|
|
611
|
+
console.log(' OpenCode skills and agents removed from .opencode/');
|
|
612
|
+
} catch (err) {
|
|
613
|
+
console.error(` Warning: could not remove OpenCode artifacts: ${err.message}`);
|
|
614
|
+
}
|
|
615
|
+
if (uninstallOpenCodePlugin(projectRoot)) {
|
|
616
|
+
console.log(' OpenCode plugin removed from .opencode/plugins/');
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
443
620
|
try {
|
|
444
621
|
const result = removeCompactGuidance(projectRoot);
|
|
445
622
|
if (result.status === 'removed') {
|
|
@@ -460,8 +637,10 @@ function help() {
|
|
|
460
637
|
refacil-sdd-ai — SDD-AI Methodology
|
|
461
638
|
|
|
462
639
|
Commands:
|
|
463
|
-
init Install skills in .claude/ and .
|
|
464
|
-
|
|
640
|
+
init Install skills in .claude/, .cursor/ and/or .opencode/ (interactive IDE selector).
|
|
641
|
+
Use --all to install for all three IDEs without prompting.
|
|
642
|
+
Creates CLAUDE.md, .cursorrules and .opencode/opencode.json as appropriate.
|
|
643
|
+
update Re-copy skills for all detected IDEs (.claude/, .cursor/, .opencode/)
|
|
465
644
|
migration-pending Same validation as hooks/notify-update: list migrations (exit 1 if any; --json)
|
|
466
645
|
check-update Sync skills and compact-guidance at session start (SessionStart hook)
|
|
467
646
|
notify-update Notify methodology migration only if applicable (UserPromptSubmit hook)
|
|
@@ -496,18 +675,24 @@ function help() {
|
|
|
496
675
|
sdd mark-reviewed <name> Write .review-passed (requires --verdict and --summary)
|
|
497
676
|
sdd tasks-update <name> Mark task N as completed (--task N --done)
|
|
498
677
|
sdd validate-name <name> Validate change name format
|
|
499
|
-
clean Remove skills and SDD-AI hooks from
|
|
678
|
+
clean Remove skills and SDD-AI hooks from all detected IDEs
|
|
679
|
+
(.claude/settings.json, .cursor/hooks.json, .opencode/plugins/)
|
|
500
680
|
help Show this help
|
|
501
681
|
|
|
502
682
|
Full flow:
|
|
503
683
|
1. npm install -g refacil-sdd-ai
|
|
504
684
|
2. refacil-sdd-ai init
|
|
505
|
-
3. RESTART
|
|
685
|
+
3. RESTART your IDE session (Claude Code, Cursor, or OpenCode)
|
|
506
686
|
4. Run: /refacil:setup (generates AGENTS.md for your project)
|
|
507
687
|
|
|
688
|
+
IDE support:
|
|
689
|
+
- Claude Code: .claude/skills/, .claude/agents/, .claude/settings.json hooks
|
|
690
|
+
- Cursor: .cursor/skills/, .cursor/agents/, .cursor/hooks.json hooks
|
|
691
|
+
- OpenCode: .opencode/skills/, .opencode/agents/, .opencode/plugins/refacil-hooks.js
|
|
692
|
+
|
|
508
693
|
Requirements:
|
|
509
694
|
- Node.js >= 20.0.0
|
|
510
|
-
- Claude Code >= 2.1.89 (required by compact-bash for silent rewrite) or
|
|
695
|
+
- Claude Code >= 2.1.89 (required by compact-bash for silent rewrite), Cursor, or OpenCode
|
|
511
696
|
`);
|
|
512
697
|
}
|
|
513
698
|
|
|
@@ -522,7 +707,10 @@ if (command === '--version' || command === '-v') {
|
|
|
522
707
|
|
|
523
708
|
switch (command) {
|
|
524
709
|
case 'init':
|
|
525
|
-
init()
|
|
710
|
+
init().catch((err) => {
|
|
711
|
+
console.error(` Error during init: ${err.message}`);
|
|
712
|
+
process.exit(1);
|
|
713
|
+
});
|
|
526
714
|
break;
|
|
527
715
|
case 'update':
|
|
528
716
|
update();
|
package/lib/hooks.js
CHANGED
|
@@ -219,18 +219,54 @@ function cleanLegacySettingsHooks(projectRoot) {
|
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
// ── OpenCode: plugin file ────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
function installOpenCodePlugin(projectRoot) {
|
|
225
|
+
const pluginsDir = path.join(projectRoot, '.opencode', 'plugins');
|
|
226
|
+
fs.mkdirSync(pluginsDir, { recursive: true });
|
|
227
|
+
|
|
228
|
+
const srcPlugin = path.join(__dirname, 'opencode-plugin', 'index.js');
|
|
229
|
+
const destPlugin = path.join(pluginsDir, 'refacil-hooks.js');
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
fs.copyFileSync(srcPlugin, destPlugin);
|
|
233
|
+
return true;
|
|
234
|
+
} catch (err) {
|
|
235
|
+
process.stderr.write(`[refacil-sdd-ai] Could not install OpenCode plugin: ${err.message}\n`);
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function uninstallOpenCodePlugin(projectRoot) {
|
|
241
|
+
const pluginPath = path.join(projectRoot, '.opencode', 'plugins', 'refacil-hooks.js');
|
|
242
|
+
if (!fs.existsSync(pluginPath)) return false;
|
|
243
|
+
try {
|
|
244
|
+
fs.unlinkSync(pluginPath);
|
|
245
|
+
return true;
|
|
246
|
+
} catch (err) {
|
|
247
|
+
process.stderr.write(`[refacil-sdd-ai] Could not remove OpenCode plugin: ${err.message}\n`);
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
222
252
|
// ── Fachada pública ──────────────────────────────────────────────────────────
|
|
223
253
|
|
|
224
254
|
function installHooks(ideDir, projectRoot) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
255
|
+
if (ideDir === '.cursor') return installCursorHooks(projectRoot);
|
|
256
|
+
if (ideDir === '.opencode') return installOpenCodePlugin(projectRoot);
|
|
257
|
+
return installClaudeHooks(projectRoot);
|
|
228
258
|
}
|
|
229
259
|
|
|
230
260
|
function uninstallHooks(ideDir, projectRoot) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
261
|
+
if (ideDir === '.cursor') return uninstallCursorHooks(projectRoot);
|
|
262
|
+
if (ideDir === '.opencode') return uninstallOpenCodePlugin(projectRoot);
|
|
263
|
+
return uninstallClaudeHooks(projectRoot);
|
|
234
264
|
}
|
|
235
265
|
|
|
236
|
-
module.exports = {
|
|
266
|
+
module.exports = {
|
|
267
|
+
installHooks,
|
|
268
|
+
uninstallHooks,
|
|
269
|
+
cleanLegacySettingsHooks,
|
|
270
|
+
installOpenCodePlugin,
|
|
271
|
+
uninstallOpenCodePlugin,
|
|
272
|
+
};
|
package/lib/ignore-files.js
CHANGED
|
@@ -82,7 +82,8 @@ function syncIgnoreFile(filePath) {
|
|
|
82
82
|
function syncIgnoreFiles(projectRoot) {
|
|
83
83
|
const claude = syncIgnoreFile(path.join(projectRoot, '.claudeignore'));
|
|
84
84
|
const cursor = syncIgnoreFile(path.join(projectRoot, '.cursorignore'));
|
|
85
|
-
|
|
85
|
+
const opencode = syncIgnoreFile(path.join(projectRoot, '.opencodeignore'));
|
|
86
|
+
return { claude, cursor, opencode };
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
module.exports = { syncIgnoreFiles, BASE_ENTRIES };
|
package/lib/installer.js
CHANGED
|
@@ -35,7 +35,18 @@ const AGENTS = [
|
|
|
35
35
|
'proposer',
|
|
36
36
|
];
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
// All 7 refacil agents are internal sub-agents — hidden from the agent picker in OpenCode
|
|
39
|
+
const INTERNAL_AGENTS = [
|
|
40
|
+
'investigator',
|
|
41
|
+
'validator',
|
|
42
|
+
'auditor',
|
|
43
|
+
'tester',
|
|
44
|
+
'implementer',
|
|
45
|
+
'debugger',
|
|
46
|
+
'proposer',
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const REPO_VERSION_FILES = ['.claude/.sdd-version', '.cursor/.sdd-version', '.opencode/.sdd-version'];
|
|
39
50
|
|
|
40
51
|
function copyDir(src, dest) {
|
|
41
52
|
fs.mkdirSync(dest, { recursive: true });
|
|
@@ -51,23 +62,59 @@ function copyDir(src, dest) {
|
|
|
51
62
|
}
|
|
52
63
|
}
|
|
53
64
|
|
|
54
|
-
function installSkills(packageRoot, projectRoot) {
|
|
65
|
+
function installSkills(packageRoot, projectRoot, ideDirs) {
|
|
66
|
+
const dirs = ideDirs || ['.claude', '.cursor', '.opencode'];
|
|
55
67
|
let installed = 0;
|
|
56
68
|
for (const skill of SKILLS) {
|
|
57
69
|
const srcDir = path.join(packageRoot, 'skills', skill);
|
|
58
70
|
if (!fs.existsSync(srcDir)) continue;
|
|
59
71
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
72
|
+
if (dirs.includes('.claude')) {
|
|
73
|
+
copyDir(srcDir, path.join(projectRoot, '.claude', 'skills', `refacil-${skill}`));
|
|
74
|
+
}
|
|
75
|
+
if (dirs.includes('.cursor')) {
|
|
76
|
+
copyDir(srcDir, path.join(projectRoot, '.cursor', 'skills', `refacil-${skill}`));
|
|
77
|
+
}
|
|
78
|
+
if (dirs.includes('.opencode')) {
|
|
79
|
+
// OpenCode: byte-for-byte copy (same as Claude Code — no transformation needed)
|
|
80
|
+
copyDir(srcDir, path.join(projectRoot, '.opencode', 'skills', `refacil-${skill}`));
|
|
81
|
+
}
|
|
65
82
|
|
|
66
83
|
installed++;
|
|
67
84
|
}
|
|
68
85
|
return installed;
|
|
69
86
|
}
|
|
70
87
|
|
|
88
|
+
// Create or safely merge .opencode/opencode.json with SDD-AI managed keys
|
|
89
|
+
// Preserves any pre-existing keys — never destructive
|
|
90
|
+
function installOpenCodeJson(projectRoot) {
|
|
91
|
+
const ocDir = path.join(projectRoot, '.opencode');
|
|
92
|
+
fs.mkdirSync(ocDir, { recursive: true });
|
|
93
|
+
const ocJsonPath = path.join(ocDir, 'opencode.json');
|
|
94
|
+
|
|
95
|
+
let existing = {};
|
|
96
|
+
if (fs.existsSync(ocJsonPath)) {
|
|
97
|
+
try {
|
|
98
|
+
existing = JSON.parse(fs.readFileSync(ocJsonPath, 'utf8'));
|
|
99
|
+
} catch (_) {
|
|
100
|
+
existing = {};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// SDD-AI managed keys (minimal — only $schema)
|
|
105
|
+
const sddKeys = {
|
|
106
|
+
'$schema': 'https://opencode.ai/config.json',
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const merged = Object.assign({}, sddKeys, existing);
|
|
110
|
+
// Ensure $schema is always the SDD-AI value if not already set by user
|
|
111
|
+
if (!existing['$schema']) {
|
|
112
|
+
merged['$schema'] = sddKeys['$schema'];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fs.writeFileSync(ocJsonPath, JSON.stringify(merged, null, 2) + '\n');
|
|
116
|
+
}
|
|
117
|
+
|
|
71
118
|
// Claude Code: tools allowlist granular, model: sonnet|opus|haiku
|
|
72
119
|
// Cursor: readonly: true|false (booleano), model: inherit (default)
|
|
73
120
|
function transformFrontmatterForCursor(content) {
|
|
@@ -113,13 +160,72 @@ function transformFrontmatterForCursor(content) {
|
|
|
113
160
|
return `---\n${out.join('\n')}\n---\n${body}`;
|
|
114
161
|
}
|
|
115
162
|
|
|
116
|
-
|
|
163
|
+
// OpenCode: tools → permission mapping, adds mode: subagent, hidden: true for internal agents, removes model:
|
|
164
|
+
// tools:[Edit,Write,NotebookEdit] → edit:allow, tools:[Bash] → bash:allow, WebFetch → always deny
|
|
165
|
+
function transformFrontmatterForOpenCode(content) {
|
|
166
|
+
const normalized = content.replace(/\r\n/g, '\n');
|
|
167
|
+
const match = normalized.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
168
|
+
if (!match) return content;
|
|
169
|
+
|
|
170
|
+
const [, frontmatterRaw, body] = match;
|
|
171
|
+
const lines = frontmatterRaw.split('\n');
|
|
172
|
+
const out = [];
|
|
173
|
+
let toolsLine = null;
|
|
174
|
+
let agentName = null;
|
|
175
|
+
|
|
176
|
+
for (const line of lines) {
|
|
177
|
+
if (line.startsWith('tools:')) {
|
|
178
|
+
toolsLine = line;
|
|
179
|
+
// Do not emit tools: line — OpenCode uses permission block instead
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (line.startsWith('model:')) {
|
|
183
|
+
// Remove model: line — OpenCode manages model selection separately
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (line.startsWith('name:')) {
|
|
187
|
+
const nameVal = line.slice('name:'.length).trim();
|
|
188
|
+
// Extract the base agent name from "refacil-<name>" or plain "<name>"
|
|
189
|
+
const nameMatch = nameVal.match(/refacil-(\S+)/);
|
|
190
|
+
agentName = nameMatch ? nameMatch[1] : nameVal;
|
|
191
|
+
out.push(line);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
out.push(line);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Determine permission values from tools list
|
|
198
|
+
const toolsList = toolsLine ? toolsLine.slice('tools:'.length).trim() : '';
|
|
199
|
+
const canEdit = /\b(Edit|Write|NotebookEdit)\b/.test(toolsList);
|
|
200
|
+
const canBash = /\bBash\b/.test(toolsList);
|
|
201
|
+
|
|
202
|
+
// Build permission block
|
|
203
|
+
out.push(`permission:`);
|
|
204
|
+
out.push(` edit: ${canEdit ? 'allow' : 'deny'}`);
|
|
205
|
+
out.push(` bash: ${canBash ? 'allow' : 'deny'}`);
|
|
206
|
+
out.push(` webfetch: deny`);
|
|
207
|
+
|
|
208
|
+
// Add mode: subagent
|
|
209
|
+
out.push(`mode: subagent`);
|
|
210
|
+
|
|
211
|
+
// Add hidden: true for internal agents
|
|
212
|
+
if (agentName && INTERNAL_AGENTS.includes(agentName)) {
|
|
213
|
+
out.push(`hidden: true`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return `---\n${out.join('\n')}\n---\n${body}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function installAgents(packageRoot, projectRoot, ideDirs) {
|
|
220
|
+
const dirs = ideDirs || ['.claude', '.cursor', '.opencode'];
|
|
117
221
|
let installed = 0;
|
|
118
222
|
|
|
119
223
|
const claudeDir = path.join(projectRoot, '.claude', 'agents');
|
|
120
224
|
const cursorDir = path.join(projectRoot, '.cursor', 'agents');
|
|
121
|
-
|
|
122
|
-
fs.mkdirSync(
|
|
225
|
+
const openCodeDir = path.join(projectRoot, '.opencode', 'agents');
|
|
226
|
+
if (dirs.includes('.claude')) fs.mkdirSync(claudeDir, { recursive: true });
|
|
227
|
+
if (dirs.includes('.cursor')) fs.mkdirSync(cursorDir, { recursive: true });
|
|
228
|
+
if (dirs.includes('.opencode')) fs.mkdirSync(openCodeDir, { recursive: true });
|
|
123
229
|
|
|
124
230
|
for (const agent of AGENTS) {
|
|
125
231
|
const srcFile = path.join(packageRoot, 'agents', `${agent}.md`);
|
|
@@ -127,11 +233,21 @@ function installAgents(packageRoot, projectRoot) {
|
|
|
127
233
|
|
|
128
234
|
const content = fs.readFileSync(srcFile, 'utf8');
|
|
129
235
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
236
|
+
if (dirs.includes('.claude')) {
|
|
237
|
+
fs.writeFileSync(path.join(claudeDir, `refacil-${agent}.md`), content);
|
|
238
|
+
}
|
|
239
|
+
if (dirs.includes('.cursor')) {
|
|
240
|
+
fs.writeFileSync(
|
|
241
|
+
path.join(cursorDir, `refacil-${agent}.md`),
|
|
242
|
+
transformFrontmatterForCursor(content),
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
if (dirs.includes('.opencode')) {
|
|
246
|
+
fs.writeFileSync(
|
|
247
|
+
path.join(openCodeDir, `refacil-${agent}.md`),
|
|
248
|
+
transformFrontmatterForOpenCode(content),
|
|
249
|
+
);
|
|
250
|
+
}
|
|
135
251
|
|
|
136
252
|
installed++;
|
|
137
253
|
}
|
|
@@ -139,6 +255,50 @@ function installAgents(packageRoot, projectRoot) {
|
|
|
139
255
|
return installed;
|
|
140
256
|
}
|
|
141
257
|
|
|
258
|
+
function removeOpenCodeArtifacts(projectRoot) {
|
|
259
|
+
// Remove .opencode/skills/refacil-*/
|
|
260
|
+
for (const skill of SKILLS) {
|
|
261
|
+
const skillDir = path.join(projectRoot, '.opencode', 'skills', `refacil-${skill}`);
|
|
262
|
+
if (fs.existsSync(skillDir)) {
|
|
263
|
+
try { fs.rmSync(skillDir, { recursive: true }); } catch (_) {}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Remove .opencode/agents/refacil-*.md
|
|
268
|
+
const agentsDir = path.join(projectRoot, '.opencode', 'agents');
|
|
269
|
+
if (fs.existsSync(agentsDir)) {
|
|
270
|
+
try {
|
|
271
|
+
const entries = fs.readdirSync(agentsDir, { withFileTypes: true });
|
|
272
|
+
for (const entry of entries) {
|
|
273
|
+
if (entry.isFile() && entry.name.startsWith('refacil-') && entry.name.endsWith('.md')) {
|
|
274
|
+
fs.unlinkSync(path.join(agentsDir, entry.name));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
} catch (_) {}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Remove .opencode/plugins/refacil-hooks.js
|
|
281
|
+
const pluginFile = path.join(projectRoot, '.opencode', 'plugins', 'refacil-hooks.js');
|
|
282
|
+
if (fs.existsSync(pluginFile)) {
|
|
283
|
+
try { fs.unlinkSync(pluginFile); } catch (_) {}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Revert SDD-AI keys from .opencode/opencode.json (currently only $schema key, leave file if other keys remain)
|
|
287
|
+
const ocJsonPath = path.join(projectRoot, '.opencode', 'opencode.json');
|
|
288
|
+
if (fs.existsSync(ocJsonPath)) {
|
|
289
|
+
try {
|
|
290
|
+
const json = JSON.parse(fs.readFileSync(ocJsonPath, 'utf8'));
|
|
291
|
+
delete json['$schema'];
|
|
292
|
+
const remaining = Object.keys(json);
|
|
293
|
+
if (remaining.length === 0) {
|
|
294
|
+
fs.unlinkSync(ocJsonPath);
|
|
295
|
+
} else {
|
|
296
|
+
fs.writeFileSync(ocJsonPath, JSON.stringify(json, null, 2) + '\n');
|
|
297
|
+
}
|
|
298
|
+
} catch (_) {}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
142
302
|
function writeGuideFile(destPath, header, label) {
|
|
143
303
|
const content =
|
|
144
304
|
`# ${header}\n\n` +
|
|
@@ -309,10 +469,14 @@ function checkNodeVersion() {
|
|
|
309
469
|
module.exports = {
|
|
310
470
|
SKILLS,
|
|
311
471
|
AGENTS,
|
|
472
|
+
INTERNAL_AGENTS,
|
|
312
473
|
copyDir,
|
|
313
474
|
installSkills,
|
|
475
|
+
installOpenCodeJson,
|
|
314
476
|
transformFrontmatterForCursor,
|
|
477
|
+
transformFrontmatterForOpenCode,
|
|
315
478
|
installAgents,
|
|
479
|
+
removeOpenCodeArtifacts,
|
|
316
480
|
createClaudeMd,
|
|
317
481
|
createCursorRules,
|
|
318
482
|
readRepoVersion,
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* refacil-sdd-ai OpenCode plugin
|
|
5
|
+
*
|
|
6
|
+
* Provides 4 hook equivalents for OpenCode:
|
|
7
|
+
* - session.created → check-update logic (sync compact-guidance, flag pending migrations)
|
|
8
|
+
* - tui.prompt.append → notify-update logic (prompt user to run /refacil:update if pending)
|
|
9
|
+
* - tool.execute.before → check-review + compact-bash logic
|
|
10
|
+
*
|
|
11
|
+
* This file is installed as .opencode/plugins/refacil-hooks.js.
|
|
12
|
+
* It resolves lib/compact/rules.js relative to its own __dirname at install time.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
|
|
18
|
+
// ── Resolve compact rules ────────────────────────────────────────────────────
|
|
19
|
+
// When installed, this file lives at .opencode/plugins/refacil-hooks.js.
|
|
20
|
+
// The compact rules live at <package>/lib/compact/rules.js.
|
|
21
|
+
// We walk up from __dirname looking for the package (node_modules/refacil-sdd-ai or
|
|
22
|
+
// the package root directly), falling back gracefully if not found.
|
|
23
|
+
|
|
24
|
+
let findRule = null;
|
|
25
|
+
|
|
26
|
+
(function loadCompactRules() {
|
|
27
|
+
const candidates = [
|
|
28
|
+
// Installed as plugin in .opencode/plugins/ — package is in node_modules
|
|
29
|
+
path.resolve(__dirname, '..', '..', 'node_modules', 'refacil-sdd-ai', 'lib', 'compact', 'rules.js'),
|
|
30
|
+
// Running from source (lib/opencode-plugin/index.js)
|
|
31
|
+
path.resolve(__dirname, '..', 'compact', 'rules.js'),
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
for (const candidate of candidates) {
|
|
35
|
+
try {
|
|
36
|
+
if (fs.existsSync(candidate)) {
|
|
37
|
+
const rules = require(candidate);
|
|
38
|
+
if (typeof rules.findRule === 'function') {
|
|
39
|
+
findRule = rules.findRule;
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} catch (_) {
|
|
44
|
+
// Try next candidate
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!findRule) {
|
|
49
|
+
process.stderr.write('[refacil-sdd-ai] WARNING: Could not load compact/rules.js — compact-bash hook disabled.\n');
|
|
50
|
+
}
|
|
51
|
+
})();
|
|
52
|
+
|
|
53
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
function getPendingUpdateFlagPath(projectRoot) {
|
|
56
|
+
return path.join(projectRoot, '.refacil-pending-update');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readPendingUpdateFlag(projectRoot) {
|
|
60
|
+
const flagPath = getPendingUpdateFlagPath(projectRoot);
|
|
61
|
+
if (!fs.existsSync(flagPath)) return null;
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(fs.readFileSync(flagPath, 'utf8'));
|
|
64
|
+
} catch (_) {
|
|
65
|
+
return {};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function writePendingUpdateFlag(projectRoot, from, to) {
|
|
70
|
+
try {
|
|
71
|
+
fs.writeFileSync(getPendingUpdateFlagPath(projectRoot), JSON.stringify({ from, to }));
|
|
72
|
+
} catch (_) {}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function clearPendingUpdateFlag(projectRoot) {
|
|
76
|
+
try {
|
|
77
|
+
const flagPath = getPendingUpdateFlagPath(projectRoot);
|
|
78
|
+
if (fs.existsSync(flagPath)) fs.unlinkSync(flagPath);
|
|
79
|
+
} catch (_) {}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readRepoVersion(projectRoot) {
|
|
83
|
+
const versionFiles = ['.opencode/.sdd-version', '.claude/.sdd-version', '.cursor/.sdd-version'];
|
|
84
|
+
for (const rel of versionFiles) {
|
|
85
|
+
try {
|
|
86
|
+
const raw = fs.readFileSync(path.join(projectRoot, rel), 'utf8').trim();
|
|
87
|
+
if (raw) return raw;
|
|
88
|
+
} catch (_) {}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function methodologyMigrationPending(projectRoot) {
|
|
94
|
+
// Look for refacil-sdd/changes with active (non-archived) tasks that still have pending migrations
|
|
95
|
+
// This is a lightweight check: look for changes that have tasks.md but no .review-passed
|
|
96
|
+
const changesDir = path.join(projectRoot, 'refacil-sdd', 'changes');
|
|
97
|
+
if (!fs.existsSync(changesDir)) return { pending: false, reasons: [] };
|
|
98
|
+
|
|
99
|
+
let entries;
|
|
100
|
+
try {
|
|
101
|
+
entries = fs.readdirSync(changesDir, { withFileTypes: true });
|
|
102
|
+
} catch (_) {
|
|
103
|
+
return { pending: false, reasons: [] };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const reasons = [];
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
if (!entry.isDirectory() || entry.name === 'archive') continue;
|
|
109
|
+
const tasksPath = path.join(changesDir, entry.name, 'tasks.md');
|
|
110
|
+
const tasksContent = fs.existsSync(tasksPath) ? fs.readFileSync(tasksPath, 'utf8') : '';
|
|
111
|
+
// Look for unchecked tasks: "- [ ]" pattern
|
|
112
|
+
if (/- \[ \]/.test(tasksContent)) {
|
|
113
|
+
reasons.push(`Change '${entry.name}' has pending tasks`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { pending: reasons.length > 0, reasons };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Hook handlers ────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* session.created — equivalent of check-update (SessionStart hook)
|
|
124
|
+
* Checks if the installed skills are out of date and flags a pending update.
|
|
125
|
+
*/
|
|
126
|
+
async function checkUpdateHandler(event) {
|
|
127
|
+
const projectRoot = event.projectRoot || process.cwd();
|
|
128
|
+
|
|
129
|
+
// Check if there is a pending methodology migration
|
|
130
|
+
try {
|
|
131
|
+
const mig = methodologyMigrationPending(projectRoot);
|
|
132
|
+
const repoVersion = readRepoVersion(projectRoot);
|
|
133
|
+
|
|
134
|
+
// Try to get the current package version via refacil-sdd-ai CLI
|
|
135
|
+
let packageVersion = null;
|
|
136
|
+
try {
|
|
137
|
+
const { execSync } = require('child_process');
|
|
138
|
+
packageVersion = execSync('refacil-sdd-ai --version', {
|
|
139
|
+
encoding: 'utf8',
|
|
140
|
+
timeout: 5000,
|
|
141
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
142
|
+
}).trim();
|
|
143
|
+
} catch (_) {}
|
|
144
|
+
|
|
145
|
+
const existingFlag = readPendingUpdateFlag(projectRoot);
|
|
146
|
+
|
|
147
|
+
if (mig.pending) {
|
|
148
|
+
writePendingUpdateFlag(projectRoot, repoVersion, packageVersion);
|
|
149
|
+
} else if (existingFlag) {
|
|
150
|
+
clearPendingUpdateFlag(projectRoot);
|
|
151
|
+
}
|
|
152
|
+
} catch (err) {
|
|
153
|
+
process.stderr.write(`[refacil-sdd-ai] check-update handler error: ${err.message}\n`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* tui.prompt.append — equivalent of notify-update (UserPromptSubmit hook)
|
|
159
|
+
* Returns an instruction string if there is a pending update, otherwise returns nothing.
|
|
160
|
+
* Also clears the flag if the user is running /refacil:update.
|
|
161
|
+
*/
|
|
162
|
+
async function notifyUpdateHandler(event) {
|
|
163
|
+
const projectRoot = event.projectRoot || process.cwd();
|
|
164
|
+
const prompt = (event.prompt || '').trim().toLowerCase();
|
|
165
|
+
|
|
166
|
+
// If user is running /refacil:update, clear the flag and let it through
|
|
167
|
+
if (prompt.includes('refacil:update') || prompt.includes('refacil/update')) {
|
|
168
|
+
clearPendingUpdateFlag(projectRoot);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const flagInfo = readPendingUpdateFlag(projectRoot);
|
|
173
|
+
if (!flagInfo) return;
|
|
174
|
+
|
|
175
|
+
const mig = methodologyMigrationPending(projectRoot);
|
|
176
|
+
if (!mig.pending) {
|
|
177
|
+
clearPendingUpdateFlag(projectRoot);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const fromLabel = flagInfo.from ? `v${flagInfo.from}` : 'previous version';
|
|
182
|
+
const toLabel = flagInfo.to ? `v${flagInfo.to}` : 'latest';
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
`[refacil-sdd-ai] Methodology update detected (${fromLabel} → ${toLabel}). ` +
|
|
186
|
+
`Run /refacil:update to apply pending migrations before continuing.`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* tool.execute.before — handles Bash tool calls:
|
|
192
|
+
* (a) check-review: blocks git push if any active change is missing .review-passed
|
|
193
|
+
* (b) compact-bash: rewrites matched commands to reduce token usage
|
|
194
|
+
*/
|
|
195
|
+
async function toolExecuteBeforeHandler(event) {
|
|
196
|
+
// Only handle Bash tool calls
|
|
197
|
+
if (!event || !event.tool || event.tool !== 'bash') return;
|
|
198
|
+
|
|
199
|
+
const command = (event.input && event.input.command) || (event.params && event.params.command) || '';
|
|
200
|
+
if (!command) return;
|
|
201
|
+
|
|
202
|
+
const projectRoot = event.projectRoot || process.cwd();
|
|
203
|
+
|
|
204
|
+
// (a) check-review: block git push if missing .review-passed
|
|
205
|
+
if (/git\s+push/.test(command)) {
|
|
206
|
+
const sddChangesDir = path.join(projectRoot, 'refacil-sdd', 'changes');
|
|
207
|
+
if (fs.existsSync(sddChangesDir)) {
|
|
208
|
+
let entries;
|
|
209
|
+
try {
|
|
210
|
+
entries = fs.readdirSync(sddChangesDir, { withFileTypes: true });
|
|
211
|
+
} catch (_) {
|
|
212
|
+
entries = [];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const activeChanges = entries.filter(
|
|
216
|
+
(e) => e.isDirectory() && e.name !== 'archive',
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
if (activeChanges.length > 0) {
|
|
220
|
+
const missing = activeChanges.filter(
|
|
221
|
+
(e) => !fs.existsSync(path.join(sddChangesDir, e.name, '.review-passed')),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
if (missing.length > 0) {
|
|
225
|
+
const names = missing.map((e) => e.name).join(', ');
|
|
226
|
+
const reason =
|
|
227
|
+
missing.length === 1
|
|
228
|
+
? `[refacil-sdd-ai] Review pending for: ${names}. ` +
|
|
229
|
+
'Stop the push and run /refacil:review on that change before pushing code. ' +
|
|
230
|
+
'If the review passes, retry the git push.'
|
|
231
|
+
: `[refacil-sdd-ai] Multiple changes without approved review: ${names}. ` +
|
|
232
|
+
'Stop the push and ask the user to explicitly select which change they want to push. ' +
|
|
233
|
+
'Then run /refacil:review <change-name> for that specific change and retry the push.';
|
|
234
|
+
|
|
235
|
+
throw new Error(reason);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// (b) compact-bash: rewrite matched commands to reduce token usage
|
|
242
|
+
// Skip if COMPACT=0 is set or findRule is not available
|
|
243
|
+
if (!findRule) return;
|
|
244
|
+
if (/\bCOMPACT=0\b/.test(command)) return;
|
|
245
|
+
|
|
246
|
+
const rule = findRule(command);
|
|
247
|
+
if (!rule) return;
|
|
248
|
+
|
|
249
|
+
const rewritten = rule.rewrite(command);
|
|
250
|
+
// Return the rewritten command for OpenCode to use instead
|
|
251
|
+
return { command: rewritten };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── Plugin export ────────────────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
module.exports = {
|
|
257
|
+
hooks: {
|
|
258
|
+
'session.created': checkUpdateHandler,
|
|
259
|
+
'tui.prompt.append': notifyUpdateHandler,
|
|
260
|
+
'tool.execute.before': toolExecuteBeforeHandler,
|
|
261
|
+
},
|
|
262
|
+
};
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "refacil-sdd-ai",
|
|
3
|
-
"version": "4.
|
|
4
|
-
"description": "SDD-AI: Specification-Driven Development with AI — development methodology using AI with Claude Code and
|
|
3
|
+
"version": "4.5.0",
|
|
4
|
+
"description": "SDD-AI: Specification-Driven Development with AI — development methodology using AI with Claude Code, Cursor and OpenCode",
|
|
5
5
|
"bin": {
|
|
6
6
|
"refacil-sdd-ai": "./bin/cli.js"
|
|
7
7
|
},
|
|
8
8
|
"files": [
|
|
9
9
|
"bin/",
|
|
10
10
|
"lib/",
|
|
11
|
+
"lib/opencode-plugin/",
|
|
11
12
|
"skills/",
|
|
12
13
|
"agents/",
|
|
13
14
|
"templates/",
|
|
@@ -35,9 +36,12 @@
|
|
|
35
36
|
"node": ">=20.0.0"
|
|
36
37
|
},
|
|
37
38
|
"scripts": {
|
|
38
|
-
"test": "node --test test/hooks.test.js test/installer.test.js test/ignore-files.test.js test/methodology-migration-pending.test.js test/sdd.test.js test/refactor-integrar-openspec-nativo.test.js test/refactor-rutas-refacil-sdd.test.js test/refactor-agents-english.test.js test/remove-openspec-legacy.test.js test/find-project-root.test.js"
|
|
39
|
+
"test": "node --test test/hooks.test.js test/installer.test.js test/ignore-files.test.js test/methodology-migration-pending.test.js test/sdd.test.js test/refactor-integrar-openspec-nativo.test.js test/refactor-rutas-refacil-sdd.test.js test/refactor-agents-english.test.js test/remove-openspec-legacy.test.js test/find-project-root.test.js test/opencode-installer.test.js test/opencode-plugin.test.js"
|
|
39
40
|
},
|
|
40
41
|
"dependencies": {
|
|
41
42
|
"ws": "^8.18.0"
|
|
43
|
+
},
|
|
44
|
+
"optionalDependencies": {
|
|
45
|
+
"@clack/prompts": "^0.9.0"
|
|
42
46
|
}
|
|
43
47
|
}
|