mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
fix(shell): prefer PowerShell 7 on Windows with tested fallbacks (#25684)
This commit is contained in:
@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- iMessage/Reasoning safety: harden iMessage echo suppression with outbound `messageId` matching (plus scoped text fallback), and enforce reasoning-payload suppression on routed outbound delivery paths to prevent hidden thinking text from being sent as user-visible channel messages. (#25897, #1649, #25757) Thanks @rmarr and @Iranb.
|
||||
- Windows/Media safety checks: align async local-file identity validation with sync-safe-open behavior by treating win32 `dev=0` stats as unknown-device fallbacks (while keeping strict dev checks when both sides are non-zero), fixing false `Local media path is not safe to read` drops for local attachments/TTS/images. (#25708, #21989, #25699, #25878) Thanks @kevinWangSheng.
|
||||
- Windows/Exec shell selection: prefer PowerShell 7 (`pwsh`) discovery (Program Files, ProgramW6432, PATH) before falling back to Windows PowerShell 5.1, fixing `&&` command chaining failures on Windows hosts with PS7 installed. (#25684, #25638) Thanks @zerone0x.
|
||||
- macOS/WebChat panel: fix rounded-corner clipping by using panel-specific visual-effect blending and matching corner masking on both effect and hosting layers. (#22458) Thanks @apethree and @agisilaos.
|
||||
- macOS/IME input: when marked text is active, treat Return as IME candidate confirmation first in both the voice overlay composer and shared chat composer to prevent accidental sends while composing CJK text. (#25178) Thanks @bottotl.
|
||||
- macOS/Voice wake routing: default forwarded voice-wake transcripts to the `webchat` channel (instead of ambiguous `last` routing) so local voice prompts stay pinned to the control chat surface unless explicitly overridden. (#25440) Thanks @chilu18.
|
||||
|
||||
@@ -3,7 +3,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { captureEnv } from "../test-utils/env.js";
|
||||
import { getShellConfig, resolveShellFromPath } from "./shell-utils.js";
|
||||
import { getShellConfig, resolvePowerShellPath, resolveShellFromPath } from "./shell-utils.js";
|
||||
|
||||
const isWin = process.platform === "win32";
|
||||
|
||||
@@ -42,7 +42,8 @@ describe("getShellConfig", () => {
|
||||
if (isWin) {
|
||||
it("uses PowerShell on Windows", () => {
|
||||
const { shell } = getShellConfig();
|
||||
expect(shell.toLowerCase()).toContain("powershell");
|
||||
const normalized = shell.toLowerCase();
|
||||
expect(normalized.includes("powershell") || normalized.includes("pwsh")).toBe(true);
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -113,3 +114,96 @@ describe("resolveShellFromPath", () => {
|
||||
expect(resolveShellFromPath("bash")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolvePowerShellPath", () => {
|
||||
let envSnapshot: ReturnType<typeof captureEnv>;
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
envSnapshot = captureEnv([
|
||||
"ProgramFiles",
|
||||
"PROGRAMFILES",
|
||||
"ProgramW6432",
|
||||
"SystemRoot",
|
||||
"WINDIR",
|
||||
"PATH",
|
||||
]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
envSnapshot.restore();
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("prefers PowerShell 7 in ProgramFiles", () => {
|
||||
const base = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-pfiles-"));
|
||||
tempDirs.push(base);
|
||||
const pwsh7Dir = path.join(base, "PowerShell", "7");
|
||||
fs.mkdirSync(pwsh7Dir, { recursive: true });
|
||||
const pwsh7Path = path.join(pwsh7Dir, "pwsh.exe");
|
||||
fs.writeFileSync(pwsh7Path, "");
|
||||
|
||||
process.env.ProgramFiles = base;
|
||||
process.env.PATH = "";
|
||||
delete process.env.ProgramW6432;
|
||||
delete process.env.SystemRoot;
|
||||
delete process.env.WINDIR;
|
||||
|
||||
expect(resolvePowerShellPath()).toBe(pwsh7Path);
|
||||
});
|
||||
|
||||
it("prefers ProgramW6432 PowerShell 7 when ProgramFiles lacks pwsh", () => {
|
||||
const programFiles = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-pfiles-"));
|
||||
const programW6432 = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-pw6432-"));
|
||||
tempDirs.push(programFiles, programW6432);
|
||||
const pwsh7Dir = path.join(programW6432, "PowerShell", "7");
|
||||
fs.mkdirSync(pwsh7Dir, { recursive: true });
|
||||
const pwsh7Path = path.join(pwsh7Dir, "pwsh.exe");
|
||||
fs.writeFileSync(pwsh7Path, "");
|
||||
|
||||
process.env.ProgramFiles = programFiles;
|
||||
process.env.ProgramW6432 = programW6432;
|
||||
process.env.PATH = "";
|
||||
delete process.env.SystemRoot;
|
||||
delete process.env.WINDIR;
|
||||
|
||||
expect(resolvePowerShellPath()).toBe(pwsh7Path);
|
||||
});
|
||||
|
||||
it("finds pwsh on PATH when not in standard install locations", () => {
|
||||
const programFiles = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-pfiles-"));
|
||||
const binDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-bin-"));
|
||||
tempDirs.push(programFiles, binDir);
|
||||
const pwshPath = path.join(binDir, "pwsh");
|
||||
fs.writeFileSync(pwshPath, "");
|
||||
fs.chmodSync(pwshPath, 0o755);
|
||||
|
||||
process.env.ProgramFiles = programFiles;
|
||||
process.env.PATH = binDir;
|
||||
delete process.env.ProgramW6432;
|
||||
delete process.env.SystemRoot;
|
||||
delete process.env.WINDIR;
|
||||
|
||||
expect(resolvePowerShellPath()).toBe(pwshPath);
|
||||
});
|
||||
|
||||
it("falls back to Windows PowerShell 5.1 path when pwsh is unavailable", () => {
|
||||
const programFiles = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-pfiles-"));
|
||||
const sysRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sysroot-"));
|
||||
tempDirs.push(programFiles, sysRoot);
|
||||
const ps51Dir = path.join(sysRoot, "System32", "WindowsPowerShell", "v1.0");
|
||||
fs.mkdirSync(ps51Dir, { recursive: true });
|
||||
const ps51Path = path.join(ps51Dir, "powershell.exe");
|
||||
fs.writeFileSync(ps51Path, "");
|
||||
|
||||
process.env.ProgramFiles = programFiles;
|
||||
process.env.SystemRoot = sysRoot;
|
||||
process.env.PATH = "";
|
||||
delete process.env.ProgramW6432;
|
||||
delete process.env.WINDIR;
|
||||
|
||||
expect(resolvePowerShellPath()).toBe(ps51Path);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,27 @@ import { spawn } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
function resolvePowerShellPath(): string {
|
||||
export function resolvePowerShellPath(): string {
|
||||
// Prefer PowerShell 7 when available; PS 5.1 lacks "&&" support.
|
||||
const programFiles = process.env.ProgramFiles || process.env.PROGRAMFILES || "C:\\Program Files";
|
||||
const pwsh7 = path.join(programFiles, "PowerShell", "7", "pwsh.exe");
|
||||
if (fs.existsSync(pwsh7)) {
|
||||
return pwsh7;
|
||||
}
|
||||
|
||||
const programW6432 = process.env.ProgramW6432;
|
||||
if (programW6432 && programW6432 !== programFiles) {
|
||||
const pwsh7Alt = path.join(programW6432, "PowerShell", "7", "pwsh.exe");
|
||||
if (fs.existsSync(pwsh7Alt)) {
|
||||
return pwsh7Alt;
|
||||
}
|
||||
}
|
||||
|
||||
const pwshInPath = resolveShellFromPath("pwsh");
|
||||
if (pwshInPath) {
|
||||
return pwshInPath;
|
||||
}
|
||||
|
||||
const systemRoot = process.env.SystemRoot || process.env.WINDIR;
|
||||
if (systemRoot) {
|
||||
const candidate = path.join(
|
||||
|
||||
Reference in New Issue
Block a user