mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
docs: document runner scripts
This commit is contained in:
@@ -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";
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user