@claude-code-hooks/cli 0.1.7 → 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/README.md +238 -0
- package/package.json +6 -6
- package/src/cli.js +208 -101
package/README.md
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# @claude-code-hooks/cli
|
|
2
|
+
|
|
3
|
+
> Part of the [claude-code-hooks](../../README.md) monorepo.
|
|
4
|
+
|
|
5
|
+
Umbrella wizard CLI to set up and manage `@claude-code-hooks` packages for Claude Code (sound, notification, security, secrets).
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Install / run
|
|
10
|
+
|
|
11
|
+
From anywhere:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx @claude-code-hooks/cli@latest
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
You'll be prompted to choose which packages to configure and where to write settings:
|
|
18
|
+
|
|
19
|
+
- Project (shared): `.claude/settings.json`
|
|
20
|
+
- Project (local): `.claude/settings.local.json`
|
|
21
|
+
- Global: `~/.claude/settings.json`
|
|
22
|
+
|
|
23
|
+
Then you can enable/disable each package (sound, notification, security, secrets) and customize their options.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## @claude-code-hooks/sound
|
|
28
|
+
|
|
29
|
+
Cross-platform CLI (macOS, Windows, Linux) that configures **Claude Code Hooks** to play **bundled sounds**.
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
- Setup UI: `npx @claude-code-hooks/sound@latest`
|
|
34
|
+
- Hook runner: `npx --yes @claude-code-hooks/sound@latest play --event <Event> --sound <SoundId> --managed-by @claude-code-hooks/sound`
|
|
35
|
+
|
|
36
|
+
### Install / run
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx @claude-code-hooks/sound@latest
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
You'll be prompted to choose where to write settings, then enable/disable events and choose a sound per event. Selecting a sound plays a quick preview. Choose **Create my own** to generate custom text-to-speech sounds, or **Import from file** to add your own MP3/WAV files.
|
|
43
|
+
|
|
44
|
+
### Commands
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
claude-sound list-events
|
|
48
|
+
claude-sound list-sounds
|
|
49
|
+
claude-sound play --sound ring1
|
|
50
|
+
claude-sound import <path> # Import MP3/WAV into ~/.claude-sound/sounds/
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Uninstall / remove hooks
|
|
54
|
+
|
|
55
|
+
Run the setup again and choose **Remove all claude-sound hooks**, then **Apply**. Or manually delete any hook handlers whose command contains:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
--managed-by @claude-code-hooks/sound
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Create my own (text-to-speech)
|
|
62
|
+
|
|
63
|
+
When picking a sound, choose **Create my own** to generate custom sounds from text. Supports English (default) and Korean. See [packages/sound/docs/TTS.md](../sound/docs/TTS.md) for details.
|
|
64
|
+
|
|
65
|
+
### Import from file
|
|
66
|
+
|
|
67
|
+
Choose **Import from file** and enter a path to an MP3 or WAV file. Or use the CLI:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
claude-sound import ./my-notification.mp3
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Supported formats: MP3, WAV. Max file size: 5MB.
|
|
74
|
+
|
|
75
|
+
### Platform support
|
|
76
|
+
|
|
77
|
+
| Platform | Audio player | Notes |
|
|
78
|
+
|----------|--------------|-------|
|
|
79
|
+
| **macOS** | `afplay` | Built-in, no setup needed |
|
|
80
|
+
| **Windows** | `ffplay`, `mpv`, `mpg123`, or PowerShell | Install [ffmpeg](https://ffmpeg.org/) or [mpv](https://mpv.io/) for best support. PowerShell plays WAV only. |
|
|
81
|
+
| **Linux** | `ffplay`, `mpv`, `mpg123`, `aplay`, etc. | Install ffmpeg or mpv for MP3 support. |
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## @claude-code-hooks/notification
|
|
86
|
+
|
|
87
|
+
**Zero-dependency** CLI that sends OS-level notifications from Claude Code hooks.
|
|
88
|
+
|
|
89
|
+
- **macOS**: `osascript` (`display notification`)
|
|
90
|
+
- **Linux**: `notify-send` (libnotify)
|
|
91
|
+
- **Windows**: PowerShell toast via Windows Runtime types (no external modules)
|
|
92
|
+
|
|
93
|
+
Falls back to stdout when no GUI environment is detected (SSH, headless, CI).
|
|
94
|
+
|
|
95
|
+
### Install / run
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npx --yes @claude-code-hooks/notification@latest --event Stop
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### CLI options
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
--title <text> Notification title (default: derived from --event or "Claude Code")
|
|
105
|
+
--message <text> Notification body (default: derived from --event or stdin JSON)
|
|
106
|
+
--event <name> Hook event name (e.g. Stop, Notification, TaskCompleted)
|
|
107
|
+
--dry-run Build the command but don't execute it (prints JSON to stdout)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Usage as a Claude Code hook
|
|
111
|
+
|
|
112
|
+
Add to your Claude Code `settings.json`:
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"hooks": {
|
|
117
|
+
"Stop": [
|
|
118
|
+
{
|
|
119
|
+
"matcher": "*",
|
|
120
|
+
"hooks": [
|
|
121
|
+
{
|
|
122
|
+
"type": "command",
|
|
123
|
+
"command": "npx --yes @claude-code-hooks/notification@latest --event Stop",
|
|
124
|
+
"timeout": 8
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Hook event types
|
|
134
|
+
|
|
135
|
+
| Event | Description |
|
|
136
|
+
|-------|-------------|
|
|
137
|
+
| `SessionStart` | Session begins |
|
|
138
|
+
| `Stop` | Claude stops generating |
|
|
139
|
+
| `TaskCompleted` | Task is complete |
|
|
140
|
+
| `Notification` | Claude wants your attention |
|
|
141
|
+
| `PostToolUseFailure` | After a tool fails |
|
|
142
|
+
| ... and more |
|
|
143
|
+
|
|
144
|
+
### Platform support
|
|
145
|
+
|
|
146
|
+
| Platform | Method | Requirements |
|
|
147
|
+
|----------|--------|--------------|
|
|
148
|
+
| **macOS** | `osascript` | Built-in |
|
|
149
|
+
| **Linux** | `notify-send` | Install `libnotify-bin` (apt) or `libnotify` (pacman/dnf) |
|
|
150
|
+
| **Windows** | PowerShell toast | Built-in PowerShell 5+ |
|
|
151
|
+
| **Other/Headless** | stdout | Prints `[notification] Title: Message` to stdout |
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## @claude-code-hooks/security
|
|
156
|
+
|
|
157
|
+
Warns (or optionally **blocks**) risky commands/tool invocations. Heuristic and lightweight: scans for suspicious patterns like `rm -rf`, `curl | bash`, writes to `~/.ssh`, etc.
|
|
158
|
+
|
|
159
|
+
### Install / run
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
npx @claude-code-hooks/security@latest
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Project config: claude-code-hooks.config.json
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"security": {
|
|
170
|
+
"mode": "warn",
|
|
171
|
+
"enabledEvents": ["PreToolUse", "PermissionRequest"],
|
|
172
|
+
"ignore": { "regex": [] },
|
|
173
|
+
"allow": { "regex": [] }
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
- `allow.regex`: if any pattern matches, **all risks are suppressed**
|
|
179
|
+
- `ignore.regex`: if any pattern matches, risks are suppressed and a dim note is printed
|
|
180
|
+
|
|
181
|
+
### Modes
|
|
182
|
+
|
|
183
|
+
- `warn` (default): prints warnings to stderr, exits 0
|
|
184
|
+
- `block`: exits 2 when a risk is detected (**PreToolUse only**; `PermissionRequest` stays advisory)
|
|
185
|
+
|
|
186
|
+
### Commands
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
claude-security list-events
|
|
190
|
+
claude-security run --event PreToolUse --mode warn
|
|
191
|
+
claude-security doctor
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## @claude-code-hooks/secrets
|
|
197
|
+
|
|
198
|
+
Warns when **secret-like tokens** appear in tool inputs — and optionally scans **staged git files** before commits.
|
|
199
|
+
|
|
200
|
+
### Install / run
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
npx @claude-code-hooks/secrets@latest
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Project config: claude-code-hooks.config.json
|
|
207
|
+
|
|
208
|
+
```json
|
|
209
|
+
{
|
|
210
|
+
"secrets": {
|
|
211
|
+
"mode": "warn",
|
|
212
|
+
"enabledEvents": ["PreToolUse", "PermissionRequest"],
|
|
213
|
+
"scanGitCommit": false,
|
|
214
|
+
"ignore": { "regex": [] },
|
|
215
|
+
"allow": { "regex": [] }
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
- `scanGitCommit`: when `true`, intercepts `git commit` and scans staged files for secret patterns
|
|
221
|
+
|
|
222
|
+
### Modes
|
|
223
|
+
|
|
224
|
+
- `warn` (default): prints warnings to stderr, exits 0
|
|
225
|
+
- `block`: exits 2 **only for HIGH confidence** findings (private key material)
|
|
226
|
+
|
|
227
|
+
### What it detects
|
|
228
|
+
|
|
229
|
+
- **HIGH**: `-----BEGIN (RSA|OPENSSH|EC|PGP|DSA|ENCRYPTED) PRIVATE KEY-----`
|
|
230
|
+
- **MED**: OpenAI `sk-...`, GitHub `ghp_...` / `github_pat_...`, AWS `AKIA...`, Slack `xox*`, Google `AIza...`, Stripe `sk_live_...`, npm `npm_...`, PyPI `pypi-...`, DB URLs with credentials, etc.
|
|
231
|
+
|
|
232
|
+
### Commands
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
claude-secrets list-events
|
|
236
|
+
claude-secrets run --event PreToolUse --mode warn
|
|
237
|
+
claude-secrets doctor
|
|
238
|
+
```
|
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",
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@clack/prompts": "^1.0.0",
|
|
30
|
-
"@claude-code-hooks/core": "0.1.
|
|
31
|
-
"@claude-code-hooks/security": "0.1.
|
|
32
|
-
"@claude-code-hooks/secrets": "0.1.
|
|
33
|
-
"@claude-code-hooks/sound": "0.2.
|
|
34
|
-
"@claude-code-hooks/notification": "0.1.
|
|
30
|
+
"@claude-code-hooks/core": "0.1.3",
|
|
31
|
+
"@claude-code-hooks/security": "0.1.7",
|
|
32
|
+
"@claude-code-hooks/secrets": "0.1.8",
|
|
33
|
+
"@claude-code-hooks/sound": "0.2.11",
|
|
34
|
+
"@claude-code-hooks/notification": "0.1.5"
|
|
35
35
|
},
|
|
36
36
|
"keywords": [
|
|
37
37
|
"claude",
|
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,
|
|
@@ -24,7 +30,8 @@ import {
|
|
|
24
30
|
CONFIG_FILENAME,
|
|
25
31
|
configFilePath,
|
|
26
32
|
readProjectConfig,
|
|
27
|
-
writeProjectConfig
|
|
33
|
+
writeProjectConfig,
|
|
34
|
+
removeLegacyClaudeSoundHooks
|
|
28
35
|
} from '@claude-code-hooks/core';
|
|
29
36
|
|
|
30
37
|
import { buildSettingsSnippet } from './snippet.js';
|
|
@@ -41,6 +48,30 @@ function dieCancelled(msg = 'Cancelled') {
|
|
|
41
48
|
process.exit(0);
|
|
42
49
|
}
|
|
43
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
|
+
|
|
44
75
|
function usage(exitCode = 0) {
|
|
45
76
|
process.stdout.write(`\
|
|
46
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`);
|
|
@@ -84,116 +115,189 @@ async function main() {
|
|
|
84
115
|
const projectDir = process.cwd();
|
|
85
116
|
|
|
86
117
|
intro('claude-code-hooks');
|
|
118
|
+
note(`${pc.dim('ESC')} to exit • ${pc.dim('Backspace')} to go back`, 'Navigation');
|
|
87
119
|
|
|
88
|
-
// ── Step 1–3: action, target, packages (
|
|
120
|
+
// ── Step 1–3: action, target, packages (Backspace = go to previous step) ──
|
|
89
121
|
let action;
|
|
90
122
|
let target;
|
|
91
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 };
|
|
92
127
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
{ value: 'exit', label: 'Exit' }
|
|
100
|
-
]
|
|
101
|
-
});
|
|
102
|
-
if (isCancel(action) || action === 'exit') dieCancelled('Bye');
|
|
103
|
-
|
|
104
|
-
target = await select({
|
|
105
|
-
message: `${pc.dim('Step 2/5')} Choose a target`,
|
|
106
|
-
options: [
|
|
107
|
-
{ value: 'global', label: `Global (default): ${pc.dim('~/.claude/settings.json')}` },
|
|
108
|
-
{ value: 'projectOnly', label: `Project-only: write ${pc.bold(CONFIG_FILENAME)} + print a snippet` },
|
|
109
|
-
{ value: '__back__', label: 'Back' }
|
|
110
|
-
]
|
|
111
|
-
});
|
|
112
|
-
if (isCancel(target)) dieCancelled();
|
|
113
|
-
if (target === '__back__') continue;
|
|
114
|
-
|
|
115
|
-
selected = await multiselect({
|
|
116
|
-
message: `${pc.dim('Step 3/5')} Select packages`,
|
|
117
|
-
options: [
|
|
118
|
-
{ value: 'security', label: '@claude-code-hooks/security', hint: 'Warn/block risky commands' },
|
|
119
|
-
{ value: 'secrets', label: '@claude-code-hooks/secrets', hint: 'Detect secret-like tokens' },
|
|
120
|
-
{ value: 'sound', label: '@claude-code-hooks/sound', hint: 'Play sounds for key events' },
|
|
121
|
-
{ value: 'notification', label: '@claude-code-hooks/notification', hint: 'OS notifications for key events' }
|
|
122
|
-
],
|
|
123
|
-
required: true
|
|
124
|
-
});
|
|
125
|
-
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
|
+
];
|
|
126
134
|
|
|
127
|
-
|
|
128
|
-
if (
|
|
129
|
-
|
|
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');
|
|
130
297
|
|
|
131
298
|
break;
|
|
132
299
|
}
|
|
133
300
|
|
|
134
|
-
// Build per-package plan/config
|
|
135
|
-
const perPackage = { security: null, secrets: null, sound: null, notification: null };
|
|
136
|
-
|
|
137
|
-
// ── Step 4/5: configure ──
|
|
138
|
-
note(
|
|
139
|
-
selected
|
|
140
|
-
.map(
|
|
141
|
-
(k) =>
|
|
142
|
-
`${pc.bold(k)}: ${pc.dim(
|
|
143
|
-
{
|
|
144
|
-
security: 'Warn/block risky commands',
|
|
145
|
-
secrets: 'Detect secret-like tokens',
|
|
146
|
-
sound: 'Play sounds for key events',
|
|
147
|
-
notification: 'OS notifications for key events'
|
|
148
|
-
}[k] || ''
|
|
149
|
-
)}`
|
|
150
|
-
)
|
|
151
|
-
.join('\n'),
|
|
152
|
-
`${pc.dim('Step 4/5')} Configure packages`
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
if (selected.includes('security')) perPackage.security = await planSecuritySetup({ action, projectDir, ui: 'umbrella' });
|
|
156
|
-
if (selected.includes('secrets')) perPackage.secrets = await planSecretsSetup({ action, projectDir, ui: 'umbrella' });
|
|
157
|
-
if (selected.includes('sound')) perPackage.sound = await planSoundSetup({ action, projectDir, ui: 'umbrella' });
|
|
158
|
-
if (selected.includes('notification')) perPackage.notification = await planNotificationSetup({ action, projectDir, ui: 'umbrella' });
|
|
159
|
-
|
|
160
|
-
// ── Step 5/5: review ──
|
|
161
|
-
const files = [];
|
|
162
|
-
if (target === 'global') files.push(configPathForScope('global', projectDir));
|
|
163
|
-
if (target === 'projectOnly') {
|
|
164
|
-
files.push(path.join(projectDir, CONFIG_FILENAME));
|
|
165
|
-
files.push(path.join(projectDir, 'claude-code-hooks.snippet.json (optional)'));
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function summarizePlan(key, plan) {
|
|
169
|
-
if (!plan) return `${key}: (skipped)`;
|
|
170
|
-
if (action === 'uninstall') return `${key}: remove managed hooks`;
|
|
171
|
-
|
|
172
|
-
const events = plan.snippetHooks ? Object.keys(plan.snippetHooks) : [];
|
|
173
|
-
const list = events.slice(0, 5);
|
|
174
|
-
const tail = events.length > 5 ? ` +${events.length - 5} more` : '';
|
|
175
|
-
return `${key}: ${events.length} event(s)${events.length ? ` (${list.join(', ')}${tail})` : ''}`;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
note(
|
|
179
|
-
[
|
|
180
|
-
`${pc.dim('Step 5/5')} Review`,
|
|
181
|
-
'',
|
|
182
|
-
`Action: ${pc.bold(action)}`,
|
|
183
|
-
`Target: ${pc.bold(target === 'global' ? 'global settings' : 'project-only')}`,
|
|
184
|
-
'',
|
|
185
|
-
`${pc.bold('Packages')}`,
|
|
186
|
-
...selected.map((k) => ` - ${summarizePlan(k, perPackage[k])}`),
|
|
187
|
-
'',
|
|
188
|
-
`${pc.bold('Files')}`,
|
|
189
|
-
...files.map((f) => ` - ${f}`)
|
|
190
|
-
].join('\n'),
|
|
191
|
-
'Review'
|
|
192
|
-
);
|
|
193
|
-
|
|
194
|
-
const ok = await confirm({ message: 'Apply changes?', initialValue: true });
|
|
195
|
-
if (isCancel(ok) || !ok) dieCancelled('No changes made');
|
|
196
|
-
|
|
197
301
|
if (target === 'projectOnly') {
|
|
198
302
|
// Write project config
|
|
199
303
|
const s = spinner();
|
|
@@ -247,6 +351,9 @@ async function main() {
|
|
|
247
351
|
settings = await plan.applyToSettings(settings);
|
|
248
352
|
}
|
|
249
353
|
|
|
354
|
+
// Remove legacy claude-sound hooks (from old standalone package) to avoid duplicates
|
|
355
|
+
settings = removeLegacyClaudeSoundHooks(settings);
|
|
356
|
+
|
|
250
357
|
await writeJson(settingsPath, settings);
|
|
251
358
|
|
|
252
359
|
// Update project config only on setup.
|