import { spawn } from "node:child_process"; import fs from "node:fs"; import { createRequire } from "node:module"; import path, { resolve } from "node:path"; const require = createRequire(import.meta.url); const repoRoot = resolve(import.meta.dirname, ".."); const tscBin = require.resolve("typescript/bin/tsc"); const TYPE_INPUT_EXTENSIONS = new Set([".ts", ".tsx", ".d.ts", ".js", ".mjs", ".json"]); const VALID_MODES = new Set(["all", "package-boundary"]); const ROOT_DTS_INPUTS = [ "tsconfig.json", "tsconfig.plugin-sdk.dts.json", "src", "packages/memory-host-sdk/src", ]; const PACKAGE_DTS_INPUTS = [ "tsconfig.json", "packages/plugin-sdk/tsconfig.json", "src/plugin-sdk", "src/video-generation/dashscope-compatible.ts", "src/video-generation/types.ts", "src/types", ]; const ENTRY_SHIMS_INPUTS = [ "scripts/write-plugin-sdk-entry-dts.ts", "scripts/lib/plugin-sdk-entrypoints.json", "scripts/lib/plugin-sdk-entries.mjs", ]; function isRelevantTypeInput(filePath) { const basename = path.basename(filePath); if (basename.endsWith(".test.ts")) { return false; } return TYPE_INPUT_EXTENSIONS.has(path.extname(filePath)); } export function parseMode(argv = 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; } function collectNewestMtime(paths, params = {}) { const rootDir = params.rootDir ?? repoRoot; const includeFile = params.includeFile ?? (() => true); let newestMtimeMs = 0; function visit(entryPath) { if (!fs.existsSync(entryPath)) { return; } const stats = fs.statSync(entryPath); if (stats.isDirectory()) { for (const child of fs.readdirSync(entryPath)) { visit(path.join(entryPath, child)); } return; } if (!includeFile(entryPath)) { return; } newestMtimeMs = Math.max(newestMtimeMs, stats.mtimeMs); } for (const relativePath of paths) { visit(resolve(rootDir, relativePath)); } return newestMtimeMs; } function collectOldestMtime(paths, params = {}) { 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; } export function isArtifactSetFresh(params) { const newestInputMtimeMs = collectNewestMtime(params.inputPaths, { rootDir: params.rootDir, includeFile: params.includeFile, }); const oldestOutputMtimeMs = collectOldestMtime(params.outputPaths, { rootDir: params.rootDir }); return oldestOutputMtimeMs !== null && oldestOutputMtimeMs >= newestInputMtimeMs; } export function createPrefixedOutputWriter(label, target) { let buffered = ""; const prefix = `[${label}] `; return { write(chunk) { 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) { if (abortController && !abortController.signal.aborted) { abortController.abort(); } } export function runNodeStep(label, args, timeoutMs, params = {}) { const abortController = params.abortController; return new Promise((resolvePromise, rejectPromise) => { const child = spawn(process.execPath, args, { cwd: repoRoot, env: process.env, signal: abortController?.signal, stdio: ["ignore", "pipe", "pipe"], }); let settled = false; const stdoutWriter = createPrefixedOutputWriter(label, process.stdout); const stderrWriter = createPrefixedOutputWriter(label, process.stderr); const timer = setTimeout(() => { if (settled) { return; } child.kill("SIGTERM"); settled = true; stdoutWriter.flush(); stderrWriter.flush(); abortSiblingSteps(abortController); rejectPromise(new Error(`${label} timed out after ${timeoutMs}ms`)); }, timeoutMs); child.stdout.setEncoding("utf8"); child.stderr.setEncoding("utf8"); child.stdout.on("data", (chunk) => { stdoutWriter.write(chunk); }); child.stderr.on("data", (chunk) => { stderrWriter.write(chunk); }); child.on("error", (error) => { if (settled) { return; } clearTimeout(timer); settled = true; stdoutWriter.flush(); stderrWriter.flush(); if (error.name === "AbortError" && abortController?.signal.aborted) { rejectPromise(new Error(`${label} canceled after sibling failure`)); return; } abortSiblingSteps(abortController); rejectPromise(new Error(`${label} failed to start: ${error.message}`)); }); child.on("close", (code) => { if (settled) { return; } clearTimeout(timer); settled = true; stdoutWriter.flush(); stderrWriter.flush(); if (code === 0) { resolvePromise(); return; } abortSiblingSteps(abortController); rejectPromise(new Error(`${label} failed with exit code ${code ?? 1}`)); }); }); } export async function runNodeStepsInParallel(steps) { const abortController = new AbortController(); const results = await Promise.allSettled( steps.map((step) => runNodeStep(step.label, step.args, step.timeoutMs, { abortController })), ); const firstFailure = results.find((result) => result.status === "rejected"); if (firstFailure) { throw firstFailure.reason; } } export async function main(argv = process.argv.slice(2)) { try { const mode = parseMode(argv); const rootDtsFresh = isArtifactSetFresh({ inputPaths: ROOT_DTS_INPUTS, outputPaths: ["dist/plugin-sdk/.tsbuildinfo"], includeFile: isRelevantTypeInput, }); const packageDtsFresh = isArtifactSetFresh({ inputPaths: PACKAGE_DTS_INPUTS, outputPaths: ["packages/plugin-sdk/dist/.tsbuildinfo"], includeFile: isRelevantTypeInput, }); const entryShimsFresh = isArtifactSetFresh({ inputPaths: [ ...ENTRY_SHIMS_INPUTS, "dist/plugin-sdk/.tsbuildinfo", "packages/plugin-sdk/dist/.tsbuildinfo", ], outputPaths: ["dist/plugin-sdk/.boundary-entry-shims.stamp"], }); const pendingSteps = []; if (mode === "all") { if (!rootDtsFresh) { pendingSteps.push({ label: "plugin-sdk boundary dts", args: [tscBin, "-p", "tsconfig.plugin-sdk.dts.json"], timeoutMs: 300_000, }); } else { process.stdout.write("[plugin-sdk boundary dts] fresh; skipping\n"); } } if (!packageDtsFresh) { pendingSteps.push({ label: "plugin-sdk package boundary dts", args: [tscBin, "-p", "packages/plugin-sdk/tsconfig.json"], timeoutMs: 300_000, }); } else { process.stdout.write("[plugin-sdk package boundary dts] fresh; skipping\n"); } if (pendingSteps.length > 0) { await runNodeStepsInParallel(pendingSteps); } if (mode === "all" && (!entryShimsFresh || pendingSteps.length > 0)) { await runNodeStep( "plugin-sdk boundary root shims", ["--import", "tsx", resolve(repoRoot, "scripts/write-plugin-sdk-entry-dts.ts")], 120_000, ); } else if (mode === "all") { process.stdout.write("[plugin-sdk boundary root shims] fresh; skipping\n"); } } catch (error) { process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); process.exit(1); } } if (import.meta.main) { await main(); }