cc-hook-registry 2.0.0 → 3.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/index.mjs +144 -4
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -112,6 +112,7 @@ if (!command || command === '--help' || command === '-h') {
|
|
|
112
112
|
browse [category] Browse by category (safety, quality, approve, utility, monitoring, ux)
|
|
113
113
|
install <id> Install a hook
|
|
114
114
|
info <id> Show hook details
|
|
115
|
+
recommend Recommend hooks for current project
|
|
115
116
|
stats Registry statistics
|
|
116
117
|
|
|
117
118
|
Examples:
|
|
@@ -192,10 +193,55 @@ else if (command === 'install') {
|
|
|
192
193
|
console.log();
|
|
193
194
|
|
|
194
195
|
if (hook.install.startsWith('npx cc-safe-setup')) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
196
|
+
// Try direct download first (no cc-safe-setup dependency)
|
|
197
|
+
const exampleName = hook.install.match(/--install-example\s+(\S+)/)?.[1];
|
|
198
|
+
if (exampleName) {
|
|
199
|
+
const rawUrl = `https://raw.githubusercontent.com/yurukusa/cc-safe-setup/main/examples/${exampleName}.sh`;
|
|
200
|
+
const hookPath = join(HOME, '.claude', 'hooks', exampleName + '.sh');
|
|
201
|
+
try {
|
|
202
|
+
mkdirSync(join(HOME, '.claude', 'hooks'), { recursive: true });
|
|
203
|
+
const script = execSync(`curl -sL "${rawUrl}"`, { encoding: 'utf-8' });
|
|
204
|
+
if (script.startsWith('#!/bin/bash')) {
|
|
205
|
+
writeFileSync(hookPath, script);
|
|
206
|
+
chmodSync(hookPath, 0o755);
|
|
207
|
+
|
|
208
|
+
// Auto-register in settings.json
|
|
209
|
+
const trigger = script.includes('PreToolUse') ? 'PreToolUse' :
|
|
210
|
+
script.includes('PostToolUse') ? 'PostToolUse' :
|
|
211
|
+
script.includes('Stop') ? 'Stop' : 'PreToolUse';
|
|
212
|
+
const matcher = script.includes('Matcher: "Bash"') || script.includes('MATCHER: "Bash"') ? 'Bash' :
|
|
213
|
+
script.includes('Matcher: "Edit|Write"') ? 'Edit|Write' : '';
|
|
214
|
+
|
|
215
|
+
let settings = {};
|
|
216
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
217
|
+
try { settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); } catch {}
|
|
218
|
+
}
|
|
219
|
+
if (!settings.hooks) settings.hooks = {};
|
|
220
|
+
if (!settings.hooks[trigger]) settings.hooks[trigger] = [];
|
|
221
|
+
|
|
222
|
+
const existing = settings.hooks[trigger].flatMap(e => (e.hooks || []).map(h => h.command));
|
|
223
|
+
if (!existing.some(cmd => cmd.includes(exampleName))) {
|
|
224
|
+
settings.hooks[trigger].push({
|
|
225
|
+
matcher,
|
|
226
|
+
hooks: [{ type: 'command', command: hookPath }],
|
|
227
|
+
});
|
|
228
|
+
mkdirSync(dirname(SETTINGS_PATH), { recursive: true });
|
|
229
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
console.log(c.green + ' ✓ Installed: ' + hookPath + c.reset);
|
|
233
|
+
console.log(c.green + ' ✓ Registered in settings.json (' + trigger + ')' + c.reset);
|
|
234
|
+
console.log(c.dim + ' Restart Claude Code to activate.' + c.reset);
|
|
235
|
+
} else {
|
|
236
|
+
throw new Error('Invalid script');
|
|
237
|
+
}
|
|
238
|
+
} catch {
|
|
239
|
+
// Fallback to cc-safe-setup
|
|
240
|
+
console.log(c.dim + ' Direct download failed, using cc-safe-setup...' + c.reset);
|
|
241
|
+
try { execSync(hook.install, { stdio: 'inherit' }); } catch {}
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
try { execSync(hook.install, { stdio: 'inherit' }); } catch {}
|
|
199
245
|
}
|
|
200
246
|
} else {
|
|
201
247
|
console.log(c.dim + ' This hook requires manual installation. Follow the instructions above.' + c.reset);
|
|
@@ -227,6 +273,100 @@ else if (command === 'info') {
|
|
|
227
273
|
console.log();
|
|
228
274
|
}
|
|
229
275
|
|
|
276
|
+
else if (command === 'recommend') {
|
|
277
|
+
console.log();
|
|
278
|
+
console.log(c.bold + ' Recommended hooks for this project' + c.reset);
|
|
279
|
+
console.log();
|
|
280
|
+
|
|
281
|
+
const cwd = process.cwd();
|
|
282
|
+
const recommendations = [];
|
|
283
|
+
|
|
284
|
+
// Always recommend safety essentials
|
|
285
|
+
recommendations.push({ id: 'destructive-guard', reason: 'Essential — prevents rm -rf disasters', priority: 1 });
|
|
286
|
+
recommendations.push({ id: 'branch-guard', reason: 'Essential — prevents push to main', priority: 1 });
|
|
287
|
+
recommendations.push({ id: 'secret-guard', reason: 'Essential — prevents .env commits', priority: 1 });
|
|
288
|
+
|
|
289
|
+
// Detect tech stack
|
|
290
|
+
if (existsSync(join(cwd, 'package.json'))) {
|
|
291
|
+
const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf-8'));
|
|
292
|
+
recommendations.push({ id: 'auto-approve-build', reason: 'Node.js project detected', priority: 2 });
|
|
293
|
+
if (pkg.dependencies?.prisma || pkg.devDependencies?.prisma) {
|
|
294
|
+
recommendations.push({ id: 'block-database-wipe', reason: 'Prisma detected — protect against migrate reset', priority: 1 });
|
|
295
|
+
}
|
|
296
|
+
if (pkg.scripts?.deploy || pkg.scripts?.['vercel-build']) {
|
|
297
|
+
recommendations.push({ id: 'deploy-guard', reason: 'Deploy script detected', priority: 2 });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (existsSync(join(cwd, 'requirements.txt')) || existsSync(join(cwd, 'pyproject.toml'))) {
|
|
302
|
+
recommendations.push({ id: 'auto-approve-python', reason: 'Python project detected', priority: 2 });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (existsSync(join(cwd, 'Dockerfile')) || existsSync(join(cwd, 'docker-compose.yml'))) {
|
|
306
|
+
recommendations.push({ id: 'auto-approve-docker', reason: 'Docker detected', priority: 2 });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (existsSync(join(cwd, '.env')) || existsSync(join(cwd, '.env.local'))) {
|
|
310
|
+
recommendations.push({ id: 'env-source-guard', reason: '.env file present — prevent sourcing', priority: 1 });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (existsSync(join(cwd, 'Gemfile'))) {
|
|
314
|
+
recommendations.push({ id: 'block-database-wipe', reason: 'Rails detected — protect against db:drop', priority: 1 });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (existsSync(join(cwd, 'artisan'))) {
|
|
318
|
+
recommendations.push({ id: 'block-database-wipe', reason: 'Laravel detected — protect against migrate:fresh', priority: 1 });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Always useful
|
|
322
|
+
recommendations.push({ id: 'compound-command-approver', reason: 'Fixes permission matching for cd && commands', priority: 2 });
|
|
323
|
+
recommendations.push({ id: 'loop-detector', reason: 'Prevents infinite command loops', priority: 3 });
|
|
324
|
+
recommendations.push({ id: 'session-handoff', reason: 'Saves state for next session', priority: 3 });
|
|
325
|
+
recommendations.push({ id: 'cost-tracker', reason: 'Track session costs', priority: 3 });
|
|
326
|
+
|
|
327
|
+
// Deduplicate and sort by priority
|
|
328
|
+
const seen = new Set();
|
|
329
|
+
const unique = recommendations.filter(r => { if (seen.has(r.id)) return false; seen.add(r.id); return true; });
|
|
330
|
+
unique.sort((a, b) => a.priority - b.priority);
|
|
331
|
+
|
|
332
|
+
// Check what's already installed
|
|
333
|
+
let installed = new Set();
|
|
334
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
335
|
+
try {
|
|
336
|
+
const s = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
|
|
337
|
+
for (const entries of Object.values(s.hooks || {})) {
|
|
338
|
+
for (const e of entries) {
|
|
339
|
+
for (const h of (e.hooks || [])) {
|
|
340
|
+
if (h.command) installed.add(h.command.split('/').pop().replace('.sh', ''));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
} catch {}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
for (const rec of unique) {
|
|
348
|
+
const hook = REGISTRY.find(h => h.id === rec.id);
|
|
349
|
+
if (!hook) continue;
|
|
350
|
+
const isInstalled = installed.has(rec.id);
|
|
351
|
+
const icon = isInstalled ? c.green + '✓' + c.reset : c.yellow + '○' + c.reset;
|
|
352
|
+
const status = isInstalled ? c.dim + '(installed)' + c.reset : '';
|
|
353
|
+
console.log(' ' + icon + ' ' + c.bold + rec.id + c.reset + ' ' + status);
|
|
354
|
+
console.log(' ' + c.dim + rec.reason + c.reset);
|
|
355
|
+
if (!isInstalled) {
|
|
356
|
+
console.log(' ' + c.dim + 'Install: npx cc-hook-registry install ' + rec.id + c.reset);
|
|
357
|
+
}
|
|
358
|
+
console.log();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const notInstalled = unique.filter(r => !installed.has(r.id));
|
|
362
|
+
if (notInstalled.length === 0) {
|
|
363
|
+
console.log(c.green + ' All recommended hooks are installed!' + c.reset);
|
|
364
|
+
} else {
|
|
365
|
+
console.log(c.dim + ' ' + notInstalled.length + ' recommended hook(s) not yet installed.' + c.reset);
|
|
366
|
+
}
|
|
367
|
+
console.log();
|
|
368
|
+
}
|
|
369
|
+
|
|
230
370
|
else if (command === 'stats') {
|
|
231
371
|
const categories = {};
|
|
232
372
|
const sources = {};
|