hookstack-cli 0.1.19 → 0.1.21
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/README.md +7 -1
- package/bin/{cli.mjs → cli} +32 -0
- package/bin/core.mjs +17 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -24,7 +24,9 @@ npx hookstack-cli@latest install --hooks=<slug1>,<slug2>,...
|
|
|
24
24
|
Options:
|
|
25
25
|
--hooks <slugs> Comma-separated hook slugs (required)
|
|
26
26
|
--global, -g Install into ~/.claude instead of ./.claude
|
|
27
|
-
--
|
|
27
|
+
--copilot Install into ./.claude with paths adapted for GitHub Copilot
|
|
28
|
+
--scope <s> "project" (default), "global", or "copilot"
|
|
29
|
+
--with-tests Also install vitest unit tests into tests/hooks/ (project scope only)
|
|
28
30
|
--yes, -y Skip prompts (non-interactive / CI)
|
|
29
31
|
--version, -v Print version
|
|
30
32
|
--help, -h Show help
|
|
@@ -46,6 +48,9 @@ Skips all prompts — useful in CI or dotfile bootstrap scripts.
|
|
|
46
48
|
```bash
|
|
47
49
|
# CI bootstrap
|
|
48
50
|
npx hookstack-cli@latest install --hooks=secret-detection,git-push-guard --yes --scope=project
|
|
51
|
+
|
|
52
|
+
# CI bootstrap with unit tests (avoids SonarQube gating on new files without tests)
|
|
53
|
+
npx hookstack-cli@latest install --hooks=secret-detection,git-push-guard --yes --with-tests
|
|
49
54
|
```
|
|
50
55
|
|
|
51
56
|
---
|
|
@@ -56,6 +61,7 @@ For each hook the CLI:
|
|
|
56
61
|
|
|
57
62
|
- Writes the `.mjs` script to `.claude/hooks/` (or `~/.claude/hooks/` for global scope)
|
|
58
63
|
- Patches `.claude/settings.json` to register the hook on the right lifecycle event
|
|
64
|
+
- Optionally writes vitest unit tests to `tests/hooks/` when `--with-tests` is passed (or confirmed interactively)
|
|
59
65
|
|
|
60
66
|
No new dependencies are added to your project. Hooks are plain Node.js scripts — no SDK, no agent modification.
|
|
61
67
|
|
package/bin/{cli.mjs → cli}
RENAMED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, realpathSync } from 'fs'
|
|
3
|
+
import { join as pathJoin } from 'path'
|
|
3
4
|
import { homedir } from 'os'
|
|
4
5
|
import { join, dirname } from 'path'
|
|
5
6
|
import { fileURLToPath } from 'url'
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
assertSafeTarget,
|
|
13
14
|
collectIncomingHooks,
|
|
14
15
|
buildSecurityRows,
|
|
16
|
+
doInstallTests,
|
|
15
17
|
} from './core.mjs'
|
|
16
18
|
|
|
17
19
|
const API_BASE = process.env.HOOKSTACK_API_BASE || 'https://hookstack.vercel.app'
|
|
@@ -214,12 +216,32 @@ async function interactiveInstall(slugs, args) {
|
|
|
214
216
|
}
|
|
215
217
|
spin2.stop(`Installed ${pc.bold(String(result.hookCount))} hook${result.hookCount === 1 ? '' : 's'}`)
|
|
216
218
|
|
|
219
|
+
// Unit tests prompt (project/copilot scope only)
|
|
220
|
+
let testResult = null
|
|
221
|
+
const hooksWithTests = hooks.filter(h => h.test_snippet)
|
|
222
|
+
if (scope !== 'global' && hooksWithTests.length > 0) {
|
|
223
|
+
const wantTests = await p.confirm({
|
|
224
|
+
message: `Install unit tests for ${plural(hooksWithTests.length, 'hook')} into ${pc.cyan('tests/hooks/')}? ${pc.dim('(vitest — helps SonarQube gating)')}`,
|
|
225
|
+
initialValue: true,
|
|
226
|
+
})
|
|
227
|
+
if (!p.isCancel(wantTests) && wantTests) {
|
|
228
|
+
try {
|
|
229
|
+
testResult = doInstallTests(hooks, process.cwd(), { mkdirSync, writeFileSync, join: pathJoin })
|
|
230
|
+
} catch (e) {
|
|
231
|
+
p.log.warn(`Could not write tests: ${e.message}`)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
217
236
|
// Result panel
|
|
218
237
|
const resultLines = [
|
|
219
238
|
pc.green(`✓ ${dirs.settingsPath}`),
|
|
220
239
|
result.scriptCount > 0
|
|
221
240
|
? pc.green(`✓ ${result.scriptCount} script${result.scriptCount === 1 ? '' : 's'} written to ${dirs.hooksDir}`)
|
|
222
241
|
: null,
|
|
242
|
+
testResult?.testCount > 0
|
|
243
|
+
? pc.green(`✓ ${testResult.testCount} test file${testResult.testCount === 1 ? '' : 's'} written to tests/hooks/`)
|
|
244
|
+
: null,
|
|
223
245
|
'',
|
|
224
246
|
`Browse more hooks ${pc.cyan(`${API_BASE}/#catalogue`)}`,
|
|
225
247
|
`⭐ Star us ${pc.cyan(REPO_URL)}`,
|
|
@@ -250,6 +272,15 @@ async function directInstall(slugs, args) {
|
|
|
250
272
|
const log = { warn: m => console.warn(` ! ${m}`) }
|
|
251
273
|
const result = doInstall(hooks, dirs, args.scope, log)
|
|
252
274
|
console.log(` ✓ ${dirs.settingsPath}`)
|
|
275
|
+
if (args.withTests && args.scope !== 'global') {
|
|
276
|
+
try {
|
|
277
|
+
const testResult = doInstallTests(hooks, process.cwd(), { mkdirSync, writeFileSync, join: pathJoin })
|
|
278
|
+
if (testResult.testCount > 0)
|
|
279
|
+
console.log(` ✓ ${testResult.testCount} test file${testResult.testCount === 1 ? '' : 's'} written to tests/hooks/`)
|
|
280
|
+
} catch (e) {
|
|
281
|
+
console.warn(` ! Could not write tests: ${e.message}`)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
253
284
|
console.log(`\n✅ ${plural(result.hookCount, 'hook')} installed · star us → ${REPO_URL}`)
|
|
254
285
|
console.log(' Restart Claude Code to activate.\n')
|
|
255
286
|
}
|
|
@@ -266,6 +297,7 @@ const HELP = `
|
|
|
266
297
|
--global, -g Install into ~/.claude instead of ./.claude
|
|
267
298
|
--copilot Install into ./.claude with paths adapted for GitHub Copilot
|
|
268
299
|
--scope <s> "project" (default), "global", or "copilot"
|
|
300
|
+
--with-tests Also install vitest unit tests into tests/hooks/ (project scope only)
|
|
269
301
|
--yes, -y Skip prompts (non-interactive install)
|
|
270
302
|
--version, -v Show version
|
|
271
303
|
--help, -h Show this help
|
package/bin/core.mjs
CHANGED
|
@@ -27,6 +27,7 @@ export function parseArgs(argv) {
|
|
|
27
27
|
version: false,
|
|
28
28
|
scope: 'project',
|
|
29
29
|
yes: false,
|
|
30
|
+
withTests: false,
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -34,6 +35,7 @@ export function parseArgs(argv) {
|
|
|
34
35
|
if (arg === '--help' || arg === '-h') { result.help = true; continue }
|
|
35
36
|
if (arg === '--version' || arg === '-v') { result.version = true; continue }
|
|
36
37
|
if (arg === '--yes' || arg === '-y') { result.yes = true; continue }
|
|
38
|
+
if (arg === '--with-tests') { result.withTests = true; continue }
|
|
37
39
|
if (arg === '--global' || arg === '-g') { result.scope = 'global'; continue }
|
|
38
40
|
if (arg === '--project') { result.scope = 'project'; continue }
|
|
39
41
|
if (arg === '--copilot') { result.scope = 'copilot'; continue }
|
|
@@ -177,6 +179,21 @@ export function shortRepo(url) {
|
|
|
177
179
|
.replace(/\/$/, '')
|
|
178
180
|
}
|
|
179
181
|
|
|
182
|
+
// Writes test files for installed hooks into <projectRoot>/tests/hooks/.
|
|
183
|
+
// Only hooks that have a test_snippet are written; others are silently skipped.
|
|
184
|
+
export function doInstallTests(hooks, projectRoot, { mkdirSync, writeFileSync, join }) {
|
|
185
|
+
const testsDir = join(projectRoot, 'tests', 'hooks')
|
|
186
|
+
mkdirSync(testsDir, { recursive: true })
|
|
187
|
+
let testCount = 0
|
|
188
|
+
for (const hook of hooks) {
|
|
189
|
+
if (!hook.test_snippet) continue
|
|
190
|
+
const dest = join(testsDir, `${hook.slug}.test.mjs`)
|
|
191
|
+
writeFileSync(dest, hook.test_snippet, 'utf8')
|
|
192
|
+
testCount++
|
|
193
|
+
}
|
|
194
|
+
return { testCount }
|
|
195
|
+
}
|
|
196
|
+
|
|
180
197
|
// Display rows for the "Installation Summary" panel.
|
|
181
198
|
export function buildSummaryRows(hooks, { root }) {
|
|
182
199
|
return hooks.map(h => {
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hookstack-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"description": "CLI installer for the Hookstack catalogue of Claude Code hooks",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"hookstack-cli": "./bin/cli
|
|
7
|
+
"hookstack-cli": "./bin/cli"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin/",
|