@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.
Files changed (110) hide show
  1. package/.npmrc.example +2 -0
  2. package/AGENTS.md +54 -0
  3. package/CLAUDE.md +70 -0
  4. package/CONTRIBUTING.md +45 -0
  5. package/README.md +232 -0
  6. package/bin/index.js +5171 -0
  7. package/hooks/hook-agentmessages/CLAUDE.md +79 -0
  8. package/hooks/hook-agentmessages/LICENSE +21 -0
  9. package/hooks/hook-agentmessages/README.md +107 -0
  10. package/hooks/hook-agentmessages/package.json +31 -0
  11. package/hooks/hook-agentmessages/src/check-messages.ts +151 -0
  12. package/hooks/hook-agentmessages/src/install.ts +126 -0
  13. package/hooks/hook-agentmessages/src/session-start.ts +255 -0
  14. package/hooks/hook-agentmessages/src/uninstall.ts +89 -0
  15. package/hooks/hook-branchprotect/CLAUDE.md +23 -0
  16. package/hooks/hook-branchprotect/README.md +25 -0
  17. package/hooks/hook-branchprotect/package.json +42 -0
  18. package/hooks/hook-branchprotect/src/cli.ts +126 -0
  19. package/hooks/hook-branchprotect/src/hook.ts +88 -0
  20. package/hooks/hook-branchprotect/tsconfig.json +25 -0
  21. package/hooks/hook-checkbugs/LICENSE +21 -0
  22. package/hooks/hook-checkbugs/README.md +140 -0
  23. package/hooks/hook-checkbugs/package.json +58 -0
  24. package/hooks/hook-checkbugs/src/cli.ts +628 -0
  25. package/hooks/hook-checkbugs/src/hook.ts +335 -0
  26. package/hooks/hook-checkbugs/tsconfig.json +15 -0
  27. package/hooks/hook-checkdocs/README.md +137 -0
  28. package/hooks/hook-checkdocs/package.json +57 -0
  29. package/hooks/hook-checkdocs/src/cli.ts +628 -0
  30. package/hooks/hook-checkdocs/src/hook.ts +310 -0
  31. package/hooks/hook-checkdocs/tsconfig.json +15 -0
  32. package/hooks/hook-checkfiles/LICENSE +21 -0
  33. package/hooks/hook-checkfiles/README.md +141 -0
  34. package/hooks/hook-checkfiles/package.json +56 -0
  35. package/hooks/hook-checkfiles/src/cli.ts +545 -0
  36. package/hooks/hook-checkfiles/src/hook.ts +321 -0
  37. package/hooks/hook-checkfiles/tsconfig.json +15 -0
  38. package/hooks/hook-checklint/LICENSE +21 -0
  39. package/hooks/hook-checklint/README.md +147 -0
  40. package/hooks/hook-checklint/package.json +57 -0
  41. package/hooks/hook-checklint/src/cli-patch.ts +32 -0
  42. package/hooks/hook-checklint/src/cli.ts +667 -0
  43. package/hooks/hook-checklint/src/hook.ts +473 -0
  44. package/hooks/hook-checklint/tsconfig.json +15 -0
  45. package/hooks/hook-checkpoint/CLAUDE.md +23 -0
  46. package/hooks/hook-checkpoint/README.md +37 -0
  47. package/hooks/hook-checkpoint/package.json +58 -0
  48. package/hooks/hook-checkpoint/src/cli.ts +191 -0
  49. package/hooks/hook-checkpoint/src/hook.ts +207 -0
  50. package/hooks/hook-checkpoint/tsconfig.json +25 -0
  51. package/hooks/hook-checksecurity/LICENSE +21 -0
  52. package/hooks/hook-checksecurity/README.md +158 -0
  53. package/hooks/hook-checksecurity/package.json +57 -0
  54. package/hooks/hook-checksecurity/src/cli.ts +601 -0
  55. package/hooks/hook-checksecurity/src/hook.ts +334 -0
  56. package/hooks/hook-checksecurity/tsconfig.json +15 -0
  57. package/hooks/hook-checktasks/README.md +144 -0
  58. package/hooks/hook-checktasks/package.json +55 -0
  59. package/hooks/hook-checktasks/src/cli.ts +578 -0
  60. package/hooks/hook-checktasks/src/hook.ts +308 -0
  61. package/hooks/hook-checktasks/tsconfig.json +20 -0
  62. package/hooks/hook-checktests/LICENSE +21 -0
  63. package/hooks/hook-checktests/README.md +137 -0
  64. package/hooks/hook-checktests/package.json +57 -0
  65. package/hooks/hook-checktests/src/cli.ts +627 -0
  66. package/hooks/hook-checktests/src/hook.ts +334 -0
  67. package/hooks/hook-checktests/tsconfig.json +15 -0
  68. package/hooks/hook-contextrefresh/CLAUDE.md +23 -0
  69. package/hooks/hook-contextrefresh/README.md +42 -0
  70. package/hooks/hook-contextrefresh/package.json +42 -0
  71. package/hooks/hook-contextrefresh/src/cli.ts +152 -0
  72. package/hooks/hook-contextrefresh/src/hook.ts +148 -0
  73. package/hooks/hook-contextrefresh/tsconfig.json +25 -0
  74. package/hooks/hook-gitguard/CLAUDE.md +22 -0
  75. package/hooks/hook-gitguard/README.md +30 -0
  76. package/hooks/hook-gitguard/package.json +57 -0
  77. package/hooks/hook-gitguard/src/cli.ts +159 -0
  78. package/hooks/hook-gitguard/src/hook.ts +129 -0
  79. package/hooks/hook-gitguard/tsconfig.json +25 -0
  80. package/hooks/hook-packageage/CLAUDE.md +23 -0
  81. package/hooks/hook-packageage/README.md +33 -0
  82. package/hooks/hook-packageage/package.json +42 -0
  83. package/hooks/hook-packageage/src/cli.ts +165 -0
  84. package/hooks/hook-packageage/src/hook.ts +177 -0
  85. package/hooks/hook-packageage/tsconfig.json +25 -0
  86. package/hooks/hook-phonenotify/CLAUDE.md +25 -0
  87. package/hooks/hook-phonenotify/README.md +44 -0
  88. package/hooks/hook-phonenotify/package.json +42 -0
  89. package/hooks/hook-phonenotify/src/cli.ts +196 -0
  90. package/hooks/hook-phonenotify/src/hook.ts +139 -0
  91. package/hooks/hook-phonenotify/tsconfig.json +25 -0
  92. package/hooks/hook-precompact/CLAUDE.md +23 -0
  93. package/hooks/hook-precompact/README.md +36 -0
  94. package/hooks/hook-precompact/package.json +42 -0
  95. package/hooks/hook-precompact/src/cli.ts +168 -0
  96. package/hooks/hook-precompact/src/hook.ts +122 -0
  97. package/hooks/hook-precompact/tsconfig.json +25 -0
  98. package/package.json +61 -0
  99. package/src/cli/components/App.tsx +191 -0
  100. package/src/cli/components/CategorySelect.tsx +37 -0
  101. package/src/cli/components/DataTable.tsx +133 -0
  102. package/src/cli/components/Header.tsx +18 -0
  103. package/src/cli/components/HookSelect.tsx +29 -0
  104. package/src/cli/components/InstallProgress.tsx +105 -0
  105. package/src/cli/components/SearchView.tsx +86 -0
  106. package/src/cli/index.tsx +218 -0
  107. package/src/index.ts +31 -0
  108. package/src/lib/installer.ts +288 -0
  109. package/src/lib/registry.ts +205 -0
  110. package/tsconfig.json +17 -0
