pingthings 0.3.0 → 0.5.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 (119) hide show
  1. package/README.md +24 -0
  2. package/bin/pingthings.js +12 -0
  3. package/package.json +1 -1
  4. package/packs/0ad-civilizations/manifest.json +1 -0
  5. package/packs/7kaa-soldiers/manifest.json +1 -0
  6. package/packs/fighting-announcer/manifest.json +62 -0
  7. package/packs/fighting-announcer/sounds/begin.wav +0 -0
  8. package/packs/fighting-announcer/sounds/complete.wav +0 -0
  9. package/packs/fighting-announcer/sounds/congratulations.wav +0 -0
  10. package/packs/fighting-announcer/sounds/defeat.wav +0 -0
  11. package/packs/fighting-announcer/sounds/failure.wav +0 -0
  12. package/packs/fighting-announcer/sounds/fight.wav +0 -0
  13. package/packs/fighting-announcer/sounds/final-round.wav +0 -0
  14. package/packs/fighting-announcer/sounds/finish.wav +0 -0
  15. package/packs/fighting-announcer/sounds/game-over.wav +0 -0
  16. package/packs/fighting-announcer/sounds/get-set.wav +0 -0
  17. package/packs/fighting-announcer/sounds/go.wav +0 -0
  18. package/packs/fighting-announcer/sounds/great.wav +0 -0
  19. package/packs/fighting-announcer/sounds/high-score.wav +0 -0
  20. package/packs/fighting-announcer/sounds/new-record.wav +0 -0
  21. package/packs/fighting-announcer/sounds/no-contest.wav +0 -0
  22. package/packs/fighting-announcer/sounds/ready.wav +0 -0
  23. package/packs/fighting-announcer/sounds/sudden-death.wav +0 -0
  24. package/packs/fighting-announcer/sounds/victory.wav +0 -0
  25. package/packs/fighting-announcer/sounds/you-lose.wav +0 -0
  26. package/packs/fighting-announcer/sounds/you-win.wav +0 -0
  27. package/packs/freedoom-arsenal/manifest.json +1 -0
  28. package/packs/kenney-interface/manifest.json +58 -0
  29. package/packs/kenney-interface/sounds/back-1.wav +0 -0
  30. package/packs/kenney-interface/sounds/bong.wav +0 -0
  31. package/packs/kenney-interface/sounds/click-1.wav +0 -0
  32. package/packs/kenney-interface/sounds/close-1.wav +0 -0
  33. package/packs/kenney-interface/sounds/confirmation-1.wav +0 -0
  34. package/packs/kenney-interface/sounds/confirmation-2.wav +0 -0
  35. package/packs/kenney-interface/sounds/confirmation-3.wav +0 -0
  36. package/packs/kenney-interface/sounds/error-1.wav +0 -0
  37. package/packs/kenney-interface/sounds/error-2.wav +0 -0
  38. package/packs/kenney-interface/sounds/error-3.wav +0 -0
  39. package/packs/kenney-interface/sounds/glitch-1.wav +0 -0
  40. package/packs/kenney-interface/sounds/maximize-1.wav +0 -0
  41. package/packs/kenney-interface/sounds/maximize-2.wav +0 -0
  42. package/packs/kenney-interface/sounds/open-1.wav +0 -0
  43. package/packs/kenney-interface/sounds/pluck-1.wav +0 -0
  44. package/packs/kenney-interface/sounds/question-1.wav +0 -0
  45. package/packs/kenney-interface/sounds/question-2.wav +0 -0
  46. package/packs/kenney-interface/sounds/question-3.wav +0 -0
  47. package/packs/kenney-scifi/manifest.json +58 -0
  48. package/packs/kenney-scifi/sounds/computer-1.wav +0 -0
  49. package/packs/kenney-scifi/sounds/computer-2.wav +0 -0
  50. package/packs/kenney-scifi/sounds/computer-3.wav +0 -0
  51. package/packs/kenney-scifi/sounds/door-close.wav +0 -0
  52. package/packs/kenney-scifi/sounds/door-open.wav +0 -0
  53. package/packs/kenney-scifi/sounds/engine-1.wav +0 -0
  54. package/packs/kenney-scifi/sounds/explosion-1.wav +0 -0
  55. package/packs/kenney-scifi/sounds/explosion-2.wav +0 -0
  56. package/packs/kenney-scifi/sounds/forcefield-1.wav +0 -0
  57. package/packs/kenney-scifi/sounds/forcefield-2.wav +0 -0
  58. package/packs/kenney-scifi/sounds/impact-1.wav +0 -0
  59. package/packs/kenney-scifi/sounds/impact-2.wav +0 -0
  60. package/packs/kenney-scifi/sounds/laser-large.wav +0 -0
  61. package/packs/kenney-scifi/sounds/laser-retro.wav +0 -0
  62. package/packs/kenney-scifi/sounds/low-explosion.wav +0 -0
  63. package/packs/kenney-scifi/sounds/slime.wav +0 -0
  64. package/packs/kenney-scifi/sounds/thruster-1.wav +0 -0
  65. package/packs/kenney-scifi/sounds/thruster-2.wav +0 -0
  66. package/packs/openarena-announcer/manifest.json +1 -0
  67. package/packs/retro-8bit/manifest.json +58 -0
  68. package/packs/retro-8bit/sounds/button-1.wav +0 -0
  69. package/packs/retro-8bit/sounds/button-2.wav +0 -0
  70. package/packs/retro-8bit/sounds/button-3.wav +0 -0
  71. package/packs/retro-8bit/sounds/coin-1.wav +0 -0
  72. package/packs/retro-8bit/sounds/coin-2.wav +0 -0
  73. package/packs/retro-8bit/sounds/coin-cluster.wav +0 -0
  74. package/packs/retro-8bit/sounds/coin-double.wav +0 -0
  75. package/packs/retro-8bit/sounds/damage-1.wav +0 -0
  76. package/packs/retro-8bit/sounds/error-1.wav +0 -0
  77. package/packs/retro-8bit/sounds/error-2.wav +0 -0
  78. package/packs/retro-8bit/sounds/error-3.wav +0 -0
  79. package/packs/retro-8bit/sounds/fanfare-1.wav +0 -0
  80. package/packs/retro-8bit/sounds/fanfare-2.wav +0 -0
  81. package/packs/retro-8bit/sounds/fanfare-3.wav +0 -0
  82. package/packs/retro-8bit/sounds/negative-1.wav +0 -0
  83. package/packs/retro-8bit/sounds/negative-2.wav +0 -0
  84. package/packs/retro-8bit/sounds/powerup-1.wav +0 -0
  85. package/packs/retro-8bit/sounds/powerup-2.wav +0 -0
  86. package/packs/warzone2100-command/manifest.json +1 -0
  87. package/packs/wesnoth-combat/manifest.json +2 -1
  88. package/packs/xonotic-announcer/manifest.json +52 -0
  89. package/packs/xonotic-announcer/sounds/1fragleft.wav +0 -0
  90. package/packs/xonotic-announcer/sounds/airshot.wav +0 -0
  91. package/packs/xonotic-announcer/sounds/amazing.wav +0 -0
  92. package/packs/xonotic-announcer/sounds/awesome.wav +0 -0
  93. package/packs/xonotic-announcer/sounds/begin.wav +0 -0
  94. package/packs/xonotic-announcer/sounds/botlike.wav +0 -0
  95. package/packs/xonotic-announcer/sounds/electrobitch.wav +0 -0
  96. package/packs/xonotic-announcer/sounds/headshot.wav +0 -0
  97. package/packs/xonotic-announcer/sounds/impressive.wav +0 -0
  98. package/packs/xonotic-announcer/sounds/lastsecond.wav +0 -0
  99. package/packs/xonotic-announcer/sounds/leadgained.wav +0 -0
  100. package/packs/xonotic-announcer/sounds/narrowly.wav +0 -0
  101. package/packs/xonotic-announcer/sounds/prepareforbattle.wav +0 -0
  102. package/packs/xonotic-announcer/sounds/terminated.wav +0 -0
  103. package/packs/xonotic-announcer/sounds/yoda.wav +0 -0
  104. package/src/cli/browse.js +75 -0
  105. package/src/cli/completions.js +182 -0
  106. package/src/cli/config.js +32 -3
  107. package/src/cli/create.js +1 -1
  108. package/src/cli/doctor.js +134 -0
  109. package/src/cli/init.js +11 -8
  110. package/src/cli/play.js +22 -1
  111. package/src/cli/random-pack.js +44 -0
  112. package/src/cli/search.js +81 -0
  113. package/src/cli/select.js +36 -32
  114. package/src/cli/sounds.js +76 -0
  115. package/src/cli/theme.js +34 -1
  116. package/src/cli/uninstall.js +5 -0
  117. package/src/config.js +30 -0
  118. package/src/packs.js +1 -0
  119. package/src/player.js +17 -12
