mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 11:55:47 -06:00
448eb41653
* refactor(tooling): unify benchmark and process helpers * test(tooling): harden process fixture readiness * fix(tooling): keep benchmark harness types private
1382 lines
51 KiB
TypeScript
1382 lines
51 KiB
TypeScript
// Prepares declaration and entry-shim artifacts that prove plugin package
|
|
// boundary imports resolve through public package surfaces.
|
|
import {
|
|
spawn,
|
|
spawnSync,
|
|
type SpawnOptionsWithStdioTuple,
|
|
type StdioNull,
|
|
type StdioPipe,
|
|
} from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import fs from "node:fs";
|
|
import { createRequire } from "node:module";
|
|
import path, { resolve } from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import {
|
|
MAX_TIMER_TIMEOUT_MS,
|
|
resolveTimerTimeoutMs,
|
|
} from "../packages/normalization-core/src/number-coercion.ts";
|
|
import { acquireExtensionPackageBoundaryArtifactLockSync } from "./lib/extension-package-boundary-artifact-lock.mts";
|
|
import {
|
|
ensureRepoToolNodeModulesLink,
|
|
isLocalCheckEnabled,
|
|
resolveRepoToolBinPath,
|
|
} from "./lib/local-check-runtime.mts";
|
|
import { terminateManagedChild } from "./lib/managed-child-process.mts";
|
|
import { parsePositiveInt } from "./lib/numeric-options.mjs";
|
|
import {
|
|
listPluginSdkDeclarationOutputs,
|
|
pluginSdkEntrypoints,
|
|
productionPluginSdkEntrypoints,
|
|
} from "./lib/plugin-sdk-entries.mts";
|
|
import { resolveRepoRoot } from "./lib/repo-root.mjs";
|
|
const repoRoot = resolveRepoRoot(import.meta.url);
|
|
const runTsgoScript = path.join(repoRoot, "scripts/run-tsgo.mjs");
|
|
const TYPE_INPUT_EXTENSIONS = new Set([
|
|
".ts",
|
|
".tsx",
|
|
".mts",
|
|
".cts",
|
|
".js",
|
|
".mjs",
|
|
".cjs",
|
|
".json",
|
|
]);
|
|
const VALID_MODES = new Set(["all", "package-boundary"]);
|
|
const ROOT_BOUNDARY_TIMEOUT_MS = resolveBoundaryRootShimsTimeoutMs(process.env);
|
|
const ROOT_SHIMS_MAX_OLD_SPACE_SIZE =
|
|
process.env.OPENCLAW_ROOT_SHIMS_MAX_OLD_SPACE_SIZE?.trim() || "8192";
|
|
const ROOT_SHIMS_NODE_OPTIONS =
|
|
`${process.env.NODE_OPTIONS ?? ""} --max-old-space-size=${ROOT_SHIMS_MAX_OLD_SPACE_SIZE}`.trim();
|
|
const DEFAULT_NODE_STEP_ABORT_KILL_GRACE_MS = 1_000;
|
|
type NodeStepSignal = "SIGHUP" | "SIGINT" | "SIGKILL" | "SIGTERM";
|
|
const NODE_STEP_PARENT_SIGNALS = ["SIGHUP", "SIGINT", "SIGTERM"] satisfies NodeStepSignal[];
|
|
const NODE_STEP_PARENT_SIGNAL_EXIT_CODES = new Map([
|
|
["SIGHUP", 129],
|
|
["SIGINT", 130],
|
|
["SIGTERM", 143],
|
|
]);
|
|
type NodeStep = Pick<NodeStepParams, "abortKillGraceMs" | "env"> & {
|
|
args: string[];
|
|
label: string;
|
|
timeoutMs: number;
|
|
stamp?: ArtifactStamp;
|
|
};
|
|
type ArtifactFreshParams = {
|
|
includeFile?: (filePath: string) => boolean;
|
|
inputPaths: string[];
|
|
outputPaths: string[];
|
|
rootDir?: string;
|
|
hashStampPath?: string;
|
|
};
|
|
type ArtifactStamp = Pick<ArtifactFreshParams, "includeFile" | "inputPaths"> & { path: string };
|
|
type NodeStepOutput = {
|
|
on(event: "data", listener: (chunk: string) => void): unknown;
|
|
setEncoding(encoding: "utf8"): void;
|
|
};
|
|
type NodeStepChild = {
|
|
kill(signal: NodeStepSignal): void;
|
|
on(event: "close", listener: (code: number | null) => void): unknown;
|
|
on(event: "error", listener: (error: Error) => void): unknown;
|
|
pid?: number;
|
|
stderr: NodeStepOutput;
|
|
stdout: NodeStepOutput;
|
|
};
|
|
type SpawnNodeStep = (
|
|
command: string,
|
|
args: string[],
|
|
options: SpawnOptionsWithStdioTuple<StdioNull, StdioPipe, StdioPipe>,
|
|
) => NodeStepChild;
|
|
type NodeStepParams = {
|
|
abortController?: AbortController;
|
|
abortKillGraceMs?: number;
|
|
env?: NodeJS.ProcessEnv;
|
|
spawnImpl?: SpawnNodeStep;
|
|
};
|
|
const ACTIVE_NODE_STEP_KILLERS = new Map<(signal: NodeStepSignal) => void, number>();
|
|
let nodeStepParentSignalForwardersInstalled = false;
|
|
let exitingAfterParentSignal = false;
|
|
let parentSignalExitCode = 1;
|
|
let parentSignalExitTimer: ReturnType<typeof setTimeout> | undefined;
|
|
|
|
/** Resolve tsx's loader through the selected checkout toolchain. */
|
|
export function resolveTsxImportSpecifier({
|
|
resolveTool = resolveRepoToolBinPath,
|
|
createRequireFrom = createRequire,
|
|
ensureToolchain = ensureRepoToolNodeModulesLink,
|
|
}: {
|
|
resolveTool?: typeof resolveRepoToolBinPath;
|
|
createRequireFrom?: (filename: string) => { resolve(packageName: string): string };
|
|
ensureToolchain?: typeof ensureRepoToolNodeModulesLink;
|
|
} = {}) {
|
|
const tsxBinPath = resolveTool("tsx");
|
|
ensureToolchain(tsxBinPath);
|
|
return pathToFileURL(createRequireFrom(tsxBinPath).resolve("tsx")).href;
|
|
}
|
|
|
|
function listPackageDtsOutputsFromExports(packageDir: string, outputPrefix: string) {
|
|
const packageJson = JSON.parse(
|
|
fs.readFileSync(path.join(repoRoot, "packages", packageDir, "package.json"), "utf8"),
|
|
);
|
|
return Object.entries(packageJson.exports ?? {})
|
|
.flatMap(([exportKey, value]) => {
|
|
const entry =
|
|
exportKey === "." ? "index" : exportKey.startsWith("./") ? exportKey.slice(2) : "";
|
|
const importPath =
|
|
value && typeof value === "object" && !Array.isArray(value) && "import" in value
|
|
? value.import
|
|
: value;
|
|
if (!entry || entry.includes("..") || typeof importPath !== "string") {
|
|
return [];
|
|
}
|
|
if (!importPath.startsWith("./dist/") || !importPath.endsWith(".mjs")) {
|
|
return [];
|
|
}
|
|
return [`${outputPrefix}/${entry}.d.ts`];
|
|
})
|
|
.toSorted((a, b) => a.localeCompare(b));
|
|
}
|
|
|
|
function listSourceDtsOutputs(sourceDir: string, outputPrefix: string) {
|
|
const outputs: string[] = [];
|
|
|
|
function visit(relativeDir: string): void {
|
|
const absoluteDir = path.join(repoRoot, sourceDir, relativeDir);
|
|
for (const entry of fs.readdirSync(absoluteDir, { withFileTypes: true })) {
|
|
const relativePath = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
|
|
if (entry.isDirectory()) {
|
|
visit(relativePath);
|
|
continue;
|
|
}
|
|
if (
|
|
!entry.name.endsWith(".ts") ||
|
|
entry.name.endsWith(".test.ts") ||
|
|
entry.name.endsWith(".test-helpers.ts")
|
|
) {
|
|
continue;
|
|
}
|
|
outputs.push(`${outputPrefix}/${relativePath.replace(/\.ts$/u, ".d.ts")}`);
|
|
}
|
|
}
|
|
|
|
visit("");
|
|
return outputs.toSorted((a, b) => a.localeCompare(b));
|
|
}
|
|
|
|
type TypeScriptBuildInfo = {
|
|
fileNames?: unknown;
|
|
packageJsons?: unknown;
|
|
};
|
|
|
|
function collapsePluginSdkTypeInput(relativePath: string) {
|
|
const parts = relativePath.split("/");
|
|
if (parts[0] === "src" && parts.length > 2) {
|
|
return `src/${parts[1]}`;
|
|
}
|
|
if (parts[0] === "packages" && parts.length > 3) {
|
|
return `packages/${parts[1]}/${parts[2]}`;
|
|
}
|
|
if ((parts[0] === "scripts" || parts[0] === "test") && parts.length > 2) {
|
|
return `${parts[0]}/${parts[1]}`;
|
|
}
|
|
return relativePath;
|
|
}
|
|
|
|
function derivePluginSdkTypeInputs(entries: string[], baseDir: string, rootDir: string) {
|
|
const inputs = new Set<string>();
|
|
for (const entry of entries) {
|
|
const relativePath = path.relative(rootDir, resolve(baseDir, entry)).replaceAll("\\", "/");
|
|
if (
|
|
relativePath.startsWith("../") ||
|
|
relativePath === ".." ||
|
|
relativePath.startsWith("node_modules/") ||
|
|
relativePath.includes("/node_modules/") ||
|
|
relativePath.startsWith("dist/") ||
|
|
relativePath.startsWith("packages/plugin-sdk/dist/")
|
|
) {
|
|
continue;
|
|
}
|
|
inputs.add(collapsePluginSdkTypeInput(relativePath));
|
|
}
|
|
if (fs.existsSync(resolve(rootDir, "package.json"))) {
|
|
inputs.add("package.json");
|
|
}
|
|
for (const input of inputs) {
|
|
const packageName = input.match(/^packages\/([^/]+)\//u)?.[1];
|
|
if (packageName && fs.existsSync(resolve(rootDir, `packages/${packageName}/package.json`))) {
|
|
inputs.add(`packages/${packageName}/package.json`);
|
|
}
|
|
}
|
|
return [...inputs].toSorted((a, b) => a.localeCompare(b));
|
|
}
|
|
|
|
/** Derives repository-owned declaration inputs from TypeScript's build record. */
|
|
export function derivePluginSdkTypeInputsFromBuildInfo(buildInfoPath: string, rootDir = repoRoot) {
|
|
const parsed = JSON.parse(fs.readFileSync(buildInfoPath, "utf8")) as TypeScriptBuildInfo;
|
|
const fileNames = Array.isArray(parsed.fileNames) ? parsed.fileNames : [];
|
|
const packageJsons = Array.isArray(parsed.packageJsons) ? parsed.packageJsons : [];
|
|
const buildInfoDir = path.dirname(buildInfoPath);
|
|
const entries = [...fileNames, ...packageJsons];
|
|
if (entries.some((entry) => typeof entry !== "string")) {
|
|
throw new Error(`Invalid TypeScript build input in ${buildInfoPath}`);
|
|
}
|
|
return derivePluginSdkTypeInputs(entries as string[], buildInfoDir, rootDir);
|
|
}
|
|
|
|
/** Resolves declaration inputs from build metadata or the compiler on cold cache. */
|
|
export function resolvePluginSdkTypeInputs(rootDir = repoRoot) {
|
|
const buildInfoPath = resolve(rootDir, "dist/plugin-sdk/.tsbuildinfo");
|
|
if (fs.existsSync(buildInfoPath)) {
|
|
return derivePluginSdkTypeInputsFromBuildInfo(buildInfoPath, rootDir);
|
|
}
|
|
const tsgoPath = resolveRepoToolBinPath("tsgo");
|
|
ensureRepoToolNodeModulesLink(tsgoPath);
|
|
const result = spawnSync(
|
|
tsgoPath,
|
|
["-p", "tsconfig.plugin-sdk.dts.json", "--listFilesOnly", "--noEmit"],
|
|
{ cwd: rootDir, encoding: "utf8", maxBuffer: 16 * 1024 * 1024 },
|
|
);
|
|
if (result.status !== 0 || result.error) {
|
|
throw new Error(`Failed to derive plugin SDK type inputs: ${result.stderr || result.error}`);
|
|
}
|
|
return derivePluginSdkTypeInputs(result.stdout.trim().split("\n"), rootDir, rootDir);
|
|
}
|
|
|
|
// Compiler configuration is not part of .tsbuildinfo's file input record.
|
|
const resolveDtsInputs = (configPath: string) => [
|
|
"tsconfig.json",
|
|
configPath,
|
|
...resolvePluginSdkTypeInputs(),
|
|
];
|
|
const ROOT_DTS_STAMP = "dist/plugin-sdk/.boundary-dts.stamp";
|
|
const ACP_CORE_REQUIRED_DTS_OUTPUTS = listPackageDtsOutputsFromExports(
|
|
"acp-core",
|
|
"dist/plugin-sdk/packages/acp-core/src",
|
|
);
|
|
const AI_REQUIRED_DTS_OUTPUTS = listSourceDtsOutputs(
|
|
"packages/ai/src",
|
|
"dist/plugin-sdk/packages/ai/src",
|
|
);
|
|
const ROOT_DTS_REQUIRED_OUTPUTS = [
|
|
...AI_REQUIRED_DTS_OUTPUTS,
|
|
"dist/plugin-sdk/packages/memory-host-sdk/src/engine-embeddings.d.ts",
|
|
"dist/plugin-sdk/packages/memory-host-sdk/src/secret.d.ts",
|
|
"dist/plugin-sdk/packages/memory-host-sdk/src/status.d.ts",
|
|
"dist/plugin-sdk/packages/llm-core/src/index.d.ts",
|
|
"dist/plugin-sdk/packages/llm-core/src/types.d.ts",
|
|
"dist/plugin-sdk/packages/llm-core/src/utils/diagnostics.d.ts",
|
|
"dist/plugin-sdk/packages/llm-core/src/utils/event-stream.d.ts",
|
|
"dist/plugin-sdk/packages/llm-core/src/validation.d.ts",
|
|
"dist/plugin-sdk/packages/markdown-core/src/code-spans.d.ts",
|
|
"dist/plugin-sdk/packages/markdown-core/src/fences.d.ts",
|
|
"dist/plugin-sdk/packages/markdown-core/src/frontmatter.d.ts",
|
|
"dist/plugin-sdk/packages/markdown-core/src/index.d.ts",
|
|
"dist/plugin-sdk/packages/markdown-core/src/ir.d.ts",
|
|
"dist/plugin-sdk/packages/markdown-core/src/render-aware-chunking.d.ts",
|
|
"dist/plugin-sdk/packages/markdown-core/src/render.d.ts",
|
|
"dist/plugin-sdk/packages/markdown-core/src/tables.d.ts",
|
|
"dist/plugin-sdk/packages/markdown-core/src/types.d.ts",
|
|
"dist/plugin-sdk/packages/media-generation-core/src/capability-model-ref.d.ts",
|
|
"dist/plugin-sdk/packages/media-generation-core/src/catalog.d.ts",
|
|
"dist/plugin-sdk/packages/media-generation-core/src/index.d.ts",
|
|
"dist/plugin-sdk/packages/media-generation-core/src/model-ref.d.ts",
|
|
"dist/plugin-sdk/packages/media-generation-core/src/normalization.d.ts",
|
|
"dist/plugin-sdk/packages/media-core/src/base64.d.ts",
|
|
"dist/plugin-sdk/packages/media-core/src/constants.d.ts",
|
|
"dist/plugin-sdk/packages/media-core/src/content-length.d.ts",
|
|
"dist/plugin-sdk/packages/media-core/src/file-name.d.ts",
|
|
"dist/plugin-sdk/packages/media-core/src/inbound-path-policy.d.ts",
|
|
"dist/plugin-sdk/packages/media-core/src/inline-image-data-url.d.ts",
|
|
"dist/plugin-sdk/packages/media-core/src/media-source-url.d.ts",
|
|
"dist/plugin-sdk/packages/media-core/src/mime.d.ts",
|
|
"dist/plugin-sdk/packages/media-core/src/read-byte-stream-with-limit.d.ts",
|
|
...ACP_CORE_REQUIRED_DTS_OUTPUTS,
|
|
"dist/plugin-sdk/packages/terminal-core/src/ansi.d.ts",
|
|
"dist/plugin-sdk/packages/terminal-core/src/decorative-emoji.d.ts",
|
|
"dist/plugin-sdk/packages/terminal-core/src/health-style.d.ts",
|
|
"dist/plugin-sdk/packages/terminal-core/src/index.d.ts",
|
|
"dist/plugin-sdk/packages/terminal-core/src/links.d.ts",
|
|
"dist/plugin-sdk/packages/terminal-core/src/note.d.ts",
|
|
"dist/plugin-sdk/packages/terminal-core/src/osc-progress.d.ts",
|
|
"dist/plugin-sdk/packages/terminal-core/src/palette.d.ts",
|
|
"dist/plugin-sdk/packages/terminal-core/src/progress-line.d.ts",
|
|
"dist/plugin-sdk/packages/terminal-core/src/prompt-select-styled-params.d.ts",
|
|
"dist/plugin-sdk/packages/terminal-core/src/prompt-select-styled.d.ts",
|
|
"dist/plugin-sdk/packages/terminal-core/src/prompt-style.d.ts",
|
|
"dist/plugin-sdk/packages/terminal-core/src/restore.d.ts",
|
|
"dist/plugin-sdk/packages/terminal-core/src/safe-text.d.ts",
|
|
"dist/plugin-sdk/packages/terminal-core/src/stream-writer.d.ts",
|
|
"dist/plugin-sdk/packages/terminal-core/src/table.d.ts",
|
|
"dist/plugin-sdk/packages/terminal-core/src/terminal-link.d.ts",
|
|
"dist/plugin-sdk/packages/terminal-core/src/theme.d.ts",
|
|
"dist/plugin-sdk/packages/model-catalog-core/src/configured-model-refs.d.ts",
|
|
"dist/plugin-sdk/packages/model-catalog-core/src/model-catalog-normalize.d.ts",
|
|
"dist/plugin-sdk/packages/model-catalog-core/src/model-catalog-refs.d.ts",
|
|
"dist/plugin-sdk/packages/model-catalog-core/src/model-catalog-types.d.ts",
|
|
"dist/plugin-sdk/packages/model-catalog-core/src/provider-id.d.ts",
|
|
"dist/plugin-sdk/packages/model-catalog-core/src/provider-model-id-normalization.d.ts",
|
|
"dist/plugin-sdk/packages/model-catalog-core/src/provider-model-id-normalize.d.ts",
|
|
"dist/plugin-sdk/packages/retry/src/index.d.ts",
|
|
"dist/plugin-sdk/error-runtime.d.ts",
|
|
"dist/plugin-sdk/plugin-entry.d.ts",
|
|
"dist/plugin-sdk/provider-auth.d.ts",
|
|
"dist/plugin-sdk/video-generation.d.ts",
|
|
];
|
|
const PACKAGE_DTS_STAMP = "packages/plugin-sdk/dist/.boundary-dts.stamp";
|
|
const ACP_CORE_REQUIRED_PACKAGE_DTS_OUTPUTS = listPackageDtsOutputsFromExports(
|
|
"acp-core",
|
|
"packages/plugin-sdk/dist/packages/acp-core/src",
|
|
);
|
|
const AI_REQUIRED_PACKAGE_DTS_OUTPUTS = listSourceDtsOutputs(
|
|
"packages/ai/src",
|
|
"packages/plugin-sdk/dist/packages/ai/src",
|
|
);
|
|
const PACKAGE_DTS_REQUIRED_OUTPUTS = [
|
|
...AI_REQUIRED_PACKAGE_DTS_OUTPUTS,
|
|
"packages/plugin-sdk/dist/packages/markdown-core/src/code-spans.d.ts",
|
|
"packages/plugin-sdk/dist/packages/markdown-core/src/fences.d.ts",
|
|
"packages/plugin-sdk/dist/packages/markdown-core/src/frontmatter.d.ts",
|
|
"packages/plugin-sdk/dist/packages/markdown-core/src/index.d.ts",
|
|
"packages/plugin-sdk/dist/packages/markdown-core/src/ir.d.ts",
|
|
"packages/plugin-sdk/dist/packages/markdown-core/src/render-aware-chunking.d.ts",
|
|
"packages/plugin-sdk/dist/packages/markdown-core/src/render.d.ts",
|
|
"packages/plugin-sdk/dist/packages/markdown-core/src/tables.d.ts",
|
|
"packages/plugin-sdk/dist/packages/markdown-core/src/types.d.ts",
|
|
"packages/plugin-sdk/dist/packages/media-generation-core/src/capability-model-ref.d.ts",
|
|
"packages/plugin-sdk/dist/packages/media-generation-core/src/catalog.d.ts",
|
|
"packages/plugin-sdk/dist/packages/media-generation-core/src/index.d.ts",
|
|
"packages/plugin-sdk/dist/packages/media-generation-core/src/model-ref.d.ts",
|
|
"packages/plugin-sdk/dist/packages/media-generation-core/src/normalization.d.ts",
|
|
"packages/plugin-sdk/dist/packages/media-core/src/base64.d.ts",
|
|
"packages/plugin-sdk/dist/packages/media-core/src/constants.d.ts",
|
|
"packages/plugin-sdk/dist/packages/media-core/src/content-length.d.ts",
|
|
"packages/plugin-sdk/dist/packages/media-core/src/file-name.d.ts",
|
|
"packages/plugin-sdk/dist/packages/media-core/src/inbound-path-policy.d.ts",
|
|
"packages/plugin-sdk/dist/packages/media-core/src/inline-image-data-url.d.ts",
|
|
"packages/plugin-sdk/dist/packages/media-core/src/media-source-url.d.ts",
|
|
"packages/plugin-sdk/dist/packages/media-core/src/mime.d.ts",
|
|
"packages/plugin-sdk/dist/packages/media-core/src/read-byte-stream-with-limit.d.ts",
|
|
...ACP_CORE_REQUIRED_PACKAGE_DTS_OUTPUTS,
|
|
"packages/plugin-sdk/dist/packages/model-catalog-core/src/configured-model-refs.d.ts",
|
|
"packages/plugin-sdk/dist/packages/model-catalog-core/src/model-catalog-normalize.d.ts",
|
|
"packages/plugin-sdk/dist/packages/model-catalog-core/src/model-catalog-refs.d.ts",
|
|
"packages/plugin-sdk/dist/packages/model-catalog-core/src/model-catalog-types.d.ts",
|
|
"packages/plugin-sdk/dist/packages/model-catalog-core/src/provider-id.d.ts",
|
|
"packages/plugin-sdk/dist/packages/model-catalog-core/src/provider-model-id-normalization.d.ts",
|
|
"packages/plugin-sdk/dist/packages/model-catalog-core/src/provider-model-id-normalize.d.ts",
|
|
"packages/plugin-sdk/dist/packages/normalization-core/src/index.d.ts",
|
|
"packages/plugin-sdk/dist/packages/normalization-core/src/boolean-coercion.d.ts",
|
|
"packages/plugin-sdk/dist/packages/normalization-core/src/number-coercion.d.ts",
|
|
"packages/plugin-sdk/dist/packages/normalization-core/src/record-coerce.d.ts",
|
|
"packages/plugin-sdk/dist/packages/normalization-core/src/string-coerce.d.ts",
|
|
"packages/plugin-sdk/dist/packages/normalization-core/src/string-normalization.d.ts",
|
|
"packages/plugin-sdk/dist/packages/retry/src/index.d.ts",
|
|
"packages/plugin-sdk/dist/packages/terminal-core/src/ansi.d.ts",
|
|
"packages/plugin-sdk/dist/packages/terminal-core/src/decorative-emoji.d.ts",
|
|
"packages/plugin-sdk/dist/packages/terminal-core/src/health-style.d.ts",
|
|
"packages/plugin-sdk/dist/packages/terminal-core/src/index.d.ts",
|
|
"packages/plugin-sdk/dist/packages/terminal-core/src/links.d.ts",
|
|
"packages/plugin-sdk/dist/packages/terminal-core/src/note.d.ts",
|
|
"packages/plugin-sdk/dist/packages/terminal-core/src/osc-progress.d.ts",
|
|
"packages/plugin-sdk/dist/packages/terminal-core/src/palette.d.ts",
|
|
"packages/plugin-sdk/dist/packages/terminal-core/src/progress-line.d.ts",
|
|
"packages/plugin-sdk/dist/packages/terminal-core/src/prompt-select-styled-params.d.ts",
|
|
"packages/plugin-sdk/dist/packages/terminal-core/src/prompt-select-styled.d.ts",
|
|
"packages/plugin-sdk/dist/packages/terminal-core/src/prompt-style.d.ts",
|
|
"packages/plugin-sdk/dist/packages/terminal-core/src/restore.d.ts",
|
|
"packages/plugin-sdk/dist/packages/terminal-core/src/safe-text.d.ts",
|
|
"packages/plugin-sdk/dist/packages/terminal-core/src/stream-writer.d.ts",
|
|
"packages/plugin-sdk/dist/packages/terminal-core/src/table.d.ts",
|
|
"packages/plugin-sdk/dist/packages/terminal-core/src/terminal-link.d.ts",
|
|
"packages/plugin-sdk/dist/packages/terminal-core/src/theme.d.ts",
|
|
"packages/plugin-sdk/dist/src/plugin-sdk/error-runtime.d.ts",
|
|
"packages/plugin-sdk/dist/src/plugin-sdk/plugin-entry.d.ts",
|
|
"packages/plugin-sdk/dist/src/plugin-sdk/provider-auth.d.ts",
|
|
"packages/plugin-sdk/dist/src/plugin-sdk/video-generation.d.ts",
|
|
];
|
|
const QA_CHANNEL_DTS_INPUTS = [
|
|
"extensions/qa-channel/api.ts",
|
|
"extensions/qa-channel/runtime-api.ts",
|
|
"extensions/qa-channel/test-api.ts",
|
|
"extensions/qa-channel/src",
|
|
"extensions/qa-channel/tsconfig.json",
|
|
];
|
|
const QA_CHANNEL_DTS_STAMP = "dist/plugin-sdk/extensions/qa-channel/.boundary-dts.stamp";
|
|
const QA_CHANNEL_DTS_REQUIRED_OUTPUTS = ["dist/plugin-sdk/extensions/qa-channel/api.d.ts"];
|
|
const MEMORY_CORE_DTS_INPUTS = [
|
|
"extensions/memory-core/api.ts",
|
|
"extensions/memory-core/src",
|
|
"extensions/memory-core/tsconfig.json",
|
|
];
|
|
const MEMORY_CORE_DTS_STAMP = "dist/plugin-sdk/extensions/memory-core/.boundary-dts.stamp";
|
|
const MEMORY_CORE_DTS_REQUIRED_OUTPUTS = ["dist/plugin-sdk/extensions/memory-core/api.d.ts"];
|
|
const MATRIX_DTS_INPUTS = [
|
|
"extensions/matrix/test-api.ts",
|
|
"extensions/matrix/src",
|
|
"extensions/matrix/tsconfig.json",
|
|
];
|
|
const MATRIX_DTS_STAMP = "dist/plugin-sdk/extensions/matrix/.boundary-dts.stamp";
|
|
const MATRIX_DTS_REQUIRED_OUTPUTS = ["dist/plugin-sdk/extensions/matrix/test-api.d.ts"];
|
|
const DISCORD_DTS_INPUTS = [
|
|
"extensions/discord/api.ts",
|
|
"extensions/discord/src/api.ts",
|
|
"extensions/discord/tsconfig.json",
|
|
];
|
|
const DISCORD_DTS_STAMP = "dist/plugin-sdk/extensions/discord/.boundary-dts.stamp";
|
|
const DISCORD_DTS_REQUIRED_OUTPUTS = ["dist/plugin-sdk/extensions/discord/api.d.ts"];
|
|
const SLACK_DTS_INPUTS = [
|
|
"extensions/slack/api.ts",
|
|
"extensions/slack/src/client.ts",
|
|
"extensions/slack/tsconfig.json",
|
|
];
|
|
const SLACK_DTS_STAMP = "dist/plugin-sdk/extensions/slack/.boundary-dts.stamp";
|
|
const SLACK_DTS_REQUIRED_OUTPUTS = ["dist/plugin-sdk/extensions/slack/api.d.ts"];
|
|
const TELEGRAM_DTS_INPUTS = ["extensions/telegram/api.ts", "extensions/telegram/tsconfig.json"];
|
|
const TELEGRAM_DTS_STAMP = "dist/plugin-sdk/extensions/telegram/.boundary-dts.stamp";
|
|
const TELEGRAM_DTS_REQUIRED_OUTPUTS = ["dist/plugin-sdk/extensions/telegram/api.d.ts"];
|
|
const WHATSAPP_DTS_INPUTS = [
|
|
"extensions/whatsapp/api.ts",
|
|
"extensions/whatsapp/src/qa-driver.runtime.ts",
|
|
"extensions/whatsapp/tsconfig.json",
|
|
];
|
|
const WHATSAPP_DTS_STAMP = "dist/plugin-sdk/extensions/whatsapp/.boundary-dts.stamp";
|
|
const WHATSAPP_DTS_REQUIRED_OUTPUTS = ["dist/plugin-sdk/extensions/whatsapp/api.d.ts"];
|
|
const ENTRY_SHIMS_INPUTS = [
|
|
"scripts/lib/plugin-sdk-private-local-only-subpaths.json",
|
|
"scripts/write-plugin-sdk-entry-dts.ts",
|
|
"scripts/lib/plugin-sdk-entrypoints.json",
|
|
"scripts/lib/plugin-sdk-entries.mts",
|
|
];
|
|
/**
|
|
* Lists entry-shim artifacts written by scripts/write-plugin-sdk-entry-dts.ts.
|
|
*/
|
|
export function resolveBoundaryEntryShimRequiredOutputs(env: NodeJS.ProcessEnv = process.env) {
|
|
const entries =
|
|
env.OPENCLAW_BUILD_PRIVATE_QA === "1" ? pluginSdkEntrypoints : productionPluginSdkEntrypoints;
|
|
return [
|
|
...listPluginSdkDeclarationOutputs(entries),
|
|
...entries.map((entry) => `packages/plugin-sdk/dist/src/plugin-sdk/${entry}.d.ts`),
|
|
].toSorted((a, b) => a.localeCompare(b));
|
|
}
|
|
|
|
function isRelevantTypeInput(filePath: string) {
|
|
const basename = path.basename(filePath);
|
|
if (basename.endsWith(".test.ts")) {
|
|
return false;
|
|
}
|
|
return TYPE_INPUT_EXTENSIONS.has(path.extname(filePath));
|
|
}
|
|
|
|
/**
|
|
* Parses the artifact preparation mode from CLI arguments.
|
|
*/
|
|
export function parseMode(argv: string[] = process.argv.slice(2)) {
|
|
const modeArg = argv.find((arg) => arg.startsWith("--mode="));
|
|
const mode = modeArg?.slice("--mode=".length) ?? "all";
|
|
if (!VALID_MODES.has(mode)) {
|
|
throw new Error(`Unknown mode: ${mode}`);
|
|
}
|
|
return mode;
|
|
}
|
|
|
|
/**
|
|
* Reads the root boundary timeout override for long declaration and shim builds.
|
|
*/
|
|
export function resolveBoundaryRootShimsTimeoutMs(env: NodeJS.ProcessEnv = process.env) {
|
|
const raw = env.OPENCLAW_PLUGIN_SDK_BOUNDARY_ROOT_SHIMS_TIMEOUT_MS?.trim();
|
|
if (!raw) {
|
|
return 300_000;
|
|
}
|
|
return parsePositiveInt(raw, "OPENCLAW_PLUGIN_SDK_BOUNDARY_ROOT_SHIMS_TIMEOUT_MS");
|
|
}
|
|
|
|
function collectInputFiles(
|
|
paths: string[],
|
|
params: Pick<ArtifactFreshParams, "rootDir" | "includeFile"> = {},
|
|
) {
|
|
const rootDir = params.rootDir ?? repoRoot;
|
|
const includeFile = params.includeFile ?? (() => true);
|
|
const files: string[] = [];
|
|
|
|
function visit(entryPath: string): void {
|
|
if (!fs.existsSync(entryPath)) {
|
|
return;
|
|
}
|
|
if (fs.statSync(entryPath).isDirectory()) {
|
|
const basename = path.basename(entryPath);
|
|
if (basename === "dist" || basename === "node_modules") {
|
|
return;
|
|
}
|
|
for (const child of fs.readdirSync(entryPath)) {
|
|
visit(path.join(entryPath, child));
|
|
}
|
|
return;
|
|
}
|
|
if (!includeFile(entryPath)) {
|
|
return;
|
|
}
|
|
files.push(entryPath);
|
|
}
|
|
|
|
for (const relativePath of paths) {
|
|
visit(resolve(rootDir, relativePath));
|
|
}
|
|
|
|
return files;
|
|
}
|
|
|
|
function collectNewestMtime(
|
|
paths: string[],
|
|
params: Pick<ArtifactFreshParams, "rootDir" | "includeFile"> = {},
|
|
) {
|
|
let newestMtimeMs = 0;
|
|
for (const filePath of collectInputFiles(paths, params)) {
|
|
newestMtimeMs = Math.max(newestMtimeMs, fs.statSync(filePath).mtimeMs);
|
|
}
|
|
return newestMtimeMs;
|
|
}
|
|
|
|
const inputFileDigestMemo = new Map<string, string>();
|
|
|
|
// Keyed by stat identity, not path alone: entry-shim inputs include
|
|
// .tsbuildinfo files that lane builds rewrite mid-run.
|
|
function digestInputFile(filePath: string) {
|
|
const stats = fs.statSync(filePath);
|
|
const memoKey = `${filePath}\0${stats.size}\0${stats.mtimeMs}`;
|
|
const memoized = inputFileDigestMemo.get(memoKey);
|
|
if (memoized) {
|
|
return memoized;
|
|
}
|
|
const digest = createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
|
|
inputFileDigestMemo.set(memoKey, digest);
|
|
return digest;
|
|
}
|
|
|
|
/**
|
|
* Digests the exact input file set the mtime scan sees, so freshness can
|
|
* survive checkouts that re-stamp every file mtime (hosted CI runners).
|
|
*/
|
|
export function computeArtifactInputsDigest(
|
|
params: Pick<ArtifactFreshParams, "inputPaths" | "rootDir" | "includeFile">,
|
|
) {
|
|
const rootDir = params.rootDir ?? repoRoot;
|
|
const digest = createHash("sha256");
|
|
for (const filePath of collectInputFiles(params.inputPaths, params).toSorted()) {
|
|
digest.update(path.relative(rootDir, filePath));
|
|
digest.update("\0");
|
|
digest.update(digestInputFile(filePath));
|
|
digest.update("\n");
|
|
}
|
|
return digest.digest("hex");
|
|
}
|
|
|
|
// These affect artifact generation or plugin compilation but TypeScript does
|
|
// not record them as declaration-program file inputs.
|
|
const EXTENSION_BOUNDARY_NON_TYPE_INPUTS = [
|
|
"tsconfig.json",
|
|
"tsconfig.plugin-sdk.dts.json",
|
|
"packages/plugin-sdk/package.json",
|
|
"packages/plugin-sdk/tsconfig.json",
|
|
"scripts/check-extension-package-tsc-boundary.mts",
|
|
"scripts/prepare-extension-package-boundary-artifacts.mts",
|
|
"scripts/write-plugin-sdk-entry-dts.ts",
|
|
"scripts/lib/plugin-sdk-entrypoints.json",
|
|
"scripts/lib/plugin-sdk-entries.mts",
|
|
"scripts/lib/plugin-sdk-private-local-only-subpaths.json",
|
|
"pnpm-lock.yaml",
|
|
];
|
|
|
|
/** Computes the single content fingerprint used by every boundary artifact cache. */
|
|
export function computeExtensionBoundaryInputsFingerprint(rootDir = repoRoot) {
|
|
const digest = createHash("sha256");
|
|
digest.update(
|
|
computeArtifactInputsDigest({
|
|
rootDir,
|
|
inputPaths: resolvePluginSdkTypeInputs(rootDir),
|
|
includeFile: isRelevantTypeInput,
|
|
}),
|
|
);
|
|
digest.update(computeArtifactInputsDigest({ rootDir, inputPaths: ["extensions"] }));
|
|
digest.update(
|
|
computeArtifactInputsDigest({ rootDir, inputPaths: EXTENSION_BOUNDARY_NON_TYPE_INPUTS }),
|
|
);
|
|
digest.update(`\nnode=${process.versions.node}\n`);
|
|
return digest.digest("hex");
|
|
}
|
|
|
|
function collectOldestMtime(paths: string[], params: Pick<ArtifactFreshParams, "rootDir"> = {}) {
|
|
const rootDir = params.rootDir ?? repoRoot;
|
|
let oldestMtimeMs = Number.POSITIVE_INFINITY;
|
|
|
|
for (const relativePath of paths) {
|
|
const absolutePath = resolve(rootDir, relativePath);
|
|
if (!fs.existsSync(absolutePath)) {
|
|
return null;
|
|
}
|
|
oldestMtimeMs = Math.min(oldestMtimeMs, fs.statSync(absolutePath).mtimeMs);
|
|
}
|
|
|
|
return Number.isFinite(oldestMtimeMs) ? oldestMtimeMs : null;
|
|
}
|
|
|
|
/**
|
|
* Compares input and output mtimes to skip fresh generated artifacts. When the
|
|
* mtime fast path fails and the lane has a hash stamp, falls back to content
|
|
* identity: a fresh checkout re-stamps every input mtime, so cache-restored
|
|
* artifacts never look mtime-fresh on hosted CI runners even when no input
|
|
* byte changed.
|
|
*/
|
|
export function isArtifactSetFresh(params: ArtifactFreshParams) {
|
|
const newestInputMtimeMs = collectNewestMtime(params.inputPaths, {
|
|
rootDir: params.rootDir,
|
|
includeFile: params.includeFile,
|
|
});
|
|
const oldestOutputMtimeMs = collectOldestMtime(params.outputPaths, {
|
|
rootDir: params.rootDir,
|
|
});
|
|
if (oldestOutputMtimeMs !== null && oldestOutputMtimeMs >= newestInputMtimeMs) {
|
|
return true;
|
|
}
|
|
if (!params.hashStampPath || oldestOutputMtimeMs === null) {
|
|
return false;
|
|
}
|
|
const rootDir = params.rootDir ?? repoRoot;
|
|
const stampPath = resolve(rootDir, params.hashStampPath);
|
|
let recordedDigest: string;
|
|
try {
|
|
recordedDigest = fs.readFileSync(stampPath, "utf8").trim();
|
|
} catch {
|
|
return false;
|
|
}
|
|
if (!/^[0-9a-f]{64}$/.test(recordedDigest)) {
|
|
return false;
|
|
}
|
|
if (recordedDigest !== computeArtifactInputsDigest(params)) {
|
|
return false;
|
|
}
|
|
// Repair the mtime fast path so later invocations in this checkout skip
|
|
// without re-reading every input byte. The extra millisecond is required,
|
|
// not cosmetic: landing exactly on the newest input leaves no headroom for
|
|
// sub-millisecond write rounding or lagging metadata on CI filesystems, and
|
|
// an output that lands at or below its input silently keeps every later
|
|
// invocation on the expensive full-hash path this repair exists to avoid.
|
|
const now = new Date(Math.max(Date.now(), Math.ceil(newestInputMtimeMs)) + 1);
|
|
for (const relativePath of params.outputPaths) {
|
|
const outputPath = resolve(rootDir, relativePath);
|
|
if (fs.existsSync(outputPath)) {
|
|
fs.utimesSync(outputPath, now, now);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function hasMissingOutput(paths: string[]) {
|
|
return paths.some((relativePath) => !fs.existsSync(resolve(repoRoot, relativePath)));
|
|
}
|
|
|
|
// Stale inputs invalidate the whole incremental emit graph, not just missing
|
|
// outputs: reused .tsbuildinfo can skip re-emitting declarations whose own
|
|
// sources did not change even when the cached d.ts predates their current
|
|
// exports (observed on sticky-disk CI runners).
|
|
function removeStaleIncrementalState({ tsBuildInfoPath }: { tsBuildInfoPath: string }) {
|
|
fs.rmSync(resolve(repoRoot, tsBuildInfoPath), { force: true });
|
|
}
|
|
|
|
// The stamp records the lane's input digest so cache-restored artifacts stay
|
|
// fresh across checkouts that rewrite mtimes; writing it last also gives the
|
|
// mtime fast path a floor newer than the lane's build.
|
|
function writeStampFile(stamp: ArtifactStamp) {
|
|
const filePath = resolve(repoRoot, stamp.path);
|
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
fs.writeFileSync(filePath, `${computeArtifactInputsDigest(stamp)}\n`, "utf8");
|
|
}
|
|
|
|
/**
|
|
* Prefixes streamed child output line-by-line without breaking partial chunks.
|
|
*/
|
|
export function createPrefixedOutputWriter(label: string, target: { write(chunk: string): void }) {
|
|
let buffered = "";
|
|
const prefix = `[${label}] `;
|
|
|
|
return {
|
|
write(chunk: string) {
|
|
buffered += chunk;
|
|
while (true) {
|
|
const newlineIndex = buffered.indexOf("\n");
|
|
if (newlineIndex === -1) {
|
|
return;
|
|
}
|
|
const line = buffered.slice(0, newlineIndex + 1);
|
|
buffered = buffered.slice(newlineIndex + 1);
|
|
target.write(`${prefix}${line}`);
|
|
}
|
|
},
|
|
flush() {
|
|
if (!buffered) {
|
|
return;
|
|
}
|
|
target.write(`${prefix}${buffered}`);
|
|
buffered = "";
|
|
},
|
|
};
|
|
}
|
|
|
|
function abortSiblingSteps(abortController: AbortController | undefined) {
|
|
if (abortController && !abortController.signal.aborted) {
|
|
abortController.abort();
|
|
}
|
|
}
|
|
|
|
function signalActiveNodeSteps(signal: NodeStepSignal) {
|
|
for (const killNodeStep of ACTIVE_NODE_STEP_KILLERS.keys()) {
|
|
killNodeStep(signal);
|
|
}
|
|
}
|
|
|
|
function activeNodeStepKillGraceMs() {
|
|
return ACTIVE_NODE_STEP_KILLERS.size > 0
|
|
? Math.max(...ACTIVE_NODE_STEP_KILLERS.values())
|
|
: DEFAULT_NODE_STEP_ABORT_KILL_GRACE_MS;
|
|
}
|
|
|
|
function installNodeStepParentSignalForwarders() {
|
|
if (nodeStepParentSignalForwardersInstalled) {
|
|
return;
|
|
}
|
|
nodeStepParentSignalForwardersInstalled = true;
|
|
for (const signal of NODE_STEP_PARENT_SIGNALS) {
|
|
process.on(signal, () => {
|
|
const exitCode = NODE_STEP_PARENT_SIGNAL_EXIT_CODES.get(signal) ?? 1;
|
|
if (exitingAfterParentSignal) {
|
|
signalActiveNodeSteps("SIGKILL");
|
|
process.exit(exitCode);
|
|
}
|
|
exitingAfterParentSignal = true;
|
|
parentSignalExitCode = exitCode;
|
|
signalActiveNodeSteps(signal);
|
|
parentSignalExitTimer ??= setTimeout(
|
|
() => process.exit(parentSignalExitCode),
|
|
activeNodeStepKillGraceMs(),
|
|
);
|
|
});
|
|
}
|
|
process.on("exit", () => {
|
|
signalActiveNodeSteps("SIGKILL");
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Runs one artifact step with timeout, abort propagation, and prefixed output.
|
|
*/
|
|
export function runNodeStep(
|
|
label: string,
|
|
args: string[],
|
|
timeoutMs: number,
|
|
params: NodeStepParams = {},
|
|
) {
|
|
const resolvedTimeoutMs = resolveTimerTimeoutMs(timeoutMs, MAX_TIMER_TIMEOUT_MS);
|
|
const abortKillGraceMs = Math.max(
|
|
0,
|
|
Math.floor(params.abortKillGraceMs ?? DEFAULT_NODE_STEP_ABORT_KILL_GRACE_MS),
|
|
);
|
|
const abortController = params.abortController;
|
|
const spawnImpl: SpawnNodeStep = params.spawnImpl ?? spawn;
|
|
installNodeStepParentSignalForwarders();
|
|
return new Promise<void>((resolvePromise, rejectPromise) => {
|
|
const child = spawnImpl(process.execPath, args, {
|
|
cwd: repoRoot,
|
|
detached: process.platform !== "win32",
|
|
env: params.env ? { ...process.env, ...params.env } : process.env,
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
|
|
let settled = false;
|
|
let canceled = false;
|
|
let killTimer: ReturnType<typeof setTimeout> | undefined;
|
|
let killDeadlineAt = 0;
|
|
const stdoutWriter = createPrefixedOutputWriter(label, process.stdout);
|
|
const stderrWriter = createPrefixedOutputWriter(label, process.stderr);
|
|
const useProcessGroup = process.platform !== "win32";
|
|
const killNodeStep = (signal: NodeStepSignal) =>
|
|
terminateManagedChild(child, signal, { useProcessGroup });
|
|
const processGroupAlive = () => {
|
|
if (!useProcessGroup || !child.pid) {
|
|
return false;
|
|
}
|
|
try {
|
|
process.kill(-child.pid, 0);
|
|
return true;
|
|
} catch (error) {
|
|
return Boolean(
|
|
error && typeof error === "object" && "code" in error && error.code === "EPERM",
|
|
);
|
|
}
|
|
};
|
|
const waitForProcessGroupExit = async (waitMs: number) => {
|
|
const deadlineAt = Date.now() + waitMs;
|
|
while (Date.now() < deadlineAt) {
|
|
if (!processGroupAlive()) {
|
|
return true;
|
|
}
|
|
await new Promise((resolvePoll) => {
|
|
setTimeout(resolvePoll, 25);
|
|
});
|
|
}
|
|
return !processGroupAlive();
|
|
};
|
|
const waitForCanceledStepTeardown = async () => {
|
|
const remainingGraceMs = Math.max(0, killDeadlineAt - Date.now());
|
|
if (remainingGraceMs > 0) {
|
|
await waitForProcessGroupExit(remainingGraceMs);
|
|
}
|
|
if (processGroupAlive()) {
|
|
killNodeStep("SIGKILL");
|
|
await waitForProcessGroupExit(100);
|
|
}
|
|
};
|
|
ACTIVE_NODE_STEP_KILLERS.set(killNodeStep, abortKillGraceMs);
|
|
const abortStep = () => {
|
|
if (settled || canceled) {
|
|
return;
|
|
}
|
|
canceled = true;
|
|
killNodeStep("SIGTERM");
|
|
killDeadlineAt = Date.now() + abortKillGraceMs;
|
|
killTimer = setTimeout(() => {
|
|
killTimer = undefined;
|
|
killNodeStep("SIGKILL");
|
|
}, abortKillGraceMs);
|
|
killTimer.unref?.();
|
|
};
|
|
function cleanup() {
|
|
clearTimeout(timer);
|
|
clearTimeout(killTimer);
|
|
ACTIVE_NODE_STEP_KILLERS.delete(killNodeStep);
|
|
abortController?.signal.removeEventListener("abort", abortStep);
|
|
}
|
|
const timer = setTimeout(() => {
|
|
if (settled) {
|
|
return;
|
|
}
|
|
settled = true;
|
|
killNodeStep("SIGKILL");
|
|
cleanup();
|
|
stdoutWriter.flush();
|
|
stderrWriter.flush();
|
|
abortSiblingSteps(abortController);
|
|
rejectPromise(new Error(`${label} timed out after ${resolvedTimeoutMs}ms`));
|
|
}, resolvedTimeoutMs);
|
|
abortController?.signal.addEventListener("abort", abortStep, { once: true });
|
|
|
|
child.stdout.setEncoding("utf8");
|
|
child.stderr.setEncoding("utf8");
|
|
child.stdout.on("data", (chunk: string) => {
|
|
stdoutWriter.write(chunk);
|
|
});
|
|
child.stderr.on("data", (chunk: string) => {
|
|
stderrWriter.write(chunk);
|
|
});
|
|
child.on("error", (error: Error) => {
|
|
if (settled) {
|
|
return;
|
|
}
|
|
settled = true;
|
|
stdoutWriter.flush();
|
|
stderrWriter.flush();
|
|
if (exitingAfterParentSignal) {
|
|
killNodeStep("SIGKILL");
|
|
cleanup();
|
|
return;
|
|
}
|
|
cleanup();
|
|
abortSiblingSteps(abortController);
|
|
rejectPromise(new Error(`${label} failed to start: ${error.message}`));
|
|
});
|
|
child.on("close", (code: number | null) => {
|
|
if (settled) {
|
|
return;
|
|
}
|
|
void (async () => {
|
|
settled = true;
|
|
stdoutWriter.flush();
|
|
stderrWriter.flush();
|
|
if (exitingAfterParentSignal) {
|
|
killNodeStep("SIGKILL");
|
|
cleanup();
|
|
return;
|
|
}
|
|
if (canceled) {
|
|
await waitForCanceledStepTeardown();
|
|
cleanup();
|
|
rejectPromise(new Error(`${label} canceled after sibling failure`));
|
|
return;
|
|
}
|
|
cleanup();
|
|
if (code === 0) {
|
|
resolvePromise();
|
|
return;
|
|
}
|
|
abortSiblingSteps(abortController);
|
|
rejectPromise(new Error(`${label} failed with exit code ${code ?? 1}`));
|
|
})();
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Runs independent artifact steps together and aborts siblings on first failure.
|
|
*/
|
|
export async function runNodeStepsInParallel(steps: NodeStep[]) {
|
|
const abortController = new AbortController();
|
|
const results = await Promise.allSettled(
|
|
steps.map((step) =>
|
|
runNodeStep(step.label, step.args, step.timeoutMs, {
|
|
abortController,
|
|
abortKillGraceMs: step.abortKillGraceMs,
|
|
env: step.env,
|
|
}),
|
|
),
|
|
);
|
|
const firstFailure = results.find((result) => result.status === "rejected");
|
|
if (firstFailure) {
|
|
throw firstFailure.reason;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Chooses serial or parallel artifact execution based on local check policy.
|
|
*/
|
|
export async function runNodeSteps(steps: NodeStep[], env: NodeJS.ProcessEnv = process.env) {
|
|
if (!isLocalCheckEnabled(env)) {
|
|
await runNodeStepsInParallel(steps);
|
|
return;
|
|
}
|
|
|
|
for (const step of steps) {
|
|
await runNodeStep(step.label, step.args, step.timeoutMs, { env: step.env });
|
|
}
|
|
}
|
|
|
|
async function main(argv: string[] = process.argv.slice(2)) {
|
|
try {
|
|
if (argv.includes("--print-input-fingerprint")) {
|
|
process.stdout.write(`${computeExtensionBoundaryInputsFingerprint()}\n`);
|
|
return;
|
|
}
|
|
const mode = parseMode(argv);
|
|
const rootDtsInputs = resolveDtsInputs("tsconfig.plugin-sdk.dts.json");
|
|
const packageDtsInputs = resolveDtsInputs("packages/plugin-sdk/tsconfig.json");
|
|
const rootDtsFresh =
|
|
isArtifactSetFresh({
|
|
inputPaths: rootDtsInputs,
|
|
outputPaths: [ROOT_DTS_STAMP, "dist/plugin-sdk/.tsbuildinfo", ...ROOT_DTS_REQUIRED_OUTPUTS],
|
|
hashStampPath: ROOT_DTS_STAMP,
|
|
includeFile: isRelevantTypeInput,
|
|
}) && !hasMissingOutput(ROOT_DTS_REQUIRED_OUTPUTS);
|
|
const packageDtsFresh =
|
|
isArtifactSetFresh({
|
|
inputPaths: packageDtsInputs,
|
|
outputPaths: [
|
|
PACKAGE_DTS_STAMP,
|
|
"packages/plugin-sdk/dist/.tsbuildinfo",
|
|
...PACKAGE_DTS_REQUIRED_OUTPUTS,
|
|
],
|
|
hashStampPath: PACKAGE_DTS_STAMP,
|
|
includeFile: isRelevantTypeInput,
|
|
}) && !hasMissingOutput(PACKAGE_DTS_REQUIRED_OUTPUTS);
|
|
const entryShimsStamp: ArtifactStamp = {
|
|
path: "dist/plugin-sdk/.boundary-entry-shims.stamp",
|
|
inputPaths: [
|
|
...ENTRY_SHIMS_INPUTS,
|
|
"dist/plugin-sdk/.tsbuildinfo",
|
|
"packages/plugin-sdk/dist/.tsbuildinfo",
|
|
],
|
|
};
|
|
const entryShimsFresh = isArtifactSetFresh({
|
|
inputPaths: entryShimsStamp.inputPaths,
|
|
outputPaths: [
|
|
entryShimsStamp.path,
|
|
...resolveBoundaryEntryShimRequiredOutputs({
|
|
...process.env,
|
|
OPENCLAW_BUILD_PRIVATE_QA: "1",
|
|
}),
|
|
],
|
|
hashStampPath: entryShimsStamp.path,
|
|
});
|
|
const qaChannelDtsFresh =
|
|
isArtifactSetFresh({
|
|
inputPaths: QA_CHANNEL_DTS_INPUTS,
|
|
outputPaths: [QA_CHANNEL_DTS_STAMP, ...QA_CHANNEL_DTS_REQUIRED_OUTPUTS],
|
|
hashStampPath: QA_CHANNEL_DTS_STAMP,
|
|
includeFile: isRelevantTypeInput,
|
|
}) && !hasMissingOutput(QA_CHANNEL_DTS_REQUIRED_OUTPUTS);
|
|
const memoryCoreDtsFresh =
|
|
isArtifactSetFresh({
|
|
inputPaths: MEMORY_CORE_DTS_INPUTS,
|
|
outputPaths: [MEMORY_CORE_DTS_STAMP, ...MEMORY_CORE_DTS_REQUIRED_OUTPUTS],
|
|
hashStampPath: MEMORY_CORE_DTS_STAMP,
|
|
includeFile: isRelevantTypeInput,
|
|
}) && !hasMissingOutput(MEMORY_CORE_DTS_REQUIRED_OUTPUTS);
|
|
const matrixDtsFresh =
|
|
isArtifactSetFresh({
|
|
inputPaths: MATRIX_DTS_INPUTS,
|
|
outputPaths: [MATRIX_DTS_STAMP, ...MATRIX_DTS_REQUIRED_OUTPUTS],
|
|
hashStampPath: MATRIX_DTS_STAMP,
|
|
includeFile: isRelevantTypeInput,
|
|
}) && !hasMissingOutput(MATRIX_DTS_REQUIRED_OUTPUTS);
|
|
const discordDtsFresh =
|
|
isArtifactSetFresh({
|
|
inputPaths: DISCORD_DTS_INPUTS,
|
|
outputPaths: [DISCORD_DTS_STAMP, ...DISCORD_DTS_REQUIRED_OUTPUTS],
|
|
hashStampPath: DISCORD_DTS_STAMP,
|
|
includeFile: isRelevantTypeInput,
|
|
}) && !hasMissingOutput(DISCORD_DTS_REQUIRED_OUTPUTS);
|
|
const slackDtsFresh =
|
|
isArtifactSetFresh({
|
|
inputPaths: SLACK_DTS_INPUTS,
|
|
outputPaths: [SLACK_DTS_STAMP, ...SLACK_DTS_REQUIRED_OUTPUTS],
|
|
hashStampPath: SLACK_DTS_STAMP,
|
|
includeFile: isRelevantTypeInput,
|
|
}) && !hasMissingOutput(SLACK_DTS_REQUIRED_OUTPUTS);
|
|
const telegramDtsFresh =
|
|
isArtifactSetFresh({
|
|
inputPaths: TELEGRAM_DTS_INPUTS,
|
|
outputPaths: [TELEGRAM_DTS_STAMP, ...TELEGRAM_DTS_REQUIRED_OUTPUTS],
|
|
hashStampPath: TELEGRAM_DTS_STAMP,
|
|
includeFile: isRelevantTypeInput,
|
|
}) && !hasMissingOutput(TELEGRAM_DTS_REQUIRED_OUTPUTS);
|
|
const whatsappDtsFresh =
|
|
isArtifactSetFresh({
|
|
inputPaths: WHATSAPP_DTS_INPUTS,
|
|
outputPaths: [WHATSAPP_DTS_STAMP, ...WHATSAPP_DTS_REQUIRED_OUTPUTS],
|
|
hashStampPath: WHATSAPP_DTS_STAMP,
|
|
includeFile: isRelevantTypeInput,
|
|
}) && !hasMissingOutput(WHATSAPP_DTS_REQUIRED_OUTPUTS);
|
|
|
|
const prerequisiteSteps = [];
|
|
const dependentSteps = [];
|
|
if (mode === "all") {
|
|
if (!rootDtsFresh) {
|
|
removeStaleIncrementalState({
|
|
tsBuildInfoPath: "dist/plugin-sdk/.tsbuildinfo",
|
|
});
|
|
prerequisiteSteps.push({
|
|
label: "plugin-sdk boundary dts",
|
|
args: [runTsgoScript, "-p", "tsconfig.plugin-sdk.dts.json", "--declaration", "true"],
|
|
timeoutMs: ROOT_BOUNDARY_TIMEOUT_MS,
|
|
stamp: {
|
|
path: ROOT_DTS_STAMP,
|
|
inputPaths: rootDtsInputs,
|
|
includeFile: isRelevantTypeInput,
|
|
},
|
|
});
|
|
} else {
|
|
process.stdout.write("[plugin-sdk boundary dts] fresh; skipping\n");
|
|
}
|
|
}
|
|
if (!packageDtsFresh) {
|
|
removeStaleIncrementalState({
|
|
tsBuildInfoPath: "packages/plugin-sdk/dist/.tsbuildinfo",
|
|
});
|
|
prerequisiteSteps.push({
|
|
label: "plugin-sdk package boundary dts",
|
|
args: [runTsgoScript, "-p", "packages/plugin-sdk/tsconfig.json", "--declaration", "true"],
|
|
timeoutMs: ROOT_BOUNDARY_TIMEOUT_MS,
|
|
stamp: {
|
|
path: PACKAGE_DTS_STAMP,
|
|
inputPaths: packageDtsInputs,
|
|
includeFile: isRelevantTypeInput,
|
|
},
|
|
});
|
|
} else {
|
|
process.stdout.write("[plugin-sdk package boundary dts] fresh; skipping\n");
|
|
}
|
|
if (mode === "all") {
|
|
if (!qaChannelDtsFresh) {
|
|
removeStaleIncrementalState({
|
|
tsBuildInfoPath: "dist/plugin-sdk/extensions/qa-channel/.tsbuildinfo",
|
|
});
|
|
dependentSteps.push({
|
|
label: "qa-channel boundary dts",
|
|
args: [
|
|
runTsgoScript,
|
|
"-p",
|
|
"extensions/qa-channel/tsconfig.json",
|
|
"--declaration",
|
|
"true",
|
|
"--emitDeclarationOnly",
|
|
"true",
|
|
"--noEmit",
|
|
"false",
|
|
"--outDir",
|
|
"dist/plugin-sdk/extensions/qa-channel",
|
|
"--rootDir",
|
|
"extensions/qa-channel",
|
|
"--tsBuildInfoFile",
|
|
"dist/plugin-sdk/extensions/qa-channel/.tsbuildinfo",
|
|
],
|
|
timeoutMs: 300_000,
|
|
stamp: {
|
|
path: QA_CHANNEL_DTS_STAMP,
|
|
inputPaths: QA_CHANNEL_DTS_INPUTS,
|
|
includeFile: isRelevantTypeInput,
|
|
},
|
|
});
|
|
} else {
|
|
process.stdout.write("[qa-channel boundary dts] fresh; skipping\n");
|
|
}
|
|
if (!memoryCoreDtsFresh) {
|
|
removeStaleIncrementalState({
|
|
tsBuildInfoPath: "dist/plugin-sdk/extensions/memory-core/.tsbuildinfo",
|
|
});
|
|
dependentSteps.push({
|
|
label: "memory-core boundary dts",
|
|
args: [
|
|
runTsgoScript,
|
|
"-p",
|
|
"extensions/memory-core/tsconfig.json",
|
|
"--declaration",
|
|
"true",
|
|
"--emitDeclarationOnly",
|
|
"true",
|
|
"--noEmit",
|
|
"false",
|
|
"--outDir",
|
|
"dist/plugin-sdk/extensions/memory-core",
|
|
"--rootDir",
|
|
"extensions/memory-core",
|
|
"--tsBuildInfoFile",
|
|
"dist/plugin-sdk/extensions/memory-core/.tsbuildinfo",
|
|
],
|
|
timeoutMs: 300_000,
|
|
stamp: {
|
|
path: MEMORY_CORE_DTS_STAMP,
|
|
inputPaths: MEMORY_CORE_DTS_INPUTS,
|
|
includeFile: isRelevantTypeInput,
|
|
},
|
|
});
|
|
} else {
|
|
process.stdout.write("[memory-core boundary dts] fresh; skipping\n");
|
|
}
|
|
if (!matrixDtsFresh) {
|
|
removeStaleIncrementalState({
|
|
tsBuildInfoPath: "dist/plugin-sdk/extensions/matrix/.tsbuildinfo",
|
|
});
|
|
dependentSteps.push({
|
|
label: "matrix boundary dts",
|
|
args: [
|
|
runTsgoScript,
|
|
"-p",
|
|
"extensions/matrix/tsconfig.json",
|
|
"--declaration",
|
|
"true",
|
|
"--emitDeclarationOnly",
|
|
"true",
|
|
"--noEmit",
|
|
"false",
|
|
"--outDir",
|
|
"dist/plugin-sdk/extensions/matrix",
|
|
"--rootDir",
|
|
"extensions/matrix",
|
|
"--tsBuildInfoFile",
|
|
"dist/plugin-sdk/extensions/matrix/.tsbuildinfo",
|
|
],
|
|
timeoutMs: 300_000,
|
|
stamp: {
|
|
path: MATRIX_DTS_STAMP,
|
|
inputPaths: MATRIX_DTS_INPUTS,
|
|
includeFile: isRelevantTypeInput,
|
|
},
|
|
});
|
|
} else {
|
|
process.stdout.write("[matrix boundary dts] fresh; skipping\n");
|
|
}
|
|
if (!discordDtsFresh) {
|
|
removeStaleIncrementalState({
|
|
tsBuildInfoPath: "dist/plugin-sdk/extensions/discord/.tsbuildinfo",
|
|
});
|
|
dependentSteps.push({
|
|
label: "discord boundary dts",
|
|
args: [
|
|
runTsgoScript,
|
|
"-p",
|
|
"extensions/discord/tsconfig.json",
|
|
"--declaration",
|
|
"true",
|
|
"--emitDeclarationOnly",
|
|
"true",
|
|
"--noEmit",
|
|
"false",
|
|
"--outDir",
|
|
"dist/plugin-sdk/extensions/discord",
|
|
"--rootDir",
|
|
"extensions/discord",
|
|
"--tsBuildInfoFile",
|
|
"dist/plugin-sdk/extensions/discord/.tsbuildinfo",
|
|
],
|
|
timeoutMs: 300_000,
|
|
stamp: {
|
|
path: DISCORD_DTS_STAMP,
|
|
inputPaths: DISCORD_DTS_INPUTS,
|
|
includeFile: isRelevantTypeInput,
|
|
},
|
|
});
|
|
} else {
|
|
process.stdout.write("[discord boundary dts] fresh; skipping\n");
|
|
}
|
|
if (!slackDtsFresh) {
|
|
removeStaleIncrementalState({
|
|
tsBuildInfoPath: "dist/plugin-sdk/extensions/slack/.tsbuildinfo",
|
|
});
|
|
dependentSteps.push({
|
|
label: "slack boundary dts",
|
|
args: [
|
|
runTsgoScript,
|
|
"-p",
|
|
"extensions/slack/tsconfig.json",
|
|
"--declaration",
|
|
"true",
|
|
"--emitDeclarationOnly",
|
|
"true",
|
|
"--noEmit",
|
|
"false",
|
|
"--outDir",
|
|
"dist/plugin-sdk/extensions/slack",
|
|
"--rootDir",
|
|
"extensions/slack",
|
|
"--tsBuildInfoFile",
|
|
"dist/plugin-sdk/extensions/slack/.tsbuildinfo",
|
|
],
|
|
timeoutMs: 300_000,
|
|
stamp: {
|
|
path: SLACK_DTS_STAMP,
|
|
inputPaths: SLACK_DTS_INPUTS,
|
|
includeFile: isRelevantTypeInput,
|
|
},
|
|
});
|
|
} else {
|
|
process.stdout.write("[slack boundary dts] fresh; skipping\n");
|
|
}
|
|
if (!whatsappDtsFresh) {
|
|
removeStaleIncrementalState({
|
|
tsBuildInfoPath: "dist/plugin-sdk/extensions/whatsapp/.tsbuildinfo",
|
|
});
|
|
dependentSteps.push({
|
|
label: "whatsapp boundary dts",
|
|
args: [
|
|
runTsgoScript,
|
|
"-p",
|
|
"extensions/whatsapp/tsconfig.json",
|
|
"--declaration",
|
|
"true",
|
|
"--emitDeclarationOnly",
|
|
"true",
|
|
"--noEmit",
|
|
"false",
|
|
"--outDir",
|
|
"dist/plugin-sdk/extensions/whatsapp",
|
|
"--rootDir",
|
|
"extensions/whatsapp",
|
|
"--tsBuildInfoFile",
|
|
"dist/plugin-sdk/extensions/whatsapp/.tsbuildinfo",
|
|
],
|
|
timeoutMs: 300_000,
|
|
stamp: {
|
|
path: WHATSAPP_DTS_STAMP,
|
|
inputPaths: WHATSAPP_DTS_INPUTS,
|
|
includeFile: isRelevantTypeInput,
|
|
},
|
|
});
|
|
} else {
|
|
process.stdout.write("[whatsapp boundary dts] fresh; skipping\n");
|
|
}
|
|
if (!telegramDtsFresh) {
|
|
removeStaleIncrementalState({
|
|
tsBuildInfoPath: "dist/plugin-sdk/extensions/telegram/.tsbuildinfo",
|
|
});
|
|
dependentSteps.push({
|
|
label: "telegram boundary dts",
|
|
args: [
|
|
runTsgoScript,
|
|
"-p",
|
|
"extensions/telegram/tsconfig.json",
|
|
"--declaration",
|
|
"true",
|
|
"--emitDeclarationOnly",
|
|
"true",
|
|
"--noEmit",
|
|
"false",
|
|
"--outDir",
|
|
"dist/plugin-sdk/extensions/telegram",
|
|
"--rootDir",
|
|
"extensions/telegram",
|
|
"--tsBuildInfoFile",
|
|
"dist/plugin-sdk/extensions/telegram/.tsbuildinfo",
|
|
],
|
|
timeoutMs: 300_000,
|
|
stamp: {
|
|
path: TELEGRAM_DTS_STAMP,
|
|
inputPaths: TELEGRAM_DTS_INPUTS,
|
|
includeFile: isRelevantTypeInput,
|
|
},
|
|
});
|
|
} else {
|
|
process.stdout.write("[telegram boundary dts] fresh; skipping\n");
|
|
}
|
|
}
|
|
|
|
if (prerequisiteSteps.length > 0) {
|
|
await runNodeSteps(prerequisiteSteps);
|
|
for (const step of prerequisiteSteps) {
|
|
if (step.stamp) {
|
|
writeStampFile(step.stamp);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (mode === "all" && (!entryShimsFresh || prerequisiteSteps.length > 0)) {
|
|
await runNodeStep(
|
|
"plugin-sdk boundary root shims",
|
|
[
|
|
"--import",
|
|
resolveTsxImportSpecifier(),
|
|
resolve(repoRoot, "scripts/write-plugin-sdk-entry-dts.ts"),
|
|
],
|
|
ROOT_BOUNDARY_TIMEOUT_MS,
|
|
{
|
|
env: {
|
|
NODE_OPTIONS: ROOT_SHIMS_NODE_OPTIONS,
|
|
OPENCLAW_BUILD_PRIVATE_QA: "1",
|
|
},
|
|
},
|
|
);
|
|
// Overwrite the child's timestamp stamp with the input digest after the
|
|
// prerequisite tsbuildinfo files have settled.
|
|
writeStampFile(entryShimsStamp);
|
|
} else if (mode === "all") {
|
|
process.stdout.write("[plugin-sdk boundary root shims] fresh; skipping\n");
|
|
}
|
|
|
|
if (dependentSteps.length > 0) {
|
|
await runNodeSteps(dependentSteps);
|
|
for (const step of dependentSteps) {
|
|
if (step.stamp) {
|
|
writeStampFile(step.stamp);
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
|
|
if (import.meta.main) {
|
|
const releaseArtifactLock = acquireExtensionPackageBoundaryArtifactLockSync(repoRoot);
|
|
try {
|
|
await main();
|
|
} finally {
|
|
releaseArtifactLock();
|
|
}
|
|
}
|