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.
Files changed (3) hide show
  1. package/README.md +12 -3
  2. package/bin/cli.js +420 -65
  3. 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
- That's it. Requires macOS (uses `afplay`) and Node.js 16+.
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 # Install default theme (wc3-peon)
42
- npx claude-code-sounds <theme> # Install a specific theme
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 { execSync } = require("child_process");
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
- console.log(msg);
21
+ process.stdout.write(msg + "\n");
21
22
  }
22
23
 
23
24
  function die(msg) {
24
- console.error(`Error: ${msg}`);
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 Install default theme (wc3-peon)");
86
- print(" npx claude-code-sounds <theme> Install a specific theme");
87
- print(" npx claude-code-sounds --list List available themes");
88
- print(" npx claude-code-sounds --uninstall Remove all sounds and hooks");
89
- print(" npx claude-code-sounds --help Show this 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(" Removed ~/.claude/sounds/");
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(" Removed ~/.claude/hooks/play-sound.sh");
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(" Removed hooks from settings.json");
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
- function install(themeName) {
131
- const themeDir = path.join(THEMES_DIR, themeName);
132
- const themeJsonPath = path.join(themeDir, "theme.json");
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
- if (!fs.existsSync(themeJsonPath)) {
135
- die(`Theme '${themeName}' not found.\n\nAvailable themes:\n${listThemes().map((t) => ` ${t.name} — ${t.description}`).join("\n")}`);
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
- const theme = JSON.parse(fs.readFileSync(themeJsonPath, "utf-8"));
139
- const categories = Object.keys(theme.sounds);
403
+ if (missing.includes("afplay")) {
404
+ die("afplay is not available. claude-code-sounds requires macOS.");
405
+ }
140
406
 
141
- // Preflight
142
- try {
143
- exec("which afplay");
144
- } catch {
145
- die("afplay not found. This tool requires macOS.");
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
- print("");
149
- print(" claude-code-sounds");
150
- print(" ──────────────────────────────");
151
- print(` Theme: ${theme.name}`);
152
- print("");
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
- // 1. Create directories
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
- // 2. Download sounds
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
- // 3. Sort sounds
172
- print(" [3/4] Sorting sounds...");
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 based on theme.json
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
- for (const file of config.files) {
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(` Warning: ${file.src} not found, skipping`);
558
+ print(` ${YELLOW}⚠${RESET} ${file.src} not found, skipping`);
200
559
  }
201
560
  }
202
561
  }
203
562
 
204
- // 4. Install hooks
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(" Installed! Here's what you'll hear:");
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 = config.files.length;
226
- total += count;
227
- print(` ${cat} (${count}) ${config.description}`);
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
- // Cleanup
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 arg = args[0] || "wc3-peon";
246
-
247
- switch (arg) {
248
- case "--help":
249
- case "-h":
250
- showHelp();
251
- break;
252
- case "--list":
253
- case "-l":
254
- showList();
255
- break;
256
- case "--uninstall":
257
- case "--remove":
258
- uninstall();
259
- break;
260
- default:
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-sounds",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Sound themes for Claude Code lifecycle hooks",
5
5
  "bin": {
6
6
  "claude-code-sounds": "bin/cli.js"