git-watchtower 1.8.0 → 1.8.2

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.
@@ -74,7 +74,7 @@ const { formatTimeAgo } = require('../src/utils/time');
74
74
  const { openInBrowser: openUrl } = require('../src/utils/browser');
75
75
  const { playSound: playSoundEffect } = require('../src/utils/sound');
76
76
  const { parseArgs: parseCliArgs, applyCliArgsToConfig: mergeCliArgs, getHelpText, PACKAGE_VERSION } = require('../src/cli/args');
77
- const { checkForUpdate } = require('../src/utils/version-check');
77
+ const { checkForUpdate, startPeriodicUpdateCheck } = require('../src/utils/version-check');
78
78
  const { parseRemoteUrl, buildBranchUrl, detectPlatform, buildWebUrl, extractSessionUrl } = require('../src/git/remote');
79
79
  const { parseGitHubPr, parseGitLabMr, parseGitHubPrList, parseGitLabMrList, isBaseBranch } = require('../src/git/pr');
80
80
 
@@ -1232,6 +1232,11 @@ function render() {
1232
1232
  if (state.stashConfirmMode) {
1233
1233
  renderer.renderStashConfirm(state, write);
1234
1234
  }
1235
+
1236
+ // Update notification modal renders on top of everything
1237
+ if (state.updateModalVisible) {
1238
+ renderer.renderUpdateModal(state, write);
1239
+ }
1235
1240
  }
1236
1241
 
