import { mkdirSync, openSync, readFileSync, readlinkSync, realpathSync, rmSync } from "node:fs"; import { resolve } from "node:path"; const action = process.argv[2]; const root = process.cwd(); const repoRoot = resolve(root, "../.."); const stateDir = resolve(root, ".state/tasktree-native"); const pidPath = resolve(stateDir, "supervisor.pid"); const logPath = resolve(stateDir, "supervisor.log"); const servicesLogPath = resolve(stateDir, "services.log"); function readPid(): number | null { try { const pid = Number(readFileSync(pidPath, "utf8").trim()); return Number.isInteger(pid) && pid > 0 ? pid : null; } catch { return null; } } function isRunning(pid: number | null): boolean { if (!pid) return false; try { process.kill(pid, 0); try { const stat = readFileSync(`/proc/${pid}/stat`, "utf8"); const state = stat.slice(stat.lastIndexOf(")") + 2).split(" ")[0]; if (state === "Z") return false; } catch { // Non-Linux hosts do not expose /proc; the signal probe remains authoritative there. } return true; } catch { return false; } } function canonicalPath(path: string): string { try { return realpathSync(path); } catch { return resolve(path); } } function runningProcessCwd(pid: number | null): string | null { if (!pid) return null; try { return canonicalPath(readlinkSync(`/proc/${pid}/cwd`)); } catch { return null; } } function gitCommit(workspace: string | null): string | null { if (!workspace) return null; const result = Bun.spawnSync(["git", "-C", workspace, "rev-parse", "HEAD"], { stdout: "pipe", stderr: "ignore" }); if (result.exitCode !== 0) return null; return result.stdout.toString().trim() || null; } async function waitForExit(pid: number): Promise { const deadline = Date.now() + 15000; while (Date.now() < deadline) { if (!isRunning(pid)) return true; await Bun.sleep(25); } return !isRunning(pid); } mkdirSync(stateDir, { recursive: true }); const pid = readPid(); if (action === "start") { if (isRunning(pid)) { console.log(JSON.stringify({ ok: true, action, status: "already-running", pid, logPath })); process.exit(0); } const requiredEnvironment = ["TASKTREE_NATIVE_DATABASE_ENV_FILE", "TASKTREE_TEMPORAL_ADDRESS"] as const; const missingEnvironment = requiredEnvironment.filter((key) => !process.env[key]?.trim()); if (missingEnvironment.length > 0) { console.error(JSON.stringify({ ok: false, action, status: "preflight-failed", error: "missing-required-environment", missingEnvironment, valuesPrinted: false })); process.exit(1); } rmSync(pidPath, { force: true }); const logFd = openSync(logPath, "a"); const child = Bun.spawn([process.execPath, "scripts/tasktree-native-supervisor.ts"], { cwd: root, env: process.env, stdin: "ignore", stdout: logFd, stderr: logFd, detached: true }); child.unref(); await Bun.write(pidPath, `${child.pid}\n`); await Bun.sleep(150); if (!isRunning(child.pid)) { rmSync(pidPath, { force: true }); console.error(JSON.stringify({ ok: false, action, status: "start-failed", error: "supervisor-exited-before-ready", logPath, valuesPrinted: false })); process.exit(1); } console.log(JSON.stringify({ ok: true, action, status: "started", pid: child.pid, logPath })); } else if (action === "stop") { if (!isRunning(pid)) { rmSync(pidPath, { force: true }); console.log(JSON.stringify({ ok: true, action, status: "stopped" })); } else { process.kill(pid!, "SIGTERM"); if (!await waitForExit(pid!)) { console.error(JSON.stringify({ ok: false, action, status: "stop-timeout", pid })); process.exit(1); } rmSync(pidPath, { force: true }); console.log(JSON.stringify({ ok: true, action, status: "stopped", pid })); } } else if (action === "status") { const running = isRunning(pid); const processCwd = runningProcessCwd(running ? pid : null); const sourceWorkspace = processCwd ? canonicalPath(resolve(processCwd, "../..")) : null; const expectedProcessCwd = canonicalPath(root); const expectedSourceWorkspace = canonicalPath(repoRoot); console.log(JSON.stringify({ ok: running, action, status: running ? "running" : "stopped", pid: running ? pid : null, logPath, servicesLogPath, provenance: { expectedSourceWorkspace, sourceWorkspace, processCwd, commit: gitCommit(sourceWorkspace), workspaceMatches: sourceWorkspace === expectedSourceWorkspace, processCwdMatches: processCwd === expectedProcessCwd } })); process.exit(running ? 0 : 1); } else if (action === "logs") { for (const path of [logPath, servicesLogPath]) { try { console.log(readFileSync(path, "utf8").trimEnd().split("\n").slice(-80).join("\n")); } catch { // A log is optional until its process has started. } } } else { console.error("usage: bun run scripts/tasktree-native-service.ts "); process.exit(2); }