@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.
Files changed (37) hide show
  1. package/bin/index.js +429 -133
  2. package/dist/index.js +46 -90
  3. package/hooks/hook-agentmessages/src/check-messages.ts +151 -0
  4. package/hooks/hook-agentmessages/src/install.ts +126 -0
  5. package/hooks/hook-agentmessages/src/session-start.ts +255 -0
  6. package/hooks/hook-agentmessages/src/uninstall.ts +89 -0
  7. package/hooks/hook-branchprotect/src/cli.ts +126 -0
  8. package/hooks/hook-branchprotect/src/hook.ts +88 -0
  9. package/hooks/hook-checkbugs/src/cli.ts +628 -0
  10. package/hooks/hook-checkbugs/src/hook.ts +335 -0
  11. package/hooks/hook-checkdocs/src/cli.ts +628 -0
  12. package/hooks/hook-checkdocs/src/hook.ts +310 -0
  13. package/hooks/hook-checkfiles/src/cli.ts +545 -0
  14. package/hooks/hook-checkfiles/src/hook.ts +321 -0
  15. package/hooks/hook-checklint/src/cli-patch.ts +32 -0
  16. package/hooks/hook-checklint/src/cli.ts +667 -0
  17. package/hooks/hook-checklint/src/hook.ts +473 -0
  18. package/hooks/hook-checkpoint/src/cli.ts +191 -0
  19. package/hooks/hook-checkpoint/src/hook.ts +207 -0
  20. package/hooks/hook-checksecurity/src/cli.ts +601 -0
  21. package/hooks/hook-checksecurity/src/hook.ts +334 -0
  22. package/hooks/hook-checktasks/src/cli.ts +578 -0
  23. package/hooks/hook-checktasks/src/hook.ts +308 -0
  24. package/hooks/hook-checktests/src/cli.ts +627 -0
  25. package/hooks/hook-checktests/src/hook.ts +334 -0
  26. package/hooks/hook-contextrefresh/src/cli.ts +152 -0
  27. package/hooks/hook-contextrefresh/src/hook.ts +148 -0
  28. package/hooks/hook-gitguard/src/cli.ts +159 -0
  29. package/hooks/hook-gitguard/src/hook.ts +129 -0
  30. package/hooks/hook-packageage/src/cli.ts +165 -0
  31. package/hooks/hook-packageage/src/hook.ts +177 -0
  32. package/hooks/hook-phonenotify/src/cli.ts +196 -0
  33. package/hooks/hook-phonenotify/src/hook.ts +139 -0
  34. package/hooks/hook-precompact/src/cli.ts +168 -0
  35. package/hooks/hook-precompact/src/hook.ts +122 -0
  36. package/package.json +2 -1
  37. 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, cpSync, mkdirSync, readFileSync, writeFileSync } from "fs";
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
- var SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
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 readSettings() {
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(SETTINGS_PATH)) {
4536
- return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
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 dir = dirname(SETTINGS_PATH);
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(SETTINGS_PATH, JSON.stringify(settings, null, 2) + `
4558
+ writeFileSync(path, JSON.stringify(settings, null, 2) + `
4547
4559
  `);
4548
4560
  }
4549
4561
  function installHook(name, options = {}) {
4550
- const { targetDir = process.cwd(), overwrite = false } = options;
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
- const sourcePath = getHookPath(name);
4554
- const destDir = join(targetDir, ".hooks");
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
- if (existsSync(destPath) && !overwrite) {
4564
- return {
4565
- hook: shortName,
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
- if (!existsSync(destDir)) {
4573
- mkdirSync(destDir, { recursive: true });
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 registerHookInSettings(name) {
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 = `hook-${name}`;
4602
- const existing = settings.hooks[eventKey].find((entry2) => entry2.hooks?.some((h) => h.command?.includes(hookCommand)));
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 unregisterHookFromSettings(name) {
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 = `hook-${name}`;
4625
- settings.hooks[eventKey] = settings.hooks[eventKey].filter((entry) => !entry.hooks?.some((h) => h.command?.includes(hookCommand)));
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 getInstalledHooks(targetDir = process.cwd()) {
4653
- const hooksDir = join(targetDir, ".hooks");
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 match = hook.command?.match(/hook-(\w+)/);
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 removeHook(name, targetDir = process.cwd()) {
4681
- const { rmSync } = __require("fs");
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 hooksDir = join(targetDir, ".hooks");
4685
- const hookPath = join(hooksDir, hookName);
4686
- if (!existsSync(hookPath)) {
4649
+ const registered = getRegisteredHooks(scope);
4650
+ if (!registered.includes(shortName)) {
4687
4651
  return false;
4688
4652
  }
4689
- rmSync(hookPath, { recursive: true });
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
- program2.name("hooks").description("Install Claude Code hooks for your project").version("0.0.1");
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("install").alias("add").argument("[hooks...]", "Hooks to install").option("-o, --overwrite", "Overwrite existing hooks", false).description("Install one or more hooks").action((hooks, options) => {
5036
- if (hooks.length === 0) {
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 name of hooks) {
5044
- const result = installHook(name, { overwrite: options.overwrite });
5071
+ for (const result of results) {
5045
5072
  if (result.success) {
5046
- const meta = getHook(name);
5047
- console.log(chalk2.green(`\u2713 ${name}`));
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-${name}`));
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 ${name}: ${result.error}`));
5079
+ console.log(chalk2.red(`\u2717 ${result.hook}: ${result.error}`));
5053
5080
  }
5054
5081
  }
5055
5082
  console.log(chalk2.dim(`
5056
- Hooks installed to .hooks/ and registered in ~/.claude/settings.json`));
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
- if (options.registered) {
5060
- const registered = getRegisteredHooks();
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("No hooks registered in Claude settings"));
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
- console.log(chalk2.red(`Unknown category: ${options.category}`));
5092
- console.log(chalk2.dim(`Available: ${CATEGORIES.join(", ")}`));
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 removed = removeHook(hook);
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} (unregistered from Claude settings)`));
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
- console.log(chalk2.red(`Hook '${hook}' not found`));
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("Package:")} hook-${meta.name}`);
5227
+ console.log(` ${chalk2.dim("Command:")} hooks run ${meta.name}`);
5163
5228
  console.log();
5164
- const registered = getRegisteredHooks();
5165
- if (registered.includes(meta.name)) {
5166
- console.log(chalk2.green(" \u25CF Registered in Claude settings"));
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 registered"));
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();