mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-22 18:25:32 -04:00
fix(core): force UTF-8 for Windows shells
Force cmd, PowerShell, and piped Python subprocesses to emit UTF-8 so V2 shell output is decoded without CJK mojibake. Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
This commit is contained in:
@@ -173,6 +173,7 @@ export const layer = Layer.effect(
|
||||
const file = path.join(outputDir, `${id}.out`)
|
||||
const env = {
|
||||
...process.env,
|
||||
...ShellSelect.env(),
|
||||
TERM: "xterm-256color",
|
||||
OPENCODE_TERMINAL: "1",
|
||||
} as Record<string, string>
|
||||
|
||||
@@ -163,12 +163,33 @@ function info(file: string): Item {
|
||||
}
|
||||
}
|
||||
|
||||
export function env(platform = process.platform) {
|
||||
if (platform === "win32") return { PYTHONIOENCODING: "utf-8" }
|
||||
return {}
|
||||
}
|
||||
|
||||
export function args(file: string, command: string) {
|
||||
const n = name(file)
|
||||
if (n === "nu" || n === "fish") return ["-c", command]
|
||||
if (n === "zsh" || n === "bash") return ["-c", command]
|
||||
if (n === "cmd") return ["/c", command]
|
||||
if (ps(file)) return ["-NoProfile", "-Command", command]
|
||||
if (n === "cmd") return ["/d", "/c", `chcp 65001 >nul 2>nul & ${command}`]
|
||||
if (ps(file)) {
|
||||
const payload = Buffer.from(command, "utf8").toString("base64")
|
||||
const script = [
|
||||
"$utf8 = [System.Text.UTF8Encoding]::new($false)",
|
||||
"[Console]::InputEncoding = $utf8",
|
||||
"[Console]::OutputEncoding = $utf8",
|
||||
"$OutputEncoding = $utf8",
|
||||
`& ([scriptblock]::Create([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${payload}'))))`,
|
||||
].join("; ")
|
||||
return [
|
||||
"-NoLogo",
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-EncodedCommand",
|
||||
Buffer.from(script, "utf16le").toString("base64"),
|
||||
]
|
||||
}
|
||||
return ["-c", command]
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ const modelOutput = (output: Output): string | undefined => {
|
||||
// TODO: Port tree-sitter bash / PowerShell parser-based approval reduction.
|
||||
// TODO: Port BashArity reusable command-prefix approvals.
|
||||
// TODO: Replace token-based command-argument external-directory advisories with parser-based detection.
|
||||
// TODO: Restore PowerShell and cmd-specific invocation/path handling on Windows.
|
||||
// TODO: Restore Windows shell-specific path handling.
|
||||
// TODO: Add plugin shell.env environment augmentation once V2 plugin hooks exist.
|
||||
// TODO: Add durable/live progress metadata streaming for long-running commands once V2 tool invocation progress context is wired.
|
||||
// TODO: Persist job status and define restart recovery before exposing remote observation.
|
||||
|
||||
@@ -61,6 +61,23 @@ describe("shell", () => {
|
||||
expect(ShellSelect.args("/bin/bash", "echo hi")).toEqual(["-c", "echo hi"])
|
||||
})
|
||||
|
||||
test("switches cmd output to UTF-8", () => {
|
||||
expect(ShellSelect.args("cmd", 'echo "你好"')).toEqual(["/d", "/c", 'chcp 65001 >nul 2>nul & echo "你好"'])
|
||||
})
|
||||
|
||||
test("transports PowerShell commands independently of the active code page", () => {
|
||||
const args = ShellSelect.args("pwsh", 'Write-Output "你好"')
|
||||
expect(args.slice(0, 4)).toEqual(["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand"])
|
||||
expect(Buffer.from(args[4], "base64").toString("utf16le")).toBe(
|
||||
"$utf8 = [System.Text.UTF8Encoding]::new($false); [Console]::InputEncoding = $utf8; [Console]::OutputEncoding = $utf8; $OutputEncoding = $utf8; & ([scriptblock]::Create([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('V3JpdGUtT3V0cHV0ICLkvaDlpb0i'))))",
|
||||
)
|
||||
})
|
||||
|
||||
test("forces piped Python output to UTF-8 on Windows", () => {
|
||||
expect(ShellSelect.env("win32")).toEqual({ PYTHONIOENCODING: "utf-8" })
|
||||
expect(ShellSelect.env("linux")).toEqual({})
|
||||
})
|
||||
|
||||
if (process.platform === "win32") {
|
||||
test("rejects blacklisted shells case-insensitively", async () => {
|
||||
await withShell("NU.EXE", async () => {
|
||||
|
||||
@@ -570,7 +570,7 @@ test("keeps locked deferred parity TODOs visible", async () => {
|
||||
"Port tree-sitter bash / PowerShell parser-based approval reduction.",
|
||||
"Port BashArity reusable command-prefix approvals.",
|
||||
"Replace token-based command-argument external-directory advisories with parser-based detection.",
|
||||
"Restore PowerShell and cmd-specific invocation/path handling on Windows.",
|
||||
"Restore Windows shell-specific path handling.",
|
||||
"Add plugin shell.env environment augmentation once V2 plugin hooks exist.",
|
||||
"Add durable/live progress metadata streaming for long-running commands once V2 tool invocation progress context is wired.",
|
||||
"Persist job status and define restart recovery before exposing remote observation.",
|
||||
|
||||
Reference in New Issue
Block a user