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