From d23ee2f7022707b7edc0ee0cb63e9874b70dd4b1 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 27 Apr 2026 08:30:59 +0100 Subject: [PATCH] fix: hide bundled runtime npm windows --- CHANGELOG.md | 1 + scripts/postinstall-bundled-plugins.mjs | 1 + scripts/stage-bundled-plugin-runtime-deps.mjs | 8 +++- src/plugins/bundled-runtime-deps.test.ts | 37 ++++++++++++++++++- src/plugins/bundled-runtime-deps.ts | 2 + .../postinstall-bundled-plugins.test.ts | 1 + .../stage-bundled-plugin-runtime-deps.test.ts | 26 +++++++++++++ 7 files changed, 74 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11003bf1588..69385c0f3c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai - Agents/bootstrap: refresh cached workspace bootstrap snapshots on long-lived main-session turns when `AGENTS.md`, `SOUL.md`, `MEMORY.md`, or `TOOLS.md` change on disk, while preserving unchanged snapshot identity through the workspace file cache. (#64871; related #43901, #26497, #28594, #30896) Thanks @aimqwest and @mikejuyoon. - macOS Gateway: detect installed-but-unloaded LaunchAgent split-brain states during status, doctor, and restart, and re-bootstrap launchd supervision before falling back to unmanaged listener restarts. Fixes #67335, #53475, and #71060; refs #58890, #60885, and #70801. Thanks @ze1tgeist88, @dafacto, and @vishutdhar. - Plugins/install: treat mirrored core logger dependencies as staged bundled runtime deps so packaged Gateway starts do not crash when the external plugin-runtime-deps root is missing `tslog`. Fixes #72228; supersedes #72493. Thanks @deepujain. +- Plugins/install: hide bundled runtime-dependency npm child windows on Windows across Gateway startup, postinstall, and packaged staging paths so Telegram/Anthropic dependency repair no longer flashes shell windows. Fixes #72315. Thanks @athuljayaram and @joshfeng. - Plugins/install: stage bundled plugin runtime dependencies before Gateway startup, drain update restarts, and materialize plugin-owned root chunks in external mirrors so staged deps resolve under native ESM. Fixes #72058; supersedes #72084. Thanks @amnesia106 and @drvoss. - TTS/SecretRef: resolve `messages.tts.providers.*.apiKey` from the active runtime snapshot so SecretRef-backed MiniMax and other TTS provider keys work in runtime reply/audio paths. Fixes #68690. Thanks @joshavant. - Gateway/install: surface systemd user-bus recovery hints during Linux service activation and retry via the machine user scope when `systemctl --user` reports no-medium bus failures. Fixes #39673; refs #44417 and #63561. Thanks @Arbor4, @myrsu, and @mssteuer. diff --git a/scripts/postinstall-bundled-plugins.mjs b/scripts/postinstall-bundled-plugins.mjs index c4917a0ca43..4d97e71dd5f 100644 --- a/scripts/postinstall-bundled-plugins.mjs +++ b/scripts/postinstall-bundled-plugins.mjs @@ -817,6 +817,7 @@ export function runBundledPluginPostinstall(params = {}) { encoding: "utf8", env: npmRunner.env ?? installEnv, stdio: "pipe", + windowsHide: true, shell: npmRunner.shell, windowsVerbatimArguments: npmRunner.windowsVerbatimArguments, }); diff --git a/scripts/stage-bundled-plugin-runtime-deps.mjs b/scripts/stage-bundled-plugin-runtime-deps.mjs index 56d44f1f44f..db8a5eafd7c 100644 --- a/scripts/stage-bundled-plugin-runtime-deps.mjs +++ b/scripts/stage-bundled-plugin-runtime-deps.mjs @@ -877,13 +877,15 @@ function runNpmInstall(params) { npm_config_save: "false", npm_config_yes: "true", }; - const result = spawnSync(params.npmRunner.command, params.npmRunner.args, { + const runSpawnSync = params.spawnSyncImpl ?? spawnSync; + const result = runSpawnSync(params.npmRunner.command, params.npmRunner.args, { cwd: params.cwd, encoding: "utf8", env: npmEnv, shell: params.npmRunner.shell, stdio: ["ignore", "pipe", "pipe"], timeout: params.timeoutMs ?? 5 * 60 * 1000, + windowsHide: true, windowsVerbatimArguments: params.npmRunner.windowsVerbatimArguments, }); if (result.status === 0) { @@ -1240,6 +1242,10 @@ export function stageBundledPluginRuntimeDeps(params = {}) { } } +export const __testing = { + runNpmInstall, +}; + if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { stageBundledPluginRuntimeDeps(); } diff --git a/src/plugins/bundled-runtime-deps.test.ts b/src/plugins/bundled-runtime-deps.test.ts index 857da9aa65b..12d5ce6e792 100644 --- a/src/plugins/bundled-runtime-deps.test.ts +++ b/src/plugins/bundled-runtime-deps.test.ts @@ -1,5 +1,6 @@ -import { spawnSync } from "node:child_process"; +import { spawn, spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; +import { EventEmitter } from "node:events"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; @@ -27,9 +28,11 @@ import { vi.mock("node:child_process", async (importOriginal) => ({ ...(await importOriginal()), + spawn: vi.fn(), spawnSync: vi.fn(), })); +const spawnMock = vi.mocked(spawn); const spawnSyncMock = vi.mocked(spawnSync); const tempDirs: string[] = []; @@ -91,6 +94,7 @@ function statfsFixture(params: { afterEach(() => { vi.restoreAllMocks(); + spawnMock.mockReset(); spawnSyncMock.mockReset(); bundledRuntimeDepsActivityTesting.resetBundledRuntimeDepsInstallActivity(); for (const dir of tempDirs.splice(0)) { @@ -312,6 +316,7 @@ describe("installBundledRuntimeDeps", () => { ["C:\\node\\node_modules\\npm\\bin\\npm-cli.js", "install", "--ignore-scripts", "acpx@0.5.3"], expect.objectContaining({ cwd: installRoot, + windowsHide: true, env: expect.objectContaining({ npm_config_legacy_peer_deps: "true", npm_config_package_lock: "false", @@ -330,6 +335,36 @@ describe("installBundledRuntimeDeps", () => { ); }); + it("hides async npm child windows for startup repair installs", async () => { + const installRoot = makeTempDir(); + spawnMock.mockImplementation((_command, _args, options) => { + writeInstalledPackage(String(options?.cwd ?? ""), "acpx", "0.5.3"); + const child = new EventEmitter() as ReturnType; + Object.assign(child, { + stdout: new EventEmitter(), + stderr: new EventEmitter(), + }); + queueMicrotask(() => child.emit("close", 0, null)); + return child; + }); + + await repairBundledRuntimeDepsInstallRootAsync({ + installRoot, + missingSpecs: ["acpx@0.5.3"], + installSpecs: ["acpx@0.5.3"], + env: {}, + }); + + expect(spawnMock).toHaveBeenCalledWith( + expect.any(String), + expect.any(Array), + expect.objectContaining({ + cwd: installRoot, + windowsHide: true, + }), + ); + }); + it("anchors non-isolated external install roots with a package manifest", () => { const parentRoot = makeTempDir(); const installRoot = path.join(parentRoot, ".openclaw", "plugin-runtime-deps", "openclaw-test"); diff --git a/src/plugins/bundled-runtime-deps.ts b/src/plugins/bundled-runtime-deps.ts index bd39ee6b7c1..6e8eff55cb0 100644 --- a/src/plugins/bundled-runtime-deps.ts +++ b/src/plugins/bundled-runtime-deps.ts @@ -1409,6 +1409,7 @@ async function spawnBundledRuntimeDepsInstall(params: { cwd: params.cwd, env: params.env, stdio: ["ignore", "pipe", "pipe"], + windowsHide: true, }); const stdout: Buffer[] = []; const stderr: Buffer[] = []; @@ -1480,6 +1481,7 @@ export function installBundledRuntimeDeps(params: { encoding: "utf8", env: npmRunner.env ?? installEnv, stdio: "pipe", + windowsHide: true, }); if (result.status !== 0 || result.error) { throw new Error(formatBundledRuntimeDepsInstallError(result)); diff --git a/test/scripts/postinstall-bundled-plugins.test.ts b/test/scripts/postinstall-bundled-plugins.test.ts index d32542c8d2e..34107e7262f 100644 --- a/test/scripts/postinstall-bundled-plugins.test.ts +++ b/test/scripts/postinstall-bundled-plugins.test.ts @@ -79,6 +79,7 @@ describe("bundled plugin postinstall", () => { }, shell: false, stdio: "pipe", + windowsHide: true, windowsVerbatimArguments: undefined, }); } diff --git a/test/scripts/stage-bundled-plugin-runtime-deps.test.ts b/test/scripts/stage-bundled-plugin-runtime-deps.test.ts index 378b83927a0..a03facfbf25 100644 --- a/test/scripts/stage-bundled-plugin-runtime-deps.test.ts +++ b/test/scripts/stage-bundled-plugin-runtime-deps.test.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { + __testing as stageBundledPluginRuntimeDepsTesting, collectRuntimeDependencyInstallManifest, collectRuntimeDependencyInstallSpecs, stageBundledPluginRuntimeDeps, @@ -129,6 +130,31 @@ describe("stageBundledPluginRuntimeDeps", () => { }); }); + it("hides npm child windows during fallback runtime installs", () => { + const spawnSyncImpl = vi.fn(() => ({ status: 0, stderr: "", stdout: "" })); + + stageBundledPluginRuntimeDepsTesting.runNpmInstall({ + cwd: "C:\\openclaw\\dist\\extensions\\telegram\\.openclaw-install-stage", + npmRunner: { + command: "npm.cmd", + args: ["install", "--silent"], + env: { PATH: "C:\\node" }, + shell: false, + windowsVerbatimArguments: true, + }, + spawnSyncImpl, + }); + + expect(spawnSyncImpl).toHaveBeenCalledWith( + "npm.cmd", + ["install", "--silent"], + expect.objectContaining({ + windowsHide: true, + windowsVerbatimArguments: true, + }), + ); + }); + it("skips restaging when runtime deps stamp matches the sanitized manifest", () => { const { pluginDir, repoRoot } = createBundledPluginFixture({ packageJson: {