@claude-code-hooks/cli 0.1.8 → 0.1.9
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/package.json +2 -2
- package/src/cli.js +203 -100
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-code-hooks/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Wizard CLI to set up and manage @claude-code-hooks packages for Claude Code.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/beefiker/claude-code-hooks/tree/main/packages/cli",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"@claude-code-hooks/core": "0.1.3",
|
|
31
31
|
"@claude-code-hooks/security": "0.1.7",
|
|
32
32
|
"@claude-code-hooks/secrets": "0.1.8",
|
|
33
|
-
"@claude-code-hooks/sound": "0.2.
|
|
33
|
+
"@claude-code-hooks/sound": "0.2.11",
|
|
34
34
|
"@claude-code-hooks/notification": "0.1.5"
|
|
35
35
|
},
|
|
36
36
|
"keywords": [
|
package/src/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import process from 'node:process';
|
|
4
|
+
import readline from 'node:readline';
|
|
4
5
|
import fs from 'node:fs/promises';
|
|
5
6
|
import path from 'node:path';
|
|
6
7
|
|
|
@@ -16,6 +17,11 @@ import {
|
|
|
16
17
|
spinner
|
|
17
18
|
} from '@clack/prompts';
|
|
18
19
|
|
|
20
|
+
// Enable keypress events so we can intercept Backspace
|
|
21
|
+
if (process.stdin.isTTY) {
|
|
22
|
+
readline.emitKeypressEvents(process.stdin);
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
import {
|
|
20
26
|
ansi as pc,
|
|
21
27
|
configPathForScope,
|
|
@@ -42,6 +48,30 @@ function dieCancelled(msg = 'Cancelled') {
|
|
|
42
48
|
process.exit(0);
|
|
43
49
|
}
|
|
44
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Run a prompt with Backspace = go back. When Backspace is pressed, aborts and returns
|
|
53
|
+
* { wentBack: true }. ESC still exits. Caller must handle wentBack by continuing the loop.
|
|
54
|
+
* @param {AbortController} controller
|
|
55
|
+
* @param {() => Promise<T>} runPrompt - async function that runs the prompt (receives no args)
|
|
56
|
+
* @returns {Promise<{ result: T; wentBack: boolean }>}
|
|
57
|
+
*/
|
|
58
|
+
async function withBackspaceBack(controller, runPrompt) {
|
|
59
|
+
let wentBack = false;
|
|
60
|
+
const handler = (_str, key) => {
|
|
61
|
+
if (key?.name === 'backspace') {
|
|
62
|
+
wentBack = true;
|
|
63
|
+
controller.abort();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
process.stdin.on('keypress', handler);
|
|
67
|
+
try {
|
|
68
|
+
const result = await runPrompt();
|
|
69
|
+
return { result, wentBack };
|
|
70
|
+
} finally {
|
|
71
|
+
process.stdin.off('keypress', handler);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
45
75
|
function usage(exitCode = 0) {
|
|
46
76
|
process.stdout.write(`\
|
|
47
77
|
claude-code-hooks\n\nUsage:\n claude-code-hooks\n npx @claude-code-hooks/cli@latest\n\nWhat it does:\n - Update Claude Code settings (global), or generate a project-only config + pasteable snippet.\n`);
|
|
@@ -85,116 +115,189 @@ async function main() {
|
|
|
85
115
|
const projectDir = process.cwd();
|
|
86
116
|
|
|
87
117
|
intro('claude-code-hooks');
|
|
118
|
+
note(`${pc.dim('ESC')} to exit • ${pc.dim('Backspace')} to go back`, 'Navigation');
|
|
88
119
|
|
|
89
|
-
// ── Step 1–3: action, target, packages (
|
|
120
|
+
// ── Step 1–3: action, target, packages (Backspace = go to previous step) ──
|
|
90
121
|
let action;
|
|
91
122
|
let target;
|
|
92
123
|
let selected;
|
|
124
|
+
let step = 1;
|
|
125
|
+
/** @type {{ security: unknown; secrets: unknown; sound: unknown; notification: unknown }} */
|
|
126
|
+
let perPackage = { security: null, secrets: null, sound: null, notification: null };
|
|
93
127
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
{ value: 'exit', label: 'Exit' }
|
|
101
|
-
]
|
|
102
|
-
});
|
|
103
|
-
if (isCancel(action) || action === 'exit') dieCancelled('Bye');
|
|
104
|
-
|
|
105
|
-
target = await select({
|
|
106
|
-
message: `${pc.dim('Step 2/5')} Choose a target`,
|
|
107
|
-
options: [
|
|
108
|
-
{ value: 'global', label: `Global (default): ${pc.dim('~/.claude/settings.json')}` },
|
|
109
|
-
{ value: 'projectOnly', label: `Project-only: write ${pc.bold(CONFIG_FILENAME)} + print a snippet` },
|
|
110
|
-
{ value: '__back__', label: 'Back' }
|
|
111
|
-
]
|
|
112
|
-
});
|
|
113
|
-
if (isCancel(target)) dieCancelled();
|
|
114
|
-
if (target === '__back__') continue;
|
|
115
|
-
|
|
116
|
-
selected = await multiselect({
|
|
117
|
-
message: `${pc.dim('Step 3/5')} Select packages`,
|
|
118
|
-
options: [
|
|
119
|
-
{ value: 'security', label: '@claude-code-hooks/security', hint: 'Warn/block risky commands' },
|
|
120
|
-
{ value: 'secrets', label: '@claude-code-hooks/secrets', hint: 'Detect secret-like tokens' },
|
|
121
|
-
{ value: 'sound', label: '@claude-code-hooks/sound', hint: 'Play sounds for key events' },
|
|
122
|
-
{ value: 'notification', label: '@claude-code-hooks/notification', hint: 'OS notifications for key events' }
|
|
123
|
-
],
|
|
124
|
-
required: true
|
|
125
|
-
});
|
|
126
|
-
if (isCancel(selected)) dieCancelled();
|
|
128
|
+
const packageOptions = [
|
|
129
|
+
{ value: 'security', label: '@claude-code-hooks/security', hint: 'Warn/block risky commands' },
|
|
130
|
+
{ value: 'secrets', label: '@claude-code-hooks/secrets', hint: 'Detect secret-like tokens' },
|
|
131
|
+
{ value: 'sound', label: '@claude-code-hooks/sound', hint: 'Play sounds for key events' },
|
|
132
|
+
{ value: 'notification', label: '@claude-code-hooks/notification', hint: 'OS notifications for key events' }
|
|
133
|
+
];
|
|
127
134
|
|
|
128
|
-
|
|
129
|
-
if (
|
|
130
|
-
|
|
135
|
+
while (true) {
|
|
136
|
+
if (step === 1) {
|
|
137
|
+
action = await select({
|
|
138
|
+
message: `${pc.dim('Step 1/5')} Choose an action`,
|
|
139
|
+
options: [
|
|
140
|
+
{ value: 'setup', label: 'Install / update packages' },
|
|
141
|
+
{ value: 'uninstall', label: 'Uninstall (remove managed hooks)' },
|
|
142
|
+
{ value: 'exit', label: 'Exit' }
|
|
143
|
+
]
|
|
144
|
+
});
|
|
145
|
+
if (isCancel(action) || action === 'exit') dieCancelled('Bye');
|
|
146
|
+
step = 2;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (step === 2) {
|
|
150
|
+
const targetCtrl = new AbortController();
|
|
151
|
+
const { result: targetResult, wentBack: targetBack } = await withBackspaceBack(targetCtrl, () =>
|
|
152
|
+
select({
|
|
153
|
+
message: `${pc.dim('Step 2/5')} Choose a target`,
|
|
154
|
+
options: [
|
|
155
|
+
{ value: 'global', label: `Global (default): ${pc.dim('~/.claude/settings.json')}` },
|
|
156
|
+
{ value: 'projectOnly', label: `Project-only: write ${pc.bold(CONFIG_FILENAME)} + print a snippet` }
|
|
157
|
+
],
|
|
158
|
+
signal: targetCtrl.signal
|
|
159
|
+
})
|
|
160
|
+
);
|
|
161
|
+
if (targetBack) {
|
|
162
|
+
step = 1;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (isCancel(targetResult)) dieCancelled();
|
|
166
|
+
target = targetResult;
|
|
167
|
+
step = 3;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (step === 3) {
|
|
171
|
+
const pkgsCtrl = new AbortController();
|
|
172
|
+
const { result: pkgsResult, wentBack: pkgsBack } = await withBackspaceBack(pkgsCtrl, () =>
|
|
173
|
+
multiselect({
|
|
174
|
+
message: `${pc.dim('Step 3/5')} Select packages`,
|
|
175
|
+
options: packageOptions,
|
|
176
|
+
required: true,
|
|
177
|
+
signal: pkgsCtrl.signal
|
|
178
|
+
})
|
|
179
|
+
);
|
|
180
|
+
if (pkgsBack) {
|
|
181
|
+
step = 2;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (isCancel(pkgsResult)) dieCancelled();
|
|
185
|
+
selected = pkgsResult;
|
|
186
|
+
step = 4;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (step === 4) {
|
|
190
|
+
const proceedCtrl = new AbortController();
|
|
191
|
+
const { result: proceedResult, wentBack: proceedBack } = await withBackspaceBack(proceedCtrl, () =>
|
|
192
|
+
confirm({ message: 'Configure these packages now?', initialValue: true, signal: proceedCtrl.signal })
|
|
193
|
+
);
|
|
194
|
+
if (proceedBack) {
|
|
195
|
+
step = 3;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (isCancel(proceedResult)) dieCancelled();
|
|
199
|
+
if (!proceedResult) {
|
|
200
|
+
step = 2;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
step = 5;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (step !== 5) continue;
|
|
207
|
+
|
|
208
|
+
// Build per-package plan/config
|
|
209
|
+
perPackage = { security: null, secrets: null, sound: null, notification: null };
|
|
210
|
+
|
|
211
|
+
const packageDescs = {
|
|
212
|
+
security: 'Warn/block risky commands',
|
|
213
|
+
secrets: 'Detect secret-like tokens',
|
|
214
|
+
sound: 'Play sounds for key events',
|
|
215
|
+
notification: 'OS notifications for key events'
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const formatPackageList = (/** @type {string | null} */ highlightKey) =>
|
|
219
|
+
selected
|
|
220
|
+
.map(
|
|
221
|
+
(k) => {
|
|
222
|
+
const desc = pc.dim(packageDescs[k] || '');
|
|
223
|
+
const label = highlightKey === k ? pc.cyan(pc.bold(k)) : pc.bold(k);
|
|
224
|
+
return `${label}: ${desc}`;
|
|
225
|
+
}
|
|
226
|
+
)
|
|
227
|
+
.join('\n');
|
|
228
|
+
|
|
229
|
+
// ── Step 4/5: configure (highlight current package) ──
|
|
230
|
+
if (selected.includes('security')) {
|
|
231
|
+
note(formatPackageList('security'), `${pc.dim('Step 4/5')} Configure packages`);
|
|
232
|
+
perPackage.security = await planSecuritySetup({ action, projectDir, ui: 'umbrella' });
|
|
233
|
+
}
|
|
234
|
+
if (selected.includes('secrets')) {
|
|
235
|
+
note(formatPackageList('secrets'), `${pc.dim('Step 4/5')} Configure packages`);
|
|
236
|
+
perPackage.secrets = await planSecretsSetup({ action, projectDir, ui: 'umbrella' });
|
|
237
|
+
}
|
|
238
|
+
if (selected.includes('sound')) {
|
|
239
|
+
note(formatPackageList('sound'), `${pc.dim('Step 4/5')} Configure packages`);
|
|
240
|
+
perPackage.sound = await planSoundSetup({ action, projectDir, ui: 'umbrella' });
|
|
241
|
+
}
|
|
242
|
+
if (selected.includes('notification')) {
|
|
243
|
+
note(formatPackageList('notification'), `${pc.dim('Step 4/5')} Configure packages`);
|
|
244
|
+
perPackage.notification = await planNotificationSetup({ action, projectDir, ui: 'umbrella' });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ── Step 5/5: review ──
|
|
248
|
+
const files = [];
|
|
249
|
+
if (target === 'global') files.push(configPathForScope('global', projectDir));
|
|
250
|
+
if (target === 'projectOnly') {
|
|
251
|
+
files.push(path.join(projectDir, CONFIG_FILENAME));
|
|
252
|
+
files.push(path.join(projectDir, 'claude-code-hooks.snippet.json (optional)'));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function summarizePlan(key, plan) {
|
|
256
|
+
if (!plan) return `${key}: (skipped)`;
|
|
257
|
+
if (action === 'uninstall') return `${key}: remove managed hooks`;
|
|
258
|
+
|
|
259
|
+
const events = plan.snippetHooks ? Object.keys(plan.snippetHooks) : [];
|
|
260
|
+
const list = events.slice(0, 5);
|
|
261
|
+
const tail = events.length > 5 ? ` +${events.length - 5} more` : '';
|
|
262
|
+
return `${key}: ${events.length} event(s)${events.length ? ` (${list.join(', ')}${tail})` : ''}`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
note(
|
|
266
|
+
[
|
|
267
|
+
`${pc.dim('Step 5/5')} Review`,
|
|
268
|
+
'',
|
|
269
|
+
`Action: ${pc.bold(action)}`,
|
|
270
|
+
`Target: ${pc.bold(target === 'global' ? 'global settings' : 'project-only')}`,
|
|
271
|
+
'',
|
|
272
|
+
`${pc.bold('Packages')}`,
|
|
273
|
+
...selected.map((k) => ` - ${summarizePlan(k, perPackage[k])}`),
|
|
274
|
+
'',
|
|
275
|
+
`${pc.bold('Files')}`,
|
|
276
|
+
...files.map((f) => ` - ${f}`)
|
|
277
|
+
].join('\n'),
|
|
278
|
+
'Review'
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const applyCtrl = new AbortController();
|
|
282
|
+
const { result: applyResult, wentBack: applyBack } = await withBackspaceBack(applyCtrl, () =>
|
|
283
|
+
select({
|
|
284
|
+
message: 'Apply changes?',
|
|
285
|
+
options: [
|
|
286
|
+
{ value: 'yes', label: 'Yes, apply' },
|
|
287
|
+
{ value: 'cancel', label: 'Cancel (exit)' }
|
|
288
|
+
],
|
|
289
|
+
signal: applyCtrl.signal
|
|
290
|
+
})
|
|
291
|
+
);
|
|
292
|
+
if (applyBack) {
|
|
293
|
+
step = 3;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (isCancel(applyResult) || applyResult === 'cancel') dieCancelled('No changes made');
|
|
131
297
|
|
|
132
298
|
break;
|
|
133
299
|
}
|
|
134
300
|
|
|
135
|
-
// Build per-package plan/config
|
|
136
|
-
const perPackage = { security: null, secrets: null, sound: null, notification: null };
|
|
137
|
-
|
|
138
|
-
// ── Step 4/5: configure ──
|
|
139
|
-
note(
|
|
140
|
-
selected
|
|
141
|
-
.map(
|
|
142
|
-
(k) =>
|
|
143
|
-
`${pc.bold(k)}: ${pc.dim(
|
|
144
|
-
{
|
|
145
|
-
security: 'Warn/block risky commands',
|
|
146
|
-
secrets: 'Detect secret-like tokens',
|
|
147
|
-
sound: 'Play sounds for key events',
|
|
148
|
-
notification: 'OS notifications for key events'
|
|
149
|
-
}[k] || ''
|
|
150
|
-
)}`
|
|
151
|
-
)
|
|
152
|
-
.join('\n'),
|
|
153
|
-
`${pc.dim('Step 4/5')} Configure packages`
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
if (selected.includes('security')) perPackage.security = await planSecuritySetup({ action, projectDir, ui: 'umbrella' });
|
|
157
|
-
if (selected.includes('secrets')) perPackage.secrets = await planSecretsSetup({ action, projectDir, ui: 'umbrella' });
|
|
158
|
-
if (selected.includes('sound')) perPackage.sound = await planSoundSetup({ action, projectDir, ui: 'umbrella' });
|
|
159
|
-
if (selected.includes('notification')) perPackage.notification = await planNotificationSetup({ action, projectDir, ui: 'umbrella' });
|
|
160
|
-
|
|
161
|
-
// ── Step 5/5: review ──
|
|
162
|
-
const files = [];
|
|
163
|
-
if (target === 'global') files.push(configPathForScope('global', projectDir));
|
|
164
|
-
if (target === 'projectOnly') {
|
|
165
|
-
files.push(path.join(projectDir, CONFIG_FILENAME));
|
|
166
|
-
files.push(path.join(projectDir, 'claude-code-hooks.snippet.json (optional)'));
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function summarizePlan(key, plan) {
|
|
170
|
-
if (!plan) return `${key}: (skipped)`;
|
|
171
|
-
if (action === 'uninstall') return `${key}: remove managed hooks`;
|
|
172
|
-
|
|
173
|
-
const events = plan.snippetHooks ? Object.keys(plan.snippetHooks) : [];
|
|
174
|
-
const list = events.slice(0, 5);
|
|
175
|
-
const tail = events.length > 5 ? ` +${events.length - 5} more` : '';
|
|
176
|
-
return `${key}: ${events.length} event(s)${events.length ? ` (${list.join(', ')}${tail})` : ''}`;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
note(
|
|
180
|
-
[
|
|
181
|
-
`${pc.dim('Step 5/5')} Review`,
|
|
182
|
-
'',
|
|
183
|
-
`Action: ${pc.bold(action)}`,
|
|
184
|
-
`Target: ${pc.bold(target === 'global' ? 'global settings' : 'project-only')}`,
|
|
185
|
-
'',
|
|
186
|
-
`${pc.bold('Packages')}`,
|
|
187
|
-
...selected.map((k) => ` - ${summarizePlan(k, perPackage[k])}`),
|
|
188
|
-
'',
|
|
189
|
-
`${pc.bold('Files')}`,
|
|
190
|
-
...files.map((f) => ` - ${f}`)
|
|
191
|
-
].join('\n'),
|
|
192
|
-
'Review'
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
const ok = await confirm({ message: 'Apply changes?', initialValue: true });
|
|
196
|
-
if (isCancel(ok) || !ok) dieCancelled('No changes made');
|
|
197
|
-
|
|
198
301
|
if (target === 'projectOnly') {
|
|
199
302
|
// Write project config
|
|
200
303
|
const s = spinner();
|