docs: document runner scripts

This commit is contained in:
Peter Steinberger
2026-06-04 23:52:04 -04:00
parent 13078d24ab
commit ff83d4d164
12 changed files with 226 additions and 21 deletions

View File

@@ -1,3 +1,4 @@
// Runs oxlint over extension channel test roots through the shared extension lint runner.
import { extensionChannelTestRoots } from "../test/vitest/vitest.channel-paths.mjs";
import { runExtensionOxlint } from "./lib/run-extension-oxlint.mjs";

View File

@@ -1,3 +1,4 @@
// Defines source/config paths that pnpm dev watches for rebuilds and restarts.
import path from "node:path";
import {
BUNDLED_PLUGIN_PATH_PREFIX,
@@ -21,13 +22,17 @@ const RUN_NODE_PACKAGE_SOURCE_ROOTS = [
"packages/net-policy/src",
];
/** Source roots whose changes require the root dev build pipeline. */
export const runNodeSourceRoots = [
"src",
...RUN_NODE_PACKAGE_SOURCE_ROOTS,
BUNDLED_PLUGIN_ROOT_DIR,
];
/** Root config files whose changes invalidate the dev build. */
export const runNodeConfigFiles = ["tsconfig.json", "package.json", "tsdown.config.ts"];
/** Combined watch list used by the run-node wrapper. */
export const runNodeWatchedPaths = [...runNodeSourceRoots, ...runNodeConfigFiles];
/** Plugin metadata files that require a runtime restart even without source edits. */
export const extensionRestartMetadataFiles = new Set(["openclaw.plugin.json", "package.json"]);
const ignoredRunNodeRepoPathPatterns = [
@@ -36,6 +41,7 @@ const ignoredRunNodeRepoPathPatterns = [
];
const extensionSourceFilePattern = /\.(?:[cm]?[jt]sx?)$/;
/** Normalizes watch paths to repository-style POSIX separators. */
export const normalizeRunNodePath = (filePath) => String(filePath ?? "").replaceAll("\\", "/");
const isIgnoredSourcePath = (relativePath) => {
@@ -82,8 +88,10 @@ const isRelevantRunNodePath = (repoPath, isRelevantBundledPluginPath) => {
return false;
};
/** Returns true when a repo path should trigger a dev rebuild. */
export const isBuildRelevantRunNodePath = (repoPath) =>
isRelevantRunNodePath(repoPath, isBuildRelevantSourcePath);
/** Returns true when a repo path should restart the running dev process. */
export const isRestartRelevantRunNodePath = (repoPath) =>
isRelevantRunNodePath(repoPath, isRestartRelevantExtensionPath);

View File

@@ -1,4 +1,6 @@
#!/usr/bin/env node
// Development runner that rebuilds OpenClaw, runs runtime postbuild steps, and
// restarts the CLI when watched source or metadata changes.
import { spawn, spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
@@ -498,6 +500,7 @@ const listRequiredCoreRuntimePostBuildOutputs = (deps) =>
path.join(deps.cwd, normalizePath(relativePath)),
);
/** Lists runtime postbuild outputs that must exist before the dev CLI starts. */
export const listRequiredRuntimePostBuildOutputs = (deps) => {
const builtPluginEntries = listBuiltBundledPluginEntries(deps);
return [
@@ -514,6 +517,7 @@ const hasMissingRequiredRuntimePostBuildOutput = (deps) =>
(filePath) => statMtime(filePath, deps.fs) == null,
);
/** Decides whether source changes require a new dev build. */
export const resolveBuildRequirement = (deps) => {
if (deps.env.OPENCLAW_FORCE_BUILD === "1") {
return { shouldBuild: true, reason: "force_build" };
@@ -571,6 +575,7 @@ export const resolveBuildRequirement = (deps) => {
return { shouldBuild: false, reason: "clean" };
};
/** Decides whether runtime postbuild artifacts need to be regenerated. */
export const resolveRuntimePostBuildRequirement = (deps) => {
if (deps.env.OPENCLAW_FORCE_RUNTIME_POSTBUILD === "1") {
return { shouldSync: true, reason: "force_runtime_postbuild" };
@@ -1142,6 +1147,7 @@ const removeStaleBuildLock = (deps, lockDir, staleMs) => {
}
};
/** Acquires the dev-build lock used to serialize local rebuilds. */
export const acquireRunNodeBuildLock = async (deps) => {
const lockRoot = path.join(deps.cwd, ".artifacts");
const lockDir = path.join(lockRoot, "run-node-build.lock");
@@ -1385,6 +1391,9 @@ const runQaCoverageReportFromSource = async (deps) => {
return res.exitCode ?? 1;
};
/**
* Runs the dev build/watch loop and keeps the child CLI in sync with changes.
*/
export async function runNodeMain(params = {}) {
const deps = {
spawn: params.spawn ?? spawn,

View File

@@ -1,3 +1,4 @@
// Splits oxlint into resource-aware shards with heartbeat and timeout handling.
import { spawn, spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
@@ -39,6 +40,9 @@ const SCRIPTS_SHARD = {
args: ["--tsconfig", "config/tsconfig/oxlint.scripts.json", "scripts"],
};
/**
* Builds the platform-specific oxlint shard list.
*/
export function createOxlintShards({
cwd = process.cwd(),
env = process.env,
@@ -53,6 +57,9 @@ export function createOxlintShards({
return [...coreShards, ...extensionShards, SCRIPTS_SHARD];
}
/**
* Splits core oxlint targets into smaller source/package/UI shards.
*/
export function createCoreOxlintShards({ cwd = process.cwd(), readDir = fs.readdirSync } = {}) {
const sourceShards = listSourceRootTargetGroups({ cwd, readDir }).map((targets) => ({
name: targets.length === 1 ? `core:${targets[0].replaceAll("/", ":")}` : "core:src:root",
@@ -70,6 +77,9 @@ function createCoreShard(target) {
};
}
/**
* Chunks extension lint targets to avoid Windows command-line and memory limits.
*/
export function createWindowsExtensionShards({
cwd = process.cwd(),
env = process.env,
@@ -101,6 +111,9 @@ export function createWindowsExtensionShards({
return shards;
}
/**
* Reads the Windows extension shard chunk size.
*/
export function resolveWindowsExtensionChunkSize(env = process.env) {
return resolvePositiveEnvIntWithFallback(
env,
@@ -109,6 +122,9 @@ export function resolveWindowsExtensionChunkSize(env = process.env) {
);
}
/**
* Chooses serial shard execution for constrained hosts or Windows.
*/
export function shouldRunOxlintShardsSerial({
env = process.env,
platform = process.platform,
@@ -148,8 +164,7 @@ export function shouldRunOxlintShardsSerial({
function isRemoteChangedGateEnv(env) {
return (
env.OPENCLAW_CHECK_CHANGED_REMOTE_CHILD === "1" ||
env.OPENCLAW_CHANGED_LANES_RAW_SYNC === "1"
env.OPENCLAW_CHECK_CHANGED_REMOTE_CHILD === "1" || env.OPENCLAW_CHANGED_LANES_RAW_SYNC === "1"
);
}
@@ -199,6 +214,9 @@ function listSourceRootTargetGroups({ cwd, readDir }) {
return [...dirs.map((target) => [target]), ...(rootFiles.length > 0 ? [rootFiles] : [])];
}
/**
* Runs selected oxlint shards and returns process-style success/failure.
*/
export async function main(extraArgs = process.argv.slice(2), runtimeEnv = process.env) {
const runner = path.resolve("scripts", "run-oxlint.mjs");
const shardArgs = parseShardRunnerArgs(extraArgs);
@@ -252,20 +270,21 @@ export async function main(extraArgs = process.argv.slice(2), runtimeEnv = proce
platform: process.platform,
splitCore: shardArgs.splitCore,
});
const results = shardConcurrency <= 1
? await runShardsSerial({
entries: selectedShards,
env,
extraArgs: shardArgs.oxlintArgs,
runner,
})
: await runShardsParallel({
concurrency: Math.min(shardConcurrency, selectedShards.length),
entries: selectedShards,
env,
extraArgs: shardArgs.oxlintArgs,
runner,
});
const results =
shardConcurrency <= 1
? await runShardsSerial({
entries: selectedShards,
env,
extraArgs: shardArgs.oxlintArgs,
runner,
})
: await runShardsParallel({
concurrency: Math.min(shardConcurrency, selectedShards.length),
entries: selectedShards,
env,
extraArgs: shardArgs.oxlintArgs,
runner,
});
process.exitCode = results.find((status) => status !== 0) ?? 0;
}
} finally {
@@ -289,6 +308,9 @@ function resolveHostResources(hostResources) {
};
}
/**
* Parses shard-runner flags separately from forwarded oxlint args.
*/
export function parseShardRunnerArgs(args) {
const only = new Set();
const oxlintArgs = [];
@@ -321,6 +343,9 @@ export function parseShardRunnerArgs(args) {
return { only, oxlintArgs, splitCore };
}
/**
* Filters shards by an optional comma-separated shard name list.
*/
export function filterOxlintShards(shards, only) {
if (only.size === 0) {
return shards;
@@ -329,6 +354,9 @@ export function filterOxlintShards(shards, only) {
return shards.filter((shard) => only.has(shard.name) || only.has(shard.name.split(":")[0]));
}
/**
* Resolves shard concurrency from env, platform, and host resources.
*/
export function resolveOxlintShardConcurrency({
env = process.env,
platform = process.platform,
@@ -390,6 +418,9 @@ async function runShardsParallel({ concurrency, entries, env, extraArgs, runner
return results.filter((status) => status !== undefined);
}
/**
* Runs one oxlint shard with bounded output, heartbeat, and forced cleanup.
*/
export async function runShard({ env, extraArgs, runner, shard }) {
console.error(`[oxlint:${shard.name}] starting`);
const startedAt = Date.now();
@@ -475,6 +506,9 @@ export async function runShard({ env, extraArgs, runner, shard }) {
});
}
/**
* Reads the shard heartbeat interval.
*/
export function resolveShardHeartbeatMs(env) {
return resolveNonNegativeEnvInt(
env,
@@ -483,6 +517,9 @@ export function resolveShardHeartbeatMs(env) {
);
}
/**
* Reads the per-shard timeout.
*/
export function resolveShardTimeoutMs(env) {
return resolveNonNegativeEnvInt(
env,
@@ -491,6 +528,9 @@ export function resolveShardTimeoutMs(env) {
);
}
/**
* Reads the graceful shutdown window before SIGKILL.
*/
export function resolveShardKillGraceMs(env) {
return resolveNonNegativeEnvInt(
env,

View File

@@ -1,3 +1,5 @@
// Runs oxlint with local heavy-check policy, sparse-checkout filtering, and
// plugin package-boundary artifact preparation when needed.
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
@@ -7,10 +9,7 @@ import {
resolveLocalHeavyCheckEnv,
shouldAcquireLocalHeavyCheckLockForOxlint,
} from "./lib/local-heavy-check-runtime.mjs";
import {
createManagedCommandInvocation,
runManagedCommand,
} from "./lib/managed-child-process.mjs";
import { createManagedCommandInvocation, runManagedCommand } from "./lib/managed-child-process.mjs";
const oxlintPath = path.resolve("node_modules", ".bin", "oxlint");
const PREPARE_EXTENSION_BOUNDARY_ARGS = [
@@ -41,10 +40,16 @@ const OXLINT_VALUE_FLAGS = new Set([
"--warn",
]);
/**
* Returns whether oxlint args need package-boundary declaration artifacts first.
*/
export function shouldPrepareExtensionPackageBoundaryArtifacts(args) {
return !args.some((arg) => OXLINT_PREPARE_SKIP_FLAGS.has(arg));
}
/**
* Drops tracked-but-missing sparse-checkout targets so narrow sparse checks can pass.
*/
export function filterSparseMissingOxlintTargets(
args,
{
@@ -197,6 +202,9 @@ async function prepareExtensionPackageBoundaryArtifacts(env) {
}
}
/**
* Applies wrapper policy and runs oxlint with the final argument list.
*/
export async function main(argv = process.argv.slice(2), runtimeEnv = process.env) {
const { args: policyArgs, env } = applyLocalOxlintPolicy(
argv,

View File

@@ -1,3 +1,4 @@
// Runs tsgo through local heavy-check policy and sparse-checkout guards.
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
@@ -8,11 +9,11 @@ import {
resolveLocalHeavyCheckEnv,
shouldAcquireLocalHeavyCheckLockForTsgo,
} from "./lib/local-heavy-check-runtime.mjs";
import { createManagedCommandInvocation } from "./lib/managed-child-process.mjs";
import {
getSparseTsgoGuardError,
shouldSkipSparseTsgoGuardError,
} from "./lib/tsgo-sparse-guard.mjs";
import { createManagedCommandInvocation } from "./lib/managed-child-process.mjs";
const { args: finalArgs, env } = applyLocalTsgoPolicy(
process.argv.slice(2),

View File

@@ -1,3 +1,4 @@
// Profiles Vitest main or runner processes and writes CPU/heap artifacts.
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
@@ -6,6 +7,9 @@ import { pathToFileURL } from "node:url";
import { formatErrorMessage } from "./lib/error-format.mjs";
import { createPnpmRunnerSpawnSpec } from "./pnpm-runner.mjs";
/**
* Parses Vitest profiler mode, output directory, and forwarded Vitest args.
*/
export function parseArgs(argv) {
const args = {
mode: "",
@@ -44,6 +48,9 @@ export function parseArgs(argv) {
return args;
}
/**
* Resolves or creates the directory used for profiler artifacts.
*/
export function resolveVitestProfileDir({ mode, outputDir }) {
if (outputDir && outputDir.trim()) {
return path.resolve(outputDir);
@@ -52,10 +59,16 @@ export function resolveVitestProfileDir({ mode, outputDir }) {
return fs.mkdtempSync(path.join(os.tmpdir(), `openclaw-vitest-${mode}-profile-`));
}
/**
* Builds a profiler command without additional Vitest args.
*/
export function buildVitestProfileCommand({ mode, outputDir }) {
return buildVitestProfileCommandWithArgs({ mode, outputDir, vitestArgs: [] });
}
/**
* Builds the profiler command for either Vitest main or worker-runner profiling.
*/
export function buildVitestProfileCommandWithArgs({ mode, outputDir, vitestArgs }) {
if (mode === "main") {
return {
@@ -90,6 +103,9 @@ export function buildVitestProfileCommandWithArgs({ mode, outputDir, vitestArgs
};
}
/**
* Converts a profiler plan into a spawn spec, routing pnpm through the wrapper.
*/
export function buildVitestProfileSpawnSpec(plan, runnerOptions = {}) {
if (plan.command === "pnpm") {
return createPnpmRunnerSpawnSpec({

View File

@@ -1,3 +1,5 @@
// Runs Vitest through repo project selection, local scheduling policy, output
// watchdogs, and process-group cleanup.
import { spawn } from "node:child_process";
import fs from "node:fs";
import { createRequire } from "node:module";
@@ -17,8 +19,11 @@ const TRUTHY_ENV_VALUES = new Set(["1", "true", "yes", "on"]);
const ANSI_CSI_PREFIX = `${String.fromCharCode(27)}[`;
const ANSI_CSI_SUFFIX_RE = /^[0-?]*[ -/]*[@-~]/u;
const SUPPRESSED_VITEST_STDERR_PATTERNS = ["[PLUGIN_TIMINGS]"];
/** Default watchdog timeout for Vitest runs that stop producing output. */
export const DEFAULT_VITEST_NO_OUTPUT_TIMEOUT_MS = 120_000;
/** Default heartbeat interval while waiting on silent Vitest output. */
export const DEFAULT_VITEST_NO_OUTPUT_HEARTBEAT_MS = 60_000;
/** Longer watchdog timeout for known long-running Vitest configs. */
export const DEFAULT_LONG_RUNNING_VITEST_NO_OUTPUT_TIMEOUT_MS = 300_000;
const VITEST_NO_OUTPUT_TIMEOUT_ENV_KEY = "OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS";
const VITEST_NO_OUTPUT_HEARTBEAT_ENV_KEY = "OPENCLAW_VITEST_NO_OUTPUT_HEARTBEAT_MS";
@@ -107,6 +112,9 @@ function parsePositiveInt(value) {
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
}
/**
* Resolves default Node flags for Vitest, including the local Maglev opt-in.
*/
export function resolveVitestNodeArgs(env = process.env) {
if (isTruthyEnvValue(env.OPENCLAW_VITEST_ENABLE_MAGLEV)) {
return [];
@@ -123,6 +131,9 @@ function isMissingVitestResolveError(error) {
);
}
/**
* Builds the actionable dependency-install message when Vitest is unavailable.
*/
export function resolveMissingVitestDependencyMessage(baseDir = repoRoot, fsImpl = fs) {
const hasNodeModules = fsImpl.existsSync(path.join(baseDir, "node_modules"));
const reason = hasNodeModules
@@ -199,6 +210,9 @@ function resolveHydratedVitestCliEntry({ baseDir, env, fsImpl, platform }) {
return path.join(nodeModulesPath, "vitest", "vitest.mjs");
}
/**
* Resolves the Vitest CLI entry from normal or hydrated node_modules layouts.
*/
export function resolveVitestCliEntry({
baseDir = repoRoot,
env = process.env,
@@ -230,10 +244,16 @@ export function resolveVitestCliEntry({
return path.join(path.dirname(vitestPackageJson), "vitest.mjs");
}
/**
* Reads the explicit no-output watchdog timeout, if configured.
*/
export function resolveVitestNoOutputTimeoutMs(env = process.env) {
return parsePositiveInt(env[VITEST_NO_OUTPUT_TIMEOUT_ENV_KEY]);
}
/**
* Reads the explicit no-output heartbeat interval, if configured.
*/
export function resolveVitestNoOutputHeartbeatMs(env = process.env) {
return parsePositiveInt(env[VITEST_NO_OUTPUT_HEARTBEAT_ENV_KEY]);
}
@@ -309,6 +329,9 @@ function resolveExplicitVitestMode(argv) {
return mode;
}
/**
* Adds default watchdog env for non-watch Vitest runs.
*/
export function resolveRunVitestSpawnEnv(env = process.env, argv = []) {
const explicitMode = resolveExplicitVitestMode(argv);
if (explicitMode === "watch") {
@@ -332,6 +355,9 @@ export function resolveRunVitestSpawnEnv(env = process.env, argv = []) {
};
}
/**
* Chooses the default watchdog timeout from the selected Vitest config.
*/
export function resolveDefaultVitestNoOutputTimeoutMs(argv = []) {
const config = resolveVitestConfigArg(argv);
if (config !== null && isLongRunningVitestConfig(config)) {
@@ -366,6 +392,9 @@ function isLongRunningVitestConfig(config) {
return false;
}
/**
* Builds spawn options for the primary Vitest child process.
*/
export function resolveVitestSpawnParams(env = process.env, platform = process.platform) {
return {
env: resolveVitestSpawnEnv(env),
@@ -374,6 +403,9 @@ export function resolveVitestSpawnParams(env = process.env, platform = process.p
};
}
/**
* Applies local Vitest scheduling and native worker budget env.
*/
export function resolveVitestSpawnEnv(env = process.env) {
const nextEnv = resolveLocalVitestEnv(env);
if (!shouldApplyNativeWorkerBudget(nextEnv)) {
@@ -405,6 +437,9 @@ function resolveExplicitVitestWorkerBudget(env) {
return parsePositiveInt(env.OPENCLAW_VITEST_MAX_WORKERS ?? env.OPENCLAW_TEST_WORKERS);
}
/**
* Filters known noisy Vitest stderr lines after stripping ANSI escapes.
*/
export function shouldSuppressVitestStderrLine(line) {
const normalizedLine = line
.split(ANSI_CSI_PREFIX)
@@ -413,6 +448,9 @@ export function shouldSuppressVitestStderrLine(line) {
return SUPPRESSED_VITEST_STDERR_PATTERNS.some((pattern) => normalizedLine.includes(pattern));
}
/**
* Detects pnpm exec node invocations so the wrapper can spawn Node directly.
*/
export function resolveDirectNodeVitestArgs(pnpmArgs) {
return pnpmArgs[0] === "exec" && pnpmArgs[1] === "node" ? pnpmArgs.slice(2) : null;
}
@@ -473,6 +511,9 @@ function collectExplicitTestFileArgs(argv) {
return collectExplicitFileTargetArgs(argv, isExplicitTestFileArg);
}
/**
* Forces explicit test-file targets to fail when Vitest finds no matching tests.
*/
export function resolveExplicitTestFileNoPassArgs(argv) {
if (collectExplicitTestFileArgs(argv).length === 0) {
return argv;
@@ -584,6 +625,9 @@ function hasNonRunVitestSubcommand(argv) {
return false;
}
/**
* Delegates default or explicit-file runs to the repo test-projects runner.
*/
export function resolveTestProjectsDelegationArgs(argv) {
if (
hasExplicitVitestConfigArg(argv) ||
@@ -600,6 +644,9 @@ export function resolveTestProjectsDelegationArgs(argv) {
return stripRunSubcommand(argv);
}
/**
* Lists explicit test file targets missing from the current checkout.
*/
export function resolveMissingExplicitTestFiles(argv, cwd = process.cwd(), fsImpl = fs) {
if (hasExplicitVitestConfigArg(argv) || hasAlternateVitestRootArg(argv)) {
return [];
@@ -630,6 +677,9 @@ function isToolingTestTarget(target) {
);
}
/**
* Resolves config defaults and explicit-file handling for wrapper-inferred runs.
*/
export function resolveImplicitVitestArgs(argv, cwd = process.cwd()) {
if (hasExplicitVitestConfigArg(argv)) {
return argv;
@@ -663,6 +713,9 @@ function spawnVitestProcess({ pnpmArgs, spawnParams }) {
});
}
/**
* Installs the no-output watchdog for long-running Vitest children.
*/
export function installVitestNoOutputWatchdog(params) {
const timeoutMs = params.timeoutMs;
if (!timeoutMs || timeoutMs <= 0) {
@@ -784,6 +837,9 @@ export function installVitestNoOutputWatchdog(params) {
};
}
/**
* Forwards child output while optionally suppressing complete stderr lines.
*/
export function forwardVitestOutput(stream, target, shouldSuppressLine = () => false) {
if (!stream) {
return;
@@ -812,6 +868,9 @@ export function forwardVitestOutput(stream, target, shouldSuppressLine = () => f
});
}
/**
* Spawns Vitest with output forwarding, watchdogs, and process-group cleanup.
*/
export function spawnWatchedVitestProcess({
pnpmArgs,
spawnParams,
@@ -860,10 +919,16 @@ export function spawnWatchedVitestProcess({
};
}
/**
* Builds env for the delegated test-projects runner.
*/
export function resolveTestProjectsRunnerEnv(env) {
return resolveVitestSpawnEnv(env);
}
/**
* Builds spawn options for the delegated test-projects runner.
*/
export function resolveTestProjectsRunnerSpawnParams(env, platform = process.platform) {
return {
env: resolveTestProjectsRunnerEnv(env),

View File

@@ -1,8 +1,12 @@
// Runs a command with inline KEY=value assignments while preserving signal behavior.
import { spawn } from "node:child_process";
const ENV_ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*=/u;
const USAGE = "Usage: node scripts/run-with-env.mjs KEY=value [KEY=value ...] -- command [args...]";
/**
* Detects help requests before the command separator.
*/
export function isRunWithEnvHelpRequest(argv) {
for (const arg of argv) {
if (arg === "--") {
@@ -15,6 +19,9 @@ export function isRunWithEnvHelpRequest(argv) {
return false;
}
/**
* Parses KEY=value assignments and the command following --.
*/
export function parseRunWithEnvArgs(argv) {
const separatorIndex = argv.indexOf("--");
if (separatorIndex <= 0 || separatorIndex === argv.length - 1) {
@@ -38,6 +45,9 @@ export function parseRunWithEnvArgs(argv) {
};
}
/**
* Resolves node to the current executable so wrapper and child use the same runtime.
*/
export function resolveSpawnCommand(command, args, execPath = process.execPath) {
if (command === "node") {
return {

View File

@@ -1,6 +1,10 @@
// Shared filesystem helpers for runtime postbuild scripts.
import fs from "node:fs";
import { dirname } from "node:path";
/**
* Writes text only when contents changed and returns whether a write happened.
*/
export function writeTextFileIfChanged(filePath, contents) {
const next = String(contents);
try {
@@ -16,6 +20,9 @@ export function writeTextFileIfChanged(filePath, contents) {
return true;
}
/**
* Removes one file if present, treating missing paths as success.
*/
export function removeFileIfExists(filePath) {
try {
fs.rmSync(filePath, { force: true });
@@ -25,6 +32,9 @@ export function removeFileIfExists(filePath) {
}
}
/**
* Removes a file or directory tree if present.
*/
export function removePathIfExists(filePath) {
try {
fs.rmSync(filePath, { recursive: true, force: true });

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env node
// Writes the runtime postbuild stamp after generated runtime artifacts are current.
import process from "node:process";
import { pathToFileURL } from "node:url";
import { writeRuntimePostBuildStamp } from "./lib/local-build-metadata.mjs";

View File

@@ -1,3 +1,5 @@
// Generates postbuild runtime artifacts: plugin metadata, SDK aliases, stable
// runtime aliases, static assets, and compatibility chunks for live upgrades.
import fs from "node:fs";
import path from "node:path";
import { performance } from "node:perf_hooks";
@@ -126,6 +128,7 @@ const LEGACY_PLUGIN_INSTALL_RUNTIME_COMPAT_ALIASES = [
aliasFileName: PLUGIN_INSTALL_RUNTIME_ALIAS.aliasFileName,
sourceIncludes: LEGACY_PLUGIN_INSTALL_RUNTIME_MARKERS,
}));
/** Compatibility chunks kept for live gateways loading old CLI exit modules. */
export const LEGACY_CLI_EXIT_COMPAT_CHUNKS = [
{
dest: "dist/memory-state-CcqRgDZU.js",
@@ -137,10 +140,16 @@ export const LEGACY_CLI_EXIT_COMPAT_CHUNKS = [
},
];
/**
* Lists generated plugin SDK root-alias outputs.
*/
export function listPluginSdkRootAliasOutputs() {
return [PLUGIN_SDK_ROOT_ALIAS_OUTPUT];
}
/**
* Lists generated official channel catalog outputs.
*/
export function listOfficialChannelCatalogOutputs() {
return [OFFICIAL_CHANNEL_CATALOG_OUTPUT];
}
@@ -214,6 +223,9 @@ function resolveStableRootRuntimeAliasCandidate(params) {
return wrappers.length === 1 ? wrappers[0].candidate : null;
}
/**
* Lists stable aliases for hashed root runtime/contract chunks.
*/
export function listStableRootRuntimeAliasOutputs(params = {}) {
const rootDir = params.rootDir ?? ROOT;
const distDir = path.join(rootDir, "dist");
@@ -231,6 +243,9 @@ export function listStableRootRuntimeAliasOutputs(params = {}) {
.toSorted((left, right) => left.localeCompare(right));
}
/**
* Lists compatibility chunk outputs required for old CLI exit paths.
*/
export function listLegacyCliExitCompatOutputs(params = {}) {
const chunks = params.chunks ?? LEGACY_CLI_EXIT_COMPAT_CHUNKS;
return chunks
@@ -238,6 +253,9 @@ export function listLegacyCliExitCompatOutputs(params = {}) {
.toSorted((left, right) => left.localeCompare(right));
}
/**
* Lists legacy hashed runtime aliases that may be needed during live upgrades.
*/
export function listLegacyRootRuntimeCompatOutputs(params = {}) {
const rootDir = params.rootDir ?? ROOT;
const distDir = path.join(rootDir, "dist");
@@ -262,6 +280,9 @@ export function listLegacyRootRuntimeCompatOutputs(params = {}) {
.toSorted((left, right) => left.localeCompare(right));
}
/**
* Lists all core runtime postbuild outputs expected after a build.
*/
export function listCoreRuntimePostBuildOutputs(params = {}) {
return [
...listPluginSdkRootAliasOutputs(),
@@ -272,6 +293,9 @@ export function listCoreRuntimePostBuildOutputs(params = {}) {
].toSorted((left, right) => left.localeCompare(right));
}
/**
* Writes stable aliases for current hashed runtime chunks.
*/
export function writeStableRootRuntimeAliases(params = {}) {
const rootDir = params.rootDir ?? ROOT;
const distDir = path.join(rootDir, "dist");
@@ -294,6 +318,9 @@ export function writeStableRootRuntimeAliases(params = {}) {
}
}
/**
* Rewrites hashed runtime imports to stable aliases so live updates survive swaps.
*/
export function rewriteRootRuntimeImportsToStableAliases(params = {}) {
const rootDir = params.rootDir ?? ROOT;
const distDir = path.join(rootDir, "dist");
@@ -415,6 +442,9 @@ function resolveLegacyRootRuntimeCompatTarget(params) {
});
}
/**
* Writes compatibility aliases for shipped hashed runtime chunk names.
*/
export function writeLegacyRootRuntimeCompatAliases(params = {}) {
const rootDir = params.rootDir ?? ROOT;
const distDir = path.join(rootDir, "dist");
@@ -445,6 +475,9 @@ export function writeLegacyRootRuntimeCompatAliases(params = {}) {
}
}
/**
* Writes small compatibility chunks for old CLI exit imports.
*/
export function writeLegacyCliExitCompatChunks(params = {}) {
const rootDir = params.rootDir ?? ROOT;
const chunks = params.chunks ?? LEGACY_CLI_EXIT_COMPAT_CHUNKS;
@@ -458,6 +491,9 @@ function shouldCopyStaticExtensionAssets(params) {
return env.OPENCLAW_RUNTIME_POSTBUILD_STATIC_ASSETS !== "0";
}
/**
* Runs every runtime postbuild phase after the main dist build.
*/
export function runRuntimePostBuild(params = {}) {
const timingsEnabled = params.timings ?? process.env.OPENCLAW_RUNTIME_POSTBUILD_TIMINGS !== "0";
const runPhase = (label, action) => {