@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.
Files changed (62) hide show
  1. package/dist/index.js +366 -0
  2. package/hooks/hook-agentmessages/bin/cli.ts +125 -0
  3. package/package.json +2 -2
  4. package/hooks/hook-agentmessages/src/check-messages.ts +0 -151
  5. package/hooks/hook-agentmessages/src/install.ts +0 -126
  6. package/hooks/hook-agentmessages/src/session-start.ts +0 -255
  7. package/hooks/hook-agentmessages/src/uninstall.ts +0 -89
  8. package/hooks/hook-branchprotect/src/cli.ts +0 -126
  9. package/hooks/hook-branchprotect/src/hook.ts +0 -88
  10. package/hooks/hook-branchprotect/tsconfig.json +0 -25
  11. package/hooks/hook-checkbugs/src/cli.ts +0 -628
  12. package/hooks/hook-checkbugs/src/hook.ts +0 -335
  13. package/hooks/hook-checkbugs/tsconfig.json +0 -15
  14. package/hooks/hook-checkdocs/src/cli.ts +0 -628
  15. package/hooks/hook-checkdocs/src/hook.ts +0 -310
  16. package/hooks/hook-checkdocs/tsconfig.json +0 -15
  17. package/hooks/hook-checkfiles/src/cli.ts +0 -545
  18. package/hooks/hook-checkfiles/src/hook.ts +0 -321
  19. package/hooks/hook-checkfiles/tsconfig.json +0 -15
  20. package/hooks/hook-checklint/src/cli-patch.ts +0 -32
  21. package/hooks/hook-checklint/src/cli.ts +0 -667
  22. package/hooks/hook-checklint/src/hook.ts +0 -473
  23. package/hooks/hook-checklint/tsconfig.json +0 -15
  24. package/hooks/hook-checkpoint/src/cli.ts +0 -191
  25. package/hooks/hook-checkpoint/src/hook.ts +0 -207
  26. package/hooks/hook-checkpoint/tsconfig.json +0 -25
  27. package/hooks/hook-checksecurity/src/cli.ts +0 -601
  28. package/hooks/hook-checksecurity/src/hook.ts +0 -334
  29. package/hooks/hook-checksecurity/tsconfig.json +0 -15
  30. package/hooks/hook-checktasks/src/cli.ts +0 -578
  31. package/hooks/hook-checktasks/src/hook.ts +0 -308
  32. package/hooks/hook-checktasks/tsconfig.json +0 -20
  33. package/hooks/hook-checktests/src/cli.ts +0 -627
  34. package/hooks/hook-checktests/src/hook.ts +0 -334
  35. package/hooks/hook-checktests/tsconfig.json +0 -15
  36. package/hooks/hook-contextrefresh/src/cli.ts +0 -152
  37. package/hooks/hook-contextrefresh/src/hook.ts +0 -148
  38. package/hooks/hook-contextrefresh/tsconfig.json +0 -25
  39. package/hooks/hook-gitguard/src/cli.ts +0 -159
  40. package/hooks/hook-gitguard/src/hook.ts +0 -129
  41. package/hooks/hook-gitguard/tsconfig.json +0 -25
  42. package/hooks/hook-packageage/src/cli.ts +0 -165
  43. package/hooks/hook-packageage/src/hook.ts +0 -177
  44. package/hooks/hook-packageage/tsconfig.json +0 -25
  45. package/hooks/hook-phonenotify/src/cli.ts +0 -196
  46. package/hooks/hook-phonenotify/src/hook.ts +0 -139
  47. package/hooks/hook-phonenotify/tsconfig.json +0 -25
  48. package/hooks/hook-precompact/src/cli.ts +0 -168
  49. package/hooks/hook-precompact/src/hook.ts +0 -122
  50. package/hooks/hook-precompact/tsconfig.json +0 -25
  51. package/src/cli/components/App.tsx +0 -191
  52. package/src/cli/components/CategorySelect.tsx +0 -37
  53. package/src/cli/components/DataTable.tsx +0 -133
  54. package/src/cli/components/Header.tsx +0 -18
  55. package/src/cli/components/HookSelect.tsx +0 -29
  56. package/src/cli/components/InstallProgress.tsx +0 -105
  57. package/src/cli/components/SearchView.tsx +0 -86
  58. package/src/cli/index.tsx +0 -218
  59. package/src/index.ts +0 -31
  60. package/src/lib/installer.ts +0 -288
  61. package/src/lib/registry.ts +0 -205
  62. 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
- }