Fix Claude CLI resolution on Windows
This commit is contained in:
parent
d60ac09925
commit
42e5a2431c
2 changed files with 118 additions and 10 deletions
|
|
@ -1,3 +1,2 @@
|
|||
ignoredBuiltDependencies:
|
||||
- electron
|
||||
- esbuild
|
||||
|
|
|
|||
|
|
@ -3,6 +3,15 @@ import * as os from 'os';
|
|||
import * as path from 'path';
|
||||
|
||||
async function isExecutable(filePath: string): Promise<boolean> {
|
||||
if (process.platform === 'win32') {
|
||||
try {
|
||||
const stat = await fs.promises.stat(filePath);
|
||||
return stat.isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.promises.access(filePath, fs.constants.X_OK);
|
||||
return true;
|
||||
|
|
@ -11,6 +20,46 @@ async function isExecutable(filePath: string): Promise<boolean> {
|
|||
}
|
||||
}
|
||||
|
||||
function stripSurroundingQuotes(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
||||
return trimmed.slice(1, -1);
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function getWindowsExecutableExtensions(): string[] {
|
||||
const raw = process.env.PATHEXT;
|
||||
if (!raw) {
|
||||
return ['.exe', '.cmd', '.bat', '.com'];
|
||||
}
|
||||
|
||||
const exts = raw
|
||||
.split(';')
|
||||
.map((ext) => ext.trim())
|
||||
.filter((ext) => ext.length > 0)
|
||||
.map((ext) => (ext.startsWith('.') ? ext : `.${ext}`))
|
||||
.map((ext) => ext.toLowerCase());
|
||||
|
||||
return Array.from(new Set(exts));
|
||||
}
|
||||
|
||||
function expandWindowsBinaryNames(binaryName: string): string[] {
|
||||
const trimmed = binaryName.trim();
|
||||
if (!trimmed) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const ext = path.extname(trimmed);
|
||||
if (ext) {
|
||||
return [trimmed];
|
||||
}
|
||||
|
||||
const exts = getWindowsExecutableExtensions();
|
||||
const withExt = exts.map((e) => `${trimmed}${e}`);
|
||||
return [...withExt, trimmed];
|
||||
}
|
||||
|
||||
async function collectNvmCandidates(): Promise<string[]> {
|
||||
const nvmNodeRoot = path.join(os.homedir(), '.nvm', 'versions', 'node');
|
||||
let versions: string[];
|
||||
|
|
@ -33,16 +82,53 @@ async function resolveFromPathEnv(binaryName: string): Promise<string | null> {
|
|||
}
|
||||
|
||||
const pathParts = rawPath.split(path.delimiter);
|
||||
const binaryNames =
|
||||
process.platform === 'win32' ? expandWindowsBinaryNames(binaryName) : [binaryName];
|
||||
for (const part of pathParts) {
|
||||
if (!part) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const candidate = path.join(part, binaryName);
|
||||
const cleanedPart = stripSurroundingQuotes(part);
|
||||
if (!cleanedPart) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const name of binaryNames) {
|
||||
const candidate = path.join(cleanedPart, name);
|
||||
if (await isExecutable(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function resolveFromExplicitPath(inputPath: string): Promise<string | null> {
|
||||
const trimmed = inputPath.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (await isExecutable(trimmed)) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
if (process.platform !== 'win32') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (path.extname(trimmed)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const ext of getWindowsExecutableExtensions()) {
|
||||
const candidate = `${trimmed}${ext}`;
|
||||
if (await isExecutable(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -52,22 +138,45 @@ export class ClaudeBinaryResolver {
|
|||
static async resolve(): Promise<string | null> {
|
||||
if (cachedPath !== undefined) return cachedPath;
|
||||
|
||||
const platformBinaryName = process.platform === 'win32' ? 'claude.cmd' : 'claude';
|
||||
const fromPath = await resolveFromPathEnv(platformBinaryName);
|
||||
const overrideRaw = process.env.CLAUDE_CLI_PATH?.trim();
|
||||
if (overrideRaw) {
|
||||
const looksLikePath =
|
||||
path.isAbsolute(overrideRaw) || overrideRaw.includes('\\') || overrideRaw.includes('/');
|
||||
const resolvedOverride = looksLikePath
|
||||
? await resolveFromExplicitPath(overrideRaw)
|
||||
: await resolveFromPathEnv(overrideRaw);
|
||||
|
||||
if (resolvedOverride) {
|
||||
cachedPath = resolvedOverride;
|
||||
return cachedPath;
|
||||
}
|
||||
}
|
||||
|
||||
const baseBinaryName = 'claude';
|
||||
const fromPath = await resolveFromPathEnv(baseBinaryName);
|
||||
if (fromPath) {
|
||||
cachedPath = fromPath;
|
||||
return cachedPath;
|
||||
}
|
||||
|
||||
const candidates: string[] = [
|
||||
path.join(os.homedir(), '.npm-global', 'bin', platformBinaryName),
|
||||
path.join(os.homedir(), '.npm', 'bin', platformBinaryName),
|
||||
const platformBinaryNames =
|
||||
process.platform === 'win32' ? expandWindowsBinaryNames(baseBinaryName) : [baseBinaryName];
|
||||
|
||||
const candidateDirs: string[] = [
|
||||
path.join(os.homedir(), '.npm-global', 'bin'),
|
||||
path.join(os.homedir(), '.npm', 'bin'),
|
||||
process.platform === 'win32'
|
||||
? path.join(process.env.APPDATA ?? '', 'npm', 'claude.cmd')
|
||||
: '/usr/local/bin/claude',
|
||||
process.platform === 'win32' ? '' : '/opt/homebrew/bin/claude',
|
||||
? process.env.APPDATA
|
||||
? path.join(process.env.APPDATA, 'npm')
|
||||
: ''
|
||||
: '/usr/local/bin',
|
||||
process.platform === 'win32' ? '' : '/opt/homebrew/bin',
|
||||
].filter((candidate) => candidate.length > 0);
|
||||
|
||||
const candidates = candidateDirs.flatMap((dir) =>
|
||||
platformBinaryNames.map((name) => path.join(dir, name))
|
||||
);
|
||||
|
||||
const nvmCandidates = process.platform === 'win32' ? [] : await collectNvmCandidates();
|
||||
for (const candidate of [...candidates, ...nvmCandidates]) {
|
||||
if (await isExecutable(candidate)) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue