claude-code-sounds 1.0.0 → 1.1.0
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 +12 -3
- package/bin/cli.js +420 -65
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,15 @@ Ships with a **WC3 Orc Peon** theme. Bring your own sounds or create new themes.
|
|
|
20
20
|
npx claude-code-sounds
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
The interactive installer checks dependencies, lets you pick a theme, and optionally customize which sounds map to each hook — all in the terminal.
|
|
24
|
+
|
|
25
|
+
Requires macOS (uses `afplay`) and Node.js 16+.
|
|
26
|
+
|
|
27
|
+
For scripted or CI usage, skip all prompts with `--yes`:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx claude-code-sounds --yes
|
|
31
|
+
```
|
|
24
32
|
|
|
25
33
|
<details>
|
|
26
34
|
<summary>Alternative: install from source</summary>
|
|
@@ -38,10 +46,11 @@ The bash installer requires `jq` (`brew install jq`).
|
|
|
38
46
|
## Usage
|
|
39
47
|
|
|
40
48
|
```bash
|
|
41
|
-
npx claude-code-sounds #
|
|
42
|
-
npx claude-code-sounds
|
|
49
|
+
npx claude-code-sounds # Interactive install
|
|
50
|
+
npx claude-code-sounds --yes # Install defaults, skip all prompts
|
|
43
51
|
npx claude-code-sounds --list # List available themes
|
|
44
52
|
npx claude-code-sounds --uninstall # Remove all sounds and hooks
|
|
53
|
+
npx claude-code-sounds --help # Show help
|
|
45
54
|
```
|
|
46
55
|
|
|
47
56
|
## WC3 Orc Peon Theme
|
package/bin/cli.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
const fs = require("fs");
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const os = require("os");
|
|
6
|
-
const
|
|
6
|
+
const readline = require("readline");
|
|
7
|
+
const { execSync, spawn } = require("child_process");
|
|
7
8
|
|
|
8
9
|
// ─── Paths ───────────────────────────────────────────────────────────────────
|
|
9
10
|
|
|
@@ -17,11 +18,11 @@ const THEMES_DIR = path.join(PKG_DIR, "themes");
|
|
|
17
18
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
18
19
|
|
|
19
20
|
function print(msg = "") {
|
|
20
|
-
|
|
21
|
+
process.stdout.write(msg + "\n");
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
function die(msg) {
|
|
24
|
-
console.error(
|
|
25
|
+
console.error(`\n Error: ${msg}\n`);
|
|
25
26
|
process.exit(1);
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -56,6 +57,248 @@ function writeSettings(settings) {
|
|
|
56
57
|
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n");
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
function hasCommand(name) {
|
|
61
|
+
try {
|
|
62
|
+
exec(`which ${name}`);
|
|
63
|
+
return true;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── ANSI helpers ────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
const CSI = "\x1b[";
|
|
72
|
+
const CLEAR_LINE = `${CSI}2K`;
|
|
73
|
+
const HIDE_CURSOR = `${CSI}?25l`;
|
|
74
|
+
const SHOW_CURSOR = `${CSI}?25h`;
|
|
75
|
+
const BOLD = `${CSI}1m`;
|
|
76
|
+
const DIM = `${CSI}2m`;
|
|
77
|
+
const RESET = `${CSI}0m`;
|
|
78
|
+
const GREEN = `${CSI}32m`;
|
|
79
|
+
const RED = `${CSI}31m`;
|
|
80
|
+
const CYAN = `${CSI}36m`;
|
|
81
|
+
const YELLOW = `${CSI}33m`;
|
|
82
|
+
|
|
83
|
+
function moveCursorUp(n) {
|
|
84
|
+
if (n > 0) process.stdout.write(`${CSI}${n}A`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function clearLines(n) {
|
|
88
|
+
for (let i = 0; i < n; i++) {
|
|
89
|
+
process.stdout.write(`${CLEAR_LINE}\n`);
|
|
90
|
+
}
|
|
91
|
+
moveCursorUp(n);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─── Interactive UI ──────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
let previewProcess = null;
|
|
97
|
+
|
|
98
|
+
function killPreview() {
|
|
99
|
+
if (previewProcess) {
|
|
100
|
+
try { previewProcess.kill(); } catch {}
|
|
101
|
+
previewProcess = null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function playPreview(filePath) {
|
|
106
|
+
killPreview();
|
|
107
|
+
if (fs.existsSync(filePath)) {
|
|
108
|
+
previewProcess = spawn("afplay", [filePath], { stdio: "ignore", detached: true });
|
|
109
|
+
previewProcess.unref();
|
|
110
|
+
previewProcess.on("exit", () => { previewProcess = null; });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function cleanupAndExit() {
|
|
115
|
+
killPreview();
|
|
116
|
+
process.stdout.write(SHOW_CURSOR);
|
|
117
|
+
print("\n");
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Single-select menu with arrow keys.
|
|
123
|
+
* Returns the index of the chosen option.
|
|
124
|
+
*/
|
|
125
|
+
function select(title, options) {
|
|
126
|
+
return new Promise((resolve) => {
|
|
127
|
+
let cursor = 0;
|
|
128
|
+
const lineCount = options.length + 3; // title + blank + options + hint
|
|
129
|
+
|
|
130
|
+
function render(initial) {
|
|
131
|
+
if (!initial) moveCursorUp(lineCount);
|
|
132
|
+
print(` ${title}\n`);
|
|
133
|
+
for (let i = 0; i < options.length; i++) {
|
|
134
|
+
const prefix = i === cursor ? `${CYAN} ❯ ` : " ";
|
|
135
|
+
const label = options[i].label;
|
|
136
|
+
const desc = options[i].description ? ` ${DIM}— ${options[i].description}${RESET}` : "";
|
|
137
|
+
print(`${prefix}${RESET}${i === cursor ? BOLD : ""}${label}${RESET}${desc}`);
|
|
138
|
+
}
|
|
139
|
+
print(`${DIM} ↑↓ navigate · enter select${RESET}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
process.stdout.write(HIDE_CURSOR);
|
|
143
|
+
render(true);
|
|
144
|
+
|
|
145
|
+
process.stdin.setRawMode(true);
|
|
146
|
+
process.stdin.resume();
|
|
147
|
+
process.stdin.setEncoding("utf-8");
|
|
148
|
+
|
|
149
|
+
function onKey(key) {
|
|
150
|
+
// Ctrl+C or q
|
|
151
|
+
if (key === "\x03" || key === "q") {
|
|
152
|
+
process.stdin.setRawMode(false);
|
|
153
|
+
process.stdin.pause();
|
|
154
|
+
process.stdin.removeListener("data", onKey);
|
|
155
|
+
cleanupAndExit();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Arrow up
|
|
160
|
+
if (key === "\x1b[A" || key === "k") {
|
|
161
|
+
cursor = (cursor - 1 + options.length) % options.length;
|
|
162
|
+
render(false);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Arrow down
|
|
167
|
+
if (key === "\x1b[B" || key === "j") {
|
|
168
|
+
cursor = (cursor + 1) % options.length;
|
|
169
|
+
render(false);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Enter
|
|
174
|
+
if (key === "\r" || key === "\n") {
|
|
175
|
+
process.stdin.setRawMode(false);
|
|
176
|
+
process.stdin.pause();
|
|
177
|
+
process.stdin.removeListener("data", onKey);
|
|
178
|
+
// Redraw final state
|
|
179
|
+
moveCursorUp(lineCount);
|
|
180
|
+
clearLines(lineCount);
|
|
181
|
+
print(` ${title} ${GREEN}${options[cursor].label}${RESET}\n`);
|
|
182
|
+
process.stdout.write(SHOW_CURSOR);
|
|
183
|
+
resolve(cursor);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
process.stdin.on("data", onKey);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Multi-select checklist with toggle, preview, and confirm.
|
|
194
|
+
* Returns array of selected indices.
|
|
195
|
+
*/
|
|
196
|
+
function multiSelect(title, items, defaults, previewDir) {
|
|
197
|
+
return new Promise((resolve) => {
|
|
198
|
+
let cursor = 0;
|
|
199
|
+
const checked = items.map((_, i) => defaults.includes(i));
|
|
200
|
+
const lineCount = items.length + 3; // title + blank + items + hint
|
|
201
|
+
|
|
202
|
+
function render(initial) {
|
|
203
|
+
if (!initial) moveCursorUp(lineCount);
|
|
204
|
+
print(` ${title}\n`);
|
|
205
|
+
for (let i = 0; i < items.length; i++) {
|
|
206
|
+
const pointer = i === cursor ? `${CYAN} ❯ ` : " ";
|
|
207
|
+
const box = checked[i] ? `${GREEN}[✓]${RESET}` : `${DIM}[ ]${RESET}`;
|
|
208
|
+
const label = items[i].label;
|
|
209
|
+
const desc = items[i].description ? ` ${DIM}${items[i].description}${RESET}` : "";
|
|
210
|
+
print(`${pointer}${RESET}${box} ${label}${desc}`);
|
|
211
|
+
}
|
|
212
|
+
const previewHint = previewDir ? " · p preview" : "";
|
|
213
|
+
print(`${DIM} ↑↓ navigate · space toggle${previewHint} · enter confirm${RESET}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
process.stdout.write(HIDE_CURSOR);
|
|
217
|
+
render(true);
|
|
218
|
+
|
|
219
|
+
process.stdin.setRawMode(true);
|
|
220
|
+
process.stdin.resume();
|
|
221
|
+
process.stdin.setEncoding("utf-8");
|
|
222
|
+
|
|
223
|
+
function onKey(key) {
|
|
224
|
+
if (key === "\x03" || key === "q") {
|
|
225
|
+
process.stdin.setRawMode(false);
|
|
226
|
+
process.stdin.pause();
|
|
227
|
+
process.stdin.removeListener("data", onKey);
|
|
228
|
+
killPreview();
|
|
229
|
+
cleanupAndExit();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (key === "\x1b[A" || key === "k") {
|
|
234
|
+
cursor = (cursor - 1 + items.length) % items.length;
|
|
235
|
+
render(false);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (key === "\x1b[B" || key === "j") {
|
|
240
|
+
cursor = (cursor + 1) % items.length;
|
|
241
|
+
render(false);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Space — toggle
|
|
246
|
+
if (key === " ") {
|
|
247
|
+
checked[cursor] = !checked[cursor];
|
|
248
|
+
render(false);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// p — preview sound
|
|
253
|
+
if (key === "p" && previewDir && items[cursor].file) {
|
|
254
|
+
const soundPath = path.join(previewDir, items[cursor].file);
|
|
255
|
+
playPreview(soundPath);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Enter — confirm
|
|
260
|
+
if (key === "\r" || key === "\n") {
|
|
261
|
+
process.stdin.setRawMode(false);
|
|
262
|
+
process.stdin.pause();
|
|
263
|
+
process.stdin.removeListener("data", onKey);
|
|
264
|
+
killPreview();
|
|
265
|
+
|
|
266
|
+
const selected = [];
|
|
267
|
+
for (let i = 0; i < checked.length; i++) {
|
|
268
|
+
if (checked[i]) selected.push(i);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Redraw final state
|
|
272
|
+
moveCursorUp(lineCount);
|
|
273
|
+
clearLines(lineCount);
|
|
274
|
+
const count = selected.length;
|
|
275
|
+
print(` ${title} ${GREEN}${count}/${items.length} selected${RESET}\n`);
|
|
276
|
+
process.stdout.write(SHOW_CURSOR);
|
|
277
|
+
resolve(selected);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
process.stdin.on("data", onKey);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Y/n confirmation prompt.
|
|
288
|
+
*/
|
|
289
|
+
function confirm(message, defaultYes = true) {
|
|
290
|
+
return new Promise((resolve) => {
|
|
291
|
+
const hint = defaultYes ? "Y/n" : "y/N";
|
|
292
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
293
|
+
rl.question(` ${message} (${hint}) `, (answer) => {
|
|
294
|
+
rl.close();
|
|
295
|
+
const a = answer.trim().toLowerCase();
|
|
296
|
+
if (a === "") resolve(defaultYes);
|
|
297
|
+
else resolve(a === "y" || a === "yes");
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
59
302
|
// ─── Hooks Config ────────────────────────────────────────────────────────────
|
|
60
303
|
|
|
61
304
|
const HOOKS_CONFIG = {
|
|
@@ -74,7 +317,7 @@ const HOOKS_CONFIG = {
|
|
|
74
317
|
TeammateIdle: [{ hooks: [{ type: "command", command: '/bin/bash "$HOME/.claude/hooks/play-sound.sh" teammate-idle', timeout: 5 }] }],
|
|
75
318
|
};
|
|
76
319
|
|
|
77
|
-
// ─── Commands
|
|
320
|
+
// ─── Non-Interactive Commands ───────────────────────────────────────────────
|
|
78
321
|
|
|
79
322
|
function showHelp() {
|
|
80
323
|
print("");
|
|
@@ -82,11 +325,16 @@ function showHelp() {
|
|
|
82
325
|
print(" ──────────────────────────────");
|
|
83
326
|
print("");
|
|
84
327
|
print(" Usage:");
|
|
85
|
-
print(" npx claude-code-sounds
|
|
86
|
-
print(" npx claude-code-sounds
|
|
87
|
-
print(" npx claude-code-sounds --list
|
|
88
|
-
print(" npx claude-code-sounds --uninstall
|
|
89
|
-
print(" npx claude-code-sounds --help
|
|
328
|
+
print(" npx claude-code-sounds Interactive install");
|
|
329
|
+
print(" npx claude-code-sounds --yes Install defaults, skip prompts");
|
|
330
|
+
print(" npx claude-code-sounds --list List available themes");
|
|
331
|
+
print(" npx claude-code-sounds --uninstall Remove all sounds and hooks");
|
|
332
|
+
print(" npx claude-code-sounds --help Show this help");
|
|
333
|
+
print("");
|
|
334
|
+
print(" Flags:");
|
|
335
|
+
print(" -y, --yes Skip all prompts, use defaults");
|
|
336
|
+
print(" -l, --list List available themes");
|
|
337
|
+
print(" -h, --help Show this help");
|
|
90
338
|
print("");
|
|
91
339
|
}
|
|
92
340
|
|
|
@@ -106,20 +354,20 @@ function uninstall() {
|
|
|
106
354
|
|
|
107
355
|
if (fs.existsSync(SOUNDS_DIR)) {
|
|
108
356
|
fs.rmSync(SOUNDS_DIR, { recursive: true });
|
|
109
|
-
print("
|
|
357
|
+
print(" Removed ~/.claude/sounds/");
|
|
110
358
|
}
|
|
111
359
|
|
|
112
360
|
const hookScript = path.join(HOOKS_DIR, "play-sound.sh");
|
|
113
361
|
if (fs.existsSync(hookScript)) {
|
|
114
362
|
fs.unlinkSync(hookScript);
|
|
115
|
-
print("
|
|
363
|
+
print(" Removed ~/.claude/hooks/play-sound.sh");
|
|
116
364
|
}
|
|
117
365
|
|
|
118
366
|
if (fs.existsSync(SETTINGS_PATH)) {
|
|
119
367
|
const settings = readSettings();
|
|
120
368
|
delete settings.hooks;
|
|
121
369
|
writeSettings(settings);
|
|
122
|
-
print("
|
|
370
|
+
print(" Removed hooks from settings.json");
|
|
123
371
|
}
|
|
124
372
|
|
|
125
373
|
print("");
|
|
@@ -127,39 +375,89 @@ function uninstall() {
|
|
|
127
375
|
print("");
|
|
128
376
|
}
|
|
129
377
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
378
|
+
// ─── Install Flow ───────────────────────────────────────────────────────────
|
|
379
|
+
|
|
380
|
+
async function interactiveInstall(autoYes) {
|
|
381
|
+
print("");
|
|
382
|
+
print(` ${BOLD}claude-code-sounds${RESET}`);
|
|
383
|
+
print(" ──────────────────────────────");
|
|
384
|
+
print("");
|
|
385
|
+
|
|
386
|
+
// ── Step 1: Dependency Check ──────────────────────────────────────────────
|
|
133
387
|
|
|
134
|
-
|
|
135
|
-
|
|
388
|
+
const deps = ["afplay", "curl", "unzip"];
|
|
389
|
+
const missing = [];
|
|
390
|
+
|
|
391
|
+
print(" Checking dependencies...");
|
|
392
|
+
for (const dep of deps) {
|
|
393
|
+
const ok = hasCommand(dep);
|
|
394
|
+
if (ok) {
|
|
395
|
+
print(` ${GREEN}✓${RESET} ${dep}`);
|
|
396
|
+
} else {
|
|
397
|
+
print(` ${RED}✗${RESET} ${dep} — required${dep === "afplay" ? " (macOS only)" : ""}`);
|
|
398
|
+
missing.push(dep);
|
|
399
|
+
}
|
|
136
400
|
}
|
|
401
|
+
print("");
|
|
137
402
|
|
|
138
|
-
|
|
139
|
-
|
|
403
|
+
if (missing.includes("afplay")) {
|
|
404
|
+
die("afplay is not available. claude-code-sounds requires macOS.");
|
|
405
|
+
}
|
|
140
406
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
407
|
+
if (missing.length > 0) {
|
|
408
|
+
if (autoYes) {
|
|
409
|
+
die(`Missing dependencies: ${missing.join(", ")}. Install them and try again.`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const installDeps = await confirm(`Install missing dependencies with Homebrew?`, true);
|
|
413
|
+
if (installDeps) {
|
|
414
|
+
try {
|
|
415
|
+
exec("which brew");
|
|
416
|
+
} catch {
|
|
417
|
+
die("Homebrew not found. Install missing dependencies manually:\n brew install " + missing.join(" "));
|
|
418
|
+
}
|
|
419
|
+
print(` Installing ${missing.join(", ")}...`);
|
|
420
|
+
try {
|
|
421
|
+
exec(`brew install ${missing.join(" ")}`, { stdio: "inherit" });
|
|
422
|
+
print(` ${GREEN}✓${RESET} Dependencies installed.\n`);
|
|
423
|
+
} catch {
|
|
424
|
+
die("Failed to install dependencies. Run manually:\n brew install " + missing.join(" "));
|
|
425
|
+
}
|
|
426
|
+
} else {
|
|
427
|
+
die("Missing dependencies. Install them manually:\n brew install " + missing.join(" "));
|
|
428
|
+
}
|
|
146
429
|
}
|
|
147
430
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
431
|
+
// ── Step 2: Theme Selection ───────────────────────────────────────────────
|
|
432
|
+
|
|
433
|
+
const themes = listThemes();
|
|
434
|
+
let selectedTheme;
|
|
435
|
+
|
|
436
|
+
if (themes.length === 0) {
|
|
437
|
+
die("No themes found in themes/ directory.");
|
|
438
|
+
} else if (themes.length === 1 || autoYes) {
|
|
439
|
+
selectedTheme = themes[0];
|
|
440
|
+
print(` Theme: ${BOLD}${selectedTheme.display}${RESET} — ${selectedTheme.description}\n`);
|
|
441
|
+
} else {
|
|
442
|
+
const options = themes.map((t) => ({ label: t.display, description: t.description }));
|
|
443
|
+
const idx = await select("Select a theme:", options);
|
|
444
|
+
selectedTheme = themes[idx];
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ── Step 3: Download ──────────────────────────────────────────────────────
|
|
448
|
+
|
|
449
|
+
const themeDir = path.join(THEMES_DIR, selectedTheme.name);
|
|
450
|
+
const themeJsonPath = path.join(themeDir, "theme.json");
|
|
451
|
+
const theme = JSON.parse(fs.readFileSync(themeJsonPath, "utf-8"));
|
|
452
|
+
const categories = Object.keys(theme.sounds);
|
|
153
453
|
|
|
154
|
-
//
|
|
155
|
-
print(" [1/4] Creating directories...");
|
|
454
|
+
// Create directories
|
|
156
455
|
for (const cat of categories) {
|
|
157
456
|
mkdirp(path.join(SOUNDS_DIR, cat));
|
|
158
457
|
}
|
|
159
458
|
mkdirp(HOOKS_DIR);
|
|
160
459
|
|
|
161
|
-
|
|
162
|
-
print(" [2/4] Downloading sounds...");
|
|
460
|
+
print(" Downloading sounds...");
|
|
163
461
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "claude-sounds-"));
|
|
164
462
|
|
|
165
463
|
try {
|
|
@@ -167,9 +465,66 @@ function install(themeName) {
|
|
|
167
465
|
if (fs.existsSync(downloadScript)) {
|
|
168
466
|
exec(`bash "${downloadScript}" "${SOUNDS_DIR}" "${tmpDir}"`, { stdio: "inherit" });
|
|
169
467
|
}
|
|
468
|
+
print(` ${GREEN}✓${RESET} Download complete.\n`);
|
|
170
469
|
|
|
171
|
-
//
|
|
172
|
-
|
|
470
|
+
// ── Step 4: Customize or Accept Defaults ──────────────────────────────
|
|
471
|
+
|
|
472
|
+
// Build a selection map: category -> array of file indices to include
|
|
473
|
+
const selections = {};
|
|
474
|
+
for (const cat of categories) {
|
|
475
|
+
selections[cat] = theme.sounds[cat].files.map((_, i) => i);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (!autoYes) {
|
|
479
|
+
const customizeOptions = [
|
|
480
|
+
{ label: "No, use defaults", description: "Recommended" },
|
|
481
|
+
{ label: "Yes, let me pick", description: "Choose sounds per hook" },
|
|
482
|
+
];
|
|
483
|
+
const customizeIdx = await select("Customize sounds for each hook?", customizeOptions);
|
|
484
|
+
|
|
485
|
+
if (customizeIdx === 1) {
|
|
486
|
+
// Customize each category
|
|
487
|
+
for (const cat of categories) {
|
|
488
|
+
const config = theme.sounds[cat];
|
|
489
|
+
const items = config.files.map((f) => ({
|
|
490
|
+
label: f.name.replace(/\.(wav|mp3)$/, ""),
|
|
491
|
+
description: f.description || "",
|
|
492
|
+
file: f.name,
|
|
493
|
+
}));
|
|
494
|
+
const defaults = config.files.map((_, i) => i); // all selected by default
|
|
495
|
+
|
|
496
|
+
// Build preview dir: sounds are in tmpDir/Orc/... but we need them by name
|
|
497
|
+
// Copy files to a temp preview dir first
|
|
498
|
+
const previewDir = path.join(tmpDir, "_preview", cat);
|
|
499
|
+
mkdirp(previewDir);
|
|
500
|
+
const srcBase = path.join(tmpDir, "Orc");
|
|
501
|
+
for (const file of config.files) {
|
|
502
|
+
let srcFile;
|
|
503
|
+
if (file.src.startsWith("@soundfxcenter/")) {
|
|
504
|
+
srcFile = path.join(srcBase, path.basename(file.src));
|
|
505
|
+
} else {
|
|
506
|
+
srcFile = path.join(srcBase, file.src);
|
|
507
|
+
}
|
|
508
|
+
const destFile = path.join(previewDir, file.name);
|
|
509
|
+
if (fs.existsSync(srcFile)) {
|
|
510
|
+
fs.copyFileSync(srcFile, destFile);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const selected = await multiSelect(
|
|
515
|
+
`${BOLD}${cat}${RESET} ${DIM}— ${config.description}${RESET}`,
|
|
516
|
+
items,
|
|
517
|
+
defaults,
|
|
518
|
+
previewDir
|
|
519
|
+
);
|
|
520
|
+
selections[cat] = selected;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// ── Step 5: Install & Summary ─────────────────────────────────────────
|
|
526
|
+
|
|
527
|
+
print(" Installing sounds...");
|
|
173
528
|
|
|
174
529
|
// Clear existing sounds
|
|
175
530
|
for (const cat of categories) {
|
|
@@ -181,10 +536,13 @@ function install(themeName) {
|
|
|
181
536
|
}
|
|
182
537
|
}
|
|
183
538
|
|
|
184
|
-
// Copy files
|
|
539
|
+
// Copy selected files
|
|
185
540
|
const srcBase = path.join(tmpDir, "Orc");
|
|
541
|
+
let total = 0;
|
|
186
542
|
for (const [category, config] of Object.entries(theme.sounds)) {
|
|
187
|
-
|
|
543
|
+
const selectedIndices = selections[category];
|
|
544
|
+
for (const idx of selectedIndices) {
|
|
545
|
+
const file = config.files[idx];
|
|
188
546
|
let srcFile;
|
|
189
547
|
if (file.src.startsWith("@soundfxcenter/")) {
|
|
190
548
|
srcFile = path.join(srcBase, path.basename(file.src));
|
|
@@ -195,16 +553,14 @@ function install(themeName) {
|
|
|
195
553
|
const destFile = path.join(SOUNDS_DIR, category, file.name);
|
|
196
554
|
if (fs.existsSync(srcFile)) {
|
|
197
555
|
fs.copyFileSync(srcFile, destFile);
|
|
556
|
+
total++;
|
|
198
557
|
} else {
|
|
199
|
-
print(`
|
|
558
|
+
print(` ${YELLOW}⚠${RESET} ${file.src} not found, skipping`);
|
|
200
559
|
}
|
|
201
560
|
}
|
|
202
561
|
}
|
|
203
562
|
|
|
204
|
-
//
|
|
205
|
-
print(" [4/4] Installing hooks...");
|
|
206
|
-
|
|
207
|
-
// Copy play-sound.sh
|
|
563
|
+
// Copy play-sound.sh hook
|
|
208
564
|
const hookSrc = path.join(PKG_DIR, "hooks", "play-sound.sh");
|
|
209
565
|
const hookDest = path.join(HOOKS_DIR, "play-sound.sh");
|
|
210
566
|
fs.copyFileSync(hookSrc, hookDest);
|
|
@@ -217,14 +573,14 @@ function install(themeName) {
|
|
|
217
573
|
|
|
218
574
|
// Summary
|
|
219
575
|
print("");
|
|
220
|
-
print(
|
|
576
|
+
print(` ${GREEN}✓${RESET} Installed! Here's what you'll hear:`);
|
|
221
577
|
print(" ─────────────────────────────────────");
|
|
222
578
|
|
|
223
|
-
let total = 0;
|
|
224
579
|
for (const [cat, config] of Object.entries(theme.sounds)) {
|
|
225
|
-
const count =
|
|
226
|
-
|
|
227
|
-
|
|
580
|
+
const count = selections[cat].length;
|
|
581
|
+
const totalAvailable = config.files.length;
|
|
582
|
+
const suffix = count < totalAvailable ? ` (${count}/${totalAvailable})` : ` (${count})`;
|
|
583
|
+
print(` ${cat}${suffix} — ${config.description}`);
|
|
228
584
|
}
|
|
229
585
|
|
|
230
586
|
print("");
|
|
@@ -234,7 +590,7 @@ function install(themeName) {
|
|
|
234
590
|
print(" Zug zug.");
|
|
235
591
|
print("");
|
|
236
592
|
} finally {
|
|
237
|
-
|
|
593
|
+
killPreview();
|
|
238
594
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
239
595
|
}
|
|
240
596
|
}
|
|
@@ -242,21 +598,20 @@ function install(themeName) {
|
|
|
242
598
|
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
243
599
|
|
|
244
600
|
const args = process.argv.slice(2);
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
install(arg);
|
|
601
|
+
const flags = new Set(args);
|
|
602
|
+
const autoYes = flags.has("--yes") || flags.has("-y");
|
|
603
|
+
|
|
604
|
+
// Handle non-interactive commands first
|
|
605
|
+
if (flags.has("--help") || flags.has("-h")) {
|
|
606
|
+
showHelp();
|
|
607
|
+
} else if (flags.has("--list") || flags.has("-l")) {
|
|
608
|
+
showList();
|
|
609
|
+
} else if (flags.has("--uninstall") || flags.has("--remove")) {
|
|
610
|
+
uninstall();
|
|
611
|
+
} else {
|
|
612
|
+
interactiveInstall(autoYes).catch((err) => {
|
|
613
|
+
killPreview();
|
|
614
|
+
process.stdout.write(SHOW_CURSOR);
|
|
615
|
+
die(err.message);
|
|
616
|
+
});
|
|
262
617
|
}
|