hookstack-cli 0.1.0
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/bin/cli.mjs +162 -0
- package/package.json +23 -0
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'
|
|
3
|
+
import { join } from 'path'
|
|
4
|
+
|
|
5
|
+
const API_BASE = 'https://claudehooks.vercel.app'
|
|
6
|
+
const VERSION = '0.1.0'
|
|
7
|
+
|
|
8
|
+
function parseArgs(argv) {
|
|
9
|
+
const args = argv.slice(2)
|
|
10
|
+
const result = { command: null, hooks: [], help: false, version: false }
|
|
11
|
+
|
|
12
|
+
for (let i = 0; i < args.length; i++) {
|
|
13
|
+
const arg = args[i]
|
|
14
|
+
if (arg === '--help' || arg === '-h') { result.help = true; continue }
|
|
15
|
+
if (arg === '--version' || arg === '-v') { result.version = true; continue }
|
|
16
|
+
if (arg.startsWith('--hooks=')) {
|
|
17
|
+
result.hooks = arg.slice('--hooks='.length).split(',').filter(Boolean)
|
|
18
|
+
continue
|
|
19
|
+
}
|
|
20
|
+
if (arg === '--hooks' && args[i + 1]) {
|
|
21
|
+
result.hooks = args[++i].split(',').filter(Boolean)
|
|
22
|
+
continue
|
|
23
|
+
}
|
|
24
|
+
if (!result.command) result.command = arg
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return result
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function mergeHooks(existing, incoming) {
|
|
31
|
+
const merged = JSON.parse(JSON.stringify(existing))
|
|
32
|
+
for (const [event, entries] of Object.entries(incoming)) {
|
|
33
|
+
merged[event] ??= []
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
const found = merged[event].find(e => (e.matcher ?? '') === (entry.matcher ?? ''))
|
|
36
|
+
if (found) {
|
|
37
|
+
found.hooks.push(...entry.hooks)
|
|
38
|
+
} else {
|
|
39
|
+
merged[event].push({ ...entry, hooks: [...entry.hooks] })
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return merged
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function fetchHooks(slugs) {
|
|
47
|
+
const url = `${API_BASE}/api/hooks?slugs=${slugs.join(',')}`
|
|
48
|
+
const res = await fetch(url)
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
const body = await res.text()
|
|
51
|
+
throw new Error(`API error ${res.status}: ${body}`)
|
|
52
|
+
}
|
|
53
|
+
return res.json()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function install(slugs, root) {
|
|
57
|
+
console.log(`\nFetching ${slugs.length} hook${slugs.length > 1 ? 's' : ''}…`)
|
|
58
|
+
|
|
59
|
+
let data
|
|
60
|
+
try {
|
|
61
|
+
data = await fetchHooks(slugs)
|
|
62
|
+
} catch (e) {
|
|
63
|
+
console.error(`\n✗ Failed to fetch hooks: ${e.message}`)
|
|
64
|
+
process.exit(1)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const { hooks } = data
|
|
68
|
+
const notFound = slugs.filter(s => !hooks.find(h => h.slug === s))
|
|
69
|
+
if (notFound.length > 0) {
|
|
70
|
+
console.warn(` ! Unknown slugs (skipped): ${notFound.join(', ')}`)
|
|
71
|
+
}
|
|
72
|
+
if (hooks.length === 0) {
|
|
73
|
+
console.error('\n✗ No hooks to install.')
|
|
74
|
+
process.exit(1)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const claudeDir = join(root, '.claude')
|
|
78
|
+
const settingsPath = join(claudeDir, 'settings.json')
|
|
79
|
+
const hooksDir = join(claudeDir, 'hooks')
|
|
80
|
+
|
|
81
|
+
mkdirSync(claudeDir, { recursive: true })
|
|
82
|
+
mkdirSync(hooksDir, { recursive: true })
|
|
83
|
+
|
|
84
|
+
let settings = {}
|
|
85
|
+
if (existsSync(settingsPath)) {
|
|
86
|
+
try {
|
|
87
|
+
settings = JSON.parse(readFileSync(settingsPath, 'utf8'))
|
|
88
|
+
console.log(' Found existing settings.json — merging…')
|
|
89
|
+
} catch {
|
|
90
|
+
console.warn(' ! Could not parse settings.json — starting fresh')
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const incomingHooks = {}
|
|
95
|
+
for (const hook of hooks) {
|
|
96
|
+
const fragment = hook.config?.hooks
|
|
97
|
+
if (!fragment) continue
|
|
98
|
+
for (const [event, entries] of Object.entries(fragment)) {
|
|
99
|
+
incomingHooks[event] ??= []
|
|
100
|
+
incomingHooks[event].push(...entries)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
settings.hooks = mergeHooks(settings.hooks ?? {}, incomingHooks)
|
|
105
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n')
|
|
106
|
+
console.log(' ✓ .claude/settings.json updated')
|
|
107
|
+
|
|
108
|
+
let scriptCount = 0
|
|
109
|
+
for (const hook of hooks) {
|
|
110
|
+
if (!hook.script_path || !hook.code_snippet) continue
|
|
111
|
+
const dest = join(root, hook.script_path)
|
|
112
|
+
mkdirSync(join(dest, '..'), { recursive: true })
|
|
113
|
+
writeFileSync(dest, hook.code_snippet, 'utf8')
|
|
114
|
+
scriptCount++
|
|
115
|
+
console.log(` ✓ ${hook.script_path}`)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log(`\n✅ Installed ${hooks.length} hook${hooks.length > 1 ? 's' : ''}${scriptCount > 0 ? ` + ${scriptCount} script${scriptCount > 1 ? 's' : ''}` : ''}.`)
|
|
119
|
+
console.log(' Restart Claude Code to activate.\n')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function main() {
|
|
123
|
+
const { command, hooks, help, version } = parseArgs(process.argv)
|
|
124
|
+
|
|
125
|
+
if (version) {
|
|
126
|
+
console.log(VERSION)
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (help || (!command && hooks.length === 0)) {
|
|
131
|
+
console.log(`
|
|
132
|
+
hookstack — Claude Code hook installer
|
|
133
|
+
|
|
134
|
+
Usage:
|
|
135
|
+
npx hookstack-cli-cli install --hooks=<slug1>,<slug2>,...
|
|
136
|
+
|
|
137
|
+
Options:
|
|
138
|
+
--hooks <slugs> Comma-separated list of hook slugs
|
|
139
|
+
--version, -v Show version
|
|
140
|
+
--help, -h Show this help
|
|
141
|
+
|
|
142
|
+
Browse hooks at https://claudehooks.vercel.app
|
|
143
|
+
`)
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (command === 'install' || command === null) {
|
|
148
|
+
if (hooks.length === 0) {
|
|
149
|
+
console.error('✗ No hooks specified. Use --hooks=<slug1>,<slug2>')
|
|
150
|
+
console.error(' Browse hooks at https://claudehooks.vercel.app')
|
|
151
|
+
process.exit(1)
|
|
152
|
+
}
|
|
153
|
+
await install(hooks, process.cwd())
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.error(`✗ Unknown command: ${command}`)
|
|
158
|
+
console.error(' Run --help for usage.')
|
|
159
|
+
process.exit(1)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
main()
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hookstack-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI installer for the Hookstack catalogue of Claude Code hooks",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"hookstack-cli": "./bin/cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/"
|
|
11
|
+
],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"claude-code",
|
|
17
|
+
"hooks",
|
|
18
|
+
"ai",
|
|
19
|
+
"claude"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"homepage": "https://hookstack.vercel.app"
|
|
23
|
+
}
|