pingthings 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/LICENSE +11 -0
  2. package/README.md +262 -0
  3. package/bin/pingthings.js +78 -0
  4. package/package.json +34 -0
  5. package/packs/0ad-civilizations/manifest.json +77 -0
  6. package/packs/0ad-civilizations/sounds/alarm-alert.wav +0 -0
  7. package/packs/0ad-civilizations/sounds/alarm-attack.wav +0 -0
  8. package/packs/0ad-civilizations/sounds/alarm-defeat.wav +0 -0
  9. package/packs/0ad-civilizations/sounds/alarm-no-resource.wav +0 -0
  10. package/packs/0ad-civilizations/sounds/alarm-objective.wav +0 -0
  11. package/packs/0ad-civilizations/sounds/alarm-research-complete.wav +0 -0
  12. package/packs/0ad-civilizations/sounds/alarm-victory.wav +0 -0
  13. package/packs/0ad-civilizations/sounds/greek-as-you-wish.wav +0 -0
  14. package/packs/0ad-civilizations/sounds/greek-attack-1.wav +0 -0
  15. package/packs/0ad-civilizations/sounds/greek-build-1.wav +0 -0
  16. package/packs/0ad-civilizations/sounds/greek-heal-1.wav +0 -0
  17. package/packs/0ad-civilizations/sounds/greek-march-1.wav +0 -0
  18. package/packs/0ad-civilizations/sounds/greek-my-lord-1.wav +0 -0
  19. package/packs/0ad-civilizations/sounds/greek-my-lord-2.wav +0 -0
  20. package/packs/0ad-civilizations/sounds/greek-retreat-1.wav +0 -0
  21. package/packs/0ad-civilizations/sounds/greek-what-is-it-1.wav +0 -0
  22. package/packs/0ad-civilizations/sounds/greek-yes.wav +0 -0
  23. package/packs/0ad-civilizations/sounds/latin-attack-1.wav +0 -0
  24. package/packs/0ad-civilizations/sounds/latin-build-1.wav +0 -0
  25. package/packs/0ad-civilizations/sounds/latin-fight-1.wav +0 -0
  26. package/packs/0ad-civilizations/sounds/latin-heal-1.wav +0 -0
  27. package/packs/0ad-civilizations/sounds/latin-hello-1.wav +0 -0
  28. package/packs/0ad-civilizations/sounds/latin-my-lord-1.wav +0 -0
  29. package/packs/0ad-civilizations/sounds/latin-what-is-it.wav +0 -0
  30. package/packs/0ad-civilizations/sounds/persian-attack-1.wav +0 -0
  31. package/packs/0ad-civilizations/sounds/persian-by-your-order.wav +0 -0
  32. package/packs/0ad-civilizations/sounds/persian-forward-1.wav +0 -0
  33. package/packs/0ad-civilizations/sounds/persian-i-will-go.wav +0 -0
  34. package/packs/7kaa-soldiers/manifest.json +111 -0
  35. package/packs/7kaa-soldiers/sounds/00001-CH_A2.wav +0 -0
  36. package/packs/7kaa-soldiers/sounds/00006-NO_V2.wav +0 -0
  37. package/packs/7kaa-soldiers/sounds/00007-NO_V3.wav +0 -0
  38. package/packs/7kaa-soldiers/sounds/00008-MA_V1.wav +0 -0
  39. package/packs/7kaa-soldiers/sounds/00009-GR_V2.wav +0 -0
  40. package/packs/7kaa-soldiers/sounds/00010-GR_V3.wav +0 -0
  41. package/packs/7kaa-soldiers/sounds/00011-VI_V1.wav +0 -0
  42. package/packs/7kaa-soldiers/sounds/00012-NO_A1.wav +0 -0
  43. package/packs/7kaa-soldiers/sounds/00012-VI_V2.wav +0 -0
  44. package/packs/7kaa-soldiers/sounds/00013-GR_V1.wav +0 -0
  45. package/packs/7kaa-soldiers/sounds/00013-NO_A2.wav +0 -0
  46. package/packs/7kaa-soldiers/sounds/00014-MA_A1.wav +0 -0
  47. package/packs/7kaa-soldiers/sounds/00014-PE_V1.wav +0 -0
  48. package/packs/7kaa-soldiers/sounds/00015-GR_A1.wav +0 -0
  49. package/packs/7kaa-soldiers/sounds/00015-MA_V2.wav +0 -0
  50. package/packs/7kaa-soldiers/sounds/00016-MA_V3.wav +0 -0
  51. package/packs/7kaa-soldiers/sounds/00016-VI_A1.wav +0 -0
  52. package/packs/7kaa-soldiers/sounds/00017-MA_V4.wav +0 -0
  53. package/packs/7kaa-soldiers/sounds/00017-VI_A2.wav +0 -0
  54. package/packs/7kaa-soldiers/sounds/00018-PE_A2.wav +0 -0
  55. package/packs/7kaa-soldiers/sounds/00018-PE_V2.wav +0 -0
  56. package/packs/7kaa-soldiers/sounds/00019-PE_V3.wav +0 -0
  57. package/packs/7kaa-soldiers/sounds/00020-PE_A1.wav +0 -0
  58. package/packs/7kaa-soldiers/sounds/00020-PE_V4.wav +0 -0
  59. package/packs/7kaa-soldiers/sounds/00021-CH_A1.wav +0 -0
  60. package/packs/7kaa-soldiers/sounds/00021-CH_V1.wav +0 -0
  61. package/packs/7kaa-soldiers/sounds/00022-CH_V2.wav +0 -0
  62. package/packs/7kaa-soldiers/sounds/00022-JA_A1.wav +0 -0
  63. package/packs/7kaa-soldiers/sounds/00023-CH_V3.wav +0 -0
  64. package/packs/7kaa-soldiers/sounds/00023-JA_A2.wav +0 -0
  65. package/packs/7kaa-soldiers/sounds/00024-CH_V4.wav +0 -0
  66. package/packs/7kaa-soldiers/sounds/00025-CH_V5.wav +0 -0
  67. package/packs/7kaa-soldiers/sounds/00026-JA_V1.wav +0 -0
  68. package/packs/7kaa-soldiers/sounds/00027-JA_V2.wav +0 -0
  69. package/packs/7kaa-soldiers/sounds/00028-JA_V3.wav +0 -0
  70. package/packs/7kaa-soldiers/sounds/00029-JA_V4.wav +0 -0
  71. package/packs/7kaa-soldiers/sounds/00030-JA_V5.wav +0 -0
  72. package/packs/7kaa-soldiers/sounds/00035-MA_V5.wav +0 -0
  73. package/packs/7kaa-soldiers/sounds/00083-READY.wav +0 -0
  74. package/packs/7kaa-soldiers/sounds/00084-NO_V1.wav +0 -0
  75. package/packs/7kaa-soldiers/sounds/00085-EG_V1.wav +0 -0
  76. package/packs/7kaa-soldiers/sounds/00086-EG_V2.wav +0 -0
  77. package/packs/7kaa-soldiers/sounds/00087-EG_V3.wav +0 -0
  78. package/packs/7kaa-soldiers/sounds/00088-EG_V4.wav +0 -0
  79. package/packs/7kaa-soldiers/sounds/00089-IN_V1.wav +0 -0
  80. package/packs/7kaa-soldiers/sounds/00090-IN_V2.wav +0 -0
  81. package/packs/7kaa-soldiers/sounds/00091-IN_V3.wav +0 -0
  82. package/packs/7kaa-soldiers/sounds/00092-IN_V4.wav +0 -0
  83. package/packs/7kaa-soldiers/sounds/00093-ZU_V2.wav +0 -0
  84. package/packs/7kaa-soldiers/sounds/00094-ZU_V3.wav +0 -0
  85. package/packs/7kaa-soldiers/sounds/00095-ZU_V5.wav +0 -0
  86. package/packs/7kaa-soldiers/sounds/00096-ZU_V7.wav +0 -0
  87. package/packs/7kaa-soldiers/sounds/00097-ZU_V8.wav +0 -0
  88. package/packs/freedoom-arsenal/manifest.json +57 -0
  89. package/packs/freedoom-arsenal/sounds/dsbarexp.wav +0 -0
  90. package/packs/freedoom-arsenal/sounds/dsbfg.wav +0 -0
  91. package/packs/freedoom-arsenal/sounds/dsdoropn.wav +0 -0
  92. package/packs/freedoom-arsenal/sounds/dsdshtgn.wav +0 -0
  93. package/packs/freedoom-arsenal/sounds/dsgetpow.wav +0 -0
  94. package/packs/freedoom-arsenal/sounds/dsitemup.wav +0 -0
  95. package/packs/freedoom-arsenal/sounds/dsoof.wav +0 -0
  96. package/packs/freedoom-arsenal/sounds/dspistol.wav +0 -0
  97. package/packs/freedoom-arsenal/sounds/dsplasma.wav +0 -0
  98. package/packs/freedoom-arsenal/sounds/dspldeth.wav +0 -0
  99. package/packs/freedoom-arsenal/sounds/dspunch.wav +0 -0
  100. package/packs/freedoom-arsenal/sounds/dsrlaunc.wav +0 -0
  101. package/packs/freedoom-arsenal/sounds/dsrxplod.wav +0 -0
  102. package/packs/freedoom-arsenal/sounds/dssawup.wav +0 -0
  103. package/packs/freedoom-arsenal/sounds/dssgcock.wav +0 -0
  104. package/packs/freedoom-arsenal/sounds/dsshotgn.wav +0 -0
  105. package/packs/freedoom-arsenal/sounds/dsswtchn.wav +0 -0
  106. package/packs/freedoom-arsenal/sounds/dstelept.wav +0 -0
  107. package/packs/freedoom-arsenal/sounds/dswpnup.wav +0 -0
  108. package/packs/openarena-announcer/manifest.json +54 -0
  109. package/packs/openarena-announcer/sounds/accuracy.wav +0 -0
  110. package/packs/openarena-announcer/sounds/assist.wav +0 -0
  111. package/packs/openarena-announcer/sounds/defense.wav +0 -0
  112. package/packs/openarena-announcer/sounds/denied.wav +0 -0
  113. package/packs/openarena-announcer/sounds/excellent.wav +0 -0
  114. package/packs/openarena-announcer/sounds/fight.wav +0 -0
  115. package/packs/openarena-announcer/sounds/gauntlet.wav +0 -0
  116. package/packs/openarena-announcer/sounds/humiliation.wav +0 -0
  117. package/packs/openarena-announcer/sounds/impressive.wav +0 -0
  118. package/packs/openarena-announcer/sounds/one.wav +0 -0
  119. package/packs/openarena-announcer/sounds/perfect.wav +0 -0
  120. package/packs/openarena-announcer/sounds/prepare.wav +0 -0
  121. package/packs/openarena-announcer/sounds/sudden_death.wav +0 -0
  122. package/packs/openarena-announcer/sounds/takenlead.wav +0 -0
  123. package/packs/openarena-announcer/sounds/three.wav +0 -0
  124. package/packs/openarena-announcer/sounds/two.wav +0 -0
  125. package/packs/openarena-announcer/sounds/voc_holyshit.wav +0 -0
  126. package/packs/openarena-announcer/sounds/youwin.wav +0 -0
  127. package/packs/warzone2100-command/manifest.json +63 -0
  128. package/packs/warzone2100-command/sounds/construction-completed.wav +0 -0
  129. package/packs/warzone2100-command/sounds/design-completed.wav +0 -0
  130. package/packs/warzone2100-command/sounds/enemy-detected.wav +0 -0
  131. package/packs/warzone2100-command/sounds/incoming-enemy-transport.wav +0 -0
  132. package/packs/warzone2100-command/sounds/incoming-transmission.wav +0 -0
  133. package/packs/warzone2100-command/sounds/major-research-completed.wav +0 -0
  134. package/packs/warzone2100-command/sounds/mission-failed.wav +0 -0
  135. package/packs/warzone2100-command/sounds/mission-objective.wav +0 -0
  136. package/packs/warzone2100-command/sounds/mission-successful.wav +0 -0
  137. package/packs/warzone2100-command/sounds/mission-update.wav +0 -0
  138. package/packs/warzone2100-command/sounds/new-research-available.wav +0 -0
  139. package/packs/warzone2100-command/sounds/nexus-laugh1.wav +0 -0
  140. package/packs/warzone2100-command/sounds/nexus-laugh2.wav +0 -0
  141. package/packs/warzone2100-command/sounds/power-low.wav +0 -0
  142. package/packs/warzone2100-command/sounds/production-cancelled.wav +0 -0
  143. package/packs/warzone2100-command/sounds/production-completed.wav +0 -0
  144. package/packs/warzone2100-command/sounds/reinforcements-available.wav +0 -0
  145. package/packs/warzone2100-command/sounds/research-completed.wav +0 -0
  146. package/packs/warzone2100-command/sounds/retreat-heavy-damage.wav +0 -0
  147. package/packs/warzone2100-command/sounds/structure-under-attack.wav +0 -0
  148. package/packs/warzone2100-command/sounds/unit-repaired.wav +0 -0
  149. package/packs/wesnoth-combat/manifest.json +58 -0
  150. package/packs/wesnoth-combat/sounds/axe.wav +0 -0
  151. package/packs/wesnoth-combat/sounds/bow.wav +0 -0
  152. package/packs/wesnoth-combat/sounds/crossbow.wav +0 -0
  153. package/packs/wesnoth-combat/sounds/dwarf-laugh.wav +0 -0
  154. package/packs/wesnoth-combat/sounds/explosion.wav +0 -0
  155. package/packs/wesnoth-combat/sounds/fanfare-short.wav +0 -0
  156. package/packs/wesnoth-combat/sounds/flame-big.wav +0 -0
  157. package/packs/wesnoth-combat/sounds/gold.wav +0 -0
  158. package/packs/wesnoth-combat/sounds/gryphon-shriek-1.wav +0 -0
  159. package/packs/wesnoth-combat/sounds/hatchet.wav +0 -0
  160. package/packs/wesnoth-combat/sounds/heal.wav +0 -0
  161. package/packs/wesnoth-combat/sounds/lightning.wav +0 -0
  162. package/packs/wesnoth-combat/sounds/magic-dark.wav +0 -0
  163. package/packs/wesnoth-combat/sounds/magic-holy-1.wav +0 -0
  164. package/packs/wesnoth-combat/sounds/magic-missile-1.wav +0 -0
  165. package/packs/wesnoth-combat/sounds/open-chest.wav +0 -0
  166. package/packs/wesnoth-combat/sounds/potion.wav +0 -0
  167. package/packs/wesnoth-combat/sounds/sword-1.wav +0 -0
  168. package/packs/wesnoth-combat/sounds/wolf-growl-1.wav +0 -0
  169. package/src/cli/config.js +42 -0
  170. package/src/cli/install.js +8 -0
  171. package/src/cli/list.js +25 -0
  172. package/src/cli/play.js +127 -0
  173. package/src/cli/preview.js +30 -0
  174. package/src/cli/use.js +25 -0
  175. package/src/config.js +48 -0
  176. package/src/packs.js +130 -0
  177. package/src/player.js +39 -0
