#!/usr/bin/env node import { spawn } from 'node:child_process'; import { readdir, stat } from 'node:fs/promises'; import path from 'node:path'; import process from 'node:process'; const watchRoot = process.env.GO_WATCH_ROOT || process.cwd(); const intervalMs = Number(process.env.GO_WATCH_INTERVAL_MS || 700); const debounceMs = Number(process.env.GO_WATCH_DEBOUNCE_MS || 250); const shutdownGraceMs = Number(process.env.GO_WATCH_SHUTDOWN_GRACE_MS || 2500); const restartDelayMs = Number(process.env.GO_WATCH_RESTART_DELAY_MS || 200); const commandIndex = process.argv.indexOf('--'); const command = commandIndex >= 0 ? process.argv.slice(commandIndex + 1) : ['go', 'run', './cmd/gateway']; const ignoredDirs = new Set([ '.git', '.nx', 'coverage', 'dist', 'node_modules', 'tmp', 'vendor', ]); const watchedExtensions = new Set(['.go', '.mod', '.sum']); if (!command.length) { console.error('[go-watch] missing command after --'); process.exit(1); } let child = null; let knownSignature = ''; let restarting = false; let restartQueued = false; let pendingRestart = null; let shuttingDown = false; let stopped = false; function log(message) { console.log(`[go-watch] ${message}`); } function sleep(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } async function collectFiles(dir, files = []) { let entries = []; try { entries = await readdir(dir, { withFileTypes: true }); } catch { return files; } for (const entry of entries) { if (entry.name.startsWith('.') && entry.name !== '.env') { continue; } if (entry.isDirectory()) { if (!ignoredDirs.has(entry.name)) { await collectFiles(path.join(dir, entry.name), files); } continue; } if (watchedExtensions.has(path.extname(entry.name))) { files.push(path.join(dir, entry.name)); } } return files; } async function signature() { const files = await collectFiles(watchRoot); files.sort(); const parts = []; for (const file of files) { try { const info = await stat(file); parts.push(`${file}:${info.mtimeMs}:${info.size}`); } catch { parts.push(`${file}:deleted`); } } return parts.join('\n'); } function start() { if (stopped) { return; } log(`starting: ${command.join(' ')}`); child = spawn(command[0], command.slice(1), { cwd: watchRoot, detached: process.platform !== 'win32', env: process.env, stdio: 'inherit', }); child.on('exit', (code, signal) => { if (stopped || restarting) { return; } log(`process exited code=${code ?? 'null'} signal=${signal ?? 'null'}`); }); } function signalProcessTree(processToStop, signal) { if (!processToStop || processToStop.pid === undefined) { return; } try { if (process.platform === 'win32') { processToStop.kill(signal); } else { process.kill(-processToStop.pid, signal); } } catch (error) { if (error?.code !== 'ESRCH') { console.error(`[go-watch] failed to send ${signal}`, error); } } } function stopProcessTree(processToStop) { if (!processToStop || processToStop.exitCode !== null || processToStop.signalCode !== null) { return Promise.resolve(); } return new Promise((resolve) => { let done = false; const finish = () => { if (done) { return; } done = true; clearTimeout(forceTimer); clearTimeout(resolveTimer); resolve(); }; const forceTimer = setTimeout(() => { signalProcessTree(processToStop, 'SIGKILL'); }, shutdownGraceMs); const resolveTimer = setTimeout(finish, shutdownGraceMs + 500); forceTimer.unref(); resolveTimer.unref(); processToStop.once('exit', finish); signalProcessTree(processToStop, 'SIGTERM'); }); } async function restartNow() { if (restarting) { restartQueued = true; return; } restarting = true; log('change detected, restarting'); const previous = child; child = null; await stopProcessTree(previous); if (restartDelayMs > 0) { await sleep(restartDelayMs); } restarting = false; if (!stopped) { start(); } if (restartQueued && !stopped) { restartQueued = false; restart(); } } function restart() { if (pendingRestart) { clearTimeout(pendingRestart); } pendingRestart = setTimeout(() => { pendingRestart = null; void restartNow(); }, debounceMs); } async function poll() { try { const nextSignature = await signature(); if (!knownSignature) { knownSignature = nextSignature; } else if (nextSignature !== knownSignature) { knownSignature = nextSignature; restart(); } } catch (error) { console.error('[go-watch] scan failed', error); } } async function main() { log(`watching ${watchRoot}`); knownSignature = await signature(); start(); const timer = setInterval(poll, intervalMs); const shutdown = async (exitCode) => { if (shuttingDown) { return; } shuttingDown = true; stopped = true; clearInterval(timer); if (pendingRestart) { clearTimeout(pendingRestart); } await stopProcessTree(child); process.exit(exitCode); }; process.on('SIGINT', () => { void shutdown(130); }); process.on('SIGTERM', () => { void shutdown(143); }); } main().catch((error) => { console.error('[go-watch] fatal', error); process.exit(1); });