summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/cli/src/gemini.tsx3
-rw-r--r--packages/cli/src/ui/hooks/useAuthCommand.ts6
-rw-r--r--packages/core/src/code_assist/oauth2.test.ts7
-rw-r--r--packages/core/src/code_assist/oauth2.ts11
-rw-r--r--packages/core/src/index.ts1
-rw-r--r--packages/core/src/utils/browser.ts53
6 files changed, 73 insertions, 8 deletions
diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx
index 71e69952..f00dfd45 100644
--- a/packages/cli/src/gemini.tsx
+++ b/packages/cli/src/gemini.tsx
@@ -37,6 +37,7 @@ import {
logUserPrompt,
AuthType,
getOauthClient,
+ shouldAttemptBrowserLaunch,
} from '@google/gemini-cli-core';
import { validateAuthMethod } from './config/auth.js';
import { setMaxSizedBoxDebugging } from './ui/components/shared/MaxSizedBox.js';
@@ -184,7 +185,7 @@ export async function main() {
if (
settings.merged.selectedAuthType === AuthType.LOGIN_WITH_GOOGLE &&
- config.getNoBrowser()
+ (config.getNoBrowser() || !shouldAttemptBrowserLaunch())
) {
// Do oauth before app renders to make copying the link possible.
await getOauthClient(settings.merged.selectedAuthType, config);
diff --git a/packages/cli/src/ui/hooks/useAuthCommand.ts b/packages/cli/src/ui/hooks/useAuthCommand.ts
index afc276c0..e4f1f093 100644
--- a/packages/cli/src/ui/hooks/useAuthCommand.ts
+++ b/packages/cli/src/ui/hooks/useAuthCommand.ts
@@ -11,6 +11,7 @@ import {
Config,
clearCachedCredentialFile,
getErrorMessage,
+ shouldAttemptBrowserLaunch,
} from '@google/gemini-cli-core';
import { runExitCleanup } from '../../utils/cleanup.js';
@@ -56,7 +57,10 @@ export const useAuthCommand = (
if (authType) {
await clearCachedCredentialFile();
settings.setValue(scope, 'selectedAuthType', authType);
- if (authType === AuthType.LOGIN_WITH_GOOGLE && config.getNoBrowser()) {
+ if (
+ authType === AuthType.LOGIN_WITH_GOOGLE &&
+ (config.getNoBrowser() || !shouldAttemptBrowserLaunch())
+ ) {
runExitCleanup();
console.log(
`
diff --git a/packages/core/src/code_assist/oauth2.test.ts b/packages/core/src/code_assist/oauth2.test.ts
index 9d8fb892..8a1af056 100644
--- a/packages/core/src/code_assist/oauth2.test.ts
+++ b/packages/core/src/code_assist/oauth2.test.ts
@@ -31,6 +31,9 @@ vi.mock('http');
vi.mock('open');
vi.mock('crypto');
vi.mock('node:readline');
+vi.mock('../utils/browser.js', () => ({
+ shouldAttemptBrowserLaunch: () => true,
+}));
const mockConfig = {
getNoBrowser: () => false,
@@ -83,7 +86,7 @@ describe('oauth2', () => {
);
vi.spyOn(crypto, 'randomBytes').mockReturnValue(mockState as never);
- (open as Mock).mockImplementation(async () => ({}) as never);
+ (open as Mock).mockImplementation(async () => ({ on: vi.fn() }) as never);
// Mock the UserInfo API response
(global.fetch as Mock).mockResolvedValue({
@@ -236,7 +239,7 @@ describe('oauth2', () => {
expect(mockGetToken).toHaveBeenCalledWith({
code: mockCode,
codeVerifier: mockCodeVerifier.codeVerifier,
- redirect_uri: 'https://sdk.cloud.google.com/authcode_cloudcode.html',
+ redirect_uri: 'https://codeassist.google.com/authcode',
});
expect(mockSetCredentials).toHaveBeenCalledWith(mockTokens);
diff --git a/packages/core/src/code_assist/oauth2.ts b/packages/core/src/code_assist/oauth2.ts
index ac45177c..51227086 100644
--- a/packages/core/src/code_assist/oauth2.ts
+++ b/packages/core/src/code_assist/oauth2.ts
@@ -26,6 +26,7 @@ import {
clearCachedGoogleAccount,
} from '../utils/user_account.js';
import { AuthType } from '../core/contentGenerator.js';
+import { shouldAttemptBrowserLaunch } from '../utils/browser.js';
import readline from 'node:readline';
// OAuth Client ID used to initiate OAuth2Client class.
@@ -121,7 +122,7 @@ export async function getOauthClient(
}
}
- if (config.getNoBrowser()) {
+ if (config.getNoBrowser() || !shouldAttemptBrowserLaunch()) {
let success = false;
const maxRetries = 2;
for (let i = 0; !success && i < maxRetries; i++) {
@@ -156,15 +157,17 @@ export async function getOauthClient(
// causing the entire Node.js process to crash.
childProcess.on('error', (_) => {
console.error(
- 'Failed to open browser automatically. Please open the URL manually:',
+ 'Failed to open browser automatically. Please try running again with NO_BROWSER=true set.',
);
- console.error(webLogin.authUrl);
+ process.exit(1);
});
} catch (err) {
console.error(
'An unexpected error occurred while trying to open the browser:',
err,
+ '\nPlease try running again with NO_BROWSER=true set.',
);
+ process.exit(1);
}
console.log('Waiting for authentication...');
@@ -175,7 +178,7 @@ export async function getOauthClient(
}
async function authWithUserCode(client: OAuth2Client): Promise<boolean> {
- const redirectUri = 'https://sdk.cloud.google.com/authcode_cloudcode.html';
+ const redirectUri = 'https://codeassist.google.com/authcode';
const codeVerifier = await client.generateCodeVerifierAsync();
const state = crypto.randomBytes(32).toString('hex');
const authUrl: string = client.generateAuthUrl({
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 2e85deff..a3d77ddc 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -80,3 +80,4 @@ export { OAuthUtils } from './mcp/oauth-utils.js';
// Export telemetry functions
export * from './telemetry/index.js';
export { sessionId } from './utils/session.js';
+export * from './utils/browser.js';
diff --git a/packages/core/src/utils/browser.ts b/packages/core/src/utils/browser.ts
new file mode 100644
index 00000000..a9b2b013
--- /dev/null
+++ b/packages/core/src/utils/browser.ts
@@ -0,0 +1,53 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * Determines if we should attempt to launch a browser for authentication
+ * based on the user's environment.
+ *
+ * This is an adaptation of the logic from the Google Cloud SDK.
+ * @returns True if the tool should attempt to launch a browser.
+ */
+export function shouldAttemptBrowserLaunch(): boolean {
+ // A list of browser names that indicate we should not attempt to open a
+ // web browser for the user.
+ const browserBlocklist = ['www-browser'];
+ const browserEnv = process.env.BROWSER;
+ if (browserEnv && browserBlocklist.includes(browserEnv)) {
+ return false;
+ }
+ // Common environment variables used in CI/CD or other non-interactive shells.
+ if (process.env.CI || process.env.DEBIAN_FRONTEND === 'noninteractive') {
+ return false;
+ }
+
+ // The presence of SSH_CONNECTION indicates a remote session.
+ // We should not attempt to launch a browser unless a display is explicitly available
+ // (checked below for Linux).
+ const isSSH = !!process.env.SSH_CONNECTION;
+
+ // On Linux, the presence of a display server is a strong indicator of a GUI.
+ if (process.platform === 'linux') {
+ // These are environment variables that can indicate a running compositor on
+ // Linux.
+ const displayVariables = ['DISPLAY', 'WAYLAND_DISPLAY', 'MIR_SOCKET'];
+ const hasDisplay = displayVariables.some((v) => !!process.env[v]);
+ if (!hasDisplay) {
+ return false;
+ }
+ }
+
+ // If in an SSH session on a non-Linux OS (e.g., macOS), don't launch browser.
+ // The Linux case is handled above (it's allowed if DISPLAY is set).
+ if (isSSH && process.platform !== 'linux') {
+ return false;
+ }
+
+ // For non-Linux OSes, we generally assume a GUI is available
+ // unless other signals (like SSH) suggest otherwise.
+ // The `open` command's error handling will catch final edge cases.
+ return true;
+}