From aacae4ce62a34625936999f22972607d4198aa01 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 21 Apr 2026 07:01:05 +0100 Subject: [PATCH] fix: use npm for bundled runtime dep repair --- CHANGELOG.md | 1 + src/plugins/bundled-runtime-deps.test.ts | 19 +++++++++++++++++++ src/plugins/bundled-runtime-deps.ts | 8 +++++++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21f1bd45459..ecf1ffe1892 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai - OpenAI/Responses: resolve `/think` levels against each GPT model's supported reasoning efforts so `/think off` no longer becomes high reasoning or sends unsupported `reasoning.effort: "none"` payloads. - Lobster/TaskFlow: allow managed approval resumes to use `approvalId` without a resume token, and persist that id in approval wait state. (#69559) Thanks @kirkluokun. - Plugins/startup: install bundled runtime dependencies into each plugin's own runtime directory, reuse source-checkout repair caches after rebuilds, and log only packages that were actually installed so repeated Gateway starts stay quiet once deps are present. +- Plugins/startup: ignore pnpm's `npm_execpath` when repairing bundled plugin runtime dependencies so npm-only install flags are not passed to pnpm-launched gateways. - Setup/TUI: relaunch the setup hatch TUI in a fresh process while preserving the configured gateway target and auth source, so onboarding recovers terminal state cleanly without exposing gateway secrets on command-line args. (#69524) Thanks @shakkernerd. - Codex: avoid re-exposing the image-generation tool on native vision turns with inbound images, and keep bare image-model overrides on the configured image provider. (#65061) Thanks @zhulijin1991. - Sessions/reset: clear auto-sourced model, provider, and auth-profile overrides on `/new` and `/reset` while preserving explicit user selections, so channel sessions stop staying pinned to runtime fallback choices. (#69419) Thanks @sk7n4k3d. diff --git a/src/plugins/bundled-runtime-deps.test.ts b/src/plugins/bundled-runtime-deps.test.ts index 5bac1100fe8..f5bc40bc2b9 100644 --- a/src/plugins/bundled-runtime-deps.test.ts +++ b/src/plugins/bundled-runtime-deps.test.ts @@ -90,6 +90,25 @@ describe("resolveBundledRuntimeDepsNpmRunner", () => { }); }); + it("ignores pnpm npm_execpath and falls back to npm", () => { + const execPath = "/opt/node/bin/node"; + const npmCliPath = "/opt/node/lib/node_modules/npm/bin/npm-cli.js"; + const runner = resolveBundledRuntimeDepsNpmRunner({ + env: { + npm_execpath: "/home/runner/setup-pnpm/node_modules/.bin/pnpm.cjs", + }, + execPath, + existsSync: (candidate) => candidate === npmCliPath, + npmArgs: ["install", "acpx@0.5.3"], + platform: "linux", + }); + + expect(runner).toEqual({ + command: execPath, + args: [npmCliPath, "install", "acpx@0.5.3"], + }); + }); + it("falls back to npm.cmd through shell on Windows", () => { const runner = resolveBundledRuntimeDepsNpmRunner({ env: {}, diff --git a/src/plugins/bundled-runtime-deps.ts b/src/plugins/bundled-runtime-deps.ts index fb7f68b9bbd..a1b5679e2cc 100644 --- a/src/plugins/bundled-runtime-deps.ts +++ b/src/plugins/bundled-runtime-deps.ts @@ -233,6 +233,11 @@ function resolvePathEnvKey(env: NodeJS.ProcessEnv, platform: NodeJS.Platform): s return Object.keys(env).find((key) => key.toLowerCase() === "path") ?? "Path"; } +function isNpmCliPath(candidate: string): boolean { + const normalized = candidate.replaceAll("\\", "/").toLowerCase(); + return normalized.endsWith("/npm-cli.js") || normalized.endsWith("/npm/bin/npm-cli.js"); +} + export function resolveBundledRuntimeDepsNpmRunner(params: { npmArgs: string[]; env?: NodeJS.ProcessEnv; @@ -246,9 +251,10 @@ export function resolveBundledRuntimeDepsNpmRunner(params: { const platform = params.platform ?? process.platform; const pathImpl = platform === "win32" ? path.win32 : path.posix; const nodeDir = pathImpl.dirname(execPath); - const npmExecPath = normalizeOptionalLowercaseString(env.npm_execpath) + const rawNpmExecPath = normalizeOptionalLowercaseString(env.npm_execpath) ? env.npm_execpath : undefined; + const npmExecPath = rawNpmExecPath && isNpmCliPath(rawNpmExecPath) ? rawNpmExecPath : undefined; const npmCliCandidates = [ npmExecPath,