claude-code-hookkit 1.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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +311 -0
  3. package/dist/add-O4OSFQ76.js +140 -0
  4. package/dist/chunk-2BZZUQQ3.js +34 -0
  5. package/dist/chunk-LRXKKJDU.js +101 -0
  6. package/dist/chunk-PEDGREZY.js +46 -0
  7. package/dist/chunk-QKT647BI.js +30 -0
  8. package/dist/chunk-XLX5K6TZ.js +113 -0
  9. package/dist/cli.js +76 -0
  10. package/dist/create-DBLA6PTS.js +268 -0
  11. package/dist/doctor-UBK2C2TW.js +137 -0
  12. package/dist/info-FLYMAHDX.js +84 -0
  13. package/dist/init-RHEFGGUF.js +70 -0
  14. package/dist/list-SCSGYOBR.js +54 -0
  15. package/dist/remove-Z5QIW45P.js +109 -0
  16. package/dist/restore-7JQ3CHWZ.js +31 -0
  17. package/dist/test-ZRRLZ62R.js +194 -0
  18. package/package.json +59 -0
  19. package/registry/hooks/cost-tracker.sh +44 -0
  20. package/registry/hooks/error-advisor.sh +114 -0
  21. package/registry/hooks/exit-code-enforcer.sh +76 -0
  22. package/registry/hooks/fixtures/cost-tracker/allow-bash-tool.json +5 -0
  23. package/registry/hooks/fixtures/cost-tracker/allow-no-session.json +5 -0
  24. package/registry/hooks/fixtures/error-advisor/allow-enoent.json +5 -0
  25. package/registry/hooks/fixtures/error-advisor/allow-no-error.json +5 -0
  26. package/registry/hooks/fixtures/exit-code-enforcer/allow-npm-test.json +5 -0
  27. package/registry/hooks/fixtures/exit-code-enforcer/block-rm-rf.json +5 -0
  28. package/registry/hooks/fixtures/post-edit-lint/allow-ts-file.json +5 -0
  29. package/registry/hooks/fixtures/post-edit-lint/allow-unknown-ext.json +5 -0
  30. package/registry/hooks/fixtures/sensitive-path-guard/allow-src.json +5 -0
  31. package/registry/hooks/fixtures/sensitive-path-guard/block-env.json +5 -0
  32. package/registry/hooks/fixtures/ts-check/allow-non-ts.json +5 -0
  33. package/registry/hooks/fixtures/ts-check/allow-ts-file.json +5 -0
  34. package/registry/hooks/fixtures/web-budget-gate/allow-within-budget.json +6 -0
  35. package/registry/hooks/fixtures/web-budget-gate/block-over-budget.json +6 -0
  36. package/registry/hooks/post-edit-lint.sh +82 -0
  37. package/registry/hooks/sensitive-path-guard.sh +103 -0
  38. package/registry/hooks/ts-check.sh +98 -0
  39. package/registry/hooks/web-budget-gate.sh +60 -0
  40. package/registry/registry.json +81 -0
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/registry/index.ts
4
+ import { createRequire } from "module";
5
+ import { fileURLToPath } from "url";
6
+ import { dirname, join } from "path";
7
+
8
+ // src/registry/types.ts
9
+ import { z as z2 } from "zod";
10
+
11
+ // src/types/hooks.ts
12
+ import { z } from "zod";
13
+ var hookEventSchema = z.enum([
14
+ "PreToolUse",
15
+ "PostToolUse",
16
+ "PostToolUseFailure",
17
+ "PermissionRequest",
18
+ "SessionStart",
19
+ "Stop",
20
+ "StopFailure",
21
+ "SessionEnd",
22
+ "Notification",
23
+ "SubagentStart",
24
+ "SubagentStop",
25
+ "UserPromptSubmit",
26
+ "ConfigChange",
27
+ "PreCompact",
28
+ "PostCompact",
29
+ "WorktreeCreate",
30
+ "WorktreeRemove",
31
+ "TeammateIdle",
32
+ "TaskCompleted",
33
+ "InstructionsLoaded",
34
+ "Elicitation",
35
+ "ElicitationResult"
36
+ ]);
37
+ var hookEntrySchema = z.object({
38
+ type: z.literal("command"),
39
+ command: z.string().min(1)
40
+ });
41
+ var hookGroupSchema = z.object({
42
+ matcher: z.string().optional(),
43
+ hooks: z.array(hookEntrySchema)
44
+ });
45
+ var claudeSettingsSchema = z.object({
46
+ env: z.record(z.string()).optional(),
47
+ hooks: z.record(z.array(hookGroupSchema)).optional(),
48
+ mcpServers: z.record(z.unknown()).optional(),
49
+ enabledPlugins: z.record(z.boolean()).optional(),
50
+ statusLine: z.unknown().optional()
51
+ }).passthrough();
52
+
53
+ // src/registry/types.ts
54
+ var hookDefinitionSchema = z2.object({
55
+ name: z2.string().min(1),
56
+ description: z2.string().min(1),
57
+ event: hookEventSchema,
58
+ matcher: z2.string().optional(),
59
+ pack: z2.string().optional(),
60
+ scriptFile: z2.string().min(1)
61
+ });
62
+ var hookPackSchema = z2.object({
63
+ name: z2.string().min(1),
64
+ description: z2.string().min(1),
65
+ hooks: z2.array(z2.string())
66
+ });
67
+ var registrySchema = z2.object({
68
+ hooks: z2.record(hookDefinitionSchema),
69
+ packs: z2.record(hookPackSchema)
70
+ });
71
+
72
+ // src/registry/index.ts
73
+ import { existsSync } from "fs";
74
+ var _require = createRequire(import.meta.url);
75
+ var __filename = fileURLToPath(import.meta.url);
76
+ var __dirname = dirname(__filename);
77
+ function findRegistryPath() {
78
+ let dir = __dirname;
79
+ for (let i = 0; i < 4; i++) {
80
+ const candidate = join(dir, "registry", "registry.json");
81
+ if (existsSync(candidate)) return candidate;
82
+ const parent = join(dir, "..");
83
+ if (parent === dir) break;
84
+ dir = parent;
85
+ }
86
+ throw new Error(`registry/registry.json not found searching up from: ${__dirname}`);
87
+ }
88
+ var _cache = null;
89
+ function loadRegistry() {
90
+ if (_cache !== null) {
91
+ return _cache;
92
+ }
93
+ const registryPath = findRegistryPath();
94
+ const raw = _require(registryPath);
95
+ const parsed = registrySchema.parse(raw);
96
+ _cache = parsed;
97
+ return _cache;
98
+ }
99
+ function getHook(name) {
100
+ return loadRegistry().hooks[name];
101
+ }
102
+ function getPack(name) {
103
+ return loadRegistry().packs[name];
104
+ }
105
+ function listHooks() {
106
+ return Object.values(loadRegistry().hooks);
107
+ }
108
+
109
+ export {
110
+ getHook,
111
+ getPack,
112
+ listHooks
113
+ };
package/dist/cli.js ADDED
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+ var program = new Command().name("claude-code-hookkit").description("Hook manager for Claude Code \u2014 install, manage, test, and share hooks").version("1.0.0");
6
+ program.command("init").description("Initialize claude-code-hookkit in your project").option("--scope <scope>", "Settings scope: user, project, or local", "project").option("--dry-run", "Preview changes without writing").addHelpText("after", `
7
+ Examples:
8
+ $ claude-code-hookkit init
9
+ $ claude-code-hookkit init --scope user
10
+ $ claude-code-hookkit init --dry-run`).action(async (opts) => {
11
+ const { initCommand } = await import("./init-RHEFGGUF.js");
12
+ await initCommand(opts);
13
+ });
14
+ program.command("restore").description("Restore settings.json from the last backup").option("--scope <scope>", "Settings scope: user, project, or local", "project").addHelpText("after", `
15
+ Examples:
16
+ $ claude-code-hookkit restore
17
+ $ claude-code-hookkit restore --scope user`).action(async (opts) => {
18
+ const { restoreCommand } = await import("./restore-7JQ3CHWZ.js");
19
+ await restoreCommand(opts);
20
+ });
21
+ program.command("add <name>").description("Install a hook from the registry into your project").option("--scope <scope>", "Settings scope: user, project, or local", "project").option("--dry-run", "Preview changes without writing").addHelpText("after", `
22
+ Examples:
23
+ $ claude-code-hookkit add sensitive-path-guard
24
+ $ claude-code-hookkit add security-pack
25
+ $ claude-code-hookkit add cost-tracker --scope user
26
+ $ claude-code-hookkit add post-edit-lint --dry-run`).action(async (name, opts) => {
27
+ const { addCommand } = await import("./add-O4OSFQ76.js");
28
+ await addCommand({ ...opts, hookName: name });
29
+ });
30
+ program.command("remove <name>").description("Remove an installed hook from your project").option("--scope <scope>", "Settings scope: user, project, or local", "project").option("--dry-run", "Preview changes without writing").addHelpText("after", `
31
+ Examples:
32
+ $ claude-code-hookkit remove sensitive-path-guard
33
+ $ claude-code-hookkit remove post-edit-lint --scope user
34
+ $ claude-code-hookkit remove web-budget-gate --dry-run`).action(async (name, opts) => {
35
+ const { removeCommand } = await import("./remove-Z5QIW45P.js");
36
+ await removeCommand({ ...opts, hookName: name });
37
+ });
38
+ program.command("list").description("List all available hooks with installed status").option("--scope <scope>", "Settings scope: user, project, or local", "project").addHelpText("after", `
39
+ Examples:
40
+ $ claude-code-hookkit list
41
+ $ claude-code-hookkit list --scope user`).action(async (opts) => {
42
+ const { listCommand } = await import("./list-SCSGYOBR.js");
43
+ await listCommand(opts);
44
+ });
45
+ program.command("doctor").description("Validate hook installation health").option("--scope <scope>", "Settings scope: user, project, or local", "project").addHelpText("after", `
46
+ Examples:
47
+ $ claude-code-hookkit doctor
48
+ $ claude-code-hookkit doctor --scope user
49
+ $ claude-code-hookkit doctor --scope project`).action(async (opts) => {
50
+ const { doctorCommand } = await import("./doctor-UBK2C2TW.js");
51
+ await doctorCommand(opts);
52
+ });
53
+ program.command("test [hook]").description("Test hooks with fixture data").option("--all", "Test all installed hooks").option("--scope <scope>", "Settings scope: user, project, or local", "project").addHelpText("after", `
54
+ Examples:
55
+ $ claude-code-hookkit test sensitive-path-guard
56
+ $ claude-code-hookkit test --all`).action(async (hook, opts) => {
57
+ const { testCommand } = await import("./test-ZRRLZ62R.js");
58
+ await testCommand({ hookName: hook, ...opts });
59
+ });
60
+ program.command("create <name>").description("Scaffold a new custom hook from a template").requiredOption("--event <type>", "Hook event type (PreToolUse, PostToolUse, SessionStart, Stop)").option("--matcher <pattern>", 'Tool matcher pattern (e.g., "Bash", "Edit|Write")').option("--scope <scope>", "Settings scope: user, project, or local", "project").addHelpText("after", `
61
+ Examples:
62
+ $ claude-code-hookkit create my-guard --event PreToolUse --matcher Bash
63
+ $ claude-code-hookkit create session-logger --event SessionStart
64
+ $ claude-code-hookkit create cleanup --event Stop`).action(async (name, opts) => {
65
+ const { createCommand } = await import("./create-DBLA6PTS.js");
66
+ await createCommand({ name, ...opts });
67
+ });
68
+ program.command("info <hook>").description("Show details for a hook: description, event, matcher, pack, and examples").addHelpText("after", `
69
+ Examples:
70
+ $ claude-code-hookkit info sensitive-path-guard
71
+ $ claude-code-hookkit info web-budget-gate
72
+ $ claude-code-hookkit info error-advisor`).action(async (hook) => {
73
+ const { infoCommand } = await import("./info-FLYMAHDX.js");
74
+ await infoCommand({ hookName: hook });
75
+ });
76
+ program.parse();
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ log
4
+ } from "./chunk-2BZZUQQ3.js";
5
+ import {
6
+ getHooksDir
7
+ } from "./chunk-PEDGREZY.js";
8
+
9
+ // src/commands/create.ts
10
+ import { mkdirSync, writeFileSync, chmodSync, existsSync } from "fs";
11
+ import { join } from "path";
12
+
13
+ // src/templates/index.ts
14
+ var SUPPORTED_EVENTS = ["PreToolUse", "PostToolUse", "SessionStart", "Stop"];
15
+ function listTemplateEvents() {
16
+ return [...SUPPORTED_EVENTS];
17
+ }
18
+ function getTemplate(event, name, matcher) {
19
+ switch (event) {
20
+ case "PreToolUse":
21
+ return preToolUseTemplate(name, matcher);
22
+ case "PostToolUse":
23
+ return postToolUseTemplate(name, matcher);
24
+ case "SessionStart":
25
+ return sessionStartTemplate(name);
26
+ case "Stop":
27
+ return stopTemplate(name);
28
+ default:
29
+ return genericTemplate(name, event);
30
+ }
31
+ }
32
+ function preToolUseTemplate(name, matcher) {
33
+ const matcherLine = matcher ? `# Matcher: ${matcher}` : "# Matcher: (all tools)";
34
+ return `#!/usr/bin/env bash
35
+ # ${name} \u2014 PreToolUse hook
36
+ ${matcherLine}
37
+ # Created by claude-code-hookkit create
38
+ #
39
+ # PreToolUse hooks can block tool execution:
40
+ # exit 0 \u2192 allow the tool to run
41
+ # exit 2 \u2192 block the tool (stderr is shown to Claude as feedback)
42
+ set -euo pipefail
43
+
44
+ # Read JSON input from stdin
45
+ INPUT=$(cat)
46
+
47
+ # Extract fields using grep (pure bash, no external JSON parser required)
48
+ TOOL_NAME=$(echo "$INPUT" | grep -o '"tool_name":"[^"]*"' | head -1 | cut -d'"' -f4)
49
+ FILE_PATH=$(echo "$INPUT" | grep -o '"file_path":"[^"]*"' | head -1 | cut -d'"' -f4)
50
+ COMMAND=$(echo "$INPUT" | grep -o '"command":"[^"]*"' | head -1 | cut -d'"' -f4)
51
+
52
+ # TODO: Add your blocking logic here
53
+ # Example: block if file_path matches a sensitive pattern
54
+ # if echo "$FILE_PATH" | grep -qE '\\.env|\\.secret|credentials'; then
55
+ # echo "BLOCKED: Sensitive file access denied" >&2
56
+ # exit 2
57
+ # fi
58
+
59
+ # Allow by default
60
+ exit 0
61
+ `;
62
+ }
63
+ function postToolUseTemplate(name, matcher) {
64
+ const matcherLine = matcher ? `# Matcher: ${matcher}` : "# Matcher: (all tools)";
65
+ return `#!/usr/bin/env bash
66
+ # ${name} \u2014 PostToolUse hook
67
+ ${matcherLine}
68
+ # Created by claude-code-hookkit create
69
+ #
70
+ # PostToolUse hooks are informational \u2014 they should always exit 0.
71
+ # They run after the tool has already executed.
72
+ set -euo pipefail
73
+
74
+ # Read JSON input from stdin
75
+ INPUT=$(cat)
76
+
77
+ # Extract fields using grep (pure bash, no external JSON parser required, bash 3.2+ compatible)
78
+ TOOL_NAME=$(echo "$INPUT" | grep -o '"tool_name":"[^"]*"' | head -1 | cut -d'"' -f4)
79
+ TOOL_RESPONSE=$(echo "$INPUT" | grep -o '"tool_response":"[^"]*"' | head -1 | cut -d'"' -f4)
80
+
81
+ # TODO: Add your post-tool logic here (informational only)
82
+ # Example: log tool invocations, trigger notifications, update metrics
83
+ # echo "[$TOOL_NAME] completed" >> /tmp/tool-log.txt
84
+
85
+ # PostToolUse hooks should always exit 0
86
+ exit 0
87
+ `;
88
+ }
89
+ function sessionStartTemplate(name) {
90
+ return `#!/usr/bin/env bash
91
+ # ${name} \u2014 SessionStart hook
92
+ # Created by claude-code-hookkit create
93
+ #
94
+ # SessionStart hooks run when a new Claude Code session begins.
95
+ set -euo pipefail
96
+
97
+ # Read JSON input from stdin
98
+ INPUT=$(cat)
99
+
100
+ # Extract fields using grep (pure bash, no external JSON parser required, bash 3.2+ compatible)
101
+ SESSION_ID=$(echo "$INPUT" | grep -o '"session_id":"[^"]*"' | head -1 | cut -d'"' -f4)
102
+ CWD=$(echo "$INPUT" | grep -o '"cwd":"[^"]*"' | head -1 | cut -d'"' -f4)
103
+
104
+ # TODO: Add your session initialization logic here
105
+ # Example: log session start, set up environment, notify external services
106
+ # echo "[$SESSION_ID] Session started in $CWD" >> /tmp/session-log.txt
107
+
108
+ exit 0
109
+ `;
110
+ }
111
+ function stopTemplate(name) {
112
+ return `#!/usr/bin/env bash
113
+ # ${name} \u2014 Stop hook
114
+ # Created by claude-code-hookkit create
115
+ #
116
+ # Stop hooks run when a Claude Code session ends.
117
+ set -euo pipefail
118
+
119
+ # Read JSON input from stdin
120
+ INPUT=$(cat)
121
+
122
+ # Extract fields using grep (pure bash, no external JSON parser required, bash 3.2+ compatible)
123
+ SESSION_ID=$(echo "$INPUT" | grep -o '"session_id":"[^"]*"' | head -1 | cut -d'"' -f4)
124
+
125
+ # TODO: Add your cleanup/summary logic here
126
+ # Example: summarize session, flush logs, send notifications
127
+ # echo "[$SESSION_ID] Session ended" >> /tmp/session-log.txt
128
+
129
+ exit 0
130
+ `;
131
+ }
132
+ function genericTemplate(name, event) {
133
+ return `#!/usr/bin/env bash
134
+ # ${name} \u2014 ${event} hook
135
+ # Created by claude-code-hookkit create
136
+ set -euo pipefail
137
+
138
+ # Read JSON input from stdin
139
+ INPUT=$(cat)
140
+
141
+ # Extract session_id using grep (pure bash, no external JSON parser required, bash 3.2+ compatible)
142
+ SESSION_ID=$(echo "$INPUT" | grep -o '"session_id":"[^"]*"' | head -1 | cut -d'"' -f4)
143
+
144
+ # TODO: Add your hook logic here
145
+
146
+ exit 0
147
+ `;
148
+ }
149
+
150
+ // src/commands/create.ts
151
+ function buildFixtures(event) {
152
+ switch (event) {
153
+ case "PreToolUse":
154
+ return {
155
+ allow: {
156
+ description: "Example: allows safe input",
157
+ input: { tool_input: { command: "echo hello" } },
158
+ expectedExitCode: 0
159
+ },
160
+ block: {
161
+ description: "Example: blocks dangerous input",
162
+ input: { tool_input: { command: "REPLACE_ME" } },
163
+ expectedExitCode: 2
164
+ }
165
+ };
166
+ case "PostToolUse":
167
+ return {
168
+ allow: {
169
+ description: "Example: processes tool output",
170
+ input: { tool_name: "Bash", tool_response: "output here" },
171
+ expectedExitCode: 0
172
+ }
173
+ };
174
+ case "SessionStart":
175
+ return {
176
+ allow: {
177
+ description: "Example: session initialization",
178
+ input: { session_id: "test-session", cwd: "/tmp" },
179
+ expectedExitCode: 0
180
+ }
181
+ };
182
+ case "Stop":
183
+ return {
184
+ allow: {
185
+ description: "Example: session cleanup",
186
+ input: { session_id: "test-session" },
187
+ expectedExitCode: 0
188
+ }
189
+ };
190
+ default:
191
+ return {
192
+ allow: {
193
+ description: "Example: allow input",
194
+ input: { session_id: "test-session" },
195
+ expectedExitCode: 0
196
+ }
197
+ };
198
+ }
199
+ }
200
+ async function _createAt(opts) {
201
+ const { name, event, matcher, hooksDir } = opts;
202
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) {
203
+ return { invalidName: true };
204
+ }
205
+ const supportedEvents = listTemplateEvents();
206
+ if (!supportedEvents.includes(event)) {
207
+ return { invalidEvent: true };
208
+ }
209
+ const scriptPath = join(hooksDir, `${name}.sh`);
210
+ const fixturesDir = join(hooksDir, "fixtures", name);
211
+ if (existsSync(scriptPath)) {
212
+ return { skipped: true };
213
+ }
214
+ mkdirSync(hooksDir, { recursive: true });
215
+ mkdirSync(fixturesDir, { recursive: true });
216
+ const template = getTemplate(event, name, matcher);
217
+ writeFileSync(scriptPath, template, "utf8");
218
+ chmodSync(scriptPath, 493);
219
+ const fixtures = buildFixtures(event);
220
+ writeFileSync(
221
+ join(fixturesDir, "allow-example.json"),
222
+ JSON.stringify(fixtures.allow, null, 2) + "\n",
223
+ "utf8"
224
+ );
225
+ if (fixtures.block) {
226
+ writeFileSync(
227
+ join(fixturesDir, "block-example.json"),
228
+ JSON.stringify(fixtures.block, null, 2) + "\n",
229
+ "utf8"
230
+ );
231
+ }
232
+ return { created: true, scriptPath, fixturesDir };
233
+ }
234
+ async function createCommand(opts) {
235
+ const { name, event, matcher, scope = "project" } = opts;
236
+ const hooksDir = getHooksDir(scope);
237
+ const result = await _createAt({ name, event, matcher, hooksDir });
238
+ if (result.invalidName) {
239
+ log.error(
240
+ `Invalid hook name: "${name}". Names must be lowercase alphanumeric with hyphens (e.g., my-guard).`
241
+ );
242
+ return;
243
+ }
244
+ if (result.invalidEvent) {
245
+ const supported = listTemplateEvents().join(", ");
246
+ log.error(`Unsupported event type: "${event}". Supported types: ${supported}`);
247
+ return;
248
+ }
249
+ if (result.skipped) {
250
+ log.warn(`Hook already exists: ${join(hooksDir, name + ".sh")}. Remove it first to recreate.`);
251
+ return;
252
+ }
253
+ if (result.created) {
254
+ log.success(`Created hook: ${result.scriptPath}`);
255
+ log.dim(` Fixtures: ${result.fixturesDir}/allow-example.json`);
256
+ if (event === "PreToolUse") {
257
+ log.dim(` Fixtures: ${result.fixturesDir}/block-example.json`);
258
+ }
259
+ log.info("");
260
+ log.info("Next steps:");
261
+ log.dim(` 1. Edit ${result.scriptPath} to add your logic`);
262
+ log.dim(` 2. Run: claude-code-hookkit test ${name}`);
263
+ }
264
+ }
265
+ export {
266
+ _createAt,
267
+ createCommand
268
+ };
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ readSettings
4
+ } from "./chunk-LRXKKJDU.js";
5
+ import "./chunk-QKT647BI.js";
6
+ import {
7
+ getHooksDir,
8
+ getSettingsPath
9
+ } from "./chunk-PEDGREZY.js";
10
+
11
+ // src/commands/doctor.ts
12
+ import { existsSync, accessSync, constants } from "fs";
13
+ import pc from "picocolors";
14
+ function extractCommands(hooks) {
15
+ const result = [];
16
+ for (const [event, groups] of Object.entries(hooks)) {
17
+ if (!Array.isArray(groups)) continue;
18
+ for (const group of groups) {
19
+ if (!group.hooks || !Array.isArray(group.hooks)) continue;
20
+ for (const entry of group.hooks) {
21
+ if (entry.type === "command" && entry.command) {
22
+ result.push({ event, matcher: group.matcher, command: entry.command });
23
+ }
24
+ }
25
+ }
26
+ }
27
+ return result;
28
+ }
29
+ async function _doctorAt(opts) {
30
+ const { settingsPath, hooksDir } = opts;
31
+ const checks = [];
32
+ let settings;
33
+ if (!existsSync(settingsPath)) {
34
+ checks.push({ level: "warn", message: `Settings file not found: ${settingsPath}` });
35
+ } else {
36
+ try {
37
+ settings = await readSettings(settingsPath);
38
+ checks.push({ level: "pass", message: "Settings file valid" });
39
+ } catch {
40
+ checks.push({ level: "fail", message: `Settings file parse error: malformed JSON at ${settingsPath}` });
41
+ }
42
+ }
43
+ if (!existsSync(hooksDir)) {
44
+ checks.push({ level: "fail", message: `Hooks dir not found: ${hooksDir}` });
45
+ } else {
46
+ checks.push({ level: "pass", message: "Hooks directory exists" });
47
+ }
48
+ if (settings !== void 0) {
49
+ const hookDefs = settings.hooks;
50
+ const allEntries = hookDefs ? extractCommands(hookDefs) : [];
51
+ if (allEntries.length === 0) {
52
+ checks.push({ level: "pass", message: "No hook scripts to validate" });
53
+ } else {
54
+ for (const { command: scriptPath } of allEntries) {
55
+ if (!existsSync(scriptPath)) {
56
+ checks.push({ level: "fail", message: `Script not found: ${scriptPath}` });
57
+ } else {
58
+ try {
59
+ accessSync(scriptPath, constants.X_OK);
60
+ checks.push({ level: "pass", message: `Script OK: ${scriptPath}` });
61
+ } catch {
62
+ checks.push({ level: "fail", message: `Script not executable: ${scriptPath}` });
63
+ }
64
+ }
65
+ }
66
+ }
67
+ if (hookDefs) {
68
+ const tupleMap = /* @__PURE__ */ new Map();
69
+ for (const { event, matcher, command } of allEntries) {
70
+ const key = `${event}::${matcher ?? "__none__"}`;
71
+ if (!tupleMap.has(key)) {
72
+ tupleMap.set(key, /* @__PURE__ */ new Set());
73
+ }
74
+ tupleMap.get(key).add(command);
75
+ }
76
+ let conflictFound = false;
77
+ for (const [key, commands] of tupleMap.entries()) {
78
+ if (commands.size > 1) {
79
+ const parts = key.split("::");
80
+ const event = parts[0];
81
+ const matcher = parts[1];
82
+ const label = matcher === "__none__" ? event : `${event}/${matcher}`;
83
+ checks.push({
84
+ level: "warn",
85
+ message: `Conflicting hooks: ${label} has ${commands.size} commands`
86
+ });
87
+ conflictFound = true;
88
+ }
89
+ }
90
+ if (!conflictFound) {
91
+ checks.push({ level: "pass", message: "No conflicting hooks" });
92
+ }
93
+ } else {
94
+ checks.push({ level: "pass", message: "No conflicting hooks" });
95
+ }
96
+ }
97
+ const passed = checks.filter((c) => c.level === "pass").length;
98
+ const failed = checks.filter((c) => c.level === "fail").length;
99
+ const warnings = checks.filter((c) => c.level === "warn").length;
100
+ const exitCode = failed > 0 ? 1 : 0;
101
+ return { checks, passed, failed, warnings, exitCode };
102
+ }
103
+ function printResults(result) {
104
+ console.log("");
105
+ for (const check of result.checks) {
106
+ if (check.level === "pass") {
107
+ console.log(pc.green("[PASS]") + " " + check.message);
108
+ } else if (check.level === "fail") {
109
+ console.log(pc.red("[FAIL]") + " " + check.message);
110
+ } else {
111
+ console.log(pc.yellow("[WARN]") + " " + check.message);
112
+ }
113
+ }
114
+ console.log("");
115
+ const summary = `${result.passed} passed, ${result.failed} failed, ${result.warnings} warning${result.warnings !== 1 ? "s" : ""}`;
116
+ if (result.failed > 0) {
117
+ console.log(pc.red(summary));
118
+ } else if (result.warnings > 0) {
119
+ console.log(pc.yellow(summary));
120
+ } else {
121
+ console.log(pc.green(summary));
122
+ }
123
+ }
124
+ async function doctorCommand(opts) {
125
+ const validScopes = ["user", "project", "local"];
126
+ const scope = validScopes.includes(opts.scope) ? opts.scope : "project";
127
+ const result = await _doctorAt({
128
+ settingsPath: getSettingsPath(scope),
129
+ hooksDir: getHooksDir(scope)
130
+ });
131
+ printResults(result);
132
+ process.exit(result.exitCode);
133
+ }
134
+ export {
135
+ _doctorAt,
136
+ doctorCommand
137
+ };
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getHook
4
+ } from "./chunk-XLX5K6TZ.js";
5
+
6
+ // src/commands/info.ts
7
+ import { readdir, readFile } from "fs/promises";
8
+ import { join, dirname } from "path";
9
+ import { fileURLToPath } from "url";
10
+ import { existsSync } from "fs";
11
+ import pc from "picocolors";
12
+ var __filename = fileURLToPath(import.meta.url);
13
+ var __dirname = dirname(__filename);
14
+ function fixturesDir(hookName) {
15
+ let dir = __dirname;
16
+ for (let i = 0; i < 4; i++) {
17
+ const candidate = join(dir, "registry", "hooks", "fixtures", hookName);
18
+ if (existsSync(candidate)) return candidate;
19
+ const parent = join(dir, "..");
20
+ if (parent === dir) break;
21
+ dir = parent;
22
+ }
23
+ return join(__dirname, "..", "registry", "hooks", "fixtures", hookName);
24
+ }
25
+ async function loadFixtures(hookName) {
26
+ const dir = fixturesDir(hookName);
27
+ if (!existsSync(dir)) return [];
28
+ let files;
29
+ try {
30
+ files = await readdir(dir);
31
+ } catch {
32
+ return [];
33
+ }
34
+ const jsonFiles = files.filter((f) => f.endsWith(".json")).sort();
35
+ const fixtures = [];
36
+ for (const file of jsonFiles) {
37
+ try {
38
+ const raw = await readFile(join(dir, file), "utf8");
39
+ const parsed = JSON.parse(raw);
40
+ fixtures.push(parsed);
41
+ } catch {
42
+ }
43
+ }
44
+ return fixtures;
45
+ }
46
+ async function infoCommand(opts) {
47
+ const { hookName } = opts;
48
+ const hook = getHook(hookName);
49
+ if (!hook) {
50
+ console.error(pc.red(`Hook "${hookName}" not found in registry.`));
51
+ console.error(pc.dim("Run `claude-code-hookkit list` to see all available hooks."));
52
+ process.exit(1);
53
+ }
54
+ console.log("");
55
+ console.log(pc.bold(pc.cyan(`${hook.name}`)));
56
+ console.log(pc.dim("\u2500".repeat(50)));
57
+ console.log(`${pc.bold("Description:")} ${hook.description}`);
58
+ console.log(`${pc.bold("Event:")} ${pc.yellow(hook.event)}`);
59
+ console.log(`${pc.bold("Matcher:")} ${hook.matcher ?? pc.dim("(all tools)")}`);
60
+ console.log(`${pc.bold("Pack:")} ${pc.cyan(hook.pack ?? pc.dim("(standalone)"))}`);
61
+ console.log(`${pc.bold("Script:")} ${hook.scriptFile}`);
62
+ const fixtures = await loadFixtures(hookName);
63
+ if (fixtures.length === 0) {
64
+ console.log("");
65
+ console.log(pc.dim("No example fixtures found."));
66
+ } else {
67
+ console.log("");
68
+ console.log(pc.bold("Examples:"));
69
+ console.log("");
70
+ for (const fixture of fixtures) {
71
+ const exitLabel = fixture.expectedExitCode === 0 ? pc.green(`exit ${fixture.expectedExitCode} (allow)`) : pc.red(`exit ${fixture.expectedExitCode} (block)`);
72
+ console.log(` ${pc.bold(fixture.description)}`);
73
+ console.log(` ${pc.dim("Input:")} ${JSON.stringify(fixture.input)}`);
74
+ console.log(` ${pc.dim("Outcome:")} ${exitLabel}`);
75
+ console.log("");
76
+ }
77
+ }
78
+ console.log(pc.dim(`Install with: claude-code-hookkit add ${hookName}`));
79
+ console.log(pc.dim(`Test with: claude-code-hookkit test ${hookName}`));
80
+ console.log("");
81
+ }
82
+ export {
83
+ infoCommand
84
+ };