git-watchtower 1.10.20 → 1.11.1

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.
@@ -2531,7 +2531,7 @@ function setupKeyboardInput() {
2531
2531
  if (key === '\r' || key === '\n') {
2532
2532
  const selectedIdx = store.get('updateModalSelectedIndex') || 0;
2533
2533
  if (selectedIdx === 0) {
2534
- // Update now — run npm i -g git-watchtower
2534
+ // Update & restart — run npm i -g git-watchtower, then re-exec
2535
2535
  store.setState({ updateInProgress: true });
2536
2536
  render();
2537
2537
  const { spawn } = require('child_process');
@@ -2544,8 +2544,8 @@ function setupKeyboardInput() {
2544
2544
  store.setState({ updateInProgress: false, updateModalVisible: false, updateModalSelectedIndex: 0 });
2545
2545
  if (code === 0) {
2546
2546
  store.setState({ updateAvailable: null });
2547
- addLog('Successfully updated git-watchtower! Restart to use new version.', 'update');
2548
- showFlash('Updated! Restart to use new version.');
2547
+ addLog('Successfully updated git-watchtower! Restarting...', 'update');
2548
+ restartProcess();
2549
2549
  } else {
2550
2550
  addLog(`Update failed (exit code ${code}). Run manually: npm i -g git-watchtower`, 'error');
2551
2551
  showFlash('Update failed. Try manually: npm i -g git-watchtower');
@@ -2987,6 +2987,36 @@ async function handleWebAction(action, payload) {
2987
2987
  }
2988
2988
  }
2989
2989
  break;
2990
+ case 'checkUpdate':
2991
+ if (payload && payload.install) {
2992
+ store.setState({ updateInProgress: true });
2993
+ render();
2994
+ const { spawn: spawnUpdate } = require('child_process');
2995
+ const updateChild = spawnUpdate('npm', ['i', '-g', 'git-watchtower'], {
2996
+ stdio: 'ignore',
2997
+ detached: false,
2998
+ });
2999
+ updateChild.on('close', (code) => {
3000
+ store.setState({ updateInProgress: false });
3001
+ if (code === 0) {
3002
+ store.setState({ updateAvailable: null });
3003
+ sendResult(true, 'Updated! Restarting...');
3004
+ addLog('Successfully updated git-watchtower! Restarting...', 'update');
3005
+ restartProcess();
3006
+ } else {
3007
+ sendResult(false, `Update failed (exit code ${code})`);
3008
+ addLog(`Update failed (exit code ${code}). Run manually: npm i -g git-watchtower`, 'error');
3009
+ render();
3010
+ }
3011
+ });
3012
+ updateChild.on('error', (err2) => {
3013
+ store.setState({ updateInProgress: false });
3014
+ sendResult(false, err2.message);
3015
+ addLog(`Update failed: ${err2.message}`, 'error');
3016
+ render();
3017
+ });
3018
+ }
3019
+ break;
2990
3020
  }
2991
3021
  } catch (err) {
2992
3022
  addLog(`Web action error: ${err.message}`, 'error');
@@ -3121,6 +3151,47 @@ function stopWebDashboard() {
3121
3151
  return wasPort;
3122
3152
  }
3123
3153
 
3154
+ // ============================================================================
3155
+ // Restart after update
3156
+ // ============================================================================
3157
+
3158
+ /**
3159
+ * Restart the process after a successful update by re-execing with the same
3160
+ * arguments. Cleans up terminal state and spawns a replacement process,
3161
+ * then exits the current one.
3162
+ */
3163
+ function restartProcess() {
3164
+ // Restore terminal state
3165
+ write(ansi.showCursor);
3166
+ write(ansi.restoreScreen);
3167
+ restoreTerminalTitle();
3168
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
3169
+
3170
+ // Stop server, watcher, polling
3171
+ if (fileWatcher) fileWatcher.close();
3172
+ if (pollIntervalId) clearTimeout(pollIntervalId);
3173
+ if (SERVER_MODE === 'command') stopServerProcess();
3174
+ else if (SERVER_MODE === 'static') {
3175
+ clients.forEach(client => client.end());
3176
+ clients.clear();
3177
+ }
3178
+ stopWebDashboard();
3179
+
3180
+ console.log('\n♻ Restarting git-watchtower...\n');
3181
+
3182
+ const { spawn: spawnChild } = require('child_process');
3183
+ const child = spawnChild(process.argv[0], process.argv.slice(1), {
3184
+ stdio: 'inherit',
3185
+ detached: false,
3186
+ });
3187
+ child.on('error', () => {
3188
+ console.error('Failed to restart. Please run git-watchtower manually.');
3189
+ process.exit(1);
3190
+ });
3191
+ // Forward the child's exit code when it finishes
3192
+ child.on('close', (code) => process.exit(code || 0));
3193
+ }
3194
+
3124
3195
  // ============================================================================
3125
3196
  // Shutdown
3126
3197
  // ============================================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-watchtower",
3
- "version": "1.10.20",
3
+ "version": "1.11.1",
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": {
@@ -968,7 +968,7 @@ ${pureFnBlock}
968
968
  } else {
969
969
  html += '<div class="confirm-actions">';
970
970
  html += '<button class="confirm-btn" id="update-dismiss">Dismiss</button>';
971
- html += '<button class="confirm-btn primary" id="update-install">Update Now</button>';
971
+ html += '<button class="confirm-btn primary" id="update-install">Update &amp; Restart</button>';
972
972
  html += '</div>';
973
973
  }
974
974
  document.getElementById('update-content').innerHTML = html;
@@ -1485,7 +1485,7 @@ function renderUpdateModal(state, write) {
1485
1485
  const updateCmd = 'npm i -g git-watchtower';
1486
1486
 
1487
1487
  const options = [
1488
- 'Update now',
1488
+ 'Update & restart',
1489
1489
  'Show update command',
1490
1490
  ];
1491
1491
  const selectedIdx = state.updateModalSelectedIndex || 0;
@@ -5,6 +5,22 @@
5
5
 
6
6
  const { execFile } = require('child_process');
7
7
 
8
+ /**
9
+ * Validate that a string is a safe URL to pass to OS open commands.
10
+ * Rejects URLs containing shell metacharacters that could lead to
11
+ * command injection when passed through cmd.exe on Windows.
12
+ * @param {string} url
13
+ * @returns {boolean}
14
+ */
15
+ function isSafeUrl(url) {
16
+ if (!url || typeof url !== 'string') return false;
17
+ // Must start with http://, https://, or file://
18
+ if (!/^https?:\/\/|^file:\/\//i.test(url)) return false;
19
+ // Reject shell metacharacters that cmd.exe would interpret
20
+ if (/[&|<>^"!%]/.test(url)) return false;
21
+ return true;
22
+ }
23
+
8
24
  /**
9
25
  * Open a URL in the user's default browser.
10
26
  * Cross-platform: macOS (open), Windows (start), Linux (xdg-open).
@@ -13,6 +29,13 @@ const { execFile } = require('child_process');
13
29
  * @param {function} [onError] - Optional error callback (receives Error)
14
30
  */
15
31
  function openInBrowser(url, onError) {
32
+ if (!isSafeUrl(url)) {
33
+ if (onError) {
34
+ onError(new Error(`Refusing to open unsafe URL: ${url}`));
35
+ }
36
+ return;
37
+ }
38
+
16
39
  const platform = process.platform;
17
40
  let command;
18
41
  let args;
@@ -22,7 +45,7 @@ function openInBrowser(url, onError) {
22
45
  args = [url];
23
46
  } else if (platform === 'win32') {
24
47
  // On Windows, 'start' is a shell built-in, so we must use cmd.exe.
25
- // The URL is passed as a separate argument, not interpolated into a string.
48
+ // URL is validated above to reject shell metacharacters.
26
49
  command = 'cmd.exe';
27
50
  args = ['/c', 'start', '', url];
28
51
  } else {
@@ -37,4 +60,4 @@ function openInBrowser(url, onError) {
37
60
  });
38
61
  }
39
62
 
40
- module.exports = { openInBrowser };
63
+ module.exports = { openInBrowser, isSafeUrl };