git-watchtower 1.8.1 → 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.
- package/bin/git-watchtower.js +87 -2
- package/package.json +1 -1
- package/src/state/store.js +6 -0
- package/src/ui/renderer.js +106 -0
- package/src/utils/version-check.js +22 -1
package/bin/git-watchtower.js
CHANGED
|
@@ -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
package/src/state/store.js
CHANGED
|
@@ -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
|
|
package/src/ui/renderer.js
CHANGED
|
@@ -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
|
-
|
|
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 };
|