@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,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
|
+
}
|