git-watchtower 1.6.0 → 1.6.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.
@@ -676,17 +676,27 @@ function stopServerProcess() {
676
676
 
677
677
  addLog('Stopping server...', 'update');
678
678
 
679
+ // Capture reference before nulling — needed for deferred SIGKILL
680
+ const proc = serverProcess;
681
+
679
682
  // Try graceful shutdown first
680
683
  if (process.platform === 'win32') {
681
- spawn('taskkill', ['/pid', serverProcess.pid, '/f', '/t']);
684
+ spawn('taskkill', ['/pid', proc.pid.toString(), '/f', '/t']);
682
685
  } else {
683
- serverProcess.kill('SIGTERM');
684
- // Force kill after timeout
685
- setTimeout(() => {
686
- if (serverProcess) {
687
- serverProcess.kill('SIGKILL');
686
+ proc.kill('SIGTERM');
687
+ // Force kill after grace period if process hasn't exited
688
+ const forceKillTimeout = setTimeout(() => {
689
+ try {
690
+ proc.kill('SIGKILL');
691
+ } catch (e) {
692
+ // Process may already be dead
688
693
  }
689
694
  }, 3000);
695
+
696
+ // Clear the force-kill timer if the process exits cleanly
697
+ proc.once('close', () => {
698
+ clearTimeout(forceKillTimeout);
699
+ });
690
700
  }
691
701
 
692
702
  serverProcess = null;
@@ -796,15 +806,28 @@ const LIVE_RELOAD_SCRIPT = `
796
806
  // Utility Functions
797
807
  // ============================================================================
798
808
 
809
+ // Default timeout for execAsync (30 seconds) — prevents hung git/CLI commands
810
+ // from permanently blocking the polling loop
811
+ const EXEC_ASYNC_TIMEOUT = 30000;
812
+
799
813
  function execAsync(command, options = {}) {
814
+ const { timeout = EXEC_ASYNC_TIMEOUT, ...restOptions } = options;
800
815
  return new Promise((resolve, reject) => {
801
- exec(command, { cwd: PROJECT_ROOT, ...options }, (error, stdout, stderr) => {
816
+ const child = exec(command, { cwd: PROJECT_ROOT, timeout, ...restOptions }, (error, stdout, stderr) => {
802
817
  if (error) {
803
818
  reject({ error, stdout, stderr });
804
819
  } else {
805
820
  resolve({ stdout: stdout.trim(), stderr: stderr.trim() });
806
821
  }
807
822
  });
823
+ // Also kill the child if the timeout fires (exec timeout sends SIGTERM
824
+ // but doesn't guarantee cleanup of process trees)
825
+ if (timeout > 0) {
826
+ const killTimer = setTimeout(() => {
827
+ try { child.kill('SIGKILL'); } catch (e) { /* already dead */ }
828
+ }, timeout + 5000);
829
+ child.on('close', () => clearTimeout(killTimer));
830
+ }
808
831
  });
809
832
  }
810
833
 
@@ -2010,6 +2033,16 @@ const server = http.createServer((req, res) => {
2010
2033
  pathname = path.normalize(pathname).replace(/^(\.\.[\/\\])+/, '');
2011
2034
  let filePath = path.join(STATIC_DIR, pathname);
2012
2035
 
2036
+ // Security: ensure resolved path stays within STATIC_DIR to prevent path traversal
2037
+ const resolvedPath = path.resolve(filePath);
2038
+ const resolvedStaticDir = path.resolve(STATIC_DIR);
2039
+ if (!resolvedPath.startsWith(resolvedStaticDir + path.sep) && resolvedPath !== resolvedStaticDir) {
2040
+ res.writeHead(403, { 'Content-Type': 'text/html' });
2041
+ res.end('<h1>403 Forbidden</h1>');
2042
+ addServerLog(`GET ${logPath} → 403 (path traversal blocked)`, true);
2043
+ return;
2044
+ }
2045
+
2013
2046
  if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
2014
2047
  filePath = path.join(filePath, 'index.html');
2015
2048
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-watchtower",
3
- "version": "1.6.0",
3
+ "version": "1.6.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": {