package/src/cli/select.js CHANGED
@@ -45,37 +45,41 @@ export default async function select(args) {
45
45
  output: process.stdout,
46
46
  });
47
47
 
48
- const ask = () => {
49
- rl.question('> ', (answer) => {
50
- const trimmed = answer.trim().toLowerCase();
51
-
52
- if (trimmed === 'q' || trimmed === 'quit' || trimmed === '') {
48
+ await new Promise((resolve) => {
49
+ const ask = () => {
50
+ rl.question('> ', (answer) => {
51
+ const trimmed = answer.trim().toLowerCase();
52
+
53
+ if (trimmed === 'q' || trimmed === 'quit' || trimmed === '') {
54
+ rl.close();
55
+ resolve();
56
+ return;
57
+ }
58
+
59
+ const num = parseInt(trimmed, 10);
60
+ if (isNaN(num) || num < 1 || num > packs.length) {
61
+ console.log(`Enter 1-${packs.length} or q to quit.`);
62
+ ask();
63
+ return;
64
+ }
65
+
66
+ const chosen = packs[num - 1];
67
+ config.activePack = chosen.name;
68
+ writeConfig(config);
69
+
70
+ // Preview a sound from the chosen pack
71
+ const sounds = getPackSounds(chosen.name);
72
+ if (sounds.length > 0) {
73
+ const sample = sounds[Math.floor(Math.random() * sounds.length)];
74
+ playSound(sample, config.volume);
75
+ }
76
+
77
+ console.log(`\nActive pack set to: ${chosen.name}`);
53
78
  rl.close();
54
- return;
55
- }
56
-
57
- const num = parseInt(trimmed, 10);
58
- if (isNaN(num) || num < 1 || num > packs.length) {
59
- console.log(`Enter 1-${packs.length} or q to quit.`);
60
- ask();
61
- return;
62
- }
63
-
64
- const chosen = packs[num - 1];
65
- config.activePack = chosen.name;
66
- writeConfig(config);
67
-
68
- // Preview a sound from the chosen pack
69
- const sounds = getPackSounds(chosen.name);
70
- if (sounds.length > 0) {
71
- const sample = sounds[Math.floor(Math.random() * sounds.length)];
72
- playSound(sample, config.volume);
73
- }
74
-
75
- console.log(`\nActive pack set to: ${chosen.name}`);
76
- rl.close();
77
- });
78
- };
79
-
80
- ask();
79
+ resolve();
80
+ });
81
+ };
82
+
83
+ ask();
84
+ });
81
85
  }
