import { closeSync, mkdirSync, openSync, readFileSync } from "node:fs"; import { resolve } from "node:path"; const webRoot = process.cwd(); const repoRoot = resolve(webRoot, "../.."); const stateDir = resolve(webRoot, ".state/tasktree-native"); const databaseEnvFile = process.env.TASKTREE_NATIVE_DATABASE_ENV_FILE; const temporalAddress = process.env.TASKTREE_TEMPORAL_ADDRESS; if (!databaseEnvFile) throw new Error("TASKTREE_NATIVE_DATABASE_ENV_FILE is required"); if (!temporalAddress) throw new Error("TASKTREE_TEMPORAL_ADDRESS is required"); function readEnvValue(path: string, key: string): string { const line = readFileSync(path, "utf8") .split(/\r?\n/u) .find((candidate) => candidate.startsWith(`${key}=`)); if (!line) throw new Error(`${key} is missing from the configured env source`); const raw = line.slice(key.length + 1).trim(); if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) return raw.slice(1, -1); return raw; } mkdirSync(stateDir, { recursive: true }); const logFd = openSync(resolve(stateDir, "services.log"), "a"); const databaseUrl = readEnvValue(databaseEnvFile, "DATABASE_URL"); const commonEnv = { ...process.env, TASKTREE_DATABASE_URL: databaseUrl, TASKTREE_TEMPORAL_ADDRESS: temporalAddress, TASKTREE_TEMPORAL_NAMESPACE: process.env.TASKTREE_TEMPORAL_NAMESPACE ?? "unidesk", TASKTREE_TEMPORAL_TASK_QUEUE: process.env.TASKTREE_TEMPORAL_TASK_QUEUE ?? "hwlab-v03-tasktree" }; const children = [ Bun.spawn([process.execPath, "cmd/hwlab-tasktree-worker/main.ts"], { cwd: repoRoot, env: commonEnv, stdin: "ignore", stdout: logFd, stderr: logFd }), Bun.spawn([process.execPath, "cmd/hwlab-tasktree-api/main.ts"], { cwd: repoRoot, env: { ...commonEnv, TASKTREE_API_HOST: "127.0.0.1", TASKTREE_API_PORT: "6673" }, stdin: "ignore", stdout: logFd, stderr: logFd }), Bun.spawn([process.execPath, "x", "vite", "--config", "scripts/tasktree-native-vite.config.ts"], { cwd: webRoot, env: { ...process.env, HWLAB_TASKTREE_NATIVE_API_URL: "http://127.0.0.1:6673" }, stdin: "ignore", stdout: logFd, stderr: logFd }) ]; process.stdout.write(`${JSON.stringify({ serviceId: "hwlab-tasktree-native", status: "started", pids: children.map((child) => child.pid), valuesPrinted: false })}\n`); let stopping = false; async function stop(): Promise { if (stopping) return; stopping = true; for (const child of children) child.kill("SIGTERM"); await Promise.allSettled(children.map((child) => child.exited)); closeSync(logFd); process.exit(0); } process.once("SIGINT", stop); process.once("SIGTERM", stop); const exited = await Promise.race(children.map(async (child, index) => ({ index, code: await child.exited }))); if (!stopping) { process.stderr.write(`${JSON.stringify({ serviceId: "hwlab-tasktree-native", status: "child-exited", ...exited })}\n`); await stop(); }