singularity-forge/scripts/ensure-source-resources.cjs
Mikael Hugo c046ff9a6c fix: auto-rebuild workspace packages when 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 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>
2026-05-17 17:19:25 +02:00

160 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 510 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();