@hasna/hooks 0.0.1
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/.npmrc.example +2 -0
- package/AGENTS.md +54 -0
- package/CLAUDE.md +70 -0
- package/CONTRIBUTING.md +45 -0
- package/README.md +232 -0
- package/bin/index.js +5171 -0
- package/hooks/hook-agentmessages/CLAUDE.md +79 -0
- package/hooks/hook-agentmessages/LICENSE +21 -0
- package/hooks/hook-agentmessages/README.md +107 -0
- package/hooks/hook-agentmessages/package.json +31 -0
- 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/CLAUDE.md +23 -0
- package/hooks/hook-branchprotect/README.md +25 -0
- package/hooks/hook-branchprotect/package.json +42 -0
- package/hooks/hook-branchprotect/src/cli.ts +126 -0
- package/hooks/hook-branchprotect/src/hook.ts +88 -0
- package/hooks/hook-branchprotect/tsconfig.json +25 -0
- package/hooks/hook-checkbugs/LICENSE +21 -0
- package/hooks/hook-checkbugs/README.md +140 -0
- package/hooks/hook-checkbugs/package.json +58 -0
- package/hooks/hook-checkbugs/src/cli.ts +628 -0
- package/hooks/hook-checkbugs/src/hook.ts +335 -0
- package/hooks/hook-checkbugs/tsconfig.json +15 -0
- package/hooks/hook-checkdocs/README.md +137 -0
- package/hooks/hook-checkdocs/package.json +57 -0
- package/hooks/hook-checkdocs/src/cli.ts +628 -0
- package/hooks/hook-checkdocs/src/hook.ts +310 -0
- package/hooks/hook-checkdocs/tsconfig.json +15 -0
- package/hooks/hook-checkfiles/LICENSE +21 -0
- package/hooks/hook-checkfiles/README.md +141 -0
- package/hooks/hook-checkfiles/package.json +56 -0
- package/hooks/hook-checkfiles/src/cli.ts +545 -0
- package/hooks/hook-checkfiles/src/hook.ts +321 -0
- package/hooks/hook-checkfiles/tsconfig.json +15 -0
- package/hooks/hook-checklint/LICENSE +21 -0
- package/hooks/hook-checklint/README.md +147 -0
- package/hooks/hook-checklint/package.json +57 -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-checklint/tsconfig.json +15 -0
- package/hooks/hook-checkpoint/CLAUDE.md +23 -0
- package/hooks/hook-checkpoint/README.md +37 -0
- package/hooks/hook-checkpoint/package.json +58 -0
- package/hooks/hook-checkpoint/src/cli.ts +191 -0
- package/hooks/hook-checkpoint/src/hook.ts +207 -0
- package/hooks/hook-checkpoint/tsconfig.json +25 -0
- package/hooks/hook-checksecurity/LICENSE +21 -0
- package/hooks/hook-checksecurity/README.md +158 -0
- package/hooks/hook-checksecurity/package.json +57 -0
- package/hooks/hook-checksecurity/src/cli.ts +601 -0
- package/hooks/hook-checksecurity/src/hook.ts +334 -0
- package/hooks/hook-checksecurity/tsconfig.json +15 -0
- package/hooks/hook-checktasks/README.md +144 -0
- package/hooks/hook-checktasks/package.json +55 -0
- package/hooks/hook-checktasks/src/cli.ts +578 -0
- package/hooks/hook-checktasks/src/hook.ts +308 -0
- package/hooks/hook-checktasks/tsconfig.json +20 -0
- package/hooks/hook-checktests/LICENSE +21 -0
- package/hooks/hook-checktests/README.md +137 -0
- package/hooks/hook-checktests/package.json +57 -0
- package/hooks/hook-checktests/src/cli.ts +627 -0
- package/hooks/hook-checktests/src/hook.ts +334 -0
- package/hooks/hook-checktests/tsconfig.json +15 -0
- package/hooks/hook-contextrefresh/CLAUDE.md +23 -0
- package/hooks/hook-contextrefresh/README.md +42 -0
- package/hooks/hook-contextrefresh/package.json +42 -0
- package/hooks/hook-contextrefresh/src/cli.ts +152 -0
- package/hooks/hook-contextrefresh/src/hook.ts +148 -0
- package/hooks/hook-contextrefresh/tsconfig.json +25 -0
- package/hooks/hook-gitguard/CLAUDE.md +22 -0
- package/hooks/hook-gitguard/README.md +30 -0
- package/hooks/hook-gitguard/package.json +57 -0
- package/hooks/hook-gitguard/src/cli.ts +159 -0
- package/hooks/hook-gitguard/src/hook.ts +129 -0
- package/hooks/hook-gitguard/tsconfig.json +25 -0
- package/hooks/hook-packageage/CLAUDE.md +23 -0
- package/hooks/hook-packageage/README.md +33 -0
- package/hooks/hook-packageage/package.json +42 -0
- package/hooks/hook-packageage/src/cli.ts +165 -0
- package/hooks/hook-packageage/src/hook.ts +177 -0
- package/hooks/hook-packageage/tsconfig.json +25 -0
- package/hooks/hook-phonenotify/CLAUDE.md +25 -0
- package/hooks/hook-phonenotify/README.md +44 -0
- package/hooks/hook-phonenotify/package.json +42 -0
- package/hooks/hook-phonenotify/src/cli.ts +196 -0
- package/hooks/hook-phonenotify/src/hook.ts +139 -0
- package/hooks/hook-phonenotify/tsconfig.json +25 -0
- package/hooks/hook-precompact/CLAUDE.md +23 -0
- package/hooks/hook-precompact/README.md +36 -0
- package/hooks/hook-precompact/package.json +42 -0
- package/hooks/hook-precompact/src/cli.ts +168 -0
- package/hooks/hook-precompact/src/hook.ts +122 -0
- package/hooks/hook-precompact/tsconfig.json +25 -0
- package/package.json +61 -0
- package/src/cli/components/App.tsx +191 -0
- package/src/cli/components/CategorySelect.tsx +37 -0
- package/src/cli/components/DataTable.tsx +133 -0
- package/src/cli/components/Header.tsx +18 -0
- package/src/cli/components/HookSelect.tsx +29 -0
- package/src/cli/components/InstallProgress.tsx +105 -0
- package/src/cli/components/SearchView.tsx +86 -0
- package/src/cli/index.tsx +218 -0
- package/src/index.ts +31 -0
- package/src/lib/installer.ts +288 -0
- package/src/lib/registry.ts +205 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { render } from "ink";
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import { App } from "./components/App.js";
|
|
7
|
+
import {
|
|
8
|
+
HOOKS,
|
|
9
|
+
CATEGORIES,
|
|
10
|
+
getHooksByCategory,
|
|
11
|
+
searchHooks,
|
|
12
|
+
getHook,
|
|
13
|
+
} from "../lib/registry.js";
|
|
14
|
+
import {
|
|
15
|
+
installHook,
|
|
16
|
+
getInstalledHooks,
|
|
17
|
+
getRegisteredHooks,
|
|
18
|
+
removeHook,
|
|
19
|
+
} from "../lib/installer.js";
|
|
20
|
+
|
|
21
|
+
const program = new Command();
|
|
22
|
+
|
|
23
|
+
program
|
|
24
|
+
.name("hooks")
|
|
25
|
+
.description("Install Claude Code hooks for your project")
|
|
26
|
+
.version("0.0.1");
|
|
27
|
+
|
|
28
|
+
// Interactive mode (default)
|
|
29
|
+
program
|
|
30
|
+
.command("interactive", { isDefault: true })
|
|
31
|
+
.alias("i")
|
|
32
|
+
.description("Interactive hook browser")
|
|
33
|
+
.action(() => {
|
|
34
|
+
render(<App />);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Install command
|
|
38
|
+
program
|
|
39
|
+
.command("install")
|
|
40
|
+
.alias("add")
|
|
41
|
+
.argument("[hooks...]", "Hooks to install")
|
|
42
|
+
.option("-o, --overwrite", "Overwrite existing hooks", false)
|
|
43
|
+
.description("Install one or more hooks")
|
|
44
|
+
.action((hooks: string[], options) => {
|
|
45
|
+
if (hooks.length === 0) {
|
|
46
|
+
render(<App />);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log(chalk.bold("\nInstalling hooks...\n"));
|
|
51
|
+
|
|
52
|
+
for (const name of hooks) {
|
|
53
|
+
const result = installHook(name, { overwrite: options.overwrite });
|
|
54
|
+
if (result.success) {
|
|
55
|
+
const meta = getHook(name);
|
|
56
|
+
console.log(chalk.green(`✓ ${name}`));
|
|
57
|
+
if (meta) {
|
|
58
|
+
console.log(
|
|
59
|
+
chalk.dim(` ${meta.event}${meta.matcher ? ` [${meta.matcher}]` : ""} → hook-${name}`)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
console.log(chalk.red(`✗ ${name}: ${result.error}`));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log(chalk.dim("\nHooks installed to .hooks/ and registered in ~/.claude/settings.json"));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// List command
|
|
71
|
+
program
|
|
72
|
+
.command("list")
|
|
73
|
+
.alias("ls")
|
|
74
|
+
.option("-c, --category <category>", "Filter by category")
|
|
75
|
+
.option("-a, --all", "Show all available hooks", false)
|
|
76
|
+
.option("-i, --installed", "Show only installed hooks", false)
|
|
77
|
+
.option("-r, --registered", "Show hooks registered in Claude settings", false)
|
|
78
|
+
.description("List available or installed hooks")
|
|
79
|
+
.action((options) => {
|
|
80
|
+
if (options.registered) {
|
|
81
|
+
const registered = getRegisteredHooks();
|
|
82
|
+
if (registered.length === 0) {
|
|
83
|
+
console.log(chalk.dim("No hooks registered in Claude settings"));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
console.log(chalk.bold(`\nRegistered hooks (${registered.length}):\n`));
|
|
87
|
+
for (const name of registered) {
|
|
88
|
+
const meta = getHook(name);
|
|
89
|
+
console.log(
|
|
90
|
+
` ${chalk.cyan(name)} ${chalk.dim(`[${meta?.event || "unknown"}]`)} - ${meta?.description || ""}`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (options.installed) {
|
|
97
|
+
const installed = getInstalledHooks();
|
|
98
|
+
if (installed.length === 0) {
|
|
99
|
+
console.log(chalk.dim("No hooks installed"));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
console.log(chalk.bold(`\nInstalled hooks (${installed.length}):\n`));
|
|
103
|
+
for (const name of installed) {
|
|
104
|
+
console.log(` ${name}`);
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (options.category) {
|
|
110
|
+
const category = CATEGORIES.find(
|
|
111
|
+
(c) => c.toLowerCase() === options.category.toLowerCase()
|
|
112
|
+
);
|
|
113
|
+
if (!category) {
|
|
114
|
+
console.log(chalk.red(`Unknown category: ${options.category}`));
|
|
115
|
+
console.log(chalk.dim(`Available: ${CATEGORIES.join(", ")}`));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const hooks = getHooksByCategory(category);
|
|
119
|
+
console.log(chalk.bold(`\n${category} (${hooks.length}):\n`));
|
|
120
|
+
for (const h of hooks) {
|
|
121
|
+
console.log(
|
|
122
|
+
` ${chalk.cyan(h.name)} ${chalk.dim(`[${h.event}]`)} - ${h.description}`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Show all by category
|
|
129
|
+
console.log(chalk.bold(`\nAvailable hooks (${HOOKS.length}):\n`));
|
|
130
|
+
for (const category of CATEGORIES) {
|
|
131
|
+
const hooks = getHooksByCategory(category);
|
|
132
|
+
console.log(chalk.bold(`${category} (${hooks.length}):`));
|
|
133
|
+
for (const h of hooks) {
|
|
134
|
+
console.log(
|
|
135
|
+
` ${chalk.cyan(h.name)} ${chalk.dim(`[${h.event}]`)} - ${h.description}`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
console.log();
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Search command
|
|
143
|
+
program
|
|
144
|
+
.command("search")
|
|
145
|
+
.argument("<query>", "Search term")
|
|
146
|
+
.description("Search for hooks")
|
|
147
|
+
.action((query: string) => {
|
|
148
|
+
const results = searchHooks(query);
|
|
149
|
+
if (results.length === 0) {
|
|
150
|
+
console.log(chalk.dim(`No hooks found for "${query}"`));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
console.log(chalk.bold(`\nFound ${results.length} hook(s):\n`));
|
|
154
|
+
for (const h of results) {
|
|
155
|
+
console.log(
|
|
156
|
+
` ${chalk.cyan(h.name)} ${chalk.dim(`[${h.event}] [${h.category}]`)}`
|
|
157
|
+
);
|
|
158
|
+
console.log(` ${h.description}`);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Remove command
|
|
163
|
+
program
|
|
164
|
+
.command("remove")
|
|
165
|
+
.alias("rm")
|
|
166
|
+
.argument("<hook>", "Hook to remove")
|
|
167
|
+
.description("Remove an installed hook")
|
|
168
|
+
.action((hook: string) => {
|
|
169
|
+
const removed = removeHook(hook);
|
|
170
|
+
if (removed) {
|
|
171
|
+
console.log(chalk.green(`✓ Removed ${hook} (unregistered from Claude settings)`));
|
|
172
|
+
} else {
|
|
173
|
+
console.log(chalk.red(`✗ ${hook} is not installed`));
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Categories command
|
|
178
|
+
program
|
|
179
|
+
.command("categories")
|
|
180
|
+
.description("List all categories")
|
|
181
|
+
.action(() => {
|
|
182
|
+
console.log(chalk.bold("\nCategories:\n"));
|
|
183
|
+
for (const category of CATEGORIES) {
|
|
184
|
+
const count = getHooksByCategory(category).length;
|
|
185
|
+
console.log(` ${category} (${count})`);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Info command
|
|
190
|
+
program
|
|
191
|
+
.command("info")
|
|
192
|
+
.argument("<hook>", "Hook name")
|
|
193
|
+
.description("Show detailed info about a hook")
|
|
194
|
+
.action((hook: string) => {
|
|
195
|
+
const meta = getHook(hook);
|
|
196
|
+
if (!meta) {
|
|
197
|
+
console.log(chalk.red(`Hook '${hook}' not found`));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
console.log(chalk.bold(`\n${meta.displayName}\n`));
|
|
201
|
+
console.log(` ${meta.description}`);
|
|
202
|
+
console.log();
|
|
203
|
+
console.log(` ${chalk.dim("Category:")} ${meta.category}`);
|
|
204
|
+
console.log(` ${chalk.dim("Event:")} ${meta.event}`);
|
|
205
|
+
console.log(` ${chalk.dim("Matcher:")} ${meta.matcher || "(none)"}`);
|
|
206
|
+
console.log(` ${chalk.dim("Tags:")} ${meta.tags.join(", ")}`);
|
|
207
|
+
console.log(` ${chalk.dim("Package:")} hook-${meta.name}`);
|
|
208
|
+
console.log();
|
|
209
|
+
|
|
210
|
+
const registered = getRegisteredHooks();
|
|
211
|
+
if (registered.includes(meta.name)) {
|
|
212
|
+
console.log(chalk.green(" ● Registered in Claude settings"));
|
|
213
|
+
} else {
|
|
214
|
+
console.log(chalk.dim(" ○ Not registered"));
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
program.parse();
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hasna/hooks - Open source Claude Code hooks library
|
|
3
|
+
*
|
|
4
|
+
* Install hooks with a single command:
|
|
5
|
+
* npx @hasna/hooks install gitguard branchprotect
|
|
6
|
+
*
|
|
7
|
+
* Or use the interactive CLI:
|
|
8
|
+
* npx @hasna/hooks
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
HOOKS,
|
|
13
|
+
CATEGORIES,
|
|
14
|
+
getHook,
|
|
15
|
+
getHooksByCategory,
|
|
16
|
+
searchHooks,
|
|
17
|
+
type HookMeta,
|
|
18
|
+
type Category,
|
|
19
|
+
} from "./lib/registry.js";
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
installHook,
|
|
23
|
+
installHooks,
|
|
24
|
+
getInstalledHooks,
|
|
25
|
+
getRegisteredHooks,
|
|
26
|
+
removeHook,
|
|
27
|
+
hookExists,
|
|
28
|
+
getHookPath,
|
|
29
|
+
type InstallResult,
|
|
30
|
+
type InstallOptions,
|
|
31
|
+
} from "./lib/installer.js";
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook installer - handles copying hooks to user projects and configuring settings
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, cpSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
6
|
+
import { join, dirname } from "path";
|
|
7
|
+
import { homedir } from "os";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
import { getHook } from "./registry.js";
|
|
10
|
+
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const HOOKS_DIR = join(__dirname, "..", "..", "hooks");
|
|
13
|
+
const SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
|
|
14
|
+
|
|
15
|
+
export interface InstallResult {
|
|
16
|
+
hook: string;
|
|
17
|
+
success: boolean;
|
|
18
|
+
error?: string;
|
|
19
|
+
path?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface InstallOptions {
|
|
23
|
+
targetDir?: string;
|
|
24
|
+
overwrite?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get the path to a hook in the package
|
|
29
|
+
*/
|
|
30
|
+
export function getHookPath(name: string): string {
|
|
31
|
+
const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
|
|
32
|
+
return join(HOOKS_DIR, hookName);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if a hook exists in the package
|
|
37
|
+
*/
|
|
38
|
+
export function hookExists(name: string): boolean {
|
|
39
|
+
return existsSync(getHookPath(name));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Read Claude settings.json
|
|
44
|
+
*/
|
|
45
|
+
function readSettings(): Record<string, any> {
|
|
46
|
+
try {
|
|
47
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
48
|
+
return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
|
|
49
|
+
}
|
|
50
|
+
} catch {}
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Write Claude settings.json
|
|
56
|
+
*/
|
|
57
|
+
function writeSettings(settings: Record<string, any>): void {
|
|
58
|
+
const dir = dirname(SETTINGS_PATH);
|
|
59
|
+
if (!existsSync(dir)) {
|
|
60
|
+
mkdirSync(dir, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Install a single hook to the target directory and register in settings
|
|
67
|
+
*/
|
|
68
|
+
export function installHook(
|
|
69
|
+
name: string,
|
|
70
|
+
options: InstallOptions = {}
|
|
71
|
+
): InstallResult {
|
|
72
|
+
const { targetDir = process.cwd(), overwrite = false } = options;
|
|
73
|
+
|
|
74
|
+
const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
|
|
75
|
+
const shortName = hookName.replace("hook-", "");
|
|
76
|
+
const sourcePath = getHookPath(name);
|
|
77
|
+
const destDir = join(targetDir, ".hooks");
|
|
78
|
+
const destPath = join(destDir, hookName);
|
|
79
|
+
|
|
80
|
+
// Check if hook exists in package
|
|
81
|
+
if (!existsSync(sourcePath)) {
|
|
82
|
+
return {
|
|
83
|
+
hook: shortName,
|
|
84
|
+
success: false,
|
|
85
|
+
error: `Hook '${shortName}' not found`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check if already installed
|
|
90
|
+
if (existsSync(destPath) && !overwrite) {
|
|
91
|
+
return {
|
|
92
|
+
hook: shortName,
|
|
93
|
+
success: false,
|
|
94
|
+
error: `Already installed. Use --overwrite to replace.`,
|
|
95
|
+
path: destPath,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
// Ensure .hooks directory exists
|
|
101
|
+
if (!existsSync(destDir)) {
|
|
102
|
+
mkdirSync(destDir, { recursive: true });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Copy hook
|
|
106
|
+
cpSync(sourcePath, destPath, { recursive: true });
|
|
107
|
+
|
|
108
|
+
// Register hook in Claude settings
|
|
109
|
+
registerHookInSettings(shortName);
|
|
110
|
+
|
|
111
|
+
// Update .hooks/index.ts
|
|
112
|
+
updateHooksIndex(destDir);
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
hook: shortName,
|
|
116
|
+
success: true,
|
|
117
|
+
path: destPath,
|
|
118
|
+
};
|
|
119
|
+
} catch (error) {
|
|
120
|
+
return {
|
|
121
|
+
hook: shortName,
|
|
122
|
+
success: false,
|
|
123
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Register a hook in ~/.claude/settings.json
|
|
130
|
+
*/
|
|
131
|
+
function registerHookInSettings(name: string): void {
|
|
132
|
+
const meta = getHook(name);
|
|
133
|
+
if (!meta) return;
|
|
134
|
+
|
|
135
|
+
const settings = readSettings();
|
|
136
|
+
if (!settings.hooks) settings.hooks = {};
|
|
137
|
+
|
|
138
|
+
const eventKey = meta.event;
|
|
139
|
+
if (!settings.hooks[eventKey]) settings.hooks[eventKey] = [];
|
|
140
|
+
|
|
141
|
+
// Check if already registered
|
|
142
|
+
const hookCommand = `hook-${name}`;
|
|
143
|
+
const existing = settings.hooks[eventKey].find((entry: any) =>
|
|
144
|
+
entry.hooks?.some((h: any) => h.command?.includes(hookCommand))
|
|
145
|
+
);
|
|
146
|
+
if (existing) return;
|
|
147
|
+
|
|
148
|
+
const entry: Record<string, any> = {
|
|
149
|
+
hooks: [{ type: "command", command: hookCommand }],
|
|
150
|
+
};
|
|
151
|
+
if (meta.matcher) {
|
|
152
|
+
entry.matcher = meta.matcher;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
settings.hooks[eventKey].push(entry);
|
|
156
|
+
writeSettings(settings);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Unregister a hook from ~/.claude/settings.json
|
|
161
|
+
*/
|
|
162
|
+
function unregisterHookFromSettings(name: string): void {
|
|
163
|
+
const meta = getHook(name);
|
|
164
|
+
if (!meta) return;
|
|
165
|
+
|
|
166
|
+
const settings = readSettings();
|
|
167
|
+
if (!settings.hooks) return;
|
|
168
|
+
|
|
169
|
+
const eventKey = meta.event;
|
|
170
|
+
if (!settings.hooks[eventKey]) return;
|
|
171
|
+
|
|
172
|
+
const hookCommand = `hook-${name}`;
|
|
173
|
+
settings.hooks[eventKey] = settings.hooks[eventKey].filter(
|
|
174
|
+
(entry: any) =>
|
|
175
|
+
!entry.hooks?.some((h: any) => h.command?.includes(hookCommand))
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// Clean up empty arrays
|
|
179
|
+
if (settings.hooks[eventKey].length === 0) {
|
|
180
|
+
delete settings.hooks[eventKey];
|
|
181
|
+
}
|
|
182
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
183
|
+
delete settings.hooks;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
writeSettings(settings);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Install multiple hooks
|
|
191
|
+
*/
|
|
192
|
+
export function installHooks(
|
|
193
|
+
names: string[],
|
|
194
|
+
options: InstallOptions = {}
|
|
195
|
+
): InstallResult[] {
|
|
196
|
+
return names.map((name) => installHook(name, options));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Update the .hooks/index.ts file to export all installed hooks
|
|
201
|
+
*/
|
|
202
|
+
function updateHooksIndex(hooksDir: string): void {
|
|
203
|
+
const indexPath = join(hooksDir, "index.ts");
|
|
204
|
+
const { readdirSync } = require("fs");
|
|
205
|
+
const hooks = readdirSync(hooksDir).filter(
|
|
206
|
+
(f: string) => f.startsWith("hook-") && !f.includes(".")
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const exports = hooks
|
|
210
|
+
.map((h: string) => {
|
|
211
|
+
const name = h.replace("hook-", "");
|
|
212
|
+
return `export * as ${name} from './${h}/src/index.js';`;
|
|
213
|
+
})
|
|
214
|
+
.join("\n");
|
|
215
|
+
|
|
216
|
+
const content = `/**
|
|
217
|
+
* Auto-generated index of installed hooks
|
|
218
|
+
* Do not edit manually - run 'hooks install' to update
|
|
219
|
+
*/
|
|
220
|
+
|
|
221
|
+
${exports}
|
|
222
|
+
`;
|
|
223
|
+
|
|
224
|
+
writeFileSync(indexPath, content);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get list of installed hooks in a directory
|
|
229
|
+
*/
|
|
230
|
+
export function getInstalledHooks(targetDir: string = process.cwd()): string[] {
|
|
231
|
+
const hooksDir = join(targetDir, ".hooks");
|
|
232
|
+
|
|
233
|
+
if (!existsSync(hooksDir)) {
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const { readdirSync, statSync } = require("fs");
|
|
238
|
+
return readdirSync(hooksDir)
|
|
239
|
+
.filter((f: string) => {
|
|
240
|
+
const fullPath = join(hooksDir, f);
|
|
241
|
+
return f.startsWith("hook-") && statSync(fullPath).isDirectory();
|
|
242
|
+
})
|
|
243
|
+
.map((f: string) => f.replace("hook-", ""));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Check which hooks are registered in Claude settings
|
|
248
|
+
*/
|
|
249
|
+
export function getRegisteredHooks(): string[] {
|
|
250
|
+
const settings = readSettings();
|
|
251
|
+
if (!settings.hooks) return [];
|
|
252
|
+
|
|
253
|
+
const registered: string[] = [];
|
|
254
|
+
for (const eventKey of Object.keys(settings.hooks)) {
|
|
255
|
+
for (const entry of settings.hooks[eventKey]) {
|
|
256
|
+
for (const hook of entry.hooks || []) {
|
|
257
|
+
const match = hook.command?.match(/hook-(\w+)/);
|
|
258
|
+
if (match) {
|
|
259
|
+
registered.push(match[1]);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return [...new Set(registered)];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Remove an installed hook
|
|
269
|
+
*/
|
|
270
|
+
export function removeHook(
|
|
271
|
+
name: string,
|
|
272
|
+
targetDir: string = process.cwd()
|
|
273
|
+
): boolean {
|
|
274
|
+
const { rmSync } = require("fs");
|
|
275
|
+
const hookName = name.startsWith("hook-") ? name : `hook-${name}`;
|
|
276
|
+
const shortName = hookName.replace("hook-", "");
|
|
277
|
+
const hooksDir = join(targetDir, ".hooks");
|
|
278
|
+
const hookPath = join(hooksDir, hookName);
|
|
279
|
+
|
|
280
|
+
if (!existsSync(hookPath)) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
rmSync(hookPath, { recursive: true });
|
|
285
|
+
unregisterHookFromSettings(shortName);
|
|
286
|
+
updateHooksIndex(hooksDir);
|
|
287
|
+
return true;
|
|
288
|
+
}
|