diff --git a/CHANGELOG.md b/CHANGELOG.md index c3da626ac5d8..fb85a779009a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ Docs: https://docs.openclaw.ai - Discord: suppress a bot's previous reply body and referenced media from prompt context when a user replies to that bot message, while keeping reply metadata for routing. (#86238) Thanks @fuller-stack-dev. - Docker E2E: avoid rebuilding the Control UI twice while preparing the shared OpenClaw package tarball for package-backed scenario runs. - Tests: avoid rebuilding the Control UI twice during the installer Docker smoke now that `pnpm build` includes `ui:build`. +- Build: route `scripts/ui.js` through the shared pnpm runner and keep Control UI chunking helpers in sparse-included source so native Windows Corepack builds can produce `dist/control-ui`. - Tests: collect QA gateway CPU/RSS metrics on native Windows and give the channel baseline enough turn budget to report slow gateway runs instead of timing out before proof. - Install/update: bypass npm `min-release-age` policies with `--min-release-age=0` instead of `--before` so hosted installers keep working on npm versions that reject the combined config. (#84749) Thanks @TeodoroRodrigo. - WebChat: keep message-tool replies visible in the chat while still summarizing internal tool results for the model. Fixes #86347. Thanks @shakkernerd. diff --git a/scripts/ui.js b/scripts/ui.js index c7c4331984d6..e22b9ba729da 100644 --- a/scripts/ui.js +++ b/scripts/ui.js @@ -4,6 +4,7 @@ import fs from "node:fs"; import { createRequire } from "node:module"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { resolvePnpmRunner } from "./pnpm-runner.mjs"; import { buildCmdExeCommandLine } from "./windows-cmd-helpers.mjs"; const here = path.dirname(fileURLToPath(import.meta.url)); @@ -17,42 +18,6 @@ function usage() { process.stderr.write("Usage: node scripts/ui.js [...args]\n"); } -function which(cmd) { - try { - const key = process.platform === "win32" ? "Path" : "PATH"; - const paths = (process.env[key] ?? process.env.PATH ?? "") - .split(path.delimiter) - .filter(Boolean); - const extensions = - process.platform === "win32" - ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").filter(Boolean) - : [""]; - for (const entry of paths) { - for (const ext of extensions) { - const candidate = path.join(entry, process.platform === "win32" ? `${cmd}${ext}` : cmd); - try { - if (fs.existsSync(candidate)) { - return candidate; - } - } catch { - // ignore - } - } - } - } catch { - // ignore - } - return null; -} - -function resolveRunner() { - const pnpm = which("pnpm"); - if (pnpm) { - return { cmd: pnpm, kind: "pnpm" }; - } - return null; -} - export function shouldUseCmdExeForCommand(cmd, platform = process.platform) { if (platform !== "win32") { return false; @@ -89,19 +54,42 @@ export function resolveSpawnCall(cmd, args, envOverride, params = {}) { }; } -function run(cmd, args) { - const { command, args: spawnArgs, options } = resolveSpawnCall(cmd, args); +export function resolvePnpmSpawnCall(pnpmArgs, envOverride, params = {}) { + const env = envOverride ?? process.env; + const platform = params.platform ?? process.platform; + const runner = resolvePnpmRunner({ + pnpmArgs, + nodeExecPath: params.nodeExecPath ?? process.execPath, + npmExecPath: params.npmExecPath ?? env.npm_execpath, + comSpec: params.comSpec ?? env.ComSpec, + platform, + }); + return { + command: runner.command, + args: runner.args, + options: { + cwd: params.cwd ?? uiDir, + stdio: "inherit", + env, + shell: runner.shell, + windowsVerbatimArguments: runner.windowsVerbatimArguments, + }, + }; +} + +function runSpawnCall(spawnCall, label) { + const { command, args: spawnArgs, options } = spawnCall; let child; try { child = spawn(command, spawnArgs, options); } catch (err) { - console.error(`Failed to launch ${cmd}:`, err); + console.error(`Failed to launch ${label}:`, err); process.exit(1); return; } child.on("error", (err) => { - console.error(`Failed to launch ${cmd}:`, err); + console.error(`Failed to launch ${label}:`, err); process.exit(1); }); child.on("exit", (code) => { @@ -111,13 +99,21 @@ function run(cmd, args) { }); } -function runSync(cmd, args, envOverride) { - const { command, args: spawnArgs, options } = resolveSpawnCall(cmd, args, envOverride); +function run(cmd, args) { + runSpawnCall(resolveSpawnCall(cmd, args), cmd); +} + +function runPnpm(args, envOverride) { + runSpawnCall(resolvePnpmSpawnCall(args, envOverride), "pnpm"); +} + +function runSpawnCallSync(spawnCall, label) { + const { command, args: spawnArgs, options } = spawnCall; let result; try { result = spawnSync(command, spawnArgs, options); } catch (err) { - console.error(`Failed to launch ${cmd}:`, err); + console.error(`Failed to launch ${label}:`, err); process.exit(1); return; } @@ -129,6 +125,10 @@ function runSync(cmd, args, envOverride) { } } +function runPnpmSync(args, envOverride) { + runSpawnCallSync(resolvePnpmSpawnCall(args, envOverride), "pnpm"); +} + function depsInstalled(kind) { try { const require = createRequire(path.join(uiDir, "package.json")); @@ -179,24 +179,18 @@ export function main(argv = process.argv.slice(2)) { return; } - const runner = resolveRunner(); - if (!runner) { - process.stderr.write("Missing UI runner: install pnpm, then retry.\n"); - process.exit(1); - } - if (action === "install") { - run(runner.cmd, ["install", ...rest]); + runPnpm(["install", ...rest]); return; } if (!depsInstalled(action === "test" ? "test" : "build")) { const installEnv = process.env; const installArgs = ["install"]; - runSync(runner.cmd, installArgs, installEnv); + runPnpmSync(installArgs, installEnv); } - run(runner.cmd, ["run", script, ...rest]); + runPnpm(["run", script, ...rest]); } export function resolveDirectExecutionPath(entry, realpath = fs.realpathSync.native) { diff --git a/test/scripts/ui.test.ts b/test/scripts/ui.test.ts index 187a5e98046b..16056a8dbcc9 100644 --- a/test/scripts/ui.test.ts +++ b/test/scripts/ui.test.ts @@ -3,6 +3,7 @@ import path from "node:path"; import { describe, expect, it } from "vitest"; import { isDirectScriptExecution, + resolvePnpmSpawnCall, resolveSpawnCall, shouldUseCmdExeForCommand, } from "../../scripts/ui.js"; @@ -75,6 +76,42 @@ describe("scripts/ui windows spawn behavior", () => { ).toThrow(/unsafe windows cmd\.exe argument/i); }); + it("routes Windows Corepack pnpm entrypoints through node", () => { + expect( + resolvePnpmSpawnCall( + ["run", "build"], + { + npm_execpath: + "C:\\Users\\runner\\AppData\\Local\\node\\corepack\\v1\\pnpm\\11.2.2\\bin\\pnpm.mjs", + ComSpec: "C:\\Windows\\System32\\cmd.exe", + }, + { + cwd: "C:\\repo\\ui", + nodeExecPath: "C:\\Program Files\\nodejs\\node.exe", + platform: "win32", + }, + ), + ).toEqual({ + command: "C:\\Program Files\\nodejs\\node.exe", + args: [ + "C:\\Users\\runner\\AppData\\Local\\node\\corepack\\v1\\pnpm\\11.2.2\\bin\\pnpm.mjs", + "run", + "build", + ], + options: { + cwd: "C:\\repo\\ui", + stdio: "inherit", + env: { + npm_execpath: + "C:\\Users\\runner\\AppData\\Local\\node\\corepack\\v1\\pnpm\\11.2.2\\bin\\pnpm.mjs", + ComSpec: "C:\\Windows\\System32\\cmd.exe", + }, + shell: false, + windowsVerbatimArguments: undefined, + }, + }); + }); + it("keeps non-Windows launches direct even with shell metacharacters", () => { expect( resolveSpawnCall( diff --git a/ui/src/ui/control-ui-chunking.test.ts b/ui/src/ui/control-ui-chunking.test.ts index e8423a2b9d03..b6e15eb44474 100644 --- a/ui/src/ui/control-ui-chunking.test.ts +++ b/ui/src/ui/control-ui-chunking.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { controlUiManualChunk, normalizeModuleId } from "../../build/chunking.ts"; +import { controlUiManualChunk, normalizeModuleId } from "./control-ui-chunking.ts"; describe("Control UI build chunking", () => { it("groups stable runtime dependencies into bounded chunks", () => { diff --git a/ui/build/chunking.ts b/ui/src/ui/control-ui-chunking.ts similarity index 100% rename from ui/build/chunking.ts rename to ui/src/ui/control-ui-chunking.ts diff --git a/ui/vite.config.ts b/ui/vite.config.ts index 3700de38cde4..4ba88b94c2ba 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -3,7 +3,7 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { defineConfig, type Plugin } from "vite"; -import { controlUiManualChunk } from "./build/chunking.ts"; +import { controlUiManualChunk } from "./src/ui/control-ui-chunking.ts"; const here = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(here, "..");