git-watchtower 1.0.0 → 1.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.
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Git Watchtower
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/git-watchtower.svg)](https://www.npmjs.com/package/git-watchtower)
4
+ [![npm downloads](https://img.shields.io/npm/dm/git-watchtower.svg)](https://www.npmjs.com/package/git-watchtower)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
+
3
7
  Monitor and switch between git branches in real-time. Built for working with web based AI coding agents, like Claude Code Web & Codex.
4
8
 
5
9
  - **Live branch monitoring** - Watches your remote for new commits, branches, and deletions
@@ -89,6 +93,40 @@ git-watchtower --help
89
93
  ### Static Site Mode (Default)
90
94
  Serves static files with automatic live reload. Good for static HTML/CSS/JS sites, projects without a build step, quick prototyping.
91
95
 
96
+ #### Live Reload
97
+
98
+ The static server includes automatic live reload powered by Server-Sent Events (SSE). When you save a file, all connected browsers refresh instantly.
99
+
100
+ **How it works:**
101
+ 1. A small script is automatically injected into HTML pages
102
+ 2. The script opens an SSE connection to `/livereload`
103
+ 3. When files change in your static directory, the server notifies all browsers
104
+ 4. Browsers automatically reload to show your changes
105
+
106
+ **File watching behavior:**
107
+ - Uses Node.js native `fs.watch()` with recursive watching
108
+ - Changes are debounced (100ms) to prevent rapid reloads during saves
109
+ - Press `r` to manually trigger a reload for all connected browsers
110
+
111
+ **Ignored files:**
112
+
113
+ The file watcher automatically ignores certain files to prevent unnecessary reloads:
114
+
115
+ | Ignored | Reason |
116
+ |---------|--------|
117
+ | `.git/` directory | Git internals change frequently during commits, fetches, etc. |
118
+ | `.gitignore` patterns | Respects your project's ignore rules |
119
+
120
+ If a `.gitignore` file exists in your static directory (or project root), those patterns are used to filter file change events. This means changes to `node_modules/`, build artifacts, log files, and other ignored paths won't trigger reloads.
121
+
122
+ **Supported `.gitignore` patterns:**
123
+ - Simple filenames: `foo.txt`
124
+ - Wildcards: `*.log`, `file?.txt`
125
+ - Globstar: `**/logs`, `logs/**/*.log`
126
+ - Directory patterns: `node_modules/`, `dist/`
127
+ - Anchored patterns: `/build` (root only)
128
+ - Comments and blank lines are ignored
129
+
92
130
  ### Custom Server Command Mode
93
131
  Runs your own dev server command (`next dev`, `npm run dev`, `vite`, etc.). Press `l` to view server logs, `R` to restart the server.
94
132
 
@@ -198,7 +236,7 @@ GIT_POLL_INTERVAL=10000 git-watchtower
198
236
 
199
237
  ## Requirements
200
238
 
201
- - **Node.js** 14.0.0 or higher
239
+ - **Node.js** 18.0.0 or higher
202
240
  - **Git** installed and in PATH
203
241
  - **Git remote** configured (any name, defaults to `origin`)
204
242
  - **Terminal** with ANSI color support
@@ -17,11 +17,14 @@
17
17
  * - Network failure detection with offline indicator
18
18
  * - Graceful shutdown handling
19
19
  * - Support for custom dev server commands (Next.js, Vite, etc.)
20
+ * - Casino Mode: Vegas-style feedback with slot reels, marquee lights,
21
+ * and win celebrations based on diff size (toggle with 'c' or --casino)
20
22
  *
21
23
  * Usage:
22
24
  * git-watchtower # Run with config or defaults
23
25
  * git-watchtower --port 8080 # Override port
24
26
  * git-watchtower --no-server # Branch monitoring only
27
+ * git-watchtower --casino # Enable casino mode
25
28
  * git-watchtower --init # Run configuration wizard
26
29
  * git-watchtower --version # Show version
27
30
  *
@@ -39,8 +42,10 @@
39
42
  * r - Force reload all browsers (static mode)
40
43
  * R - Restart dev server (command mode)
41
44
  * l - View server logs (command mode)
45
+ * o - Open live server in browser
42
46
  * f - Fetch all branches + refresh sparklines
43
47
  * s - Toggle sound notifications
48
+ * c - Toggle casino mode (Vegas-style feedback)
44
49
  * i - Show server info (port, connections)
45
50
  * 1-0 - Set visible branch count (1-10)
46
51
  * +/- - Increase/decrease visible branches
@@ -53,6 +58,13 @@ const path = require('path');
53
58
  const { exec, spawn } = require('child_process');
54
59
  const readline = require('readline');
55
60
 
61
+ // Casino mode - Vegas-style feedback effects
62
+ const casino = require('../src/casino');
63
+ const casinoSounds = require('../src/casino/sounds');
64
+
65
+ // Gitignore utilities for file watcher
66
+ const { loadGitignorePatterns, shouldIgnoreFile } = require('../src/utils/gitignore');
67
+
56
68
  // Package info for --version
57
69
  const PACKAGE_VERSION = '1.0.0';
58
70
 
@@ -498,6 +510,7 @@ const MAX_SERVER_LOG_LINES = 500;
498
510
  // Dynamic settings
499
511
  let visibleBranchCount = 7;
500
512
  let soundEnabled = true;
513
+ let casinoModeEnabled = false;
501
514
 
502
515
  // Server process management (for command mode)
503
516
  let serverProcess = null;
@@ -525,6 +538,12 @@ function applyConfig(config) {
525
538
  // UI settings
526
539
  visibleBranchCount = config.visibleBranches || 7;
527
540
  soundEnabled = config.soundEnabled !== false;
541
+
542
+ // Casino mode
543
+ casinoModeEnabled = config.casinoMode === true;
544
+ if (casinoModeEnabled) {
545
+ casino.enable();
546
+ }
528
547
  }
529
548
 
530
549
  // Server log management
@@ -540,6 +559,27 @@ function clearServerLog() {
540
559
  serverLogBuffer = [];
541
560
  }
542
561
 
562
+ // Open URL in default browser (cross-platform)
563
+ function openInBrowser(url) {
564
+ const platform = process.platform;
565
+ let command;
566
+
567
+ if (platform === 'darwin') {
568
+ command = `open "${url}"`;
569
+ } else if (platform === 'win32') {
570
+ command = `start "" "${url}"`;
571
+ } else {
572
+ // Linux and other Unix-like systems
573
+ command = `xdg-open "${url}"`;
574
+ }
575
+
576
+ exec(command, (error) => {
577
+ if (error) {
578
+ addLog(`Failed to open browser: ${error.message}`, 'error');
579
+ }
580
+ });
581
+ }
582
+
543
583
  // Command mode server management
544
584
  function startServerProcess() {
545
585
  if (SERVER_MODE !== 'command' || !SERVER_COMMAND) return;
@@ -678,6 +718,7 @@ const ansi = {
678
718
  italic: `${CSI}3m`,
679
719
  underline: `${CSI}4m`,
680
720
  inverse: `${CSI}7m`,
721
+ blink: `${CSI}5m`,
681
722
 
682
723
  // Foreground colors
683
724
  black: `${CSI}30m`,
@@ -690,6 +731,15 @@ const ansi = {
690
731
  white: `${CSI}37m`,
691
732
  gray: `${CSI}90m`,
692
733
 
734
+ // Bright foreground colors
735
+ brightRed: `${CSI}91m`,
736
+ brightGreen: `${CSI}92m`,
737
+ brightYellow: `${CSI}93m`,
738
+ brightBlue: `${CSI}94m`,
739
+ brightMagenta: `${CSI}95m`,
740
+ brightCyan: `${CSI}96m`,
741
+ brightWhite: `${CSI}97m`,
742
+
693
743
  // Background colors
694
744
  bgBlack: `${CSI}40m`,
695
745
  bgRed: `${CSI}41m`,
@@ -700,6 +750,15 @@ const ansi = {
700
750
  bgCyan: `${CSI}46m`,
701
751
  bgWhite: `${CSI}47m`,
702
752
 
753
+ // Bright background colors
754
+ bgBrightRed: `${CSI}101m`,
755
+ bgBrightGreen: `${CSI}102m`,
756
+ bgBrightYellow: `${CSI}103m`,
757
+ bgBrightBlue: `${CSI}104m`,
758
+ bgBrightMagenta: `${CSI}105m`,
759
+ bgBrightCyan: `${CSI}106m`,
760
+ bgBrightWhite: `${CSI}107m`,
761
+
703
762
  // 256 colors
704
763
  fg256: (n) => `${CSI}38;5;${n}m`,
705
764
  bg256: (n) => `${CSI}48;5;${n}m`,
@@ -820,6 +879,32 @@ function execAsync(command, options = {}) {
820
879
  });
821
880
  }
822
881
 
882
+ /**
883
+ * Get diff stats between two commits
884
+ * @param {string} fromCommit - Starting commit
885
+ * @param {string} toCommit - Ending commit (default HEAD)
886
+ * @returns {Promise<{added: number, deleted: number}>}
887
+ */
888
+ async function getDiffStats(fromCommit, toCommit = 'HEAD') {
889
+ try {
890
+ const { stdout } = await execAsync(`git diff --stat ${fromCommit}..${toCommit}`);
891
+ // Parse the summary line: "X files changed, Y insertions(+), Z deletions(-)"
892
+ const match = stdout.match(/(\d+) insertions?\(\+\).*?(\d+) deletions?\(-\)/);
893
+ if (match) {
894
+ return { added: parseInt(match[1], 10), deleted: parseInt(match[2], 10) };
895
+ }
896
+ // Try to match just insertions or just deletions
897
+ const insertMatch = stdout.match(/(\d+) insertions?\(\+\)/);
898
+ const deleteMatch = stdout.match(/(\d+) deletions?\(-\)/);
899
+ return {
900
+ added: insertMatch ? parseInt(insertMatch[1], 10) : 0,
901
+ deleted: deleteMatch ? parseInt(deleteMatch[1], 10) : 0,
902
+ };
903
+ } catch (e) {
904
+ return { added: 0, deleted: 0 };
905
+ }
906
+ }
907
+
823
908
  function formatTimeAgo(date) {
824
909
  const now = new Date();
825
910
  const diffMs = now - date;
@@ -859,6 +944,47 @@ function padLeft(str, len) {
859
944
  return ' '.repeat(len - str.length) + str;
860
945
  }
861
946
 
947
+ // Casino mode funny messages
948
+ const CASINO_WIN_MESSAGES = [
949
+ "Here's your dopamine hit! 🎰",
950
+ "The house always wins... and this is YOUR house!",
951
+ "Cha-ching! Fresh code incoming!",
952
+ "🎲 Lucky roll! New commits detected!",
953
+ "Jackpot! Someone's been busy coding!",
954
+ "💰 Cashing out some fresh changes!",
955
+ "The slot gods smile upon you!",
956
+ "Winner winner, chicken dinner! 🍗",
957
+ "Your patience has been rewarded!",
958
+ "🎯 Bullseye! Updates acquired!",
959
+ "Dopamine delivery service! 📦",
960
+ "The code fairy visited while you waited!",
961
+ "🌟 Wish granted: new commits!",
962
+ "Variable reward unlocked! 🔓",
963
+ ];
964
+
965
+ const CASINO_PULL_MESSAGES = [
966
+ "Pulling the lever... 🎰",
967
+ "Spinning the reels of fate...",
968
+ "Checking if luck is on your side...",
969
+ "Rolling the dice on git fetch...",
970
+ "Summoning the code spirits...",
971
+ "Consulting the commit oracle...",
972
+ ];
973
+
974
+ const CASINO_LOSS_MESSAGES = [
975
+ "Better luck next merge!",
976
+ "🎲 Snake eyes! Conflict detected!",
977
+ "Busted! Time to resolve manually.",
978
+ "The git gods are displeased...",
979
+ ];
980
+
981
+ function getCasinoMessage(type) {
982
+ const messages = type === 'win' ? CASINO_WIN_MESSAGES
983
+ : type === 'pull' ? CASINO_PULL_MESSAGES
984
+ : CASINO_LOSS_MESSAGES;
985
+ return messages[Math.floor(Math.random() * messages.length)];
986
+ }
987
+
862
988
  function addLog(message, type = 'info') {
863
989
  const timestamp = new Date().toLocaleTimeString();
864
990
  const icons = { info: '○', success: '✓', warning: '●', error: '✗', update: '⟳' };
@@ -1043,6 +1169,9 @@ function clearArea(row, col, width, height) {
1043
1169
 
1044
1170
  function renderHeader() {
1045
1171
  const width = terminalWidth;
1172
+ // Header row: 1 normally, 2 when casino mode (row 1 is marquee)
1173
+ const headerRow = casinoModeEnabled ? 2 : 1;
1174
+
1046
1175
  let statusIcon = { idle: ansi.green + '●', fetching: ansi.yellow + '⟳', error: ansi.red + '●' }[pollingStatus];
1047
1176
 
1048
1177
  // Override status for special states
@@ -1053,7 +1182,7 @@ function renderHeader() {
1053
1182
  const soundIcon = soundEnabled ? ansi.green + '🔔' : ansi.gray + '🔕';
1054
1183
  const projectName = path.basename(PROJECT_ROOT);
1055
1184
 
1056
- write(ansi.moveTo(1, 1));
1185
+ write(ansi.moveTo(headerRow, 1));
1057
1186
  write(ansi.bgBlue + ansi.white + ansi.bold);
1058
1187
 
1059
1188
  // Left side: Title + separator + project name
@@ -1065,6 +1194,9 @@ function renderHeader() {
1065
1194
  // Warning badges (center area)
1066
1195
  let badges = '';
1067
1196
  let badgesVisibleLen = 0;
1197
+
1198
+ // Casino mode slot display moved to its own row below header (row 3)
1199
+
1068
1200
  if (SERVER_MODE === 'command' && serverCrashed) {
1069
1201
  const label = ' CRASHED ';
1070
1202
  badges += ' ' + ansi.bgRed + ansi.white + label + ansi.bgBlue + ansi.white;
@@ -1124,7 +1256,8 @@ function renderHeader() {
1124
1256
  }
1125
1257
 
1126
1258
  function renderBranchList() {
1127
- const startRow = 3;
1259
+ // Start row: 3 normally, 4 when casino mode (row 1 is marquee, row 2 is header)
1260
+ const startRow = casinoModeEnabled ? 4 : 3;
1128
1261
  const boxWidth = terminalWidth;
1129
1262
  const contentWidth = boxWidth - 4; // Space between borders
1130
1263
  const height = Math.min(visibleBranchCount * 2 + 4, Math.floor(terminalHeight * 0.5));
@@ -1278,6 +1411,49 @@ function renderActivityLog(startRow) {
1278
1411
  return startRow + height;
1279
1412
  }
1280
1413
 
1414
+ function renderCasinoStats(startRow) {
1415
+ if (!casinoModeEnabled) return startRow;
1416
+
1417
+ const boxWidth = terminalWidth;
1418
+ const height = 6; // Box with two content lines
1419
+
1420
+ // Don't draw if not enough space
1421
+ if (startRow + height > terminalHeight - 3) return startRow;
1422
+
1423
+ drawBox(startRow, 1, boxWidth, height, '🎰 CASINO WINNINGS 🎰', ansi.brightMagenta);
1424
+
1425
+ // Clear content area
1426
+ for (let i = 1; i < height - 1; i++) {
1427
+ write(ansi.moveTo(startRow + i, 2));
1428
+ write(' '.repeat(boxWidth - 2));
1429
+ }
1430
+
1431
+ const stats = casino.getStats();
1432
+
1433
+ // Net winnings color
1434
+ const netColor = stats.netWinnings >= 0 ? ansi.brightGreen : ansi.brightRed;
1435
+ const netSign = stats.netWinnings >= 0 ? '+' : '';
1436
+
1437
+ // Line 1: Line Changes | Poll Cost | Net Earnings
1438
+ write(ansi.moveTo(startRow + 2, 3));
1439
+ write('📝 Line Changes: ');
1440
+ write(ansi.brightGreen + '+' + stats.totalLinesAdded + ansi.reset);
1441
+ write(' / ');
1442
+ write(ansi.brightRed + '-' + stats.totalLinesDeleted + ansi.reset);
1443
+ write(' = ' + ansi.brightYellow + '$' + stats.totalLines + ansi.reset);
1444
+ write(' | 💸 Poll Cost: ' + ansi.brightRed + '$' + stats.totalPolls + ansi.reset);
1445
+ write(' | 💰 Net Earnings: ' + netColor + netSign + '$' + stats.netWinnings + ansi.reset);
1446
+
1447
+ // Line 2: House Edge | Vibes Quality | Luck Meter | Dopamine Hits
1448
+ write(ansi.moveTo(startRow + 3, 3));
1449
+ write('🎰 House Edge: ' + ansi.brightCyan + stats.houseEdge + '%' + ansi.reset);
1450
+ write(' | 😎 Vibes: ' + stats.vibesQuality);
1451
+ write(' | 🎲 Luck: ' + ansi.brightYellow + stats.luckMeter + '%' + ansi.reset);
1452
+ write(' | 🧠 Dopamine Hits: ' + ansi.brightGreen + stats.dopamineHits + ansi.reset);
1453
+
1454
+ return startRow + height;
1455
+ }
1456
+
1281
1457
  function renderFooter() {
1282
1458
  const row = terminalHeight - 1;
1283
1459
 
@@ -1294,6 +1470,7 @@ function renderFooter() {
1294
1470
  // Mode-specific keys
1295
1471
  if (!NO_SERVER) {
1296
1472
  write(ansi.gray + '[l]' + ansi.reset + ansi.bgBlack + ' Logs ');
1473
+ write(ansi.gray + '[o]' + ansi.reset + ansi.bgBlack + ' Open ');
1297
1474
  }
1298
1475
  if (SERVER_MODE === 'static') {
1299
1476
  write(ansi.gray + '[r]' + ansi.reset + ansi.bgBlack + ' Reload ');
@@ -1302,6 +1479,14 @@ function renderFooter() {
1302
1479
  }
1303
1480
 
1304
1481
  write(ansi.gray + '[±]' + ansi.reset + ansi.bgBlack + ' List:' + ansi.cyan + visibleBranchCount + ansi.reset + ansi.bgBlack + ' ');
1482
+
1483
+ // Casino mode toggle indicator
1484
+ if (casinoModeEnabled) {
1485
+ write(ansi.brightMagenta + '[c]' + ansi.reset + ansi.bgBlack + ' 🎰 ');
1486
+ } else {
1487
+ write(ansi.gray + '[c]' + ansi.reset + ansi.bgBlack + ' Casino ');
1488
+ }
1489
+
1305
1490
  write(ansi.gray + '[q]' + ansi.reset + ansi.bgBlack + ' Quit ');
1306
1491
  write(ansi.reset);
1307
1492
  }
@@ -1722,11 +1907,84 @@ function render() {
1722
1907
  write(ansi.moveToTop);
1723
1908
  write(ansi.clearScreen);
1724
1909
 
1910
+ // Casino mode: top marquee border
1911
+ if (casinoModeEnabled) {
1912
+ write(ansi.moveTo(1, 1));
1913
+ write(casino.renderMarqueeLine(terminalWidth, 'top'));
1914
+ }
1915
+
1725
1916
  renderHeader();
1726
1917
  const logStart = renderBranchList();
1727
- renderActivityLog(logStart);
1918
+ const statsStart = renderActivityLog(logStart);
1919
+ renderCasinoStats(statsStart);
1728
1920
  renderFooter();
1729
1921
 
1922
+ // Casino mode: full border (top, bottom, left, right)
1923
+ if (casinoModeEnabled) {
1924
+ // Bottom marquee border
1925
+ write(ansi.moveTo(terminalHeight, 1));
1926
+ write(casino.renderMarqueeLine(terminalWidth, 'bottom'));
1927
+
1928
+ // Left and right side borders
1929
+ for (let row = 2; row < terminalHeight; row++) {
1930
+ // Left side
1931
+ write(ansi.moveTo(row, 1));
1932
+ write(casino.getMarqueeSideChar(row, terminalHeight, 'left'));
1933
+ // Right side
1934
+ write(ansi.moveTo(row, terminalWidth));
1935
+ write(casino.getMarqueeSideChar(row, terminalHeight, 'right'));
1936
+ }
1937
+ }
1938
+
1939
+ // Casino mode: slot reels on row 3 (below header) when polling or showing result
1940
+ if (casinoModeEnabled && casino.isSlotsActive()) {
1941
+ const slotDisplay = casino.getSlotReelDisplay();
1942
+ if (slotDisplay) {
1943
+ // Row 3: below header (row 1 is marquee, row 2 is header)
1944
+ const resultLabel = casino.getSlotResultLabel();
1945
+ let leftLabel, rightLabel;
1946
+
1947
+ if (casino.isSlotSpinning()) {
1948
+ leftLabel = ansi.bgBrightYellow + ansi.black + ansi.bold + ' POLLING ' + ansi.reset;
1949
+ rightLabel = '';
1950
+ } else if (resultLabel) {
1951
+ leftLabel = ansi.bgBrightGreen + ansi.black + ansi.bold + ' RESULT ' + ansi.reset;
1952
+ // Flash effect for jackpots, use result color for text
1953
+ const flash = resultLabel.isJackpot && (Math.floor(Date.now() / 150) % 2 === 0);
1954
+ const bgColor = flash ? ansi.bgBrightYellow : ansi.bgWhite;
1955
+ rightLabel = ' ' + bgColor + resultLabel.color + ansi.bold + ' ' + resultLabel.text + ' ' + ansi.reset;
1956
+ } else {
1957
+ leftLabel = ansi.bgBrightGreen + ansi.black + ansi.bold + ' RESULT ' + ansi.reset;
1958
+ rightLabel = '';
1959
+ }
1960
+
1961
+ const fullDisplay = leftLabel + ' ' + slotDisplay + rightLabel;
1962
+ const col = Math.floor((terminalWidth - 70) / 2); // Center the display
1963
+ write(ansi.moveTo(3, Math.max(2, col)));
1964
+ write(fullDisplay);
1965
+ }
1966
+ }
1967
+
1968
+ // Casino mode: win animation overlay
1969
+ if (casinoModeEnabled && casino.isWinAnimating()) {
1970
+ const winDisplay = casino.getWinDisplay(terminalWidth);
1971
+ if (winDisplay) {
1972
+ const row = Math.floor(terminalHeight / 2);
1973
+ write(ansi.moveTo(row, 1));
1974
+ write(winDisplay);
1975
+ }
1976
+ }
1977
+
1978
+ // Casino mode: loss animation overlay
1979
+ if (casinoModeEnabled && casino.isLossAnimating()) {
1980
+ const lossDisplay = casino.getLossDisplay(terminalWidth);
1981
+ if (lossDisplay) {
1982
+ const row = Math.floor(terminalHeight / 2);
1983
+ write(ansi.moveTo(row, 1));
1984
+ write(lossDisplay);
1985
+ }
1986
+ }
1987
+
1730
1988
  if (flashMessage) {
1731
1989
  renderFlash();
1732
1990
  }
@@ -2122,6 +2380,12 @@ async function pollGitChanges() {
2122
2380
  if (isPolling) return;
2123
2381
  isPolling = true;
2124
2382
  pollingStatus = 'fetching';
2383
+
2384
+ // Casino mode: start slot reels spinning (no sound - too annoying)
2385
+ if (casinoModeEnabled) {
2386
+ casino.startSlotReels(render);
2387
+ }
2388
+
2125
2389
  render();
2126
2390
 
2127
2391
  const fetchStartTime = Date.now();
@@ -2229,9 +2493,33 @@ async function pollGitChanges() {
2229
2493
  for (const branch of updatedBranches) {
2230
2494
  addLog(`Update on ${branch.name}: ${branch.commit}`, 'update');
2231
2495
  }
2496
+
2497
+ // Casino mode: add funny commentary
2498
+ if (casinoModeEnabled) {
2499
+ addLog(`🎰 ${getCasinoMessage('win')}`, 'success');
2500
+ }
2501
+
2232
2502
  const names = notifyBranches.map(b => b.name).join(', ');
2233
2503
  showFlash(names);
2234
2504
  playSound();
2505
+
2506
+ // Casino mode: trigger win effect based on number of updated branches
2507
+ if (casinoModeEnabled) {
2508
+ // Estimate line changes: more branches = bigger "win"
2509
+ // Each branch update counts as ~100 lines (placeholder until we calculate actual diff)
2510
+ const estimatedLines = notifyBranches.length * 100;
2511
+ const winLevel = casino.getWinLevel(estimatedLines);
2512
+ casino.stopSlotReels(true, render, winLevel); // Win - matching symbols + flash + label
2513
+ casino.triggerWin(estimatedLines, 0, render);
2514
+ if (winLevel) {
2515
+ casinoSounds.playForWinLevel(winLevel.key);
2516
+ }
2517
+ casino.recordPoll(true);
2518
+ }
2519
+ } else if (casinoModeEnabled) {
2520
+ // No updates - stop reels and show result briefly
2521
+ casino.stopSlotReels(false, render);
2522
+ casino.recordPoll(false);
2235
2523
  }
2236
2524
 
2237
2525
  // Remember which branch was selected before updating the list
@@ -2271,6 +2559,9 @@ async function pollGitChanges() {
2271
2559
  addLog(`Auto-pulling changes for ${currentBranch}...`, 'update');
2272
2560
  render();
2273
2561
 
2562
+ // Save the old commit for diff calculation (casino mode)
2563
+ const oldCommit = currentInfo.commit;
2564
+
2274
2565
  try {
2275
2566
  await execAsync(`git pull "${REMOTE_NAME}" "${currentBranch}"`);
2276
2567
  addLog(`Pulled successfully from ${currentBranch}`, 'success');
@@ -2282,6 +2573,20 @@ async function pollGitChanges() {
2282
2573
  previousBranchStates.set(currentBranch, newCommit.stdout.trim());
2283
2574
  // Reload browsers
2284
2575
  notifyClients();
2576
+
2577
+ // Casino mode: calculate actual diff and trigger win effect
2578
+ if (casinoModeEnabled && oldCommit) {
2579
+ const diffStats = await getDiffStats(oldCommit, 'HEAD');
2580
+ const totalLines = diffStats.added + diffStats.deleted;
2581
+ if (totalLines > 0) {
2582
+ casino.triggerWin(diffStats.added, diffStats.deleted, render);
2583
+ const winLevel = casino.getWinLevel(totalLines);
2584
+ if (winLevel) {
2585
+ addLog(`🎰 ${winLevel.label} +${diffStats.added}/-${diffStats.deleted} lines`, 'success');
2586
+ casinoSounds.playForWinLevel(winLevel.key);
2587
+ }
2588
+ }
2589
+ }
2285
2590
  } catch (e) {
2286
2591
  const errMsg = e.stderr || e.stdout || e.message || String(e);
2287
2592
  if (isMergeConflict(errMsg)) {
@@ -2293,6 +2598,12 @@ async function pollGitChanges() {
2293
2598
  'Auto-pull resulted in merge conflicts that need manual resolution.',
2294
2599
  'Run: git status to see conflicts'
2295
2600
  );
2601
+ // Casino mode: trigger loss effect
2602
+ if (casinoModeEnabled) {
2603
+ casino.triggerLoss('MERGE CONFLICT!', render);
2604
+ casinoSounds.playLoss();
2605
+ addLog(`💀 ${getCasinoMessage('loss')}`, 'error');
2606
+ }
2296
2607
  } else if (isAuthError(errMsg)) {
2297
2608
  addLog(`Authentication failed during pull`, 'error');
2298
2609
  addLog(`Check your Git credentials`, 'warning');
@@ -2313,9 +2624,20 @@ async function pollGitChanges() {
2313
2624
  }
2314
2625
 
2315
2626
  pollingStatus = 'idle';
2627
+ // Casino mode: stop slot reels if still spinning (already handled above, just cleanup)
2628
+ if (casinoModeEnabled && casino.isSlotSpinning()) {
2629
+ casino.stopSlotReels(false, render);
2630
+ }
2316
2631
  } catch (err) {
2317
2632
  const errMsg = err.stderr || err.message || String(err);
2318
2633
 
2634
+ // Casino mode: stop slot reels and show loss on error
2635
+ if (casinoModeEnabled) {
2636
+ casino.stopSlotReels(false, render);
2637
+ casino.triggerLoss('BUST!', render);
2638
+ casinoSounds.playLoss();
2639
+ }
2640
+
2319
2641
  // Handle different error types
2320
2642
  if (isNetworkError(errMsg)) {
2321
2643
  consecutiveNetworkFailures++;
@@ -2449,13 +2771,26 @@ const server = http.createServer((req, res) => {
2449
2771
 
2450
2772
  let fileWatcher = null;
2451
2773
  let debounceTimer = null;
2774
+ let ignorePatterns = [];
2452
2775
 
2453
2776
  function setupFileWatcher() {
2454
2777
  if (fileWatcher) fileWatcher.close();
2455
2778
 
2779
+ // Load gitignore patterns before setting up the watcher
2780
+ ignorePatterns = loadGitignorePatterns([STATIC_DIR, PROJECT_ROOT]);
2781
+ if (ignorePatterns.length > 0) {
2782
+ addLog(`Loaded ${ignorePatterns.length} ignore patterns from .gitignore`, 'info');
2783
+ }
2784
+
2456
2785
  try {
2457
2786
  fileWatcher = fs.watch(STATIC_DIR, { recursive: true }, (eventType, filename) => {
2458
2787
  if (!filename) return;
2788
+
2789
+ // Skip ignored files (.git directory and gitignore patterns)
2790
+ if (shouldIgnoreFile(filename, ignorePatterns)) {
2791
+ return;
2792
+ }
2793
+
2459
2794
  clearTimeout(debounceTimer);
2460
2795
  debounceTimer = setTimeout(() => {
2461
2796
  addLog(`File changed: ${filename}`, 'info');
@@ -2716,6 +3051,15 @@ function setupKeyboardInput() {
2716
3051
  }
2717
3052
  break;
2718
3053
 
3054
+ case 'o': // Open live server in browser
3055
+ if (!NO_SERVER) {
3056
+ const serverUrl = `http://localhost:${PORT}`;
3057
+ addLog(`Opening ${serverUrl} in browser...`, 'info');
3058
+ openInBrowser(serverUrl);
3059
+ render();
3060
+ }
3061
+ break;
3062
+
2719
3063
  case 'f':
2720
3064
  addLog('Fetching all branches...', 'update');
2721
3065
  await pollGitChanges();
@@ -2733,6 +3077,18 @@ function setupKeyboardInput() {
2733
3077
  render();
2734
3078
  break;
2735
3079
 
3080
+ case 'c': // Toggle casino mode
3081
+ casinoModeEnabled = casino.toggle();
3082
+ addLog(`Casino mode ${casinoModeEnabled ? '🎰 ENABLED' : 'disabled'}`, casinoModeEnabled ? 'success' : 'info');
3083
+ if (casinoModeEnabled) {
3084
+ addLog(`Have you noticed this game has that 'variable rewards' thing going on? 🤔😉`, 'info');
3085
+ if (soundEnabled) {
3086
+ casinoSounds.playJackpot();
3087
+ }
3088
+ }
3089
+ render();
3090
+ break;
3091
+
2736
3092
  // Number keys to set visible branch count
2737
3093
  case '1': case '2': case '3': case '4': case '5':
2738
3094
  case '6': case '7': case '8': case '9':
@@ -2850,6 +3206,9 @@ async function start() {
2850
3206
  const config = await ensureConfig(cliArgs);
2851
3207
  applyConfig(config);
2852
3208
 
3209
+ // Set up casino mode render callback for animations
3210
+ casino.setRenderCallback(render);
3211
+
2853
3212
  // Check for remote before starting TUI
2854
3213
  const hasRemote = await checkRemoteExists();
2855
3214
  if (!hasRemote) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-watchtower",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Terminal-based Git branch monitor with activity sparklines and optional dev server with live reload",
5
5
  "main": "bin/git-watchtower.js",
6
6
  "bin": {
@@ -18,8 +18,11 @@
18
18
  "typecheck": "tsc --noEmit"
19
19
  },
20
20
  "devDependencies": {
21
+ "@semantic-release/changelog": "^6.0.3",
22
+ "@semantic-release/git": "^10.0.1",
21
23
  "@types/node": "^22.0.0",
22
24
  "c8": "^10.1.2",
25
+ "semantic-release": "^25.0.3",
23
26
  "typescript": "^5.7.0"
24
27
  },
25
28
  "keywords": [
@@ -34,7 +37,7 @@
34
37
  "dashboard",
35
38
  "sparklines"
36
39
  ],
37
- "author": "",
40
+ "author": "drummel <drummel@gmail.com>",
38
41
  "license": "MIT",
39
42
  "repository": {
40
43
  "type": "git",
@@ -51,5 +54,36 @@
51
54
  "bin/git-watchtower.js",
52
55
  "README.md",
53
56
  "LICENSE"
54
- ]
57
+ ],
58
+ "publishConfig": {
59
+ "access": "public",
60
+ "registry": "https://registry.npmjs.org/"
61
+ },
62
+ "release": {
63
+ "branches": [
64
+ "main"
65
+ ],
66
+ "plugins": [
67
+ "@semantic-release/commit-analyzer",
68
+ "@semantic-release/release-notes-generator",
69
+ [
70
+ "@semantic-release/changelog",
71
+ {
72
+ "changelogFile": "CHANGELOG.md"
73
+ }
74
+ ],
75
+ "@semantic-release/npm",
76
+ "@semantic-release/github",
77
+ [
78
+ "@semantic-release/git",
79
+ {
80
+ "assets": [
81
+ "package.json",
82
+ "CHANGELOG.md"
83
+ ],
84
+ "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
85
+ }
86
+ ]
87
+ ]
88
+ }
55
89
  }