git-watchtower 1.6.0 → 1.7.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.
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Casino Mode Sound Effects
3
+ *
4
+ * Plays casino-themed sounds for wins, jackpots, and losses.
5
+ * Uses system audio tools when available, falls back gracefully.
6
+ */
7
+
8
+ const { exec } = require('child_process');
9
+ const path = require('path');
10
+ const fs = require('fs');
11
+
12
+ // ============================================================================
13
+ // Sound Configuration
14
+ // ============================================================================
15
+
16
+ // Path to bundled sound files (if we add them)
17
+ const SOUNDS_DIR = path.join(__dirname, '../../sounds');
18
+
19
+ // System sound fallbacks by platform
20
+ const SYSTEM_SOUNDS = {
21
+ darwin: {
22
+ win: '/System/Library/Sounds/Glass.aiff',
23
+ jackpot: '/System/Library/Sounds/Hero.aiff',
24
+ spin: '/System/Library/Sounds/Pop.aiff',
25
+ loss: '/System/Library/Sounds/Basso.aiff',
26
+ },
27
+ linux: {
28
+ win: '/usr/share/sounds/freedesktop/stereo/complete.oga',
29
+ jackpot: '/usr/share/sounds/freedesktop/stereo/bell.oga',
30
+ spin: '/usr/share/sounds/freedesktop/stereo/message.oga',
31
+ loss: '/usr/share/sounds/freedesktop/stereo/dialog-error.oga',
32
+ },
33
+ };
34
+
35
+ // Volume levels (0.0 - 1.0, not all platforms support this)
36
+ const VOLUME = {
37
+ win: 0.5,
38
+ jackpot: 0.8,
39
+ spin: 0.3,
40
+ loss: 0.6,
41
+ };
42
+
43
+ // ============================================================================
44
+ // Sound Playback
45
+ // ============================================================================
46
+
47
+ /**
48
+ * Play a sound file (non-blocking)
49
+ * @param {string} soundPath - Path to sound file
50
+ * @param {number} [volume=0.5] - Volume level (0.0-1.0)
51
+ */
52
+ function playFile(soundPath, volume = 0.5) {
53
+ if (!soundPath) return;
54
+
55
+ const { platform } = process;
56
+
57
+ try {
58
+ if (platform === 'darwin') {
59
+ // macOS: afplay with volume
60
+ exec(`afplay -v ${volume} "${soundPath}" 2>/dev/null &`);
61
+ } else if (platform === 'linux') {
62
+ // Linux: paplay (PulseAudio) or aplay (ALSA)
63
+ // Note: paplay doesn't support volume easily, aplay does via amixer
64
+ exec(
65
+ `paplay "${soundPath}" 2>/dev/null || aplay -q "${soundPath}" 2>/dev/null &`
66
+ );
67
+ } else if (platform === 'win32') {
68
+ // Windows: Use PowerShell to play sound
69
+ exec(
70
+ `powershell -c "(New-Object Media.SoundPlayer '${soundPath}').PlaySync()" 2>nul`,
71
+ { windowsHide: true }
72
+ );
73
+ }
74
+ } catch (e) {
75
+ // Silently fail - sounds are optional
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Play terminal bell as fallback
81
+ */
82
+ function playBell() {
83
+ process.stdout.write('\x07');
84
+ }
85
+
86
+ /**
87
+ * Get the appropriate sound file for a sound type
88
+ * @param {string} soundType - 'win', 'jackpot', 'spin', or 'loss'
89
+ * @returns {string|null}
90
+ */
91
+ function getSoundPath(soundType) {
92
+ const { platform } = process;
93
+
94
+ // First check for bundled sounds
95
+ const bundledPath = path.join(SOUNDS_DIR, `${soundType}.wav`);
96
+ if (fs.existsSync(bundledPath)) {
97
+ return bundledPath;
98
+ }
99
+
100
+ // Fall back to system sounds
101
+ const systemSounds = SYSTEM_SOUNDS[platform];
102
+ if (systemSounds && systemSounds[soundType]) {
103
+ const systemPath = systemSounds[soundType];
104
+ if (fs.existsSync(systemPath)) {
105
+ return systemPath;
106
+ }
107
+ }
108
+
109
+ return null;
110
+ }
111
+
112
+ // ============================================================================
113
+ // Casino Sound Effects
114
+ // ============================================================================
115
+
116
+ /**
117
+ * Play a win sound (small to medium wins)
118
+ */
119
+ function playWin() {
120
+ const soundPath = getSoundPath('win');
121
+ if (soundPath) {
122
+ playFile(soundPath, VOLUME.win);
123
+ } else {
124
+ playBell();
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Play a jackpot sound (big wins)
130
+ */
131
+ function playJackpot() {
132
+ const soundPath = getSoundPath('jackpot');
133
+ if (soundPath) {
134
+ playFile(soundPath, VOLUME.jackpot);
135
+ } else {
136
+ // Multiple bells for jackpot!
137
+ playBell();
138
+ setTimeout(playBell, 200);
139
+ setTimeout(playBell, 400);
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Play a mega jackpot sound (huge wins)
145
+ */
146
+ function playMegaJackpot() {
147
+ const soundPath = getSoundPath('jackpot');
148
+ if (soundPath) {
149
+ // Play jackpot sound multiple times
150
+ playFile(soundPath, VOLUME.jackpot);
151
+ setTimeout(() => playFile(soundPath, VOLUME.jackpot), 300);
152
+ setTimeout(() => playFile(soundPath, VOLUME.jackpot), 600);
153
+ } else {
154
+ // Lots of bells!
155
+ for (let i = 0; i < 5; i++) {
156
+ setTimeout(playBell, i * 150);
157
+ }
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Play slot spin sound
163
+ */
164
+ function playSpin() {
165
+ const soundPath = getSoundPath('spin');
166
+ if (soundPath) {
167
+ playFile(soundPath, VOLUME.spin);
168
+ }
169
+ // No fallback for spin - it would be annoying
170
+ }
171
+
172
+ /**
173
+ * Play loss/failure sound (sad trombone effect)
174
+ */
175
+ function playLoss() {
176
+ const soundPath = getSoundPath('loss');
177
+ if (soundPath) {
178
+ playFile(soundPath, VOLUME.loss);
179
+ } else {
180
+ // Low-pitched bell equivalent
181
+ playBell();
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Play sound based on win level
187
+ * @param {string} levelKey - 'small', 'medium', 'large', 'huge', 'jackpot', 'mega'
188
+ */
189
+ function playForWinLevel(levelKey) {
190
+ switch (levelKey) {
191
+ case 'small':
192
+ case 'medium':
193
+ playWin();
194
+ break;
195
+ case 'large':
196
+ case 'huge':
197
+ playJackpot();
198
+ break;
199
+ case 'jackpot':
200
+ playJackpot();
201
+ break;
202
+ case 'mega':
203
+ playMegaJackpot();
204
+ break;
205
+ default:
206
+ playWin();
207
+ }
208
+ }
209
+
210
+ // ============================================================================
211
+ // Sound Sources Documentation
212
+ // ============================================================================
213
+
214
+ /**
215
+ * For users who want to add custom sounds:
216
+ *
217
+ * Create a 'sounds' directory in the git-watchtower root with:
218
+ * - win.wav - Short victory sound
219
+ * - jackpot.wav - Exciting jackpot fanfare
220
+ * - spin.wav - Slot machine spinning
221
+ * - loss.wav - Sad trombone / failure sound
222
+ *
223
+ * Free sound sources:
224
+ * - https://freesound.org/
225
+ * - https://mixkit.co/free-sound-effects/
226
+ * - https://www.zapsplat.com/
227
+ *
228
+ * Recommended search terms:
229
+ * - "slot machine win"
230
+ * - "casino jackpot"
231
+ * - "coin drop"
232
+ * - "sad trombone"
233
+ * - "game over"
234
+ */
235
+
236
+ module.exports = {
237
+ playWin,
238
+ playJackpot,
239
+ playMegaJackpot,
240
+ playSpin,
241
+ playLoss,
242
+ playForWinLevel,
243
+ getSoundPath,
244
+ SOUNDS_DIR,
245
+ };
@@ -0,0 +1,239 @@
1
+ /**
2
+ * CLI argument parsing
3
+ * @module cli/args
4
+ */
5
+
6
+ const PACKAGE_VERSION = '1.2.0';
7
+
8
+ /**
9
+ * @typedef {object} CliArgs
10
+ * @property {string|null} mode - Server mode override
11
+ * @property {boolean} noServer - Shorthand for --mode none
12
+ * @property {number|null} port - Port override
13
+ * @property {string|null} staticDir - Static directory override
14
+ * @property {string|null} command - Server command override
15
+ * @property {boolean|null} restartOnSwitch - Restart on branch switch
16
+ * @property {string|null} remote - Git remote name override
17
+ * @property {boolean|null} autoPull - Auto-pull override
18
+ * @property {number|null} pollInterval - Poll interval override in ms
19
+ * @property {boolean|null} sound - Sound override
20
+ * @property {number|null} visibleBranches - Visible branches override
21
+ * @property {boolean} init - Run configuration wizard
22
+ * @property {boolean} casino - Enable casino mode
23
+ */
24
+
25
+ /**
26
+ * Parse CLI arguments into a structured object.
27
+ * @param {string[]} argv - Arguments array (typically process.argv.slice(2))
28
+ * @param {object} [options]
29
+ * @param {function} [options.onVersion] - Called when --version is encountered
30
+ * @param {function} [options.onHelp] - Called when --help is encountered
31
+ * @returns {CliArgs}
32
+ */
33
+ function parseArgs(argv, options = {}) {
34
+ const args = argv || [];
35
+ const result = {
36
+ // Server settings
37
+ mode: null,
38
+ noServer: false,
39
+ port: null,
40
+ staticDir: null,
41
+ command: null,
42
+ restartOnSwitch: null,
43
+ // Git settings
44
+ remote: null,
45
+ autoPull: null,
46
+ pollInterval: null,
47
+ // UI settings
48
+ sound: null,
49
+ visibleBranches: null,
50
+ // Actions
51
+ init: false,
52
+ casino: false,
53
+ };
54
+
55
+ for (let i = 0; i < args.length; i++) {
56
+ // Server settings
57
+ if (args[i] === '--mode' || args[i] === '-m') {
58
+ const mode = args[i + 1];
59
+ if (['static', 'command', 'none'].includes(mode)) {
60
+ result.mode = mode;
61
+ }
62
+ i++;
63
+ } else if (args[i] === '--port' || args[i] === '-p') {
64
+ const portValue = parseInt(args[i + 1], 10);
65
+ if (!isNaN(portValue) && portValue > 0 && portValue < 65536) {
66
+ result.port = portValue;
67
+ }
68
+ i++;
69
+ } else if (args[i] === '--no-server' || args[i] === '-n') {
70
+ result.noServer = true;
71
+ } else if (args[i] === '--static-dir') {
72
+ result.staticDir = args[i + 1];
73
+ i++;
74
+ } else if (args[i] === '--command' || args[i] === '-c') {
75
+ result.command = args[i + 1];
76
+ i++;
77
+ } else if (args[i] === '--restart-on-switch') {
78
+ result.restartOnSwitch = true;
79
+ } else if (args[i] === '--no-restart-on-switch') {
80
+ result.restartOnSwitch = false;
81
+ }
82
+ // Git settings
83
+ else if (args[i] === '--remote' || args[i] === '-r') {
84
+ result.remote = args[i + 1];
85
+ i++;
86
+ } else if (args[i] === '--auto-pull') {
87
+ result.autoPull = true;
88
+ } else if (args[i] === '--no-auto-pull') {
89
+ result.autoPull = false;
90
+ } else if (args[i] === '--poll-interval') {
91
+ const interval = parseInt(args[i + 1], 10);
92
+ if (!isNaN(interval) && interval > 0) {
93
+ result.pollInterval = interval;
94
+ }
95
+ i++;
96
+ }
97
+ // UI settings
98
+ else if (args[i] === '--sound') {
99
+ result.sound = true;
100
+ } else if (args[i] === '--no-sound') {
101
+ result.sound = false;
102
+ } else if (args[i] === '--visible-branches') {
103
+ const count = parseInt(args[i + 1], 10);
104
+ if (!isNaN(count) && count > 0) {
105
+ result.visibleBranches = count;
106
+ }
107
+ i++;
108
+ } else if (args[i] === '--casino') {
109
+ result.casino = true;
110
+ }
111
+ // Actions and info
112
+ else if (args[i] === '--init') {
113
+ result.init = true;
114
+ } else if (args[i] === '--version' || args[i] === '-v') {
115
+ if (options.onVersion) {
116
+ options.onVersion(PACKAGE_VERSION);
117
+ }
118
+ } else if (args[i] === '--help' || args[i] === '-h') {
119
+ if (options.onHelp) {
120
+ options.onHelp(PACKAGE_VERSION);
121
+ }
122
+ }
123
+ }
124
+ return result;
125
+ }
126
+
127
+ /**
128
+ * Apply CLI args on top of a config object. CLI takes precedence.
129
+ * @param {object} config - Base configuration
130
+ * @param {CliArgs} cliArgs - Parsed CLI args
131
+ * @returns {object} Merged config
132
+ */
133
+ function applyCliArgsToConfig(config, cliArgs) {
134
+ const merged = JSON.parse(JSON.stringify(config)); // deep clone
135
+
136
+ // Server settings
137
+ if (cliArgs.mode !== null) {
138
+ merged.server.mode = cliArgs.mode;
139
+ }
140
+ if (cliArgs.noServer) {
141
+ merged.server.mode = 'none';
142
+ }
143
+ if (cliArgs.port !== null) {
144
+ merged.server.port = cliArgs.port;
145
+ }
146
+ if (cliArgs.staticDir !== null) {
147
+ merged.server.staticDir = cliArgs.staticDir;
148
+ }
149
+ if (cliArgs.command !== null) {
150
+ merged.server.command = cliArgs.command;
151
+ }
152
+ if (cliArgs.restartOnSwitch !== null) {
153
+ merged.server.restartOnSwitch = cliArgs.restartOnSwitch;
154
+ }
155
+
156
+ // Git settings
157
+ if (cliArgs.remote !== null) {
158
+ merged.remoteName = cliArgs.remote;
159
+ }
160
+ if (cliArgs.autoPull !== null) {
161
+ merged.autoPull = cliArgs.autoPull;
162
+ }
163
+ if (cliArgs.pollInterval !== null) {
164
+ merged.gitPollInterval = cliArgs.pollInterval;
165
+ }
166
+
167
+ // UI settings
168
+ if (cliArgs.sound !== null) {
169
+ merged.soundEnabled = cliArgs.sound;
170
+ }
171
+ if (cliArgs.visibleBranches !== null) {
172
+ merged.visibleBranches = cliArgs.visibleBranches;
173
+ }
174
+ if (cliArgs.casino) {
175
+ merged.casinoMode = true;
176
+ }
177
+
178
+ return merged;
179
+ }
180
+
181
+ /**
182
+ * Get the help text for the CLI.
183
+ * @param {string} version
184
+ * @returns {string}
185
+ */
186
+ function getHelpText(version) {
187
+ return `
188
+ Git Watchtower v${version} - Branch Monitor & Dev Server
189
+
190
+ Usage:
191
+ git-watchtower [options]
192
+
193
+ Server Options:
194
+ -m, --mode <mode> Server mode: static, command, or none
195
+ -p, --port <port> Server port (default: 3000)
196
+ -n, --no-server Shorthand for --mode none
197
+ --static-dir <dir> Directory for static file serving (default: public)
198
+ -c, --command <cmd> Command to run in command mode (e.g., "npm run dev")
199
+ --restart-on-switch Restart server on branch switch (default)
200
+ --no-restart-on-switch Don't restart server on branch switch
201
+
202
+ Git Options:
203
+ -r, --remote <name> Git remote name (default: origin)
204
+ --auto-pull Auto-pull on branch switch (default)
205
+ --no-auto-pull Don't auto-pull on branch switch
206
+ --poll-interval <ms> Git polling interval in ms (default: 5000)
207
+
208
+ UI Options:
209
+ --sound Enable sound notifications (default)
210
+ --no-sound Disable sound notifications
211
+ --visible-branches <n> Number of branches to display (default: 7)
212
+ --casino Enable casino mode
213
+
214
+ General:
215
+ --init Run the configuration wizard
216
+ -v, --version Show version number
217
+ -h, --help Show this help message
218
+
219
+ Server Modes:
220
+ static Serve static files with live reload (default)
221
+ command Run your own dev server (Next.js, Vite, Nuxt, etc.)
222
+ none Branch monitoring only
223
+
224
+ Configuration:
225
+ On first run, Git Watchtower will prompt you to configure settings.
226
+ Settings are saved to .watchtowerrc.json in your project directory.
227
+ CLI options override config file settings for the current session.
228
+
229
+ Examples:
230
+ git-watchtower # Start with config or defaults
231
+ git-watchtower --init # Re-run configuration wizard
232
+ git-watchtower --no-server # Branch monitoring only
233
+ git-watchtower -p 8080 # Override port
234
+ git-watchtower -m command -c "npm run dev" # Use custom dev server
235
+ git-watchtower --no-sound --poll-interval 10000
236
+ `;
237
+ }
238
+
239
+ module.exports = { parseArgs, applyCliArgsToConfig, getHelpText, PACKAGE_VERSION };