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 +39 -1
- package/bin/git-watchtower.js +362 -3
- package/package.json +37 -3
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Git Watchtower
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/git-watchtower)
|
|
4
|
+
[](https://www.npmjs.com/package/git-watchtower)
|
|
5
|
+
[](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**
|
|
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
|
package/bin/git-watchtower.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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.
|
|
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
|
}
|