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.
Files changed (2) hide show
  1. package/bin/cli.mjs +45 -45
  2. 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
- s.start(isDefault ? 'Fetching default HookStack…' : `Fetching ${plural(slugs.length, 'hook')}`)
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
- s.stop(pc.red('Fetch failed'))
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
- s.stop(isDefault
143
- ? `Default HookStack — ${plural(hooks.length, 'hook')}`
144
- : `Fetched ${plural(hooks.length, 'hook')}`)
145
- if (notFound.length) p.log.warn(`Unknown slugs skipped: ${notFound.join(', ')}`)
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
- p.cancel('No hooks to install.')
147
+ console.error('\n✗ No hooks to install.')
148
148
  process.exit(1)
149
149
  }
150
150
 
151
- if (isDefault) {
152
- p.log.info(`The default HookStack gives your Claude Code setup ${plural(hooks.length, 'battle-tested hook')} covering security, context, validation and workflow.`)
153
- }
154
-
155
- const scope = await p.select({
156
- message: 'Where do you want to install?',
157
- initialValue: args.scope,
158
- options: [
159
- { value: 'project', label: 'This project', hint: './.claude — committed with your repo' },
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
- p.note(summaryPanel(buildSummaryRows(hooks, { root: dirs.root })), `${plural(hooks.length, 'Hook')} to install`)
168
- p.note(securityPanel(buildSecurityRows(hooks)), 'Security')
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 ok = await p.confirm({ message: `Install ${plural(hooks.length, 'hook')} into ${scope === 'global' ? '~/.claude' : './.claude'}?` })
171
- if (p.isCancel(ok) || !ok) { p.cancel('Cancelled.'); process.exit(0) }
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
- const s2 = p.spinner()
174
- s2.start('Installing…')
174
+ console.log('\nInstalling…')
175
175
  let result
176
176
  try {
177
- result = doInstall(hooks, dirs, scope, p.log)
177
+ result = doInstall(hooks, dirs, scope, { warn: m => console.warn(` ! ${m}`) })
178
178
  } catch (e) {
179
- s2.stop(pc.red('Install failed'))
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
- p.log.info(`Browse more hooks → ${pc.cyan(`${API_BASE}/#catalogue`)}`)
186
- p.log.info(`⭐ star us → ${pc.cyan(REPO_URL)}`)
187
- p.outro(pc.green(`✓ ${plural(result.hookCount, 'hook')} installed — restart Claude Code to activate.`))
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 3 */
252
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
253
- main()
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.10",
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": [