From f71f5bc5866f40306c6fb6fb05b67ddced5d2b26 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 28 Apr 2026 00:23:38 +0100 Subject: [PATCH] fix: repair packaged plugin runtime mirrors --- scripts/e2e/doctor-install-switch-docker.sh | 4 ++ src/plugins/bundled-runtime-deps.test.ts | 4 +- src/plugins/bundled-runtime-deps.ts | 2 +- src/plugins/bundled-runtime-root.test.ts | 80 +++++++++++++++++++++ src/plugins/bundled-runtime-root.ts | 79 ++++++++++++++++++-- 5 files changed, 161 insertions(+), 8 deletions(-) diff --git a/scripts/e2e/doctor-install-switch-docker.sh b/scripts/e2e/doctor-install-switch-docker.sh index 933101f30ce..bfd3c1ff49b 100755 --- a/scripts/e2e/doctor-install-switch-docker.sh +++ b/scripts/e2e/doctor-install-switch-docker.sh @@ -44,6 +44,10 @@ case "$cmd" in status) exit 0 ;; + is-active) + echo "inactive" >&2 + exit 3 + ;; is-enabled) unit="${args[1]:-}" unit_path="$HOME/.config/systemd/user/${unit}" diff --git a/src/plugins/bundled-runtime-deps.test.ts b/src/plugins/bundled-runtime-deps.test.ts index ce60ae1fcec..d53138243ba 100644 --- a/src/plugins/bundled-runtime-deps.test.ts +++ b/src/plugins/bundled-runtime-deps.test.ts @@ -1072,7 +1072,7 @@ describe("scanBundledPluginRuntimeDeps config policy", () => { JSON.stringify({ name: "openclaw", version: "2026.4.25", - dependencies: { tslog: "^4.10.2" }, + dependencies: { semver: "7.7.4", tslog: "^4.10.2" }, }), ); writeBundledPluginPackage({ @@ -1090,10 +1090,12 @@ describe("scanBundledPluginRuntimeDeps config policy", () => { expect(result.deps.map((dep) => `${dep.name}@${dep.version}`)).toEqual([ "discord-runtime@1.0.0", + "semver@7.7.4", "tslog@^4.10.2", ]); expect(result.missing.map((dep) => `${dep.name}@${dep.version}`)).toEqual([ "discord-runtime@1.0.0", + "semver@7.7.4", "tslog@^4.10.2", ]); }); diff --git a/src/plugins/bundled-runtime-deps.ts b/src/plugins/bundled-runtime-deps.ts index 3bc10c64b22..42d301186de 100644 --- a/src/plugins/bundled-runtime-deps.ts +++ b/src/plugins/bundled-runtime-deps.ts @@ -69,7 +69,7 @@ const BUNDLED_RUNTIME_DEPS_OWNERLESS_LOCK_STALE_MS = 30_000; const BUNDLED_RUNTIME_DEPS_INSTALL_PROGRESS_INTERVAL_MS = 5_000; const BUNDLED_RUNTIME_MIRROR_MATERIALIZED_EXTENSIONS = new Set([".cjs", ".js", ".mjs"]); const BUNDLED_EXTENSION_DIST_DIR = "extensions"; -const MIRRORED_CORE_RUNTIME_DEP_NAMES = ["tslog"] as const; +const MIRRORED_CORE_RUNTIME_DEP_NAMES = ["semver", "tslog"] as const; const MIRRORED_PACKAGE_RUNTIME_DEP_PLUGIN_ID = "openclaw-core"; const BUNDLED_RUNTIME_MIRROR_PLUGIN_REGION_RE = /(?:^|\n)\/\/#region extensions\/[^/\s]+(?:\/|$)/u; const BUNDLED_RUNTIME_MIRROR_IMPORT_SPECIFIER_RE = diff --git a/src/plugins/bundled-runtime-root.test.ts b/src/plugins/bundled-runtime-root.test.ts index 8f625721ef6..a00e5096c35 100644 --- a/src/plugins/bundled-runtime-root.test.ts +++ b/src/plugins/bundled-runtime-root.test.ts @@ -180,6 +180,86 @@ describe("prepareBundledPluginRuntimeRoot", () => { expect(fs.readFileSync(distChunk, "utf8")).toContain("same-root"); }); + it("mirrors canonical dist chunks when loading from dist-runtime", () => { + const packageRoot = makeTempRoot(); + const stageDir = makeTempRoot(); + const canonicalPluginRoot = path.join(packageRoot, "dist", "extensions", "qqbot"); + const runtimePluginRoot = path.join(packageRoot, "dist-runtime", "extensions", "qqbot"); + const env = { ...process.env, OPENCLAW_PLUGIN_STAGE_DIR: stageDir }; + fs.mkdirSync(canonicalPluginRoot, { recursive: true }); + fs.mkdirSync(runtimePluginRoot, { recursive: true }); + fs.writeFileSync( + path.join(packageRoot, "package.json"), + JSON.stringify({ name: "openclaw", version: "2026.4.27", type: "module" }), + "utf8", + ); + fs.writeFileSync( + path.join(packageRoot, "dist", "onboard-abc123.js"), + "export const setup = 'canonical-setup';\n", + "utf8", + ); + fs.writeFileSync( + path.join(canonicalPluginRoot, "index.js"), + `import { setup } from "../../onboard-abc123.js"; export default { id: "qqbot", setup };\n`, + "utf8", + ); + fs.writeFileSync( + path.join(canonicalPluginRoot, "package.json"), + JSON.stringify( + { + name: "@openclaw/qqbot", + version: "1.0.0", + type: "module", + dependencies: { "qqbot-runtime": "1.0.0" }, + openclaw: { extensions: ["./index.js"] }, + }, + null, + 2, + ), + "utf8", + ); + fs.writeFileSync( + path.join(runtimePluginRoot, "index.js"), + `export { default } from "../../../dist/extensions/qqbot/index.js";\n`, + "utf8", + ); + fs.writeFileSync( + path.join(runtimePluginRoot, "package.json"), + JSON.stringify( + { + name: "@openclaw/qqbot", + version: "1.0.0", + type: "module", + dependencies: { "qqbot-runtime": "1.0.0" }, + openclaw: { extensions: ["./index.js"] }, + }, + null, + 2, + ), + "utf8", + ); + const installRoot = resolveBundledRuntimeDependencyInstallRoot(runtimePluginRoot, { env }); + fs.mkdirSync(path.join(installRoot, "node_modules", "qqbot-runtime"), { recursive: true }); + fs.writeFileSync( + path.join(installRoot, "node_modules", "qqbot-runtime", "package.json"), + JSON.stringify({ name: "qqbot-runtime", version: "1.0.0", type: "module" }), + "utf8", + ); + + const prepared = prepareBundledPluginRuntimeRoot({ + pluginId: "qqbot", + pluginRoot: runtimePluginRoot, + modulePath: path.join(runtimePluginRoot, "index.js"), + env, + }); + + expect(prepared.pluginRoot).toBe(path.join(installRoot, "dist-runtime", "extensions", "qqbot")); + expect(fs.existsSync(path.join(installRoot, "dist", "onboard-abc123.js"))).toBe(true); + expect( + fs.readFileSync(path.join(installRoot, "dist", "extensions", "qqbot", "index.js"), "utf8"), + ).toContain("onboard-abc123"); + }); + it("reuses unchanged external runtime mirrors from the original plugin root", async () => { const packageRoot = makeTempRoot(); const stageDir = makeTempRoot(); diff --git a/src/plugins/bundled-runtime-root.ts b/src/plugins/bundled-runtime-root.ts index 895b08c932d..091bdd3ed82 100644 --- a/src/plugins/bundled-runtime-root.ts +++ b/src/plugins/bundled-runtime-root.ts @@ -138,16 +138,52 @@ function prepareBundledPluginRuntimeDistMirror(params: { }): string { const sourceExtensionsRoot = path.dirname(params.pluginRoot); const sourceDistRoot = path.dirname(sourceExtensionsRoot); - const mirrorDistRoot = path.join(params.installRoot, "dist"); + const sourceDistRootName = path.basename(sourceDistRoot); + const mirrorDistRoot = path.join(params.installRoot, sourceDistRootName); const mirrorExtensionsRoot = path.join(mirrorDistRoot, "extensions"); + ensureBundledRuntimeMirrorDirectory(mirrorDistRoot); fs.mkdirSync(mirrorExtensionsRoot, { recursive: true, mode: 0o755 }); ensureBundledRuntimeDistPackageJson(mirrorDistRoot); - for (const entry of fs.readdirSync(sourceDistRoot, { withFileTypes: true })) { + mirrorBundledRuntimeDistRootEntries({ + sourceDistRoot, + mirrorDistRoot, + }); + if (sourceDistRootName === "dist-runtime") { + mirrorCanonicalBundledRuntimeDistRoot({ + installRoot: params.installRoot, + pluginRoot: params.pluginRoot, + sourceRuntimeDistRoot: sourceDistRoot, + }); + } + ensureOpenClawPluginSdkAlias(mirrorDistRoot); + return mirrorExtensionsRoot; +} + +function ensureBundledRuntimeMirrorDirectory(targetRoot: string): void { + try { + const stat = fs.lstatSync(targetRoot); + if (stat.isDirectory() && !stat.isSymbolicLink()) { + return; + } + fs.rmSync(targetRoot, { recursive: true, force: true }); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") { + throw error; + } + } + fs.mkdirSync(targetRoot, { recursive: true, mode: 0o755 }); +} + +function mirrorBundledRuntimeDistRootEntries(params: { + sourceDistRoot: string; + mirrorDistRoot: string; +}): void { + for (const entry of fs.readdirSync(params.sourceDistRoot, { withFileTypes: true })) { if (entry.name === "extensions") { continue; } - const sourcePath = path.join(sourceDistRoot, entry.name); - const targetPath = path.join(mirrorDistRoot, entry.name); + const sourcePath = path.join(params.sourceDistRoot, entry.name); + const targetPath = path.join(params.mirrorDistRoot, entry.name); if (path.resolve(sourcePath) === path.resolve(targetPath)) { continue; } @@ -171,8 +207,39 @@ function prepareBundledPluginRuntimeDistMirror(params: { } } } - ensureOpenClawPluginSdkAlias(mirrorDistRoot); - return mirrorExtensionsRoot; +} + +function mirrorCanonicalBundledRuntimeDistRoot(params: { + installRoot: string; + pluginRoot: string; + sourceRuntimeDistRoot: string; +}): void { + const sourceCanonicalDistRoot = path.join(path.dirname(params.sourceRuntimeDistRoot), "dist"); + if (!fs.existsSync(sourceCanonicalDistRoot)) { + return; + } + const targetCanonicalDistRoot = path.join(params.installRoot, "dist"); + ensureBundledRuntimeMirrorDirectory(targetCanonicalDistRoot); + fs.mkdirSync(path.join(targetCanonicalDistRoot, "extensions"), { recursive: true, mode: 0o755 }); + ensureBundledRuntimeDistPackageJson(targetCanonicalDistRoot); + mirrorBundledRuntimeDistRootEntries({ + sourceDistRoot: sourceCanonicalDistRoot, + mirrorDistRoot: targetCanonicalDistRoot, + }); + ensureOpenClawPluginSdkAlias(targetCanonicalDistRoot); + + const pluginId = path.basename(params.pluginRoot); + const sourceCanonicalPluginRoot = path.join(sourceCanonicalDistRoot, "extensions", pluginId); + if (!fs.existsSync(sourceCanonicalPluginRoot)) { + return; + } + const targetCanonicalPluginRoot = path.join(targetCanonicalDistRoot, "extensions", pluginId); + refreshBundledPluginRuntimeMirrorRoot({ + pluginId, + sourceRoot: sourceCanonicalPluginRoot, + targetRoot: targetCanonicalPluginRoot, + tempDirParent: path.dirname(targetCanonicalPluginRoot), + }); } function ensureBundledRuntimeDistPackageJson(mirrorDistRoot: string): void {