@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,628 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @hasnaxyz/hook-checkbugs CLI
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* hook-checkbugs install Auto-detect location, configure options
|
|
8
|
+
* hook-checkbugs install --global Force global install
|
|
9
|
+
* hook-checkbugs install /path Install to specific path
|
|
10
|
+
* hook-checkbugs config Update configuration
|
|
11
|
+
* hook-checkbugs uninstall Remove hook
|
|
12
|
+
* hook-checkbugs run Execute hook (called by Claude Code)
|
|
13
|
+
* hook-checkbugs status Show installation status
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "fs";
|
|
17
|
+
import { join, dirname, resolve } from "path";
|
|
18
|
+
import { homedir } from "os";
|
|
19
|
+
import * as readline from "readline";
|
|
20
|
+
|
|
21
|
+
const PACKAGE_NAME = "@hasnaxyz/hook-checkbugs";
|
|
22
|
+
const CONFIG_KEY = "checkBugsConfig";
|
|
23
|
+
|
|
24
|
+
// Colors
|
|
25
|
+
const c = {
|
|
26
|
+
red: (s: string) => `\x1b[31m${s}\x1b[0m`,
|
|
27
|
+
green: (s: string) => `\x1b[32m${s}\x1b[0m`,
|
|
28
|
+
yellow: (s: string) => `\x1b[33m${s}\x1b[0m`,
|
|
29
|
+
cyan: (s: string) => `\x1b[36m${s}\x1b[0m`,
|
|
30
|
+
dim: (s: string) => `\x1b[2m${s}\x1b[0m`,
|
|
31
|
+
bold: (s: string) => `\x1b[1m${s}\x1b[0m`,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
interface CheckBugsConfig {
|
|
35
|
+
taskListId?: string;
|
|
36
|
+
editThreshold?: number;
|
|
37
|
+
keywords?: string[];
|
|
38
|
+
reviewPrompt?: string;
|
|
39
|
+
enabled?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface InstallOptions {
|
|
43
|
+
taskListId?: string;
|
|
44
|
+
keywords?: string[];
|
|
45
|
+
threshold?: number;
|
|
46
|
+
nonInteractive: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseInstallArgs(args: string[]): InstallOptions {
|
|
50
|
+
const options: InstallOptions = {
|
|
51
|
+
nonInteractive: false,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < args.length; i++) {
|
|
55
|
+
const arg = args[i];
|
|
56
|
+
const nextArg = args[i + 1];
|
|
57
|
+
|
|
58
|
+
switch (arg) {
|
|
59
|
+
case "--task-list-id":
|
|
60
|
+
case "-t":
|
|
61
|
+
if (nextArg && !nextArg.startsWith("-")) {
|
|
62
|
+
options.taskListId = nextArg;
|
|
63
|
+
i++;
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
case "--keywords":
|
|
67
|
+
case "-k":
|
|
68
|
+
if (nextArg && !nextArg.startsWith("-")) {
|
|
69
|
+
options.keywords = nextArg.split(",").map((k) => k.trim().toLowerCase()).filter(Boolean);
|
|
70
|
+
i++;
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
case "--threshold":
|
|
74
|
+
case "-n":
|
|
75
|
+
if (nextArg && !nextArg.startsWith("-")) {
|
|
76
|
+
const num = parseInt(nextArg, 10);
|
|
77
|
+
if (!isNaN(num) && num >= 3 && num <= 7) {
|
|
78
|
+
options.threshold = num;
|
|
79
|
+
}
|
|
80
|
+
i++;
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
case "--yes":
|
|
84
|
+
case "-y":
|
|
85
|
+
options.nonInteractive = true;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// If any explicit option is provided, also enable non-interactive mode
|
|
91
|
+
if (options.taskListId !== undefined || options.keywords !== undefined || options.threshold !== undefined) {
|
|
92
|
+
options.nonInteractive = true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return options;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function printUsage() {
|
|
99
|
+
console.log(`
|
|
100
|
+
${c.bold("hook-checkbugs")} - Checks for bugs via Codex headless agent
|
|
101
|
+
|
|
102
|
+
${c.bold("USAGE:")}
|
|
103
|
+
hook-checkbugs install [path] Install the hook
|
|
104
|
+
hook-checkbugs config [path] Update configuration
|
|
105
|
+
hook-checkbugs uninstall [path] Remove the hook
|
|
106
|
+
hook-checkbugs status Show hook status
|
|
107
|
+
hook-checkbugs run Execute hook ${c.dim("(called by Claude Code)")}
|
|
108
|
+
|
|
109
|
+
${c.bold("OPTIONS:")}
|
|
110
|
+
${c.dim("(no args)")} Auto-detect: if in git repo -> install there, else -> prompt
|
|
111
|
+
--global, -g Apply to ~/.claude/settings.json
|
|
112
|
+
/path/to/repo Apply to specific project path
|
|
113
|
+
|
|
114
|
+
${c.bold("INSTALL OPTIONS:")}
|
|
115
|
+
--task-list-id, -t <id> Task list ID for dispatching tasks
|
|
116
|
+
--keywords, -k <k1,k2> Keywords (comma-separated, default: dev)
|
|
117
|
+
--threshold, -n <num> Edit threshold (3-7, default: 3)
|
|
118
|
+
--yes, -y Non-interactive mode (use defaults/provided values)
|
|
119
|
+
|
|
120
|
+
${c.bold("EXAMPLES:")}
|
|
121
|
+
hook-checkbugs install ${c.dim("# Install with config prompts")}
|
|
122
|
+
hook-checkbugs install --global ${c.dim("# Global install")}
|
|
123
|
+
hook-checkbugs install -y ${c.dim("# Non-interactive with defaults")}
|
|
124
|
+
hook-checkbugs install -t my-list -n 5 -y ${c.dim("# Non-interactive with options")}
|
|
125
|
+
hook-checkbugs config ${c.dim("# Update threshold, task list")}
|
|
126
|
+
hook-checkbugs status ${c.dim("# Check what's installed")}
|
|
127
|
+
|
|
128
|
+
${c.bold("CONFIGURATION:")}
|
|
129
|
+
editThreshold Run review after this many edits (3-7, default: 3)
|
|
130
|
+
taskListId Task list for dispatching tasks (auto-detected if not set)
|
|
131
|
+
keywords Only run for sessions matching keywords (default: dev)
|
|
132
|
+
reviewPrompt Custom prompt for the headless agent
|
|
133
|
+
|
|
134
|
+
${c.bold("HOW IT WORKS:")}
|
|
135
|
+
1. Tracks file edits (Edit, Write, NotebookEdit)
|
|
136
|
+
2. After N edits, spawns headless Codex agent
|
|
137
|
+
3. Agent reviews files and identifies potential bugs
|
|
138
|
+
4. Tasks are dispatched to the configured task list (prefers *-bugfixes)
|
|
139
|
+
|
|
140
|
+
${c.bold("GLOBAL CLI INSTALL:")}
|
|
141
|
+
bun add -g ${PACKAGE_NAME}
|
|
142
|
+
`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isGitRepo(path: string): boolean {
|
|
146
|
+
return existsSync(join(path, ".git"));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function getSettingsPath(targetPath: string | "global"): string {
|
|
150
|
+
if (targetPath === "global") {
|
|
151
|
+
return join(homedir(), ".claude", "settings.json");
|
|
152
|
+
}
|
|
153
|
+
return join(targetPath, ".claude", "settings.json");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function readSettings(path: string): Record<string, unknown> {
|
|
157
|
+
if (!existsSync(path)) return {};
|
|
158
|
+
try {
|
|
159
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
160
|
+
} catch {
|
|
161
|
+
return {};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function writeSettings(path: string, settings: Record<string, unknown>) {
|
|
166
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
167
|
+
writeFileSync(path, JSON.stringify(settings, null, 2) + "\n");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getHookCommand(): string {
|
|
171
|
+
return `bunx ${PACKAGE_NAME}@latest run`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function hookExists(settings: Record<string, unknown>): boolean {
|
|
175
|
+
const hooks = settings.hooks as Record<string, unknown[]> | undefined;
|
|
176
|
+
if (!hooks?.PostToolUse) return false;
|
|
177
|
+
const postToolHooks = hooks.PostToolUse as Array<{ hooks?: Array<{ command?: string }> }>;
|
|
178
|
+
return postToolHooks.some((group) =>
|
|
179
|
+
group.hooks?.some((h) => h.command?.includes(PACKAGE_NAME))
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function getConfig(settings: Record<string, unknown>): CheckBugsConfig {
|
|
184
|
+
return (settings[CONFIG_KEY] as CheckBugsConfig) || {};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function setConfig(settings: Record<string, unknown>, config: CheckBugsConfig): Record<string, unknown> {
|
|
188
|
+
settings[CONFIG_KEY] = config;
|
|
189
|
+
return settings;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function addHook(settings: Record<string, unknown>): Record<string, unknown> {
|
|
193
|
+
const hookConfig = {
|
|
194
|
+
type: "command",
|
|
195
|
+
command: getHookCommand(),
|
|
196
|
+
timeout: 120,
|
|
197
|
+
async: true, // Run async (non-blocking)
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// Match only Edit, Write, NotebookEdit tools
|
|
201
|
+
const matcher = {
|
|
202
|
+
tool_name: "^(Edit|Write|NotebookEdit)$",
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
if (!settings.hooks) settings.hooks = {};
|
|
206
|
+
const hooks = settings.hooks as Record<string, unknown[]>;
|
|
207
|
+
|
|
208
|
+
if (!hooks.PostToolUse) {
|
|
209
|
+
hooks.PostToolUse = [{ matcher, hooks: [hookConfig] }];
|
|
210
|
+
} else {
|
|
211
|
+
const postToolHooks = hooks.PostToolUse as Array<{ matcher?: unknown; hooks?: unknown[] }>;
|
|
212
|
+
// Check if there's already a group for our matcher
|
|
213
|
+
const existingGroup = postToolHooks.find((g) =>
|
|
214
|
+
JSON.stringify(g.matcher) === JSON.stringify(matcher)
|
|
215
|
+
);
|
|
216
|
+
if (existingGroup?.hooks) {
|
|
217
|
+
existingGroup.hooks.push(hookConfig);
|
|
218
|
+
} else {
|
|
219
|
+
postToolHooks.push({ matcher, hooks: [hookConfig] });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return settings;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function removeHook(settings: Record<string, unknown>): Record<string, unknown> {
|
|
226
|
+
const hooks = settings.hooks as Record<string, unknown[]> | undefined;
|
|
227
|
+
if (!hooks?.PostToolUse) return settings;
|
|
228
|
+
|
|
229
|
+
const postToolHooks = hooks.PostToolUse as Array<{ hooks?: Array<{ command?: string }> }>;
|
|
230
|
+
for (const group of postToolHooks) {
|
|
231
|
+
if (group.hooks) {
|
|
232
|
+
group.hooks = group.hooks.filter((h) => !h.command?.includes(PACKAGE_NAME));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
hooks.PostToolUse = postToolHooks.filter((g) => g.hooks && g.hooks.length > 0);
|
|
236
|
+
if (hooks.PostToolUse.length === 0) delete hooks.PostToolUse;
|
|
237
|
+
|
|
238
|
+
// Also remove config
|
|
239
|
+
delete settings[CONFIG_KEY];
|
|
240
|
+
|
|
241
|
+
return settings;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function prompt(question: string): Promise<string> {
|
|
245
|
+
const rl = readline.createInterface({
|
|
246
|
+
input: process.stdin,
|
|
247
|
+
output: process.stdout,
|
|
248
|
+
});
|
|
249
|
+
return new Promise((resolve) => {
|
|
250
|
+
rl.question(question, (answer) => {
|
|
251
|
+
rl.close();
|
|
252
|
+
resolve(answer.trim());
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function getAllTaskLists(): string[] {
|
|
258
|
+
const tasksDir = join(homedir(), ".claude", "tasks");
|
|
259
|
+
if (!existsSync(tasksDir)) return [];
|
|
260
|
+
try {
|
|
261
|
+
return readdirSync(tasksDir, { withFileTypes: true })
|
|
262
|
+
.filter((d) => d.isDirectory())
|
|
263
|
+
.map((d) => d.name);
|
|
264
|
+
} catch {
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function getProjectTaskLists(projectPath: string): string[] {
|
|
270
|
+
const allLists = getAllTaskLists();
|
|
271
|
+
const dirName = projectPath.split("/").filter(Boolean).pop() || "";
|
|
272
|
+
|
|
273
|
+
return allLists.filter((list) => {
|
|
274
|
+
const listLower = list.toLowerCase();
|
|
275
|
+
const dirLower = dirName.toLowerCase();
|
|
276
|
+
if (listLower.startsWith(dirLower + "-")) return true;
|
|
277
|
+
if (listLower.includes(dirLower)) return true;
|
|
278
|
+
return false;
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function resolveTarget(
|
|
283
|
+
args: string[]
|
|
284
|
+
): Promise<{ path: string | "global"; label: string } | null> {
|
|
285
|
+
if (args.includes("--global") || args.includes("-g")) {
|
|
286
|
+
return { path: "global", label: "global (~/.claude/settings.json)" };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const pathArg = args.find((a) => !a.startsWith("-"));
|
|
290
|
+
if (pathArg) {
|
|
291
|
+
const fullPath = resolve(pathArg);
|
|
292
|
+
if (!existsSync(fullPath)) {
|
|
293
|
+
console.log(c.red("X"), `Path does not exist: ${fullPath}`);
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
return { path: fullPath, label: `project (${fullPath})` };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const cwd = process.cwd();
|
|
300
|
+
if (isGitRepo(cwd)) {
|
|
301
|
+
console.log(c.green("V"), `Detected git repo: ${c.cyan(cwd)}`);
|
|
302
|
+
return { path: cwd, label: `project (${cwd})` };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
console.log(c.yellow("!"), `Current directory: ${c.cyan(cwd)}`);
|
|
306
|
+
console.log(c.dim(" (not a git repository)\n"));
|
|
307
|
+
console.log("Where would you like to install?\n");
|
|
308
|
+
console.log(" 1. Here", c.dim(`(${cwd})`));
|
|
309
|
+
console.log(" 2. Global", c.dim("(~/.claude/settings.json)"));
|
|
310
|
+
console.log(" 3. Enter a different path\n");
|
|
311
|
+
|
|
312
|
+
const choice = await prompt("Choice (1/2/3): ");
|
|
313
|
+
|
|
314
|
+
if (choice === "1") {
|
|
315
|
+
return { path: cwd, label: `project (${cwd})` };
|
|
316
|
+
} else if (choice === "2") {
|
|
317
|
+
return { path: "global", label: "global (~/.claude/settings.json)" };
|
|
318
|
+
} else if (choice === "3") {
|
|
319
|
+
const inputPath = await prompt("Path: ");
|
|
320
|
+
if (!inputPath) {
|
|
321
|
+
console.log(c.red("X"), "No path provided");
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
const fullPath = resolve(inputPath);
|
|
325
|
+
if (!existsSync(fullPath)) {
|
|
326
|
+
console.log(c.red("X"), `Path does not exist: ${fullPath}`);
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
return { path: fullPath, label: `project (${fullPath})` };
|
|
330
|
+
} else {
|
|
331
|
+
console.log(c.red("X"), "Invalid choice");
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function promptForConfig(existingConfig: CheckBugsConfig = {}, projectPath?: string): Promise<CheckBugsConfig> {
|
|
337
|
+
const config: CheckBugsConfig = { ...existingConfig };
|
|
338
|
+
|
|
339
|
+
console.log(`\n${c.bold("Configuration")}\n`);
|
|
340
|
+
|
|
341
|
+
// Edit threshold
|
|
342
|
+
const currentThreshold = config.editThreshold || 3;
|
|
343
|
+
console.log(c.bold("Edit Threshold:"));
|
|
344
|
+
console.log(c.dim(" Run review after this many file edits (3-7)"));
|
|
345
|
+
const thresholdInput = await prompt(`Threshold [${c.cyan(currentThreshold.toString())}]: `);
|
|
346
|
+
|
|
347
|
+
if (thresholdInput) {
|
|
348
|
+
const num = parseInt(thresholdInput, 10);
|
|
349
|
+
if (!isNaN(num) && num >= 3 && num <= 7) {
|
|
350
|
+
config.editThreshold = num;
|
|
351
|
+
} else {
|
|
352
|
+
console.log(c.yellow("!"), "Invalid threshold, using default (3)");
|
|
353
|
+
config.editThreshold = 3;
|
|
354
|
+
}
|
|
355
|
+
} else if (!existingConfig.editThreshold) {
|
|
356
|
+
config.editThreshold = 3;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Task list
|
|
360
|
+
const availableLists = projectPath ? getProjectTaskLists(projectPath) : getAllTaskLists();
|
|
361
|
+
const bugfixLists = availableLists.filter((l) => l.toLowerCase().includes("bugfix"));
|
|
362
|
+
|
|
363
|
+
console.log();
|
|
364
|
+
console.log(c.bold("Task List ID:"));
|
|
365
|
+
if (bugfixLists.length > 0) {
|
|
366
|
+
console.log(c.dim(" Bugfix lists for this project:"));
|
|
367
|
+
bugfixLists.forEach((list, i) => {
|
|
368
|
+
console.log(c.dim(` ${i + 1}. ${list}`));
|
|
369
|
+
});
|
|
370
|
+
} else if (availableLists.length > 0) {
|
|
371
|
+
console.log(c.dim(" Available lists:"));
|
|
372
|
+
availableLists.slice(0, 5).forEach((list, i) => {
|
|
373
|
+
console.log(c.dim(` ${i + 1}. ${list}`));
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
console.log(c.dim(" Leave empty to auto-detect (prefers *-bugfixes list)"));
|
|
377
|
+
|
|
378
|
+
const currentList = config.taskListId || "(auto-detect)";
|
|
379
|
+
const listInput = await prompt(`Task list ID [${c.cyan(currentList)}]: `);
|
|
380
|
+
|
|
381
|
+
if (listInput) {
|
|
382
|
+
const num = parseInt(listInput, 10);
|
|
383
|
+
const selectableLists = bugfixLists.length > 0 ? bugfixLists : availableLists;
|
|
384
|
+
if (!isNaN(num) && num > 0 && num <= selectableLists.length) {
|
|
385
|
+
config.taskListId = selectableLists[num - 1];
|
|
386
|
+
} else {
|
|
387
|
+
config.taskListId = listInput;
|
|
388
|
+
}
|
|
389
|
+
} else if (!existingConfig.taskListId) {
|
|
390
|
+
config.taskListId = undefined;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Keywords
|
|
394
|
+
const currentKeywords = config.keywords?.join(", ") || "dev";
|
|
395
|
+
console.log();
|
|
396
|
+
console.log(c.bold("Keywords:"));
|
|
397
|
+
console.log(c.dim(" Only run review for sessions matching these keywords"));
|
|
398
|
+
const keywordsInput = await prompt(`Keywords (comma-separated) [${c.cyan(currentKeywords)}]: `);
|
|
399
|
+
|
|
400
|
+
if (keywordsInput) {
|
|
401
|
+
config.keywords = keywordsInput.split(",").map((k) => k.trim().toLowerCase()).filter(Boolean);
|
|
402
|
+
} else if (!existingConfig.keywords) {
|
|
403
|
+
config.keywords = ["dev"];
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
config.enabled = true;
|
|
407
|
+
|
|
408
|
+
return config;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function install(args: string[]) {
|
|
412
|
+
console.log(`\n${c.bold("hook-checkbugs install")}\n`);
|
|
413
|
+
|
|
414
|
+
const installOptions = parseInstallArgs(args);
|
|
415
|
+
const target = await resolveTarget(args);
|
|
416
|
+
if (!target) return;
|
|
417
|
+
|
|
418
|
+
const settingsPath = getSettingsPath(target.path);
|
|
419
|
+
let settings = readSettings(settingsPath);
|
|
420
|
+
|
|
421
|
+
if (hookExists(settings)) {
|
|
422
|
+
console.log(c.yellow("!"), `Hook already installed in ${target.label}`);
|
|
423
|
+
if (installOptions.nonInteractive) {
|
|
424
|
+
console.log(c.dim(" Updating configuration (non-interactive mode)"));
|
|
425
|
+
} else {
|
|
426
|
+
const update = await prompt("Update configuration? (y/n): ");
|
|
427
|
+
if (update.toLowerCase() !== "y") return;
|
|
428
|
+
}
|
|
429
|
+
} else {
|
|
430
|
+
settings = addHook(settings);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Configure
|
|
434
|
+
const existingConfig = getConfig(settings);
|
|
435
|
+
const projectPath = target.path === "global" ? undefined : target.path;
|
|
436
|
+
|
|
437
|
+
let config: CheckBugsConfig;
|
|
438
|
+
if (installOptions.nonInteractive) {
|
|
439
|
+
// Non-interactive mode: use provided values or defaults
|
|
440
|
+
config = {
|
|
441
|
+
...existingConfig,
|
|
442
|
+
editThreshold: installOptions.threshold ?? existingConfig.editThreshold ?? 3,
|
|
443
|
+
taskListId: installOptions.taskListId ?? existingConfig.taskListId,
|
|
444
|
+
keywords: installOptions.keywords ?? existingConfig.keywords ?? ["dev"],
|
|
445
|
+
enabled: true,
|
|
446
|
+
};
|
|
447
|
+
} else {
|
|
448
|
+
// Interactive mode: prompt for config
|
|
449
|
+
config = await promptForConfig(existingConfig, projectPath);
|
|
450
|
+
}
|
|
451
|
+
settings = setConfig(settings, config);
|
|
452
|
+
|
|
453
|
+
writeSettings(settingsPath, settings);
|
|
454
|
+
|
|
455
|
+
console.log();
|
|
456
|
+
console.log(c.green("V"), `Installed to ${target.label}`);
|
|
457
|
+
console.log();
|
|
458
|
+
console.log(c.bold("Configuration:"));
|
|
459
|
+
console.log(` Threshold: ${config.editThreshold || 3} edits`);
|
|
460
|
+
console.log(` Task list: ${config.taskListId || c.cyan("(auto-detect)")}`);
|
|
461
|
+
console.log(` Keywords: ${config.keywords?.join(", ") || "dev"}`);
|
|
462
|
+
console.log(` Async: ${c.green("yes")} (non-blocking)`);
|
|
463
|
+
console.log();
|
|
464
|
+
console.log(c.bold("Requires:"));
|
|
465
|
+
console.log(` - service-implementation CLI (for task dispatch)`);
|
|
466
|
+
console.log(` - codex CLI (for headless agent)`);
|
|
467
|
+
console.log();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function configure(args: string[]) {
|
|
471
|
+
console.log(`\n${c.bold("hook-checkbugs config")}\n`);
|
|
472
|
+
|
|
473
|
+
const target = await resolveTarget(args);
|
|
474
|
+
if (!target) return;
|
|
475
|
+
|
|
476
|
+
const settingsPath = getSettingsPath(target.path);
|
|
477
|
+
|
|
478
|
+
if (!existsSync(settingsPath)) {
|
|
479
|
+
console.log(c.red("X"), `No settings file at ${settingsPath}`);
|
|
480
|
+
console.log(c.dim(" Run 'hook-checkbugs install' first"));
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
let settings = readSettings(settingsPath);
|
|
485
|
+
|
|
486
|
+
if (!hookExists(settings)) {
|
|
487
|
+
console.log(c.red("X"), `Hook not installed in ${target.label}`);
|
|
488
|
+
console.log(c.dim(" Run 'hook-checkbugs install' first"));
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const existingConfig = getConfig(settings);
|
|
493
|
+
const projectPath = target.path === "global" ? undefined : target.path;
|
|
494
|
+
const config = await promptForConfig(existingConfig, projectPath);
|
|
495
|
+
settings = setConfig(settings, config);
|
|
496
|
+
|
|
497
|
+
writeSettings(settingsPath, settings);
|
|
498
|
+
|
|
499
|
+
console.log();
|
|
500
|
+
console.log(c.green("V"), `Configuration updated`);
|
|
501
|
+
console.log();
|
|
502
|
+
console.log(c.bold("New configuration:"));
|
|
503
|
+
console.log(` Threshold: ${config.editThreshold || 3} edits`);
|
|
504
|
+
console.log(` Task list: ${config.taskListId || c.cyan("(auto-detect)")}`);
|
|
505
|
+
console.log(` Keywords: ${config.keywords?.join(", ") || "dev"}`);
|
|
506
|
+
console.log();
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function uninstall(args: string[]) {
|
|
510
|
+
console.log(`\n${c.bold("hook-checkbugs uninstall")}\n`);
|
|
511
|
+
|
|
512
|
+
const target = await resolveTarget(args);
|
|
513
|
+
if (!target) return;
|
|
514
|
+
|
|
515
|
+
const settingsPath = getSettingsPath(target.path);
|
|
516
|
+
|
|
517
|
+
if (!existsSync(settingsPath)) {
|
|
518
|
+
console.log(c.yellow("!"), `No settings file at ${settingsPath}`);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const settings = readSettings(settingsPath);
|
|
523
|
+
|
|
524
|
+
if (!hookExists(settings)) {
|
|
525
|
+
console.log(c.yellow("!"), `Hook not found in ${target.label}`);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const updated = removeHook(settings);
|
|
530
|
+
writeSettings(settingsPath, updated);
|
|
531
|
+
|
|
532
|
+
console.log(c.green("V"), `Removed from ${target.label}`);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function status() {
|
|
536
|
+
console.log(`\n${c.bold("hook-checkbugs status")}\n`);
|
|
537
|
+
|
|
538
|
+
// Global
|
|
539
|
+
const globalPath = getSettingsPath("global");
|
|
540
|
+
const globalSettings = readSettings(globalPath);
|
|
541
|
+
const globalInstalled = hookExists(globalSettings);
|
|
542
|
+
const globalConfig = getConfig(globalSettings);
|
|
543
|
+
|
|
544
|
+
console.log(
|
|
545
|
+
globalInstalled ? c.green("V") : c.red("X"),
|
|
546
|
+
"Global:",
|
|
547
|
+
globalInstalled ? "Installed" : "Not installed",
|
|
548
|
+
c.dim(`(${globalPath})`)
|
|
549
|
+
);
|
|
550
|
+
if (globalInstalled) {
|
|
551
|
+
console.log(c.dim(` Threshold: ${globalConfig.editThreshold || 3}, List: ${globalConfig.taskListId || "(auto)"}`));
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Current directory
|
|
555
|
+
const cwd = process.cwd();
|
|
556
|
+
const projectPath = getSettingsPath(cwd);
|
|
557
|
+
if (existsSync(projectPath)) {
|
|
558
|
+
const projectSettings = readSettings(projectPath);
|
|
559
|
+
const projectInstalled = hookExists(projectSettings);
|
|
560
|
+
const projectConfig = getConfig(projectSettings);
|
|
561
|
+
|
|
562
|
+
console.log(
|
|
563
|
+
projectInstalled ? c.green("V") : c.red("X"),
|
|
564
|
+
"Project:",
|
|
565
|
+
projectInstalled ? "Installed" : "Not installed",
|
|
566
|
+
c.dim(`(${projectPath})`)
|
|
567
|
+
);
|
|
568
|
+
if (projectInstalled) {
|
|
569
|
+
console.log(c.dim(` Threshold: ${projectConfig.editThreshold || 3}, List: ${projectConfig.taskListId || "(auto)"}`));
|
|
570
|
+
}
|
|
571
|
+
} else {
|
|
572
|
+
console.log(c.dim("."), "Project:", c.dim("No .claude/settings.json"));
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Check dependencies
|
|
576
|
+
console.log();
|
|
577
|
+
console.log(c.bold("Dependencies:"));
|
|
578
|
+
|
|
579
|
+
try {
|
|
580
|
+
const { execSync } = require("child_process");
|
|
581
|
+
execSync("which service-implementation", { stdio: "pipe" });
|
|
582
|
+
console.log(c.green("V"), "service-implementation CLI");
|
|
583
|
+
} catch {
|
|
584
|
+
console.log(c.red("X"), "service-implementation CLI", c.dim("(required for task dispatch)"));
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
try {
|
|
588
|
+
const { execSync } = require("child_process");
|
|
589
|
+
execSync("which codex", { stdio: "pipe" });
|
|
590
|
+
console.log(c.green("V"), "codex CLI");
|
|
591
|
+
} catch {
|
|
592
|
+
console.log(c.red("X"), "codex CLI", c.dim("(required for headless review)"));
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
console.log();
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Main
|
|
599
|
+
const args = process.argv.slice(2);
|
|
600
|
+
const command = args[0];
|
|
601
|
+
const commandArgs = args.slice(1);
|
|
602
|
+
|
|
603
|
+
switch (command) {
|
|
604
|
+
case "install":
|
|
605
|
+
install(commandArgs);
|
|
606
|
+
break;
|
|
607
|
+
case "config":
|
|
608
|
+
configure(commandArgs);
|
|
609
|
+
break;
|
|
610
|
+
case "uninstall":
|
|
611
|
+
uninstall(commandArgs);
|
|
612
|
+
break;
|
|
613
|
+
case "run":
|
|
614
|
+
import("./hook.js").then((m) => m.run());
|
|
615
|
+
break;
|
|
616
|
+
case "status":
|
|
617
|
+
status();
|
|
618
|
+
break;
|
|
619
|
+
case "--help":
|
|
620
|
+
case "-h":
|
|
621
|
+
case undefined:
|
|
622
|
+
printUsage();
|
|
623
|
+
break;
|
|
624
|
+
default:
|
|
625
|
+
console.error(c.red(`Unknown command: ${command}`));
|
|
626
|
+
printUsage();
|
|
627
|
+
process.exit(1);
|
|
628
|
+
}
|