From ff83d4d1641e7e817b7cb762daf72ee540d7a92a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 4 Jun 2026 23:52:04 -0400 Subject: [PATCH] docs: document runner scripts --- scripts/run-extension-channel-oxlint.mjs | 1 + scripts/run-node-watch-paths.mjs | 8 +++ scripts/run-node.mjs | 9 +++ scripts/run-oxlint-shards.mjs | 72 ++++++++++++++++++------ scripts/run-oxlint.mjs | 16 ++++-- scripts/run-tsgo.mjs | 3 +- scripts/run-vitest-profile.mjs | 16 ++++++ scripts/run-vitest.mjs | 65 +++++++++++++++++++++ scripts/run-with-env.mjs | 10 ++++ scripts/runtime-postbuild-shared.mjs | 10 ++++ scripts/runtime-postbuild-stamp.mjs | 1 + scripts/runtime-postbuild.mjs | 36 ++++++++++++ 12 files changed, 226 insertions(+), 21 deletions(-) diff --git a/scripts/run-extension-channel-oxlint.mjs b/scripts/run-extension-channel-oxlint.mjs index a874ab022d5f..4351b8de415c 100644 --- a/scripts/run-extension-channel-oxlint.mjs +++ b/scripts/run-extension-channel-oxlint.mjs @@ -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"; diff --git a/scripts/run-node-watch-paths.mjs b/scripts/run-node-watch-paths.mjs index d1364ca8790f..b40503271260 100644 --- a/scripts/run-node-watch-paths.mjs +++ b/scripts/run-node-watch-paths.mjs @@ -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); diff --git a/scripts/run-node.mjs b/scripts/run-node.mjs index d70f401a0b80..c58a9006cdcd 100644 --- a/scripts/run-node.mjs +++ b/scripts/run-node.mjs @@ -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, diff --git a/scripts/run-oxlint-shards.mjs b/scripts/run-oxlint-shards.mjs index 4c92d21dd187..a8d4b6f8b111 100644 --- a/scripts/run-oxlint-shards.mjs +++ b/scripts/run-oxlint-shards.mjs @@ -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, diff --git a/scripts/run-oxlint.mjs b/scripts/run-oxlint.mjs index ca82111e304f..3c285449ee47 100644 --- a/scripts/run-oxlint.mjs +++ b/scripts/run-oxlint.mjs @@ -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, diff --git a/scripts/run-tsgo.mjs b/scripts/run-tsgo.mjs index acbbf3a48bc6..b5fabf49f562 100644 --- a/scripts/run-tsgo.mjs +++ b/scripts/run-tsgo.mjs @@ -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), diff --git a/scripts/run-vitest-profile.mjs b/scripts/run-vitest-profile.mjs index 6ab9a45d863d..ccb64e38b2be 100644 --- a/scripts/run-vitest-profile.mjs +++ b/scripts/run-vitest-profile.mjs @@ -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({ diff --git a/scripts/run-vitest.mjs b/scripts/run-vitest.mjs index db87430acaf8..f4e73238b490 100644 --- a/scripts/run-vitest.mjs +++ b/scripts/run-vitest.mjs @@ -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), diff --git a/scripts/run-with-env.mjs b/scripts/run-with-env.mjs index b78acb0c10f7..602c62dd6e5f 100644 --- a/scripts/run-with-env.mjs +++ b/scripts/run-with-env.mjs @@ -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 { diff --git a/scripts/runtime-postbuild-shared.mjs b/scripts/runtime-postbuild-shared.mjs index 7d60be6f7464..fbb75da3ac2d 100644 --- a/scripts/runtime-postbuild-shared.mjs +++ b/scripts/runtime-postbuild-shared.mjs @@ -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 }); diff --git a/scripts/runtime-postbuild-stamp.mjs b/scripts/runtime-postbuild-stamp.mjs index 7bbd8e691d77..00b6b88e4dc5 100644 --- a/scripts/runtime-postbuild-stamp.mjs +++ b/scripts/runtime-postbuild-stamp.mjs @@ -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"; diff --git a/scripts/runtime-postbuild.mjs b/scripts/runtime-postbuild.mjs index 97ed4c972951..f77abd5eab2e 100644 --- a/scripts/runtime-postbuild.mjs +++ b/scripts/runtime-postbuild.mjs @@ -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) => {