diff --git a/extensions/google-gemini-cli-auth/oauth.test.ts b/extensions/google-gemini-cli-auth/oauth.test.ts index 018eae78dd6..3d5df634ff6 100644 --- a/extensions/google-gemini-cli-auth/oauth.test.ts +++ b/extensions/google-gemini-cli-auth/oauth.test.ts @@ -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: [] }); diff --git a/extensions/google-gemini-cli-auth/oauth.ts b/extensions/google-gemini-cli-auth/oauth.ts index 7977ab52981..bba4c6b1f39 100644 --- a/extensions/google-gemini-cli-auth/oauth.ts +++ b/extensions/google-gemini-cli-auth/oauth.ts @@ -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(); + 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)) {