diff --git a/CHANGELOG.md b/CHANGELOG.md index afe6d23f5..c77a0b2bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/scripts/run-node.mjs b/scripts/run-node.mjs index d833a8ac0..d8b48e334 100644 --- a/scripts/run-node.mjs +++ b/scripts/run-node.mjs @@ -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(); + }); +} diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index c7b60c0a0..dc598cf06 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -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");