diff --git a/CHANGELOG.md b/CHANGELOG.md index 0725bc44700..60155c797fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -116,6 +116,7 @@ Docs: https://docs.openclaw.ai - Install/Windows: run Git hook setup through a Node prepare helper so native Windows installs no longer print POSIX shell errors. - Checks/Windows: chunk and serialize extension oxlint shards on native Windows so changed gates avoid Go-backed linter memory spikes. - Release/Windows: run installed `openclaw.cmd` verification through explicit `cmd.exe` wrapping so npm prepublish/postpublish checks avoid Node shell-argv warnings. +- Plugins/Windows: run plugin npm package staging through the shared npm runner so native Windows release checks avoid bare `npm` lookup and `.cmd` shell-argv handling. - Checks/Windows: route full `pnpm check` stage commands through the managed child runner so Windows avoids Node shell-argv deprecation warnings there too. - Agents/fs: allow workspace-only host write/edit tools to write through in-workspace symlink directory parents while preserving outside-workspace symlink rejection. Fixes #84696. Thanks @garbagenetwork. - Checks/Windows: run managed child commands through explicit `cmd.exe` wrapping instead of Node shell mode with argv, avoiding Node 24 subprocess deprecation warnings during changed checks. diff --git a/scripts/lib/plugin-npm-package-manifest.mjs b/scripts/lib/plugin-npm-package-manifest.mjs index 3d6e295f687..eba47a2cdc7 100644 --- a/scripts/lib/plugin-npm-package-manifest.mjs +++ b/scripts/lib/plugin-npm-package-manifest.mjs @@ -8,6 +8,7 @@ import { listPluginNpmRuntimeBuildOutputs, resolvePluginNpmRuntimeBuildPlan, } from "./plugin-npm-runtime-build.mjs"; +import { resolveNpmRunner } from "../npm-runner.mjs"; const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA_PATH = "src/config/bundled-channel-config-metadata.generated.ts"; @@ -136,28 +137,26 @@ function listConfiguredBundledDependencyNames(packageJson) { return []; } -function npmInvocation() { - if (process.platform !== "win32") { - return { args: [], command: "npm" }; - } - const npmCliPath = path.join( - path.dirname(process.execPath), - "node_modules", - "npm", - "bin", - "npm-cli.js", - ); - if (fs.existsSync(npmCliPath)) { - return { args: [npmCliPath], command: process.execPath }; - } - return { args: [], command: "npm.cmd", shell: true }; +export function resolvePluginNpmCommand(args, params = {}) { + return resolveNpmRunner({ + comSpec: params.comSpec, + env: params.env, + execPath: params.execPath, + existsSync: params.existsSync, + npmArgs: args, + platform: params.platform, + }); } -function spawnNpmSync(args, options) { - const invocation = npmInvocation(); - return spawnSync(invocation.command, [...invocation.args, ...args], { +function spawnNpmSync(args, options = {}) { + const invocation = resolvePluginNpmCommand(args, { env: options.env ?? process.env }); + return spawnSync(invocation.command, invocation.args, { ...options, - ...(invocation.shell ? { shell: invocation.shell } : {}), + ...(invocation.env ? { env: invocation.env } : {}), + ...(invocation.shell !== undefined ? { shell: invocation.shell } : {}), + ...(invocation.windowsVerbatimArguments !== undefined + ? { windowsVerbatimArguments: invocation.windowsVerbatimArguments } + : {}), }); } diff --git a/test/plugin-npm-package-manifest.test.ts b/test/plugin-npm-package-manifest.test.ts index 11b692783b3..602b674b39b 100644 --- a/test/plugin-npm-package-manifest.test.ts +++ b/test/plugin-npm-package-manifest.test.ts @@ -1,10 +1,11 @@ import { spawnSync } from "node:child_process"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; -import { dirname, join } from "node:path"; +import { dirname, join, win32 } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { resolveAugmentedPluginNpmPackageJson, resolveAugmentedPluginNpmManifest, + resolvePluginNpmCommand, withAugmentedPluginNpmManifestForPackage, } from "../scripts/lib/plugin-npm-package-manifest.mjs"; import { cleanupTempDirs, makeTempRepoRoot, writeJsonFile } from "./helpers/temp-repo.js"; @@ -51,10 +52,16 @@ function writeFileText(filePath: string, text: string): void { } function listNpmPackDryRunFiles(packageDir: string): string[] { - const result = spawnSync("npm", ["pack", "--dry-run", "--json", "--ignore-scripts"], { + const invocation = resolvePluginNpmCommand(["pack", "--dry-run", "--json", "--ignore-scripts"]); + const result = spawnSync(invocation.command, invocation.args, { cwd: packageDir, encoding: "utf8", + ...(invocation.env ? { env: invocation.env } : {}), + ...(invocation.shell !== undefined ? { shell: invocation.shell } : {}), stdio: ["ignore", "pipe", "pipe"], + ...(invocation.windowsVerbatimArguments !== undefined + ? { windowsVerbatimArguments: invocation.windowsVerbatimArguments } + : {}), }); if (result.error) { throw result.error; @@ -132,6 +139,41 @@ function writeOptionalPlatformDependencyPackage(packageDir: string): string { } describe("plugin npm package manifest staging", () => { + it("wraps Windows npm.cmd staging through cmd.exe without shell mode", () => { + const nodeDir = "C:\\Program Files\\nodejs"; + const npmCmdPath = win32.resolve(nodeDir, "npm.cmd"); + + expect( + resolvePluginNpmCommand(["install", "--package-lock-only"], { + comSpec: "C:\\Windows\\System32\\cmd.exe", + env: { PATH: "C:\\bin" }, + execPath: win32.join(nodeDir, "node.exe"), + existsSync: (candidate: string) => candidate === npmCmdPath, + platform: "win32", + }), + ).toEqual({ + command: "C:\\Windows\\System32\\cmd.exe", + args: [ + "/d", + "/s", + "/c", + '""C:\\Program Files\\nodejs\\npm.cmd" install --package-lock-only"', + ], + shell: false, + windowsVerbatimArguments: true, + }); + }); + + it("rejects bare npm fallback on Windows plugin package staging", () => { + expect(() => + resolvePluginNpmCommand(["install"], { + execPath: "C:\\nodejs\\node.exe", + existsSync: () => false, + platform: "win32", + }), + ).toThrow("OpenClaw refuses to shell out to bare npm on Windows"); + }); + it("overlays generated channel configs while packing and restores source manifest", () => { const repoDir = makeTempRepoRoot(tempDirs, "openclaw-plugin-npm-package-manifest-"); const packageDir = join(repoDir, "extensions", "twitch");