fix(windows): broken PATH propagation, ps usage, and claude binary resolution
- sherpa-runtime-env: use case-insensitive key lookup when modifying PATH
on plain env objects. On Windows, `{...process.env}` stores PATH as
`Path` but `applySherpaLoaderEnv` used hardcoded `"PATH"`, creating a
duplicate key that could shadow the real system PATH in child processes.
- runtime-toolchain: use `wmic` on Windows instead of Unix-only `ps -o`
to resolve the node executable path from a PID.
- claude-agent: pass `findExecutable("claude")` as
`pathToClaudeCodeExecutable` so the SDK uses the user's installed
binary when available. Update spawn hook to only replace bare
"node"/"bun" with process.execPath, preserving native binary paths.
- desktop/paseo.cmd: use ELECTRON_RUN_AS_NODE with node-entrypoint-runner
for the Windows CLI wrapper.
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { spawnSync } from "node:child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { platform } from "node:os";
|
||||||
|
|
||||||
export interface NodePathFromPidResult {
|
export interface NodePathFromPidResult {
|
||||||
nodePath: string | null;
|
nodePath: string | null;
|
||||||
@@ -12,17 +13,14 @@ function normalizeError(error: unknown): string {
|
|||||||
return String(error);
|
return String(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveNodePathFromPid(pid: number): NodePathFromPidResult {
|
function resolveNodePathFromPidUnix(pid: number): NodePathFromPidResult {
|
||||||
const result = spawnSync("ps", ["-o", "comm=", "-p", String(pid)], {
|
const result = spawnSync("ps", ["-o", "comm=", "-p", String(pid)], {
|
||||||
encoding: "utf8",
|
encoding: "utf8",
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
return {
|
return { nodePath: null, error: `ps failed: ${normalizeError(result.error)}` };
|
||||||
nodePath: null,
|
|
||||||
error: `ps failed: ${normalizeError(result.error)}`,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((result.status ?? 1) !== 0) {
|
if ((result.status ?? 1) !== 0) {
|
||||||
@@ -34,12 +32,36 @@ export function resolveNodePathFromPid(pid: number): NodePathFromPidResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resolved = result.stdout.trim();
|
const resolved = result.stdout.trim();
|
||||||
if (!resolved) {
|
return resolved ? { nodePath: resolved } : { nodePath: null, error: "ps returned an empty command path" };
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveNodePathFromPidWindows(pid: number): NodePathFromPidResult {
|
||||||
|
const result = spawnSync(
|
||||||
|
"wmic",
|
||||||
|
["process", "where", `ProcessId=${pid}`, "get", "ExecutablePath", "/VALUE"],
|
||||||
|
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
return { nodePath: null, error: `wmic failed: ${normalizeError(result.error)}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((result.status ?? 1) !== 0) {
|
||||||
|
const details = result.stderr?.trim();
|
||||||
return {
|
return {
|
||||||
nodePath: null,
|
nodePath: null,
|
||||||
error: "ps returned an empty command path",
|
error: details ? `wmic failed: ${details}` : `wmic exited with code ${result.status ?? 1}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { nodePath: resolved };
|
// wmic output format: "ExecutablePath=C:\path\to\node.exe\r\n"
|
||||||
|
const match = result.stdout.match(/ExecutablePath=(.+)/);
|
||||||
|
const resolved = match?.[1]?.trim();
|
||||||
|
return resolved ? { nodePath: resolved } : { nodePath: null, error: "wmic returned no executable path" };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveNodePathFromPid(pid: number): NodePathFromPidResult {
|
||||||
|
return platform() === "win32"
|
||||||
|
? resolveNodePathFromPidWindows(pid)
|
||||||
|
: resolveNodePathFromPidUnix(pid);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,5 +9,6 @@ if not exist "%APP_EXECUTABLE%" (
|
|||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
set "PASEO_DESKTOP_CLI=1"
|
set "ELECTRON_RUN_AS_NODE=1"
|
||||||
"%APP_EXECUTABLE%" %*
|
"%APP_EXECUTABLE%" "%RESOURCES_DIR%\app.asar\dist\daemon\node-entrypoint-runner.js" bare "%RESOURCES_DIR%\app.asar\node_modules\@getpaseo\cli\dist\index.js" %*
|
||||||
|
exit /b %errorlevel%
|
||||||
|
|||||||
@@ -67,7 +67,11 @@ import type {
|
|||||||
McpServerConfig,
|
McpServerConfig,
|
||||||
PersistedAgentDescriptor,
|
PersistedAgentDescriptor,
|
||||||
} from "../agent-sdk-types.js";
|
} from "../agent-sdk-types.js";
|
||||||
import { applyProviderEnv, type ProviderRuntimeSettings } from "../provider-launch-config.js";
|
import {
|
||||||
|
applyProviderEnv,
|
||||||
|
findExecutable,
|
||||||
|
type ProviderRuntimeSettings,
|
||||||
|
} from "../provider-launch-config.js";
|
||||||
import { getOrchestratorModeInstructions } from "../orchestrator-instructions.js";
|
import { getOrchestratorModeInstructions } from "../orchestrator-instructions.js";
|
||||||
|
|
||||||
const fsPromises = promises;
|
const fsPromises = promises;
|
||||||
@@ -201,11 +205,14 @@ function applyRuntimeSettingsToClaudeOptions(
|
|||||||
...options,
|
...options,
|
||||||
spawnClaudeCodeProcess: (spawnOptions) => {
|
spawnClaudeCodeProcess: (spawnOptions) => {
|
||||||
const resolved = resolveClaudeSpawnCommand(spawnOptions, runtimeSettings);
|
const resolved = resolveClaudeSpawnCommand(spawnOptions, runtimeSettings);
|
||||||
// The SDK defaults to spawning "node" via PATH lookup, which fails when
|
// When the SDK passes a default JS runtime ("node"/"bun"), replace it with
|
||||||
// running from the managed runtime bundle where node isn't in PATH.
|
// process.execPath — the actual node binary running the daemon. This avoids
|
||||||
// Always use process.execPath — the actual node binary running the daemon.
|
// PATH lookup failures in the managed runtime bundle.
|
||||||
const command =
|
// When the SDK passes a native binary path (from pathToClaudeCodeExecutable)
|
||||||
resolved.command === spawnOptions.command ? process.execPath : resolved.command;
|
// or the user overrides the command via runtime settings, use that directly.
|
||||||
|
const isDefaultRuntime =
|
||||||
|
resolved.command === "node" || resolved.command === "bun";
|
||||||
|
const command = isDefaultRuntime ? process.execPath : resolved.command;
|
||||||
const child = spawn(command, resolved.args, {
|
const child = spawn(command, resolved.args, {
|
||||||
cwd: spawnOptions.cwd,
|
cwd: spawnOptions.cwd,
|
||||||
env: {
|
env: {
|
||||||
@@ -1855,12 +1862,14 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
.filter((entry): entry is string => typeof entry === "string" && entry.length > 0)
|
.filter((entry): entry is string => typeof entry === "string" && entry.length > 0)
|
||||||
.join("\n\n");
|
.join("\n\n");
|
||||||
|
|
||||||
|
const claudeBinary = findExecutable("claude");
|
||||||
const base: ClaudeOptions = {
|
const base: ClaudeOptions = {
|
||||||
cwd: this.config.cwd,
|
cwd: this.config.cwd,
|
||||||
includePartialMessages: true,
|
includePartialMessages: true,
|
||||||
permissionMode: this.currentMode,
|
permissionMode: this.currentMode,
|
||||||
agents: this.defaults?.agents,
|
agents: this.defaults?.agents,
|
||||||
canUseTool: this.handlePermissionRequest,
|
canUseTool: this.handlePermissionRequest,
|
||||||
|
...(claudeBinary ? { pathToClaudeCodeExecutable: claudeBinary } : {}),
|
||||||
// Use Claude Code preset system prompt and load CLAUDE.md files
|
// Use Claude Code preset system prompt and load CLAUDE.md files
|
||||||
// Append provider-agnostic system prompt and orchestrator instructions for agents.
|
// Append provider-agnostic system prompt and orchestrator instructions for agents.
|
||||||
systemPrompt: {
|
systemPrompt: {
|
||||||
|
|||||||
@@ -70,6 +70,21 @@ export function resolveSherpaLoaderEnv(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the actual case-sensitive key in a plain object that matches the given
|
||||||
|
* key case-insensitively. On Windows, `{...process.env}` produces a plain
|
||||||
|
* (case-sensitive) object where PATH is typically stored as `Path`. Using a
|
||||||
|
* hardcoded `"PATH"` would miss the existing key and create a duplicate,
|
||||||
|
* breaking the child process's PATH.
|
||||||
|
*/
|
||||||
|
function findEnvKey(env: NodeJS.ProcessEnv, key: string): string {
|
||||||
|
const lower = key.toLowerCase();
|
||||||
|
for (const k of Object.keys(env)) {
|
||||||
|
if (k.toLowerCase() === lower) return k;
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
export function applySherpaLoaderEnv(
|
export function applySherpaLoaderEnv(
|
||||||
env: NodeJS.ProcessEnv,
|
env: NodeJS.ProcessEnv,
|
||||||
platform: NodeJS.Platform = process.platform,
|
platform: NodeJS.Platform = process.platform,
|
||||||
@@ -90,9 +105,10 @@ export function applySherpaLoaderEnv(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const next = prependEnvPath(env[resolved.key], resolved.libDir);
|
const actualKey = findEnvKey(env, resolved.key);
|
||||||
const changed = next !== (env[resolved.key] ?? "");
|
const next = prependEnvPath(env[actualKey], resolved.libDir);
|
||||||
env[resolved.key] = next;
|
const changed = next !== (env[actualKey] ?? "");
|
||||||
|
env[actualKey] = next;
|
||||||
return {
|
return {
|
||||||
changed,
|
changed,
|
||||||
key: resolved.key,
|
key: resolved.key,
|
||||||
|
|||||||
Reference in New Issue
Block a user