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:
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user