1237
1242
  function showFlash(message) {
@@ -2438,6 +2443,71 @@ function setupKeyboardInput() {
2438
2443
  return; // Ignore other keys in cleanup mode
2439
2444
  }
2440
2445
 
2446
+ // Handle update notification modal
2447
+ if (store.get('updateModalVisible')) {
2448
+ if (store.get('updateInProgress')) {
2449
+ return; // Block all keys while update is running
2450
+ }
2451
+ if (key === '\u001b') {
2452
+ store.setState({ updateModalVisible: false, updateModalSelectedIndex: 0 });
2453
+ render();
2454
+ return;
2455
+ }
2456
+ if (key === '\u001b[A' || key === 'k') { // Up
2457
+ const idx = store.get('updateModalSelectedIndex');
2458
+ if (idx > 0) {
2459
+ store.setState({ updateModalSelectedIndex: idx - 1 });
2460
+ render();
2461
+ }
2462
+ return;
2463
+ }
2464
+ if (key === '\u001b[B' || key === 'j') { // Down
2465
+ const idx = store.get('updateModalSelectedIndex');
2466
+ if (idx < 1) {
2467
+ store.setState({ updateModalSelectedIndex: idx + 1 });
2468
+ render();
2469
+ }
2470
+ return;
2471
+ }
2472
+ if (key === '\r' || key === '\n') {
2473
+ const selectedIdx = store.get('updateModalSelectedIndex') || 0;
2474
+ if (selectedIdx === 0) {
2475
+ // Update now — run npm i -g git-watchtower
2476
+ store.setState({ updateInProgress: true });
2477
+ render();
2478
+ const { spawn } = require('child_process');
2479
+ const child = spawn('npm', ['i', '-g', 'git-watchtower'], {
2480
+ stdio: 'ignore',
2481
+ detached: false,
2482
+ });
2483
+ child.on('close', (code) => {
2484
+ store.setState({ updateInProgress: false, updateModalVisible: false, updateModalSelectedIndex: 0 });
2485
+ if (code === 0) {
2486
+ store.setState({ updateAvailable: null });
2487
+ addLog('Successfully updated git-watchtower! Restart to use new version.', 'update');
2488
+ showFlash('Updated! Restart to use new version.');
2489
+ } else {
2490
+ addLog(`Update failed (exit code ${code}). Run manually: npm i -g git-watchtower`, 'error');
2491
+ showFlash('Update failed. Try manually: npm i -g git-watchtower');
2492
+ }
2493
+ render();
2494
+ });
2495
+ child.on('error', (err) => {
2496
+ store.setState({ updateInProgress: false, updateModalVisible: false, updateModalSelectedIndex: 0 });
2497
+ addLog(`Update failed: ${err.message}. Run manually: npm i -g git-watchtower`, 'error');
2498
+ showFlash('Update failed. Try manually: npm i -g git-watchtower');
2499
+ render();
2500
+ });
2501
+ } else {
2502
+ // Show update command — dismiss modal with flash showing the command
2503
+ store.setState({ updateModalVisible: false, updateModalSelectedIndex: 0 });
2504
+ showFlash('Run: npm i -g git-watchtower');
2505
+ }
2506
+ return;
2507
+ }
2508
+ return; // Block all other keys while modal is shown
2509
+ }
2510
+
2441
2511
  // Handle stash confirmation dialog
2442
2512
  if (store.get('stashConfirmMode')) {
2443
2513
  if (key === '\u001b[A' || key === 'k') { // Up
@@ -2958,11 +3028,26 @@ async function start() {
2958
3028
  // Check for newer version on npm (non-blocking, silent on failure)
2959
3029
  checkForUpdate().then((latestVersion) => {
2960
3030
  if (latestVersion) {
2961
- store.setState({ updateAvailable: latestVersion });
3031
+ store.setState({ updateAvailable: latestVersion, updateModalVisible: true });
2962
3032
  addLog(`New version available: ${latestVersion} \u2192 npm i -g git-watchtower`, 'update');
2963
3033
  render();
2964
3034
  }
2965
3035
  }).catch(() => {});
3036
+
3037
+ // Re-check for updates periodically (every 4 hours) while running
3038
+ const periodicCheck = startPeriodicUpdateCheck((latestVersion) => {
3039
+ const alreadyKnown = store.get('updateAvailable');
3040
+ store.setState({ updateAvailable: latestVersion });
3041
+ if (!alreadyKnown) {
3042
+ // First time discovering an update during this session — show modal
3043
+ store.setState({ updateModalVisible: true });
3044
+ addLog(`New version available: ${latestVersion} \u2192 npm i -g git-watchtower`, 'update');
3045
+ }
3046
+ render();
3047
+ });
3048
+
3049
+ // Clean up periodic check on exit
3050
+ process.on('exit', () => periodicCheck.stop());
2966
3051
  }
2967
3052
 
2968
3053
  start().catch(err => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-watchtower",
3
- "version": "1.8.0",
3
+ "version": "1.8.2",
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": {
@@ -102,6 +102,9 @@
102
102
  * @property {string} projectName - Project name
103
103
  * @property {number} clientCount - Connected SSE clients
104
104
  * @property {string|null} updateAvailable - Latest version if update available, or null
105
+ * @property {boolean} updateModalVisible - Whether the update notification modal is shown
106
+ * @property {number} updateModalSelectedIndex - Selected option index in the update modal
107
+ * @property {boolean} updateInProgress - Whether an update is currently being installed
105
108
  */
106
109
 
107
110
  /**
@@ -184,6 +187,9 @@ function getInitialState() {
184
187
 
185
188
  // Version check
186
189
  updateAvailable: null,
190
+ updateModalVisible: false,
191
+ updateModalSelectedIndex: 0,
192
+ updateInProgress: false,
187
193
  };
188
194
  }
189
195
 
@@ -96,19 +96,19 @@ function renderHeader(state, write) {
96
96
  }
97
97
 
98
98
  let serverInfo = '';
99
- let serverInfoVisible = '';
99
+ let serverInfoVisibleLen = 0;
100
100
  if (state.serverMode === 'none') {
101
- serverInfoVisible = '';
101
+ serverInfoVisibleLen = 0;
102
102
  } else {
103
103
  const statusDot = state.serverRunning
104
104
  ? ansi.green + '\u25CF'
105
105
  : (state.serverCrashed ? ansi.red + '\u25CF' : ansi.gray + '\u25CB');
106
- serverInfoVisible = `localhost:${state.port} `;
106
+ serverInfoVisibleLen = 2 + `localhost:${state.port} `.length; // dot + space + "localhost:PORT "
107
107
  serverInfo = statusDot + ansi.white + ` localhost:${state.port} `;
108
108
  }
109
109
 
110
110
  const rightContent = `${modeBadge} ${serverInfo}${statusIcon}${ansi.bgBlue} ${soundIcon}${ansi.bgBlue} `;
111
- const rightVisibleLen = modeLabel.length + 1 + serverInfoVisible.length + 5;
111
+ const rightVisibleLen = modeLabel.length + 1 + serverInfoVisibleLen + 5;
112
112
 
113
113
  const usedSpace = leftVisibleLen + badgesVisibleLen + rightVisibleLen;
114
114
  const padding = Math.max(1, width - usedSpace);
@@ -1311,6 +1311,111 @@ function renderCleanupConfirm(state, write) {
1311
1311
  }
1312
1312
  }
1313
1313
 
1314
+ // ---------------------------------------------------------------------------
1315
+ // renderUpdateModal
1316
+ // ---------------------------------------------------------------------------
1317
+
1318
+ /**
1319
+ * Render a prominent update-available notification modal.
1320
+ *
1321
+ * @param {object} state
1322
+ * @param {function} write
1323
+ */
1324
+ function renderUpdateModal(state, write) {
1325
+ if (!state.updateModalVisible || !state.updateAvailable) return;
1326
+
1327
+ const latestVersion = state.updateAvailable;
1328
+ const currentVer = PACKAGE_VERSION;
1329
+ const updateCmd = 'npm i -g git-watchtower';
1330
+
1331
+ const options = [
1332
+ 'Update now',
1333
+ 'Show update command',
1334
+ ];
1335
+ const selectedIdx = state.updateModalSelectedIndex || 0;
1336
+
1337
+ const width = Math.min(52, state.terminalWidth - 4);
1338
+ const col = Math.floor((state.terminalWidth - width) / 2);
1339
+ const row = Math.max(2, Math.floor((state.terminalHeight - 14) / 2));
1340
+
1341
+ const lines = [];
1342
+ lines.push('Update Available');
1343
+ lines.push('');
1344
+ lines.push(`Current version: v${currentVer}`);
1345
+ lines.push(`Latest version: v${latestVersion}`);
1346
+ lines.push('');
1347
+
1348
+ if (state.updateInProgress) {
1349
+ lines.push('Updating...');
1350
+ lines.push('');
1351
+ lines.push(` ${updateCmd}`);
1352
+ lines.push('');
1353
+ lines.push('Please wait...');
1354
+ } else {
1355
+ // Option lines
1356
+ const optionStartIdx = lines.length;
1357
+ for (const opt of options) {
1358
+ lines.push(opt);
1359
+ }
1360
+ lines.push('');
1361
+ lines.push('[Enter] Select [Esc] Dismiss');
1362
+ }
1363
+
1364
+ const optionStartIdx = state.updateInProgress ? -1 : 5;
1365
+ const height = lines.length + 2;
1366
+
1367
+ // Draw magenta double-border box
1368
+ write(ansi.moveTo(row, col));
1369
+ write(ansi.magenta + ansi.bold);
1370
+ write(box.dTopLeft + box.dHorizontal.repeat(width - 2) + box.dTopRight);
1371
+
1372
+ for (let i = 1; i < height - 1; i++) {
1373
+ write(ansi.moveTo(row + i, col));
1374
+ write(ansi.magenta + box.dVertical + ansi.reset + ' ' + ' '.repeat(width - 6) + ' ' + ansi.magenta + box.dVertical + ansi.reset);
1375
+ }
1376
+
1377
+ write(ansi.moveTo(row + height - 1, col));
1378
+ write(ansi.magenta + box.dBottomLeft + box.dHorizontal.repeat(width - 2) + box.dBottomRight);
1379
+ write(ansi.reset);
1380
+
1381
+ // Render content
1382
+ let contentRow = row + 1;
1383
+ for (let i = 0; i < lines.length; i++) {
1384
+ const line = lines[i];
1385
+ write(ansi.moveTo(contentRow, col + 3));
1386
+
1387
+ if (i === 0) {
1388
+ // Title — centered, bold magenta
1389
+ const titlePadding = Math.floor((width - 6 - line.length) / 2);
1390
+ write(' '.repeat(titlePadding) + ansi.magenta + ansi.bold + line + ansi.reset + ' '.repeat(Math.max(0, width - 6 - titlePadding - line.length)));
1391
+ } else if (i >= optionStartIdx && optionStartIdx >= 0 && i < optionStartIdx + options.length) {
1392
+ // Selectable option
1393
+ const optIdx = i - optionStartIdx;
1394
+ const isSelected = optIdx === selectedIdx;
1395
+ const prefix = isSelected ? '\u25b8 ' : ' ';
1396
+ const optText = prefix + line;
1397
+ if (isSelected) {
1398
+ write(ansi.bold + ansi.cyan + padRight(optText, width - 6) + ansi.reset);
1399
+ } else {
1400
+ write(ansi.gray + padRight(optText, width - 6) + ansi.reset);
1401
+ }
1402
+ } else if (line === '[Enter] Select [Esc] Dismiss') {
1403
+ // Keyboard hints — centered, dim
1404
+ const lPadding = Math.floor((width - 6 - line.length) / 2);
1405
+ write(ansi.dim + ' '.repeat(Math.max(0, lPadding)) + line + ' '.repeat(Math.max(0, width - 6 - lPadding - line.length)) + ansi.reset);
1406
+ } else if (line === 'Updating...') {
1407
+ write(ansi.bold + ansi.yellow + padRight(line, width - 6) + ansi.reset);
1408
+ } else if (line === 'Please wait...') {
1409
+ const lPadding = Math.floor((width - 6 - line.length) / 2);
1410
+ write(ansi.dim + ' '.repeat(Math.max(0, lPadding)) + line + ' '.repeat(Math.max(0, width - 6 - lPadding - line.length)) + ansi.reset);
1411
+ } else {
1412
+ // Regular content
1413
+ write(padRight(line, width - 6));
1414
+ }
1415
+ contentRow++;
1416
+ }
1417
+ }
1418
+
1314
1419
  // ---------------------------------------------------------------------------
1315
1420
  // Exports
1316
1421
  // ---------------------------------------------------------------------------
@@ -1330,4 +1435,5 @@ module.exports = {
1330
1435
  renderActionModal,
1331
1436
  renderStashConfirm,
1332
1437
  renderCleanupConfirm,
1438
+ renderUpdateModal,
1333
1439
  };
@@ -54,4 +54,25 @@ function checkForUpdate() {
54
54
  });
55
55
  }
56
56
 
57
- module.exports = { checkForUpdate, compareVersions };
57
+ /** Default interval between periodic update checks (4 hours in ms) */
58
+ const UPDATE_CHECK_INTERVAL = 4 * 60 * 60 * 1000;
59
+
60
+ /**
61
+ * Create a periodic update checker that re-checks npm at a fixed interval.
62
+ * @param {(latestVersion: string) => void} onUpdateFound - Called when a new version is detected
63
+ * @param {number} [interval] - Check interval in ms (default: 4 hours)
64
+ * @returns {{ stop: () => void }} Controller with stop() to clear the timer
65
+ */
66
+ function startPeriodicUpdateCheck(onUpdateFound, interval = UPDATE_CHECK_INTERVAL) {
67
+ const timerId = setInterval(() => {
68
+ checkForUpdate()
69
+ .then((latestVersion) => {
70
+ if (latestVersion) onUpdateFound(latestVersion);
71
+ })
72
+ .catch(() => {});
73
+ }, interval);
74
+
75
+ return { stop: () => clearInterval(timerId) };
76
+ }
77
+
78
+ module.exports = { checkForUpdate, compareVersions, startPeriodicUpdateCheck, UPDATE_CHECK_INTERVAL };