claude-code-sounds 1.2.0 → 1.3.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/bin/cli.js +718 -705
- package/package.json +5 -1
- package/themes/mario/sounds/1-up.wav +0 -0
- package/themes/mario/sounds/block-bump.wav +0 -0
- package/themes/mario/sounds/break-block.wav +0 -0
- package/themes/mario/sounds/burned.wav +0 -0
- package/themes/mario/sounds/coin.wav +0 -0
- package/themes/mario/sounds/death.wav +0 -0
- package/themes/mario/sounds/doh.wav +0 -0
- package/themes/mario/sounds/fireball.wav +0 -0
- package/themes/mario/sounds/flagpole.wav +0 -0
- package/themes/mario/sounds/game-over.wav +0 -0
- package/themes/mario/sounds/haha.wav +0 -0
- package/themes/mario/sounds/happy-message.wav +0 -0
- package/themes/mario/sounds/hello.wav +0 -0
- package/themes/mario/sounds/here-we-go.wav +0 -0
- package/themes/mario/sounds/hurt.wav +0 -0
- package/themes/mario/sounds/its-a-me-mario.wav +0 -0
- package/themes/mario/sounds/lets-a-go.wav +0 -0
- package/themes/mario/sounds/level-clear.wav +0 -0
- package/themes/mario/sounds/mamma-mia.wav +0 -0
- package/themes/mario/sounds/message-block.wav +0 -0
- package/themes/mario/sounds/mushroom-appears.wav +0 -0
- package/themes/mario/sounds/okey-dokey.wav +0 -0
- package/themes/mario/sounds/oof.wav +0 -0
- package/themes/mario/sounds/pause.wav +0 -0
- package/themes/mario/sounds/pipe-warp.wav +0 -0
- package/themes/mario/sounds/power-up.wav +0 -0
- package/themes/mario/sounds/question-block.wav +0 -0
- package/themes/mario/sounds/raccoon-transform.wav +0 -0
- package/themes/mario/sounds/shrink.wav +0 -0
- package/themes/mario/sounds/smb3-power-up.wav +0 -0
- package/themes/mario/sounds/smw-1-up.wav +0 -0
- package/themes/mario/sounds/smw-pipe.wav +0 -0
- package/themes/mario/sounds/smw-power-up.wav +0 -0
- package/themes/mario/sounds/snore.wav +0 -0
- package/themes/mario/sounds/stage-clear.wav +0 -0
- package/themes/mario/sounds/star-appears.wav +0 -0
- package/themes/mario/sounds/stomp.wav +0 -0
- package/themes/mario/sounds/tired.wav +0 -0
- package/themes/mario/sounds/vine-grow.wav +0 -0
- package/themes/mario/sounds/waha.wav +0 -0
- package/themes/mario/sounds/warp-whistle.wav +0 -0
- package/themes/mario/sounds/warp.wav +0 -0
- package/themes/mario/sounds/whoa.wav +0 -0
- package/themes/mario/sounds/world-clear.wav +0 -0
- package/themes/mario/sounds/yahoo.wav +0 -0
- package/themes/mario/sounds/yawn.wav +0 -0
- package/themes/mario/sounds/yippee.wav +0 -0
- package/themes/mario/theme.json +144 -48
- package/themes/mgs/sounds/alert-mode.mp3 +0 -0
- package/themes/mgs/sounds/alert-sfx.mp3 +0 -0
- package/themes/mgs/sounds/alert.mp3 +0 -0
- package/themes/mgs/sounds/cigar.mp3 +0 -0
- package/themes/mgs/sounds/codec-beep.mp3 +0 -0
- package/themes/mgs/sounds/codec-call-out.mp3 +0 -0
- package/themes/mgs/sounds/codec-call.mp3 +0 -0
- package/themes/mgs/sounds/codec-close.mp3 +0 -0
- package/themes/mgs/sounds/codec-dial.mp3 +0 -0
- package/themes/mgs/sounds/codec-exit.mp3 +0 -0
- package/themes/mgs/sounds/codec-hangup.mp3 +0 -0
- package/themes/mgs/sounds/codec-ring.mp3 +0 -0
- package/themes/mgs/sounds/friendly-fire.mp3 +0 -0
- package/themes/mgs/sounds/game-over-fade.mp3 +0 -0
- package/themes/mgs/sounds/game-over-screen.mp3 +0 -0
- package/themes/mgs/sounds/game-over.mp3 +0 -0
- package/themes/mgs/sounds/good-shooting.mp3 +0 -0
- package/themes/mgs/sounds/if-you-say-so.mp3 +0 -0
- package/themes/mgs/sounds/ill-do-my-best.mp3 +0 -0
- package/themes/mgs/sounds/item-drop.mp3 +0 -0
- package/themes/mgs/sounds/just-to-suffer.mp3 +0 -0
- package/themes/mgs/sounds/kept-you-waiting-huh.mp3 +0 -0
- package/themes/mgs/sounds/kept-you-waiting.mp3 +0 -0
- package/themes/mgs/sounds/mission-complete.mp3 +0 -0
- package/themes/mgs/sounds/mission-qualify.mp3 +0 -0
- package/themes/mgs/sounds/ocelot-meow.mp3 +0 -0
- package/themes/mgs/sounds/original-game-over.mp3 +0 -0
- package/themes/mgs/sounds/roger-that.mp3 +0 -0
- package/themes/mgs/sounds/snake-dies.mp3 +0 -0
- package/themes/mgs/sounds/snake-scream.mp3 +0 -0
- package/themes/mgs/sounds/sounds-like-a-plan.mp3 +0 -0
- package/themes/mgs/sounds/sweet-dreams.mp3 +0 -0
- package/themes/mgs/sounds/this-is-snake.mp3 +0 -0
- package/themes/mgs/sounds/what-the-hell.mp3 +0 -0
- package/themes/mgs/sounds/what-was-that-noise.mp3 +0 -0
- package/themes/mgs/sounds/what-was-that.mp3 +0 -0
- package/themes/mgs/sounds/youre-pretty-good.mp3 +0 -0
- package/themes/mgs/theme.json +123 -41
- package/themes/pokemon-gen1/sounds/ball-poof.wav +0 -0
- package/themes/pokemon-gen1/sounds/ball-toss.wav +0 -0
- package/themes/pokemon-gen1/sounds/caught-mon.wav +0 -0
- package/themes/pokemon-gen1/sounds/charizard-cry.wav +0 -0
- package/themes/pokemon-gen1/sounds/collision.wav +0 -0
- package/themes/pokemon-gen1/sounds/confused.wav +0 -0
- package/themes/pokemon-gen1/sounds/denied.wav +0 -0
- package/themes/pokemon-gen1/sounds/dex-page-added.wav +0 -0
- package/themes/pokemon-gen1/sounds/enter-pc.wav +0 -0
- package/themes/pokemon-gen1/sounds/faint-fall.wav +0 -0
- package/themes/pokemon-gen1/sounds/faint-thud.wav +0 -0
- package/themes/pokemon-gen1/sounds/get-item-fanfare.wav +0 -0
- package/themes/pokemon-gen1/sounds/get-item.wav +0 -0
- package/themes/pokemon-gen1/sounds/get-key-item.wav +0 -0
- package/themes/pokemon-gen1/sounds/go-outside.wav +0 -0
- package/themes/pokemon-gen1/sounds/heal-ailment.wav +0 -0
- package/themes/pokemon-gen1/sounds/heal-up.wav +0 -0
- package/themes/pokemon-gen1/sounds/intro-lunge.wav +0 -0
- package/themes/pokemon-gen1/sounds/intro-whoosh.wav +0 -0
- package/themes/pokemon-gen1/sounds/jigglypuff-cry.wav +0 -0
- package/themes/pokemon-gen1/sounds/ledge.wav +0 -0
- package/themes/pokemon-gen1/sounds/level-up.wav +0 -0
- package/themes/pokemon-gen1/sounds/pikachu-cry.wav +0 -0
- package/themes/pokemon-gen1/sounds/poisoned.wav +0 -0
- package/themes/pokemon-gen1/sounds/pokeball-open.wav +0 -0
- package/themes/pokemon-gen1/sounds/pokeball-throw.wav +0 -0
- package/themes/pokemon-gen1/sounds/pokedex-rating.wav +0 -0
- package/themes/pokemon-gen1/sounds/pokemon-switch.wav +0 -0
- package/themes/pokemon-gen1/sounds/press-ab.wav +0 -0
- package/themes/pokemon-gen1/sounds/psyduck-cry.wav +0 -0
- package/themes/pokemon-gen1/sounds/purchase.wav +0 -0
- package/themes/pokemon-gen1/sounds/rest.wav +0 -0
- package/themes/pokemon-gen1/sounds/save-game.wav +0 -0
- package/themes/pokemon-gen1/sounds/screech.wav +0 -0
- package/themes/pokemon-gen1/sounds/self-destruct.wav +0 -0
- package/themes/pokemon-gen1/sounds/shrink.wav +0 -0
- package/themes/pokemon-gen1/sounds/silph-scope.wav +0 -0
- package/themes/pokemon-gen1/sounds/slots-new-spin.wav +0 -0
- package/themes/pokemon-gen1/sounds/slots-stop.wav +0 -0
- package/themes/pokemon-gen1/sounds/splash.wav +0 -0
- package/themes/pokemon-gen1/sounds/start-menu.wav +0 -0
- package/themes/pokemon-gen1/sounds/substitute.wav +0 -0
- package/themes/pokemon-gen1/sounds/swap.wav +0 -0
- package/themes/pokemon-gen1/sounds/teleport.wav +0 -0
- package/themes/pokemon-gen1/sounds/tink.wav +0 -0
- package/themes/pokemon-gen1/sounds/trade-machine.wav +0 -0
- package/themes/pokemon-gen1/sounds/turn-off-pc.wav +0 -0
- package/themes/pokemon-gen1/sounds/withdraw-deposit.wav +0 -0
- package/themes/pokemon-gen1/theme.json +150 -50
- package/themes/portal/sounds/announcer-post.wav +0 -0
- package/themes/portal/sounds/button-positive.wav +0 -0
- package/themes/portal/sounds/button-press.wav +0 -0
- package/themes/portal/sounds/button-release.wav +0 -0
- package/themes/portal/sounds/core-attach.wav +0 -0
- package/themes/portal/sounds/core-complete.wav +0 -0
- package/themes/portal/sounds/emancipation-grill.wav +0 -0
- package/themes/portal/sounds/fizzler-shutdown.wav +0 -0
- package/themes/portal/sounds/fizzler-start.wav +0 -0
- package/themes/portal/sounds/invalid-surface.wav +0 -0
- package/themes/portal/sounds/klaxon-alarm.wav +0 -0
- package/themes/portal/sounds/portal-close-alt.wav +0 -0
- package/themes/portal/sounds/portal-close.wav +0 -0
- package/themes/portal/sounds/portal-enter.wav +0 -0
- package/themes/portal/sounds/portal-exit.wav +0 -0
- package/themes/portal/sounds/portal-fizzle.wav +0 -0
- package/themes/portal/sounds/portal-open.wav +0 -0
- package/themes/portal/sounds/portal-whoosh-close.wav +0 -0
- package/themes/portal/sounds/portalgun-powerup.wav +0 -0
- package/themes/portal/sounds/shoot-blue-portal.wav +0 -0
- package/themes/portal/sounds/shoot-orange-portal.wav +0 -0
- package/themes/portal/sounds/synth-negative.wav +0 -0
- package/themes/portal/sounds/synth-positive.wav +0 -0
- package/themes/portal/sounds/test-chamber-complete.wav +0 -0
- package/themes/portal/sounds/test-chamber-start.wav +0 -0
- package/themes/portal/sounds/turret-activated.wav +0 -0
- package/themes/portal/sounds/turret-alert.wav +0 -0
- package/themes/portal/sounds/turret-are-you-still-there.wav +0 -0
- package/themes/portal/sounds/turret-deploy.wav +0 -0
- package/themes/portal/sounds/turret-done.wav +0 -0
- package/themes/portal/sounds/turret-goodbye.wav +0 -0
- package/themes/portal/sounds/turret-hello.wav +0 -0
- package/themes/portal/sounds/turret-hellooo.wav +0 -0
- package/themes/portal/sounds/turret-i-see-you.wav +0 -0
- package/themes/portal/sounds/turret-no-hard-feelings.wav +0 -0
- package/themes/portal/sounds/turret-ow.wav +0 -0
- package/themes/portal/sounds/turret-ping.wav +0 -0
- package/themes/portal/sounds/turret-retract.wav +0 -0
- package/themes/portal/sounds/turret-searching.wav +0 -0
- package/themes/portal/sounds/turret-target-lost.wav +0 -0
- package/themes/portal/sounds/turret-whos-there.wav +0 -0
- package/themes/portal/sounds/ui-click.wav +0 -0
- package/themes/portal/theme.json +129 -43
- package/themes/star-wars/sounds/bad-feeling.wav +0 -0
- package/themes/star-wars/sounds/blaster-firing.wav +0 -0
- package/themes/star-wars/sounds/chewie-chatting.wav +0 -0
- package/themes/star-wars/sounds/chewie-roar.wav +0 -0
- package/themes/star-wars/sounds/dark-side.wav +0 -0
- package/themes/star-wars/sounds/darth-maul-reveal-ourselves.wav +0 -0
- package/themes/star-wars/sounds/decided-to-rescue-you.wav +0 -0
- package/themes/star-wars/sounds/destiny-fulfilled.wav +0 -0
- package/themes/star-wars/sounds/destroy-you.wav +0 -0
- package/themes/star-wars/sounds/force-is-strong-with-this-one.wav +0 -0
- package/themes/star-wars/sounds/force-is-strong.wav +0 -0
- package/themes/star-wars/sounds/force-will-be-with-you.wav +0 -0
- package/themes/star-wars/sounds/great-disturbance.wav +0 -0
- package/themes/star-wars/sounds/i-am-your-father.wav +0 -0
- package/themes/star-wars/sounds/its-a-trap.wav +0 -0
- package/themes/star-wars/sounds/jabba-laughing.wav +0 -0
- package/themes/star-wars/sounds/lightsaber-ignite.wav +0 -0
- package/themes/star-wars/sounds/lightsaber-off.wav +0 -0
- package/themes/star-wars/sounds/lord-vader-rise.wav +0 -0
- package/themes/star-wars/sounds/luke-dont-do-that.wav +0 -0
- package/themes/star-wars/sounds/r2d2-beep.wav +0 -0
- package/themes/star-wars/sounds/r2d2-hey-you.wav +0 -0
- package/themes/star-wars/sounds/r2d2-woo-hoo.wav +0 -0
- package/themes/star-wars/sounds/r2d2-yeah.wav +0 -0
- package/themes/star-wars/sounds/vader-breathing.wav +0 -0
- package/themes/star-wars/sounds/vader-what-is-thy-bidding.wav +0 -0
- package/themes/star-wars/sounds/vader-yes-my-master.wav +0 -0
- package/themes/star-wars/sounds/wilhelm-scream.wav +0 -0
- package/themes/star-wars/sounds/yoda-900-years-old.wav +0 -0
- package/themes/star-wars/sounds/yoda-always-two.wav +0 -0
- package/themes/star-wars/sounds/yoda-dangerous-disturbing.wav +0 -0
- package/themes/star-wars/sounds/yoda-do-or-do-not.wav +0 -0
- package/themes/star-wars/sounds/yoda-laughing.wav +0 -0
- package/themes/star-wars/sounds/yoda-much-fear.wav +0 -0
- package/themes/star-wars/sounds/yoda-twisted-by-dark-side.wav +0 -0
- package/themes/star-wars/sounds/you-were-the-chosen-one.wav +0 -0
- package/themes/star-wars/theme.json +112 -37
- package/themes/wc3-peon/sounds/anything-you-want.wav +0 -0
- package/themes/wc3-peon/sounds/be-happy-to.wav +0 -0
- package/themes/wc3-peon/sounds/concentrate-and-ask-again.wav +0 -0
- package/themes/wc3-peon/sounds/dabu.wav +0 -0
- package/themes/wc3-peon/sounds/death.wav +0 -0
- package/themes/wc3-peon/sounds/finally.wav +0 -0
- package/themes/wc3-peon/sounds/for-the-horde.wav +0 -0
- package/themes/wc3-peon/sounds/get-em.wav +0 -0
- package/themes/wc3-peon/sounds/grunt-death.wav +0 -0
- package/themes/wc3-peon/sounds/headhunter-death.wav +0 -0
- package/themes/wc3-peon/sounds/hmmm.wav +0 -0
- package/themes/wc3-peon/sounds/how-can-i-help.wav +0 -0
- package/themes/wc3-peon/sounds/i-can-do-that.wav +0 -0
- package/themes/wc3-peon/sounds/i-can-wait-no-longer.wav +0 -0
- package/themes/wc3-peon/sounds/ill-try.wav +0 -0
- package/themes/wc3-peon/sounds/immediately.wav +0 -0
- package/themes/wc3-peon/sounds/it-is-certain.wav +0 -0
- package/themes/wc3-peon/sounds/me-busy-leave-me-alone.wav +0 -0
- package/themes/wc3-peon/sounds/me-not-that-kind-of-orc.wav +0 -0
- package/themes/wc3-peon/sounds/more-work.mp3 +0 -0
- package/themes/wc3-peon/sounds/no-time-for-play.wav +0 -0
- package/themes/wc3-peon/sounds/not-easy-being-green.wav +0 -0
- package/themes/wc3-peon/sounds/of-course.wav +0 -0
- package/themes/wc3-peon/sounds/ok.wav +0 -0
- package/themes/wc3-peon/sounds/okie-dokie.wav +0 -0
- package/themes/wc3-peon/sounds/outlook-not-so-good.wav +0 -0
- package/themes/wc3-peon/sounds/peon-death.wav +0 -0
- package/themes/wc3-peon/sounds/ready-to-work.wav +0 -0
- package/themes/wc3-peon/sounds/reply-hazy-try-again.wav +0 -0
- package/themes/wc3-peon/sounds/right-away.wav +0 -0
- package/themes/wc3-peon/sounds/someone-call-for-the-doctor.wav +0 -0
- package/themes/wc3-peon/sounds/something-need-doing.wav +0 -0
- package/themes/wc3-peon/sounds/taste-the-fury.wav +0 -0
- package/themes/wc3-peon/sounds/understood.wav +0 -0
- package/themes/wc3-peon/sounds/well-done.wav +0 -0
- package/themes/wc3-peon/sounds/what-you-want-me-to-do.wav +0 -0
- package/themes/wc3-peon/sounds/what-you-want.wav +0 -0
- package/themes/wc3-peon/sounds/what.wav +0 -0
- package/themes/wc3-peon/sounds/whatever-you-say.wav +0 -0
- package/themes/wc3-peon/sounds/who-you-want-me-kill.wav +0 -0
- package/themes/wc3-peon/sounds/why-not.wav +0 -0
- package/themes/wc3-peon/sounds/why-you-poking-me.wav +0 -0
- package/themes/wc3-peon/sounds/work-work.wav +0 -0
- package/themes/wc3-peon/sounds/yes.wav +0 -0
- package/themes/wc3-peon/sounds/you-seek-me-help.wav +0 -0
- package/themes/wc3-peon/sounds/zug-zug.wav +0 -0
- package/themes/wc3-peon/theme.json +175 -58
- package/themes/zelda-oot/sounds/dialogue-done.wav +0 -0
- package/themes/zelda-oot/sounds/dialogue-next.wav +0 -0
- package/themes/zelda-oot/sounds/error.wav +0 -0
- package/themes/zelda-oot/sounds/ganondorf-laugh.wav +0 -0
- package/themes/zelda-oot/sounds/get-heart.wav +0 -0
- package/themes/zelda-oot/sounds/get-item.wav +0 -0
- package/themes/zelda-oot/sounds/get-rupee.wav +0 -0
- package/themes/zelda-oot/sounds/great-fairy-laugh.wav +0 -0
- package/themes/zelda-oot/sounds/hello.wav +0 -0
- package/themes/zelda-oot/sounds/hey.wav +0 -0
- package/themes/zelda-oot/sounds/item-fanfare.wav +0 -0
- package/themes/zelda-oot/sounds/link-attack.wav +0 -0
- package/themes/zelda-oot/sounds/link-hurt.wav +0 -0
- package/themes/zelda-oot/sounds/link-strong-attack.wav +0 -0
- package/themes/zelda-oot/sounds/listen.wav +0 -0
- package/themes/zelda-oot/sounds/look.wav +0 -0
- package/themes/zelda-oot/sounds/low-health.wav +0 -0
- package/themes/zelda-oot/sounds/menu-select.wav +0 -0
- package/themes/zelda-oot/sounds/open-chest.wav +0 -0
- package/themes/zelda-oot/sounds/open-small-chest.wav +0 -0
- package/themes/zelda-oot/sounds/pause-close.wav +0 -0
- package/themes/zelda-oot/sounds/pause-menu.wav +0 -0
- package/themes/zelda-oot/sounds/secret-discovered.wav +0 -0
- package/themes/zelda-oot/sounds/snore.wav +0 -0
- package/themes/zelda-oot/sounds/song-correct.wav +0 -0
- package/themes/zelda-oot/sounds/song-error.wav +0 -0
- package/themes/zelda-oot/sounds/spin-attack.wav +0 -0
- package/themes/zelda-oot/sounds/sword-draw.wav +0 -0
- package/themes/zelda-oot/sounds/watch-out.wav +0 -0
- package/themes/zelda-oot/sounds/z-target.wav +0 -0
- package/themes/zelda-oot/theme.json +144 -48
- package/themes/mario/download.sh +0 -123
- package/themes/mgs/download.sh +0 -74
- package/themes/pokemon-gen1/download.sh +0 -75
- package/themes/portal/download.sh +0 -95
- package/themes/star-wars/download.sh +0 -119
- package/themes/wc3-peon/download.sh +0 -31
- package/themes/zelda-oot/download.sh +0 -67
package/bin/cli.js
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
const fs = require("fs");
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const os = require("os");
|
|
6
|
-
const readline = require("readline");
|
|
7
6
|
const { execSync, spawn } = require("child_process");
|
|
7
|
+
const p = require("@clack/prompts");
|
|
8
|
+
const { Prompt } = require("@clack/core");
|
|
9
|
+
const color = require("picocolors");
|
|
8
10
|
|
|
9
11
|
// ─── Paths ───────────────────────────────────────────────────────────────────
|
|
10
12
|
|
|
@@ -18,15 +20,6 @@ const INSTALLED_PATH = path.join(SOUNDS_DIR, ".installed.json");
|
|
|
18
20
|
|
|
19
21
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
20
22
|
|
|
21
|
-
function print(msg = "") {
|
|
22
|
-
process.stdout.write(msg + "\n");
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function die(msg) {
|
|
26
|
-
console.error(`\n Error: ${msg}\n`);
|
|
27
|
-
process.exit(1);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
23
|
function mkdirp(dir) {
|
|
31
24
|
fs.mkdirSync(dir, { recursive: true });
|
|
32
25
|
}
|
|
@@ -35,13 +28,34 @@ function exec(cmd, opts = {}) {
|
|
|
35
28
|
return execSync(cmd, { encoding: "utf-8", stdio: "pipe", ...opts });
|
|
36
29
|
}
|
|
37
30
|
|
|
31
|
+
function hasCommand(name) {
|
|
32
|
+
try {
|
|
33
|
+
exec(`which ${name}`);
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
38
40
|
function listThemes() {
|
|
39
41
|
const themes = [];
|
|
40
42
|
for (const name of fs.readdirSync(THEMES_DIR)) {
|
|
41
43
|
const themeJson = path.join(THEMES_DIR, name, "theme.json");
|
|
42
44
|
if (!fs.existsSync(themeJson)) continue;
|
|
43
45
|
const meta = JSON.parse(fs.readFileSync(themeJson, "utf-8"));
|
|
44
|
-
|
|
46
|
+
let soundCount = 0;
|
|
47
|
+
if (meta.sounds) {
|
|
48
|
+
for (const cat of Object.values(meta.sounds)) {
|
|
49
|
+
soundCount += cat.files.length;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
themes.push({
|
|
53
|
+
name,
|
|
54
|
+
description: meta.description || "",
|
|
55
|
+
display: meta.name || name,
|
|
56
|
+
soundCount,
|
|
57
|
+
sources: meta.sources || [],
|
|
58
|
+
});
|
|
45
59
|
}
|
|
46
60
|
return themes;
|
|
47
61
|
}
|
|
@@ -58,15 +72,6 @@ function writeSettings(settings) {
|
|
|
58
72
|
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n");
|
|
59
73
|
}
|
|
60
74
|
|
|
61
|
-
function hasCommand(name) {
|
|
62
|
-
try {
|
|
63
|
-
exec(`which ${name}`);
|
|
64
|
-
return true;
|
|
65
|
-
} catch {
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
75
|
function readInstalled() {
|
|
71
76
|
if (fs.existsSync(INSTALLED_PATH)) {
|
|
72
77
|
return JSON.parse(fs.readFileSync(INSTALLED_PATH, "utf-8"));
|
|
@@ -74,72 +79,22 @@ function readInstalled() {
|
|
|
74
79
|
return null;
|
|
75
80
|
}
|
|
76
81
|
|
|
77
|
-
function writeInstalled(
|
|
82
|
+
function writeInstalled(data) {
|
|
78
83
|
mkdirp(SOUNDS_DIR);
|
|
79
|
-
fs.writeFileSync(INSTALLED_PATH, JSON.stringify(
|
|
84
|
+
fs.writeFileSync(INSTALLED_PATH, JSON.stringify(data, null, 2) + "\n");
|
|
80
85
|
}
|
|
81
86
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
};
|
|
87
|
+
function readThemeJson(themeName) {
|
|
88
|
+
return JSON.parse(
|
|
89
|
+
fs.readFileSync(path.join(THEMES_DIR, themeName, "theme.json"), "utf-8")
|
|
90
|
+
);
|
|
115
91
|
}
|
|
116
92
|
|
|
117
|
-
|
|
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);
|
|
93
|
+
function resolveThemeSoundPath(themeName, fileName) {
|
|
94
|
+
return path.join(THEMES_DIR, themeName, "sounds", fileName);
|
|
140
95
|
}
|
|
141
96
|
|
|
142
|
-
// ───
|
|
97
|
+
// ─── Preview ─────────────────────────────────────────────────────────────────
|
|
143
98
|
|
|
144
99
|
let previewProcess = null;
|
|
145
100
|
|
|
@@ -159,761 +114,818 @@ function playPreview(filePath) {
|
|
|
159
114
|
}
|
|
160
115
|
}
|
|
161
116
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
117
|
+
// ─── Hooks Config ────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
const HOOKS_CONFIG = {
|
|
120
|
+
SessionStart: [{ matcher: "startup", hooks: [{ type: "command", command: '/bin/bash "$HOME/.claude/hooks/play-sound.sh" start', timeout: 5 }] }],
|
|
121
|
+
SessionEnd: [{ hooks: [{ type: "command", command: '/bin/bash "$HOME/.claude/hooks/play-sound.sh" end', timeout: 5 }] }],
|
|
122
|
+
Notification: [
|
|
123
|
+
{ matcher: "permission_prompt", hooks: [{ type: "command", command: '/bin/bash "$HOME/.claude/hooks/play-sound.sh" permission', timeout: 5 }] },
|
|
124
|
+
{ matcher: "idle_prompt", hooks: [{ type: "command", command: '/bin/bash "$HOME/.claude/hooks/play-sound.sh" idle', timeout: 5 }] },
|
|
125
|
+
],
|
|
126
|
+
Stop: [{ hooks: [{ type: "command", command: '/bin/bash "$HOME/.claude/hooks/play-sound.sh" stop', timeout: 5 }] }],
|
|
127
|
+
SubagentStart: [{ hooks: [{ type: "command", command: '/bin/bash "$HOME/.claude/hooks/play-sound.sh" subagent', timeout: 5 }] }],
|
|
128
|
+
PostToolUseFailure: [{ hooks: [{ type: "command", command: '/bin/bash "$HOME/.claude/hooks/play-sound.sh" error', timeout: 5 }] }],
|
|
129
|
+
UserPromptSubmit: [{ hooks: [{ type: "command", command: '/bin/bash "$HOME/.claude/hooks/play-sound.sh" prompt', timeout: 5 }] }],
|
|
130
|
+
TaskCompleted: [{ hooks: [{ type: "command", command: '/bin/bash "$HOME/.claude/hooks/play-sound.sh" task-completed', timeout: 5 }] }],
|
|
131
|
+
PreCompact: [{ hooks: [{ type: "command", command: '/bin/bash "$HOME/.claude/hooks/play-sound.sh" compact', timeout: 5 }] }],
|
|
132
|
+
TeammateIdle: [{ hooks: [{ type: "command", command: '/bin/bash "$HOME/.claude/hooks/play-sound.sh" teammate-idle', timeout: 5 }] }],
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// ─── Sound Grid Prompt ───────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
const HOOKS = [
|
|
138
|
+
{ key: "start", abbr: "str", description: "Session starting" },
|
|
139
|
+
{ key: "prompt", abbr: "pmt", description: "User submitted prompt" },
|
|
140
|
+
{ key: "permission", abbr: "prm", description: "Permission prompt" },
|
|
141
|
+
{ key: "stop", abbr: "stp", description: "Done responding" },
|
|
142
|
+
{ key: "subagent", abbr: "sub", description: "Spawning subagent" },
|
|
143
|
+
{ key: "task-completed", abbr: "tsk", description: "Task finished" },
|
|
144
|
+
{ key: "error", abbr: "err", description: "Tool failure" },
|
|
145
|
+
{ key: "compact", abbr: "cmp", description: "Context compaction" },
|
|
146
|
+
{ key: "idle", abbr: "idl", description: "Waiting for input" },
|
|
147
|
+
{ key: "teammate-idle", abbr: "tmt", description: "Teammate went idle" },
|
|
148
|
+
{ key: "end", abbr: "end", description: "Session over" },
|
|
149
|
+
];
|
|
168
150
|
|
|
169
151
|
/**
|
|
170
|
-
*
|
|
171
|
-
*
|
|
152
|
+
* A 2D grid prompt for assigning sounds to hooks.
|
|
153
|
+
*
|
|
154
|
+
* Rows are sounds (grouped by theme with visual headers), columns are hooks.
|
|
155
|
+
* Navigate with arrow keys, toggle with space, preview with 'p', toggle
|
|
156
|
+
* entire column with 'a'.
|
|
157
|
+
*
|
|
158
|
+
* @param {object} opts
|
|
159
|
+
* @param {string} opts.message - Prompt title
|
|
160
|
+
* @param {Array<{type: 'header'|'sound', theme: string, label: string, fileName?: string, previewPath?: string}>} opts.rows
|
|
161
|
+
* @param {Array<{key: string, abbr: string, description: string}>} opts.hooks
|
|
162
|
+
* @param {boolean[][]} opts.initialGrid - [soundIndex][hookIndex]
|
|
172
163
|
*/
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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}`);
|
|
164
|
+
class SoundGrid extends Prompt {
|
|
165
|
+
constructor({ message, rows, hooks, initialGrid }) {
|
|
166
|
+
const soundIndices = [];
|
|
167
|
+
for (let i = 0; i < rows.length; i++) {
|
|
168
|
+
if (rows[i].type === 'sound') soundIndices.push(i);
|
|
188
169
|
}
|
|
189
170
|
|
|
190
|
-
|
|
191
|
-
|
|
171
|
+
super({
|
|
172
|
+
render() {
|
|
173
|
+
const grid = this._grid;
|
|
174
|
+
const cursorRow = this._cursorRow;
|
|
175
|
+
const cursorCol = this._cursorCol;
|
|
176
|
+
let scrollTop = this._scrollTop;
|
|
177
|
+
const myRows = this._rows;
|
|
178
|
+
const myHooks = this._hooks;
|
|
179
|
+
const myMessage = this._message;
|
|
180
|
+
const mySoundIndices = this._soundIndices;
|
|
181
|
+
|
|
182
|
+
const termCols = process.stdout.columns || 80;
|
|
183
|
+
const termRows = process.stdout.rows || 24;
|
|
184
|
+
const lines = [];
|
|
185
|
+
|
|
186
|
+
if (this.state === 'submit') {
|
|
187
|
+
lines.push(`${color.gray(p.S_BAR)}`);
|
|
188
|
+
let totalSel = 0;
|
|
189
|
+
for (const r of grid) for (const c of r) if (c) totalSel++;
|
|
190
|
+
lines.push(`${color.green(p.S_STEP_SUBMIT)} ${myMessage} ${color.dim(`(${totalSel} assigned)`)}`);
|
|
191
|
+
return lines.join('\n');
|
|
192
|
+
}
|
|
192
193
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
if (this.state === 'cancel') {
|
|
195
|
+
lines.push(`${color.gray(p.S_BAR)}`);
|
|
196
|
+
lines.push(`${color.red(p.S_STEP_ACTIVE)} ${myMessage}`);
|
|
197
|
+
return lines.join('\n');
|
|
198
|
+
}
|
|
196
199
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
200
|
+
// Active state
|
|
201
|
+
lines.push(`${color.gray(p.S_BAR)}`);
|
|
202
|
+
lines.push(`${color.cyan(p.S_STEP_ACTIVE)} ${myMessage}`);
|
|
203
|
+
|
|
204
|
+
// ── Layout calculation ──
|
|
205
|
+
// Line structure: "│ " (3) + cursor+space (2) + label (labelWidth-2) + [◂] + columns + [▸]
|
|
206
|
+
// Header uses: "│ " (3) + spaces (labelWidth) + [◂] + columns + [▸]
|
|
207
|
+
// Both total: 3 + labelWidth + margins + visibleCols*4
|
|
208
|
+
const totalHooks = myHooks.length;
|
|
209
|
+
const maxLabelWidth = 25;
|
|
210
|
+
const colWidth = 4;
|
|
211
|
+
const linePrefix = 3; // "│ "
|
|
212
|
+
|
|
213
|
+
// First try: all columns without scroll margins
|
|
214
|
+
let labelWidth = maxLabelWidth;
|
|
215
|
+
const noMarginCols = Math.floor((termCols - linePrefix - labelWidth) / colWidth);
|
|
216
|
+
let needsHScroll, visibleCols;
|
|
217
|
+
|
|
218
|
+
if (noMarginCols >= totalHooks) {
|
|
219
|
+
needsHScroll = false;
|
|
220
|
+
visibleCols = totalHooks;
|
|
221
|
+
} else {
|
|
222
|
+
// Need horizontal scroll — reserve 2 chars for ◂/▸ indicators
|
|
223
|
+
needsHScroll = true;
|
|
224
|
+
const withMarginCols = Math.floor((termCols - linePrefix - labelWidth - 2) / colWidth);
|
|
225
|
+
if (withMarginCols >= 1) {
|
|
226
|
+
visibleCols = withMarginCols;
|
|
227
|
+
} else {
|
|
228
|
+
// Very narrow — shrink label to fit at least 1 column
|
|
229
|
+
labelWidth = Math.max(8, termCols - linePrefix - 2 - colWidth);
|
|
230
|
+
visibleCols = 1;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
206
233
|
|
|
207
|
-
|
|
208
|
-
if (key === "\x1b[A" || key === "k") {
|
|
209
|
-
cursor = (cursor - 1 + options.length) % options.length;
|
|
210
|
-
render(false);
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
234
|
+
const maxLabel = labelWidth - 2; // label text area after cursor+space
|
|
213
235
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
236
|
+
// ── Horizontal scroll ──
|
|
237
|
+
let colStart = this._colStart || 0;
|
|
238
|
+
if (needsHScroll) {
|
|
239
|
+
if (cursorCol < colStart) colStart = cursorCol;
|
|
240
|
+
if (cursorCol >= colStart + visibleCols) colStart = cursorCol - visibleCols + 1;
|
|
241
|
+
colStart = Math.max(0, Math.min(colStart, totalHooks - visibleCols));
|
|
242
|
+
} else {
|
|
243
|
+
colStart = 0;
|
|
244
|
+
}
|
|
245
|
+
this._colStart = colStart;
|
|
246
|
+
|
|
247
|
+
const showLeftArrow = needsHScroll && colStart > 0;
|
|
248
|
+
const showRightArrow = needsHScroll && colStart + visibleCols < totalHooks;
|
|
249
|
+
const leftMargin = needsHScroll ? (showLeftArrow ? color.dim('\u25C2') : ' ') : '';
|
|
250
|
+
const rightMargin = needsHScroll ? (showRightArrow ? color.dim('\u25B8') : ' ') : '';
|
|
251
|
+
|
|
252
|
+
// ── Column header line ──
|
|
253
|
+
let headerLine = `${color.gray(p.S_BAR)} ${''.padEnd(labelWidth)}${leftMargin}`;
|
|
254
|
+
for (let c = colStart; c < colStart + visibleCols; c++) {
|
|
255
|
+
const abbr = myHooks[c].abbr.padStart(colWidth);
|
|
256
|
+
headerLine += c === cursorCol ? color.cyan(color.bold(abbr)) : color.dim(abbr);
|
|
257
|
+
}
|
|
258
|
+
headerLine += rightMargin;
|
|
259
|
+
lines.push(headerLine);
|
|
260
|
+
|
|
261
|
+
// ── Vertical scrolling ──
|
|
262
|
+
const reservedLines = 4;
|
|
263
|
+
const maxVisible = Math.max(5, termRows - lines.length - reservedLines - 2);
|
|
264
|
+
const currentRowIdx = mySoundIndices[cursorRow];
|
|
265
|
+
|
|
266
|
+
if (currentRowIdx < scrollTop) {
|
|
267
|
+
scrollTop = Math.max(0, currentRowIdx - 1);
|
|
268
|
+
} else if (currentRowIdx >= scrollTop + maxVisible) {
|
|
269
|
+
scrollTop = currentRowIdx - maxVisible + 1;
|
|
270
|
+
}
|
|
271
|
+
if (scrollTop > 0 && myRows[scrollTop]?.type === 'sound') {
|
|
272
|
+
for (let i = scrollTop - 1; i >= Math.max(0, scrollTop - 2); i--) {
|
|
273
|
+
if (myRows[i].type === 'header') {
|
|
274
|
+
scrollTop = i;
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
220
279
|
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
}
|
|
280
|
+
const showScrollUp = scrollTop > 0;
|
|
281
|
+
const showScrollDown = scrollTop + maxVisible < myRows.length;
|
|
235
282
|
|
|
236
|
-
|
|
237
|
-
});
|
|
238
|
-
}
|
|
283
|
+
if (showScrollUp) {
|
|
284
|
+
lines.push(`${color.gray(p.S_BAR)} ${color.dim(' \u25B2')}`);
|
|
285
|
+
}
|
|
239
286
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
287
|
+
// ── Render rows ──
|
|
288
|
+
const contentWidth = needsHScroll
|
|
289
|
+
? labelWidth + 2 + visibleCols * colWidth
|
|
290
|
+
: labelWidth + visibleCols * colWidth;
|
|
291
|
+
|
|
292
|
+
let visibleCount = 0;
|
|
293
|
+
for (let i = scrollTop; i < myRows.length && visibleCount < maxVisible; i++) {
|
|
294
|
+
const row = myRows[i];
|
|
295
|
+
if (row.type === 'header') {
|
|
296
|
+
const hdr = `\u2500\u2500 ${row.label} `;
|
|
297
|
+
const dashLen = Math.max(2, contentWidth - hdr.length);
|
|
298
|
+
lines.push(`${color.gray(p.S_BAR)} ${color.gray(hdr + '\u2500'.repeat(dashLen))}`);
|
|
299
|
+
} else {
|
|
300
|
+
const soundIdx = mySoundIndices.indexOf(i);
|
|
301
|
+
const isActiveRow = soundIdx === cursorRow;
|
|
302
|
+
const pointer = isActiveRow ? color.cyan('\u203A') : ' ';
|
|
303
|
+
const rawLabel = row.label.length > maxLabel
|
|
304
|
+
? row.label.substring(0, maxLabel - 1) + '\u2026'
|
|
305
|
+
: row.label;
|
|
306
|
+
const paddedLabel = rawLabel.padEnd(maxLabel);
|
|
307
|
+
const styledLabel = isActiveRow
|
|
308
|
+
? color.white(paddedLabel)
|
|
309
|
+
: color.dim(paddedLabel);
|
|
310
|
+
|
|
311
|
+
let cellsStr = leftMargin;
|
|
312
|
+
for (let c = colStart; c < colStart + visibleCols; c++) {
|
|
313
|
+
const isActive = isActiveRow && c === cursorCol;
|
|
314
|
+
const isChecked = grid[soundIdx][c];
|
|
315
|
+
const cell = isChecked ? ' [x]' : ' [ ]';
|
|
316
|
+
|
|
317
|
+
if (isActive) {
|
|
318
|
+
cellsStr += color.cyan(color.bold(cell));
|
|
319
|
+
} else if (isChecked) {
|
|
320
|
+
cellsStr += color.green(cell);
|
|
321
|
+
} else {
|
|
322
|
+
cellsStr += color.dim(cell);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
cellsStr += rightMargin;
|
|
263
326
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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}`);
|
|
327
|
+
lines.push(`${color.gray(p.S_BAR)} ${pointer} ${styledLabel}${cellsStr}`);
|
|
328
|
+
}
|
|
329
|
+
visibleCount++;
|
|
287
330
|
}
|
|
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
331
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
}
|
|
332
|
+
if (showScrollDown) {
|
|
333
|
+
lines.push(`${color.gray(p.S_BAR)} ${color.dim(' \u25BC')}`);
|
|
334
|
+
}
|
|
312
335
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
336
|
+
// ── Footer ──
|
|
337
|
+
lines.push(`${color.gray(p.S_BAR)}`);
|
|
338
|
+
const helpFull = '\u2191\u2193 sounds \u00B7 \u2190\u2192 hooks \u00B7 space toggle \u00B7 p preview \u00B7 a column all \u00B7 enter done';
|
|
339
|
+
const helpShort = '\u2191\u2193/\u2190\u2192 move \u00B7 space \u00B7 p play \u00B7 a all \u00B7 enter';
|
|
340
|
+
const helpText = (helpFull.length + 5 <= termCols) ? helpFull : helpShort;
|
|
341
|
+
lines.push(`${color.gray(p.S_BAR)} ${color.dim(helpText)}`);
|
|
342
|
+
|
|
343
|
+
const hook = myHooks[cursorCol];
|
|
344
|
+
const hookLine = needsHScroll
|
|
345
|
+
? `${color.dim('Hook:')} ${color.cyan(hook.key)} ${color.dim('\u2014')} ${color.dim(hook.description)} ${color.dim(`(${colStart + 1}\u2013${colStart + visibleCols} of ${totalHooks})`)}`
|
|
346
|
+
: `${color.dim('Hook:')} ${color.cyan(hook.key)} ${color.dim('\u2014')} ${color.dim(hook.description)}`;
|
|
347
|
+
lines.push(`${color.gray(p.S_BAR)} ${hookLine}`);
|
|
348
|
+
|
|
349
|
+
this._scrollTop = scrollTop;
|
|
350
|
+
return lines.join('\n');
|
|
351
|
+
},
|
|
352
|
+
}, false);
|
|
353
|
+
|
|
354
|
+
this._rows = rows;
|
|
355
|
+
this._hooks = hooks;
|
|
356
|
+
this._soundIndices = soundIndices;
|
|
357
|
+
this._message = message;
|
|
358
|
+
this._cursorRow = 0;
|
|
359
|
+
this._cursorCol = 0;
|
|
360
|
+
this._grid = initialGrid.map(r => [...r]);
|
|
361
|
+
this._scrollTop = 0;
|
|
362
|
+
this._colStart = 0;
|
|
363
|
+
|
|
364
|
+
this.on('cursor', (action) => {
|
|
365
|
+
if (action === 'up') {
|
|
366
|
+
if (this._soundIndices.length > 0) {
|
|
367
|
+
this._cursorRow = (this._cursorRow - 1 + this._soundIndices.length) % this._soundIndices.length;
|
|
368
|
+
const rowIdx = this._soundIndices[this._cursorRow];
|
|
369
|
+
const row = this._rows[rowIdx];
|
|
370
|
+
if (row?.previewPath) playPreview(row.previewPath);
|
|
371
|
+
}
|
|
372
|
+
} else if (action === 'down') {
|
|
373
|
+
if (this._soundIndices.length > 0) {
|
|
374
|
+
this._cursorRow = (this._cursorRow + 1) % this._soundIndices.length;
|
|
375
|
+
const rowIdx = this._soundIndices[this._cursorRow];
|
|
376
|
+
const row = this._rows[rowIdx];
|
|
377
|
+
if (row?.previewPath) playPreview(row.previewPath);
|
|
378
|
+
}
|
|
379
|
+
} else if (action === 'left') {
|
|
380
|
+
this._cursorCol = (this._cursorCol - 1 + this._hooks.length) % this._hooks.length;
|
|
381
|
+
} else if (action === 'right') {
|
|
382
|
+
this._cursorCol = (this._cursorCol + 1) % this._hooks.length;
|
|
383
|
+
} else if (action === 'space') {
|
|
384
|
+
this._grid[this._cursorRow][this._cursorCol] = !this._grid[this._cursorRow][this._cursorCol];
|
|
324
385
|
}
|
|
386
|
+
});
|
|
325
387
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
388
|
+
this.on('key', (char) => {
|
|
389
|
+
if (char === 'p') {
|
|
390
|
+
const rowIdx = this._soundIndices[this._cursorRow];
|
|
391
|
+
const row = this._rows[rowIdx];
|
|
392
|
+
if (row && row.previewPath) {
|
|
393
|
+
playPreview(row.previewPath);
|
|
394
|
+
}
|
|
331
395
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
396
|
+
if (char === 'a') {
|
|
397
|
+
const col = this._cursorCol;
|
|
398
|
+
const allChecked = this._grid.every(r => r[col]);
|
|
399
|
+
for (let i = 0; i < this._grid.length; i++) {
|
|
400
|
+
this._grid[i][col] = !allChecked;
|
|
401
|
+
}
|
|
338
402
|
}
|
|
403
|
+
});
|
|
339
404
|
|
|
340
|
-
|
|
341
|
-
if (
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
405
|
+
this.on('finalize', () => {
|
|
406
|
+
if (this.state === 'submit') {
|
|
407
|
+
const result = {};
|
|
408
|
+
for (let c = 0; c < this._hooks.length; c++) {
|
|
409
|
+
const hookKey = this._hooks[c].key;
|
|
410
|
+
result[hookKey] = [];
|
|
411
|
+
for (let r = 0; r < this._soundIndices.length; r++) {
|
|
412
|
+
if (this._grid[r][c]) {
|
|
413
|
+
const rowIdx = this._soundIndices[r];
|
|
414
|
+
const row = this._rows[rowIdx];
|
|
415
|
+
result[hookKey].push({ theme: row.theme, fileName: row.fileName });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
this.value = result;
|
|
345
420
|
}
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
346
424
|
|
|
347
|
-
|
|
348
|
-
if (key === "a") {
|
|
349
|
-
const allChecked = checked.every(Boolean);
|
|
350
|
-
checked.fill(!allChecked);
|
|
351
|
-
render(false);
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
425
|
+
// ─── Install Sounds ──────────────────────────────────────────────────────────
|
|
354
426
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
427
|
+
/**
|
|
428
|
+
* Copy selected sound files from package to SOUNDS_DIR.
|
|
429
|
+
*
|
|
430
|
+
* @param {Object<string, Array<{themeName: string, fileName: string}>>} selections
|
|
431
|
+
* @returns {number} Total files installed
|
|
432
|
+
*/
|
|
433
|
+
function installSounds(selections) {
|
|
434
|
+
let total = 0;
|
|
361
435
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
process.stdin.pause();
|
|
366
|
-
process.stdin.removeListener("data", onKey);
|
|
367
|
-
killPreview();
|
|
436
|
+
for (const [cat, items] of Object.entries(selections)) {
|
|
437
|
+
const catDir = path.join(SOUNDS_DIR, cat);
|
|
438
|
+
mkdirp(catDir);
|
|
368
439
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
440
|
+
// Clear existing sounds in this category
|
|
441
|
+
try {
|
|
442
|
+
for (const f of fs.readdirSync(catDir)) {
|
|
443
|
+
if (f.endsWith(".wav") || f.endsWith(".mp3")) {
|
|
444
|
+
fs.unlinkSync(path.join(catDir, f));
|
|
372
445
|
}
|
|
446
|
+
}
|
|
447
|
+
} catch {}
|
|
373
448
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
449
|
+
// Copy selected sounds
|
|
450
|
+
for (const item of items) {
|
|
451
|
+
const srcPath = resolveThemeSoundPath(item.themeName, item.fileName);
|
|
452
|
+
const destPath = path.join(catDir, item.fileName);
|
|
453
|
+
|
|
454
|
+
if (fs.existsSync(srcPath)) {
|
|
455
|
+
fs.copyFileSync(srcPath, destPath);
|
|
456
|
+
total++;
|
|
382
457
|
}
|
|
383
458
|
}
|
|
459
|
+
}
|
|
384
460
|
|
|
385
|
-
|
|
386
|
-
});
|
|
461
|
+
return total;
|
|
387
462
|
}
|
|
388
463
|
|
|
389
464
|
/**
|
|
390
|
-
*
|
|
465
|
+
* Write hooks config and copy play-sound.sh script.
|
|
391
466
|
*/
|
|
392
|
-
function
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
467
|
+
function installHooksConfig() {
|
|
468
|
+
mkdirp(HOOKS_DIR);
|
|
469
|
+
|
|
470
|
+
const hookSrc = path.join(PKG_DIR, "hooks", "play-sound.sh");
|
|
471
|
+
const hookDest = path.join(HOOKS_DIR, "play-sound.sh");
|
|
472
|
+
fs.copyFileSync(hookSrc, hookDest);
|
|
473
|
+
fs.chmodSync(hookDest, 0o755);
|
|
474
|
+
|
|
475
|
+
const settings = readSettings();
|
|
476
|
+
settings.hooks = HOOKS_CONFIG;
|
|
477
|
+
writeSettings(settings);
|
|
403
478
|
}
|
|
404
479
|
|
|
405
|
-
// ───
|
|
480
|
+
// ─── Print Summary ───────────────────────────────────────────────────────────
|
|
406
481
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
PostToolUseFailure: [{ hooks: [{ type: "command", command: '/bin/bash "$HOME/.claude/hooks/play-sound.sh" error', timeout: 5 }] }],
|
|
417
|
-
UserPromptSubmit: [{ hooks: [{ type: "command", command: '/bin/bash "$HOME/.claude/hooks/play-sound.sh" prompt', timeout: 5 }] }],
|
|
418
|
-
TaskCompleted: [{ hooks: [{ type: "command", command: '/bin/bash "$HOME/.claude/hooks/play-sound.sh" task-completed', timeout: 5 }] }],
|
|
419
|
-
PreCompact: [{ hooks: [{ type: "command", command: '/bin/bash "$HOME/.claude/hooks/play-sound.sh" compact', timeout: 5 }] }],
|
|
420
|
-
TeammateIdle: [{ hooks: [{ type: "command", command: '/bin/bash "$HOME/.claude/hooks/play-sound.sh" teammate-idle', timeout: 5 }] }],
|
|
421
|
-
};
|
|
482
|
+
function printSummary(selections) {
|
|
483
|
+
const cats = Object.keys(selections);
|
|
484
|
+
let total = 0;
|
|
485
|
+
|
|
486
|
+
for (const cat of cats) {
|
|
487
|
+
const count = selections[cat].length;
|
|
488
|
+
total += count;
|
|
489
|
+
p.log.step(`${cat} (${count})`);
|
|
490
|
+
}
|
|
422
491
|
|
|
423
|
-
|
|
492
|
+
p.log.success(`Installed ${total} sounds across ${cats.length} hooks.`);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ─── Non-Interactive Commands ────────────────────────────────────────────────
|
|
424
496
|
|
|
425
497
|
function showHelp() {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
498
|
+
console.log(`
|
|
499
|
+
claude-code-sounds
|
|
500
|
+
──────────────────────────────
|
|
501
|
+
|
|
502
|
+
Usage:
|
|
503
|
+
npx claude-code-sounds Interactive install
|
|
504
|
+
npx claude-code-sounds --yes Install defaults, skip prompts
|
|
505
|
+
npx claude-code-sounds --list List available themes
|
|
506
|
+
npx claude-code-sounds --uninstall Remove all sounds and hooks
|
|
507
|
+
npx claude-code-sounds --help Show this help
|
|
508
|
+
|
|
509
|
+
Flags:
|
|
510
|
+
-y, --yes Skip all prompts, use defaults
|
|
511
|
+
-l, --list List available themes
|
|
512
|
+
-h, --help Show this help
|
|
513
|
+
`);
|
|
442
514
|
}
|
|
443
515
|
|
|
444
516
|
function showList() {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
517
|
+
const themes = listThemes();
|
|
518
|
+
console.log("\n Available themes:\n");
|
|
519
|
+
for (const t of themes) {
|
|
520
|
+
const src = t.sources.length > 0 ? ` [${t.sources.join(", ")}]` : "";
|
|
521
|
+
console.log(` ${t.name} — ${t.description} (${t.soundCount} sounds)${src}`);
|
|
450
522
|
}
|
|
451
|
-
|
|
523
|
+
console.log();
|
|
452
524
|
}
|
|
453
525
|
|
|
454
|
-
function
|
|
455
|
-
print("");
|
|
456
|
-
print(" Uninstalling claude-code-sounds...");
|
|
457
|
-
|
|
526
|
+
function uninstallAll() {
|
|
458
527
|
if (fs.existsSync(SOUNDS_DIR)) {
|
|
459
528
|
fs.rmSync(SOUNDS_DIR, { recursive: true });
|
|
460
|
-
|
|
529
|
+
p.log.step("Removed ~/.claude/sounds/");
|
|
461
530
|
}
|
|
462
531
|
|
|
463
532
|
const hookScript = path.join(HOOKS_DIR, "play-sound.sh");
|
|
464
533
|
if (fs.existsSync(hookScript)) {
|
|
465
534
|
fs.unlinkSync(hookScript);
|
|
466
|
-
|
|
535
|
+
p.log.step("Removed ~/.claude/hooks/play-sound.sh");
|
|
467
536
|
}
|
|
468
537
|
|
|
469
538
|
if (fs.existsSync(SETTINGS_PATH)) {
|
|
470
539
|
const settings = readSettings();
|
|
471
540
|
delete settings.hooks;
|
|
472
541
|
writeSettings(settings);
|
|
473
|
-
|
|
542
|
+
p.log.step("Removed hooks from settings.json");
|
|
474
543
|
}
|
|
475
|
-
|
|
476
|
-
print("");
|
|
477
|
-
print(" Done. All sounds removed.");
|
|
478
|
-
print("");
|
|
479
544
|
}
|
|
480
545
|
|
|
481
|
-
// ───
|
|
546
|
+
// ─── Check Dependencies ─────────────────────────────────────────────────────
|
|
482
547
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
}
|
|
548
|
+
function checkDependencies() {
|
|
549
|
+
if (!hasCommand("afplay")) {
|
|
550
|
+
p.cancel("afplay is not available. claude-code-sounds requires macOS.");
|
|
551
|
+
process.exit(1);
|
|
501
552
|
}
|
|
502
|
-
|
|
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
553
|
}
|
|
535
554
|
|
|
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
|
-
}
|
|
555
|
+
// ─── Detect Existing Install ─────────────────────────────────────────────────
|
|
545
556
|
|
|
546
|
-
|
|
557
|
+
function detectExistingInstall() {
|
|
558
|
+
const installed = readInstalled();
|
|
559
|
+
if (!installed) return null;
|
|
547
560
|
|
|
548
|
-
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
const categories = Object.keys(theme.sounds);
|
|
552
|
-
const tmpDirs = [];
|
|
561
|
+
// Support both old format { theme: "name" } and new { themes: [...], mode }
|
|
562
|
+
const themeNames = installed.themes || (installed.theme ? [installed.theme] : []);
|
|
563
|
+
if (themeNames.length === 0) return null;
|
|
553
564
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
}
|
|
565
|
+
// Count enabled sounds across all categories
|
|
566
|
+
let totalEnabled = 0;
|
|
567
|
+
const allCategories = new Set();
|
|
600
568
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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
|
-
}
|
|
569
|
+
for (const themeName of themeNames) {
|
|
570
|
+
try {
|
|
571
|
+
const theme = readThemeJson(themeName);
|
|
572
|
+
for (const cat of Object.keys(theme.sounds)) {
|
|
573
|
+
allCategories.add(cat);
|
|
626
574
|
}
|
|
627
|
-
|
|
628
|
-
catIdx++;
|
|
629
|
-
}
|
|
630
|
-
} finally {
|
|
631
|
-
for (const dir of tmpDirs) {
|
|
632
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
633
|
-
}
|
|
575
|
+
} catch {}
|
|
634
576
|
}
|
|
635
577
|
|
|
636
|
-
|
|
637
|
-
let total = 0;
|
|
638
|
-
print(` ${GREEN}✓${RESET} Configuration updated!`);
|
|
639
|
-
print(" ─────────────────────────────────────");
|
|
640
|
-
|
|
641
|
-
for (const cat of categories) {
|
|
578
|
+
for (const cat of allCategories) {
|
|
642
579
|
const catDir = path.join(SOUNDS_DIR, cat);
|
|
643
|
-
let count = 0;
|
|
644
580
|
try {
|
|
645
581
|
for (const f of fs.readdirSync(catDir)) {
|
|
646
|
-
if (f.endsWith(".wav") || f.endsWith(".mp3"))
|
|
582
|
+
if (f.endsWith(".wav") || f.endsWith(".mp3")) totalEnabled++;
|
|
647
583
|
}
|
|
648
584
|
} catch {}
|
|
649
|
-
total += count;
|
|
650
|
-
print(` ${cat} (${count}) — ${theme.sounds[cat].description}`);
|
|
651
585
|
}
|
|
652
586
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
587
|
+
if (totalEnabled === 0) return null;
|
|
588
|
+
|
|
589
|
+
const displays = themeNames.map((n) => {
|
|
590
|
+
try { return readThemeJson(n).name; } catch { return n; }
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
return {
|
|
594
|
+
themes: themeNames,
|
|
595
|
+
themeDisplays: displays,
|
|
596
|
+
totalEnabled,
|
|
597
|
+
mode: installed.mode || "quick",
|
|
598
|
+
};
|
|
656
599
|
}
|
|
657
600
|
|
|
658
|
-
// ─── Install
|
|
601
|
+
// ─── Quick Install ───────────────────────────────────────────────────────────
|
|
659
602
|
|
|
660
|
-
async function
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
print(" ──────────────────────────────");
|
|
664
|
-
print("");
|
|
603
|
+
async function quickInstall(theme) {
|
|
604
|
+
const themeJson = readThemeJson(theme.name);
|
|
605
|
+
const categories = Object.keys(themeJson.sounds);
|
|
665
606
|
|
|
666
|
-
|
|
607
|
+
for (const cat of categories) mkdirp(path.join(SOUNDS_DIR, cat));
|
|
667
608
|
|
|
668
|
-
|
|
609
|
+
// Build selections: all native sounds per category
|
|
610
|
+
const selections = {};
|
|
611
|
+
for (const cat of categories) {
|
|
612
|
+
selections[cat] = themeJson.sounds[cat].files.map((f) => ({
|
|
613
|
+
themeName: theme.name,
|
|
614
|
+
fileName: f.name,
|
|
615
|
+
}));
|
|
616
|
+
}
|
|
669
617
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
618
|
+
const total = installSounds(selections);
|
|
619
|
+
writeInstalled({ themes: [theme.name], mode: "quick" });
|
|
620
|
+
installHooksConfig();
|
|
673
621
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
{ label: "Reinstall", description: "Re-download and start fresh" },
|
|
677
|
-
{ label: "Uninstall", description: "Remove all sounds and hooks" },
|
|
678
|
-
]);
|
|
622
|
+
p.log.success(`Installed ${total} sounds across ${categories.length} hooks.`);
|
|
623
|
+
}
|
|
679
624
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
625
|
+
// ─── Custom Install ──────────────────────────────────────────────────────────
|
|
626
|
+
|
|
627
|
+
async function customInstall(selectedThemes) {
|
|
628
|
+
const themeData = {};
|
|
629
|
+
for (const theme of selectedThemes) {
|
|
630
|
+
themeData[theme.name] = readThemeJson(theme.name);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Build rows (sound items + group headers)
|
|
634
|
+
const rows = [];
|
|
635
|
+
for (const theme of selectedThemes) {
|
|
636
|
+
const themeJson = themeData[theme.name];
|
|
637
|
+
rows.push({ type: 'header', theme: theme.name, label: theme.display });
|
|
638
|
+
|
|
639
|
+
const seenFiles = new Set();
|
|
640
|
+
for (const cat of Object.keys(themeJson.sounds)) {
|
|
641
|
+
for (const file of themeJson.sounds[cat].files) {
|
|
642
|
+
const key = `${theme.name}:${file.name}`;
|
|
643
|
+
if (seenFiles.has(key)) continue;
|
|
644
|
+
seenFiles.add(key);
|
|
645
|
+
|
|
646
|
+
const srcPath = resolveThemeSoundPath(theme.name, file.name);
|
|
647
|
+
rows.push({
|
|
648
|
+
type: 'sound',
|
|
649
|
+
theme: theme.name,
|
|
650
|
+
label: file.name.replace(/\.(wav|mp3)$/, ''),
|
|
651
|
+
fileName: file.name,
|
|
652
|
+
previewPath: fs.existsSync(srcPath) ? srcPath : undefined,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
687
655
|
}
|
|
688
|
-
// actionIdx === 1 falls through to full install
|
|
689
656
|
}
|
|
690
657
|
|
|
691
|
-
//
|
|
658
|
+
// Build initial grid: pre-check each sound for its native hook(s)
|
|
659
|
+
const soundOnlyRows = rows.filter(r => r.type === 'sound');
|
|
660
|
+
const initialGrid = soundOnlyRows.map(soundRow => {
|
|
661
|
+
return HOOKS.map(hook => {
|
|
662
|
+
const themeJson = themeData[soundRow.theme];
|
|
663
|
+
const catSounds = themeJson.sounds[hook.key];
|
|
664
|
+
if (!catSounds) return false;
|
|
665
|
+
return catSounds.files.some(f => f.name === soundRow.fileName);
|
|
666
|
+
});
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
const gridResult = await new SoundGrid({
|
|
670
|
+
message: 'Assign sounds to hooks',
|
|
671
|
+
rows,
|
|
672
|
+
hooks: HOOKS,
|
|
673
|
+
initialGrid,
|
|
674
|
+
}).prompt();
|
|
692
675
|
|
|
693
|
-
|
|
694
|
-
|
|
676
|
+
killPreview();
|
|
677
|
+
|
|
678
|
+
if (p.isCancel(gridResult)) {
|
|
679
|
+
p.cancel("Cancelled.");
|
|
680
|
+
process.exit(0);
|
|
681
|
+
}
|
|
695
682
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
683
|
+
// Convert grid result to selections format for installSounds()
|
|
684
|
+
const selections = {};
|
|
685
|
+
for (const hook of HOOKS) {
|
|
686
|
+
const items = gridResult[hook.key];
|
|
687
|
+
if (items && items.length > 0) {
|
|
688
|
+
selections[hook.key] = items.map(item => ({
|
|
689
|
+
themeName: item.theme,
|
|
690
|
+
fileName: item.fileName,
|
|
691
|
+
}));
|
|
704
692
|
}
|
|
705
693
|
}
|
|
706
|
-
print("");
|
|
707
694
|
|
|
708
|
-
|
|
709
|
-
|
|
695
|
+
for (const cat of Object.keys(selections)) {
|
|
696
|
+
mkdirp(path.join(SOUNDS_DIR, cat));
|
|
710
697
|
}
|
|
711
698
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
699
|
+
const total = installSounds(selections);
|
|
700
|
+
writeInstalled({ themes: selectedThemes.map((t) => t.name), mode: "custom" });
|
|
701
|
+
installHooksConfig();
|
|
702
|
+
|
|
703
|
+
printSummary(selections);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// ─── Reconfigure ─────────────────────────────────────────────────────────────
|
|
716
707
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
708
|
+
async function reconfigure(existingInstall) {
|
|
709
|
+
const allThemes = listThemes();
|
|
710
|
+
|
|
711
|
+
// Theme selection with current themes pre-checked
|
|
712
|
+
const themeValues = await p.multiselect({
|
|
713
|
+
message: "Select themes to include:",
|
|
714
|
+
options: allThemes.map((t) => ({
|
|
715
|
+
value: t.name,
|
|
716
|
+
label: t.display,
|
|
717
|
+
hint: `${t.soundCount} sounds — from ${t.sources.join(", ") || "local"}`,
|
|
718
|
+
})),
|
|
719
|
+
initialValues: existingInstall.themes,
|
|
720
|
+
required: true,
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
if (p.isCancel(themeValues)) {
|
|
724
|
+
p.cancel("Cancelled.");
|
|
725
|
+
process.exit(0);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const selectedThemes = allThemes.filter((t) => themeValues.includes(t.name));
|
|
729
|
+
const themeData = {};
|
|
730
|
+
for (const theme of selectedThemes) {
|
|
731
|
+
themeData[theme.name] = readThemeJson(theme.name);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Get currently installed files per category
|
|
735
|
+
const currentFiles = {};
|
|
736
|
+
for (const hook of HOOKS) {
|
|
737
|
+
currentFiles[hook.key] = new Set();
|
|
738
|
+
const catDir = path.join(SOUNDS_DIR, hook.key);
|
|
739
|
+
try {
|
|
740
|
+
for (const f of fs.readdirSync(catDir)) {
|
|
741
|
+
if (f.endsWith(".wav") || f.endsWith(".mp3")) {
|
|
742
|
+
currentFiles[hook.key].add(f);
|
|
743
|
+
}
|
|
723
744
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
745
|
+
} catch {}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Build rows
|
|
749
|
+
const rows = [];
|
|
750
|
+
for (const theme of selectedThemes) {
|
|
751
|
+
const themeJson = themeData[theme.name];
|
|
752
|
+
rows.push({ type: 'header', theme: theme.name, label: theme.display });
|
|
753
|
+
|
|
754
|
+
const seenFiles = new Set();
|
|
755
|
+
for (const cat of Object.keys(themeJson.sounds)) {
|
|
756
|
+
for (const file of themeJson.sounds[cat].files) {
|
|
757
|
+
const key = `${theme.name}:${file.name}`;
|
|
758
|
+
if (seenFiles.has(key)) continue;
|
|
759
|
+
seenFiles.add(key);
|
|
760
|
+
|
|
761
|
+
const srcPath = resolveThemeSoundPath(theme.name, file.name);
|
|
762
|
+
rows.push({
|
|
763
|
+
type: 'sound',
|
|
764
|
+
theme: theme.name,
|
|
765
|
+
label: file.name.replace(/\.(wav|mp3)$/, ''),
|
|
766
|
+
fileName: file.name,
|
|
767
|
+
previewPath: fs.existsSync(srcPath) ? srcPath : undefined,
|
|
768
|
+
});
|
|
730
769
|
}
|
|
731
|
-
} else {
|
|
732
|
-
die("Missing dependencies. Install them manually:\n brew install " + missing.join(" "));
|
|
733
770
|
}
|
|
734
771
|
}
|
|
735
772
|
|
|
736
|
-
//
|
|
773
|
+
// Build initial grid from currently installed files
|
|
774
|
+
const soundOnlyRows = rows.filter(r => r.type === 'sound');
|
|
775
|
+
const initialGrid = soundOnlyRows.map(soundRow => {
|
|
776
|
+
return HOOKS.map(hook => {
|
|
777
|
+
return currentFiles[hook.key]?.has(soundRow.fileName) || false;
|
|
778
|
+
});
|
|
779
|
+
});
|
|
737
780
|
|
|
738
|
-
const
|
|
739
|
-
|
|
781
|
+
const gridResult = await new SoundGrid({
|
|
782
|
+
message: 'Assign sounds to hooks',
|
|
783
|
+
rows,
|
|
784
|
+
hooks: HOOKS,
|
|
785
|
+
initialGrid,
|
|
786
|
+
}).prompt();
|
|
740
787
|
|
|
741
|
-
|
|
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
|
-
}
|
|
788
|
+
killPreview();
|
|
751
789
|
|
|
752
|
-
|
|
790
|
+
if (p.isCancel(gridResult)) {
|
|
791
|
+
p.cancel("Cancelled.");
|
|
792
|
+
process.exit(0);
|
|
793
|
+
}
|
|
753
794
|
|
|
754
|
-
|
|
755
|
-
const
|
|
756
|
-
const
|
|
757
|
-
|
|
795
|
+
// Convert grid result to selections
|
|
796
|
+
const selections = {};
|
|
797
|
+
for (const hook of HOOKS) {
|
|
798
|
+
const items = gridResult[hook.key];
|
|
799
|
+
if (items && items.length > 0) {
|
|
800
|
+
selections[hook.key] = items.map(item => ({
|
|
801
|
+
themeName: item.theme,
|
|
802
|
+
fileName: item.fileName,
|
|
803
|
+
}));
|
|
804
|
+
}
|
|
805
|
+
}
|
|
758
806
|
|
|
759
|
-
|
|
760
|
-
for (const cat of categories) {
|
|
807
|
+
for (const cat of Object.keys(selections)) {
|
|
761
808
|
mkdirp(path.join(SOUNDS_DIR, cat));
|
|
762
809
|
}
|
|
763
|
-
mkdirp(HOOKS_DIR);
|
|
764
810
|
|
|
765
|
-
|
|
766
|
-
|
|
811
|
+
const total = installSounds(selections);
|
|
812
|
+
writeInstalled({ themes: selectedThemes.map((t) => t.name), mode: "custom" });
|
|
813
|
+
installHooksConfig();
|
|
767
814
|
|
|
768
|
-
|
|
769
|
-
const downloadScript = path.join(themeDir, "download.sh");
|
|
770
|
-
if (fs.existsSync(downloadScript)) {
|
|
771
|
-
exec(`bash "${downloadScript}" "${SOUNDS_DIR}" "${tmpDir}"`, { stdio: "inherit" });
|
|
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
|
-
}
|
|
815
|
+
printSummary(selections);
|
|
786
816
|
|
|
787
|
-
|
|
788
|
-
|
|
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
|
-
}
|
|
817
|
+
return true;
|
|
818
|
+
}
|
|
814
819
|
|
|
815
|
-
|
|
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
|
-
}
|
|
820
|
+
// ─── Interactive Install ─────────────────────────────────────────────────────
|
|
827
821
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
}
|
|
822
|
+
async function interactiveInstall(autoYes) {
|
|
823
|
+
p.intro(color.bold("claude-code-sounds"));
|
|
833
824
|
|
|
834
|
-
|
|
825
|
+
checkDependencies();
|
|
835
826
|
|
|
836
|
-
|
|
827
|
+
const existing = detectExistingInstall();
|
|
837
828
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
}
|
|
848
|
-
|
|
829
|
+
if (existing && !autoYes) {
|
|
830
|
+
p.log.info(
|
|
831
|
+
`Already installed — ${color.bold(existing.themeDisplays.join(", "))} (${existing.totalEnabled} sounds enabled)`
|
|
832
|
+
);
|
|
833
|
+
|
|
834
|
+
const action = await p.select({
|
|
835
|
+
message: "What would you like to do?",
|
|
836
|
+
options: [
|
|
837
|
+
{ value: "modify", label: "Modify install", hint: "Add themes, change sounds" },
|
|
838
|
+
{ value: "fresh", label: "Fresh install", hint: "Start over from scratch" },
|
|
839
|
+
{ value: "uninstall", label: "Uninstall", hint: "Remove all sounds and hooks" },
|
|
840
|
+
],
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
if (p.isCancel(action)) {
|
|
844
|
+
p.cancel("Cancelled.");
|
|
845
|
+
process.exit(0);
|
|
849
846
|
}
|
|
850
847
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
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;
|
|
869
|
-
}
|
|
848
|
+
if (action === "uninstall") {
|
|
849
|
+
uninstallAll();
|
|
850
|
+
p.outro("All sounds removed.");
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
870
853
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
mkdirp(disabledDir);
|
|
877
|
-
fs.copyFileSync(srcFile, path.join(disabledDir, item.file));
|
|
878
|
-
}
|
|
879
|
-
// Unselected borrowed sounds: skip (no need to store)
|
|
854
|
+
if (action === "modify") {
|
|
855
|
+
const ok = await reconfigure(existing);
|
|
856
|
+
if (ok) {
|
|
857
|
+
p.outro("Start a new Claude Code session to hear it.");
|
|
858
|
+
return;
|
|
880
859
|
}
|
|
860
|
+
// Fall through to fresh install if reconfigure failed
|
|
881
861
|
}
|
|
862
|
+
// "fresh" falls through
|
|
863
|
+
}
|
|
882
864
|
|
|
883
|
-
|
|
884
|
-
|
|
865
|
+
const themes = listThemes();
|
|
866
|
+
if (themes.length === 0) {
|
|
867
|
+
p.cancel("No themes found in themes/ directory.");
|
|
868
|
+
process.exit(1);
|
|
869
|
+
}
|
|
885
870
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
871
|
+
// --yes: quick install first theme
|
|
872
|
+
if (autoYes) {
|
|
873
|
+
await quickInstall(themes[0]);
|
|
874
|
+
p.log.info(`To customize which sounds play on each hook, run:\n${color.gray(p.S_BAR)}\n${color.gray(p.S_BAR)} ${color.cyan("npx claude-code-sounds")}\n${color.gray(p.S_BAR)}\n${color.gray(p.S_BAR)} and choose ${color.bold("Modify install")}.`);
|
|
875
|
+
p.outro("Start a new Claude Code session to hear it.");
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
891
878
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
879
|
+
const mode = await p.select({
|
|
880
|
+
message: "How do you want to install?",
|
|
881
|
+
options: [
|
|
882
|
+
{ value: "quick", label: "Quick install", hint: "One theme, all defaults" },
|
|
883
|
+
{ value: "custom", label: "Custom mix", hint: "Pick sounds per hook from multiple themes" },
|
|
884
|
+
],
|
|
885
|
+
});
|
|
896
886
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
887
|
+
if (p.isCancel(mode)) {
|
|
888
|
+
p.cancel("Cancelled.");
|
|
889
|
+
process.exit(0);
|
|
890
|
+
}
|
|
901
891
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
892
|
+
if (mode === "quick") {
|
|
893
|
+
const themeValue = await p.select({
|
|
894
|
+
message: "Select a theme:",
|
|
895
|
+
options: themes.map((t) => ({
|
|
896
|
+
value: t.name,
|
|
897
|
+
label: t.display,
|
|
898
|
+
hint: `${t.soundCount} sounds — ${t.description} [${t.sources.join(", ") || "local"}]`,
|
|
899
|
+
})),
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
if (p.isCancel(themeValue)) {
|
|
903
|
+
p.cancel("Cancelled.");
|
|
904
|
+
process.exit(0);
|
|
905
905
|
}
|
|
906
906
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
907
|
+
await quickInstall(themes.find((t) => t.name === themeValue));
|
|
908
|
+
p.log.info(`To customize which sounds play on each hook, run:\n${color.gray(p.S_BAR)}\n${color.gray(p.S_BAR)} ${color.cyan("npx claude-code-sounds")}\n${color.gray(p.S_BAR)}\n${color.gray(p.S_BAR)} and choose ${color.bold("Modify install")}.`);
|
|
909
|
+
} else {
|
|
910
|
+
const themeValues = await p.multiselect({
|
|
911
|
+
message: "Select themes to include:",
|
|
912
|
+
options: themes.map((t) => ({
|
|
913
|
+
value: t.name,
|
|
914
|
+
label: t.display,
|
|
915
|
+
hint: `${t.soundCount} sounds — from ${t.sources.join(", ") || "local"}`,
|
|
916
|
+
})),
|
|
917
|
+
required: true,
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
if (p.isCancel(themeValues)) {
|
|
921
|
+
p.cancel("Cancelled.");
|
|
922
|
+
process.exit(0);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
await customInstall(themes.filter((t) => themeValues.includes(t.name)));
|
|
916
926
|
}
|
|
927
|
+
|
|
928
|
+
p.outro("Start a new Claude Code session to hear it.");
|
|
917
929
|
}
|
|
918
930
|
|
|
919
931
|
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
@@ -922,17 +934,18 @@ const args = process.argv.slice(2);
|
|
|
922
934
|
const flags = new Set(args);
|
|
923
935
|
const autoYes = flags.has("--yes") || flags.has("-y");
|
|
924
936
|
|
|
925
|
-
// Handle non-interactive commands first
|
|
926
937
|
if (flags.has("--help") || flags.has("-h")) {
|
|
927
938
|
showHelp();
|
|
928
939
|
} else if (flags.has("--list") || flags.has("-l")) {
|
|
929
940
|
showList();
|
|
930
941
|
} else if (flags.has("--uninstall") || flags.has("--remove")) {
|
|
931
|
-
|
|
942
|
+
p.intro(color.bold("claude-code-sounds"));
|
|
943
|
+
uninstallAll();
|
|
944
|
+
p.outro("All sounds removed.");
|
|
932
945
|
} else {
|
|
933
946
|
interactiveInstall(autoYes).catch((err) => {
|
|
934
947
|
killPreview();
|
|
935
|
-
|
|
936
|
-
|
|
948
|
+
p.cancel(err.message);
|
|
949
|
+
process.exit(1);
|
|
937
950
|
});
|
|
938
951
|
}
|