feat: locate CLI binary

Search PATH, then standard install dirs, since the packaged app often
starts with a minimal PATH.
This commit is contained in:
Ivanko1608
2026-10-24 20:24:57 +04:00
parent 8981456dfd
commit 1df95fdfdd
2 changed files with 115 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
type BinaryLookup = {
env: Record<string, string | undefined>;
platform: NodeJS.Platform;
homeDir: string;
isExecutable: (path: string) => boolean;
};
type SearchSpec = {
paths: path.PlatformPath;
names: string[];
knownDirs: string[];
};
function getWindowsSearchSpec(env: BinaryLookup["env"], homeDir: string): SearchSpec {
const paths = path.win32;
const appData = env.APPDATA ?? paths.join(homeDir, "AppData", "Roaming");
return {
paths,
names: ["claude.cmd", "claude.exe"],
knownDirs: [paths.join(homeDir, ".local", "bin"), paths.join(homeDir, ".claude", "local"), env.npm_config_prefix ?? paths.join(appData, "npm")],
};
}
function getPosixSearchSpec(env: BinaryLookup["env"], homeDir: string): SearchSpec {
const paths = path.posix;
const npmBin = env.npm_config_prefix ? paths.join(env.npm_config_prefix, "bin") : paths.join(homeDir, ".npm-global", "bin");
return {
paths,
names: ["claude"],
knownDirs: [paths.join(homeDir, ".local", "bin"), paths.join(homeDir, ".claude", "local"), npmBin, "/opt/homebrew/bin", "/usr/local/bin"],
};
}
export function resolveClaudeBinary({ env, platform, homeDir, isExecutable }: BinaryLookup): string | null {
const { paths, names, knownDirs } = platform === "win32" ? getWindowsSearchSpec(env, homeDir) : getPosixSearchSpec(env, homeDir);
const pathDirs = (env.PATH ?? "").split(paths.delimiter).filter(Boolean);
for (const dir of [...pathDirs, ...knownDirs]) {
for (const name of names) {
const candidate = paths.join(dir, name);
if (isExecutable(candidate)) return candidate;
}
}
return null;
}
function isExecutableFile(filePath: string): boolean {
try {
fs.accessSync(filePath, fs.constants.X_OK);
return fs.statSync(filePath).isFile();
} catch {
return false;
}
}
export function getClaudeBinary(): string | null {
return resolveClaudeBinary({ env: process.env, platform: process.platform, homeDir: os.homedir(), isExecutable: isExecutableFile });
}