mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-05 20:25:52 +00:00
140 lines
4.0 KiB
TypeScript
140 lines
4.0 KiB
TypeScript
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);
|
|
}
|