diff --git a/scripts/run-node.mjs b/scripts/run-node.mjs index 6d00626c4271..60084a0e4a7e 100644 --- a/scripts/run-node.mjs +++ b/scripts/run-node.mjs @@ -32,7 +32,10 @@ import { runNodeSourceRoots, runNodeWatchedPaths, } from "./run-node-watch-paths.mjs"; -import { runRuntimePostBuild } from "./runtime-postbuild.mjs"; +import { + listCoreRuntimePostBuildOutputs, + runRuntimePostBuild, +} from "./runtime-postbuild.mjs"; export { isBuildRelevantRunNodePath, isRestartRelevantRunNodePath, runNodeWatchedPaths }; @@ -427,9 +430,15 @@ const listRequiredStaticExtensionAssetOutputs = (deps) => { .toSorted((left, right) => left.localeCompare(right)); }; -const listRequiredRuntimePostBuildOutputs = (deps) => { +const listRequiredCoreRuntimePostBuildOutputs = (deps) => + listCoreRuntimePostBuildOutputs({ rootDir: deps.cwd, fs: deps.fs }).map((relativePath) => + path.join(deps.cwd, normalizePath(relativePath)), + ); + +export const listRequiredRuntimePostBuildOutputs = (deps) => { const builtPluginEntries = listBuiltBundledPluginEntries(deps); return [ + ...listRequiredCoreRuntimePostBuildOutputs(deps), ...listRequiredOpenClawExtensionAliasOutputs(deps), ...listRequiredStaticExtensionAssetOutputs(deps), ...listRequiredBundledPluginMetadataOutputs(builtPluginEntries, deps), diff --git a/scripts/runtime-postbuild.mjs b/scripts/runtime-postbuild.mjs index fae1cf9b920f..c605708b692f 100644 --- a/scripts/runtime-postbuild.mjs +++ b/scripts/runtime-postbuild.mjs @@ -20,6 +20,8 @@ const ROOT_STABLE_RUNTIME_ALIAS_PATTERN = /^.+\.(?:runtime|contract)\.js$/u; const ROOT_RUNTIME_IMPORT_SPECIFIER_PATTERN = /(["'])\.\/([^"']+\.(?:runtime|contract)-[A-Za-z0-9_-]+\.js)\1/gu; const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); +const PLUGIN_SDK_ROOT_ALIAS_OUTPUT = "dist/plugin-sdk/root-alias.cjs"; +const OFFICIAL_CHANNEL_CATALOG_OUTPUT = "dist/channel-catalog.json"; const LEGACY_ROOT_RUNTIME_COMPAT_ALIASES = [ // v2026.4.29 dispatch lazy chunks. Package updates used to replace the // dist tree before the live gateway had restarted, so an already-loaded old @@ -117,7 +119,7 @@ const LEGACY_PLUGIN_INSTALL_RUNTIME_COMPAT_ALIASES = [ aliasFileName: PLUGIN_INSTALL_RUNTIME_ALIAS.aliasFileName, sourceIncludes: LEGACY_PLUGIN_INSTALL_RUNTIME_MARKERS, })); -const LEGACY_CLI_EXIT_COMPAT_CHUNKS = [ +export const LEGACY_CLI_EXIT_COMPAT_CHUNKS = [ { dest: "dist/memory-state-CcqRgDZU.js", contents: "export function hasMemoryRuntime() {\n return false;\n}\n", @@ -128,6 +130,48 @@ const LEGACY_CLI_EXIT_COMPAT_CHUNKS = [ }, ]; +export function listPluginSdkRootAliasOutputs() { + return [PLUGIN_SDK_ROOT_ALIAS_OUTPUT]; +} + +export function listOfficialChannelCatalogOutputs() { + return [OFFICIAL_CHANNEL_CATALOG_OUTPUT]; +} + +export function listStableRootRuntimeAliasOutputs(params = {}) { + const rootDir = params.rootDir ?? ROOT; + const distDir = path.join(rootDir, "dist"); + const fsImpl = params.fs ?? fs; + let entries = []; + try { + entries = fsImpl.readdirSync(distDir, { withFileTypes: true }); + } catch { + return []; + } + + return entries + .filter((entry) => entry.isFile()) + .map((entry) => entry.name.match(ROOT_RUNTIME_ALIAS_PATTERN)?.groups?.base) + .filter((base) => typeof base === "string" && base.length > 0) + .map((base) => `dist/${base}.js`) + .toSorted((left, right) => left.localeCompare(right)); +} + +export function listLegacyCliExitCompatOutputs(params = {}) { + const chunks = params.chunks ?? LEGACY_CLI_EXIT_COMPAT_CHUNKS; + return chunks + .map(({ dest }) => dest.replace(/\\/g, "/")) + .toSorted((left, right) => left.localeCompare(right)); +} + +export function listCoreRuntimePostBuildOutputs(params = {}) { + return [ + ...listPluginSdkRootAliasOutputs(), + ...listOfficialChannelCatalogOutputs(), + ...listStableRootRuntimeAliasOutputs(params), + ...listLegacyCliExitCompatOutputs(params), + ].toSorted((left, right) => left.localeCompare(right)); +} export function writeStableRootRuntimeAliases(params = {}) { const rootDir = params.rootDir ?? ROOT; const distDir = path.join(rootDir, "dist"); diff --git a/src/infra/run-node.test.ts b/src/infra/run-node.test.ts index a893a878f4db..52de06736e69 100644 --- a/src/infra/run-node.test.ts +++ b/src/infra/run-node.test.ts @@ -30,6 +30,12 @@ const DIST_ENTRY = "dist/entry.js"; const BUILD_STAMP = `dist/${BUILD_STAMP_FILE}`; const RUNTIME_POSTBUILD_STAMP = `dist/${RUNTIME_POSTBUILD_STAMP_FILE}`; const DIST_PLUGIN_SDK_INDEX = "dist/plugin-sdk/index.js"; +const DIST_PLUGIN_SDK_ROOT_ALIAS = "dist/plugin-sdk/root-alias.cjs"; +const DIST_CHANNEL_CATALOG = "dist/channel-catalog.json"; +const DIST_LEGACY_CLI_EXIT_COMPAT = "dist/memory-state-CcqRgDZU.js"; +const DIST_LEGACY_CLI_EXIT_COMPAT_ALT = "dist/memory-state-DwGdReW4.js"; +const DIST_STABLE_ROOT_RUNTIME_SOURCE = "dist/model-catalog.runtime-AbCd1234.js"; +const DIST_STABLE_ROOT_RUNTIME_ALIAS = "dist/model-catalog.runtime.js"; const QA_LAB_PLUGIN_SDK_ENTRY = "dist/plugin-sdk/qa-lab.js"; const QA_RUNTIME_PLUGIN_SDK_ENTRY = "dist/plugin-sdk/qa-runtime.js"; const EXTENSION_INDEX = bundledPluginFile("demo", "index.ts"); @@ -126,6 +132,25 @@ async function writeRuntimePostBuildScaffold(tmp: string): Promise { await fs.mkdir(path.join(tmp, "extensions"), { recursive: true }); await fs.writeFile(pluginSdkAliasPath, "module.exports = {};\n", "utf-8"); await fs.utimes(pluginSdkAliasPath, BUILD_TIME, BUILD_TIME); + await writeProjectFiles(tmp, { + [DIST_PLUGIN_SDK_ROOT_ALIAS]: "module.exports = {};\n", + [DIST_CHANNEL_CATALOG]: '{"entries":[]}\n', + [DIST_LEGACY_CLI_EXIT_COMPAT]: "export function hasMemoryRuntime() { return false; }\n", + [DIST_LEGACY_CLI_EXIT_COMPAT_ALT]: "export function hasMemoryRuntime() { return false; }\n", + [DIST_OPENCLAW_ALIAS_PACKAGE]: + '{"name":"openclaw","type":"module","exports":{"./plugin-sdk":"./plugin-sdk/index.js"}}\n', + }); + await touchProjectFiles( + tmp, + [ + DIST_PLUGIN_SDK_ROOT_ALIAS, + DIST_CHANNEL_CATALOG, + DIST_LEGACY_CLI_EXIT_COMPAT, + DIST_LEGACY_CLI_EXIT_COMPAT_ALT, + DIST_OPENCLAW_ALIAS_PACKAGE, + ], + BUILD_TIME, + ); } function expectedBuildSpawn() { @@ -1787,6 +1812,48 @@ describe("run-node script", () => { }); }); + it("reports missing core runtime postbuild outputs when runtime stamps match HEAD", async () => { + for (const missingPath of [ + DIST_PLUGIN_SDK_ROOT_ALIAS, + DIST_CHANNEL_CATALOG, + DIST_LEGACY_CLI_EXIT_COMPAT, + DIST_STABLE_ROOT_RUNTIME_ALIAS, + ]) { + await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => { + await setupTrackedProject(tmp, { + files: { + [ROOT_SRC]: "export const value = 1;\n", + [DIST_STABLE_ROOT_RUNTIME_SOURCE]: "export const value = 1;\n", + [DIST_STABLE_ROOT_RUNTIME_ALIAS]: + "export * from './model-catalog.runtime-AbCd1234.js';\n", + [RUNTIME_POSTBUILD_STAMP]: '{"head":"abc123"}\n', + }, + buildPaths: [ + ROOT_SRC, + DIST_ENTRY, + DIST_STABLE_ROOT_RUNTIME_SOURCE, + DIST_STABLE_ROOT_RUNTIME_ALIAS, + BUILD_STAMP, + RUNTIME_POSTBUILD_STAMP, + ], + }); + await fs.rm(resolvePath(tmp, missingPath)); + + const requirement = resolveRuntimePostBuildRequirement( + createBuildRequirementDeps(tmp, { + gitHead: "abc123\n", + gitStatus: "", + }), + ); + + expect(requirement).toEqual({ + shouldSync: true, + reason: "missing_runtime_postbuild_output", + }); + }); + } + }); + it("reports missing runtime skill outputs even when stamps match HEAD", async () => { await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => { await setupTrackedProject(tmp, {