@@ -0,0 +1,578 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * @hasnaxyz/hook-checktasks CLI
5
+ *
6
+ * Usage:
7
+ * hook-checktasks install Auto-detect location, configure task list
8
+ * hook-checktasks install --global Force global install
9
+ * hook-checktasks install /path Install to specific path
10
+ * hook-checktasks config Update configuration
11
+ * hook-checktasks uninstall Remove hook
12
+ * hook-checktasks run Execute hook (called by Claude Code)
13
+ * hook-checktasks 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-checktasks";
22
+ const CONFIG_KEY = "checkTasksConfig";
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 CheckTasksConfig {
35
+ taskListId?: string; // specific list, or undefined = check all
36
+ keywords?: string[]; // keywords to match (default: ["dev"])
37
+ enabled?: boolean; // enable/disable hook
38
+ }
39
+
40
+ function printUsage() {
41
+ console.log(`
42
+ ${c.bold("hook-checktasks")} - Prevents Claude from stopping with pending tasks
43
+
44
+ ${c.bold("USAGE:")}
45
+ hook-checktasks install [path] Install the hook
46
+ hook-checktasks config [path] Update configuration
47
+ hook-checktasks uninstall [path] Remove the hook
48
+ hook-checktasks status Show hook status
49
+ hook-checktasks run Execute hook ${c.dim("(called by Claude Code)")}
50
+
51
+ ${c.bold("OPTIONS:")}
52
+ ${c.dim("(no args)")} Auto-detect: if in git repo → install there, else → prompt
53
+ --global, -g Apply to ~/.claude/settings.json
54
+ --task-list-id, -t <id> Task list ID (non-interactive)
55
+ --keywords, -k <k1,k2> Keywords, comma-separated (non-interactive)
56
+ --yes, -y Non-interactive mode, use defaults
57
+ /path/to/repo Apply to specific project path
58
+
59
+ ${c.bold("EXAMPLES:")}
60
+ hook-checktasks install ${c.dim("# Install with config prompts")}
61
+ hook-checktasks install --global ${c.dim("# Global install")}
62
+ hook-checktasks install -t myproject-dev -y ${c.dim("# Non-interactive")}
63
+ hook-checktasks config ${c.dim("# Update task list ID, keywords")}
64
+ hook-checktasks status ${c.dim("# Check what's installed")}
65
+
66
+ ${c.bold("GLOBAL CLI INSTALL:")}
67
+ bun add -g ${PACKAGE_NAME}
68
+ `);
69
+ }
70
+
71
+ function isGitRepo(path: string): boolean {
72
+ return existsSync(join(path, ".git"));
73
+ }
74
+
75
+ function getSettingsPath(targetPath: string | "global"): string {
76
+ if (targetPath === "global") {
77
+ return join(homedir(), ".claude", "settings.json");
78
+ }
79
+ return join(targetPath, ".claude", "settings.json");
80
+ }
81
+
82
+ function readSettings(path: string): Record<string, unknown> {
83
+ if (!existsSync(path)) return {};
84
+ try {
85
+ return JSON.parse(readFileSync(path, "utf-8"));
86
+ } catch {
87
+ return {};
88
+ }
89
+ }
90
+
91
+ function writeSettings(path: string, settings: Record<string, unknown>) {
92
+ mkdirSync(dirname(path), { recursive: true });
93
+ writeFileSync(path, JSON.stringify(settings, null, 2) + "\n");
94
+ }
95
+
96
+ function getHookCommand(): string {
97
+ return `bunx ${PACKAGE_NAME}@latest run`;
98
+ }
99
+
100
+ function hookExists(settings: Record<string, unknown>): boolean {
101
+ const hooks = settings.hooks as Record<string, unknown[]> | undefined;
102
+ if (!hooks?.Stop) return false;
103
+ const stopHooks = hooks.Stop as Array<{ hooks?: Array<{ command?: string }> }>;
104
+ return stopHooks.some((group) =>
105
+ group.hooks?.some((h) => h.command?.includes(PACKAGE_NAME))
106
+ );
107
+ }
108
+
109
+ function getConfig(settings: Record<string, unknown>): CheckTasksConfig {
110
+ return (settings[CONFIG_KEY] as CheckTasksConfig) || {};
111
+ }
112
+
113
+ function setConfig(settings: Record<string, unknown>, config: CheckTasksConfig): Record<string, unknown> {
114
+ settings[CONFIG_KEY] = config;
115
+ return settings;
116
+ }
117
+
118
+ function addHook(settings: Record<string, unknown>): Record<string, unknown> {
119
+ const hookConfig = {
120
+ type: "command",
121
+ command: getHookCommand(),
122
+ timeout: 120,
123
+ };
124
+
125
+ if (!settings.hooks) settings.hooks = {};
126
+ const hooks = settings.hooks as Record<string, unknown[]>;
127
+
128
+ if (!hooks.Stop) {
129
+ hooks.Stop = [{ hooks: [hookConfig] }];
130
+ } else {
131
+ const stopHooks = hooks.Stop as Array<{ hooks?: unknown[] }>;
132
+ if (stopHooks[0]?.hooks) {
133
+ stopHooks[0].hooks.push(hookConfig);
134
+ } else {
135
+ stopHooks.push({ hooks: [hookConfig] });
136
+ }
137
+ }
138
+ return settings;
139
+ }
140
+
141
+ function removeHook(settings: Record<string, unknown>): Record<string, unknown> {
142
+ const hooks = settings.hooks as Record<string, unknown[]> | undefined;
143
+ if (!hooks?.Stop) return settings;
144
+
145
+ const stopHooks = hooks.Stop as Array<{ hooks?: Array<{ command?: string }> }>;
146
+ for (const group of stopHooks) {
147
+ if (group.hooks) {
148
+ group.hooks = group.hooks.filter((h) => !h.command?.includes(PACKAGE_NAME));
149
+ }
150
+ }
151
+ hooks.Stop = stopHooks.filter((g) => g.hooks && g.hooks.length > 0);
152
+ if (hooks.Stop.length === 0) delete hooks.Stop;
153
+
154
+ // Also remove config
155
+ delete settings[CONFIG_KEY];
156
+
157
+ return settings;
158
+ }
159
+
160
+ async function prompt(question: string): Promise<string> {
161
+ const rl = readline.createInterface({
162
+ input: process.stdin,
163
+ output: process.stdout,
164
+ });
165
+ return new Promise((resolve) => {
166
+ rl.question(question, (answer) => {
167
+ rl.close();
168
+ resolve(answer.trim());
169
+ });
170
+ });
171
+ }
172
+
173
+ function getAllTaskLists(): string[] {
174
+ const tasksDir = join(homedir(), ".claude", "tasks");
175
+ if (!existsSync(tasksDir)) return [];
176
+ try {
177
+ return readdirSync(tasksDir, { withFileTypes: true })
178
+ .filter((d) => d.isDirectory())
179
+ .map((d) => d.name);
180
+ } catch {
181
+ return [];
182
+ }
183
+ }
184
+
185
+ function getProjectTaskLists(projectPath: string): string[] {
186
+ const allLists = getAllTaskLists();
187
+
188
+ // Get the directory name as project identifier
189
+ const dirName = projectPath.split("/").filter(Boolean).pop() || "";
190
+
191
+ // Filter lists that match the project name
192
+ // Match patterns like: project-dev, project-plan, project-bugfixes, etc.
193
+ const projectLists = allLists.filter((list) => {
194
+ const listLower = list.toLowerCase();
195
+ const dirLower = dirName.toLowerCase();
196
+
197
+ // Exact prefix match (e.g., "connect-x" matches "connect-x-dev")
198
+ if (listLower.startsWith(dirLower + "-")) return true;
199
+
200
+ // Also match if the list contains the dir name as a segment
201
+ // e.g., "iapp-copypine-dev" matches if we're in "iapp-copypine"
202
+ if (listLower.includes(dirLower)) return true;
203
+
204
+ return false;
205
+ });
206
+
207
+ return projectLists;
208
+ }
209
+
210
+ interface InstallOptions {
211
+ global?: boolean;
212
+ taskListId?: string;
213
+ keywords?: string[];
214
+ yes?: boolean;
215
+ path?: string;
216
+ }
217
+
218
+ function parseInstallArgs(args: string[]): InstallOptions {
219
+ const options: InstallOptions = {};
220
+
221
+ for (let i = 0; i < args.length; i++) {
222
+ const arg = args[i];
223
+
224
+ if (arg === "--global" || arg === "-g") {
225
+ options.global = true;
226
+ } else if (arg === "--yes" || arg === "-y") {
227
+ options.yes = true;
228
+ } else if (arg === "--task-list-id" || arg === "-t") {
229
+ options.taskListId = args[++i];
230
+ } else if (arg === "--keywords" || arg === "-k") {
231
+ options.keywords = args[++i]?.split(",").map(k => k.trim().toLowerCase()).filter(Boolean);
232
+ } else if (!arg.startsWith("-")) {
233
+ options.path = arg;
234
+ }
235
+ }
236
+
237
+ return options;
238
+ }
239
+
240
+ async function resolveTarget(
241
+ args: string[]
242
+ ): Promise<{ path: string | "global"; label: string } | null> {
243
+ if (args.includes("--global") || args.includes("-g")) {
244
+ return { path: "global", label: "global (~/.claude/settings.json)" };
245
+ }
246
+
247
+ const pathArg = args.find((a) => !a.startsWith("-"));
248
+ if (pathArg) {
249
+ const fullPath = resolve(pathArg);
250
+ if (!existsSync(fullPath)) {
251
+ console.log(c.red("✗"), `Path does not exist: ${fullPath}`);
252
+ return null;
253
+ }
254
+ return { path: fullPath, label: `project (${fullPath})` };
255
+ }
256
+
257
+ const cwd = process.cwd();
258
+ if (isGitRepo(cwd)) {
259
+ console.log(c.green("✓"), `Detected git repo: ${c.cyan(cwd)}`);
260
+ return { path: cwd, label: `project (${cwd})` };
261
+ }
262
+
263
+ console.log(c.yellow("!"), `Current directory: ${c.cyan(cwd)}`);
264
+ console.log(c.dim(" (not a git repository)\n"));
265
+ console.log("Where would you like to install?\n");
266
+ console.log(" 1. Here", c.dim(`(${cwd})`));
267
+ console.log(" 2. Global", c.dim("(~/.claude/settings.json)"));
268
+ console.log(" 3. Enter a different path\n");
269
+
270
+ const choice = await prompt("Choice (1/2/3): ");
271
+
272
+ if (choice === "1") {
273
+ return { path: cwd, label: `project (${cwd})` };
274
+ } else if (choice === "2") {
275
+ return { path: "global", label: "global (~/.claude/settings.json)" };
276
+ } else if (choice === "3") {
277
+ const inputPath = await prompt("Path: ");
278
+ if (!inputPath) {
279
+ console.log(c.red("✗"), "No path provided");
280
+ return null;
281
+ }
282
+ const fullPath = resolve(inputPath);
283
+ if (!existsSync(fullPath)) {
284
+ console.log(c.red("✗"), `Path does not exist: ${fullPath}`);
285
+ return null;
286
+ }
287
+ return { path: fullPath, label: `project (${fullPath})` };
288
+ } else {
289
+ console.log(c.red("✗"), "Invalid choice");
290
+ return null;
291
+ }
292
+ }
293
+
294
+ async function promptForConfig(existingConfig: CheckTasksConfig = {}, projectPath?: string): Promise<CheckTasksConfig> {
295
+ const config: CheckTasksConfig = { ...existingConfig };
296
+
297
+ // Show available task lists for this project
298
+ const availableLists = projectPath ? getProjectTaskLists(projectPath) : getAllTaskLists();
299
+
300
+ console.log(`\n${c.bold("Configuration")}\n`);
301
+
302
+ // Task list selection
303
+ console.log(c.bold("Task List ID:"));
304
+ if (availableLists.length > 0) {
305
+ console.log(c.dim(" Available lists for this project:"));
306
+ availableLists.forEach((list, i) => {
307
+ console.log(c.dim(` ${i + 1}. ${list}`));
308
+ });
309
+ } else {
310
+ console.log(c.dim(" No task lists found for this project"));
311
+ }
312
+ console.log(c.dim(" Leave empty to check all matching lists\n"));
313
+
314
+ const currentList = config.taskListId || "(all lists)";
315
+ const listInput = await prompt(`Task list ID [${c.cyan(currentList)}]: `);
316
+
317
+ if (listInput) {
318
+ // Check if user entered a number (selecting from list)
319
+ const num = parseInt(listInput, 10);
320
+ if (!isNaN(num) && num > 0 && num <= availableLists.length) {
321
+ config.taskListId = availableLists[num - 1];
322
+ } else {
323
+ config.taskListId = listInput;
324
+ }
325
+ } else if (!existingConfig.taskListId) {
326
+ // Empty input + no existing = check all
327
+ config.taskListId = undefined;
328
+ }
329
+
330
+ // Keywords
331
+ const currentKeywords = config.keywords?.join(", ") || "dev";
332
+ const keywordsInput = await prompt(`Keywords (comma-separated) [${c.cyan(currentKeywords)}]: `);
333
+
334
+ if (keywordsInput) {
335
+ config.keywords = keywordsInput.split(",").map((k) => k.trim().toLowerCase()).filter(Boolean);
336
+ } else if (!existingConfig.keywords) {
337
+ config.keywords = ["dev"];
338
+ }
339
+
340
+ config.enabled = true;
341
+
342
+ return config;
343
+ }
344
+
345
+ async function install(args: string[]) {
346
+ console.log(`\n${c.bold("hook-checktasks install")}\n`);
347
+
348
+ const options = parseInstallArgs(args);
349
+
350
+ // Resolve target path
351
+ let target: { path: string | "global"; label: string } | null = null;
352
+
353
+ if (options.global) {
354
+ target = { path: "global", label: "global (~/.claude/settings.json)" };
355
+ } else if (options.path) {
356
+ const fullPath = resolve(options.path);
357
+ if (!existsSync(fullPath)) {
358
+ console.log(c.red("✗"), `Path does not exist: ${fullPath}`);
359
+ return;
360
+ }
361
+ target = { path: fullPath, label: `project (${fullPath})` };
362
+ } else if (options.yes) {
363
+ // Non-interactive mode: use current directory
364
+ const cwd = process.cwd();
365
+ target = { path: cwd, label: `project (${cwd})` };
366
+ } else {
367
+ target = await resolveTarget(args);
368
+ }
369
+
370
+ if (!target) return;
371
+
372
+ const settingsPath = getSettingsPath(target.path);
373
+ let settings = readSettings(settingsPath);
374
+
375
+ if (hookExists(settings)) {
376
+ console.log(c.yellow("!"), `Hook already installed in ${target.label}`);
377
+ if (!options.yes) {
378
+ const update = await prompt("Update configuration? (y/n): ");
379
+ if (update.toLowerCase() !== "y") return;
380
+ }
381
+ } else {
382
+ settings = addHook(settings);
383
+ }
384
+
385
+ // Configure
386
+ const existingConfig = getConfig(settings);
387
+ let config: CheckTasksConfig;
388
+
389
+ if (options.yes || options.taskListId || options.keywords) {
390
+ // Non-interactive mode
391
+ config = {
392
+ ...existingConfig,
393
+ taskListId: options.taskListId || existingConfig.taskListId,
394
+ keywords: options.keywords || existingConfig.keywords || ["dev"],
395
+ enabled: true,
396
+ };
397
+ } else {
398
+ // Interactive mode
399
+ const projectPath = target.path === "global" ? undefined : target.path;
400
+ config = await promptForConfig(existingConfig, projectPath);
401
+ }
402
+
403
+ settings = setConfig(settings, config);
404
+ writeSettings(settingsPath, settings);
405
+
406
+ console.log();
407
+ console.log(c.green("✓"), `Installed to ${target.label}`);
408
+ console.log();
409
+ console.log(c.bold("Configuration:"));
410
+ console.log(` Task list: ${config.taskListId || c.cyan("(all lists)")}`);
411
+ console.log(` Keywords: ${config.keywords?.join(", ") || "dev"}`);
412
+ console.log();
413
+ }
414
+
415
+ async function configure(args: string[]) {
416
+ console.log(`\n${c.bold("hook-checktasks config")}\n`);
417
+
418
+ const target = await resolveTarget(args);
419
+ if (!target) return;
420
+
421
+ const settingsPath = getSettingsPath(target.path);
422
+
423
+ if (!existsSync(settingsPath)) {
424
+ console.log(c.red("✗"), `No settings file at ${settingsPath}`);
425
+ console.log(c.dim(" Run 'hook-checktasks install' first"));
426
+ return;
427
+ }
428
+
429
+ let settings = readSettings(settingsPath);
430
+
431
+ if (!hookExists(settings)) {
432
+ console.log(c.red("✗"), `Hook not installed in ${target.label}`);
433
+ console.log(c.dim(" Run 'hook-checktasks install' first"));
434
+ return;
435
+ }
436
+
437
+ const existingConfig = getConfig(settings);
438
+ const projectPath = target.path === "global" ? undefined : target.path;
439
+ const config = await promptForConfig(existingConfig, projectPath);
440
+ settings = setConfig(settings, config);
441
+
442
+ writeSettings(settingsPath, settings);
443
+
444
+ console.log();
445
+ console.log(c.green("✓"), `Configuration updated`);
446
+ console.log();
447
+ console.log(c.bold("New configuration:"));
448
+ console.log(` Task list: ${config.taskListId || c.cyan("(all lists)")}`);
449
+ console.log(` Keywords: ${config.keywords?.join(", ") || "dev"}`);
450
+ console.log();
451
+ }
452
+
453
+ async function uninstall(args: string[]) {
454
+ console.log(`\n${c.bold("hook-checktasks uninstall")}\n`);
455
+
456
+ const target = await resolveTarget(args);
457
+ if (!target) return;
458
+
459
+ const settingsPath = getSettingsPath(target.path);
460
+
461
+ if (!existsSync(settingsPath)) {
462
+ console.log(c.yellow("!"), `No settings file at ${settingsPath}`);
463
+ return;
464
+ }
465
+
466
+ const settings = readSettings(settingsPath);
467
+
468
+ if (!hookExists(settings)) {
469
+ console.log(c.yellow("!"), `Hook not found in ${target.label}`);
470
+ return;
471
+ }
472
+
473
+ const updated = removeHook(settings);
474
+ writeSettings(settingsPath, updated);
475
+
476
+ console.log(c.green("✓"), `Removed from ${target.label}`);
477
+ }
478
+
479
+ function status() {
480
+ console.log(`\n${c.bold("hook-checktasks status")}\n`);
481
+
482
+ // Global
483
+ const globalPath = getSettingsPath("global");
484
+ const globalSettings = readSettings(globalPath);
485
+ const globalInstalled = hookExists(globalSettings);
486
+ const globalConfig = getConfig(globalSettings);
487
+
488
+ console.log(
489
+ globalInstalled ? c.green("✓") : c.red("✗"),
490
+ "Global:",
491
+ globalInstalled ? "Installed" : "Not installed",
492
+ c.dim(`(${globalPath})`)
493
+ );
494
+ if (globalInstalled) {
495
+ console.log(c.dim(` List: ${globalConfig.taskListId || "(all)"}, Keywords: ${globalConfig.keywords?.join(", ") || "dev"}`));
496
+ }
497
+
498
+ // Current directory
499
+ const cwd = process.cwd();
500
+ const projectPath = getSettingsPath(cwd);
501
+ if (existsSync(projectPath)) {
502
+ const projectSettings = readSettings(projectPath);
503
+ const projectInstalled = hookExists(projectSettings);
504
+ const projectConfig = getConfig(projectSettings);
505
+
506
+ console.log(
507
+ projectInstalled ? c.green("✓") : c.red("✗"),
508
+ "Project:",
509
+ projectInstalled ? "Installed" : "Not installed",
510
+ c.dim(`(${projectPath})`)
511
+ );
512
+ if (projectInstalled) {
513
+ console.log(c.dim(` List: ${projectConfig.taskListId || "(all)"}, Keywords: ${projectConfig.keywords?.join(", ") || "dev"}`));
514
+ }
515
+ } else {
516
+ console.log(c.dim("·"), "Project:", c.dim("No .claude/settings.json"));
517
+ }
518
+
519
+ // Available task lists for current directory
520
+ const projectLists = getProjectTaskLists(cwd);
521
+ if (projectLists.length > 0) {
522
+ console.log();
523
+ console.log(c.bold("Task lists for this project:"));
524
+ projectLists.forEach((list) => console.log(c.dim(` - ${list}`)));
525
+ } else {
526
+ const allLists = getAllTaskLists();
527
+ if (allLists.length > 0) {
528
+ console.log();
529
+ console.log(c.bold("All task lists:"), c.dim("(none match this project)"));
530
+ allLists.slice(0, 10).forEach((list) => console.log(c.dim(` - ${list}`)));
531
+ if (allLists.length > 10) {
532
+ console.log(c.dim(` ... and ${allLists.length - 10} more`));
533
+ }
534
+ }
535
+ }
536
+
537
+ // Environment (legacy)
538
+ const envTaskList = process.env.CLAUDE_CODE_TASK_LIST_ID;
539
+ if (envTaskList) {
540
+ console.log();
541
+ console.log(c.bold("Environment (legacy):"));
542
+ console.log(` CLAUDE_CODE_TASK_LIST_ID: ${envTaskList}`);
543
+ }
544
+
545
+ console.log();
546
+ }
547
+
548
+ // Main
549
+ const args = process.argv.slice(2);
550
+ const command = args[0];
551
+ const commandArgs = args.slice(1);
552
+
553
+ switch (command) {
554
+ case "install":
555
+ install(commandArgs);
556
+ break;
557
+ case "config":
558
+ configure(commandArgs);
559
+ break;
560
+ case "uninstall":
561
+ uninstall(commandArgs);
562
+ break;
563
+ case "run":
564
+ import("./hook.js").then((m) => m.run());
565
+ break;
566
+ case "status":
567
+ status();
568
+ break;
569
+ case "--help":
570
+ case "-h":
571
+ case undefined:
572
+ printUsage();
573
+ break;
574
+ default:
575
+ console.error(c.red(`Unknown command: ${command}`));
576
+ printUsage();
577
+ process.exit(1);
578
+ }