import { spawn } from "node:child_process"; import { existsSync, readdirSync, watch } from "node:fs"; import path from "node:path"; const RESTART_DEBOUNCE_MS = 300; export function resolvePython(repoRoot) { if (process.env.PYTHON) { return process.env.PYTHON; } const venvPython = path.join(repoRoot, ".venv", "bin", "python"); if (existsSync(venvPython)) { return venvPython; } return "python3"; } function watchPythonTree(rootDir, onChange) { const watchers = []; const watchDirectory = (dir) => { watchers.push( watch(dir, (_event, filename) => { if (!filename || !filename.endsWith(".py")) { return; } onChange(path.join(dir, filename)); }), ); for (const entry of readdirSync(dir, { withFileTypes: true })) { if (entry.isDirectory()) { watchDirectory(path.join(dir, entry.name)); } } }; watchDirectory(rootDir); return () => { for (const watcher of watchers) { watcher.close(); } }; } /** * Run the Chess Tutor API and restart it when Python sources change. */ export function startApiWithReload(repoRoot, { port = "8765", watchFiles = true } = {}) { const python = resolvePython(repoRoot); const srcDir = path.join(repoRoot, "src", "chess_tutor"); let proc = null; let stopping = false; let restartPending = false; let debounceTimer = null; let closeWatcher = null; function start() { proc = spawn(python, ["-m", "chess_tutor.web", "--api-only", "--port", port], { cwd: repoRoot, stdio: "inherit", }); proc.on("exit", (code) => { proc = null; if (stopping) { return; } if (restartPending) { restartPending = false; console.log("\n[dev] API reloaded.\n"); start(); return; } if (code !== 0 && code !== null) { process.exit(code); } }); } function scheduleRestart(changedPath) { if (stopping) { return; } clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { const rel = path.relative(repoRoot, changedPath); console.log(`\n[dev] Python change detected (${rel}) — reloading API…`); restartPending = true; if (proc) { proc.kill("SIGTERM"); } else { start(); } }, RESTART_DEBOUNCE_MS); } if (watchFiles && existsSync(srcDir)) { closeWatcher = watchPythonTree(srcDir, scheduleRestart); console.log(`[dev] Python hot reload: watching ${srcDir}`); } start(); return { port, stop() { stopping = true; clearTimeout(debounceTimer); closeWatcher?.(); if (proc) { proc.kill("SIGTERM"); } }, }; } /** Poll until the API responds (avoids Vite proxy ECONNREFUSED on startup). */ export async function waitForApiReady(port = "8765", { timeoutMs = 60000, intervalMs = 200 } = {}) { const url = `http://127.0.0.1:${port}/api/version`; const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { try { const res = await fetch(url); if (res.ok) { return; } } catch { // API not listening yet } await new Promise((resolve) => setTimeout(resolve, intervalMs)); } throw new Error(`API did not become ready at ${url} within ${timeoutMs / 1000}s`); }