// 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 { createManagedCommandInvocation, 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 & { 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 & { 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, ) => 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 | 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(); 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 tsgo = createManagedCommandInvocation({ args: ["-p", "tsconfig.plugin-sdk.dts.json", "--listFilesOnly", "--noEmit"], bin: tsgoPath, }); const result = spawnSync(tsgo.command, tsgo.args, { cwd: rootDir, encoding: "utf8", maxBuffer: 16 * 1024 * 1024, shell: tsgo.shell, windowsVerbatimArguments: tsgo.windowsVerbatimArguments, }); 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 = {}, ) { 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 = {}, ) { let newestMtimeMs = 0; for (const filePath of collectInputFiles(paths, params)) { newestMtimeMs = Math.max(newestMtimeMs, fs.statSync(filePath).mtimeMs); } return newestMtimeMs; } const inputFileDigestMemo = new Map(); // 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, ) { 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 = {}) { 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((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 | 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(); } }