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