mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-07 05:05:44 +00:00
fix(cli): resolve executable path for Claude CLI on Windows
This commit is contained in:
139
server/shared/claude-cli-path.ts
Normal file
139
server/shared/claude-cli-path.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const DEFAULT_CLAUDE_COMMAND = 'claude';
|
||||
const CLAUDE_SCRIPT_EXTENSIONS = new Set(['.cjs', '.js', '.jsx', '.mjs', '.ts', '.tsx']);
|
||||
const CLAUDE_WRAPPER_SEGMENTS = ['node_modules', '@anthropic-ai', 'claude-code', 'bin', 'claude.exe'] as const;
|
||||
|
||||
export type ResolveClaudeCodeExecutablePathDependencies = {
|
||||
execFileSync?: typeof execFileSync;
|
||||
existsSync?: typeof fs.existsSync;
|
||||
platform?: NodeJS.Platform;
|
||||
readFileSync?: typeof fs.readFileSync;
|
||||
};
|
||||
|
||||
function getPathApi(platform: NodeJS.Platform) {
|
||||
return platform === 'win32' ? path.win32 : path;
|
||||
}
|
||||
|
||||
function stripWrappingQuotes(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (
|
||||
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
||||
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
||||
) {
|
||||
return trimmed.slice(1, -1);
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function isPathLike(value: string): boolean {
|
||||
return value.includes('/') || value.includes('\\');
|
||||
}
|
||||
|
||||
function resolveClaudeWrapperBinary(
|
||||
wrapperPath: string,
|
||||
deps: Required<ResolveClaudeCodeExecutablePathDependencies>,
|
||||
): string | null {
|
||||
const pathApi = getPathApi(deps.platform);
|
||||
const directCandidate = pathApi.resolve(pathApi.dirname(wrapperPath), ...CLAUDE_WRAPPER_SEGMENTS);
|
||||
|
||||
if (deps.existsSync(directCandidate)) {
|
||||
return directCandidate;
|
||||
}
|
||||
|
||||
let content: string;
|
||||
try {
|
||||
content = deps.readFileSync(wrapperPath, 'utf8');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const matches = content.matchAll(/["']([^"'\\r\\n]*claude\.exe)["']/gi);
|
||||
for (const match of matches) {
|
||||
const rawTarget = match[1]
|
||||
.replace(/^\$basedir[\\/]/i, '')
|
||||
.replace(/^%dp0%[\\/]/i, '')
|
||||
.replace(/^%~dp0[\\/]/i, '');
|
||||
const normalizedTarget = rawTarget.replace(/[\\/]/g, pathApi.sep);
|
||||
const candidate = pathApi.isAbsolute(normalizedTarget)
|
||||
? normalizedTarget
|
||||
: pathApi.resolve(pathApi.dirname(wrapperPath), normalizedTarget);
|
||||
|
||||
if (deps.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveWindowsClaudeExecutablePath(
|
||||
configuredPath: string,
|
||||
deps: Required<ResolveClaudeCodeExecutablePathDependencies>,
|
||||
): string {
|
||||
const pathApi = getPathApi(deps.platform);
|
||||
const extension = pathApi.extname(configuredPath).toLowerCase();
|
||||
const explicitPath = isPathLike(configuredPath) || pathApi.isAbsolute(configuredPath);
|
||||
|
||||
if (CLAUDE_SCRIPT_EXTENSIONS.has(extension)) {
|
||||
return configuredPath;
|
||||
}
|
||||
|
||||
if (explicitPath && extension === '.exe') {
|
||||
return configuredPath;
|
||||
}
|
||||
|
||||
if (explicitPath) {
|
||||
return resolveClaudeWrapperBinary(configuredPath, deps) ?? configuredPath;
|
||||
}
|
||||
|
||||
try {
|
||||
const stdout = deps.execFileSync('where.exe', [configuredPath], {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
windowsHide: true,
|
||||
});
|
||||
const candidates = stdout
|
||||
.split(/\r?\n/)
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (pathApi.extname(candidate).toLowerCase() === '.exe') {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const resolved = resolveClaudeWrapperBinary(candidate, deps);
|
||||
if (resolved) {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return configuredPath;
|
||||
}
|
||||
|
||||
return configuredPath;
|
||||
}
|
||||
|
||||
export function resolveClaudeCodeExecutablePath(
|
||||
configuredPath: string | undefined = process.env.CLAUDE_CLI_PATH,
|
||||
dependencies: ResolveClaudeCodeExecutablePathDependencies = {},
|
||||
): string {
|
||||
const deps: Required<ResolveClaudeCodeExecutablePathDependencies> = {
|
||||
execFileSync: dependencies.execFileSync ?? execFileSync,
|
||||
existsSync: dependencies.existsSync ?? fs.existsSync,
|
||||
platform: dependencies.platform ?? process.platform,
|
||||
readFileSync: dependencies.readFileSync ?? fs.readFileSync,
|
||||
};
|
||||
|
||||
const normalizedPath = stripWrappingQuotes(configuredPath || DEFAULT_CLAUDE_COMMAND);
|
||||
if (deps.platform !== 'win32') {
|
||||
return normalizedPath;
|
||||
}
|
||||
|
||||
return resolveWindowsClaudeExecutablePath(normalizedPath, deps);
|
||||
}
|
||||
Reference in New Issue
Block a user