fix(server): resolve claude from current windows path
This commit is contained in:
@@ -134,6 +134,40 @@ describe("applyProviderEnv", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("findExecutable", () => {
|
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", () => {
|
test("uses the last line from login-shell which output", () => {
|
||||||
findExecutableDependencies.shell = "/bin/zsh";
|
findExecutableDependencies.shell = "/bin/zsh";
|
||||||
findExecutableDependencies.execSync.mockReturnValue(
|
findExecutableDependencies.execSync.mockReturnValue(
|
||||||
|
|||||||
@@ -64,6 +64,33 @@ interface FindExecutableDependencies {
|
|||||||
shell: string | undefined;
|
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(
|
function resolveExecutableFromWhichOutput(
|
||||||
name: string,
|
name: string,
|
||||||
output: string,
|
output: string,
|
||||||
@@ -186,7 +213,21 @@ export function findExecutable(
|
|||||||
|
|
||||||
if (deps.platform() === "win32") {
|
if (deps.platform() === "win32") {
|
||||||
try {
|
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();
|
const firstLine = out.split(/\r?\n/)[0]?.trim();
|
||||||
return firstLine || null;
|
return firstLine || null;
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -258,6 +258,7 @@ type ClaudeOptionsLogSummary = {
|
|||||||
hasCanUseTool: boolean;
|
hasCanUseTool: boolean;
|
||||||
hasSpawnOverride: boolean;
|
hasSpawnOverride: boolean;
|
||||||
hasStderrHandler: boolean;
|
hasStderrHandler: boolean;
|
||||||
|
pathToClaudeCodeExecutable: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_RECENT_STDERR_CHARS = 4000;
|
const MAX_RECENT_STDERR_CHARS = 4000;
|
||||||
@@ -301,6 +302,10 @@ function summarizeClaudeOptionsForLog(options: ClaudeOptions): ClaudeOptionsLogS
|
|||||||
hasCanUseTool: typeof options.canUseTool === "function",
|
hasCanUseTool: typeof options.canUseTool === "function",
|
||||||
hasSpawnOverride: typeof options.spawnClaudeCodeProcess === "function",
|
hasSpawnOverride: typeof options.spawnClaudeCodeProcess === "function",
|
||||||
hasStderrHandler: typeof options.stderr === "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");
|
.join("\n\n");
|
||||||
|
|
||||||
const claudeBinary = findExecutable("claude");
|
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 = {
|
const base: ClaudeOptions = {
|
||||||
cwd: this.config.cwd,
|
cwd: this.config.cwd,
|
||||||
includePartialMessages: true,
|
includePartialMessages: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user