@@ -0,0 +1,76 @@
1
+ import { readConfig } from '../config.js';
2
+ import { getPackSounds, resolvePack, getPackEvents, getEventSounds } from '../packs.js';
3
+ import { basename } from 'node:path';
4
+
5
+ function showHelp() {
6
+ console.log(`
7
+ Usage: pingthings sounds [pack]
8
+
9
+ List all individual sounds in a pack.
10
+
11
+ Arguments:
12
+ pack Pack to list sounds from (defaults to active pack)
13
+
14
+ Options:
15
+ --events Show sounds grouped by event mapping
16
+
17
+ Examples:
18
+ pingthings sounds List sounds in active pack
19
+ pingthings sounds openarena-announcer List sounds in a specific pack
20
+ pingthings sounds --events Show event mappings
21
+ `);
22
+ }
23
+
24
+ export default function sounds(args) {
25
+ if (args.includes('--help') || args.includes('-h')) {
26
+ showHelp();
27
+ return;
28
+ }
29
+
30
+ const showEvents = args.includes('--events');
31
+ const packName = args.find(a => !a.startsWith('-')) || readConfig().activePack;
32
+
33
+ const pack = resolvePack(packName);
34
+ if (!pack) {
35
+ console.error(`Pack not found: ${packName}`);
36
+ console.error('Run "pingthings list" to see available packs.');
37
+ process.exit(1);
38
+ }
39
+
40
+ const allSounds = getPackSounds(packName);
41
+
42
+ if (showEvents) {
43
+ const events = getPackEvents(packName);
44
+ console.log(`\n${packName} — sounds by event:\n`);
45
+
46
+ for (const event of events) {
47
+ const eventSounds = getEventSounds(packName, event);
48
+ console.log(` ${event}:`);
49
+ for (const s of eventSounds) {
50
+ console.log(` ${basename(s)}`);
51
+ }
52
+ }
53
+
54
+ // Show unmapped sounds
55
+ const mappedPaths = new Set();
56
+ for (const event of events) {
57
+ for (const s of getEventSounds(packName, event)) {
58
+ mappedPaths.add(s);
59
+ }
60
+ }
61
+ const unmapped = allSounds.filter(s => !mappedPaths.has(s));
62
+ if (unmapped.length > 0) {
63
+ console.log(' (unmapped):');
64
+ for (const s of unmapped) {
65
+ console.log(` ${basename(s)}`);
66
+ }
67
+ }
68
+ } else {
69
+ console.log(`\n${packName} — ${allSounds.length} sounds:\n`);
70
+ for (const s of allSounds) {
71
+ console.log(` ${basename(s)}`);
72
+ }
73
+ }
74
+
75
+ console.log('');
76
+ }
package/src/cli/theme.js CHANGED
@@ -57,11 +57,44 @@ const THEMES = {
57
57
  blocked: '0ad-civilizations',
58
58
  },
