Without this, edits to packages/coding-agent/src/* (or any other workspace
package src) silently land while the dist stays stale — agents continue
loading the old compiled JS and operators see "why didn't my edit take
effect?" symptoms. Observed 2026-05-17 wiring in the AST tools: vitest
(reading TS source) passed; runtime smoke test against dist failed because
no auto-rebuild fired.
Extends ensure-source-resources.cjs (which sf-from-source runs on every
launch) to also check workspace packages: agent-core, ai, coding-agent,
daemon, google-gemini-cli-provider, openai-codex-provider, rpc-client, tui.
For each, compare latest src mtime vs latest dist mtime (with a 100ms grace
window). If src is newer, run `npm run build -w @singularity-forge/<pkg>`.
Excludes:
- packages/native (Rust build is 5–10 min; trigger manually via
`node rust-engine/scripts/build.js --dev`).
- Any package in SF_SKIP_WORKSPACE_AUTOBUILD (comma-separated).
- Whole step disabled by SF_SKIP_WORKSPACE_AUTOBUILD=all.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
160 lines
4.3 KiB
JavaScript
160 lines
4.3 KiB
JavaScript
#!/usr/bin/env node
|
||
const { spawnSync } = require("node:child_process");
|
||
const { existsSync, readdirSync, statSync } = require("node:fs");
|
||
const { join, resolve } = require("node:path");
|
||
|
||
const root = resolve(__dirname, "..");
|
||
const srcResources = join(root, "src", "resources");
|
||
const distResources = join(root, "dist", "resources");
|
||
const stampPath = join(distResources, ".sf-resource-build-stamp");
|
||
const copyResourcesScript = join(root, "scripts", "copy-resources.cjs");
|
||
|
||
function latestMtimeMs(path) {
|
||
let latest = 0;
|
||
const stack = [path];
|
||
|
||
while (stack.length > 0) {
|
||
const current = stack.pop();
|
||
if (!current) continue;
|
||
|
||
let entries;
|
||
try {
|
||
entries = readdirSync(current, { withFileTypes: true });
|
||
} catch {
|
||
continue;
|
||
}
|
||
|
||
for (const entry of entries) {
|
||
const entryPath = join(current, entry.name);
|
||
let stat;
|
||
try {
|
||
stat = statSync(entryPath);
|
||
} catch {
|
||
continue;
|
||
}
|
||
latest = Math.max(latest, stat.mtimeMs);
|
||
if (entry.isDirectory()) {
|
||
stack.push(entryPath);
|
||
}
|
||
}
|
||
}
|
||
|
||
return latest;
|
||
}
|
||
|
||
function sourceInputsMtimeMs() {
|
||
return Math.max(
|
||
latestMtimeMs(srcResources),
|
||
existsSync(copyResourcesScript) ? statSync(copyResourcesScript).mtimeMs : 0,
|
||
existsSync(join(root, "tsconfig.resources.json"))
|
||
? statSync(join(root, "tsconfig.resources.json")).mtimeMs
|
||
: 0,
|
||
);
|
||
}
|
||
|
||
function hasCompleteResourceBuild() {
|
||
return (
|
||
existsSync(stampPath) &&
|
||
existsSync(join(distResources, "SF-WORKFLOW.md")) &&
|
||
existsSync(join(distResources, "agents")) &&
|
||
existsSync(join(distResources, "extensions"))
|
||
);
|
||
}
|
||
|
||
function shouldRebuild() {
|
||
if (process.env.SF_DEV_CLI_SKIP_RESOURCE_BUILD === "1") return false;
|
||
if (process.env.SF_SKIP_SOURCE_RESOURCE_BUILD === "1") return false;
|
||
if (!hasCompleteResourceBuild()) return true;
|
||
|
||
let stampMtime = 0;
|
||
try {
|
||
stampMtime = statSync(stampPath).mtimeMs;
|
||
} catch {
|
||
return true;
|
||
}
|
||
|
||
return sourceInputsMtimeMs() > stampMtime;
|
||
}
|
||
|
||
if (shouldRebuild()) {
|
||
console.error(
|
||
"[forge] Source resources changed; rebuilding dist/resources before launch...",
|
||
);
|
||
const result = spawnSync(process.execPath, [copyResourcesScript], {
|
||
cwd: root,
|
||
stdio: "inherit",
|
||
env: process.env,
|
||
});
|
||
|
||
if (result.status !== 0) {
|
||
process.exit(result.status ?? 1);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Auto-rebuild workspace packages whose src/ is newer than dist/.
|
||
*
|
||
* Without this, edits to packages/coding-agent/src/* (or any other workspace
|
||
* package src) silently land while the dist stays stale — agents continue
|
||
* loading the old compiled JS and operators see "why didn't my edit take
|
||
* effect?" symptoms (observed 2026-05-17 with the AST tools wire-in).
|
||
*
|
||
* Skipped:
|
||
* - native: Rust build takes 5–10 min; trigger manually via
|
||
* `node rust-engine/scripts/build.js --dev` instead.
|
||
* - any package set in SF_SKIP_WORKSPACE_AUTOBUILD (comma-separated names).
|
||
*
|
||
* Override the whole step with SF_SKIP_WORKSPACE_AUTOBUILD=all.
|
||
*/
|
||
const WORKSPACE_AUTOBUILD_PACKAGES = [
|
||
"agent-core",
|
||
"ai",
|
||
"coding-agent",
|
||
"daemon",
|
||
"google-gemini-cli-provider",
|
||
"openai-codex-provider",
|
||
"rpc-client",
|
||
"tui",
|
||
];
|
||
|
||
function packageNeedsRebuild(pkgName) {
|
||
const pkgDir = join(root, "packages", pkgName);
|
||
const pkgSrc = join(pkgDir, "src");
|
||
const pkgDist = join(pkgDir, "dist");
|
||
if (!existsSync(pkgSrc) || !existsSync(pkgDist)) return false;
|
||
const srcMtime = latestMtimeMs(pkgSrc);
|
||
const distMtime = latestMtimeMs(pkgDist);
|
||
// Add a small grace window so files written within the same second as a
|
||
// dist sync don't trigger redundant rebuilds.
|
||
return srcMtime > distMtime + 100;
|
||
}
|
||
|
||
function rebuildWorkspacePackagesIfStale() {
|
||
const skip = (process.env.SF_SKIP_WORKSPACE_AUTOBUILD || "")
|
||
.split(",")
|
||
.map((s) => s.trim())
|
||
.filter(Boolean);
|
||
if (skip.includes("all")) return;
|
||
|
||
const stale = WORKSPACE_AUTOBUILD_PACKAGES.filter(
|
||
(pkg) => !skip.includes(pkg) && packageNeedsRebuild(pkg),
|
||
);
|
||
if (stale.length === 0) return;
|
||
|
||
console.error(
|
||
`[forge] Workspace src newer than dist for: ${stale.join(", ")} — rebuilding...`,
|
||
);
|
||
for (const pkg of stale) {
|
||
const result = spawnSync(
|
||
"npm",
|
||
["run", "build", "-w", `@singularity-forge/${pkg}`],
|
||
{ cwd: root, stdio: "inherit", env: process.env },
|
||
);
|
||
if (result.status !== 0) {
|
||
console.error(`[forge] FAILED to rebuild packages/${pkg}`);
|
||
process.exit(result.status ?? 1);
|
||
}
|
||
}
|
||
}
|
||
|
||
rebuildWorkspacePackagesIfStale();
|