claude-code-sounds 1.0.0 → 1.1.1
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/LICENSE +21 -0
- package/README.md +167 -11
- package/bin/cli.js +750 -74
- package/package.json +1 -1
- package/themes/wc3-peon/download.sh +2 -1
- package/themes/wc3-peon/theme.json +5 -2
- package/themes/zelda-oot/download.sh +67 -0
- package/themes/zelda-oot/theme.json +110 -0
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
|
|
|
@@ -13,15 +14,16 @@ const SOUNDS_DIR = path.join(CLAUDE_DIR, "sounds");
|
|
|
13
14
|
const HOOKS_DIR = path.join(CLAUDE_DIR, "hooks");
|
|
14
15
|
const SETTINGS_PATH = path.join(CLAUDE_DIR, "settings.json");
|
|
15
16
|
const THEMES_DIR = path.join(PKG_DIR, "themes");
|
|
17
|
+
const INSTALLED_PATH = path.join(SOUNDS_DIR, ".installed.json");
|
|
16
18
|
|
|
17
19
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
18
20
|
|
|
19
21
|
function print(msg = "") {
|
|
20
|
-
|
|
22
|
+
process.stdout.write(msg + "\n");
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
function die(msg) {
|
|
24
|
-
console.error(
|
|
26
|
+
console.error(`\n Error: ${msg}\n`);
|
|
25
27
|
process.exit(1);
|
|
26
28
|
}
|
|
27
29
|
|
|
@@ -56,6 +58,350 @@ function writeSettings(settings) {
|
|
|
56
58
|
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n");
|
|
57
59
|
}
|
|
58
60
|
|
|
61
|
+
function hasCommand(name) {
|
|
62
|
+
try {
|
|
63
|
+
exec(`which ${name}`);
|
|
64
|
+
return true;
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readInstalled() {
|
|
71
|
+
if (fs.existsSync(INSTALLED_PATH)) {
|
|
72
|
+
return JSON.parse(fs.readFileSync(INSTALLED_PATH, "utf-8"));
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function writeInstalled(themeName) {
|
|
78
|
+
mkdirp(SOUNDS_DIR);
|
|
79
|
+
fs.writeFileSync(INSTALLED_PATH, JSON.stringify({ theme: themeName }, null, 2) + "\n");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check if sounds are already installed.
|
|
84
|
+
* Returns { theme, themeDisplay, totalEnabled, totalAvailable, categories } or null.
|
|
85
|
+
*/
|
|
86
|
+
function getExistingInstall() {
|
|
87
|
+
const installed = readInstalled();
|
|
88
|
+
if (!installed) return null;
|
|
89
|
+
|
|
90
|
+
const themeJsonPath = path.join(THEMES_DIR, installed.theme, "theme.json");
|
|
91
|
+
if (!fs.existsSync(themeJsonPath)) return null;
|
|
92
|
+
|
|
93
|
+
const theme = JSON.parse(fs.readFileSync(themeJsonPath, "utf-8"));
|
|
94
|
+
let totalEnabled = 0;
|
|
95
|
+
const totalAvailable = Object.values(theme.sounds).reduce((sum, c) => sum + c.files.length, 0);
|
|
96
|
+
|
|
97
|
+
for (const cat of Object.keys(theme.sounds)) {
|
|
98
|
+
const catDir = path.join(SOUNDS_DIR, cat);
|
|
99
|
+
try {
|
|
100
|
+
for (const f of fs.readdirSync(catDir)) {
|
|
101
|
+
if (f.endsWith(".wav") || f.endsWith(".mp3")) totalEnabled++;
|
|
102
|
+
}
|
|
103
|
+
} catch {}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (totalEnabled === 0) return null;
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
theme: installed.theme,
|
|
110
|
+
themeDisplay: theme.name,
|
|
111
|
+
themeDescription: theme.description,
|
|
112
|
+
totalEnabled,
|
|
113
|
+
totalAvailable,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── ANSI helpers ────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
const CSI = "\x1b[";
|
|
120
|
+
const CLEAR_LINE = `${CSI}2K`;
|
|
121
|
+
const HIDE_CURSOR = `${CSI}?25l`;
|
|
122
|
+
const SHOW_CURSOR = `${CSI}?25h`;
|
|
123
|
+
const BOLD = `${CSI}1m`;
|
|
124
|
+
const DIM = `${CSI}2m`;
|
|
125
|
+
const RESET = `${CSI}0m`;
|
|
126
|
+
const GREEN = `${CSI}32m`;
|
|
127
|
+
const RED = `${CSI}31m`;
|
|
128
|
+
const CYAN = `${CSI}36m`;
|
|
129
|
+
const YELLOW = `${CSI}33m`;
|
|
130
|
+
|
|
131
|
+
function moveCursorUp(n) {
|
|
132
|
+
if (n > 0) process.stdout.write(`${CSI}${n}A`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function clearLines(n) {
|
|
136
|
+
for (let i = 0; i < n; i++) {
|
|
137
|
+
process.stdout.write(`${CLEAR_LINE}\n`);
|
|
138
|
+
}
|
|
139
|
+
moveCursorUp(n);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ─── Interactive UI ──────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
let previewProcess = null;
|
|
145
|
+
|
|
146
|
+
function killPreview() {
|
|
147
|
+
if (previewProcess) {
|
|
148
|
+
try { previewProcess.kill(); } catch {}
|
|
149
|
+
previewProcess = null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function playPreview(filePath) {
|
|
154
|
+
killPreview();
|
|
155
|
+
if (fs.existsSync(filePath)) {
|
|
156
|
+
previewProcess = spawn("afplay", [filePath], { stdio: "ignore", detached: true });
|
|
157
|
+
previewProcess.unref();
|
|
158
|
+
previewProcess.on("exit", () => { previewProcess = null; });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function cleanupAndExit() {
|
|
163
|
+
killPreview();
|
|
164
|
+
process.stdout.write(SHOW_CURSOR);
|
|
165
|
+
print("\n");
|
|
166
|
+
process.exit(0);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Single-select menu with arrow keys.
|
|
171
|
+
* Returns the index of the chosen option.
|
|
172
|
+
*/
|
|
173
|
+
function select(title, options) {
|
|
174
|
+
return new Promise((resolve) => {
|
|
175
|
+
let cursor = 0;
|
|
176
|
+
const lineCount = options.length + 3; // title + blank + options + hint
|
|
177
|
+
|
|
178
|
+
function render(initial) {
|
|
179
|
+
if (!initial) moveCursorUp(lineCount);
|
|
180
|
+
print(` ${title}\n`);
|
|
181
|
+
for (let i = 0; i < options.length; i++) {
|
|
182
|
+
const prefix = i === cursor ? `${CYAN} ❯ ` : " ";
|
|
183
|
+
const label = options[i].label;
|
|
184
|
+
const desc = options[i].description ? ` ${DIM}— ${options[i].description}${RESET}` : "";
|
|
185
|
+
print(`${prefix}${RESET}${i === cursor ? BOLD : ""}${label}${RESET}${desc}`);
|
|
186
|
+
}
|
|
187
|
+
print(`${DIM} ↑↓ navigate · enter select${RESET}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
process.stdout.write(HIDE_CURSOR);
|
|
191
|
+
render(true);
|
|
192
|
+
|
|
193
|
+
process.stdin.setRawMode(true);
|
|
194
|
+
process.stdin.resume();
|
|
195
|
+
process.stdin.setEncoding("utf-8");
|
|
196
|
+
|
|
197
|
+
function onKey(key) {
|
|
198
|
+
// Ctrl+C or q
|
|
199
|
+
if (key === "\x03" || key === "q") {
|
|
200
|
+
process.stdin.setRawMode(false);
|
|
201
|
+
process.stdin.pause();
|
|
202
|
+
process.stdin.removeListener("data", onKey);
|
|
203
|
+
cleanupAndExit();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Arrow up
|
|
208
|
+
if (key === "\x1b[A" || key === "k") {
|
|
209
|
+
cursor = (cursor - 1 + options.length) % options.length;
|
|
210
|
+
render(false);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Arrow down
|
|
215
|
+
if (key === "\x1b[B" || key === "j") {
|
|
216
|
+
cursor = (cursor + 1) % options.length;
|
|
217
|
+
render(false);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Enter
|
|
222
|
+
if (key === "\r" || key === "\n") {
|
|
223
|
+
process.stdin.setRawMode(false);
|
|
224
|
+
process.stdin.pause();
|
|
225
|
+
process.stdin.removeListener("data", onKey);
|
|
226
|
+
// Redraw final state
|
|
227
|
+
moveCursorUp(lineCount);
|
|
228
|
+
clearLines(lineCount);
|
|
229
|
+
print(` ${title} ${GREEN}${options[cursor].label}${RESET}\n`);
|
|
230
|
+
process.stdout.write(SHOW_CURSOR);
|
|
231
|
+
resolve(cursor);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
process.stdin.on("data", onKey);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Multi-select checklist with toggle, preview, and confirm.
|
|
242
|
+
* Returns array of selected indices, or null if back was pressed.
|
|
243
|
+
*/
|
|
244
|
+
function multiSelect(title, items, defaults, previewDir, { allowBack = false } = {}) {
|
|
245
|
+
return new Promise((resolve) => {
|
|
246
|
+
let cursor = 0;
|
|
247
|
+
let scrollTop = 0;
|
|
248
|
+
const checked = items.map((_, i) => defaults.includes(i));
|
|
249
|
+
|
|
250
|
+
// Calculate scrolling dimensions
|
|
251
|
+
const termRows = process.stdout.rows || 24;
|
|
252
|
+
const maxItemRows = Math.max(5, termRows - 5); // 5 = title + blank + hint + 2 buffer
|
|
253
|
+
const needsScroll = items.length > maxItemRows;
|
|
254
|
+
// When scrolling, reserve 2 rows for ▲/▼ indicators (always present for stable line count)
|
|
255
|
+
const visibleCount = needsScroll ? maxItemRows - 2 : items.length;
|
|
256
|
+
const lineCount = needsScroll ? maxItemRows + 3 : items.length + 3;
|
|
257
|
+
|
|
258
|
+
function adjustScroll() {
|
|
259
|
+
if (!needsScroll) return;
|
|
260
|
+
if (cursor < scrollTop) scrollTop = cursor;
|
|
261
|
+
if (cursor >= scrollTop + visibleCount) scrollTop = cursor - visibleCount + 1;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function render(initial) {
|
|
265
|
+
if (!initial) moveCursorUp(lineCount);
|
|
266
|
+
print(` ${title}\n`);
|
|
267
|
+
|
|
268
|
+
if (needsScroll) {
|
|
269
|
+
const above = scrollTop;
|
|
270
|
+
const below = items.length - scrollTop - visibleCount;
|
|
271
|
+
print(above > 0 ? `${DIM} ▲ ${above} more${RESET}` : "");
|
|
272
|
+
for (let i = scrollTop; i < scrollTop + visibleCount; i++) {
|
|
273
|
+
const pointer = i === cursor ? `${CYAN} ❯ ` : " ";
|
|
274
|
+
const box = checked[i] ? `${GREEN}[✓]${RESET}` : `${DIM}[ ]${RESET}`;
|
|
275
|
+
const label = items[i].label;
|
|
276
|
+
const desc = items[i].description ? ` ${DIM}${items[i].description}${RESET}` : "";
|
|
277
|
+
print(`${pointer}${RESET}${box} ${label}${desc}`);
|
|
278
|
+
}
|
|
279
|
+
print(below > 0 ? `${DIM} ▼ ${below} more${RESET}` : "");
|
|
280
|
+
} else {
|
|
281
|
+
for (let i = 0; i < items.length; i++) {
|
|
282
|
+
const pointer = i === cursor ? `${CYAN} ❯ ` : " ";
|
|
283
|
+
const box = checked[i] ? `${GREEN}[✓]${RESET}` : `${DIM}[ ]${RESET}`;
|
|
284
|
+
const label = items[i].label;
|
|
285
|
+
const desc = items[i].description ? ` ${DIM}${items[i].description}${RESET}` : "";
|
|
286
|
+
print(`${pointer}${RESET}${box} ${label}${desc}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const previewHint = previewDir ? " · p preview" : "";
|
|
291
|
+
const backHint = allowBack ? "← back · " : "";
|
|
292
|
+
print(`${DIM} ${backHint}↑↓ navigate · space toggle · a all${previewHint} · →/enter confirm${RESET}`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
process.stdout.write(HIDE_CURSOR);
|
|
296
|
+
adjustScroll();
|
|
297
|
+
render(true);
|
|
298
|
+
|
|
299
|
+
process.stdin.setRawMode(true);
|
|
300
|
+
process.stdin.resume();
|
|
301
|
+
process.stdin.setEncoding("utf-8");
|
|
302
|
+
|
|
303
|
+
function onKey(key) {
|
|
304
|
+
if (key === "\x03" || key === "q") {
|
|
305
|
+
process.stdin.setRawMode(false);
|
|
306
|
+
process.stdin.pause();
|
|
307
|
+
process.stdin.removeListener("data", onKey);
|
|
308
|
+
killPreview();
|
|
309
|
+
cleanupAndExit();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Left arrow — go back
|
|
314
|
+
if (allowBack && key === "\x1b[D") {
|
|
315
|
+
process.stdin.setRawMode(false);
|
|
316
|
+
process.stdin.pause();
|
|
317
|
+
process.stdin.removeListener("data", onKey);
|
|
318
|
+
killPreview();
|
|
319
|
+
moveCursorUp(lineCount);
|
|
320
|
+
clearLines(lineCount);
|
|
321
|
+
process.stdout.write(SHOW_CURSOR);
|
|
322
|
+
resolve(null);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (key === "\x1b[A" || key === "k") {
|
|
327
|
+
cursor = (cursor - 1 + items.length) % items.length;
|
|
328
|
+
adjustScroll();
|
|
329
|
+
render(false);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (key === "\x1b[B" || key === "j") {
|
|
334
|
+
cursor = (cursor + 1) % items.length;
|
|
335
|
+
adjustScroll();
|
|
336
|
+
render(false);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Space — toggle
|
|
341
|
+
if (key === " ") {
|
|
342
|
+
checked[cursor] = !checked[cursor];
|
|
343
|
+
render(false);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// a — toggle all
|
|
348
|
+
if (key === "a") {
|
|
349
|
+
const allChecked = checked.every(Boolean);
|
|
350
|
+
checked.fill(!allChecked);
|
|
351
|
+
render(false);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// p — preview sound
|
|
356
|
+
if (key === "p" && previewDir && items[cursor].file) {
|
|
357
|
+
const soundPath = path.join(previewDir, items[cursor].file);
|
|
358
|
+
playPreview(soundPath);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Enter or right arrow — confirm
|
|
363
|
+
if (key === "\r" || key === "\n" || key === "\x1b[C") {
|
|
364
|
+
process.stdin.setRawMode(false);
|
|
365
|
+
process.stdin.pause();
|
|
366
|
+
process.stdin.removeListener("data", onKey);
|
|
367
|
+
killPreview();
|
|
368
|
+
|
|
369
|
+
const selected = [];
|
|
370
|
+
for (let i = 0; i < checked.length; i++) {
|
|
371
|
+
if (checked[i]) selected.push(i);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Redraw final state
|
|
375
|
+
moveCursorUp(lineCount);
|
|
376
|
+
clearLines(lineCount);
|
|
377
|
+
const count = selected.length;
|
|
378
|
+
print(` ${title} ${GREEN}${count}/${items.length} selected${RESET}\n`);
|
|
379
|
+
process.stdout.write(SHOW_CURSOR);
|
|
380
|
+
resolve(selected);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
process.stdin.on("data", onKey);
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Y/n confirmation prompt.
|
|
391
|
+
*/
|
|
392
|
+
function confirm(message, defaultYes = true) {
|
|
393
|
+
return new Promise((resolve) => {
|
|
394
|
+
const hint = defaultYes ? "Y/n" : "y/N";
|
|
395
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
396
|
+
rl.question(` ${message} (${hint}) `, (answer) => {
|
|
397
|
+
rl.close();
|
|
398
|
+
const a = answer.trim().toLowerCase();
|
|
399
|
+
if (a === "") resolve(defaultYes);
|
|
400
|
+
else resolve(a === "y" || a === "yes");
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
59
405
|
// ─── Hooks Config ────────────────────────────────────────────────────────────
|
|
60
406
|
|
|
61
407
|
const HOOKS_CONFIG = {
|
|
@@ -74,7 +420,7 @@ const HOOKS_CONFIG = {
|
|
|
74
420
|
TeammateIdle: [{ hooks: [{ type: "command", command: '/bin/bash "$HOME/.claude/hooks/play-sound.sh" teammate-idle', timeout: 5 }] }],
|
|
75
421
|
};
|
|
76
422
|
|
|
77
|
-
// ─── Commands
|
|
423
|
+
// ─── Non-Interactive Commands ───────────────────────────────────────────────
|
|
78
424
|
|
|
79
425
|
function showHelp() {
|
|
80
426
|
print("");
|
|
@@ -82,11 +428,16 @@ function showHelp() {
|
|
|
82
428
|
print(" ──────────────────────────────");
|
|
83
429
|
print("");
|
|
84
430
|
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
|
|
431
|
+
print(" npx claude-code-sounds Interactive install");
|
|
432
|
+
print(" npx claude-code-sounds --yes Install defaults, skip prompts");
|
|
433
|
+
print(" npx claude-code-sounds --list List available themes");
|
|
434
|
+
print(" npx claude-code-sounds --uninstall Remove all sounds and hooks");
|
|
435
|
+
print(" npx claude-code-sounds --help Show this help");
|
|
436
|
+
print("");
|
|
437
|
+
print(" Flags:");
|
|
438
|
+
print(" -y, --yes Skip all prompts, use defaults");
|
|
439
|
+
print(" -l, --list List available themes");
|
|
440
|
+
print(" -h, --help Show this help");
|
|
90
441
|
print("");
|
|
91
442
|
}
|
|
92
443
|
|
|
@@ -106,20 +457,20 @@ function uninstall() {
|
|
|
106
457
|
|
|
107
458
|
if (fs.existsSync(SOUNDS_DIR)) {
|
|
108
459
|
fs.rmSync(SOUNDS_DIR, { recursive: true });
|
|
109
|
-
print("
|
|
460
|
+
print(" Removed ~/.claude/sounds/");
|
|
110
461
|
}
|
|
111
462
|
|
|
112
463
|
const hookScript = path.join(HOOKS_DIR, "play-sound.sh");
|
|
113
464
|
if (fs.existsSync(hookScript)) {
|
|
114
465
|
fs.unlinkSync(hookScript);
|
|
115
|
-
print("
|
|
466
|
+
print(" Removed ~/.claude/hooks/play-sound.sh");
|
|
116
467
|
}
|
|
117
468
|
|
|
118
469
|
if (fs.existsSync(SETTINGS_PATH)) {
|
|
119
470
|
const settings = readSettings();
|
|
120
471
|
delete settings.hooks;
|
|
121
472
|
writeSettings(settings);
|
|
122
|
-
print("
|
|
473
|
+
print(" Removed hooks from settings.json");
|
|
123
474
|
}
|
|
124
475
|
|
|
125
476
|
print("");
|
|
@@ -127,39 +478,291 @@ function uninstall() {
|
|
|
127
478
|
print("");
|
|
128
479
|
}
|
|
129
480
|
|
|
130
|
-
|
|
131
|
-
const themeDir = path.join(THEMES_DIR, themeName);
|
|
132
|
-
const themeJsonPath = path.join(themeDir, "theme.json");
|
|
481
|
+
// ─── Sound Item Builder ─────────────────────────────────────────────────────
|
|
133
482
|
|
|
134
|
-
|
|
135
|
-
|
|
483
|
+
/**
|
|
484
|
+
* Build the full list of sound items for a category.
|
|
485
|
+
* Native sounds (from this category's theme.json entry) come first,
|
|
486
|
+
* then borrowed sounds from all other categories, deduplicated by filename.
|
|
487
|
+
*/
|
|
488
|
+
function buildCategoryItems(theme, category) {
|
|
489
|
+
const config = theme.sounds[category];
|
|
490
|
+
const categories = Object.keys(theme.sounds);
|
|
491
|
+
const items = [];
|
|
492
|
+
const seen = new Set();
|
|
493
|
+
|
|
494
|
+
// Build a map of filename -> list of hooks it appears in
|
|
495
|
+
const hookMap = {};
|
|
496
|
+
for (const cat of categories) {
|
|
497
|
+
for (const f of theme.sounds[cat].files) {
|
|
498
|
+
if (!hookMap[f.name]) hookMap[f.name] = [];
|
|
499
|
+
if (!hookMap[f.name].includes(cat)) hookMap[f.name].push(cat);
|
|
500
|
+
}
|
|
136
501
|
}
|
|
137
502
|
|
|
138
|
-
|
|
503
|
+
// Native sounds first
|
|
504
|
+
for (const f of config.files) {
|
|
505
|
+
seen.add(f.name);
|
|
506
|
+
items.push({
|
|
507
|
+
label: f.name.replace(/\.(wav|mp3)$/, ""),
|
|
508
|
+
description: hookMap[f.name].join(", "),
|
|
509
|
+
file: f.name,
|
|
510
|
+
src: f.src,
|
|
511
|
+
native: true,
|
|
512
|
+
originCat: category,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Borrowed sounds from other categories
|
|
517
|
+
for (const otherCat of categories) {
|
|
518
|
+
if (otherCat === category) continue;
|
|
519
|
+
for (const f of theme.sounds[otherCat].files) {
|
|
520
|
+
if (seen.has(f.name)) continue;
|
|
521
|
+
seen.add(f.name);
|
|
522
|
+
items.push({
|
|
523
|
+
label: f.name.replace(/\.(wav|mp3)$/, ""),
|
|
524
|
+
description: hookMap[f.name].join(", "),
|
|
525
|
+
file: f.name,
|
|
526
|
+
src: f.src,
|
|
527
|
+
native: false,
|
|
528
|
+
originCat: otherCat,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return items;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Resolve a sound file's source from download (tmpDir/<srcBase>/...).
|
|
538
|
+
*/
|
|
539
|
+
function resolveDownloadSrc(srcBase, src) {
|
|
540
|
+
if (src.startsWith("@soundfxcenter/")) {
|
|
541
|
+
return path.join(srcBase, path.basename(src));
|
|
542
|
+
}
|
|
543
|
+
return path.join(srcBase, src);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ─── Reconfigure Flow ───────────────────────────────────────────────────────
|
|
547
|
+
|
|
548
|
+
async function reconfigure(existingInstall) {
|
|
549
|
+
const themeDir = path.join(THEMES_DIR, existingInstall.theme);
|
|
550
|
+
const theme = JSON.parse(fs.readFileSync(path.join(themeDir, "theme.json"), "utf-8"));
|
|
139
551
|
const categories = Object.keys(theme.sounds);
|
|
552
|
+
const tmpDirs = [];
|
|
140
553
|
|
|
141
|
-
// Preflight
|
|
142
554
|
try {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
555
|
+
let catIdx = 0;
|
|
556
|
+
while (catIdx < categories.length) {
|
|
557
|
+
const cat = categories[catIdx];
|
|
558
|
+
const config = theme.sounds[cat];
|
|
559
|
+
const catDir = path.join(SOUNDS_DIR, cat);
|
|
560
|
+
const disabledDir = path.join(catDir, ".disabled");
|
|
561
|
+
const items = buildCategoryItems(theme, cat);
|
|
562
|
+
|
|
563
|
+
// Determine current state: checked if file exists in category dir
|
|
564
|
+
const defaults = [];
|
|
565
|
+
for (let i = 0; i < items.length; i++) {
|
|
566
|
+
if (fs.existsSync(path.join(catDir, items[i].file))) {
|
|
567
|
+
defaults.push(i);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Build preview dir with all sounds from all possible locations
|
|
572
|
+
const previewDir = fs.mkdtempSync(path.join(os.tmpdir(), `claude-preview-`));
|
|
573
|
+
tmpDirs.push(previewDir);
|
|
574
|
+
for (const item of items) {
|
|
575
|
+
const originCatDir = path.join(SOUNDS_DIR, item.originCat);
|
|
576
|
+
const originDisabledDir = path.join(originCatDir, ".disabled");
|
|
577
|
+
const searchDirs = [catDir, disabledDir, originCatDir, originDisabledDir];
|
|
578
|
+
for (const dir of searchDirs) {
|
|
579
|
+
const p = path.join(dir, item.file);
|
|
580
|
+
if (fs.existsSync(p)) {
|
|
581
|
+
fs.copyFileSync(p, path.join(previewDir, item.file));
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const selected = await multiSelect(
|
|
588
|
+
`${BOLD}${cat}${RESET} ${DIM}— ${config.description}${RESET}`,
|
|
589
|
+
items,
|
|
590
|
+
defaults,
|
|
591
|
+
previewDir,
|
|
592
|
+
{ allowBack: catIdx > 0 }
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
// Back was pressed — go to previous category
|
|
596
|
+
if (selected === null) {
|
|
597
|
+
catIdx--;
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Apply changes
|
|
602
|
+
for (let i = 0; i < items.length; i++) {
|
|
603
|
+
const item = items[i];
|
|
604
|
+
const isSelected = selected.includes(i);
|
|
605
|
+
const enabledPath = path.join(catDir, item.file);
|
|
606
|
+
|
|
607
|
+
if (item.native) {
|
|
608
|
+
const disabledPath = path.join(disabledDir, item.file);
|
|
609
|
+
if (isSelected && !fs.existsSync(enabledPath) && fs.existsSync(disabledPath)) {
|
|
610
|
+
fs.renameSync(disabledPath, enabledPath);
|
|
611
|
+
} else if (!isSelected && fs.existsSync(enabledPath)) {
|
|
612
|
+
mkdirp(disabledDir);
|
|
613
|
+
fs.renameSync(enabledPath, disabledPath);
|
|
614
|
+
}
|
|
615
|
+
} else {
|
|
616
|
+
// Borrowed sound: copy in or delete
|
|
617
|
+
if (isSelected && !fs.existsSync(enabledPath)) {
|
|
618
|
+
const previewFile = path.join(previewDir, item.file);
|
|
619
|
+
if (fs.existsSync(previewFile)) {
|
|
620
|
+
fs.copyFileSync(previewFile, enabledPath);
|
|
621
|
+
}
|
|
622
|
+
} else if (!isSelected && fs.existsSync(enabledPath)) {
|
|
623
|
+
fs.unlinkSync(enabledPath);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
catIdx++;
|
|
629
|
+
}
|
|
630
|
+
} finally {
|
|
631
|
+
for (const dir of tmpDirs) {
|
|
632
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Summary
|
|
637
|
+
let total = 0;
|
|
638
|
+
print(` ${GREEN}✓${RESET} Configuration updated!`);
|
|
639
|
+
print(" ─────────────────────────────────────");
|
|
640
|
+
|
|
641
|
+
for (const cat of categories) {
|
|
642
|
+
const catDir = path.join(SOUNDS_DIR, cat);
|
|
643
|
+
let count = 0;
|
|
644
|
+
try {
|
|
645
|
+
for (const f of fs.readdirSync(catDir)) {
|
|
646
|
+
if (f.endsWith(".wav") || f.endsWith(".mp3")) count++;
|
|
647
|
+
}
|
|
648
|
+
} catch {}
|
|
649
|
+
total += count;
|
|
650
|
+
print(` ${cat} (${count}) — ${theme.sounds[cat].description}`);
|
|
146
651
|
}
|
|
147
652
|
|
|
148
653
|
print("");
|
|
149
|
-
print(
|
|
654
|
+
print(` ${total} sound files across ${categories.length} events.`);
|
|
655
|
+
print("");
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// ─── Install Flow ───────────────────────────────────────────────────────────
|
|
659
|
+
|
|
660
|
+
async function interactiveInstall(autoYes) {
|
|
661
|
+
print("");
|
|
662
|
+
print(` ${BOLD}claude-code-sounds${RESET}`);
|
|
150
663
|
print(" ──────────────────────────────");
|
|
151
|
-
print(` Theme: ${theme.name}`);
|
|
152
664
|
print("");
|
|
153
665
|
|
|
154
|
-
//
|
|
155
|
-
|
|
666
|
+
// ── Detect existing install ───────────────────────────────────────────────
|
|
667
|
+
|
|
668
|
+
const existingInstall = getExistingInstall();
|
|
669
|
+
|
|
670
|
+
if (existingInstall && !autoYes) {
|
|
671
|
+
print(` ${GREEN}✓${RESET} Already installed — ${BOLD}${existingInstall.themeDisplay}${RESET}`);
|
|
672
|
+
print(` ${existingInstall.totalEnabled}/${existingInstall.totalAvailable} sounds enabled\n`);
|
|
673
|
+
|
|
674
|
+
const actionIdx = await select("What would you like to do?", [
|
|
675
|
+
{ label: "Reconfigure", description: "Choose which sounds are enabled" },
|
|
676
|
+
{ label: "Reinstall", description: "Re-download and start fresh" },
|
|
677
|
+
{ label: "Uninstall", description: "Remove all sounds and hooks" },
|
|
678
|
+
]);
|
|
679
|
+
|
|
680
|
+
if (actionIdx === 0) {
|
|
681
|
+
await reconfigure(existingInstall);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (actionIdx === 2) {
|
|
685
|
+
uninstall();
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
// actionIdx === 1 falls through to full install
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// ── Step 1: Dependency Check ──────────────────────────────────────────────
|
|
692
|
+
|
|
693
|
+
const deps = ["afplay", "curl", "unzip"];
|
|
694
|
+
const missing = [];
|
|
695
|
+
|
|
696
|
+
print(" Checking dependencies...");
|
|
697
|
+
for (const dep of deps) {
|
|
698
|
+
const ok = hasCommand(dep);
|
|
699
|
+
if (ok) {
|
|
700
|
+
print(` ${GREEN}✓${RESET} ${dep}`);
|
|
701
|
+
} else {
|
|
702
|
+
print(` ${RED}✗${RESET} ${dep} — required${dep === "afplay" ? " (macOS only)" : ""}`);
|
|
703
|
+
missing.push(dep);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
print("");
|
|
707
|
+
|
|
708
|
+
if (missing.includes("afplay")) {
|
|
709
|
+
die("afplay is not available. claude-code-sounds requires macOS.");
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (missing.length > 0) {
|
|
713
|
+
if (autoYes) {
|
|
714
|
+
die(`Missing dependencies: ${missing.join(", ")}. Install them and try again.`);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const installDeps = await confirm(`Install missing dependencies with Homebrew?`, true);
|
|
718
|
+
if (installDeps) {
|
|
719
|
+
try {
|
|
720
|
+
exec("which brew");
|
|
721
|
+
} catch {
|
|
722
|
+
die("Homebrew not found. Install missing dependencies manually:\n brew install " + missing.join(" "));
|
|
723
|
+
}
|
|
724
|
+
print(` Installing ${missing.join(", ")}...`);
|
|
725
|
+
try {
|
|
726
|
+
exec(`brew install ${missing.join(" ")}`, { stdio: "inherit" });
|
|
727
|
+
print(` ${GREEN}✓${RESET} Dependencies installed.\n`);
|
|
728
|
+
} catch {
|
|
729
|
+
die("Failed to install dependencies. Run manually:\n brew install " + missing.join(" "));
|
|
730
|
+
}
|
|
731
|
+
} else {
|
|
732
|
+
die("Missing dependencies. Install them manually:\n brew install " + missing.join(" "));
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// ── Step 2: Theme Selection ───────────────────────────────────────────────
|
|
737
|
+
|
|
738
|
+
const themes = listThemes();
|
|
739
|
+
let selectedTheme;
|
|
740
|
+
|
|
741
|
+
if (themes.length === 0) {
|
|
742
|
+
die("No themes found in themes/ directory.");
|
|
743
|
+
} else if (themes.length === 1 || autoYes) {
|
|
744
|
+
selectedTheme = themes[0];
|
|
745
|
+
print(` Theme: ${BOLD}${selectedTheme.display}${RESET} — ${selectedTheme.description}\n`);
|
|
746
|
+
} else {
|
|
747
|
+
const options = themes.map((t) => ({ label: t.display, description: t.description }));
|
|
748
|
+
const idx = await select("Select a theme:", options);
|
|
749
|
+
selectedTheme = themes[idx];
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// ── Step 3: Download ──────────────────────────────────────────────────────
|
|
753
|
+
|
|
754
|
+
const themeDir = path.join(THEMES_DIR, selectedTheme.name);
|
|
755
|
+
const themeJsonPath = path.join(themeDir, "theme.json");
|
|
756
|
+
const theme = JSON.parse(fs.readFileSync(themeJsonPath, "utf-8"));
|
|
757
|
+
const categories = Object.keys(theme.sounds);
|
|
758
|
+
|
|
759
|
+
// Create directories
|
|
156
760
|
for (const cat of categories) {
|
|
157
761
|
mkdirp(path.join(SOUNDS_DIR, cat));
|
|
158
762
|
}
|
|
159
763
|
mkdirp(HOOKS_DIR);
|
|
160
764
|
|
|
161
|
-
|
|
162
|
-
print(" [2/4] Downloading sounds...");
|
|
765
|
+
print(" Downloading sounds...");
|
|
163
766
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "claude-sounds-"));
|
|
164
767
|
|
|
165
768
|
try {
|
|
@@ -167,44 +770,120 @@ function install(themeName) {
|
|
|
167
770
|
if (fs.existsSync(downloadScript)) {
|
|
168
771
|
exec(`bash "${downloadScript}" "${SOUNDS_DIR}" "${tmpDir}"`, { stdio: "inherit" });
|
|
169
772
|
}
|
|
773
|
+
print(` ${GREEN}✓${RESET} Download complete.\n`);
|
|
774
|
+
|
|
775
|
+
// ── Step 4: Customize or Accept Defaults ──────────────────────────────
|
|
776
|
+
|
|
777
|
+
// Build items and selections for each category (includes all theme sounds)
|
|
778
|
+
const categoryItems = {};
|
|
779
|
+
const selections = {};
|
|
780
|
+
for (const cat of categories) {
|
|
781
|
+
const items = buildCategoryItems(theme, cat);
|
|
782
|
+
categoryItems[cat] = items;
|
|
783
|
+
// Default: select only native sounds
|
|
784
|
+
selections[cat] = items.map((item, i) => item.native ? i : -1).filter(i => i >= 0);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (!autoYes) {
|
|
788
|
+
const customizeOptions = [
|
|
789
|
+
{ label: "No, use defaults", description: "Recommended" },
|
|
790
|
+
{ label: "Yes, let me pick", description: "Choose sounds per hook" },
|
|
791
|
+
];
|
|
792
|
+
const customizeIdx = await select("Customize sounds for each hook?", customizeOptions);
|
|
793
|
+
|
|
794
|
+
if (customizeIdx === 1) {
|
|
795
|
+
const srcBase = path.join(tmpDir, theme.srcBase || "Orc");
|
|
796
|
+
let catIdx = 0;
|
|
797
|
+
|
|
798
|
+
while (catIdx < categories.length) {
|
|
799
|
+
const cat = categories[catIdx];
|
|
800
|
+
const config = theme.sounds[cat];
|
|
801
|
+
const items = categoryItems[cat];
|
|
802
|
+
const defaults = selections[cat];
|
|
803
|
+
|
|
804
|
+
// Build preview dir with ALL theme sounds
|
|
805
|
+
const previewDir = path.join(tmpDir, "_preview", cat);
|
|
806
|
+
mkdirp(previewDir);
|
|
807
|
+
for (const item of items) {
|
|
808
|
+
const srcFile = resolveDownloadSrc(srcBase, item.src);
|
|
809
|
+
const destFile = path.join(previewDir, item.file);
|
|
810
|
+
if (fs.existsSync(srcFile)) {
|
|
811
|
+
fs.copyFileSync(srcFile, destFile);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const selected = await multiSelect(
|
|
816
|
+
`${BOLD}${cat}${RESET} ${DIM}— ${config.description}${RESET}`,
|
|
817
|
+
items,
|
|
818
|
+
defaults,
|
|
819
|
+
previewDir,
|
|
820
|
+
{ allowBack: catIdx > 0 }
|
|
821
|
+
);
|
|
822
|
+
|
|
823
|
+
if (selected === null) {
|
|
824
|
+
catIdx--;
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
selections[cat] = selected;
|
|
829
|
+
catIdx++;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// ── Step 5: Install & Summary ─────────────────────────────────────────
|
|
170
835
|
|
|
171
|
-
|
|
172
|
-
print(" [3/4] Sorting sounds...");
|
|
836
|
+
print(" Installing sounds...");
|
|
173
837
|
|
|
174
|
-
// Clear existing sounds
|
|
838
|
+
// Clear existing sounds and .disabled dirs
|
|
175
839
|
for (const cat of categories) {
|
|
176
840
|
const catDir = path.join(SOUNDS_DIR, cat);
|
|
177
841
|
for (const f of fs.readdirSync(catDir)) {
|
|
178
|
-
|
|
179
|
-
|
|
842
|
+
const fp = path.join(catDir, f);
|
|
843
|
+
if (f === ".disabled") {
|
|
844
|
+
fs.rmSync(fp, { recursive: true, force: true });
|
|
845
|
+
} else if (f.endsWith(".wav") || f.endsWith(".mp3")) {
|
|
846
|
+
fs.unlinkSync(fp);
|
|
180
847
|
}
|
|
181
848
|
}
|
|
182
849
|
}
|
|
183
850
|
|
|
184
|
-
// Copy files based on
|
|
185
|
-
const srcBase = path.join(tmpDir, "Orc");
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
851
|
+
// Copy files from download based on selections
|
|
852
|
+
const srcBase = path.join(tmpDir, theme.srcBase || "Orc");
|
|
853
|
+
let total = 0;
|
|
854
|
+
for (const cat of categories) {
|
|
855
|
+
const items = categoryItems[cat];
|
|
856
|
+
const selectedIndices = selections[cat];
|
|
857
|
+
const catDir = path.join(SOUNDS_DIR, cat);
|
|
858
|
+
const disabledDir = path.join(catDir, ".disabled");
|
|
859
|
+
|
|
860
|
+
for (let i = 0; i < items.length; i++) {
|
|
861
|
+
const item = items[i];
|
|
862
|
+
const srcFile = resolveDownloadSrc(srcBase, item.src);
|
|
863
|
+
|
|
864
|
+
if (!fs.existsSync(srcFile)) {
|
|
865
|
+
if (item.native) {
|
|
866
|
+
print(` ${YELLOW}⚠${RESET} ${item.src} not found, skipping`);
|
|
867
|
+
}
|
|
868
|
+
continue;
|
|
193
869
|
}
|
|
194
870
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
} else {
|
|
199
|
-
|
|
871
|
+
if (selectedIndices.includes(i)) {
|
|
872
|
+
fs.copyFileSync(srcFile, path.join(catDir, item.file));
|
|
873
|
+
total++;
|
|
874
|
+
} else if (item.native) {
|
|
875
|
+
// Save unselected native sounds to .disabled for future reconfigure
|
|
876
|
+
mkdirp(disabledDir);
|
|
877
|
+
fs.copyFileSync(srcFile, path.join(disabledDir, item.file));
|
|
200
878
|
}
|
|
879
|
+
// Unselected borrowed sounds: skip (no need to store)
|
|
201
880
|
}
|
|
202
881
|
}
|
|
203
882
|
|
|
204
|
-
//
|
|
205
|
-
|
|
883
|
+
// Write install marker
|
|
884
|
+
writeInstalled(selectedTheme.name);
|
|
206
885
|
|
|
207
|
-
// Copy play-sound.sh
|
|
886
|
+
// Copy play-sound.sh hook
|
|
208
887
|
const hookSrc = path.join(PKG_DIR, "hooks", "play-sound.sh");
|
|
209
888
|
const hookDest = path.join(HOOKS_DIR, "play-sound.sh");
|
|
210
889
|
fs.copyFileSync(hookSrc, hookDest);
|
|
@@ -217,14 +896,12 @@ function install(themeName) {
|
|
|
217
896
|
|
|
218
897
|
// Summary
|
|
219
898
|
print("");
|
|
220
|
-
print(
|
|
899
|
+
print(` ${GREEN}✓${RESET} Installed! Here's what you'll hear:`);
|
|
221
900
|
print(" ─────────────────────────────────────");
|
|
222
901
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
total += count;
|
|
227
|
-
print(` ${cat} (${count}) — ${config.description}`);
|
|
902
|
+
for (const cat of categories) {
|
|
903
|
+
const count = selections[cat].length;
|
|
904
|
+
print(` ${cat} (${count}) — ${theme.sounds[cat].description}`);
|
|
228
905
|
}
|
|
229
906
|
|
|
230
907
|
print("");
|
|
@@ -234,7 +911,7 @@ function install(themeName) {
|
|
|
234
911
|
print(" Zug zug.");
|
|
235
912
|
print("");
|
|
236
913
|
} finally {
|
|
237
|
-
|
|
914
|
+
killPreview();
|
|
238
915
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
239
916
|
}
|
|
240
917
|
}
|
|
@@ -242,21 +919,20 @@ function install(themeName) {
|
|
|
242
919
|
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
243
920
|
|
|
244
921
|
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);
|
|
922
|
+
const flags = new Set(args);
|
|
923
|
+
const autoYes = flags.has("--yes") || flags.has("-y");
|
|
924
|
+
|
|
925
|
+
// Handle non-interactive commands first
|
|
926
|
+
if (flags.has("--help") || flags.has("-h")) {
|
|
927
|
+
showHelp();
|
|
928
|
+
} else if (flags.has("--list") || flags.has("-l")) {
|
|
929
|
+
showList();
|
|
930
|
+
} else if (flags.has("--uninstall") || flags.has("--remove")) {
|
|
931
|
+
uninstall();
|
|
932
|
+
} else {
|
|
933
|
+
interactiveInstall(autoYes).catch((err) => {
|
|
934
|
+
killPreview();
|
|
935
|
+
process.stdout.write(SHOW_CURSOR);
|
|
936
|
+
die(err.message);
|
|
937
|
+
});
|
|
262
938
|
}
|