@@ -0,0 +1,127 @@
1
+ import { readConfig, VALID_EVENTS } from '../config.js';
2
+ import { getPackSounds, getEventSounds, pickRandom, resolvePack } from '../packs.js';
3
+ import { playSound } from '../player.js';
4
+ import { basename } from 'node:path';
5
+
6
+ function parseArgs(args) {
7
+ const result = { sound: null, event: null };
8
+
9
+ for (let i = 0; i < args.length; i++) {
10
+ if ((args[i] === '--event' || args[i] === '-e') && args[i + 1]) {
11
+ result.event = args[i + 1];
12
+ i++;
13
+ } else if (args[i] === '--help' || args[i] === '-h') {
14
+ showHelp();
15
+ process.exit(0);
16
+ } else if (!args[i].startsWith('-')) {
17
+ result.sound = args[i];
18
+ }
19
+ }
20
+
21
+ return result;
22
+ }
23
+
24
+ function showHelp() {
25
+ console.log(`
26
+ Usage: pingthings play [sound] [options]
27
+
28
+ Play a sound from the active pack.
29
+
30
+ Arguments:
31
+ sound Play a specific sound by name (partial match)
32
+
33
+ Options:
34
+ --event, -e <type> Play a sound mapped to an event type
35
+ --help, -h Show this help message
36
+
37
+ Event types:
38
+ done Task/step finished, awaiting next instruction
39
+ permission Needs user approval to continue
40
+ complete Whole project or major milestone finished
41
+ error Something went wrong
42
+ blocked Can't proceed, user action needed
43
+
44
+ Modes (set via "pingthings config mode <mode>"):
45
+ random Play any random sound from the pack (default)
46
+ specific Always play the same configured sound
47
+ informational Use event mappings — requires --event flag
48
+
49
+ Examples:
50
+ pingthings play Random sound
51
+ pingthings play READY Play sound matching "READY"
52
+ pingthings play --event done Play a "done" sound
53
+ pingthings play -e error Play an "error" sound
54
+ `);
55
+ }
56
+
57
+ export default function play(args) {
58
+ const config = readConfig();
59
+ const packName = config.activePack;
60
+ const pack = resolvePack(packName);
61
+
62
+ if (!pack) {
63
+ console.error(`Pack not found: ${packName}`);
64
+ console.error('Run "pingthings list" to see available packs.');
65
+ process.exit(1);
66
+ }
67
+
68
+ const parsed = parseArgs(args);
69
+
70
+ // Validate event if provided
71
+ if (parsed.event && !VALID_EVENTS.includes(parsed.event)) {
72
+ console.error(`Unknown event type: ${parsed.event}`);
73
+ console.error(`Valid events: ${VALID_EVENTS.join(', ')}`);
74
+ process.exit(1);
75
+ }
76
+
77
+ let soundFile;
78
+
79
+ // If --event flag is provided, use event mapping
80
+ if (parsed.event) {
81
+ const eventSounds = getEventSounds(packName, parsed.event);
82
+ if (eventSounds.length === 0) {
83
+ // Fall back to random if pack has no mapping for this event
84
+ const allSounds = getPackSounds(packName);
85
+ soundFile = pickRandom(allSounds);
86
+ } else {
87
+ soundFile = pickRandom(eventSounds);
88
+ }
89
+ }
90
+ // If a specific sound name is provided, find it
91
+ else if (parsed.sound) {
92
+ const sounds = getPackSounds(packName);
93
+ const query = parsed.sound.toLowerCase();
94
+ soundFile = sounds.find(s => basename(s).toLowerCase().includes(query));
95
+ if (!soundFile) {
96
+ console.error(`Sound "${parsed.sound}" not found in pack "${packName}".`);
97
+ process.exit(1);
98
+ }
99
+ }
100
+ // Informational mode: requires --event (show help if missing)
101
+ else if (config.mode === 'informational') {
102
+ console.error('Informational mode requires an event type.');
103
+ console.error('Usage: pingthings play --event <done|permission|complete|error|blocked>');
104
+ console.error('Or switch mode: pingthings config mode random');
105
+ process.exit(1);
106
+ }
107
+ // Specific mode: always play the configured sound
108
+ else if (config.mode === 'specific' && config.specificSound) {
109
+ const sounds = getPackSounds(packName);
110
+ const query = config.specificSound.toLowerCase();
111
+ soundFile = sounds.find(s => basename(s).toLowerCase().includes(query));
112
+ if (!soundFile) {
113
+ soundFile = pickRandom(sounds);
114
+ }
115
+ }
116
+ // Random mode (default)
117
+ else {
118
+ const sounds = getPackSounds(packName);
119
+ if (sounds.length === 0) {
120
+ console.error(`No sounds found in pack: ${packName}`);
121
+ process.exit(1);
122
+ }
123
+ soundFile = pickRandom(sounds);
124
+ }
125
+
126
+ playSound(soundFile);
127
+ }
@@ -0,0 +1,30 @@
1
+ import { getPackSounds, pickRandom, resolvePack } from '../packs.js';
2
+ import { playSound } from '../player.js';
3
+ import { basename } from 'node:path';
4
+
5
+ export default function preview(args) {
6
+ const packName = args[0];
7
+
8
+ if (!packName) {
9
+ console.error('Usage: pingthings preview <pack>');
10
+ console.error('Run "pingthings list" to see available packs.');
11
+ process.exit(1);
12
+ }
13
+
14
+ const pack = resolvePack(packName);
15
+ if (!pack) {
16
+ console.error(`Pack not found: ${packName}`);
17
+ console.error('Run "pingthings list" to see available packs.');
18
+ process.exit(1);
19
+ }
20
+
21
+ const sounds = getPackSounds(packName);
22
+ if (sounds.length === 0) {
23
+ console.error(`No sounds found in pack: ${packName}`);
24
+ process.exit(1);
25
+ }
26
+
27
+ const sound = pickRandom(sounds);
28
+ console.log(`Playing: ${basename(sound)} from "${packName}"`);
29
+ playSound(sound);
30
+ }
package/src/cli/use.js ADDED
@@ -0,0 +1,25 @@
1
+ import { readConfig, writeConfig } from '../config.js';
2
+ import { resolvePack } from '../packs.js';
3
+
4
+ export default function use(args) {
5
+ const packName = args[0];
6
+
7
+ if (!packName) {
8
+ console.error('Usage: pingthings use <pack>');
9
+ console.error('Run "pingthings list" to see available packs.');
10
+ process.exit(1);
11
+ }
12
+
13
+ const pack = resolvePack(packName);
14
+ if (!pack) {
15
+ console.error(`Pack not found: ${packName}`);
16
+ console.error('Run "pingthings list" to see available packs.');
17
+ process.exit(1);
18
+ }
19
+
20
+ const config = readConfig();
21
+ config.activePack = packName;
22
+ writeConfig(config);
23
+
24
+ console.log(`Active pack set to: ${packName}`);
25
+ }
package/src/config.js ADDED
@@ -0,0 +1,48 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+
5
+ const DEFAULTS = {
6
+ activePack: '7kaa-soldiers',
7
+ mode: 'random',
8
+ specificSound: null,
9
+ };
10
+
11
+ export function getConfigDir() {
12
+ const base = process.env.XDG_CONFIG_HOME || join(homedir(), '.config');
13
+ const dir = join(base, 'pingthings');
14
+ mkdirSync(dir, { recursive: true });
15
+ return dir;
16
+ }
17
+
18
+ export function getConfigPath() {
19
+ return join(getConfigDir(), 'config.json');
20
+ }
21
+
22
+ export function readConfig() {
23
+ const configPath = getConfigPath();
24
+ if (!existsSync(configPath)) {
25
+ return { ...DEFAULTS };
26
+ }
27
+ try {
28
+ const raw = readFileSync(configPath, 'utf8');
29
+ return { ...DEFAULTS, ...JSON.parse(raw) };
30
+ } catch {
31
+ return { ...DEFAULTS };
32
+ }
33
+ }
34
+
35
+ export function writeConfig(config) {
36
+ const configPath = getConfigPath();
37
+ const tmpPath = configPath + '.tmp';
38
+ writeFileSync(tmpPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
39
+ renameSync(tmpPath, configPath);
40
+ }
41
+
42
+ export function getDefaults() {
43
+ return { ...DEFAULTS };
44
+ }
45
+
46
+ export const VALID_MODES = ['random', 'specific', 'informational'];
47
+
48
+ export const VALID_EVENTS = ['done', 'permission', 'complete', 'error', 'blocked'];
package/src/packs.js ADDED
@@ -0,0 +1,130 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
+ import { join, extname, basename } from 'node:path';
3
+ import { dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { getConfigDir } from './config.js';
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const BUILT_IN_PACKS_DIR = join(__dirname, '..', 'packs');
9
+ const AUDIO_EXTENSIONS = new Set(['.wav', '.mp3', '.ogg', '.flac']);
10
+
11
+ function getUserPacksDir() {
12
+ return join(getConfigDir(), 'packs');
13
+ }
14
+
15
+ function readManifest(packDir) {
16
+ const manifestPath = join(packDir, 'manifest.json');
17
+ if (!existsSync(manifestPath)) return null;
18
+ try {
19
+ return JSON.parse(readFileSync(manifestPath, 'utf8'));
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ function discoverSounds(packDir) {
26
+ const soundsDir = join(packDir, 'sounds');
27
+ const searchDir = existsSync(soundsDir) ? soundsDir : packDir;
28
+
29
+ try {
30
+ return readdirSync(searchDir)
31
+ .filter(f => AUDIO_EXTENSIONS.has(extname(f).toLowerCase()))
32
+ .map(f => join(searchDir, f))
33
+ .sort();
34
+ } catch {
35
+ return [];
36
+ }
37
+ }
38
+
39
+ function scanDirectory(dir, isBuiltIn) {
40
+ if (!existsSync(dir)) return [];
41
+
42
+ return readdirSync(dir, { withFileTypes: true })
43
+ .filter(d => d.isDirectory())
44
+ .map(d => {
45
+ const packDir = join(dir, d.name);
46
+ const manifest = readManifest(packDir);
47
+ const sounds = discoverSounds(packDir);
48
+
49
+ return {
50
+ name: manifest?.name || d.name,
51
+ description: manifest?.description || '',
52
+ license: manifest?.license || 'Unknown',
53
+ credits: manifest?.credits || '',
54
+ location: packDir,
55
+ soundCount: sounds.length,
56
+ isBuiltIn,
57
+ };
58
+ })
59
+ .filter(p => p.soundCount > 0);
60
+ }
61
+
62
+ export function listPacks() {
63
+ const builtIn = scanDirectory(BUILT_IN_PACKS_DIR, true);
64
+ const user = scanDirectory(getUserPacksDir(), false);
65
+
66
+ // User packs override built-in packs with the same name
67
+ const names = new Set(user.map(p => p.name));
68
+ const merged = [...user, ...builtIn.filter(p => !names.has(p.name))];
69
+
70
+ return merged.sort((a, b) => a.name.localeCompare(b.name));
71
+ }
72
+
73
+ export function resolvePack(name) {
74
+ // Check user packs first (override built-in)
75
+ const userDir = join(getUserPacksDir(), name);
76
+ if (existsSync(userDir)) {
77
+ const manifest = readManifest(userDir);
78
+ return { name, dir: userDir, manifest };
79
+ }
80
+
81
+ // Check built-in packs
82
+ const builtInDir = join(BUILT_IN_PACKS_DIR, name);
83
+ if (existsSync(builtInDir)) {
84
+ const manifest = readManifest(builtInDir);
85
+ return { name, dir: builtInDir, manifest };
86
+ }
87
+
88
+ return null;
89
+ }
90
+
91
+ export function getPackSounds(name) {
92
+ const pack = resolvePack(name);
93
+ if (!pack) return [];
94
+
95
+ // Use manifest sounds list if available
96
+ if (pack.manifest?.sounds?.length) {
97
+ return pack.manifest.sounds
98
+ .map(s => join(pack.dir, s))
99
+ .filter(f => existsSync(f));
100
+ }
101
+
102
+ return discoverSounds(pack.dir);
103
+ }
104
+
105
+ export function getEventSounds(name, event) {
106
+ const pack = resolvePack(name);
107
+ if (!pack) return [];
108
+
109
+ const eventSounds = pack.manifest?.events?.[event];
110
+ if (!eventSounds?.length) return [];
111
+
112
+ return eventSounds
113
+ .map(s => join(pack.dir, s))
114
+ .filter(f => existsSync(f));
115
+ }
116
+
117
+ export function getPackEvents(name) {
118
+ const pack = resolvePack(name);
119
+ if (!pack) return [];
120
+
121
+ const events = pack.manifest?.events;
122
+ if (!events) return [];
123
+
124
+ return Object.keys(events);
125
+ }
126
+
127
+ export function pickRandom(sounds) {
128
+ if (sounds.length === 0) return null;
129
+ return sounds[Math.floor(Math.random() * sounds.length)];
130
+ }
package/src/player.js ADDED
@@ -0,0 +1,39 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { platform } from 'node:os';
4
+
5
+ function getPlayerCommand() {
6
+ switch (platform()) {
7
+ case 'darwin':
8
+ return 'afplay';
9
+ case 'linux': {
10
+ // Prefer PulseAudio/PipeWire, fall back to ALSA
11
+ try {
12
+ const result = spawn('which', ['paplay'], { stdio: 'pipe' });
13
+ return 'paplay';
14
+ } catch {
15
+ return 'aplay';
16
+ }
17
+ }
18
+ default:
19
+ return null;
20
+ }
21
+ }
22
+
23
+ export function playSound(filePath) {
24
+ if (!existsSync(filePath)) {
25
+ throw new Error(`Sound file not found: ${filePath}`);
26
+ }
27
+
28
+ const cmd = getPlayerCommand();
29
+ if (!cmd) {
30
+ throw new Error(`Unsupported platform: ${platform()}. Supported: macOS, Linux.`);
31
+ }
32
+
33
+ const child = spawn(cmd, [filePath], {
34
+ detached: true,
35
+ stdio: 'ignore',
36
+ });
37
+
38
+ child.unref();
39
+ }