@hasna/hooks 0.0.4 → 0.0.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/bin/index.js +429 -133
- package/dist/index.js +46 -90
- package/hooks/hook-agentmessages/src/check-messages.ts +151 -0
- package/hooks/hook-agentmessages/src/install.ts +126 -0
- package/hooks/hook-agentmessages/src/session-start.ts +255 -0
- package/hooks/hook-agentmessages/src/uninstall.ts +89 -0
- package/hooks/hook-branchprotect/src/cli.ts +126 -0
- package/hooks/hook-branchprotect/src/hook.ts +88 -0
- package/hooks/hook-checkbugs/src/cli.ts +628 -0
- package/hooks/hook-checkbugs/src/hook.ts +335 -0
- package/hooks/hook-checkdocs/src/cli.ts +628 -0
- package/hooks/hook-checkdocs/src/hook.ts +310 -0
- package/hooks/hook-checkfiles/src/cli.ts +545 -0
- package/hooks/hook-checkfiles/src/hook.ts +321 -0
- package/hooks/hook-checklint/src/cli-patch.ts +32 -0
- package/hooks/hook-checklint/src/cli.ts +667 -0
- package/hooks/hook-checklint/src/hook.ts +473 -0
- package/hooks/hook-checkpoint/src/cli.ts +191 -0
- package/hooks/hook-checkpoint/src/hook.ts +207 -0
- package/hooks/hook-checksecurity/src/cli.ts +601 -0
- package/hooks/hook-checksecurity/src/hook.ts +334 -0
- package/hooks/hook-checktasks/src/cli.ts +578 -0
- package/hooks/hook-checktasks/src/hook.ts +308 -0
- package/hooks/hook-checktests/src/cli.ts +627 -0
- package/hooks/hook-checktests/src/hook.ts +334 -0
- package/hooks/hook-contextrefresh/src/cli.ts +152 -0
- package/hooks/hook-contextrefresh/src/hook.ts +148 -0
- package/hooks/hook-gitguard/src/cli.ts +159 -0
- package/hooks/hook-gitguard/src/hook.ts +129 -0
- package/hooks/hook-packageage/src/cli.ts +165 -0
- package/hooks/hook-packageage/src/hook.ts +177 -0
- package/hooks/hook-phonenotify/src/cli.ts +196 -0
- package/hooks/hook-phonenotify/src/hook.ts +139 -0
- package/hooks/hook-precompact/src/cli.ts +168 -0
- package/hooks/hook-precompact/src/hook.ts +122 -0
- package/package.json +2 -1
- package/.hooks/index.ts +0 -6
package/bin/index.js
CHANGED
|
@@ -3519,6 +3519,8 @@ var {
|
|
|
3519
3519
|
|
|
3520
3520
|
// src/cli/index.tsx
|
|
3521
3521
|
import chalk2 from "chalk";
|
|
3522
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
3523
|
+
import { join as join2 } from "path";
|
|
3522
3524
|
|
|
3523
3525
|
// src/cli/components/App.tsx
|
|
3524
3526
|
import { useState as useState7 } from "react";
|
|
@@ -4519,67 +4521,57 @@ function Spinner({ type = "dots" }) {
|
|
|
4519
4521
|
var build_default2 = Spinner;
|
|
4520
4522
|
|
|
4521
4523
|
// src/lib/installer.ts
|
|
4522
|
-
import { existsSync,
|
|
4524
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
4523
4525
|
import { join, dirname } from "path";
|
|
4524
4526
|
import { homedir } from "os";
|
|
4525
4527
|
import { fileURLToPath } from "url";
|
|
4526
4528
|
var __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
4527
4529
|
var HOOKS_DIR = existsSync(join(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join(__dirname2, "..", "..", "hooks") : join(__dirname2, "..", "hooks");
|
|
4528
|
-
|
|
4530
|
+
function getSettingsPath(scope = "global") {
|
|
4531
|
+
if (scope === "project") {
|
|
4532
|
+
return join(process.cwd(), ".claude", "settings.json");
|
|
4533
|
+
}
|
|
4534
|
+
return join(homedir(), ".claude", "settings.json");
|
|
4535
|
+
}
|
|
4529
4536
|
function getHookPath(name) {
|
|
4530
4537
|
const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
|
|
4531
4538
|
return join(HOOKS_DIR, hookName);
|
|
4532
4539
|
}
|
|
4533
|
-
function
|
|
4540
|
+
function hookExists(name) {
|
|
4541
|
+
return existsSync(getHookPath(name));
|
|
4542
|
+
}
|
|
4543
|
+
function readSettings(scope = "global") {
|
|
4544
|
+
const path = getSettingsPath(scope);
|
|
4534
4545
|
try {
|
|
4535
|
-
if (existsSync(
|
|
4536
|
-
return JSON.parse(readFileSync(
|
|
4546
|
+
if (existsSync(path)) {
|
|
4547
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
4537
4548
|
}
|
|
4538
4549
|
} catch {}
|
|
4539
4550
|
return {};
|
|
4540
4551
|
}
|
|
4541
|
-
function writeSettings(settings) {
|
|
4542
|
-
const
|
|
4552
|
+
function writeSettings(settings, scope = "global") {
|
|
4553
|
+
const path = getSettingsPath(scope);
|
|
4554
|
+
const dir = dirname(path);
|
|
4543
4555
|
if (!existsSync(dir)) {
|
|
4544
4556
|
mkdirSync(dir, { recursive: true });
|
|
4545
4557
|
}
|
|
4546
|
-
writeFileSync(
|
|
4558
|
+
writeFileSync(path, JSON.stringify(settings, null, 2) + `
|
|
4547
4559
|
`);
|
|
4548
4560
|
}
|
|
4549
4561
|
function installHook(name, options = {}) {
|
|
4550
|
-
const {
|
|
4562
|
+
const { scope = "global", overwrite = false } = options;
|
|
4551
4563
|
const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
|
|
4552
4564
|
const shortName = hookName.replace("hook-", "");
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
const destPath = join(destDir, hookName);
|
|
4556
|
-
if (!existsSync(sourcePath)) {
|
|
4557
|
-
return {
|
|
4558
|
-
hook: shortName,
|
|
4559
|
-
success: false,
|
|
4560
|
-
error: `Hook '${shortName}' not found`
|
|
4561
|
-
};
|
|
4565
|
+
if (!hookExists(shortName)) {
|
|
4566
|
+
return { hook: shortName, success: false, error: `Hook '${shortName}' not found` };
|
|
4562
4567
|
}
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
success: false,
|
|
4567
|
-
error: `Already installed. Use --overwrite to replace.`,
|
|
4568
|
-
path: destPath
|
|
4569
|
-
};
|
|
4568
|
+
const registered = getRegisteredHooks(scope);
|
|
4569
|
+
if (registered.includes(shortName) && !overwrite) {
|
|
4570
|
+
return { hook: shortName, success: false, error: "Already installed. Use --overwrite to replace.", scope };
|
|
4570
4571
|
}
|
|
4571
4572
|
try {
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
}
|
|
4575
|
-
cpSync(sourcePath, destPath, { recursive: true });
|
|
4576
|
-
registerHookInSettings(shortName);
|
|
4577
|
-
updateHooksIndex(destDir);
|
|
4578
|
-
return {
|
|
4579
|
-
hook: shortName,
|
|
4580
|
-
success: true,
|
|
4581
|
-
path: destPath
|
|
4582
|
-
};
|
|
4573
|
+
registerHook(shortName, scope);
|
|
4574
|
+
return { hook: shortName, success: true, scope };
|
|
4583
4575
|
} catch (error) {
|
|
4584
4576
|
return {
|
|
4585
4577
|
hook: shortName,
|
|
@@ -4588,20 +4580,18 @@ function installHook(name, options = {}) {
|
|
|
4588
4580
|
};
|
|
4589
4581
|
}
|
|
4590
4582
|
}
|
|
4591
|
-
function
|
|
4583
|
+
function registerHook(name, scope = "global") {
|
|
4592
4584
|
const meta = getHook(name);
|
|
4593
4585
|
if (!meta)
|
|
4594
4586
|
return;
|
|
4595
|
-
const settings = readSettings();
|
|
4587
|
+
const settings = readSettings(scope);
|
|
4596
4588
|
if (!settings.hooks)
|
|
4597
4589
|
settings.hooks = {};
|
|
4598
4590
|
const eventKey = meta.event;
|
|
4599
4591
|
if (!settings.hooks[eventKey])
|
|
4600
4592
|
settings.hooks[eventKey] = [];
|
|
4601
|
-
const hookCommand = `
|
|
4602
|
-
|
|
4603
|
-
if (existing)
|
|
4604
|
-
return;
|
|
4593
|
+
const hookCommand = `hooks run ${name}`;
|
|
4594
|
+
settings.hooks[eventKey] = settings.hooks[eventKey].filter((entry2) => !entry2.hooks?.some((h) => h.command === hookCommand));
|
|
4605
4595
|
const entry = {
|
|
4606
4596
|
hooks: [{ type: "command", command: hookCommand }]
|
|
4607
4597
|
};
|
|
@@ -4609,66 +4599,39 @@ function registerHookInSettings(name) {
|
|
|
4609
4599
|
entry.matcher = meta.matcher;
|
|
4610
4600
|
}
|
|
4611
4601
|
settings.hooks[eventKey].push(entry);
|
|
4612
|
-
writeSettings(settings);
|
|
4602
|
+
writeSettings(settings, scope);
|
|
4613
4603
|
}
|
|
4614
|
-
function
|
|
4604
|
+
function unregisterHook(name, scope = "global") {
|
|
4615
4605
|
const meta = getHook(name);
|
|
4616
4606
|
if (!meta)
|
|
4617
4607
|
return;
|
|
4618
|
-
const settings = readSettings();
|
|
4608
|
+
const settings = readSettings(scope);
|
|
4619
4609
|
if (!settings.hooks)
|
|
4620
4610
|
return;
|
|
4621
4611
|
const eventKey = meta.event;
|
|
4622
4612
|
if (!settings.hooks[eventKey])
|
|
4623
4613
|
return;
|
|
4624
|
-
const hookCommand = `
|
|
4625
|
-
settings.hooks[eventKey] = settings.hooks[eventKey].filter((entry) => !entry.hooks?.some((h) => h.command
|
|
4614
|
+
const hookCommand = `hooks run ${name}`;
|
|
4615
|
+
settings.hooks[eventKey] = settings.hooks[eventKey].filter((entry) => !entry.hooks?.some((h) => h.command === hookCommand));
|
|
4626
4616
|
if (settings.hooks[eventKey].length === 0) {
|
|
4627
4617
|
delete settings.hooks[eventKey];
|
|
4628
4618
|
}
|
|
4629
4619
|
if (Object.keys(settings.hooks).length === 0) {
|
|
4630
4620
|
delete settings.hooks;
|
|
4631
4621
|
}
|
|
4632
|
-
writeSettings(settings);
|
|
4633
|
-
}
|
|
4634
|
-
function updateHooksIndex(hooksDir) {
|
|
4635
|
-
const indexPath = join(hooksDir, "index.ts");
|
|
4636
|
-
const { readdirSync } = __require("fs");
|
|
4637
|
-
const hooks = readdirSync(hooksDir).filter((f) => f.startsWith("hook-") && !f.includes("."));
|
|
4638
|
-
const exports = hooks.map((h) => {
|
|
4639
|
-
const name = h.replace("hook-", "");
|
|
4640
|
-
return `export * as ${name} from './${h}/src/index.js';`;
|
|
4641
|
-
}).join(`
|
|
4642
|
-
`);
|
|
4643
|
-
const content = `/**
|
|
4644
|
-
* Auto-generated index of installed hooks
|
|
4645
|
-
* Do not edit manually - run 'hooks install' to update
|
|
4646
|
-
*/
|
|
4647
|
-
|
|
4648
|
-
${exports}
|
|
4649
|
-
`;
|
|
4650
|
-
writeFileSync(indexPath, content);
|
|
4622
|
+
writeSettings(settings, scope);
|
|
4651
4623
|
}
|
|
4652
|
-
function
|
|
4653
|
-
const
|
|
4654
|
-
if (!existsSync(hooksDir)) {
|
|
4655
|
-
return [];
|
|
4656
|
-
}
|
|
4657
|
-
const { readdirSync, statSync } = __require("fs");
|
|
4658
|
-
return readdirSync(hooksDir).filter((f) => {
|
|
4659
|
-
const fullPath = join(hooksDir, f);
|
|
4660
|
-
return f.startsWith("hook-") && statSync(fullPath).isDirectory();
|
|
4661
|
-
}).map((f) => f.replace("hook-", ""));
|
|
4662
|
-
}
|
|
4663
|
-
function getRegisteredHooks() {
|
|
4664
|
-
const settings = readSettings();
|
|
4624
|
+
function getRegisteredHooks(scope = "global") {
|
|
4625
|
+
const settings = readSettings(scope);
|
|
4665
4626
|
if (!settings.hooks)
|
|
4666
4627
|
return [];
|
|
4667
4628
|
const registered = [];
|
|
4668
4629
|
for (const eventKey of Object.keys(settings.hooks)) {
|
|
4669
4630
|
for (const entry of settings.hooks[eventKey]) {
|
|
4670
4631
|
for (const hook of entry.hooks || []) {
|
|
4671
|
-
const
|
|
4632
|
+
const newMatch = hook.command?.match(/^hooks run (\w+)$/);
|
|
4633
|
+
const oldMatch = hook.command?.match(/^hook-(\w+)$/);
|
|
4634
|
+
const match = newMatch || oldMatch;
|
|
4672
4635
|
if (match) {
|
|
4673
4636
|
registered.push(match[1]);
|
|
4674
4637
|
}
|
|
@@ -4677,18 +4640,17 @@ function getRegisteredHooks() {
|
|
|
4677
4640
|
}
|
|
4678
4641
|
return [...new Set(registered)];
|
|
4679
4642
|
}
|
|
4680
|
-
function
|
|
4681
|
-
|
|
4643
|
+
function getInstalledHooks(scope = "global") {
|
|
4644
|
+
return getRegisteredHooks(scope);
|
|
4645
|
+
}
|
|
4646
|
+
function removeHook(name, scope = "global") {
|
|
4682
4647
|
const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
|
|
4683
4648
|
const shortName = hookName.replace("hook-", "");
|
|
4684
|
-
const
|
|
4685
|
-
|
|
4686
|
-
if (!existsSync(hookPath)) {
|
|
4649
|
+
const registered = getRegisteredHooks(scope);
|
|
4650
|
+
if (!registered.includes(shortName)) {
|
|
4687
4651
|
return false;
|
|
4688
4652
|
}
|
|
4689
|
-
|
|
4690
|
-
unregisterHookFromSettings(shortName);
|
|
4691
|
-
updateHooksIndex(hooksDir);
|
|
4653
|
+
unregisterHook(shortName, scope);
|
|
4692
4654
|
return true;
|
|
4693
4655
|
}
|
|
4694
4656
|
|
|
@@ -5028,42 +4990,115 @@ function App({ initialHooks, overwrite = false }) {
|
|
|
5028
4990
|
// src/cli/index.tsx
|
|
5029
4991
|
import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
|
|
5030
4992
|
var program2 = new Command;
|
|
5031
|
-
|
|
4993
|
+
function resolveScope(options) {
|
|
4994
|
+
if (options.project)
|
|
4995
|
+
return "project";
|
|
4996
|
+
return "global";
|
|
4997
|
+
}
|
|
4998
|
+
program2.name("hooks").description("Install Claude Code hooks for your project").version("0.0.6");
|
|
5032
4999
|
program2.command("interactive", { isDefault: true }).alias("i").description("Interactive hook browser").action(() => {
|
|
5033
5000
|
render(/* @__PURE__ */ jsxDEV8(App, {}, undefined, false, undefined, this));
|
|
5034
5001
|
});
|
|
5035
|
-
program2.command("
|
|
5036
|
-
|
|
5002
|
+
program2.command("run").argument("<hook>", "Hook to run").description("Execute a hook (called by Claude Code)").action(async (hook) => {
|
|
5003
|
+
const meta = getHook(hook);
|
|
5004
|
+
if (!meta) {
|
|
5005
|
+
console.error(JSON.stringify({ error: `Hook '${hook}' not found` }));
|
|
5006
|
+
process.exit(1);
|
|
5007
|
+
}
|
|
5008
|
+
const hookDir = getHookPath(hook);
|
|
5009
|
+
const hookScript = join2(hookDir, "src", "hook.ts");
|
|
5010
|
+
if (!existsSync2(hookScript)) {
|
|
5011
|
+
console.error(JSON.stringify({ error: `Hook script not found: ${hookScript}` }));
|
|
5012
|
+
process.exit(1);
|
|
5013
|
+
}
|
|
5014
|
+
const stdin = await new Response(Bun.stdin.stream()).text();
|
|
5015
|
+
const proc = Bun.spawn(["bun", "run", hookScript], {
|
|
5016
|
+
stdin: new Response(stdin),
|
|
5017
|
+
stdout: "pipe",
|
|
5018
|
+
stderr: "pipe",
|
|
5019
|
+
env: process.env
|
|
5020
|
+
});
|
|
5021
|
+
const stdout = await new Response(proc.stdout).text();
|
|
5022
|
+
const stderr = await new Response(proc.stderr).text();
|
|
5023
|
+
const exitCode = await proc.exited;
|
|
5024
|
+
if (stdout)
|
|
5025
|
+
process.stdout.write(stdout);
|
|
5026
|
+
if (stderr)
|
|
5027
|
+
process.stderr.write(stderr);
|
|
5028
|
+
process.exit(exitCode);
|
|
5029
|
+
});
|
|
5030
|
+
program2.command("install").alias("add").argument("[hooks...]", "Hooks to install").option("-o, --overwrite", "Overwrite existing hooks", false).option("-a, --all", "Install all available hooks", false).option("-c, --category <category>", "Install all hooks in a category").option("-g, --global", "Install globally (~/.claude/settings.json)", false).option("-p, --project", "Install for current project (.claude/settings.json)", false).option("-j, --json", "Output as JSON", false).description("Install one or more hooks").action((hooks, options) => {
|
|
5031
|
+
const scope = resolveScope(options);
|
|
5032
|
+
let toInstall = hooks;
|
|
5033
|
+
if (options.all) {
|
|
5034
|
+
toInstall = HOOKS.map((h) => h.name);
|
|
5035
|
+
} else if (options.category) {
|
|
5036
|
+
const category = CATEGORIES.find((c) => c.toLowerCase() === options.category.toLowerCase());
|
|
5037
|
+
if (!category) {
|
|
5038
|
+
if (options.json) {
|
|
5039
|
+
console.log(JSON.stringify({ error: `Unknown category: ${options.category}`, available: [...CATEGORIES] }));
|
|
5040
|
+
} else {
|
|
5041
|
+
console.log(chalk2.red(`Unknown category: ${options.category}`));
|
|
5042
|
+
console.log(chalk2.dim(`Available: ${CATEGORIES.join(", ")}`));
|
|
5043
|
+
}
|
|
5044
|
+
return;
|
|
5045
|
+
}
|
|
5046
|
+
toInstall = getHooksByCategory(category).map((h) => h.name);
|
|
5047
|
+
}
|
|
5048
|
+
if (toInstall.length === 0) {
|
|
5037
5049
|
render(/* @__PURE__ */ jsxDEV8(App, {}, undefined, false, undefined, this));
|
|
5038
5050
|
return;
|
|
5039
5051
|
}
|
|
5052
|
+
const results = [];
|
|
5053
|
+
for (const name of toInstall) {
|
|
5054
|
+
const result = installHook(name, { scope, overwrite: options.overwrite });
|
|
5055
|
+
results.push(result);
|
|
5056
|
+
}
|
|
5057
|
+
if (options.json) {
|
|
5058
|
+
console.log(JSON.stringify({
|
|
5059
|
+
installed: results.filter((r) => r.success).map((r) => r.hook),
|
|
5060
|
+
failed: results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error })),
|
|
5061
|
+
total: results.length,
|
|
5062
|
+
success: results.filter((r) => r.success).length,
|
|
5063
|
+
scope
|
|
5064
|
+
}));
|
|
5065
|
+
return;
|
|
5066
|
+
}
|
|
5067
|
+
const settingsFile = scope === "project" ? ".claude/settings.json" : "~/.claude/settings.json";
|
|
5040
5068
|
console.log(chalk2.bold(`
|
|
5041
|
-
Installing hooks...
|
|
5069
|
+
Installing hooks (${scope})...
|
|
5042
5070
|
`));
|
|
5043
|
-
for (const
|
|
5044
|
-
const result = installHook(name, { overwrite: options.overwrite });
|
|
5071
|
+
for (const result of results) {
|
|
5045
5072
|
if (result.success) {
|
|
5046
|
-
const meta = getHook(
|
|
5047
|
-
console.log(chalk2.green(`\u2713 ${
|
|
5073
|
+
const meta = getHook(result.hook);
|
|
5074
|
+
console.log(chalk2.green(`\u2713 ${result.hook}`));
|
|
5048
5075
|
if (meta) {
|
|
5049
|
-
console.log(chalk2.dim(` ${meta.event}${meta.matcher ? ` [${meta.matcher}]` : ""} \u2192 hook
|
|
5076
|
+
console.log(chalk2.dim(` ${meta.event}${meta.matcher ? ` [${meta.matcher}]` : ""} \u2192 hooks run ${result.hook}`));
|
|
5050
5077
|
}
|
|
5051
5078
|
} else {
|
|
5052
|
-
console.log(chalk2.red(`\u2717 ${
|
|
5079
|
+
console.log(chalk2.red(`\u2717 ${result.hook}: ${result.error}`));
|
|
5053
5080
|
}
|
|
5054
5081
|
}
|
|
5055
5082
|
console.log(chalk2.dim(`
|
|
5056
|
-
|
|
5083
|
+
Registered in ${settingsFile}`));
|
|
5057
5084
|
});
|
|
5058
|
-
program2.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-a, --all", "Show all available hooks", false).option("-i, --installed", "Show only installed hooks", false).option("-r, --registered", "Show hooks registered in Claude settings", false).description("List available or installed hooks").action((options) => {
|
|
5059
|
-
|
|
5060
|
-
|
|
5085
|
+
program2.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-a, --all", "Show all available hooks", false).option("-i, --installed", "Show only installed hooks", false).option("-r, --registered", "Show hooks registered in Claude settings", false).option("-g, --global", "Check global settings", false).option("-p, --project", "Check project settings", false).option("-j, --json", "Output as JSON", false).description("List available or installed hooks").action((options) => {
|
|
5086
|
+
const scope = resolveScope(options);
|
|
5087
|
+
if (options.registered || options.installed) {
|
|
5088
|
+
const registered = getRegisteredHooks(scope);
|
|
5089
|
+
if (options.json) {
|
|
5090
|
+
console.log(JSON.stringify(registered.map((name) => {
|
|
5091
|
+
const meta = getHook(name);
|
|
5092
|
+
return { name, event: meta?.event, version: meta?.version, description: meta?.description, scope };
|
|
5093
|
+
})));
|
|
5094
|
+
return;
|
|
5095
|
+
}
|
|
5061
5096
|
if (registered.length === 0) {
|
|
5062
|
-
console.log(chalk2.dim(
|
|
5097
|
+
console.log(chalk2.dim(`No hooks registered (${scope})`));
|
|
5063
5098
|
return;
|
|
5064
5099
|
}
|
|
5065
5100
|
console.log(chalk2.bold(`
|
|
5066
|
-
Registered hooks (${registered.length}):
|
|
5101
|
+
Registered hooks \u2014 ${scope} (${registered.length}):
|
|
5067
5102
|
`));
|
|
5068
5103
|
for (const name of registered) {
|
|
5069
5104
|
const meta = getHook(name);
|
|
@@ -5071,28 +5106,22 @@ Registered hooks (${registered.length}):
|
|
|
5071
5106
|
}
|
|
5072
5107
|
return;
|
|
5073
5108
|
}
|
|
5074
|
-
if (options.installed) {
|
|
5075
|
-
const installed = getInstalledHooks();
|
|
5076
|
-
if (installed.length === 0) {
|
|
5077
|
-
console.log(chalk2.dim("No hooks installed"));
|
|
5078
|
-
return;
|
|
5079
|
-
}
|
|
5080
|
-
console.log(chalk2.bold(`
|
|
5081
|
-
Installed hooks (${installed.length}):
|
|
5082
|
-
`));
|
|
5083
|
-
for (const name of installed) {
|
|
5084
|
-
console.log(` ${name}`);
|
|
5085
|
-
}
|
|
5086
|
-
return;
|
|
5087
|
-
}
|
|
5088
5109
|
if (options.category) {
|
|
5089
5110
|
const category = CATEGORIES.find((c) => c.toLowerCase() === options.category.toLowerCase());
|
|
5090
5111
|
if (!category) {
|
|
5091
|
-
|
|
5092
|
-
|
|
5112
|
+
if (options.json) {
|
|
5113
|
+
console.log(JSON.stringify({ error: `Unknown category: ${options.category}`, available: [...CATEGORIES] }));
|
|
5114
|
+
} else {
|
|
5115
|
+
console.log(chalk2.red(`Unknown category: ${options.category}`));
|
|
5116
|
+
console.log(chalk2.dim(`Available: ${CATEGORIES.join(", ")}`));
|
|
5117
|
+
}
|
|
5093
5118
|
return;
|
|
5094
5119
|
}
|
|
5095
5120
|
const hooks = getHooksByCategory(category);
|
|
5121
|
+
if (options.json) {
|
|
5122
|
+
console.log(JSON.stringify(hooks));
|
|
5123
|
+
return;
|
|
5124
|
+
}
|
|
5096
5125
|
console.log(chalk2.bold(`
|
|
5097
5126
|
${category} (${hooks.length}):
|
|
5098
5127
|
`));
|
|
@@ -5101,6 +5130,14 @@ ${category} (${hooks.length}):
|
|
|
5101
5130
|
}
|
|
5102
5131
|
return;
|
|
5103
5132
|
}
|
|
5133
|
+
if (options.json) {
|
|
5134
|
+
const result = {};
|
|
5135
|
+
for (const category of CATEGORIES) {
|
|
5136
|
+
result[category] = getHooksByCategory(category);
|
|
5137
|
+
}
|
|
5138
|
+
console.log(JSON.stringify(result));
|
|
5139
|
+
return;
|
|
5140
|
+
}
|
|
5104
5141
|
console.log(chalk2.bold(`
|
|
5105
5142
|
Available hooks (${HOOKS.length}):
|
|
5106
5143
|
`));
|
|
@@ -5113,8 +5150,12 @@ Available hooks (${HOOKS.length}):
|
|
|
5113
5150
|
console.log();
|
|
5114
5151
|
}
|
|
5115
5152
|
});
|
|
5116
|
-
program2.command("search").argument("<query>", "Search term").description("Search for hooks").action((query) => {
|
|
5153
|
+
program2.command("search").argument("<query>", "Search term").option("-j, --json", "Output as JSON", false).description("Search for hooks").action((query, options) => {
|
|
5117
5154
|
const results = searchHooks(query);
|
|
5155
|
+
if (options.json) {
|
|
5156
|
+
console.log(JSON.stringify(results));
|
|
5157
|
+
return;
|
|
5158
|
+
}
|
|
5118
5159
|
if (results.length === 0) {
|
|
5119
5160
|
console.log(chalk2.dim(`No hooks found for "${query}"`));
|
|
5120
5161
|
return;
|
|
@@ -5127,15 +5168,28 @@ Found ${results.length} hook(s):
|
|
|
5127
5168
|
console.log(` ${h.description}`);
|
|
5128
5169
|
}
|
|
5129
5170
|
});
|
|
5130
|
-
program2.command("remove").alias("rm").argument("<hook>", "Hook to remove").description("Remove an installed hook").action((hook) => {
|
|
5131
|
-
const
|
|
5171
|
+
program2.command("remove").alias("rm").argument("<hook>", "Hook to remove").option("-g, --global", "Remove from global settings", false).option("-p, --project", "Remove from project settings", false).option("-j, --json", "Output as JSON", false).description("Remove an installed hook").action((hook, options) => {
|
|
5172
|
+
const scope = resolveScope(options);
|
|
5173
|
+
const removed = removeHook(hook, scope);
|
|
5174
|
+
if (options.json) {
|
|
5175
|
+
console.log(JSON.stringify({ hook, removed, scope }));
|
|
5176
|
+
return;
|
|
5177
|
+
}
|
|
5132
5178
|
if (removed) {
|
|
5133
|
-
console.log(chalk2.green(`\u2713 Removed ${hook} (
|
|
5179
|
+
console.log(chalk2.green(`\u2713 Removed ${hook} (${scope})`));
|
|
5134
5180
|
} else {
|
|
5135
|
-
console.log(chalk2.red(`\u2717 ${hook} is not installed`));
|
|
5181
|
+
console.log(chalk2.red(`\u2717 ${hook} is not installed (${scope})`));
|
|
5136
5182
|
}
|
|
5137
5183
|
});
|
|
5138
|
-
program2.command("categories").description("List all categories").action(() => {
|
|
5184
|
+
program2.command("categories").option("-j, --json", "Output as JSON", false).description("List all categories").action((options) => {
|
|
5185
|
+
if (options.json) {
|
|
5186
|
+
const result = CATEGORIES.map((cat) => ({
|
|
5187
|
+
name: cat,
|
|
5188
|
+
count: getHooksByCategory(cat).length
|
|
5189
|
+
}));
|
|
5190
|
+
console.log(JSON.stringify(result));
|
|
5191
|
+
return;
|
|
5192
|
+
}
|
|
5139
5193
|
console.log(chalk2.bold(`
|
|
5140
5194
|
Categories:
|
|
5141
5195
|
`));
|
|
@@ -5144,10 +5198,20 @@ Categories:
|
|
|
5144
5198
|
console.log(` ${category} (${count})`);
|
|
5145
5199
|
}
|
|
5146
5200
|
});
|
|
5147
|
-
program2.command("info").argument("<hook>", "Hook name").description("Show detailed info about a hook").action((hook) => {
|
|
5201
|
+
program2.command("info").argument("<hook>", "Hook name").option("-j, --json", "Output as JSON", false).description("Show detailed info about a hook").action((hook, options) => {
|
|
5148
5202
|
const meta = getHook(hook);
|
|
5149
5203
|
if (!meta) {
|
|
5150
|
-
|
|
5204
|
+
if (options.json) {
|
|
5205
|
+
console.log(JSON.stringify({ error: `Hook '${hook}' not found` }));
|
|
5206
|
+
} else {
|
|
5207
|
+
console.log(chalk2.red(`Hook '${hook}' not found`));
|
|
5208
|
+
}
|
|
5209
|
+
return;
|
|
5210
|
+
}
|
|
5211
|
+
const globalInstalled = getRegisteredHooks("global").includes(meta.name);
|
|
5212
|
+
const projectInstalled = getRegisteredHooks("project").includes(meta.name);
|
|
5213
|
+
if (options.json) {
|
|
5214
|
+
console.log(JSON.stringify({ ...meta, global: globalInstalled, project: projectInstalled }));
|
|
5151
5215
|
return;
|
|
5152
5216
|
}
|
|
5153
5217
|
console.log(chalk2.bold(`
|
|
@@ -5156,16 +5220,248 @@ ${meta.displayName}
|
|
|
5156
5220
|
console.log(` ${meta.description}`);
|
|
5157
5221
|
console.log();
|
|
5158
5222
|
console.log(` ${chalk2.dim("Category:")} ${meta.category}`);
|
|
5223
|
+
console.log(` ${chalk2.dim("Version:")} ${meta.version}`);
|
|
5159
5224
|
console.log(` ${chalk2.dim("Event:")} ${meta.event}`);
|
|
5160
5225
|
console.log(` ${chalk2.dim("Matcher:")} ${meta.matcher || "(none)"}`);
|
|
5161
5226
|
console.log(` ${chalk2.dim("Tags:")} ${meta.tags.join(", ")}`);
|
|
5162
|
-
console.log(` ${chalk2.dim("
|
|
5227
|
+
console.log(` ${chalk2.dim("Command:")} hooks run ${meta.name}`);
|
|
5163
5228
|
console.log();
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5229
|
+
if (globalInstalled) {
|
|
5230
|
+
console.log(chalk2.green(" \u25CF Installed globally"));
|
|
5231
|
+
} else {
|
|
5232
|
+
console.log(chalk2.dim(" \u25CB Not installed globally"));
|
|
5233
|
+
}
|
|
5234
|
+
if (projectInstalled) {
|
|
5235
|
+
console.log(chalk2.green(" \u25CF Installed in project"));
|
|
5167
5236
|
} else {
|
|
5168
|
-
console.log(chalk2.dim(" \u25CB Not
|
|
5237
|
+
console.log(chalk2.dim(" \u25CB Not installed in project"));
|
|
5169
5238
|
}
|
|
5170
5239
|
});
|
|
5240
|
+
program2.command("doctor").option("-g, --global", "Check global settings", false).option("-p, --project", "Check project settings", false).option("-j, --json", "Output as JSON", false).description("Check health of installed hooks").action((options) => {
|
|
5241
|
+
const scope = resolveScope(options);
|
|
5242
|
+
const settingsPath = getSettingsPath(scope);
|
|
5243
|
+
const issues = [];
|
|
5244
|
+
const healthy = [];
|
|
5245
|
+
const settingsExist = existsSync2(settingsPath);
|
|
5246
|
+
if (!settingsExist) {
|
|
5247
|
+
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
5248
|
+
}
|
|
5249
|
+
const registered = getRegisteredHooks(scope);
|
|
5250
|
+
for (const name of registered) {
|
|
5251
|
+
const meta = getHook(name);
|
|
5252
|
+
let hookHealthy = true;
|
|
5253
|
+
if (!hookExists(name)) {
|
|
5254
|
+
issues.push({ hook: name, issue: "Hook not found in @hasna/hooks package", severity: "error" });
|
|
5255
|
+
hookHealthy = false;
|
|
5256
|
+
continue;
|
|
5257
|
+
}
|
|
5258
|
+
const hookDir = getHookPath(name);
|
|
5259
|
+
const hookScript = join2(hookDir, "src", "hook.ts");
|
|
5260
|
+
if (!existsSync2(hookScript)) {
|
|
5261
|
+
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
5262
|
+
hookHealthy = false;
|
|
5263
|
+
}
|
|
5264
|
+
if (meta && settingsExist) {
|
|
5265
|
+
try {
|
|
5266
|
+
const settings = JSON.parse(readFileSync2(settingsPath, "utf-8"));
|
|
5267
|
+
const eventHooks = settings.hooks?.[meta.event] || [];
|
|
5268
|
+
const found = eventHooks.some((entry) => entry.hooks?.some((h) => h.command === `hooks run ${name}`));
|
|
5269
|
+
if (!found) {
|
|
5270
|
+
issues.push({ hook: name, issue: `Not registered under correct event (${meta.event})`, severity: "error" });
|
|
5271
|
+
hookHealthy = false;
|
|
5272
|
+
}
|
|
5273
|
+
} catch {}
|
|
5274
|
+
}
|
|
5275
|
+
if (hookHealthy) {
|
|
5276
|
+
healthy.push(name);
|
|
5277
|
+
}
|
|
5278
|
+
}
|
|
5279
|
+
if (options.json) {
|
|
5280
|
+
console.log(JSON.stringify({ healthy, issues, registered, scope }));
|
|
5281
|
+
return;
|
|
5282
|
+
}
|
|
5283
|
+
console.log(chalk2.bold(`
|
|
5284
|
+
Hook Health Check (${scope})
|
|
5285
|
+
`));
|
|
5286
|
+
if (registered.length === 0) {
|
|
5287
|
+
console.log(chalk2.dim(" No hooks registered."));
|
|
5288
|
+
console.log(chalk2.dim(" Run: hooks install gitguard"));
|
|
5289
|
+
return;
|
|
5290
|
+
}
|
|
5291
|
+
if (healthy.length > 0) {
|
|
5292
|
+
console.log(chalk2.green(` \u2713 ${healthy.length} hook(s) healthy:`));
|
|
5293
|
+
for (const name of healthy) {
|
|
5294
|
+
console.log(chalk2.green(` ${name}`));
|
|
5295
|
+
}
|
|
5296
|
+
}
|
|
5297
|
+
if (issues.length > 0) {
|
|
5298
|
+
console.log();
|
|
5299
|
+
for (const issue of issues) {
|
|
5300
|
+
const icon = issue.severity === "error" ? chalk2.red("\u2717") : chalk2.yellow("!");
|
|
5301
|
+
console.log(` ${icon} ${chalk2.cyan(issue.hook)}: ${issue.issue}`);
|
|
5302
|
+
}
|
|
5303
|
+
}
|
|
5304
|
+
if (issues.length === 0) {
|
|
5305
|
+
console.log(chalk2.green(`
|
|
5306
|
+
All hooks healthy!`));
|
|
5307
|
+
}
|
|
5308
|
+
console.log();
|
|
5309
|
+
});
|
|
5310
|
+
program2.command("update").argument("[hooks...]", "Hooks to update (defaults to all installed)").option("-g, --global", "Update global hooks", false).option("-p, --project", "Update project hooks", false).option("-j, --json", "Output as JSON", false).description("Re-register hooks (picks up new package version)").action((hooks, options) => {
|
|
5311
|
+
const scope = resolveScope(options);
|
|
5312
|
+
const installed = getInstalledHooks(scope);
|
|
5313
|
+
const toUpdate = hooks.length > 0 ? hooks : installed;
|
|
5314
|
+
if (toUpdate.length === 0) {
|
|
5315
|
+
if (options.json) {
|
|
5316
|
+
console.log(JSON.stringify({ updated: [], error: "No hooks installed" }));
|
|
5317
|
+
} else {
|
|
5318
|
+
console.log(chalk2.dim("No hooks installed to update."));
|
|
5319
|
+
}
|
|
5320
|
+
return;
|
|
5321
|
+
}
|
|
5322
|
+
const results = [];
|
|
5323
|
+
for (const name of toUpdate) {
|
|
5324
|
+
if (!installed.includes(name)) {
|
|
5325
|
+
results.push({ hook: name, success: false, error: "Not installed" });
|
|
5326
|
+
continue;
|
|
5327
|
+
}
|
|
5328
|
+
const result = installHook(name, { scope, overwrite: true });
|
|
5329
|
+
results.push(result);
|
|
5330
|
+
}
|
|
5331
|
+
if (options.json) {
|
|
5332
|
+
console.log(JSON.stringify({
|
|
5333
|
+
updated: results.filter((r) => r.success).map((r) => r.hook),
|
|
5334
|
+
failed: results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error }))
|
|
5335
|
+
}));
|
|
5336
|
+
return;
|
|
5337
|
+
}
|
|
5338
|
+
console.log(chalk2.bold(`
|
|
5339
|
+
Updating hooks...
|
|
5340
|
+
`));
|
|
5341
|
+
for (const result of results) {
|
|
5342
|
+
if (result.success) {
|
|
5343
|
+
console.log(chalk2.green(`\u2713 ${result.hook} updated`));
|
|
5344
|
+
} else {
|
|
5345
|
+
console.log(chalk2.red(`\u2717 ${result.hook}: ${result.error}`));
|
|
5346
|
+
}
|
|
5347
|
+
}
|
|
5348
|
+
});
|
|
5349
|
+
program2.command("docs").argument("[hook]", "Hook name (shows general docs if omitted)").option("-j, --json", "Output as JSON", false).description("Show documentation for hooks").action((hook, options) => {
|
|
5350
|
+
if (hook) {
|
|
5351
|
+
const meta = getHook(hook);
|
|
5352
|
+
if (!meta) {
|
|
5353
|
+
if (options.json) {
|
|
5354
|
+
console.log(JSON.stringify({ error: `Hook '${hook}' not found` }));
|
|
5355
|
+
} else {
|
|
5356
|
+
console.log(chalk2.red(`Hook '${hook}' not found`));
|
|
5357
|
+
}
|
|
5358
|
+
return;
|
|
5359
|
+
}
|
|
5360
|
+
const hookPath = getHookPath(hook);
|
|
5361
|
+
const readmePath = join2(hookPath, "README.md");
|
|
5362
|
+
let readme = "";
|
|
5363
|
+
if (existsSync2(readmePath)) {
|
|
5364
|
+
readme = readFileSync2(readmePath, "utf-8");
|
|
5365
|
+
}
|
|
5366
|
+
if (options.json) {
|
|
5367
|
+
console.log(JSON.stringify({ ...meta, readme }));
|
|
5368
|
+
return;
|
|
5369
|
+
}
|
|
5370
|
+
console.log(chalk2.bold(`
|
|
5371
|
+
${meta.displayName} v${meta.version}
|
|
5372
|
+
`));
|
|
5373
|
+
console.log(` ${meta.description}
|
|
5374
|
+
`);
|
|
5375
|
+
console.log(chalk2.bold(" Configuration:"));
|
|
5376
|
+
console.log(` Event: ${meta.event}`);
|
|
5377
|
+
console.log(` Matcher: ${meta.matcher || "(all tools)"}`);
|
|
5378
|
+
console.log(` Command: hooks run ${meta.name}`);
|
|
5379
|
+
console.log();
|
|
5380
|
+
console.log(chalk2.bold(" Install:"));
|
|
5381
|
+
console.log(` hooks install ${meta.name} # global`);
|
|
5382
|
+
console.log(` hooks install ${meta.name} --project # project only`);
|
|
5383
|
+
console.log();
|
|
5384
|
+
if (readme) {
|
|
5385
|
+
console.log(chalk2.bold(` README:
|
|
5386
|
+
`));
|
|
5387
|
+
for (const line of readme.split(`
|
|
5388
|
+
`)) {
|
|
5389
|
+
console.log(` ${line}`);
|
|
5390
|
+
}
|
|
5391
|
+
}
|
|
5392
|
+
return;
|
|
5393
|
+
}
|
|
5394
|
+
const generalDocs = {
|
|
5395
|
+
overview: "Claude Code hooks are scripts that run at specific points in a Claude Code session. Install @hasna/hooks globally, then register hooks \u2014 no files are copied to your project.",
|
|
5396
|
+
events: {
|
|
5397
|
+
PreToolUse: 'Fires before a tool executes. Can block the operation by returning { "decision": "block" }.',
|
|
5398
|
+
PostToolUse: "Fires after a tool executes. Runs asynchronously, cannot block.",
|
|
5399
|
+
Stop: "Fires when a Claude Code session ends. Useful for notifications and cleanup.",
|
|
5400
|
+
Notification: "Fires on notification events like context compaction."
|
|
5401
|
+
},
|
|
5402
|
+
installation: {
|
|
5403
|
+
global: "hooks install gitguard",
|
|
5404
|
+
project: "hooks install gitguard --project",
|
|
5405
|
+
category: 'hooks install --category "Git Safety"',
|
|
5406
|
+
all: "hooks install --all"
|
|
5407
|
+
},
|
|
5408
|
+
management: {
|
|
5409
|
+
list: "hooks list",
|
|
5410
|
+
listInstalled: "hooks list --installed",
|
|
5411
|
+
search: "hooks search <query>",
|
|
5412
|
+
info: "hooks info <name>",
|
|
5413
|
+
remove: "hooks remove <name>",
|
|
5414
|
+
update: "hooks update",
|
|
5415
|
+
doctor: "hooks doctor",
|
|
5416
|
+
docs: "hooks docs <name>"
|
|
5417
|
+
},
|
|
5418
|
+
howItWorks: {
|
|
5419
|
+
install: "bun install -g @hasna/hooks",
|
|
5420
|
+
register: "hooks install gitguard \u2192 writes to ~/.claude/settings.json",
|
|
5421
|
+
execution: "Claude Code runs 'hooks run gitguard' \u2192 executes hook from global package",
|
|
5422
|
+
noFileCopy: "No files are copied to your project. Hooks run from the global @hasna/hooks package."
|
|
5423
|
+
}
|
|
5424
|
+
};
|
|
5425
|
+
if (options.json) {
|
|
5426
|
+
console.log(JSON.stringify(generalDocs));
|
|
5427
|
+
return;
|
|
5428
|
+
}
|
|
5429
|
+
console.log(chalk2.bold(`
|
|
5430
|
+
@hasna/hooks Documentation
|
|
5431
|
+
`));
|
|
5432
|
+
console.log(chalk2.bold(` Overview
|
|
5433
|
+
`));
|
|
5434
|
+
console.log(` ${generalDocs.overview}
|
|
5435
|
+
`);
|
|
5436
|
+
console.log(chalk2.bold(` How It Works
|
|
5437
|
+
`));
|
|
5438
|
+
for (const [label, desc] of Object.entries(generalDocs.howItWorks)) {
|
|
5439
|
+
console.log(` ${chalk2.dim(label + ":")} ${desc}`);
|
|
5440
|
+
}
|
|
5441
|
+
console.log(chalk2.bold(`
|
|
5442
|
+
Hook Events
|
|
5443
|
+
`));
|
|
5444
|
+
for (const [event, desc] of Object.entries(generalDocs.events)) {
|
|
5445
|
+
console.log(` ${chalk2.cyan(event)}`);
|
|
5446
|
+
console.log(` ${desc}
|
|
5447
|
+
`);
|
|
5448
|
+
}
|
|
5449
|
+
console.log(chalk2.bold(` Installation
|
|
5450
|
+
`));
|
|
5451
|
+
for (const [label, cmd] of Object.entries(generalDocs.installation)) {
|
|
5452
|
+
console.log(` ${chalk2.dim(label + ":")} ${cmd}`);
|
|
5453
|
+
}
|
|
5454
|
+
console.log(chalk2.bold(`
|
|
5455
|
+
Management
|
|
5456
|
+
`));
|
|
5457
|
+
for (const [label, cmd] of Object.entries(generalDocs.management)) {
|
|
5458
|
+
console.log(` ${chalk2.dim(label + ":")} ${cmd}`);
|
|
5459
|
+
}
|
|
5460
|
+
console.log(chalk2.bold(`
|
|
5461
|
+
Hook-Specific Docs
|
|
5462
|
+
`));
|
|
5463
|
+
console.log(` hooks docs <name> View README for a specific hook`);
|
|
5464
|
+
console.log(` hooks docs --json Machine-readable documentation`);
|
|
5465
|
+
console.log();
|
|
5466
|
+
});
|
|
5171
5467
|
program2.parse();
|