CLI: skip runner rebuilds when dist is fresh (#1231)

Co-authored-by: mukhtharcm <mukhtharcm@users.noreply.github.com>
This commit is contained in:
Shadow
2026-01-19 13:12:33 -06:00
parent 754494d1a0
commit 39dfdccf6c
3 changed files with 115 additions and 15 deletions

View File

@@ -17,6 +17,7 @@ Docs: https://docs.clawd.bot
### Fixes
- Tests: stabilize Windows gateway/CLI tests by skipping sidecars, normalizing argv, and extending timeouts.
- CLI: skip runner rebuilds when dist is fresh. (#1231) — thanks @mukhtharcm, @thewilloftheshadow.
## 2026.1.19-1

View File

@@ -1,27 +1,89 @@
#!/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 build = spawn("pnpm", ["exec", "tsc", "-p", "tsconfig.json"], {
cwd,
env,
stdio: "inherit",
});
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")];
build.on("exit", (code, signal) => {
if (signal) {
process.exit(1);
return;
const statMtime = (filePath) => {
try {
return fs.statSync(filePath).mtimeMs;
} catch {
return null;
}
if (code !== 0 && code !== null) {
process.exit(code);
return;
};
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.CLAWDBOT_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.CLAWDBOT_RUNNER_LOG === "0") return;
process.stderr.write(`[clawdbot] ${message}\n`);
};
const runNode = () => {
const nodeProcess = spawn(process.execPath, ["dist/entry.js", ...args], {
cwd,
env,
@@ -35,4 +97,39 @@ build.on("exit", (code, signal) => {
}
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()) {
logRunner("Skipping build; dist is fresh.");
runNode();
} else {
logRunner("Building TypeScript (dist is stale).");
const build = spawn("pnpm", ["exec", "tsc", "-p", "tsconfig.json"], {
cwd,
env,
stdio: "inherit",
});
build.on("exit", (code, signal) => {
if (signal) {
process.exit(1);
return;
}
if (code !== 0 && code !== null) {
process.exit(code);
return;
}
writeBuildStamp();
runNode();
});
}

View File

@@ -102,8 +102,10 @@ const normalizePluginsConfig = (config?: ClawdbotConfig["plugins"]): NormalizedP
const resolvePluginSdkAlias = (): string | null => {
try {
const preferDist = process.env.VITEST || process.env.NODE_ENV === "test";
let cursor = path.dirname(fileURLToPath(import.meta.url));
const modulePath = fileURLToPath(import.meta.url);
const isDistRuntime = modulePath.split(path.sep).includes("dist");
const preferDist = process.env.VITEST || process.env.NODE_ENV === "test" || isDistRuntime;
let cursor = path.dirname(modulePath);
for (let i = 0; i < 6; i += 1) {
const srcCandidate = path.join(cursor, "src", "plugin-sdk", "index.ts");
const distCandidate = path.join(cursor, "dist", "plugin-sdk", "index.js");