cc-hook-registry 4.0.0 → 5.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.
@@ -0,0 +1,12 @@
1
+ name: Tests
2
+ on: [push, pull_request]
3
+ jobs:
4
+ test:
5
+ runs-on: ubuntu-latest
6
+ steps:
7
+ - uses: actions/checkout@v4
8
+ - uses: actions/setup-node@v4
9
+ with:
10
+ node-version: '20'
11
+ - name: CLI tests
12
+ run: bash test.sh
package/index.mjs CHANGED
@@ -113,6 +113,8 @@ if (!command || command === '--help' || command === '-h') {
113
113
  install <id> Install a hook
114
114
  info <id> Show hook details
115
115
  recommend Recommend hooks for current project
116
+ init Interactive setup — install recommended hooks
117
+ list List all installed hooks with status
116
118
  update [id] Update one or all installed hooks
117
119
  uninstall <id> Remove an installed hook
118
120
  outdated Check installed hooks for updates
@@ -370,6 +372,175 @@ else if (command === 'recommend') {
370
372
  console.log();
371
373
  }
372
374
 
375
+ else if (command === 'init') {
376
+ console.log();
377
+ console.log(c.bold + ' cc-hook-registry init' + c.reset);
378
+ console.log(c.dim + ' Quick setup — installing essential + project-specific hooks' + c.reset);
379
+ console.log();
380
+
381
+ const cwd = process.cwd();
382
+ const toInstall = [];
383
+
384
+ // Essential hooks (always install)
385
+ toInstall.push('destructive-guard', 'branch-guard', 'secret-guard');
386
+
387
+ // Detect project type
388
+ if (existsSync(join(cwd, 'package.json'))) {
389
+ console.log(' ' + c.blue + '⬡' + c.reset + ' Node.js detected');
390
+ toInstall.push('auto-approve-build');
391
+ try {
392
+ const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf-8'));
393
+ if (pkg.dependencies?.prisma || pkg.devDependencies?.prisma) {
394
+ console.log(' ' + c.blue + '⬡' + c.reset + ' Prisma detected');
395
+ toInstall.push('block-database-wipe');
396
+ }
397
+ } catch {}
398
+ }
399
+ if (existsSync(join(cwd, 'requirements.txt')) || existsSync(join(cwd, 'pyproject.toml'))) {
400
+ console.log(' ' + c.blue + '⬡' + c.reset + ' Python detected');
401
+ toInstall.push('auto-approve-python');
402
+ }
403
+ if (existsSync(join(cwd, 'Dockerfile'))) {
404
+ console.log(' ' + c.blue + '⬡' + c.reset + ' Docker detected');
405
+ toInstall.push('auto-approve-docker');
406
+ }
407
+ if (existsSync(join(cwd, '.env'))) {
408
+ console.log(' ' + c.blue + '⬡' + c.reset + ' .env file detected');
409
+ toInstall.push('env-source-guard');
410
+ }
411
+ if (existsSync(join(cwd, 'Gemfile')) || existsSync(join(cwd, 'artisan'))) {
412
+ console.log(' ' + c.blue + '⬡' + c.reset + ' Rails/Laravel detected');
413
+ toInstall.push('block-database-wipe');
414
+ }
415
+
416
+ // Always useful
417
+ toInstall.push('compound-command-approver', 'loop-detector', 'session-handoff');
418
+
419
+ // Deduplicate
420
+ const unique = [...new Set(toInstall)];
421
+
422
+ // Check what's already installed
423
+ const installed = new Set();
424
+ if (existsSync(HOOKS_DIR)) {
425
+ const { readdirSync } = await import('fs');
426
+ for (const f of readdirSync(HOOKS_DIR)) {
427
+ installed.add(f.replace('.sh', ''));
428
+ }
429
+ }
430
+
431
+ const toActuallyInstall = unique.filter(id => !installed.has(id));
432
+
433
+ console.log();
434
+ if (toActuallyInstall.length === 0) {
435
+ console.log(c.green + ' All recommended hooks already installed!' + c.reset);
436
+ console.log();
437
+ process.exit(0);
438
+ }
439
+
440
+ console.log(c.bold + ' Installing ' + toActuallyInstall.length + ' hooks:' + c.reset);
441
+
442
+ for (const id of toActuallyInstall) {
443
+ const hook = REGISTRY.find(h => h.id === id);
444
+ if (!hook) continue;
445
+
446
+ // Try direct download
447
+ const rawUrl = `https://raw.githubusercontent.com/yurukusa/cc-safe-setup/main/examples/${id}.sh`;
448
+ const hookPath = join(HOOKS_DIR, id + '.sh');
449
+
450
+ try {
451
+ mkdirSync(HOOKS_DIR, { recursive: true });
452
+ const script = execSync(`curl -sL "${rawUrl}"`, { encoding: 'utf-8', timeout: 5000 });
453
+
454
+ if (script.startsWith('#!/bin/bash')) {
455
+ writeFileSync(hookPath, script);
456
+ chmodSync(hookPath, 0o755);
457
+
458
+ // Register in settings
459
+ const trigger = script.includes('PreToolUse') ? 'PreToolUse' :
460
+ script.includes('PostToolUse') ? 'PostToolUse' :
461
+ script.includes('Stop') ? 'Stop' :
462
+ script.includes('SessionStart') ? 'SessionStart' : 'PreToolUse';
463
+ const matcher = script.includes('MATCHER: "Bash"') ? 'Bash' :
464
+ script.includes('MATCHER: "Edit|Write"') ? 'Edit|Write' : '';
465
+
466
+ let settings = {};
467
+ if (existsSync(SETTINGS_PATH)) {
468
+ try { settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); } catch {}
469
+ }
470
+ if (!settings.hooks) settings.hooks = {};
471
+ if (!settings.hooks[trigger]) settings.hooks[trigger] = [];
472
+
473
+ const existing = settings.hooks[trigger].flatMap(e => (e.hooks || []).map(h => h.command));
474
+ if (!existing.some(cmd => cmd.includes(id))) {
475
+ settings.hooks[trigger].push({ matcher, hooks: [{ type: 'command', command: hookPath }] });
476
+ mkdirSync(dirname(SETTINGS_PATH), { recursive: true });
477
+ writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
478
+ }
479
+
480
+ console.log(' ' + c.green + '✓' + c.reset + ' ' + id);
481
+ }
482
+ } catch {
483
+ console.log(' ' + c.yellow + '✗' + c.reset + ' ' + id + c.dim + ' (download failed)' + c.reset);
484
+ }
485
+ }
486
+
487
+ console.log();
488
+ console.log(c.green + ' Done! Restart Claude Code to activate.' + c.reset);
489
+ console.log(c.dim + ' Run: npx cc-hook-registry list' + c.reset);
490
+ console.log();
491
+ }
492
+
493
+ else if (command === 'list') {
494
+ console.log();
495
+ console.log(c.bold + ' Installed Hooks' + c.reset);
496
+ console.log();
497
+
498
+ if (!existsSync(HOOKS_DIR)) {
499
+ console.log(c.dim + ' No hooks directory.' + c.reset);
500
+ process.exit(0);
501
+ }
502
+
503
+ const { readdirSync, statSync } = await import('fs');
504
+ const files = readdirSync(HOOKS_DIR).filter(f => f.endsWith('.sh')).sort();
505
+
506
+ // Count by trigger
507
+ let byTrigger = {};
508
+ if (existsSync(SETTINGS_PATH)) {
509
+ try {
510
+ const s = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
511
+ for (const [trigger, entries] of Object.entries(s.hooks || {})) {
512
+ for (const e of entries) {
513
+ for (const h of (e.hooks || [])) {
514
+ const name = (h.command || '').split('/').pop();
515
+ if (name) byTrigger[name] = trigger;
516
+ }
517
+ }
518
+ }
519
+ } catch {}
520
+ }
521
+
522
+ for (const file of files) {
523
+ const path = join(HOOKS_DIR, file);
524
+ const name = file.replace('.sh', '');
525
+ const size = statSync(path).size;
526
+ const mtime = statSync(path).mtime;
527
+ const age = Math.floor((Date.now() - mtime.getTime()) / 86400000);
528
+ const inRegistry = REGISTRY.some(h => h.id === name);
529
+ const trigger = byTrigger[file] || '?';
530
+
531
+ const icon = inRegistry ? c.green + '●' + c.reset : c.dim + '○' + c.reset;
532
+ const ageStr = age === 0 ? 'today' : age + 'd ago';
533
+ const triggerStr = c.dim + trigger.padEnd(16) + c.reset;
534
+
535
+ console.log(' ' + icon + ' ' + file.padEnd(30) + triggerStr + (size/1024).toFixed(1) + 'KB ' + c.dim + ageStr + c.reset);
536
+ }
537
+
538
+ console.log();
539
+ const inReg = files.filter(f => REGISTRY.some(h => h.id === f.replace('.sh', ''))).length;
540
+ console.log(' ' + files.length + ' hooks installed (' + c.green + inReg + ' in registry' + c.reset + ', ' + c.dim + (files.length - inReg) + ' custom' + c.reset + ')');
541
+ console.log();
542
+ }
543
+
373
544
  else if (command === 'update') {
374
545
  const targetId = args[1]; // Optional: update specific hook or all
375
546
  console.log();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-hook-registry",
3
- "version": "4.0.0",
3
+ "version": "5.0.0",
4
4
  "description": "Search, browse, and install Claude Code hooks from the community. GitHub-based registry, no server needed.",
5
5
  "main": "index.mjs",
6
6
  "bin": {