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.
@@ -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 active spinning';
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 active result win';
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 active result';
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
- if (!enabled) {
683
- if (ui.prevCasinoEnabled) {
684
- casinoCleanup();
685
- ui.prevCasinoEnabled = false;
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 = true;
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
- renderSessionStats();
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
- const t = '';
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
- // ── Session Stats ──────────────────────────────────────────────
1221
- function renderSessionStats() {
1222
- if (!state || !state.sessionStats) return;
1223
- const s = state.sessionStats;
1224
- const bar = document.getElementById('stats-bar');
1225
- const activeBranches = 0;
1226
- const staleBranches = 0;
1227
- if (state.branches) {
1228
- for (let i = 0; i < state.branches.length; i++) {
1229
- const b = state.branches[i];
1230
- // Consider stale if no updates and not current
1231
- if (b.justUpdated || b.name === state.currentBranch) {
1232
- activeBranches++;
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
- // ── Session Stats Card (sidebar) ───────────────────────────────
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) { card.innerHTML = ''; return; }
1219
+ if (!s) return '';
1257
1220
  const branches = (state && state.branches) || [];
1258
- let activeCount = 0;
1259
- let staleCount = 0;
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) activeCount++;
1263
- else staleCount++;
1264
- }
1265
- let html = '';
1266
- html += '<span class="stat-k">Session</span><span class="stat-v">' + escHtml(s.sessionDuration || '0m') + '</span>';
1267
- html += '<span class="stat-k">Lines</span><span class="stat-v"><span class="added">+' + fmtCompact(s.linesAdded || 0) + '</span> <span class="sep">/</span> <span class="deleted">-' + fmtCompact(s.linesDeleted || 0) + '</span></span>';
1268
- html += '<span class="stat-k">Polls</span><span class="stat-v">' + (s.totalPolls || 0) + ' <span class="sep">·</span> <span class="accent">' + (s.hitRate || 0) + '%</span> hit</span>';
1269
- if (s.lastUpdate) {
1270
- html += '<span class="stat-k">Last hit</span><span class="stat-v">' + escHtml(s.lastUpdate) + '</span>';
1271
- }
1272
- html += '<span class="stat-k">Branches</span><span class="stat-v">' + activeCount + ' <span class="sep">active</span> <span class="sep">·</span> ' + staleCount + ' <span class="sep">stale</span></span>';
1273
- card.innerHTML = html;
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 ────────────────────────────────
@@ -3,30 +3,57 @@
3
3
  * @module utils/sound
4
4
  */
5
5
 
6
- const { exec } = require('child_process');
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
- * Cross-platform: macOS (afplay), Linux (paplay/aplay), Windows (terminal bell).
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 for exec
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
- exec('afplay /System/Library/Sounds/Pop.aiff 2>/dev/null', { cwd });
52
+ execFile('afplay', ['/System/Library/Sounds/Pop.aiff'], { cwd }, noop);
20
53
  } else if (platform === 'linux') {
21
- exec(
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
- process.stdout.write('\x07');
56
+ playBell();
30
57
  }
31
58
  }
32
59