fix(server): resolve claude from current windows path
This commit is contained in:
@@ -134,6 +134,40 @@ describe("applyProviderEnv", () => {
|
||||
});
|
||||
|
||||
describe("findExecutable", () => {
|
||||
test("on Windows, resolves executables using current machine and user PATH entries", () => {
|
||||
findExecutableDependencies.platform = vi.fn(() => "win32");
|
||||
process.env.Path = "C:\\Windows\\System32";
|
||||
findExecutableDependencies.execFileSync.mockImplementation(
|
||||
((command: string, args?: string[]) => {
|
||||
if (command === "powershell") {
|
||||
return "C:\\Windows\\System32\r\nC:\\Users\\boudr\\.local\\bin\r\n";
|
||||
}
|
||||
if (command === "where.exe") {
|
||||
return "C:\\Users\\boudr\\.local\\bin\\claude.exe\r\n";
|
||||
}
|
||||
throw new Error(`unexpected command ${command}`);
|
||||
}) as any,
|
||||
);
|
||||
|
||||
expect(findExecutable("claude", findExecutableDependencies)).toBe(
|
||||
"C:\\Users\\boudr\\.local\\bin\\claude.exe",
|
||||
);
|
||||
const powershellCall = findExecutableDependencies.execFileSync.mock.calls[0];
|
||||
expect(powershellCall?.[0]).toBe("powershell");
|
||||
expect(powershellCall?.[1]).toContain("-NoProfile");
|
||||
expect(powershellCall?.[1]).toContain("-NonInteractive");
|
||||
expect(powershellCall?.[1]).toContain(
|
||||
'$machine = [Environment]::GetEnvironmentVariable("Path", "Machine"); $user = [Environment]::GetEnvironmentVariable("Path", "User"); if ($machine) { Write-Output $machine }; if ($user) { Write-Output $user }',
|
||||
);
|
||||
const whereCall = findExecutableDependencies.execFileSync.mock.calls[1];
|
||||
expect(whereCall?.[0]).toBe("where.exe");
|
||||
expect(whereCall?.[1]).toEqual(["claude"]);
|
||||
expect(whereCall?.[2]?.encoding).toBe("utf8");
|
||||
const env = whereCall?.[2]?.env as Record<string, string | undefined>;
|
||||
expect(env.PATH).toContain("C:\\Users\\boudr\\.local\\bin");
|
||||
expect(env.Path).toContain("C:\\Users\\boudr\\.local\\bin");
|
||||
});
|
||||
|
||||
test("uses the last line from login-shell which output", () => {
|
||||
findExecutableDependencies.shell = "/bin/zsh";
|
||||
findExecutableDependencies.execSync.mockReturnValue(
|
||||
|
||||
@@ -64,6 +64,33 @@ interface FindExecutableDependencies {
|
||||
shell: string | undefined;
|
||||
}
|
||||
|
||||
function resolveWindowsPathEntries(deps: FindExecutableDependencies): string[] {
|
||||
try {
|
||||
const output = deps.execFileSync(
|
||||
"powershell",
|
||||
[
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-Command",
|
||||
[
|
||||
'$machine = [Environment]::GetEnvironmentVariable("Path", "Machine")',
|
||||
'$user = [Environment]::GetEnvironmentVariable("Path", "User")',
|
||||
"if ($machine) { Write-Output $machine }",
|
||||
"if ($user) { Write-Output $user }",
|
||||
].join("; "),
|
||||
],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
return output
|
||||
.split(/\r?\n/)
|
||||
.flatMap((line) => line.split(";"))
|
||||
.map((entry) => entry.trim())
|
||||
.filter((entry) => entry.length > 0);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function resolveExecutableFromWhichOutput(
|
||||
name: string,
|
||||
output: string,
|
||||
@@ -186,7 +213,21 @@ export function findExecutable(
|
||||
|
||||
if (deps.platform() === "win32") {
|
||||
try {
|
||||
const out = deps.execSync(`where.exe ${trimmed}`, { encoding: "utf8" }).trim();
|
||||
const inheritedPath = process.env["Path"] ?? process.env["PATH"] ?? "";
|
||||
const resolvedPath = [
|
||||
...inheritedPath.split(";"),
|
||||
...resolveWindowsPathEntries(deps),
|
||||
]
|
||||
.map((entry) => entry.trim())
|
||||
.filter((entry) => entry.length > 0)
|
||||
.filter((entry, index, entries) => entries.indexOf(entry) === index)
|
||||
.join(";");
|
||||
const env = {
|
||||
...process.env,
|
||||
PATH: resolvedPath,
|
||||
Path: resolvedPath,
|
||||
};
|
||||
const out = deps.execFileSync("where.exe", [trimmed], { encoding: "utf8", env }).trim();
|
||||
const firstLine = out.split(/\r?\n/)[0]?.trim();
|
||||
return firstLine || null;
|
||||
} catch {
|
||||
|
||||
@@ -258,6 +258,7 @@ type ClaudeOptionsLogSummary = {
|
||||
hasCanUseTool: boolean;
|
||||
hasSpawnOverride: boolean;
|
||||
hasStderrHandler: boolean;
|
||||
pathToClaudeCodeExecutable: string | null;
|
||||
};
|
||||
|
||||
const MAX_RECENT_STDERR_CHARS = 4000;
|
||||
@@ -301,6 +302,10 @@ function summarizeClaudeOptionsForLog(options: ClaudeOptions): ClaudeOptionsLogS
|
||||
hasCanUseTool: typeof options.canUseTool === "function",
|
||||
hasSpawnOverride: typeof options.spawnClaudeCodeProcess === "function",
|
||||
hasStderrHandler: typeof options.stderr === "function",
|
||||
pathToClaudeCodeExecutable:
|
||||
typeof options.pathToClaudeCodeExecutable === "string"
|
||||
? options.pathToClaudeCodeExecutable
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1863,6 +1868,22 @@ class ClaudeAgentSession implements AgentSession {
|
||||
.join("\n\n");
|
||||
|
||||
const claudeBinary = findExecutable("claude");
|
||||
this.logger.debug(
|
||||
{
|
||||
claudeBinary,
|
||||
pathEnvKey:
|
||||
process.env["Path"] !== undefined
|
||||
? "Path"
|
||||
: process.env["PATH"] !== undefined
|
||||
? "PATH"
|
||||
: null,
|
||||
pathIncludesClaudeLocalBin:
|
||||
(process.env["Path"] ?? process.env["PATH"] ?? "")
|
||||
.toLowerCase()
|
||||
.includes("\\.local\\bin"),
|
||||
},
|
||||
"Resolved Claude executable",
|
||||
);
|
||||
const base: ClaudeOptions = {
|
||||
cwd: this.config.cwd,
|
||||
includePartialMessages: true,
|
||||
|
||||
Reference in New Issue
Block a user