fix(tool): defaulted windows bash children to utf-8

Added Windows-only UTF-8 defaults for non-interactive bash child process environments when the inherited env does not already define encoding or locale values.

Added regression coverage for missing, inherited, per-command, and non-Windows env behavior.

Fixes #2701
This commit is contained in:
roboomp
2026-06-15 21:18:46 +00:00
parent e24f789de4
commit e4ab780345
4 changed files with 83 additions and 2 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed Windows bash-tool child processes defaulting interpreter pipe I/O to the ANSI codepage by adding UTF-8 encoding defaults when the inherited environment is unset ([#2701](https://github.com/can1357/oh-my-pi/issues/2701)).
## [16.0.1] - 2026-06-15
### Breaking Changes
@@ -11,7 +11,7 @@ import { Settings, type ShellMinimizerSettings } from "../config/settings";
import { OutputSink } from "../session/streaming-output";
import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../tools/output-meta";
import { getOrCreateSnapshot } from "../utils/shell-snapshot";
import { NON_INTERACTIVE_ENV } from "./non-interactive-env";
import { buildNonInteractiveEnv } from "./non-interactive-env";
export interface BashExecutorOptions {
cwd?: string;
@@ -184,7 +184,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
const minimizer = buildMinimizerOptions(settings.getGroup("shellMinimizer"));
const commandCwd = await resolveShellCwd(options?.cwd);
const commandEnv = options?.env ? { ...NON_INTERACTIVE_ENV, ...options.env } : NON_INTERACTIVE_ENV;
const commandEnv = buildNonInteractiveEnv(options?.env);
// Apply command prefix if configured
const prefixedCommand = prefix ? `${prefix} ${command}` : command;
@@ -46,3 +46,45 @@ export const NON_INTERACTIVE_ENV: Readonly<Record<string, string>> = {
COMPOSER_NO_INTERACTION: "1",
CLOUDSDK_CORE_DISABLE_PROMPTS: "1",
};
const WINDOWS_UTF8_ENV_DEFAULTS: Readonly<Record<string, string>> = {
PYTHONIOENCODING: "utf-8",
PYTHONUTF8: "1",
LANG: "C.UTF-8",
LC_ALL: "C.UTF-8",
};
function hasEnvValue(
env: Record<string, string | undefined> | undefined,
key: string,
platform: NodeJS.Platform,
): boolean {
if (!env) return false;
if (platform !== "win32") return env[key] !== undefined;
for (const [existingKey, value] of Object.entries(env)) {
if (value !== undefined && existingKey.toLowerCase() === key.toLowerCase()) {
return true;
}
}
return false;
}
/** Builds the per-command environment for non-interactive child processes. */
export function buildNonInteractiveEnv(
overrides?: Record<string, string>,
baseEnv: Record<string, string | undefined> = Bun.env,
platform: NodeJS.Platform = process.platform,
): Record<string, string> {
if (platform !== "win32") {
return overrides ? { ...NON_INTERACTIVE_ENV, ...overrides } : NON_INTERACTIVE_ENV;
}
const env: Record<string, string> = { ...NON_INTERACTIVE_ENV };
for (const [key, value] of Object.entries(WINDOWS_UTF8_ENV_DEFAULTS)) {
if (!hasEnvValue(baseEnv, key, platform) && !hasEnvValue(overrides, key, platform)) {
env[key] = value;
}
}
return overrides ? { ...env, ...overrides } : env;
}
@@ -0,0 +1,35 @@
import { describe, expect, it } from "bun:test";
import { buildNonInteractiveEnv } from "@oh-my-pi/pi-coding-agent/exec/non-interactive-env";
describe("buildNonInteractiveEnv", () => {
it("defaults Windows child-process encoding to UTF-8 when inherited env is unset", () => {
const env = buildNonInteractiveEnv(undefined, {}, "win32");
expect(env.PYTHONIOENCODING).toBe("utf-8");
expect(env.PYTHONUTF8).toBe("1");
expect(env.LANG).toBe("C.UTF-8");
expect(env.LC_ALL).toBe("C.UTF-8");
});
it("preserves inherited and per-command Windows encoding overrides", () => {
const env = buildNonInteractiveEnv(
{ PYTHONUTF8: "0", LC_ALL: "en_US.UTF-8" },
{ pythonioencoding: "cp1252", LANG: "de_DE.UTF-8" },
"win32",
);
expect(env.PYTHONIOENCODING).toBeUndefined();
expect(env.PYTHONUTF8).toBe("0");
expect(env.LANG).toBeUndefined();
expect(env.LC_ALL).toBe("en_US.UTF-8");
});
it("does not force UTF-8 encoding defaults on non-Windows platforms", () => {
const env = buildNonInteractiveEnv(undefined, {}, "linux");
expect(env.PYTHONIOENCODING).toBeUndefined();
expect(env.PYTHONUTF8).toBeUndefined();
expect(env.LANG).toBeUndefined();
expect(env.LC_ALL).toBeUndefined();
});
});