Gemini OAuth: resolve npm global shim install layouts (#27585)

* Changelog: credit session path fixes

* test(gemini-oauth): cover npm global shim credential discovery

* fix(gemini-oauth): resolve npm global shim install roots
This commit is contained in:
Vincent Koc
2026-02-26 09:43:05 -05:00
committed by GitHub
parent 79659b2b14
commit 6daf40d3f4
2 changed files with 104 additions and 28 deletions

View File

@@ -96,6 +96,41 @@ describe("extractGeminiCliCredentials", () => {
return layout;
}
function installNpmShimLayout(params: { oauth2Exists?: boolean; oauth2Content?: string }) {
const binDir = join(rootDir, "fake", "npm-bin");
const geminiPath = join(binDir, "gemini");
const resolvedPath = geminiPath;
const oauth2Path = join(
binDir,
"node_modules",
"@google",
"gemini-cli",
"node_modules",
"@google",
"gemini-cli-core",
"dist",
"src",
"code_assist",
"oauth2.js",
);
process.env.PATH = binDir;
mockExistsSync.mockImplementation((p: string) => {
const normalized = normalizePath(p);
if (normalized === normalizePath(geminiPath)) {
return true;
}
if (params.oauth2Exists && normalized === normalizePath(oauth2Path)) {
return true;
}
return false;
});
mockRealpathSync.mockReturnValue(resolvedPath);
if (params.oauth2Content !== undefined) {
mockReadFileSync.mockReturnValue(params.oauth2Content);
}
}
beforeEach(async () => {
vi.clearAllMocks();
originalPath = process.env.PATH;
@@ -127,6 +162,19 @@ describe("extractGeminiCliCredentials", () => {
});
});
it("extracts credentials when PATH entry is an npm global shim", async () => {
installNpmShimLayout({ oauth2Exists: true, oauth2Content: FAKE_OAUTH2_CONTENT });
const { extractGeminiCliCredentials, clearCredentialsCache } = await import("./oauth.js");
clearCredentialsCache();
const result = extractGeminiCliCredentials();
expect(result).toEqual({
clientId: FAKE_CLIENT_ID,
clientSecret: FAKE_CLIENT_SECRET,
});
});
it("returns null when oauth2.js cannot be found", async () => {
installGeminiLayout({ oauth2Exists: false, readdir: [] });

View File

@@ -71,41 +71,45 @@ export function extractGeminiCliCredentials(): { clientId: string; clientSecret:
}
const resolvedPath = realpathSync(geminiPath);
const geminiCliDir = dirname(dirname(resolvedPath));
const searchPaths = [
join(
geminiCliDir,
"node_modules",
"@google",
"gemini-cli-core",
"dist",
"src",
"code_assist",
"oauth2.js",
),
join(
geminiCliDir,
"node_modules",
"@google",
"gemini-cli-core",
"dist",
"code_assist",
"oauth2.js",
),
];
const geminiCliDirs = resolveGeminiCliDirs(geminiPath, resolvedPath);
let content: string | null = null;
for (const p of searchPaths) {
if (existsSync(p)) {
content = readFileSync(p, "utf8");
for (const geminiCliDir of geminiCliDirs) {
const searchPaths = [
join(
geminiCliDir,
"node_modules",
"@google",
"gemini-cli-core",
"dist",
"src",
"code_assist",
"oauth2.js",
),
join(
geminiCliDir,
"node_modules",
"@google",
"gemini-cli-core",
"dist",
"code_assist",
"oauth2.js",
),
];
for (const p of searchPaths) {
if (existsSync(p)) {
content = readFileSync(p, "utf8");
break;
}
}
if (content) {
break;
}
}
if (!content) {
const found = findFile(geminiCliDir, "oauth2.js", 10);
if (found) {
content = readFileSync(found, "utf8");
break;
}
}
if (!content) {
@@ -124,6 +128,30 @@ export function extractGeminiCliCredentials(): { clientId: string; clientSecret:
return null;
}
function resolveGeminiCliDirs(geminiPath: string, resolvedPath: string): string[] {
const binDir = dirname(geminiPath);
const candidates = [
dirname(dirname(resolvedPath)),
join(dirname(resolvedPath), "node_modules", "@google", "gemini-cli"),
join(binDir, "node_modules", "@google", "gemini-cli"),
join(dirname(binDir), "node_modules", "@google", "gemini-cli"),
join(dirname(binDir), "lib", "node_modules", "@google", "gemini-cli"),
];
const deduped: string[] = [];
const seen = new Set<string>();
for (const candidate of candidates) {
const key =
process.platform === "win32" ? candidate.replace(/\\/g, "/").toLowerCase() : candidate;
if (seen.has(key)) {
continue;
}
seen.add(key);
deduped.push(candidate);
}
return deduped;
}
function findInPath(name: string): string | null {
const exts = process.platform === "win32" ? [".cmd", ".bat", ".exe", ""] : [""];
for (const dir of (process.env.PATH ?? "").split(delimiter)) {