git-watchtower 2.2.2 → 2.3.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/package.json +1 -1
- package/src/cli/args.js +13 -0
- package/src/git/branch.js +0 -77
- package/src/index.js +0 -3
- package/src/polling/engine.js +7 -153
- package/src/server/process.js +5 -356
- package/src/server/web-ui/css.js +207 -199
- package/src/server/web-ui/html.js +28 -18
- package/src/server/web-ui/js.js +79 -87
- package/src/utils/sound.js +39 -12
package/src/server/web-ui/js.js
CHANGED
|
@@ -525,8 +525,10 @@ ${pureFnBlock}
|
|
|
525
525
|
if (casino.lossFlashTimer) { clearInterval(casino.lossFlashTimer); casino.lossFlashTimer = null; }
|
|
526
526
|
casino.winFlashFrames = 0;
|
|
527
527
|
casino.lossFlashFrames = 0;
|
|
528
|
+
// Reels stay in DOM but drop the dynamic state classes — the casino
|
|
529
|
+
// layer's opacity transition handles hiding them visually.
|
|
528
530
|
const reels = document.getElementById('casino-reels');
|
|
529
|
-
if (reels) reels.className = 'casino-reels';
|
|
531
|
+
if (reels) reels.className = 'casino-reels-header';
|
|
530
532
|
const win = document.getElementById('casino-win-overlay');
|
|
531
533
|
if (win) win.className = 'casino-overlay';
|
|
532
534
|
const loss = document.getElementById('casino-loss-overlay');
|
|
@@ -546,7 +548,7 @@ ${pureFnBlock}
|
|
|
546
548
|
}
|
|
547
549
|
const reels = document.getElementById('casino-reels');
|
|
548
550
|
if (!reels) return;
|
|
549
|
-
reels.className = 'casino-reels
|
|
551
|
+
reels.className = 'casino-reels-header spinning';
|
|
550
552
|
const label = document.getElementById('casino-reel-label');
|
|
551
553
|
if (label) label.textContent = '';
|
|
552
554
|
casino.reelSpinTimer = setInterval(() => {
|
|
@@ -569,7 +571,7 @@ ${pureFnBlock}
|
|
|
569
571
|
const isJackpot = winLevel.key === 'jackpot' || winLevel.key === 'mega';
|
|
570
572
|
const sym = isJackpot ? '7⃣' : CASINO_SYMBOLS[Math.floor(Math.random() * (CASINO_SYMBOLS.length - 1))];
|
|
571
573
|
for (let i = 0; i < 5; i++) casinoSetReelSymbol(i, sym);
|
|
572
|
-
reels.className = 'casino-reels
|
|
574
|
+
reels.className = 'casino-reels-header result win';
|
|
573
575
|
label.textContent = winLevel.label.replace(/^[^A-Z0-9]+/, '').trim() || 'WIN';
|
|
574
576
|
label.style.color = winLevel.color;
|
|
575
577
|
// Fire the centered banner.
|
|
@@ -577,19 +579,19 @@ ${pureFnBlock}
|
|
|
577
579
|
// Let the flashing linger a beat, then settle.
|
|
578
580
|
casino.reelResultClearTimer = setTimeout(() => {
|
|
579
581
|
casino.reelResultClearTimer = null;
|
|
580
|
-
reels.className = 'casino-reels';
|
|
582
|
+
reels.className = 'casino-reels-header';
|
|
581
583
|
}, isJackpot ? 4000 : 2400);
|
|
582
584
|
} else {
|
|
583
585
|
// No updates — show a random losing line and auto-fade.
|
|
584
586
|
for (let i = 0; i < 5; i++) {
|
|
585
587
|
casinoSetReelSymbol(i, CASINO_SYMBOLS[(casino.reelFrame + i * 3) % CASINO_SYMBOLS.length]);
|
|
586
588
|
}
|
|
587
|
-
reels.className = 'casino-reels
|
|
589
|
+
reels.className = 'casino-reels-header result';
|
|
588
590
|
label.textContent = '\u{1f634} NOTHING';
|
|
589
591
|
label.style.color = '#8b949e';
|
|
590
592
|
casino.reelResultClearTimer = setTimeout(() => {
|
|
591
593
|
casino.reelResultClearTimer = null;
|
|
592
|
-
reels.className = 'casino-reels';
|
|
594
|
+
reels.className = 'casino-reels-header';
|
|
593
595
|
}, 2000);
|
|
594
596
|
}
|
|
595
597
|
}
|
|
@@ -652,42 +654,19 @@ ${pureFnBlock}
|
|
|
652
654
|
return { hadUpdates, totalLines };
|
|
653
655
|
}
|
|
654
656
|
|
|
655
|
-
function renderCasinoStats() {
|
|
656
|
-
const panel = document.getElementById('casino-stats-panel');
|
|
657
|
-
if (!panel) return;
|
|
658
|
-
const cs = state && state.casinoStats;
|
|
659
|
-
if (!cs) { panel.innerHTML = ''; return; }
|
|
660
|
-
const netClass = cs.netWinnings >= 0 ? 'pos' : 'neg';
|
|
661
|
-
const netSign = cs.netWinnings >= 0 ? '+' : '';
|
|
662
|
-
let html = '<div class="cstats-title">\u{1f3b0} CASINO WINNINGS \u{1f3b0}</div>';
|
|
663
|
-
html += '<div class="cstats-row"><span class="cstats-k">\u{1f4dd} Line Changes</span><span class="cstats-v"><span class="pos">+' + (cs.totalLinesAdded || 0) + '</span> / <span class="neg">-' + (cs.totalLinesDeleted || 0) + '</span> = <span class="gold">$' + (cs.totalLines || 0) + '</span></span></div>';
|
|
664
|
-
html += '<div class="cstats-row"><span class="cstats-k">\u{1f4b8} Poll Cost</span><span class="cstats-v neg">$' + (cs.totalPolls || 0) + '</span></div>';
|
|
665
|
-
html += '<div class="cstats-row"><span class="cstats-k">\u{1f4b0} Net Earnings</span><span class="cstats-v ' + netClass + '">' + netSign + '$' + (cs.netWinnings || 0) + '</span></div>';
|
|
666
|
-
html += '<div class="cstats-row"><span class="cstats-k">\u{1f3b0} House Edge</span><span class="cstats-v neon">' + (cs.houseEdge || 0) + '%</span></div>';
|
|
667
|
-
html += '<div class="cstats-row"><span class="cstats-k">\u{1f60e} Vibes</span><span class="cstats-v">' + escHtml(cs.vibesQuality || '') + '</span></div>';
|
|
668
|
-
html += '<div class="cstats-row"><span class="cstats-k">\u{1f3b2} Luck</span><span class="cstats-v gold">' + (cs.luckMeter || 0) + '%</span></div>';
|
|
669
|
-
html += '<div class="cstats-row"><span class="cstats-k">\u{1f9e0} Dopamine Hits</span><span class="cstats-v pos">' + (cs.dopamineHits || 0) + '</span></div>';
|
|
670
|
-
if (cs.consecutivePolls > 1) {
|
|
671
|
-
html += '<div class="cstats-row"><span class="cstats-k">\u{1f525} Streak</span><span class="cstats-v gold">' + cs.consecutivePolls + 'x</span></div>';
|
|
672
|
-
}
|
|
673
|
-
html += '<div class="cstats-row"><span class="cstats-k">⏱ Session</span><span class="cstats-v">' + escHtml(cs.sessionDuration || '') + '</span></div>';
|
|
674
|
-
panel.innerHTML = html;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
657
|
// Apply/tear down casino mode based on state.casinoModeEnabled.
|
|
658
|
+
// Stats rendering is handled by renderDashboardStats — this function
|
|
659
|
+
// only owns the body class flip, header icon swap, and timer cleanup.
|
|
678
660
|
function reconcileCasinoMode() {
|
|
679
661
|
if (!state) return;
|
|
680
662
|
const enabled = !!state.casinoModeEnabled;
|
|
681
663
|
document.body.classList.toggle('casino-active', enabled);
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
}
|
|
687
|
-
return;
|
|
664
|
+
const icon = document.getElementById('header-icon');
|
|
665
|
+
if (icon) icon.innerHTML = enabled ? '\u{1f3b0}' : '\u{1f3f0}';
|
|
666
|
+
if (!enabled && ui.prevCasinoEnabled) {
|
|
667
|
+
casinoCleanup();
|
|
688
668
|
}
|
|
689
|
-
ui.prevCasinoEnabled =
|
|
690
|
-
renderCasinoStats();
|
|
669
|
+
ui.prevCasinoEnabled = enabled;
|
|
691
670
|
}
|
|
692
671
|
|
|
693
672
|
// Drive reel spin/stop from the polling status transition, and fire win
|
|
@@ -710,6 +689,10 @@ ${pureFnBlock}
|
|
|
710
689
|
function render() {
|
|
711
690
|
if (!state) return;
|
|
712
691
|
|
|
692
|
+
// Apply casino mode FIRST so a later renderer throwing (the SSE
|
|
693
|
+
// handler wraps render() in try/catch) can't swallow casino effects.
|
|
694
|
+
reconcileCasinoMode();
|
|
695
|
+
|
|
713
696
|
// Header — hide project name pill when tabs are showing it
|
|
714
697
|
const projectEl = document.getElementById('project-name');
|
|
715
698
|
const hasTabs = state.projects && state.projects.length > 1;
|
|
@@ -739,10 +722,8 @@ ${pureFnBlock}
|
|
|
739
722
|
|
|
740
723
|
renderBranches();
|
|
741
724
|
renderActivityLog();
|
|
742
|
-
|
|
743
|
-
renderSessionStatsCard();
|
|
725
|
+
renderDashboardStats();
|
|
744
726
|
renderPrefsBar();
|
|
745
|
-
reconcileCasinoMode();
|
|
746
727
|
|
|
747
728
|
// Auto-show update notification (once per session)
|
|
748
729
|
if (state.updateAvailable && !ui.updateNotificationShown && !anyModalOpen()) {
|
|
@@ -890,7 +871,7 @@ ${pureFnBlock}
|
|
|
890
871
|
let html = '';
|
|
891
872
|
for (let i = 0; i < log.length; i++) {
|
|
892
873
|
const entry = log[i];
|
|
893
|
-
|
|
874
|
+
let t = '';
|
|
894
875
|
if (entry.timestamp) {
|
|
895
876
|
const d = new Date(entry.timestamp);
|
|
896
877
|
t = isNaN(d.getTime()) ? '' : d.toLocaleTimeString();
|
|
@@ -1217,60 +1198,71 @@ ${pureFnBlock}
|
|
|
1217
1198
|
|
|
1218
1199
|
function hideUpdate() { updateModal.hide(); }
|
|
1219
1200
|
|
|
1220
|
-
// ──
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
const
|
|
1226
|
-
|
|
1227
|
-
if (state.
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
} else {
|
|
1234
|
-
staleBranches++;
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
let html = '';
|
|
1239
|
-
html += '<span class="stat-item"><span class="stat-label">Session:</span> <span class="stat-value">' + escHtml(s.sessionDuration || '0m') + '</span></span>';
|
|
1240
|
-
html += '<span class="stat-item"><span class="stat-label">Lines:</span> <span class="stat-value">+' + (s.linesAdded || 0) + '/-' + (s.linesDeleted || 0) + '</span></span>';
|
|
1241
|
-
html += '<span class="stat-item"><span class="stat-label">Polls:</span> <span class="stat-value">' + (s.totalPolls || 0) + '</span> <span class="stat-label">(' + (s.hitRate || 0) + '% hit)</span></span>';
|
|
1242
|
-
if (s.lastUpdate) {
|
|
1243
|
-
html += '<span class="stat-item"><span class="stat-label">Last update:</span> <span class="stat-value">' + escHtml(s.lastUpdate) + '</span></span>';
|
|
1201
|
+
// ── Dashboard Stats Bar ────────────────────────────────────────
|
|
1202
|
+
// Permanent row above the keyboard footer. Default: grounded session
|
|
1203
|
+
// metrics. When casino mode is on the same row re-skins to "casino
|
|
1204
|
+
// winnings" — same DOM, different content + .casino-mode class.
|
|
1205
|
+
function renderDashboardStats() {
|
|
1206
|
+
const bar = document.getElementById('dashboard-stats');
|
|
1207
|
+
if (!bar) return;
|
|
1208
|
+
if (state && state.casinoModeEnabled && state.casinoStats) {
|
|
1209
|
+
bar.className = 'dashboard-stats casino-mode';
|
|
1210
|
+
bar.innerHTML = renderCasinoStatsRow(state.casinoStats);
|
|
1211
|
+
} else {
|
|
1212
|
+
bar.className = 'dashboard-stats';
|
|
1213
|
+
bar.innerHTML = renderSessionStatsRow();
|
|
1244
1214
|
}
|
|
1245
|
-
html += '<span class="stat-item"><span class="stat-label">Active:</span> <span class="stat-value">' + activeBranches + '</span> <span class="stat-label">Stale:</span> <span class="stat-value">' + staleBranches + '</span></span>';
|
|
1246
|
-
bar.innerHTML = html;
|
|
1247
1215
|
}
|
|
1248
1216
|
|
|
1249
|
-
|
|
1250
|
-
// Lives at the top of the activity log. Real, grounded numbers the
|
|
1251
|
-
// dashboard always shows — not dependent on casino mode.
|
|
1252
|
-
function renderSessionStatsCard() {
|
|
1253
|
-
const card = document.getElementById('session-stats-card');
|
|
1254
|
-
if (!card) return;
|
|
1217
|
+
function renderSessionStatsRow() {
|
|
1255
1218
|
const s = state && state.sessionStats;
|
|
1256
|
-
if (!s)
|
|
1219
|
+
if (!s) return '';
|
|
1257
1220
|
const branches = (state && state.branches) || [];
|
|
1258
|
-
let
|
|
1259
|
-
let
|
|
1221
|
+
let active = 0;
|
|
1222
|
+
let stale = 0;
|
|
1260
1223
|
for (let i = 0; i < branches.length; i++) {
|
|
1261
1224
|
const b = branches[i];
|
|
1262
|
-
if (b.justUpdated || b.name === state.currentBranch)
|
|
1263
|
-
else
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1225
|
+
if (b.justUpdated || b.name === state.currentBranch) active++;
|
|
1226
|
+
else stale++;
|
|
1227
|
+
}
|
|
1228
|
+
const stat = (k, v) => '<span class="stat"><span class="stat-k">' + k + '</span><span class="stat-v">' + v + '</span></span>';
|
|
1229
|
+
// Left = identity / what session this is.
|
|
1230
|
+
let left = '<div class="stats-group">';
|
|
1231
|
+
left += '<span class="stats-title">\u{1f4ca} Session Stats</span>';
|
|
1232
|
+
left += stat('Duration', escHtml(s.sessionDuration || '0m'));
|
|
1233
|
+
left += stat('Branches', active + ' <span class="sep">active</span> <span class="sep">·</span> ' + stale + ' <span class="sep">stale</span>');
|
|
1234
|
+
left += '</div>';
|
|
1235
|
+
// Right = live activity readouts.
|
|
1236
|
+
let right = '<div class="stats-group">';
|
|
1237
|
+
right += stat('Lines', '<span class="added">+' + fmtCompact(s.linesAdded || 0) + '</span> <span class="sep">/</span> <span class="deleted">-' + fmtCompact(s.linesDeleted || 0) + '</span>');
|
|
1238
|
+
right += stat('Polls', (s.totalPolls || 0) + ' <span class="sep">·</span> <span class="accent">' + (s.hitRate || 0) + '%</span> hit');
|
|
1239
|
+
if (s.lastUpdate) right += stat('Last hit', escHtml(s.lastUpdate));
|
|
1240
|
+
right += '</div>';
|
|
1241
|
+
return left + right;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function renderCasinoStatsRow(cs) {
|
|
1245
|
+
const netClass = cs.netWinnings >= 0 ? 'pos' : 'neg';
|
|
1246
|
+
const netSign = cs.netWinnings >= 0 ? '+' : '';
|
|
1247
|
+
const stat = (k, v) => '<span class="stat"><span class="stat-k">' + k + '</span><span class="stat-v">' + v + '</span></span>';
|
|
1248
|
+
const dollar = '$'; // avoid bare $ in generated JS — see js.dom.test.js
|
|
1249
|
+
// Left = identity + the underlying churn that drove the winnings.
|
|
1250
|
+
let left = '<div class="stats-group">';
|
|
1251
|
+
left += '<span class="stats-title">\u{1f3b0} Casino Stats</span>';
|
|
1252
|
+
left += stat('Session', escHtml(cs.sessionDuration || ''));
|
|
1253
|
+
left += stat('\u{1f4dd} Lines', '<span class="pos">+' + (cs.totalLinesAdded || 0) + '</span> <span class="sep">/</span> <span class="neg">-' + (cs.totalLinesDeleted || 0) + '</span> <span class="sep">=</span> <span class="gold">' + dollar + (cs.totalLines || 0) + '</span>');
|
|
1254
|
+
left += '</div>';
|
|
1255
|
+
// Right = the gambling readouts (fast-changing, attention-grabbing).
|
|
1256
|
+
let right = '<div class="stats-group">';
|
|
1257
|
+
right += stat('\u{1f4b8} Cost', '<span class="neg">' + dollar + (cs.totalPolls || 0) + '</span>');
|
|
1258
|
+
right += stat('\u{1f4b0} Net', '<span class="' + netClass + '">' + netSign + dollar + (cs.netWinnings || 0) + '</span>');
|
|
1259
|
+
right += stat('\u{1f3b0} Edge', '<span class="neon">' + (cs.houseEdge || 0) + '%</span>');
|
|
1260
|
+
right += stat('\u{1f3b2} Luck', '<span class="gold">' + (cs.luckMeter || 0) + '%</span>');
|
|
1261
|
+
right += stat('\u{1f60e} Vibes', escHtml(cs.vibesQuality || ''));
|
|
1262
|
+
right += stat('\u{1f9e0} Hits', '<span class="pos">' + (cs.dopamineHits || 0) + '</span>');
|
|
1263
|
+
if (cs.consecutivePolls > 1) right += stat('\u{1f525} Streak', '<span class="gold">' + cs.consecutivePolls + 'x</span>');
|
|
1264
|
+
right += '</div>';
|
|
1265
|
+
return left + right;
|
|
1274
1266
|
}
|
|
1275
1267
|
|
|
1276
1268
|
// ── Error Toast with Stash Hint ────────────────────────────────
|
package/src/utils/sound.js
CHANGED
|
@@ -3,30 +3,57 @@
|
|
|
3
3
|
* @module utils/sound
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const {
|
|
6
|
+
const { execFile } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const noop = () => {};
|
|
9
|
+
|
|
10
|
+
const playBell = () => {
|
|
11
|
+
process.stdout.write('\x07');
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// Linux audio-tool cascade. Each entry is [command, args]; we try them in
|
|
15
|
+
// order and fall through on non-zero exit, ending in a terminal bell.
|
|
16
|
+
// Mirrors the previous shell `||` chain without spawning a shell.
|
|
17
|
+
const LINUX_ATTEMPTS = [
|
|
18
|
+
['paplay', ['/usr/share/sounds/freedesktop/stereo/message-new-instant.oga']],
|
|
19
|
+
['paplay', ['/usr/share/sounds/freedesktop/stereo/complete.oga']],
|
|
20
|
+
['aplay', ['-q', '/usr/share/sounds/sound-icons/prompt.wav']],
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
function cascade(attempts, onAllFailed, options) {
|
|
24
|
+
if (attempts.length === 0) {
|
|
25
|
+
onAllFailed();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const [cmd, args] = attempts[0];
|
|
29
|
+
execFile(cmd, args, options, (err) => {
|
|
30
|
+
if (err) cascade(attempts.slice(1), onAllFailed, options);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
7
33
|
|
|
8
34
|
/**
|
|
9
35
|
* Play a system notification sound (non-blocking).
|
|
10
|
-
*
|
|
36
|
+
*
|
|
37
|
+
* Cross-platform: macOS (afplay), Linux (paplay/aplay cascade), Windows
|
|
38
|
+
* (terminal bell). Uses execFile (no shell) to match the pattern in
|
|
39
|
+
* casino/sounds.js — the previous exec calls passed fixed strings so
|
|
40
|
+
* there was no injection surface either way, but dropping the shell
|
|
41
|
+
* removes the per-call /bin/sh fork and makes the two sound modules
|
|
42
|
+
* consistent.
|
|
43
|
+
*
|
|
11
44
|
* @param {object} [options]
|
|
12
|
-
* @param {string} [options.cwd] - Working directory
|
|
45
|
+
* @param {string} [options.cwd] - Working directory
|
|
13
46
|
*/
|
|
14
47
|
function playSound(options = {}) {
|
|
15
48
|
const { platform } = process;
|
|
16
49
|
const cwd = options.cwd || process.cwd();
|
|
17
50
|
|
|
18
51
|
if (platform === 'darwin') {
|
|
19
|
-
|
|
52
|
+
execFile('afplay', ['/System/Library/Sounds/Pop.aiff'], { cwd }, noop);
|
|
20
53
|
} else if (platform === 'linux') {
|
|
21
|
-
|
|
22
|
-
'paplay /usr/share/sounds/freedesktop/stereo/message-new-instant.oga 2>/dev/null || ' +
|
|
23
|
-
'paplay /usr/share/sounds/freedesktop/stereo/complete.oga 2>/dev/null || ' +
|
|
24
|
-
'aplay /usr/share/sounds/sound-icons/prompt.wav 2>/dev/null || ' +
|
|
25
|
-
'printf "\\a"',
|
|
26
|
-
{ cwd }
|
|
27
|
-
);
|
|
54
|
+
cascade(LINUX_ATTEMPTS, playBell, { cwd });
|
|
28
55
|
} else {
|
|
29
|
-
|
|
56
|
+
playBell();
|
|
30
57
|
}
|
|
31
58
|
}
|
|
32
59
|
|