refacil-sdd-ai 4.4.0 → 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.
@@ -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. Read-only: does not apply fixes.
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,15 +24,15 @@ 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
- const { handleSdd, autoMigrateOpenspec } = require('../lib/commands/sdd');
30
+ const { handleSdd, autoMigrateOpenspec, findProjectRoot } = require('../lib/commands/sdd');
29
31
  const { syncIgnoreFiles } = require('../lib/ignore-files');
30
32
  const { methodologyMigrationPending } = require('../lib/methodology-migration-pending');
31
33
 
32
34
  const packageRoot = path.resolve(__dirname, '..');
33
- const projectRoot = process.cwd();
35
+ const projectRoot = findProjectRoot();
34
36
 
35
37
  // --- check-update (SessionStart) + notify-update (UserPromptSubmit) ---
36
38
 
@@ -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
- const count = installSkills(packageRoot, projectRoot);
317
- console.log(` ${count} skills installed in .claude/skills/ and .cursor/skills/`);
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 agentsCount = installAgents(packageRoot, projectRoot);
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
- console.log(` ${agentsCount} sub-agents installed in .claude/agents/ and .cursor/agents/`);
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 (createClaudeMd(packageRoot, projectRoot)) console.log(' CLAUDE.md OK');
327
- if (createCursorRules(packageRoot, projectRoot)) console.log(' .cursorrules OK');
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 (installHooks('.claude', projectRoot)) {
330
- console.log(' Hook check-update added to .claude/settings.json');
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
- if (installHooks('.cursor', projectRoot)) {
333
- console.log(' Hook check-update added to .cursor/settings.json');
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 .cursorignore created');
472
+ console.log(' .claudeignore, .cursorignore and .opencodeignore created');
341
473
  } else if (s.status === 'updated') {
342
- console.log(` .claudeignore and .cursorignore updated (${s.added} entries added)`);
474
+ console.log(` .claudeignore, .cursorignore and .opencodeignore updated (${s.added} entries added)`);
343
475
  } else {
344
- console.log(' .claudeignore and .cursorignore are up to date');
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 Claude Code or Cursor session');
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
- const count = installSkills(packageRoot, projectRoot);
372
- console.log(` ${count} skills updated in .claude/skills/ and .cursor/skills/`);
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
- console.log(` ${agentsCount} sub-agents updated in .claude/agents/ and .cursor/agents/`);
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
- createClaudeMd(packageRoot, projectRoot);
391
- createCursorRules(packageRoot, projectRoot);
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 (installHooks('.claude', projectRoot)) {
394
- console.log(' Hook check-update added to .claude/settings.json');
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
- if (installHooks('.cursor', projectRoot)) {
397
- console.log(' Hook check-update added to .cursor/settings.json');
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 .cursorignore created');
568
+ console.log(' .claudeignore, .cursorignore and .opencodeignore created');
405
569
  } else if (s.status === 'updated') {
406
- console.log(` .claudeignore and .cursorignore updated (${s.added} entries added)`);
570
+ console.log(` .claudeignore, .cursorignore and .opencodeignore updated (${s.added} entries added)`);
407
571
  } else {
408
- console.log(' .claudeignore and .cursorignore are up to date');
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 Claude Code or Cursor session to apply the changes.\n');
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 .cursor/, create CLAUDE.md and .cursorrules
464
- update Re-copy skills (to update to a new package version)
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 .claude/settings.json and .cursor/settings.json
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 the Claude Code or Cursor session
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
- - Node.js >= 20.19.0
510
- - Claude Code >= 2.1.89 (required by compact-bash for silent rewrite) or Cursor
694
+ - Node.js >= 20.0.0
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();
@@ -549,7 +737,7 @@ switch (command) {
549
737
  handleBus(process.argv[3], process.argv.slice(4), packageRoot);
550
738
  break;
551
739
  case 'sdd':
552
- handleSdd(process.argv[3], process.argv.slice(4));
740
+ handleSdd(process.argv[3], process.argv.slice(4), projectRoot);
553
741
  break;
554
742
  case 'clean':
555
743
  clean();