mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-11 22:51:00 -07:00
260 lines
7.2 KiB
TypeScript
260 lines
7.2 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import type React from 'react';
|
|
import { useCallback, useState } from 'react';
|
|
import { Box, Text } from 'ink';
|
|
import { theme } from '../semantic-colors.js';
|
|
import { RadioButtonSelect } from '../components/shared/RadioButtonSelect.js';
|
|
import type {
|
|
LoadableSettingScope,
|
|
LoadedSettings,
|
|
} from '../../config/settings.js';
|
|
import { SettingScope } from '../../config/settings.js';
|
|
import {
|
|
AuthType,
|
|
clearCachedCredentialFile,
|
|
type Config,
|
|
} from '@google/gemini-cli-core';
|
|
import { useKeypress } from '../hooks/useKeypress.js';
|
|
import { AuthState } from '../types.js';
|
|
import { runExitCleanup } from '../../utils/cleanup.js';
|
|
import { validateAuthMethodWithSettings } from './useAuth.js';
|
|
import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js';
|
|
|
|
interface AuthDialogProps {
|
|
config: Config;
|
|
settings: LoadedSettings;
|
|
setAuthState: (state: AuthState) => void;
|
|
authError: string | null;
|
|
onAuthError: (error: string | null) => void;
|
|
setAuthContext: (context: { requiresRestart?: boolean }) => void;
|
|
}
|
|
|
|
export function AuthDialog({
|
|
config,
|
|
settings,
|
|
setAuthState,
|
|
authError,
|
|
onAuthError,
|
|
setAuthContext,
|
|
}: AuthDialogProps): React.JSX.Element {
|
|
const [exiting, setExiting] = useState(false);
|
|
let items = [
|
|
{
|
|
label: 'Login with Google',
|
|
value: AuthType.LOGIN_WITH_GOOGLE,
|
|
key: AuthType.LOGIN_WITH_GOOGLE,
|
|
},
|
|
...(process.env['CLOUD_SHELL'] === 'true'
|
|
? [
|
|
{
|
|
label: 'Use Cloud Shell user credentials',
|
|
value: AuthType.COMPUTE_ADC,
|
|
key: AuthType.COMPUTE_ADC,
|
|
},
|
|
]
|
|
: process.env['GEMINI_CLI_USE_COMPUTE_ADC'] === 'true'
|
|
? [
|
|
{
|
|
label: 'Use metadata server application default credentials',
|
|
value: AuthType.COMPUTE_ADC,
|
|
key: AuthType.COMPUTE_ADC,
|
|
},
|
|
]
|
|
: []),
|
|
{
|
|
label: 'Use Gemini API Key',
|
|
value: AuthType.USE_GEMINI,
|
|
key: AuthType.USE_GEMINI,
|
|
},
|
|
{
|
|
label: 'Vertex AI',
|
|
value: AuthType.USE_VERTEX_AI,
|
|
key: AuthType.USE_VERTEX_AI,
|
|
},
|
|
];
|
|
|
|
if (settings.merged.security.auth.enforcedType) {
|
|
items = items.filter(
|
|
(item) => item.value === settings.merged.security.auth.enforcedType,
|
|
);
|
|
}
|
|
|
|
let defaultAuthType = null;
|
|
const defaultAuthTypeEnv = process.env['GEMINI_DEFAULT_AUTH_TYPE'];
|
|
if (
|
|
defaultAuthTypeEnv &&
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
Object.values(AuthType).includes(defaultAuthTypeEnv as AuthType)
|
|
) {
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
defaultAuthType = defaultAuthTypeEnv as AuthType;
|
|
}
|
|
|
|
let initialAuthIndex = items.findIndex((item) => {
|
|
if (settings.merged.security.auth.selectedType) {
|
|
return item.value === settings.merged.security.auth.selectedType;
|
|
}
|
|
|
|
if (defaultAuthType) {
|
|
return item.value === defaultAuthType;
|
|
}
|
|
|
|
if (process.env['GEMINI_API_KEY']) {
|
|
return item.value === AuthType.USE_GEMINI;
|
|
}
|
|
|
|
return item.value === AuthType.LOGIN_WITH_GOOGLE;
|
|
});
|
|
if (settings.merged.security.auth.enforcedType) {
|
|
initialAuthIndex = 0;
|
|
}
|
|
|
|
const onSelect = useCallback(
|
|
async (authType: AuthType | undefined, scope: LoadableSettingScope) => {
|
|
if (exiting) {
|
|
return;
|
|
}
|
|
if (authType) {
|
|
if (authType === AuthType.LOGIN_WITH_GOOGLE) {
|
|
setAuthContext({ requiresRestart: true });
|
|
} else {
|
|
setAuthContext({});
|
|
}
|
|
await clearCachedCredentialFile();
|
|
|
|
settings.setValue(scope, 'security.auth.selectedType', authType);
|
|
if (
|
|
authType === AuthType.LOGIN_WITH_GOOGLE &&
|
|
config.isBrowserLaunchSuppressed()
|
|
) {
|
|
setExiting(true);
|
|
setTimeout(async () => {
|
|
await runExitCleanup();
|
|
process.exit(RELAUNCH_EXIT_CODE);
|
|
}, 100);
|
|
return;
|
|
}
|
|
|
|
if (authType === AuthType.USE_GEMINI) {
|
|
if (process.env['GEMINI_API_KEY'] !== undefined) {
|
|
setAuthState(AuthState.Unauthenticated);
|
|
return;
|
|
} else {
|
|
setAuthState(AuthState.AwaitingApiKeyInput);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
setAuthState(AuthState.Unauthenticated);
|
|
},
|
|
[settings, config, setAuthState, exiting, setAuthContext],
|
|
);
|
|
|
|
const handleAuthSelect = (authMethod: AuthType) => {
|
|
const error = validateAuthMethodWithSettings(authMethod, settings);
|
|
if (error) {
|
|
onAuthError(error);
|
|
} else {
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
onSelect(authMethod, SettingScope.User);
|
|
}
|
|
};
|
|
|
|
useKeypress(
|
|
(key) => {
|
|
if (key.name === 'escape') {
|
|
// Prevent exit if there is an error message.
|
|
// This means they user is not authenticated yet.
|
|
if (authError) {
|
|
return true;
|
|
}
|
|
if (settings.merged.security.auth.selectedType === undefined) {
|
|
// Prevent exiting if no auth method is set
|
|
onAuthError(
|
|
'You must select an auth method to proceed. Press Ctrl+C twice to exit.',
|
|
);
|
|
return true;
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
onSelect(undefined, SettingScope.User);
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
{ isActive: true },
|
|
);
|
|
|
|
if (exiting) {
|
|
return (
|
|
<Box
|
|
borderStyle="round"
|
|
borderColor={theme.border.focused}
|
|
flexDirection="row"
|
|
padding={1}
|
|
width="100%"
|
|
alignItems="flex-start"
|
|
>
|
|
<Text color={theme.text.primary}>
|
|
Logging in with Google... Restarting Gemini CLI to continue.
|
|
</Text>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Box
|
|
borderStyle="round"
|
|
borderColor={theme.border.focused}
|
|
flexDirection="row"
|
|
padding={1}
|
|
width="100%"
|
|
alignItems="flex-start"
|
|
>
|
|
<Text color={theme.text.accent}>? </Text>
|
|
<Box flexDirection="column" flexGrow={1}>
|
|
<Text bold color={theme.text.primary}>
|
|
Get started
|
|
</Text>
|
|
<Box marginTop={1}>
|
|
<Text color={theme.text.primary}>
|
|
How would you like to authenticate for this project?
|
|
</Text>
|
|
</Box>
|
|
<Box marginTop={1}>
|
|
<RadioButtonSelect
|
|
items={items}
|
|
initialIndex={initialAuthIndex}
|
|
onSelect={handleAuthSelect}
|
|
onHighlight={() => {
|
|
onAuthError(null);
|
|
}}
|
|
/>
|
|
</Box>
|
|
{authError && (
|
|
<Box marginTop={1}>
|
|
<Text color={theme.status.error}>{authError}</Text>
|
|
</Box>
|
|
)}
|
|
<Box marginTop={1}>
|
|
<Text color={theme.text.secondary}>(Use Enter to select)</Text>
|
|
</Box>
|
|
<Box marginTop={1}>
|
|
<Text color={theme.text.primary}>
|
|
Terms of Services and Privacy Notice for Gemini CLI
|
|
</Text>
|
|
</Box>
|
|
<Box marginTop={1}>
|
|
<Text color={theme.text.link}>
|
|
{'https://geminicli.com/docs/resources/tos-privacy/'}
|
|
</Text>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|