@hanzlaa/rcode 4.10.4 → 4.10.6

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/cli/install.js CHANGED
@@ -159,7 +159,9 @@ const ConfigSchema = z.object({
159
159
  branching_strategy: z.string().optional(),
160
160
  }).optional(),
161
161
  // Declared for validation only — default ('every') lives in the hook (rcode-hooks.cjs prompt-router).
162
- // Install does NOT write this key; the feature stays dormant until hooks are opted into via /rcode-enable-hooks.
162
+ // Install does NOT write this key; the nudge behavior itself is controlled by
163
+ // whether hooks were enabled at install time (resolveEnableHooks / --no-hooks)
164
+ // or later via /rcode-enable-hooks.
163
165
  prompt_nudge: z.enum(['every', 'once-per-intent', 'when-stale', 'off']).optional(),
164
166
  }).passthrough();
165
167
 
@@ -199,6 +201,10 @@ function parseArgs(argv) {
199
201
  // #199 — git pre-commit hook. null = install if .git/ present (default).
200
202
  // Set false by --no-git-hooks, true by --git-hooks.
201
203
  gitHooks: null,
204
+ // Claude Code guardrail hooks (.claude/settings.json). null = resolve via
205
+ // resolveEnableHooks() (interactive prompt, or default-on for --yes/non-TTY).
206
+ // Set false by --no-hooks, true by --enable-hooks.
207
+ enableHooks: null,
202
208
  // global install mode — targets ~/.claude/, skips per-project artifacts
203
209
  global: false,
204
210
  // silent — suppress non-error output (used by postinstall auto-run)
@@ -242,6 +248,8 @@ function parseArgs(argv) {
242
248
  else if (arg === '--no-backup') opts.noBackup = true; // #381
243
249
  else if (arg === '--no-git-hooks') opts.gitHooks = false; // #199
244
250
  else if (arg === '--git-hooks') opts.gitHooks = true; // #199
251
+ else if (arg === '--no-hooks') opts.enableHooks = false;
252
+ else if (arg === '--enable-hooks') opts.enableHooks = true;
245
253
  else if (arg === '--global') opts.global = true;
246
254
  else if (arg === '--local-only') opts.localOnly = true; // #938 — force self-contained install (don't defer to global skills)
247
255
  else if (arg === '--silent') opts.silent = true;
@@ -526,6 +534,91 @@ async function resolveCommitPlanning(opts) {
526
534
  return choice === 'commit';
527
535
  }
528
536
 
537
+ /**
538
+ * Resolve whether to merge rcode's guardrail hooks (pre-edit, bash-guard,
539
+ * prompt-router, etc.) into .claude/settings.json at install time. Default
540
+ * is ON — flag wins, else interactive confirm (Y default) on TTY installs,
541
+ * else default-on for --yes/--no-prompt/non-TTY runs so hooks work out of
542
+ * the box without requiring a separate /rcode-enable-hooks step.
543
+ */
544
+ async function resolveEnableHooks(opts) {
545
+ if (opts.enableHooks !== null) return opts.enableHooks;
546
+ if (opts.global) return false; // global install has no project-local .claude/settings.json target
547
+ if (!opts.ides.includes('claude')) return false; // hooks are Claude Code specific
548
+ if (opts.noPrompt || opts.yes || !process.stdin.isTTY) return true;
549
+
550
+ const enable = await clack.confirm({
551
+ message: '🛡️ Enable rcode guardrail hooks in .claude/settings.json? (pre-edit checks, bash-guard, prompt-router, etc.)',
552
+ initialValue: true,
553
+ });
554
+
555
+ if (clack.isCancel(enable)) {
556
+ clack.cancel('Install cancelled.');
557
+ process.exit(0);
558
+ }
559
+
560
+ return enable;
561
+ }
562
+
563
+ /**
564
+ * Merge rcode's opt-in guardrail hooks (rcode/templates/settings-hooks.json)
565
+ * into .claude/settings.json. Idempotent — skips matcher+command pairs that
566
+ * already exist. Mirrors the /rcode-enable-hooks workflow so a fresh install
567
+ * doesn't require running that command separately.
568
+ *
569
+ * Returns: { action: 'merged' | 'skipped-flag' | 'skipped-template-missing' | 'skipped-error' }
570
+ */
571
+ function ensureRcodeSettingsHooks(target, options = {}) {
572
+ if (options.enableHooks !== true) return { action: 'skipped-flag' };
573
+
574
+ const templatePath = path.join(PACKAGE_ROOT, 'rcode', 'templates', 'settings-hooks.json');
575
+ if (!fs.existsSync(templatePath)) return { action: 'skipped-template-missing' };
576
+
577
+ let template;
578
+ try {
579
+ template = JSON.parse(fs.readFileSync(templatePath, 'utf8'));
580
+ } catch {
581
+ return { action: 'skipped-error' };
582
+ }
583
+
584
+ const settingsDir = path.join(target, '.claude');
585
+ const settingsPath = path.join(settingsDir, 'settings.json');
586
+
587
+ let settings = {};
588
+ if (fs.existsSync(settingsPath)) {
589
+ try {
590
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
591
+ } catch {
592
+ return { action: 'skipped-error' };
593
+ }
594
+ }
595
+
596
+ settings.hooks = settings.hooks || {};
597
+
598
+ for (const [hookType, matchers] of Object.entries(template.hooks || {})) {
599
+ settings.hooks[hookType] = settings.hooks[hookType] || [];
600
+ for (const incoming of matchers) {
601
+ let existingMatcher = settings.hooks[hookType].find((m) => m.matcher === incoming.matcher);
602
+ if (!existingMatcher) {
603
+ existingMatcher = { matcher: incoming.matcher, hooks: [] };
604
+ settings.hooks[hookType].push(existingMatcher);
605
+ }
606
+ for (const hook of incoming.hooks) {
607
+ const dup = existingMatcher.hooks.some((h) => h.command === hook.command && h.type === hook.type);
608
+ if (!dup) existingMatcher.hooks.push(hook);
609
+ }
610
+ }
611
+ }
612
+
613
+ try {
614
+ fs.mkdirSync(settingsDir, { recursive: true });
615
+ writeFileAtomic(settingsPath, JSON.stringify(settings, null, 2) + '\n');
616
+ return { action: 'merged' };
617
+ } catch {
618
+ return { action: 'skipped-error' };
619
+ }
620
+ }
621
+
529
622
  function printHelp() {
530
623
  console.log(`
531
624
  rcode installer
@@ -542,6 +635,8 @@ Options:
542
635
  --language <lang> set communication_language (default: English)
543
636
  --mode <guided|yolo> default mode (default: guided)
544
637
  --ide <name> target IDE (claude, cursor, gemini; default: claude)
638
+ --enable-hooks merge rcode guardrail hooks into .claude/settings.json (default: on)
639
+ --no-hooks skip guardrail hooks; enable later via /rcode-enable-hooks
545
640
  --dry-run preview what would be written; exit without writing any files
546
641
  --list-files alias for --dry-run
547
642
  --help this text
@@ -2071,6 +2166,9 @@ async function installInner(opts) {
2071
2166
  // Resolve commit-planning preference (interactive prompt or flag) — #189.
2072
2167
  opts.commitPlanning = await resolveCommitPlanning(opts);
2073
2168
 
2169
+ // Resolve guardrail-hooks preference (interactive prompt or flag). Default on.
2170
+ opts.enableHooks = await resolveEnableHooks(opts);
2171
+
2074
2172
  console.log(`\n🕌 ${bold('rcode')} ${pc.cyan('v' + pkgVersion)} ${dim('→')} ${opts.target}`);
2075
2173
 
2076
2174
  // Detect an existing install and surface it (#195).
@@ -2771,6 +2869,10 @@ async function installInner(opts) {
2771
2869
  // Respects --no-git-hooks flag; skips silently when .git/ is absent.
2772
2870
  const hookReport = ensureRcodePreCommitHook(opts.target, { gitHooks: opts.gitHooks });
2773
2871
 
2872
+ // Merge rcode guardrail hooks into .claude/settings.json (pre-edit, bash-guard,
2873
+ // prompt-router, etc). Default-on; resolved above via resolveEnableHooks().
2874
+ const settingsHooksReport = ensureRcodeSettingsHooks(opts.target, { enableHooks: opts.enableHooks });
2875
+
2774
2876
  // Pull rcode brain content (v2.0 — issue #158).
2775
2877
  // Runs rcode-tools brain pull as a detached background process. Placeholder
2776
2878
  // URLs are skipped gracefully so this does not fail a fresh install.
@@ -2835,6 +2937,15 @@ async function installInner(opts) {
2835
2937
  }[hookReport.action] || 'pre-commit hook unchanged';
2836
2938
  console.log(' ' + dim(hookMsg));
2837
2939
  }
2940
+ if (settingsHooksReport) {
2941
+ const settingsHooksMsg = {
2942
+ 'merged': 'guardrail hooks enabled (.claude/settings.json)',
2943
+ 'skipped-flag': 'guardrail hooks skipped (--no-hooks or declined)',
2944
+ 'skipped-template-missing': 'guardrail hooks skipped (settings-hooks.json template missing)',
2945
+ 'skipped-error': 'guardrail hooks skipped (error merging .claude/settings.json)',
2946
+ }[settingsHooksReport.action] || 'guardrail hooks unchanged';
2947
+ console.log(' ' + dim(settingsHooksMsg));
2948
+ }
2838
2949
  if (skipped > 0) console.log(' ' + dim(`${skipped} files skipped (unchanged)`));
2839
2950
 
2840
2951
  // Diff display for preserved files (#251)