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.
- package/.github/workflows/test.yml +12 -0
- package/index.mjs +171 -0
- package/package.json +1 -1
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();
|