#!/usr/bin/env node /** Supports app-core build, packaging, or development orchestration for run node mjs. */ import { spawn } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import process from "node:process"; import restartExitCodeDefinition from "../../shared/src/restart-exit-code.json" with { type: "json", }; import { registerRestartAndShouldAbort } from "./lib/restart-guard.mjs"; import { syncElizaEnvAliases } from "./lib/sync-eliza-env-aliases.mjs"; import { chooseElizaRuntime, resolveRuntimeExecPath, } from "./run-node-runtime.mjs"; const args = process.argv.slice(2); const cwd = process.cwd(); // Load worktree-specific env overrides (ports, state dir) before building the child env. // Generated by: bash scripts/worktree-env.sh const _worktreeEnvPath = path.join(cwd, ".env.worktree"); if (fs.existsSync(_worktreeEnvPath)) { try { const content = fs.readFileSync(_worktreeEnvPath, "utf-8"); for (const line of content.split("\n")) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) continue; const eq = trimmed.indexOf("="); if (eq < 1) continue; const key = trimmed.slice(0, eq).trim(); const val = trimmed.slice(eq + 1).trim(); // override: false — don't clobber explicitly set env vars if (!process.env[key]) { process.env[key] = val; } } } catch { // Ignore read errors — worktree env is optional } } syncElizaEnvAliases(); const env = { ...process.env }; if (!env.ELIZA_NAMESPACE) { env.ELIZA_NAMESPACE = "eliza"; } // WHY: The child runs dist/eliza.js, which dynamic-imports @elizaos/plugin-*. Node does not // use cwd to resolve package names for import("pkg"); we must set NODE_PATH to repo root // node_modules so those imports succeed. See docs/plugin-resolution-and-node-path.md. const rootModules = path.join(cwd, "node_modules"); env.NODE_PATH = env.NODE_PATH ? `${rootModules}${path.delimiter}${env.NODE_PATH}` : rootModules; const compiler = "tsdown"; const distRoot = path.join(cwd, "dist"); const distEntry = path.join(distRoot, "/entry.js"); const buildStampPath = path.join(distRoot, ".buildstamp"); const srcRoot = path.join(cwd, "src"); const configFiles = [ path.join(cwd, "tsconfig.json"), path.join(cwd, "package.json"), ]; const statMtime = (filePath) => { try { return fs.statSync(filePath).mtimeMs; } catch { return null; } }; const isExcludedSource = (filePath) => { const relativePath = path.relative(srcRoot, filePath); if (relativePath.startsWith("..")) { return false; } return ( relativePath.endsWith(".test.ts") || relativePath.endsWith(".test.tsx") || relativePath.endsWith(`test-utils.ts`) ); }; const findLatestMtime = (dirPath, shouldSkip) => { let latest = null; const queue = [dirPath]; while (queue.length > 0) { const current = queue.pop(); if (!current) { continue; } let entries = []; try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch { continue; } for (const entry of entries) { const fullPath = path.join(current, entry.name); if (entry.isDirectory()) { queue.push(fullPath); continue; } if (!entry.isFile()) { continue; } if (shouldSkip?.(fullPath)) { continue; } const mtime = statMtime(fullPath); if (mtime == null) { continue; } if (latest == null || mtime > latest) { latest = mtime; } } } return latest; }; const shouldBuild = () => { if (env.ELIZA_FORCE_BUILD === "1") { return true; } const stampMtime = statMtime(buildStampPath); if (stampMtime == null) { return true; } if (statMtime(distEntry) == null) { return true; } for (const filePath of configFiles) { const mtime = statMtime(filePath); if (mtime != null && mtime > stampMtime) { return true; } } const srcMtime = findLatestMtime(srcRoot, isExcludedSource); if (srcMtime != null && srcMtime > stampMtime) { return true; } return false; }; const logRunner = (message) => { if (env.ELIZA_RUNNER_LOG === "0") { return; } process.stderr.write(`[eliza] ${message}\n`); }; /** Exit code used by the restart action to signal "restart requested". */ const RESTART_EXIT_CODE = restartExitCodeDefinition.restartExitCode; /** Guard against infinite restart loops. */ const MAX_RESTARTS_IN_WINDOW = 5; const RESTART_WINDOW_MS = 60_000; const restartTimestamps = []; const commandExists = (command) => { const pathEnv = process.env.PATH ?? ""; if (!pathEnv) return false; const isWindows = process.platform === "win32"; const extensions = isWindows ? (process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";").filter(Boolean) : [""]; for (const dir of pathEnv.split(path.delimiter).filter(Boolean)) { for (const ext of extensions) { const candidate = path.join( dir, isWindows && !command.toLowerCase().endsWith(ext.toLowerCase()) ? `${command}${ext}` : command, ); if (fs.existsSync(candidate)) return true; } } return false; }; const runNode = () => { const hasBun = commandExists("bun"); const hasNode = commandExists("node"); const { runtime, warning } = chooseElizaRuntime({ requestedRuntime: process.env.ELIZA_RUNTIME, platform: process.platform, bunVersion: process.versions?.bun, hasBun, hasNode, }); if (warning) { logRunner(`${warning} Set ELIZA_RUNTIME=bun to force Bun runtime.`); } if (runtime === "bun" && !hasBun) { logRunner("Bun runtime was selected, but bun was not found in PATH."); process.exit(1); } if (runtime === "node" && !hasNode) { logRunner("Node.js runtime was selected, but node was not found in PATH."); process.exit(1); } const execPath = resolveRuntimeExecPath({ runtime, currentExecPath: process.execPath, platform: process.platform, explicitNodePath: process.env.ELIZA_NODE_PATH, }); // Forks rename `eliza.mjs` to their own entry filename. // Letting them override the spawned filename via `ELIZA_ENTRY_FILE` avoids // every fork having to ship a shim at the old name. Read from `env` (the // local snapshot built at module load) for consistency with what the child // process actually receives — `.env.worktree` overrides applied above are // visible here too. const entryFile = env.ELIZA_ENTRY_FILE?.trim() || "eliza.mjs"; const nodeProcess = spawn(execPath, [entryFile, ...args], { cwd, env, stdio: "inherit", }); nodeProcess.on("error", (err) => { logRunner(`Failed to spawn ${execPath}: ${err.message}`); process.exit(1); }); nodeProcess.on("exit", (exitCode, exitSignal) => { if (exitSignal) { process.exit(1); } // Restart loop: when the agent requests a restart it exits with code 75. // Re-run the full runner (including the build-staleness check) so any // source changes are compiled before the new process starts. if (exitCode === RESTART_EXIT_CODE) { // Guard against rapid restart loops (window/trim/threshold in restart-guard.mjs). if ( registerRestartAndShouldAbort( restartTimestamps, Date.now(), MAX_RESTARTS_IN_WINDOW, RESTART_WINDOW_MS, ) ) { logRunner( `Restart loop detected: ${restartTimestamps.length} restarts in ${RESTART_WINDOW_MS / 1000}s — aborting.`, ); process.exit(1); } logRunner("Restart requested — relaunching..."); // Re-check whether a rebuild is needed (source files may have changed). if (shouldBuild()) { logRunner("Building TypeScript (dist is stale)."); const bunxArgs = [compiler]; const buildCmd = process.platform === "win32" ? "cmd.exe" : "bunx"; const buildArgs = process.platform === "win32" ? ["/d", "/s", "/c", "bunx", ...bunxArgs] : bunxArgs; const build = spawn(buildCmd, buildArgs, { cwd, env, stdio: "inherit", }); build.on("error", (err) => { logRunner(`Failed to spawn ${buildCmd}: ${err.message}`); process.exit(1); }); build.on("exit", (code, signal) => { if (signal || (code !== 0 && code !== null)) { logRunner("Rebuild failed, restarting anyway."); } else { writeBuildStamp(); } runNode(); }); } else { runNode(); } return; } process.exit(exitCode ?? 1); }); }; const writeBuildStamp = () => { try { fs.mkdirSync(distRoot, { recursive: true }); fs.writeFileSync(buildStampPath, `${Date.now()}\n`); } catch (error) { // Best-effort stamp; still allow the runner to start. logRunner( `Failed to write build stamp: ${error?.message ?? "unknown error"}`, ); } }; if (!shouldBuild()) { runNode(); } else { logRunner("Building TypeScript (dist is stale)."); const bunxArgs = [compiler]; const buildCmd = process.platform === "win32" ? "cmd.exe" : "bunx"; const buildArgs = process.platform === "win32" ? ["/d", "/s", "/c", "bunx", ...bunxArgs] : bunxArgs; const build = spawn(buildCmd, buildArgs, { cwd, env, stdio: "inherit", }); build.on("error", (err) => { logRunner(`Failed to spawn ${buildCmd}: ${err.message}`); process.exit(1); }); build.on("exit", (code, signal) => { if (signal) { process.exit(1); } if (code !== 0 && code !== null) { process.exit(code); } writeBuildStamp(); runNode(); }); }