@hasna/hooks 0.0.1 → 0.0.2
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/dist/index.js +366 -0
- package/hooks/hook-agentmessages/bin/cli.ts +125 -0
- package/package.json +2 -2
- package/hooks/hook-agentmessages/src/check-messages.ts +0 -151
- package/hooks/hook-agentmessages/src/install.ts +0 -126
- package/hooks/hook-agentmessages/src/session-start.ts +0 -255
- package/hooks/hook-agentmessages/src/uninstall.ts +0 -89
- package/hooks/hook-branchprotect/src/cli.ts +0 -126
- package/hooks/hook-branchprotect/src/hook.ts +0 -88
- package/hooks/hook-branchprotect/tsconfig.json +0 -25
- package/hooks/hook-checkbugs/src/cli.ts +0 -628
- package/hooks/hook-checkbugs/src/hook.ts +0 -335
- package/hooks/hook-checkbugs/tsconfig.json +0 -15
- package/hooks/hook-checkdocs/src/cli.ts +0 -628
- package/hooks/hook-checkdocs/src/hook.ts +0 -310
- package/hooks/hook-checkdocs/tsconfig.json +0 -15
- package/hooks/hook-checkfiles/src/cli.ts +0 -545
- package/hooks/hook-checkfiles/src/hook.ts +0 -321
- package/hooks/hook-checkfiles/tsconfig.json +0 -15
- package/hooks/hook-checklint/src/cli-patch.ts +0 -32
- package/hooks/hook-checklint/src/cli.ts +0 -667
- package/hooks/hook-checklint/src/hook.ts +0 -473
- package/hooks/hook-checklint/tsconfig.json +0 -15
- package/hooks/hook-checkpoint/src/cli.ts +0 -191
- package/hooks/hook-checkpoint/src/hook.ts +0 -207
- package/hooks/hook-checkpoint/tsconfig.json +0 -25
- package/hooks/hook-checksecurity/src/cli.ts +0 -601
- package/hooks/hook-checksecurity/src/hook.ts +0 -334
- package/hooks/hook-checksecurity/tsconfig.json +0 -15
- package/hooks/hook-checktasks/src/cli.ts +0 -578
- package/hooks/hook-checktasks/src/hook.ts +0 -308
- package/hooks/hook-checktasks/tsconfig.json +0 -20
- package/hooks/hook-checktests/src/cli.ts +0 -627
- package/hooks/hook-checktests/src/hook.ts +0 -334
- package/hooks/hook-checktests/tsconfig.json +0 -15
- package/hooks/hook-contextrefresh/src/cli.ts +0 -152
- package/hooks/hook-contextrefresh/src/hook.ts +0 -148
- package/hooks/hook-contextrefresh/tsconfig.json +0 -25
- package/hooks/hook-gitguard/src/cli.ts +0 -159
- package/hooks/hook-gitguard/src/hook.ts +0 -129
- package/hooks/hook-gitguard/tsconfig.json +0 -25
- package/hooks/hook-packageage/src/cli.ts +0 -165
- package/hooks/hook-packageage/src/hook.ts +0 -177
- package/hooks/hook-packageage/tsconfig.json +0 -25
- package/hooks/hook-phonenotify/src/cli.ts +0 -196
- package/hooks/hook-phonenotify/src/hook.ts +0 -139
- package/hooks/hook-phonenotify/tsconfig.json +0 -25
- package/hooks/hook-precompact/src/cli.ts +0 -168
- package/hooks/hook-precompact/src/hook.ts +0 -122
- package/hooks/hook-precompact/tsconfig.json +0 -25
- package/src/cli/components/App.tsx +0 -191
- package/src/cli/components/CategorySelect.tsx +0 -37
- package/src/cli/components/DataTable.tsx +0 -133
- package/src/cli/components/Header.tsx +0 -18
- package/src/cli/components/HookSelect.tsx +0 -29
- package/src/cli/components/InstallProgress.tsx +0 -105
- package/src/cli/components/SearchView.tsx +0 -86
- package/src/cli/index.tsx +0 -218
- package/src/index.ts +0 -31
- package/src/lib/installer.ts +0 -288
- package/src/lib/registry.ts +0 -205
- package/tsconfig.json +0 -17
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* CLI for hook-gitguard
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
8
|
-
import { join } from "path";
|
|
9
|
-
import { homedir } from "os";
|
|
10
|
-
|
|
11
|
-
const HOOK_NAME = "hook-gitguard";
|
|
12
|
-
const SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
|
|
13
|
-
|
|
14
|
-
interface ClaudeSettings {
|
|
15
|
-
hooks?: {
|
|
16
|
-
PreToolUse?: Array<{
|
|
17
|
-
matcher: string;
|
|
18
|
-
hooks: Array<{ type: "command"; command: string }>;
|
|
19
|
-
}>;
|
|
20
|
-
};
|
|
21
|
-
[key: string]: unknown;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function readSettings(): ClaudeSettings {
|
|
25
|
-
try {
|
|
26
|
-
if (existsSync(SETTINGS_PATH)) {
|
|
27
|
-
return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
|
|
28
|
-
}
|
|
29
|
-
} catch {}
|
|
30
|
-
return {};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function writeSettings(settings: ClaudeSettings): void {
|
|
34
|
-
const dir = join(homedir(), ".claude");
|
|
35
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
36
|
-
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function install(): void {
|
|
40
|
-
const settings = readSettings();
|
|
41
|
-
if (!settings.hooks) settings.hooks = {};
|
|
42
|
-
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
43
|
-
|
|
44
|
-
const existing = settings.hooks.PreToolUse.find((h) =>
|
|
45
|
-
h.hooks.some((hook) => hook.command.includes(HOOK_NAME))
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
if (existing) {
|
|
49
|
-
console.log(`${HOOK_NAME} is already installed`);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
settings.hooks.PreToolUse.push({
|
|
54
|
-
matcher: "Bash",
|
|
55
|
-
hooks: [{ type: "command", command: `bunx @hasnaxyz/${HOOK_NAME}` }],
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
writeSettings(settings);
|
|
59
|
-
console.log(`${HOOK_NAME} installed successfully`);
|
|
60
|
-
console.log("Hook will block destructive git operations");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function uninstall(): void {
|
|
64
|
-
const settings = readSettings();
|
|
65
|
-
if (!settings.hooks?.PreToolUse) {
|
|
66
|
-
console.log(`${HOOK_NAME} is not installed`);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const before = settings.hooks.PreToolUse.length;
|
|
71
|
-
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
|
|
72
|
-
(h) => !h.hooks.some((hook) => hook.command.includes(HOOK_NAME))
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
if (before === settings.hooks.PreToolUse.length) {
|
|
76
|
-
console.log(`${HOOK_NAME} is not installed`);
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
writeSettings(settings);
|
|
81
|
-
console.log(`${HOOK_NAME} uninstalled successfully`);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function status(): void {
|
|
85
|
-
const settings = readSettings();
|
|
86
|
-
const installed = settings.hooks?.PreToolUse?.some((h) =>
|
|
87
|
-
h.hooks.some((hook) => hook.command.includes(HOOK_NAME))
|
|
88
|
-
);
|
|
89
|
-
console.log(`${HOOK_NAME} is ${installed ? "installed" : "not installed"}`);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function test(command: string): void {
|
|
93
|
-
const PATTERNS: Array<{ pattern: RegExp; description: string }> = [
|
|
94
|
-
{ pattern: /git\s+reset\s+--hard/, description: "git reset --hard" },
|
|
95
|
-
{ pattern: /git\s+push\s+.*--force(?!-)/, description: "git push --force" },
|
|
96
|
-
{ pattern: /git\s+push\s+.*\s-f\b/, description: "git push -f" },
|
|
97
|
-
{ pattern: /git\s+checkout\s+\.\s*$/, description: "git checkout ." },
|
|
98
|
-
{ pattern: /git\s+checkout\s+--\s+\./, description: "git checkout -- ." },
|
|
99
|
-
{ pattern: /git\s+clean\s+(-[a-zA-Z]*f|--force)/, description: "git clean -f" },
|
|
100
|
-
{ pattern: /git\s+branch\s+-D\s/, description: "git branch -D" },
|
|
101
|
-
{ pattern: /git\s+stash\s+(drop|clear)/, description: "git stash drop/clear" },
|
|
102
|
-
];
|
|
103
|
-
|
|
104
|
-
for (const { pattern, description } of PATTERNS) {
|
|
105
|
-
if (pattern.test(command)) {
|
|
106
|
-
console.log(`BLOCKED: ${description}`);
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
console.log("ALLOWED");
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function help(): void {
|
|
114
|
-
console.log(`
|
|
115
|
-
${HOOK_NAME} - Block destructive git operations in Claude Code
|
|
116
|
-
|
|
117
|
-
Usage: ${HOOK_NAME} <command>
|
|
118
|
-
|
|
119
|
-
Commands:
|
|
120
|
-
install Install hook to Claude Code settings
|
|
121
|
-
uninstall Remove hook from Claude Code settings
|
|
122
|
-
status Check if hook is installed
|
|
123
|
-
test <cmd> Test if a git command would be blocked
|
|
124
|
-
help Show this help message
|
|
125
|
-
|
|
126
|
-
Blocked operations:
|
|
127
|
-
git reset --hard git push --force / -f
|
|
128
|
-
git checkout . git checkout -- .
|
|
129
|
-
git clean -f git branch -D
|
|
130
|
-
git stash drop/clear git reflog expire/delete
|
|
131
|
-
`);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const command = process.argv[2];
|
|
135
|
-
|
|
136
|
-
switch (command) {
|
|
137
|
-
case "install": install(); break;
|
|
138
|
-
case "uninstall": uninstall(); break;
|
|
139
|
-
case "status": status(); break;
|
|
140
|
-
case "test":
|
|
141
|
-
if (process.argv[3]) {
|
|
142
|
-
test(process.argv.slice(3).join(" "));
|
|
143
|
-
} else {
|
|
144
|
-
console.error("Usage: hook-gitguard test <command>");
|
|
145
|
-
process.exit(1);
|
|
146
|
-
}
|
|
147
|
-
break;
|
|
148
|
-
case "help":
|
|
149
|
-
case "--help":
|
|
150
|
-
case "-h": help(); break;
|
|
151
|
-
default:
|
|
152
|
-
if (!command) {
|
|
153
|
-
import("./hook.ts").then((m) => m.run());
|
|
154
|
-
} else {
|
|
155
|
-
console.error(`Unknown command: ${command}`);
|
|
156
|
-
help();
|
|
157
|
-
process.exit(1);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Claude Code Hook: gitguard
|
|
5
|
-
*
|
|
6
|
-
* PreToolUse hook that blocks destructive git operations:
|
|
7
|
-
* - git reset --hard
|
|
8
|
-
* - git push --force / -f (especially to main/master)
|
|
9
|
-
* - git checkout . / git checkout -- .
|
|
10
|
-
* - git clean -f / -fd
|
|
11
|
-
* - git branch -D (force delete)
|
|
12
|
-
* - git stash drop / clear
|
|
13
|
-
* - git rebase without caution
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { readFileSync } from "fs";
|
|
17
|
-
|
|
18
|
-
interface HookInput {
|
|
19
|
-
session_id: string;
|
|
20
|
-
cwd: string;
|
|
21
|
-
tool_name: string;
|
|
22
|
-
tool_input: Record<string, unknown>;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
interface HookOutput {
|
|
26
|
-
decision?: "approve" | "block";
|
|
27
|
-
reason?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const DESTRUCTIVE_PATTERNS: Array<{ pattern: RegExp; description: string }> = [
|
|
31
|
-
// git reset --hard
|
|
32
|
-
{ pattern: /git\s+reset\s+--hard/, description: "git reset --hard (discards all uncommitted changes)" },
|
|
33
|
-
|
|
34
|
-
// git push --force (any variant)
|
|
35
|
-
{ pattern: /git\s+push\s+.*--force-with-lease/, description: "git push --force-with-lease (force push with safety)" },
|
|
36
|
-
{ pattern: /git\s+push\s+.*--force(?!-)/, description: "git push --force (overwrites remote history)" },
|
|
37
|
-
{ pattern: /git\s+push\s+.*\s-f\b/, description: "git push -f (force push)" },
|
|
38
|
-
|
|
39
|
-
// Force push to main/master specifically
|
|
40
|
-
{ pattern: /git\s+push\s+.*--force.*\s+(main|master)\b/, description: "force push to main/master" },
|
|
41
|
-
{ pattern: /git\s+push\s+.*-f\s+.*(main|master)\b/, description: "force push to main/master" },
|
|
42
|
-
|
|
43
|
-
// git checkout . / git checkout -- . (discard all changes)
|
|
44
|
-
{ pattern: /git\s+checkout\s+\.\s*$/, description: "git checkout . (discards all working directory changes)" },
|
|
45
|
-
{ pattern: /git\s+checkout\s+--\s+\./, description: "git checkout -- . (discards all working directory changes)" },
|
|
46
|
-
|
|
47
|
-
// git restore . (discard all changes)
|
|
48
|
-
{ pattern: /git\s+restore\s+\.\s*$/, description: "git restore . (discards all working directory changes)" },
|
|
49
|
-
{ pattern: /git\s+restore\s+--staged\s+--worktree\s+\./, description: "git restore --staged --worktree . (discards everything)" },
|
|
50
|
-
|
|
51
|
-
// git clean -f (remove untracked files)
|
|
52
|
-
{ pattern: /git\s+clean\s+(-[a-zA-Z]*f|--force)/, description: "git clean -f (removes untracked files permanently)" },
|
|
53
|
-
|
|
54
|
-
// git branch -D (force delete branch)
|
|
55
|
-
{ pattern: /git\s+branch\s+-D\s/, description: "git branch -D (force delete branch without merge check)" },
|
|
56
|
-
|
|
57
|
-
// git stash drop/clear
|
|
58
|
-
{ pattern: /git\s+stash\s+drop/, description: "git stash drop (permanently deletes stash entry)" },
|
|
59
|
-
{ pattern: /git\s+stash\s+clear/, description: "git stash clear (deletes all stash entries)" },
|
|
60
|
-
|
|
61
|
-
// git reflog expire/delete
|
|
62
|
-
{ pattern: /git\s+reflog\s+(expire|delete)/, description: "git reflog expire/delete (destroys recovery points)" },
|
|
63
|
-
|
|
64
|
-
// git gc --prune=now
|
|
65
|
-
{ pattern: /git\s+gc\s+--prune=now/, description: "git gc --prune=now (permanently removes unreachable objects)" },
|
|
66
|
-
];
|
|
67
|
-
|
|
68
|
-
function readStdinJson(): HookInput | null {
|
|
69
|
-
try {
|
|
70
|
-
const input = readFileSync(0, "utf-8").trim();
|
|
71
|
-
if (!input) return null;
|
|
72
|
-
return JSON.parse(input);
|
|
73
|
-
} catch {
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function checkDestructiveGit(command: string): { blocked: boolean; reason?: string } {
|
|
79
|
-
for (const { pattern, description } of DESTRUCTIVE_PATTERNS) {
|
|
80
|
-
if (pattern.test(command)) {
|
|
81
|
-
return { blocked: true, reason: `Blocked: ${description}` };
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return { blocked: false };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function respond(output: HookOutput): void {
|
|
88
|
-
console.log(JSON.stringify(output));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function run(): void {
|
|
92
|
-
const input = readStdinJson();
|
|
93
|
-
|
|
94
|
-
if (!input) {
|
|
95
|
-
respond({ decision: "approve" });
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (input.tool_name !== "Bash") {
|
|
100
|
-
respond({ decision: "approve" });
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const command = input.tool_input?.command as string;
|
|
105
|
-
if (!command || typeof command !== "string") {
|
|
106
|
-
respond({ decision: "approve" });
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Only check commands that contain "git"
|
|
111
|
-
if (!command.includes("git")) {
|
|
112
|
-
respond({ decision: "approve" });
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const result = checkDestructiveGit(command);
|
|
117
|
-
|
|
118
|
-
if (result.blocked) {
|
|
119
|
-
console.error(`[hook-gitguard] ${result.reason}`);
|
|
120
|
-
respond({ decision: "block", reason: result.reason });
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
respond({ decision: "approve" });
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (import.meta.main) {
|
|
128
|
-
run();
|
|
129
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ESNext",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"lib": ["ESNext"],
|
|
6
|
-
"moduleResolution": "bundler",
|
|
7
|
-
"allowImportingTsExtensions": true,
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"forceConsistentCasingInFileNames": true,
|
|
12
|
-
"resolveJsonModule": true,
|
|
13
|
-
"isolatedModules": true,
|
|
14
|
-
"noEmit": true,
|
|
15
|
-
"noUnusedLocals": true,
|
|
16
|
-
"noUnusedParameters": true,
|
|
17
|
-
"declaration": true,
|
|
18
|
-
"declarationMap": true,
|
|
19
|
-
"outDir": "./dist",
|
|
20
|
-
"rootDir": "./src",
|
|
21
|
-
"types": ["bun-types"]
|
|
22
|
-
},
|
|
23
|
-
"include": ["src/**/*"],
|
|
24
|
-
"exclude": ["node_modules", "dist"]
|
|
25
|
-
}
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* CLI for hook-packageage
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
8
|
-
import { join } from "path";
|
|
9
|
-
import { homedir } from "os";
|
|
10
|
-
|
|
11
|
-
const HOOK_NAME = "hook-packageage";
|
|
12
|
-
const SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
|
|
13
|
-
|
|
14
|
-
interface ClaudeSettings {
|
|
15
|
-
hooks?: {
|
|
16
|
-
PreToolUse?: Array<{
|
|
17
|
-
matcher: string;
|
|
18
|
-
hooks: Array<{ type: "command"; command: string }>;
|
|
19
|
-
}>;
|
|
20
|
-
};
|
|
21
|
-
[key: string]: unknown;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function readSettings(): ClaudeSettings {
|
|
25
|
-
try {
|
|
26
|
-
if (existsSync(SETTINGS_PATH)) {
|
|
27
|
-
return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
|
|
28
|
-
}
|
|
29
|
-
} catch {}
|
|
30
|
-
return {};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function writeSettings(settings: ClaudeSettings): void {
|
|
34
|
-
const dir = join(homedir(), ".claude");
|
|
35
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
36
|
-
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function install(): void {
|
|
40
|
-
const settings = readSettings();
|
|
41
|
-
if (!settings.hooks) settings.hooks = {};
|
|
42
|
-
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
43
|
-
|
|
44
|
-
const existing = settings.hooks.PreToolUse.find((h) =>
|
|
45
|
-
h.hooks.some((hook) => hook.command.includes(HOOK_NAME))
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
if (existing) {
|
|
49
|
-
console.log(`${HOOK_NAME} is already installed`);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
settings.hooks.PreToolUse.push({
|
|
54
|
-
matcher: "Bash",
|
|
55
|
-
hooks: [{ type: "command", command: `bunx @hasnaxyz/${HOOK_NAME}` }],
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
writeSettings(settings);
|
|
59
|
-
console.log(`${HOOK_NAME} installed successfully`);
|
|
60
|
-
console.log("Hook will check package age before npm/bun install commands");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function uninstall(): void {
|
|
64
|
-
const settings = readSettings();
|
|
65
|
-
if (!settings.hooks?.PreToolUse) {
|
|
66
|
-
console.log(`${HOOK_NAME} is not installed`);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const before = settings.hooks.PreToolUse.length;
|
|
71
|
-
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
|
|
72
|
-
(h) => !h.hooks.some((hook) => hook.command.includes(HOOK_NAME))
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
if (before === settings.hooks.PreToolUse.length) {
|
|
76
|
-
console.log(`${HOOK_NAME} is not installed`);
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
writeSettings(settings);
|
|
81
|
-
console.log(`${HOOK_NAME} uninstalled successfully`);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function status(): void {
|
|
85
|
-
const settings = readSettings();
|
|
86
|
-
const installed = settings.hooks?.PreToolUse?.some((h) =>
|
|
87
|
-
h.hooks.some((hook) => hook.command.includes(HOOK_NAME))
|
|
88
|
-
);
|
|
89
|
-
console.log(`${HOOK_NAME} is ${installed ? "installed" : "not installed"}`);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function check(packageName: string): Promise<void> {
|
|
93
|
-
console.log(`Checking ${packageName}...`);
|
|
94
|
-
try {
|
|
95
|
-
const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}`);
|
|
96
|
-
if (!response.ok) {
|
|
97
|
-
console.error(`Package not found: ${packageName}`);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
const data = await response.json() as Record<string, unknown>;
|
|
101
|
-
const time = data.time as Record<string, string> | undefined;
|
|
102
|
-
const modified = time?.modified;
|
|
103
|
-
if (modified) {
|
|
104
|
-
const days = Math.floor((Date.now() - new Date(modified).getTime()) / (1000 * 60 * 60 * 24));
|
|
105
|
-
const status = days > 730 ? "ABANDONED" : days > 365 ? "STALE" : "ACTIVE";
|
|
106
|
-
console.log(` Last updated: ${modified} (${days} days ago) — ${status}`);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const distTags = data["dist-tags"] as Record<string, string> | undefined;
|
|
110
|
-
const latestVersion = distTags?.latest;
|
|
111
|
-
const versions = data.versions as Record<string, Record<string, unknown>> | undefined;
|
|
112
|
-
if (latestVersion && versions?.[latestVersion]?.deprecated) {
|
|
113
|
-
console.log(` DEPRECATED: ${versions[latestVersion].deprecated}`);
|
|
114
|
-
}
|
|
115
|
-
} catch (error) {
|
|
116
|
-
console.error(`Error checking ${packageName}:`, error);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function help(): void {
|
|
121
|
-
console.log(`
|
|
122
|
-
${HOOK_NAME} - Check package age before install
|
|
123
|
-
|
|
124
|
-
Usage: ${HOOK_NAME} <command>
|
|
125
|
-
|
|
126
|
-
Commands:
|
|
127
|
-
install Install hook to Claude Code settings
|
|
128
|
-
uninstall Remove hook from Claude Code settings
|
|
129
|
-
status Check if hook is installed
|
|
130
|
-
check <pkg> Manually check a package's age
|
|
131
|
-
help Show this help message
|
|
132
|
-
|
|
133
|
-
Thresholds:
|
|
134
|
-
> 1 year since last publish: STALE warning
|
|
135
|
-
> 2 years since last publish: ABANDONED warning
|
|
136
|
-
Deprecated packages: always warned
|
|
137
|
-
`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const command = process.argv[2];
|
|
141
|
-
|
|
142
|
-
switch (command) {
|
|
143
|
-
case "install": install(); break;
|
|
144
|
-
case "uninstall": uninstall(); break;
|
|
145
|
-
case "status": status(); break;
|
|
146
|
-
case "check":
|
|
147
|
-
if (process.argv[3]) {
|
|
148
|
-
check(process.argv[3]);
|
|
149
|
-
} else {
|
|
150
|
-
console.error("Usage: hook-packageage check <package-name>");
|
|
151
|
-
process.exit(1);
|
|
152
|
-
}
|
|
153
|
-
break;
|
|
154
|
-
case "help":
|
|
155
|
-
case "--help":
|
|
156
|
-
case "-h": help(); break;
|
|
157
|
-
default:
|
|
158
|
-
if (!command) {
|
|
159
|
-
import("./hook.ts").then((m) => m.run());
|
|
160
|
-
} else {
|
|
161
|
-
console.error(`Unknown command: ${command}`);
|
|
162
|
-
help();
|
|
163
|
-
process.exit(1);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Claude Code Hook: packageage
|
|
5
|
-
*
|
|
6
|
-
* PreToolUse hook that checks package age before npm/bun install commands.
|
|
7
|
-
* Warns on packages that haven't been updated in over a year (potentially
|
|
8
|
-
* abandoned) or have known deprecation markers.
|
|
9
|
-
*
|
|
10
|
-
* Checks the npm registry for last publish date and warns accordingly.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { readFileSync } from "fs";
|
|
14
|
-
|
|
15
|
-
interface HookInput {
|
|
16
|
-
session_id: string;
|
|
17
|
-
cwd: string;
|
|
18
|
-
tool_name: string;
|
|
19
|
-
tool_input: Record<string, unknown>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface HookOutput {
|
|
23
|
-
decision?: "approve" | "block";
|
|
24
|
-
reason?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const INSTALL_PATTERNS = [
|
|
28
|
-
/(?:npm|bun|yarn|pnpm)\s+(?:install|add|i)\s+/,
|
|
29
|
-
];
|
|
30
|
-
|
|
31
|
-
const STALE_THRESHOLD_DAYS = 365; // 1 year
|
|
32
|
-
const ABANDONED_THRESHOLD_DAYS = 730; // 2 years
|
|
33
|
-
|
|
34
|
-
function readStdinJson(): HookInput | null {
|
|
35
|
-
try {
|
|
36
|
-
const input = readFileSync(0, "utf-8").trim();
|
|
37
|
-
if (!input) return null;
|
|
38
|
-
return JSON.parse(input);
|
|
39
|
-
} catch {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Extract package names from an install command
|
|
46
|
-
*/
|
|
47
|
-
function extractPackageNames(command: string): string[] {
|
|
48
|
-
// Match: npm install pkg1 pkg2 / bun add pkg1 / etc.
|
|
49
|
-
const match = command.match(/(?:npm|bun|yarn|pnpm)\s+(?:install|add|i)\s+(.*)/);
|
|
50
|
-
if (!match) return [];
|
|
51
|
-
|
|
52
|
-
return match[1]
|
|
53
|
-
.split(/\s+/)
|
|
54
|
-
.filter((arg) => !arg.startsWith("-") && !arg.startsWith("--"))
|
|
55
|
-
.map((pkg) => pkg.replace(/@[\^~><=\d].*$/, "")) // strip version specifier
|
|
56
|
-
.filter((pkg) => pkg.length > 0 && !pkg.startsWith(".")); // filter paths
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Check package age via npm registry
|
|
61
|
-
*/
|
|
62
|
-
async function checkPackageAge(packageName: string): Promise<{
|
|
63
|
-
name: string;
|
|
64
|
-
lastPublish: Date | null;
|
|
65
|
-
daysSincePublish: number;
|
|
66
|
-
deprecated: boolean;
|
|
67
|
-
error?: string;
|
|
68
|
-
}> {
|
|
69
|
-
try {
|
|
70
|
-
const response = await fetch(
|
|
71
|
-
`https://registry.npmjs.org/${encodeURIComponent(packageName)}`,
|
|
72
|
-
{ signal: AbortSignal.timeout(5000) }
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
if (!response.ok) {
|
|
76
|
-
return { name: packageName, lastPublish: null, daysSincePublish: 0, deprecated: false, error: `HTTP ${response.status}` };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const data = await response.json() as Record<string, unknown>;
|
|
80
|
-
|
|
81
|
-
// Check deprecation
|
|
82
|
-
const distTags = data["dist-tags"] as Record<string, string> | undefined;
|
|
83
|
-
const latestVersion = distTags?.latest;
|
|
84
|
-
const versions = data.versions as Record<string, Record<string, unknown>> | undefined;
|
|
85
|
-
const deprecated = latestVersion && versions?.[latestVersion]?.deprecated ? true : false;
|
|
86
|
-
|
|
87
|
-
// Get last publish date
|
|
88
|
-
const time = data.time as Record<string, string> | undefined;
|
|
89
|
-
const modified = time?.modified;
|
|
90
|
-
|
|
91
|
-
if (!modified) {
|
|
92
|
-
return { name: packageName, lastPublish: null, daysSincePublish: 0, deprecated };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const lastPublish = new Date(modified);
|
|
96
|
-
const daysSincePublish = Math.floor(
|
|
97
|
-
(Date.now() - lastPublish.getTime()) / (1000 * 60 * 60 * 24)
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
return { name: packageName, lastPublish, daysSincePublish, deprecated };
|
|
101
|
-
} catch (error) {
|
|
102
|
-
return {
|
|
103
|
-
name: packageName,
|
|
104
|
-
lastPublish: null,
|
|
105
|
-
daysSincePublish: 0,
|
|
106
|
-
deprecated: false,
|
|
107
|
-
error: error instanceof Error ? error.message : String(error),
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function respond(output: HookOutput): void {
|
|
113
|
-
console.log(JSON.stringify(output));
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export async function run(): Promise<void> {
|
|
117
|
-
const input = readStdinJson();
|
|
118
|
-
|
|
119
|
-
if (!input) {
|
|
120
|
-
respond({ decision: "approve" });
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (input.tool_name !== "Bash") {
|
|
125
|
-
respond({ decision: "approve" });
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const command = input.tool_input?.command as string;
|
|
130
|
-
if (!command || typeof command !== "string") {
|
|
131
|
-
respond({ decision: "approve" });
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Check if this is a package install command
|
|
136
|
-
const isInstall = INSTALL_PATTERNS.some((p) => p.test(command));
|
|
137
|
-
if (!isInstall) {
|
|
138
|
-
respond({ decision: "approve" });
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const packages = extractPackageNames(command);
|
|
143
|
-
if (packages.length === 0) {
|
|
144
|
-
respond({ decision: "approve" });
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const warnings: string[] = [];
|
|
149
|
-
|
|
150
|
-
// Check each package (in parallel, with timeout)
|
|
151
|
-
const results = await Promise.all(packages.map(checkPackageAge));
|
|
152
|
-
|
|
153
|
-
for (const result of results) {
|
|
154
|
-
if (result.deprecated) {
|
|
155
|
-
warnings.push(`${result.name}: DEPRECATED`);
|
|
156
|
-
}
|
|
157
|
-
if (result.daysSincePublish > ABANDONED_THRESHOLD_DAYS) {
|
|
158
|
-
warnings.push(`${result.name}: possibly abandoned (last updated ${result.daysSincePublish} days ago)`);
|
|
159
|
-
} else if (result.daysSincePublish > STALE_THRESHOLD_DAYS) {
|
|
160
|
-
warnings.push(`${result.name}: stale (last updated ${result.daysSincePublish} days ago)`);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (warnings.length > 0) {
|
|
165
|
-
const reason = `Package age warnings:\n${warnings.map((w) => ` - ${w}`).join("\n")}\n\nConsider using more actively maintained alternatives.`;
|
|
166
|
-
console.error(`[hook-packageage] ${reason}`);
|
|
167
|
-
// Warn but don't block — just inject the warning
|
|
168
|
-
respond({ decision: "approve", reason });
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
respond({ decision: "approve" });
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (import.meta.main) {
|
|
176
|
-
run();
|
|
177
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ESNext",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"lib": ["ESNext"],
|
|
6
|
-
"moduleResolution": "bundler",
|
|
7
|
-
"allowImportingTsExtensions": true,
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"forceConsistentCasingInFileNames": true,
|
|
12
|
-
"resolveJsonModule": true,
|
|
13
|
-
"isolatedModules": true,
|
|
14
|
-
"noEmit": true,
|
|
15
|
-
"noUnusedLocals": true,
|
|
16
|
-
"noUnusedParameters": true,
|
|
17
|
-
"declaration": true,
|
|
18
|
-
"declarationMap": true,
|
|
19
|
-
"outDir": "./dist",
|
|
20
|
-
"rootDir": "./src",
|
|
21
|
-
"types": ["bun-types"]
|
|
22
|
-
},
|
|
23
|
-
"include": ["src/**/*"],
|
|
24
|
-
"exclude": ["node_modules", "dist"]
|
|
25
|
-
}
|