Skip to content

Commit 8d5aa07

Browse files
author
ysmla
committed
fix: force UTF-8 output encoding for PowerShell on Windows
Windows PowerShell 5.1 encodes its stdout with the legacy OEM code page (e.g. CP936 on zh-CN systems) when output is redirected, while opencode decodes process output as UTF-8, producing mojibake for any non-ASCII shell output. Prepend a [Console]::OutputEncoding/$OutputEncoding UTF-8 preamble to PowerShell commands spawned by the shell tool and bang commands on Windows. Permission scanning and command titles still use the original command text since the preamble is injected at spawn time. Corresponds to upstream discussion in anomalyco#23636.
1 parent e03db9b commit 8d5aa07

3 files changed

Lines changed: 39 additions & 7 deletions

File tree

‎packages/core/src/shell.ts‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,17 @@ function info(file: string): Item {
163163
}
164164
}
165165

166+
// Windows PowerShell 5.1 encodes its own output with the legacy OEM code page
167+
// (e.g. CP936 on zh-CN systems) when stdout is redirected, while opencode
168+
// decodes process output as UTF-8. Force UTF-8 before the user command runs.
169+
const PS_UTF8_PREAMBLE =
170+
"[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new(); $OutputEncoding = [System.Text.UTF8Encoding]::new(); "
171+
172+
export function psCommand(command: string) {
173+
if (process.platform !== "win32") return command
174+
return PS_UTF8_PREAMBLE + command
175+
}
176+
166177
export function args(file: string, command: string, cwd: string) {
167178
const n = name(file)
168179
if (n === "nu" || n === "fish") return ["-c", command]
@@ -195,7 +206,7 @@ export function args(file: string, command: string, cwd: string) {
195206
]
196207
}
197208
if (n === "cmd") return ["/c", command]
198-
if (ps(file)) return ["-NoProfile", "-Command", command]
209+
if (ps(file)) return ["-NoProfile", "-Command", psCommand(command)]
199210
return ["-c", command]
200211
}
201212

‎packages/core/test/shell.test.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,23 @@ describe("shell", () => {
6363
expect(zsh.at(-1)).toBe("/tmp")
6464
})
6565

66+
test("psCommand wraps command with UTF-8 preamble on Windows", () => {
67+
const result = Shell.psCommand("echo hi")
68+
if (process.platform === "win32") {
69+
expect(result.startsWith("[Console]::OutputEncoding")).toBe(true)
70+
expect(result.endsWith("echo hi")).toBe(true)
71+
} else {
72+
expect(result).toBe("echo hi")
73+
}
74+
})
75+
76+
test("applies psCommand to PowerShell args", () => {
77+
const psArgs = Shell.args("powershell", "echo hi", "C:\\tmp")
78+
expect(psArgs[0]).toBe("-NoProfile")
79+
expect(psArgs[1]).toBe("-Command")
80+
expect(String(psArgs.at(-1))).toContain("echo hi")
81+
})
82+
6683
if (process.platform === "win32") {
6784
test("rejects blacklisted shells case-insensitively", async () => {
6885
await withShell("NU.EXE", async () => {

‎packages/opencode/src/tool/shell.ts‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -292,12 +292,16 @@ const ask = Effect.fn("ShellTool.ask")(function* (ctx: Tool.Context, scan: Scan,
292292

293293
function cmd(shell: string, command: string, cwd: string, env: NodeJS.ProcessEnv) {
294294
if (process.platform === "win32" && Shell.ps(shell)) {
295-
return ChildProcess.make(shell, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", command], {
296-
cwd,
297-
env,
298-
stdin: "ignore",
299-
detached: false,
300-
})
295+
return ChildProcess.make(
296+
shell,
297+
["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", Shell.psCommand(command)],
298+
{
299+
cwd,
300+
env,
301+
stdin: "ignore",
302+
detached: false,
303+
},
304+
)
301305
}
302306

303307
return ChildProcess.make(command, [], {

0 commit comments

Comments
 (0)