mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-18 08:07:32 +00:00
238 lines
5.5 KiB
JavaScript
238 lines
5.5 KiB
JavaScript
#!/usr/bin/env node
|
|
import { spawn, spawnSync } 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 compiler = "tsdown";
|
|
const compilerArgs = ["exec", compiler, "--no-clean"];
|
|
|
|
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 gitWatchedPaths = ["src", "tsconfig.json", "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 runGit = (args) => {
|
|
try {
|
|
const result = spawnSync("git", args, {
|
|
cwd,
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "ignore"],
|
|
});
|
|
if (result.status !== 0) {
|
|
return null;
|
|
}
|
|
return (result.stdout ?? "").trim();
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const resolveGitHead = () => {
|
|
const head = runGit(["rev-parse", "HEAD"]);
|
|
return head || null;
|
|
};
|
|
|
|
const hasDirtySourceTree = () => {
|
|
const output = runGit([
|
|
"status",
|
|
"--porcelain",
|
|
"--untracked-files=normal",
|
|
"--",
|
|
...gitWatchedPaths,
|
|
]);
|
|
if (output === null) {
|
|
return null;
|
|
}
|
|
return output.length > 0;
|
|
};
|
|
|
|
const readBuildStamp = () => {
|
|
const mtime = statMtime(buildStampPath);
|
|
if (mtime == null) {
|
|
return { mtime: null, head: null };
|
|
}
|
|
try {
|
|
const raw = fs.readFileSync(buildStampPath, "utf8").trim();
|
|
if (!raw.startsWith("{")) {
|
|
return { mtime, head: null };
|
|
}
|
|
const parsed = JSON.parse(raw);
|
|
const head = typeof parsed?.head === "string" && parsed.head.trim() ? parsed.head.trim() : null;
|
|
return { mtime, head };
|
|
} catch {
|
|
return { mtime, head: null };
|
|
}
|
|
};
|
|
|
|
const hasSourceMtimeChanged = (stampMtime) => {
|
|
const srcMtime = findLatestMtime(srcRoot, isExcludedSource);
|
|
return srcMtime != null && srcMtime > stampMtime;
|
|
};
|
|
|
|
const shouldBuild = () => {
|
|
if (env.OPENCLAW_FORCE_BUILD === "1") {
|
|
return true;
|
|
}
|
|
const stamp = readBuildStamp();
|
|
if (stamp.mtime == null) {
|
|
return true;
|
|
}
|
|
if (statMtime(distEntry) == null) {
|
|
return true;
|
|
}
|
|
|
|
for (const filePath of configFiles) {
|
|
const mtime = statMtime(filePath);
|
|
if (mtime != null && mtime > stamp.mtime) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
const currentHead = resolveGitHead();
|
|
if (currentHead && !stamp.head) {
|
|
return hasSourceMtimeChanged(stamp.mtime);
|
|
}
|
|
if (currentHead && stamp.head && currentHead !== stamp.head) {
|
|
return hasSourceMtimeChanged(stamp.mtime);
|
|
}
|
|
if (currentHead) {
|
|
const dirty = hasDirtySourceTree();
|
|
if (dirty === true) {
|
|
return true;
|
|
}
|
|
if (dirty === false) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (hasSourceMtimeChanged(stamp.mtime)) {
|
|
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 });
|
|
const stamp = {
|
|
builtAt: Date.now(),
|
|
head: resolveGitHead(),
|
|
};
|
|
fs.writeFileSync(buildStampPath, `${JSON.stringify(stamp)}\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 buildCmd = process.platform === "win32" ? "cmd.exe" : "pnpm";
|
|
const buildArgs =
|
|
process.platform === "win32" ? ["/d", "/s", "/c", "pnpm", ...compilerArgs] : compilerArgs;
|
|
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();
|
|
});
|
|
}
|