59
59
  },
60
+ 'professional': {
61
+ description: 'Clean and minimal — Kenney UI sounds for everything',
62
+ activePack: 'kenney-interface',
63
+ eventPacks: {
64
+ done: 'kenney-interface',
65
+ permission: 'kenney-interface',
66
+ complete: 'kenney-interface',
67
+ error: 'kenney-interface',
68
+ blocked: 'kenney-interface',
69
+ },
70
+ },
71
+ '8bit': {
72
+ description: 'Pure retro — 8-bit chiptune for everything',
73
+ activePack: 'retro-8bit',
74
+ eventPacks: {
75
+ done: 'retro-8bit',
76
+ permission: 'retro-8bit',
77
+ complete: 'retro-8bit',
78
+ error: 'retro-8bit',
79
+ blocked: 'retro-8bit',
80
+ },
81
+ },
82
+ 'space': {
83
+ description: 'Space station — Warzone 2100 + Kenney sci-fi',
84
+ activePack: 'kenney-scifi',
85
+ eventPacks: {
86
+ done: 'kenney-scifi',
87
+ permission: 'warzone2100-command',
88
+ complete: 'warzone2100-command',
89
+ error: 'kenney-scifi',
90
+ blocked: 'kenney-scifi',
91
+ },
92
+ },
60
93
  'chaos': {
61
94
  description: 'Random pack for every event — maximum variety',
62
95
  activePack: '7kaa-soldiers',
63
96
  eventPacks: {
64
- done: '7kaa-soldiers',
97
+ done: 'retro-8bit',
65
98
  permission: 'openarena-announcer',
66
99
  complete: 'warzone2100-command',
67
100
  error: 'freedoom-arsenal',
@@ -25,6 +25,11 @@ export default function uninstall(args) {
25
25
  return;
26
26
  }
27
27
 
28
+ if (packName.includes('..') || packName.includes('/') || packName.includes('\\')) {
29
+ console.error('Invalid pack name.');
30
+ process.exit(1);
31
+ }
32
+
28
33
  const userPackDir = join(getConfigDir(), 'packs', packName);
29
34
 
30
35
  if (!existsSync(userPackDir)) {
package/src/config.js CHANGED
@@ -8,6 +8,8 @@ const DEFAULTS = {
8
8
  specificSound: null,
9
9
  volume: 100,
10
10
  eventPacks: {},
11
+ cooldown: true,
12
+ quietHours: null,
11
13
  };
12
14
 
13
15
  export function getConfigDir() {
@@ -45,6 +47,34 @@ export function getDefaults() {
45
47
  return { ...DEFAULTS };
46
48
  }
47
49
 
50
+ export function getLastPlayed() {
51
+ const path = join(getConfigDir(), '.last-played');
52
+ try {
53
+ return readFileSync(path, 'utf8').trim();
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ export function setLastPlayed(soundPath) {
60
+ const path = join(getConfigDir(), '.last-played');
61
+ try {
62
+ writeFileSync(path, soundPath, 'utf8');
63
+ } catch {}
64
+ }
65
+
66
+ export function isQuietHours(config) {
67
+ if (!config.quietHours) return false;
68
+ const [start, end] = config.quietHours.split('-').map(Number);
69
+ if (isNaN(start) || isNaN(end)) return false;
70
+ const hour = new Date().getHours();
71
+ if (start < end) {
72
+ return hour >= start && hour < end;
73
+ }
74
+ // Wraps midnight (e.g., 22-7)
75
+ return hour >= start || hour < end;
76
+ }
77
+
48
78
  export const VALID_MODES = ['random', 'specific', 'informational'];
49
79
 
50
80
  export const VALID_EVENTS = ['done', 'permission', 'complete', 'error', 'blocked'];
package/src/packs.js CHANGED
@@ -57,6 +57,7 @@ function scanDirectory(dir, isBuiltIn) {
57
57
  description: manifest?.description || '',
58
58
  license: manifest?.license || 'Unknown',
59
59
  credits: manifest?.credits || '',
60
+ category: manifest?.category || 'other',
60
61
  location: packDir,
61
62
  soundCount: sounds.length,
62
63
  isBuiltIn,
package/src/player.js CHANGED
@@ -2,17 +2,23 @@ import { spawn, execFileSync } from 'node:child_process';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { platform } from 'node:os';
4
4
 
5
+ function commandExists(cmd) {
6
+ try {
7
+ execFileSync('which', [cmd], { stdio: 'pipe' });
8
+ return true;
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+
5
14
  function getPlayerCommand() {
6
15
  switch (platform()) {
7
16
  case 'darwin':
8
17
  return 'afplay';
9
18
  case 'linux': {
10
- try {
11
- execFileSync('which', ['paplay'], { stdio: 'pipe' });
12
- return 'paplay';
13
- } catch {
14
- return 'aplay';
15
- }
19
+ if (commandExists('paplay')) return 'paplay';
20
+ if (commandExists('aplay')) return 'aplay';
21
+ return null;
16
22
  }
17
23
  case 'win32':
18
24
  return 'powershell';
@@ -26,20 +32,16 @@ function buildArgs(cmd, filePath, volume) {
26
32
 
27
33
  switch (cmd) {
28
34
  case 'afplay': {
29
- // afplay volume: 0.0 to 1.0
30
35
  const afplayVol = (vol / 100).toFixed(2);
31
36
  return ['-v', afplayVol, filePath];
32
37
  }
33
38
  case 'paplay': {
34
- // paplay volume: 0 to 65536 (100% = 65536)
35
39
  const paplayVol = Math.round((vol / 100) * 65536).toString();
36
40
  return ['--volume', paplayVol, filePath];
37
41
  }
38
42
  case 'aplay':
39
- // aplay doesn't support volume natively
40
43
  return [filePath];
41
44
  case 'powershell': {
42
- // PowerShell SoundPlayer doesn't support volume, but it plays the file
43
45
  const script = `(New-Object System.Media.SoundPlayer '${filePath.replace(/'/g, "''")}').PlaySync()`;
44
46
  return ['-NoProfile', '-Command', script];
45
47
  }
@@ -55,7 +57,8 @@ export function playSound(filePath, volume) {
55
57
 
56
58
  const cmd = getPlayerCommand();
57
59
  if (!cmd) {
58
- throw new Error(`Unsupported platform: ${platform()}. Supported: macOS, Linux, Windows.`);
60
+ // No audio player available — skip silently (e.g. CI environments)
61
+ return;
59
62
  }
60
63
 
61
64
  const args = buildArgs(cmd, filePath, volume);
@@ -65,6 +68,8 @@ export function playSound(filePath, volume) {
65
68
  stdio: 'ignore',
66
69
  });
67
70
 
71
+ // Handle spawn errors gracefully (command not found, permission denied)
72
+ child.on('error', () => {});
68
73
  child.unref();
69
74
  }
70
75
 
@@ -75,7 +80,7 @@ export function playSoundSync(filePath, volume) {
75
80
 
76
81
  const cmd = getPlayerCommand();
77
82
  if (!cmd) {
78
- throw new Error(`Unsupported platform: ${platform()}. Supported: macOS, Linux, Windows.`);
83
+ return;
79
84
  }
80
85
 
81
86
  const args = buildArgs(cmd, filePath, volume);