2026-04-30 09:35:59 +02:00
|
|
|
|
#!/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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 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();
|