Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 31 additions & 10 deletions __tests__/scripts/dev-safe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -655,24 +655,45 @@ describe("getOrCreatePersistedSessionApiKey", () => {
});

describe("buildNpmScriptCommand", () => {
it("reuses npm's own CLI path when available", () => {
it("runs npm through cmd.exe on Windows even when npm_execpath is set", () => {
// npm_execpath points to a path with spaces like
// "C:\Program Files\nodejs\node_modules\npm\bin\npm-cli.js".
// spawnService uses shell:true on Windows, so passing that path as an
// argument causes cmd.exe to split on the space and fail with
// "'C:\Program' is not recognized as an internal or external command".
// The win32 branch must fire BEFORE the npm_execpath branch.
const command = buildNpmScriptCommand(
"dev:frontend",
"win32",
{
npm_execpath: "C:\\nodejs\\node_modules\\npm\\bin\\npm-cli.js",
npm_node_execpath: "C:\\nodejs\\node.exe",
ComSpec: "C:\\Windows\\System32\\cmd.exe",
npm_execpath:
"C:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npm-cli.js",
npm_node_execpath: "C:\\Program Files\\nodejs\\node.exe",
},
"C:\\fallback\\node.exe",
"C:\\Program Files\\nodejs\\node.exe",
);

expect(command).toEqual({
command: "C:\\nodejs\\node.exe",
args: [
"C:\\nodejs\\node_modules\\npm\\bin\\npm-cli.js",
"run",
"dev:frontend",
],
command: "C:\\Windows\\System32\\cmd.exe",
args: ["/d", "/s", "/c", "npm", "run", "dev:frontend"],
});
});

it("reuses npm's own CLI path when available on POSIX", () => {
const command = buildNpmScriptCommand(
"dev:frontend",
"linux",
{
npm_execpath: "/usr/lib/node_modules/npm/bin/npm-cli.js",
npm_node_execpath: "/usr/bin/node",
},
"/fallback/node",
);

expect(command).toEqual({
command: "/usr/bin/node",
args: ["/usr/lib/node_modules/npm/bin/npm-cli.js", "run", "dev:frontend"],
});
});

Expand Down
30 changes: 24 additions & 6 deletions scripts/dev-safe.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,17 @@ function buildConfigFromPorts(ports, cwd, env) {
*/
export function buildAgentServerEnv(config) {
return {
// Force Python to use UTF-8 for all file I/O and streams.
//
// On Windows, Python defaults to the system ANSI codepage (e.g. cp1252).
// The agent-server writes conversation metadata JSON that can contain
// emoji (e.g. ✅ U+2705) which cp1252 cannot encode, producing:
// UnicodeEncodeError: 'charmap' codec can't encode character '\u2705'
// Setting PYTHONUTF8=1 enables Python's UTF-8 mode (PEP 540) for the
// entire agent-server process, matching the behaviour on Linux/macOS
// where the locale is already UTF-8.
// This is a no-op on Linux/macOS where the locale is already UTF-8.
PYTHONUTF8: "1",
TMUX_TMPDIR: config.tmuxTmpDir,
OH_CONVERSATIONS_PATH: config.conversationsPath,
OH_BASH_EVENTS_DIR: config.bashEventsDir,
Expand Down Expand Up @@ -753,17 +764,24 @@ export function buildNpmScriptCommand(
env = process.env,
nodeExecPath = process.execPath,
) {
if (env.npm_execpath) {
// On Windows, always use cmd.exe regardless of whether npm_execpath is set.
// npm_execpath points to a path like
// "C:\Program Files\nodejs\node_modules\npm\bin\npm-cli.js" which contains
// spaces. When that path is passed as an argument with shell:true in
// spawnService, cmd.exe splits on the space and tries to run "C:\Program"
// as a command, producing "not recognized as an internal or external command".
// Using "npm" via cmd.exe avoids the problem entirely.
if (platform === "win32") {
return {
command: env.npm_node_execpath || nodeExecPath,
args: [env.npm_execpath, "run", scriptName],
command: env.ComSpec || "cmd.exe",
args: ["/d", "/s", "/c", "npm", "run", scriptName],
};
}

if (platform === "win32") {
if (env.npm_execpath) {
return {
command: env.ComSpec || "cmd.exe",
args: ["/d", "/s", "/c", "npm", "run", scriptName],
command: env.npm_node_execpath || nodeExecPath,
args: [env.npm_execpath, "run", scriptName],
};
}

Expand Down
3 changes: 3 additions & 0 deletions scripts/dev-with-automation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,9 @@ function startAutomationBackend(config) {
{
cwd: config.stateDir,
env: {
// Force UTF-8 for all Python file I/O (same reason as agent-server;
// see buildAgentServerEnv in dev-safe.mjs).
PYTHONUTF8: "1",
// The URL the automation backend itself uses to call the
// agent-server's REST API (tarball upload + bash dispatch).
//
Expand Down
Loading