| #!/usr/bin/env node |
| import { spawn } from "node:child_process"; |
| import fs from "node:fs"; |
| import path from "node:path"; |
| import process from "node:process"; |
|
|
| const args = process.argv.slice(2); |
| const env = { ...process.env }; |
| const cwd = process.cwd(); |
| const compilerOverride = env.OPENCLAW_TS_COMPILER ?? env.CLAWDBOT_TS_COMPILER; |
| const compiler = compilerOverride === "tsc" ? "tsc" : "tsgo"; |
| const projectArgs = ["--project", "tsconfig.json"]; |
|
|
| 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-helpers.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.OPENCLAW_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.OPENCLAW_RUNNER_LOG === "0") return; |
| process.stderr.write(`[openclaw] ${message}\n`); |
| }; |
|
|
| const runNode = () => { |
| const nodeProcess = spawn(process.execPath, ["openclaw.mjs", ...args], { |
| cwd, |
| env, |
| stdio: "inherit", |
| }); |
|
|
| nodeProcess.on("exit", (exitCode, exitSignal) => { |
| if (exitSignal) { |
| process.exit(1); |
| } |
| process.exit(exitCode ?? 1); |
| }); |
| }; |
|
|
| const writeBuildStamp = () => { |
| try { |
| fs.mkdirSync(distRoot, { recursive: true }); |
| fs.writeFileSync(buildStampPath, `${Date.now()}\n`); |
| } catch (error) { |
| |
| logRunner(`Failed to write build stamp: ${error?.message ?? "unknown error"}`); |
| } |
| }; |
|
|
| if (!shouldBuild()) { |
| runNode(); |
| } else { |
| logRunner("Building TypeScript (dist is stale)."); |
| const pnpmArgs = ["exec", compiler, ...projectArgs]; |
| const buildCmd = process.platform === "win32" ? "cmd.exe" : "pnpm"; |
| const buildArgs = |
| process.platform === "win32" ? ["/d", "/s", "/c", "pnpm", ...pnpmArgs] : pnpmArgs; |
| const build = spawn(buildCmd, buildArgs, { |
| cwd, |
| env, |
| stdio: "inherit", |
| }); |
|
|
| build.on("exit", (code, signal) => { |
| if (signal) { |
| process.exit(1); |
| } |
| if (code !== 0 && code !== null) { |
| process.exit(code); |
| } |
| writeBuildStamp(); |
| runNode(); |
| }); |
| } |
|
|