hookstack-cli 0.1.10 → 0.1.12
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 +45 -45
- package/package.json +1 -2
package/bin/cli.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, realpathSync } from 'fs'
|
|
3
|
+
import { createInterface } from 'readline'
|
|
3
4
|
import { homedir } from 'os'
|
|
4
5
|
import { join, dirname } from 'path'
|
|
5
6
|
import { fileURLToPath } from 'url'
|
|
6
|
-
import * as p from '@clack/prompts'
|
|
7
7
|
import pc from 'picocolors'
|
|
8
8
|
import {
|
|
9
9
|
parseArgs,
|
|
@@ -32,10 +32,13 @@ async function fetchHooks(slugs) {
|
|
|
32
32
|
return res.json()
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function ask(question) {
|
|
36
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
37
|
+
return new Promise(resolve => rl.question(question, a => { rl.close(); resolve(a.trim()) }))
|
|
38
|
+
}
|
|
39
|
+
|
|
35
40
|
// ── panel rendering ────────────────────────────────────────────────────────
|
|
36
41
|
|
|
37
|
-
// Truncate to a visible width then pad — operate on PLAIN text only. Color is
|
|
38
|
-
// applied afterwards so ANSI codes never throw off column alignment.
|
|
39
42
|
function truncPad(value, width) {
|
|
40
43
|
const s = String(value ?? '')
|
|
41
44
|
return (s.length > width ? s.slice(0, width - 1) + '…' : s).padEnd(width)
|
|
@@ -56,7 +59,6 @@ export function summaryPanel(rows) {
|
|
|
56
59
|
return lines.join('\n')
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
// Boolean capability cell: pad the plain "yes"/"no" first, then colorize.
|
|
60
62
|
function capCell(on, width) {
|
|
61
63
|
const text = (on ? 'yes' : 'no').padEnd(width)
|
|
62
64
|
return on ? pc.yellow(text) : pc.dim(text)
|
|
@@ -124,67 +126,64 @@ function doInstall(hooks, dirs, scope, log) {
|
|
|
124
126
|
const plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`
|
|
125
127
|
|
|
126
128
|
async function interactiveInstall(slugs, args) {
|
|
127
|
-
p.intro(pc.bgCyan(pc.black(' hookstack-cli ')))
|
|
128
|
-
|
|
129
|
-
const s = p.spinner()
|
|
130
129
|
const isDefault = slugs.length === 0
|
|
131
|
-
|
|
130
|
+
console.log(pc.bgCyan(pc.black(' hookstack-cli ')))
|
|
131
|
+
console.log(isDefault ? '\nFetching default HookStack…' : `\nFetching ${plural(slugs.length, 'hook')}…`)
|
|
132
|
+
|
|
132
133
|
let data
|
|
133
134
|
try {
|
|
134
135
|
data = await fetchHooks(slugs)
|
|
135
136
|
} catch (e) {
|
|
136
|
-
|
|
137
|
-
p.cancel(e.message)
|
|
137
|
+
console.error(pc.red(`\n✗ Fetch failed: ${e.message}`))
|
|
138
138
|
process.exit(1)
|
|
139
139
|
}
|
|
140
140
|
const { hooks } = data
|
|
141
141
|
const notFound = slugs.filter(slug => !hooks.find(h => h.slug === slug))
|
|
142
|
-
|
|
143
|
-
?
|
|
144
|
-
:
|
|
145
|
-
if (notFound.length)
|
|
142
|
+
console.log(isDefault
|
|
143
|
+
? `✓ Default HookStack — ${plural(hooks.length, 'hook')}`
|
|
144
|
+
: `✓ Fetched ${plural(hooks.length, 'hook')}`)
|
|
145
|
+
if (notFound.length) console.warn(` ! Unknown slugs skipped: ${notFound.join(', ')}`)
|
|
146
146
|
if (hooks.length === 0) {
|
|
147
|
-
|
|
147
|
+
console.error('\n✗ No hooks to install.')
|
|
148
148
|
process.exit(1)
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
{ value: 'global', label: 'All my projects', hint: '~/.claude — every project on this machine' },
|
|
161
|
-
],
|
|
162
|
-
})
|
|
163
|
-
if (p.isCancel(scope)) { p.cancel('Cancelled.'); process.exit(0) }
|
|
151
|
+
// Scope selection
|
|
152
|
+
let scope = args.scope
|
|
153
|
+
console.log('\n Where to install?')
|
|
154
|
+
console.log(` ${pc.cyan('1')} This project ${pc.dim('./.claude — committed with your repo')}`)
|
|
155
|
+
console.log(` ${pc.cyan('2')} All my projects ${pc.dim('~/.claude — every project on this machine')}`)
|
|
156
|
+
const scopeAnswer = await ask(` → [${scope === 'global' ? '2' : '1'}]: `)
|
|
157
|
+
if (scopeAnswer === '2' || scopeAnswer === 'global') scope = 'global'
|
|
158
|
+
else if (scopeAnswer === 'q') { console.log('Cancelled.'); process.exit(0) }
|
|
159
|
+
else scope = 'project'
|
|
164
160
|
|
|
165
161
|
const dirs = resolveScopeRoot(scope, { cwd: process.cwd(), home: homedir() })
|
|
166
162
|
|
|
167
|
-
|
|
168
|
-
|
|
163
|
+
console.log(`\n ${pc.bold(`${plural(hooks.length, 'Hook')} to install`)}`)
|
|
164
|
+
console.log(summaryPanel(buildSummaryRows(hooks, { root: dirs.root })))
|
|
165
|
+
console.log(`\n ${pc.bold('Security')}`)
|
|
166
|
+
console.log(securityPanel(buildSecurityRows(hooks)))
|
|
169
167
|
|
|
170
|
-
const
|
|
171
|
-
if (
|
|
168
|
+
const confirmAnswer = await ask(`\n Install ${plural(hooks.length, 'hook')} into ${scope === 'global' ? '~/.claude' : './.claude'}? [Y/n]: `)
|
|
169
|
+
if (confirmAnswer.toLowerCase() === 'n' || confirmAnswer.toLowerCase() === 'no') {
|
|
170
|
+
console.log('Cancelled.')
|
|
171
|
+
process.exit(0)
|
|
172
|
+
}
|
|
172
173
|
|
|
173
|
-
|
|
174
|
-
s2.start('Installing…')
|
|
174
|
+
console.log('\nInstalling…')
|
|
175
175
|
let result
|
|
176
176
|
try {
|
|
177
|
-
result = doInstall(hooks, dirs, scope,
|
|
177
|
+
result = doInstall(hooks, dirs, scope, { warn: m => console.warn(` ! ${m}`) })
|
|
178
178
|
} catch (e) {
|
|
179
|
-
|
|
180
|
-
p.cancel(e.message)
|
|
179
|
+
console.error(pc.red(`\n✗ Install failed: ${e.message}`))
|
|
181
180
|
process.exit(1)
|
|
182
181
|
}
|
|
183
|
-
s2.stop(`Wrote ${plural(result.scriptCount, 'script')} + patched settings.json`)
|
|
184
182
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
183
|
+
console.log(` ✓ ${dirs.settingsPath}`)
|
|
184
|
+
console.log(`\n Browse more hooks → ${pc.cyan(`${API_BASE}/#catalogue`)}`)
|
|
185
|
+
console.log(` ⭐ star us → ${pc.cyan(REPO_URL)}`)
|
|
186
|
+
console.log(pc.green(`\n✓ ${plural(result.hookCount, 'hook')} installed — restart Claude Code to activate.\n`))
|
|
188
187
|
}
|
|
189
188
|
|
|
190
189
|
async function directInstall(slugs, args) {
|
|
@@ -248,7 +247,8 @@ async function main() {
|
|
|
248
247
|
else await directInstall(args.hooks, args)
|
|
249
248
|
}
|
|
250
249
|
|
|
251
|
-
/* v8 ignore next
|
|
252
|
-
|
|
253
|
-
|
|
250
|
+
/* v8 ignore next 4 */
|
|
251
|
+
const _argv1 = (() => { try { return realpathSync(process.argv[1]) } catch { return process.argv[1] } })()
|
|
252
|
+
if (_argv1 === fileURLToPath(import.meta.url)) {
|
|
253
|
+
main().catch(err => { console.error(pc.red(`\n✗ ${err.message || err}`)); process.exit(1) })
|
|
254
254
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hookstack-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "CLI installer for the Hookstack catalogue of Claude Code hooks",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
"node": ">=18"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@clack/prompts": "^1.0.0",
|
|
18
17
|
"picocolors": "^1.1.1"
|
|
19
18
|
},
|
|
20
19
|
"keywords": [
|