openpencil/apps/web/server/utils/resolve-gemini-cli.ts

107 lines
4 KiB
TypeScript
Raw Normal View History

import { execSync } from 'node:child_process';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';
import { serverLog } from './server-logger';
import { posixUserBinDirs, probeViaLoginShell } from './cli-resolver-helpers';
const isWindows = process.platform === 'win32';
/** Windows npm global installs may create .cmd or .ps1 wrappers — try both */
function winNpmCandidates(dir: string, name: string): string[] {
return [join(dir, `${name}.cmd`), join(dir, `${name}.ps1`)];
}
/** On Windows, `where` may return an extensionless shell script — prefer .cmd/.ps1 */
function resolveWinExtension(binPath: string): string {
if (!isWindows) return binPath;
if (/\.(cmd|ps1|exe)$/i.test(binPath)) return binPath;
for (const ext of ['.cmd', '.ps1']) {
if (existsSync(binPath + ext)) return binPath + ext;
}
return binPath;
}
/** Resolve the Gemini CLI binary path across macOS, Linux, and Windows. */
export function resolveGeminiCli(): string | undefined {
serverLog.info(
`[resolve-gemini] platform=${process.platform}, isWindows=${isWindows}, SHELL=${process.env.SHELL ?? 'unset'}`,
);
// 1. Try PATH lookup
try {
const cmd = isWindows ? 'where gemini 2>nul' : 'which gemini 2>/dev/null';
serverLog.info(`[resolve-gemini] PATH lookup: ${cmd}`);
const result = execSync(cmd, { encoding: 'utf-8', timeout: 5000 }).trim();
// `where` on Windows may return multiple lines
const path = result.split(/\r?\n/)[0]?.trim();
serverLog.info(
`[resolve-gemini] PATH result: "${path}" (exists=${path ? existsSync(path) : false})`,
);
if (path && existsSync(path)) return resolveWinExtension(path);
} catch (err) {
serverLog.info(
`[resolve-gemini] PATH lookup failed: ${err instanceof Error ? err.message : err}`,
);
}
// 2. macOS/Linux login-shell probe
if (!isWindows) {
const viaShell = probeViaLoginShell('gemini', 'resolve-gemini');
if (viaShell) return viaShell;
}
// 3. Try `npm prefix -g` (Windows uses npm.cmd; Unix uses npm)
try {
const npmCmd = isWindows ? 'npm.cmd prefix -g' : 'npm prefix -g';
serverLog.info(`[resolve-gemini] npm prefix lookup: ${npmCmd}`);
const prefix = execSync(npmCmd, { encoding: 'utf-8', timeout: 5000 }).trim();
serverLog.info(`[resolve-gemini] npm global prefix: "${prefix}"`);
if (prefix) {
if (isWindows) {
for (const bin of winNpmCandidates(prefix, 'gemini')) {
serverLog.info(`[resolve-gemini] npm global bin: "${bin}" (exists=${existsSync(bin)})`);
if (existsSync(bin)) return bin;
}
} else {
const bin = join(prefix, 'bin', 'gemini');
serverLog.info(`[resolve-gemini] npm global bin: "${bin}" (exists=${existsSync(bin)})`);
if (existsSync(bin)) return bin;
}
}
} catch (err) {
serverLog.info(
`[resolve-gemini] npm prefix -g failed: ${err instanceof Error ? err.message : err}`,
);
}
// 4. Common install locations
const home = homedir();
const candidates = isWindows
? [
// npm global (.cmd + .ps1)
...winNpmCandidates(join(process.env.APPDATA || '', 'npm'), 'gemini'),
// nvm-windows / fnm
...winNpmCandidates(join(process.env.NVM_SYMLINK || ''), 'gemini'),
...winNpmCandidates(join(process.env.FNM_MULTISHELL_PATH || ''), 'gemini'),
// winget / native
join(process.env.LOCALAPPDATA || '', 'Microsoft', 'WinGet', 'Links', 'gemini.exe'),
]
: [
// Explicit npm-global prefix some users set manually
join(home, '.npm-global', 'bin', 'gemini'),
...posixUserBinDirs().map((dir) => join(dir, 'gemini')),
];
for (const c of candidates) {
const exists = c ? existsSync(c) : false;
serverLog.info(`[resolve-gemini] candidate: "${c}" (exists=${exists})`);
if (c && exists) return c;
}
serverLog.warn(
'[resolve-gemini] no gemini binary found after PATH, login-shell probe, and candidate scan',
);
return undefined;
}