mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
Refactor Authentication Components and Hooks (#7750)
This commit is contained in:
committed by
GitHub
parent
d8dbe6271f
commit
7239c5cd9a
@@ -15,7 +15,6 @@ import type {
|
|||||||
ToolRegistry,
|
ToolRegistry,
|
||||||
SandboxConfig,
|
SandboxConfig,
|
||||||
GeminiClient,
|
GeminiClient,
|
||||||
AuthType,
|
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import {
|
import {
|
||||||
ApprovalMode,
|
ApprovalMode,
|
||||||
@@ -34,7 +33,6 @@ import type { UpdateObject } from './utils/updateCheck.js';
|
|||||||
import { checkForUpdates } from './utils/updateCheck.js';
|
import { checkForUpdates } from './utils/updateCheck.js';
|
||||||
import { EventEmitter } from 'node:events';
|
import { EventEmitter } from 'node:events';
|
||||||
import { updateEventEmitter } from '../utils/updateEventEmitter.js';
|
import { updateEventEmitter } from '../utils/updateEventEmitter.js';
|
||||||
import * as auth from '../config/auth.js';
|
|
||||||
import * as useTerminalSize from './hooks/useTerminalSize.js';
|
import * as useTerminalSize from './hooks/useTerminalSize.js';
|
||||||
|
|
||||||
// Define a more complete mock server config based on actual Config
|
// Define a more complete mock server config based on actual Config
|
||||||
@@ -224,14 +222,12 @@ vi.mock('./hooks/useGeminiStream', () => ({
|
|||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./hooks/useAuthCommand', () => ({
|
vi.mock('./auth/useAuth.js', () => ({
|
||||||
useAuthCommand: vi.fn(() => ({
|
useAuthCommand: vi.fn(() => ({
|
||||||
isAuthDialogOpen: false,
|
authState: 'authenticated',
|
||||||
openAuthDialog: vi.fn(),
|
setAuthState: vi.fn(),
|
||||||
handleAuthSelect: vi.fn(),
|
authError: null,
|
||||||
handleAuthHighlight: vi.fn(),
|
onAuthError: vi.fn(),
|
||||||
isAuthenticating: false,
|
|
||||||
cancelAuthentication: vi.fn(),
|
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -1189,60 +1185,6 @@ describe('App UI', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('auth validation', () => {
|
|
||||||
it('should call validateAuthMethod when useExternalAuth is false', async () => {
|
|
||||||
const validateAuthMethodSpy = vi.spyOn(auth, 'validateAuthMethod');
|
|
||||||
mockSettings = createMockSettings({
|
|
||||||
workspace: {
|
|
||||||
security: {
|
|
||||||
auth: {
|
|
||||||
selectedType: 'USE_GEMINI' as AuthType,
|
|
||||||
useExternal: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ui: { theme: 'Default' },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { unmount } = renderWithProviders(
|
|
||||||
<App
|
|
||||||
config={mockConfig as unknown as ServerConfig}
|
|
||||||
settings={mockSettings}
|
|
||||||
version={mockVersion}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
currentUnmount = unmount;
|
|
||||||
|
|
||||||
expect(validateAuthMethodSpy).toHaveBeenCalledWith('USE_GEMINI');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should NOT call validateAuthMethod when useExternalAuth is true', async () => {
|
|
||||||
const validateAuthMethodSpy = vi.spyOn(auth, 'validateAuthMethod');
|
|
||||||
mockSettings = createMockSettings({
|
|
||||||
workspace: {
|
|
||||||
security: {
|
|
||||||
auth: {
|
|
||||||
selectedType: 'USE_GEMINI' as AuthType,
|
|
||||||
useExternal: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ui: { theme: 'Default' },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { unmount } = renderWithProviders(
|
|
||||||
<App
|
|
||||||
config={mockConfig as unknown as ServerConfig}
|
|
||||||
settings={mockSettings}
|
|
||||||
version={mockVersion}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
currentUnmount = unmount;
|
|
||||||
|
|
||||||
expect(validateAuthMethodSpy).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when in a narrow terminal', () => {
|
describe('when in a narrow terminal', () => {
|
||||||
it('should render with a column layout', () => {
|
it('should render with a column layout', () => {
|
||||||
vi.spyOn(useTerminalSize, 'useTerminalSize').mockReturnValue({
|
vi.spyOn(useTerminalSize, 'useTerminalSize').mockReturnValue({
|
||||||
|
|||||||
+23
-64
@@ -15,6 +15,7 @@ import {
|
|||||||
useStdout,
|
useStdout,
|
||||||
} from 'ink';
|
} from 'ink';
|
||||||
import {
|
import {
|
||||||
|
AuthState,
|
||||||
StreamingState,
|
StreamingState,
|
||||||
type HistoryItem,
|
type HistoryItem,
|
||||||
MessageType,
|
MessageType,
|
||||||
@@ -25,7 +26,7 @@ import { useTerminalSize } from './hooks/useTerminalSize.js';
|
|||||||
import { useGeminiStream } from './hooks/useGeminiStream.js';
|
import { useGeminiStream } from './hooks/useGeminiStream.js';
|
||||||
import { useLoadingIndicator } from './hooks/useLoadingIndicator.js';
|
import { useLoadingIndicator } from './hooks/useLoadingIndicator.js';
|
||||||
import { useThemeCommand } from './hooks/useThemeCommand.js';
|
import { useThemeCommand } from './hooks/useThemeCommand.js';
|
||||||
import { useAuthCommand } from './hooks/useAuthCommand.js';
|
import { useAuthCommand } from './auth/useAuth.js';
|
||||||
import { useFolderTrust } from './hooks/useFolderTrust.js';
|
import { useFolderTrust } from './hooks/useFolderTrust.js';
|
||||||
import { useIdeTrustListener } from './hooks/useIdeTrustListener.js';
|
import { useIdeTrustListener } from './hooks/useIdeTrustListener.js';
|
||||||
import { useEditorSettings } from './hooks/useEditorSettings.js';
|
import { useEditorSettings } from './hooks/useEditorSettings.js';
|
||||||
@@ -40,8 +41,6 @@ import { ShellModeIndicator } from './components/ShellModeIndicator.js';
|
|||||||
import { InputPrompt } from './components/InputPrompt.js';
|
import { InputPrompt } from './components/InputPrompt.js';
|
||||||
import { Footer } from './components/Footer.js';
|
import { Footer } from './components/Footer.js';
|
||||||
import { ThemeDialog } from './components/ThemeDialog.js';
|
import { ThemeDialog } from './components/ThemeDialog.js';
|
||||||
import { AuthDialog } from './components/AuthDialog.js';
|
|
||||||
import { AuthInProgress } from './components/AuthInProgress.js';
|
|
||||||
import { EditorSettingsDialog } from './components/EditorSettingsDialog.js';
|
import { EditorSettingsDialog } from './components/EditorSettingsDialog.js';
|
||||||
import { FolderTrustDialog } from './components/FolderTrustDialog.js';
|
import { FolderTrustDialog } from './components/FolderTrustDialog.js';
|
||||||
import { ShellConfirmationDialog } from './components/ShellConfirmationDialog.js';
|
import { ShellConfirmationDialog } from './components/ShellConfirmationDialog.js';
|
||||||
@@ -82,7 +81,6 @@ import {
|
|||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import type { IdeIntegrationNudgeResult } from './IdeIntegrationNudge.js';
|
import type { IdeIntegrationNudgeResult } from './IdeIntegrationNudge.js';
|
||||||
import { IdeIntegrationNudge } from './IdeIntegrationNudge.js';
|
import { IdeIntegrationNudge } from './IdeIntegrationNudge.js';
|
||||||
import { validateAuthMethod } from '../config/auth.js';
|
|
||||||
import { useLogger } from './hooks/useLogger.js';
|
import { useLogger } from './hooks/useLogger.js';
|
||||||
import { StreamingContext } from './contexts/StreamingContext.js';
|
import { StreamingContext } from './contexts/StreamingContext.js';
|
||||||
import {
|
import {
|
||||||
@@ -116,6 +114,8 @@ import { isNarrowWidth } from './utils/isNarrowWidth.js';
|
|||||||
import { useWorkspaceMigration } from './hooks/useWorkspaceMigration.js';
|
import { useWorkspaceMigration } from './hooks/useWorkspaceMigration.js';
|
||||||
import { WorkspaceMigrationDialog } from './components/WorkspaceMigrationDialog.js';
|
import { WorkspaceMigrationDialog } from './components/WorkspaceMigrationDialog.js';
|
||||||
import { isWorkspaceTrusted } from '../config/trustedFolders.js';
|
import { isWorkspaceTrusted } from '../config/trustedFolders.js';
|
||||||
|
import { AuthInProgress } from './auth/AuthInProgress.js';
|
||||||
|
import { AuthDialog } from './auth/AuthDialog.js';
|
||||||
|
|
||||||
const CTRL_EXIT_PROMPT_DURATION_MS = 1000;
|
const CTRL_EXIT_PROMPT_DURATION_MS = 1000;
|
||||||
// Maximum number of queued messages to display in UI to prevent performance issues
|
// Maximum number of queued messages to display in UI to prevent performance issues
|
||||||
@@ -217,7 +217,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
const [geminiMdFileCount, setGeminiMdFileCount] = useState<number>(0);
|
const [geminiMdFileCount, setGeminiMdFileCount] = useState<number>(0);
|
||||||
const [debugMessage, setDebugMessage] = useState<string>('');
|
const [debugMessage, setDebugMessage] = useState<string>('');
|
||||||
const [themeError, setThemeError] = useState<string | null>(null);
|
const [themeError, setThemeError] = useState<string | null>(null);
|
||||||
const [authError, setAuthError] = useState<string | null>(null);
|
|
||||||
const [editorError, setEditorError] = useState<string | null>(null);
|
const [editorError, setEditorError] = useState<string | null>(null);
|
||||||
const [footerHeight, setFooterHeight] = useState<number>(0);
|
const [footerHeight, setFooterHeight] = useState<number>(0);
|
||||||
const [corgiMode, setCorgiMode] = useState(false);
|
const [corgiMode, setCorgiMode] = useState(false);
|
||||||
@@ -338,52 +338,18 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
{ isActive: showIdeRestartPrompt },
|
{ isActive: showIdeRestartPrompt },
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const { authState, setAuthState, authError, onAuthError } = useAuthCommand(
|
||||||
isAuthDialogOpen,
|
settings,
|
||||||
openAuthDialog,
|
config,
|
||||||
handleAuthSelect,
|
|
||||||
isAuthenticating,
|
|
||||||
cancelAuthentication,
|
|
||||||
} = useAuthCommand(settings, setAuthError, config);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
settings.merged.security?.auth?.enforcedType &&
|
|
||||||
settings.merged.security?.auth.selectedType &&
|
|
||||||
settings.merged.security?.auth.enforcedType !==
|
|
||||||
settings.merged.security?.auth.selectedType
|
|
||||||
) {
|
|
||||||
setAuthError(
|
|
||||||
`Authentication is enforced to be ${settings.merged.security?.auth.enforcedType}, but you are currently using ${settings.merged.security?.auth.selectedType}.`,
|
|
||||||
);
|
);
|
||||||
openAuthDialog();
|
|
||||||
} else if (
|
|
||||||
settings.merged.security?.auth?.selectedType &&
|
|
||||||
!settings.merged.security?.auth?.useExternal
|
|
||||||
) {
|
|
||||||
const error = validateAuthMethod(
|
|
||||||
settings.merged.security.auth.selectedType,
|
|
||||||
);
|
|
||||||
if (error) {
|
|
||||||
setAuthError(error);
|
|
||||||
openAuthDialog();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
settings.merged.security?.auth?.selectedType,
|
|
||||||
settings.merged.security?.auth?.enforcedType,
|
|
||||||
settings.merged.security?.auth?.useExternal,
|
|
||||||
openAuthDialog,
|
|
||||||
setAuthError,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Sync user tier from config when authentication changes
|
// Sync user tier from config when authentication changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only sync when not currently authenticating
|
// Only sync when not currently authenticating
|
||||||
if (!isAuthenticating) {
|
if (authState === AuthState.Authenticated) {
|
||||||
setUserTier(config.getGeminiClient()?.getUserTier());
|
setUserTier(config.getGeminiClient()?.getUserTier());
|
||||||
}
|
}
|
||||||
}, [config, isAuthenticating]);
|
}, [config, authState]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isEditorDialogOpen,
|
isEditorDialogOpen,
|
||||||
@@ -622,11 +588,6 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
return editorType as EditorType;
|
return editorType as EditorType;
|
||||||
}, [settings, openEditorDialog]);
|
}, [settings, openEditorDialog]);
|
||||||
|
|
||||||
const onAuthError = useCallback(() => {
|
|
||||||
setAuthError('reauth required');
|
|
||||||
openAuthDialog();
|
|
||||||
}, [openAuthDialog, setAuthError]);
|
|
||||||
|
|
||||||
// Core hooks and processors
|
// Core hooks and processors
|
||||||
const {
|
const {
|
||||||
vimEnabled: vimModeEnabled,
|
vimEnabled: vimModeEnabled,
|
||||||
@@ -650,7 +611,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
refreshStatic,
|
refreshStatic,
|
||||||
setDebugMessage,
|
setDebugMessage,
|
||||||
openThemeDialog,
|
openThemeDialog,
|
||||||
openAuthDialog,
|
setAuthState,
|
||||||
openEditorDialog,
|
openEditorDialog,
|
||||||
toggleCorgiMode,
|
toggleCorgiMode,
|
||||||
setQuittingMessages,
|
setQuittingMessages,
|
||||||
@@ -843,7 +804,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
handleSlashCommand('/ide status');
|
handleSlashCommand('/ide status');
|
||||||
} else if (keyMatchers[Command.QUIT](key)) {
|
} else if (keyMatchers[Command.QUIT](key)) {
|
||||||
// When authenticating, let AuthInProgress component handle Ctrl+C.
|
// When authenticating, let AuthInProgress component handle Ctrl+C.
|
||||||
if (isAuthenticating) {
|
if (authState === AuthState.Unauthenticated) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!ctrlCPressedOnce) {
|
if (!ctrlCPressedOnce) {
|
||||||
@@ -879,7 +840,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
setCtrlDPressedOnce,
|
setCtrlDPressedOnce,
|
||||||
ctrlDTimerRef,
|
ctrlDTimerRef,
|
||||||
handleSlashCommand,
|
handleSlashCommand,
|
||||||
isAuthenticating,
|
authState,
|
||||||
cancelOngoingRequest,
|
cancelOngoingRequest,
|
||||||
settings.merged.general?.debugKeystrokeLogging,
|
settings.merged.general?.debugKeystrokeLogging,
|
||||||
],
|
],
|
||||||
@@ -981,8 +942,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
if (
|
if (
|
||||||
initialPrompt &&
|
initialPrompt &&
|
||||||
!initialPromptSubmitted.current &&
|
!initialPromptSubmitted.current &&
|
||||||
!isAuthenticating &&
|
authState === AuthState.Authenticated &&
|
||||||
!isAuthDialogOpen &&
|
|
||||||
!isThemeDialogOpen &&
|
!isThemeDialogOpen &&
|
||||||
!isEditorDialogOpen &&
|
!isEditorDialogOpen &&
|
||||||
!showPrivacyNotice &&
|
!showPrivacyNotice &&
|
||||||
@@ -994,8 +954,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
}, [
|
}, [
|
||||||
initialPrompt,
|
initialPrompt,
|
||||||
submitQuery,
|
submitQuery,
|
||||||
isAuthenticating,
|
authState,
|
||||||
isAuthDialogOpen,
|
|
||||||
isThemeDialogOpen,
|
isThemeDialogOpen,
|
||||||
isEditorDialogOpen,
|
isEditorDialogOpen,
|
||||||
showPrivacyNotice,
|
showPrivacyNotice,
|
||||||
@@ -1136,7 +1095,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
|
|
||||||
if (choice === 'auth') {
|
if (choice === 'auth') {
|
||||||
cancelOngoingRequest?.();
|
cancelOngoingRequest?.();
|
||||||
openAuthDialog();
|
setAuthState(AuthState.Updating);
|
||||||
} else {
|
} else {
|
||||||
addItem(
|
addItem(
|
||||||
{
|
{
|
||||||
@@ -1209,13 +1168,11 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
onRestartRequest={() => process.exit(0)}
|
onRestartRequest={() => process.exit(0)}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
) : isAuthenticating ? (
|
) : authState === AuthState.Unauthenticated ? (
|
||||||
<>
|
<>
|
||||||
<AuthInProgress
|
<AuthInProgress
|
||||||
onTimeout={() => {
|
onTimeout={() => {
|
||||||
setAuthError('Authentication timed out. Please try again.');
|
onAuthError('Authentication timed out. Please try again.');
|
||||||
cancelAuthentication();
|
|
||||||
openAuthDialog();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{showErrorDetails && (
|
{showErrorDetails && (
|
||||||
@@ -1233,12 +1190,14 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
</OverflowProvider>
|
</OverflowProvider>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : isAuthDialogOpen ? (
|
) : authState === AuthState.Updating ? (
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
<AuthDialog
|
<AuthDialog
|
||||||
onSelect={handleAuthSelect}
|
config={config}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
initialErrorMessage={authError}
|
authError={authError}
|
||||||
|
onAuthError={onAuthError}
|
||||||
|
setAuthState={setAuthState}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
) : isEditorDialogOpen ? (
|
) : isEditorDialogOpen ? (
|
||||||
|
|||||||
@@ -0,0 +1,258 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { renderWithProviders } from '../../test-utils/render.js';
|
||||||
|
import {
|
||||||
|
describe,
|
||||||
|
it,
|
||||||
|
expect,
|
||||||
|
vi,
|
||||||
|
beforeEach,
|
||||||
|
afterEach,
|
||||||
|
type Mock,
|
||||||
|
} from 'vitest';
|
||||||
|
import { AuthDialog } from './AuthDialog.js';
|
||||||
|
import { AuthType, type Config } from '@google/gemini-cli-core';
|
||||||
|
import type { LoadedSettings } from '../../config/settings.js';
|
||||||
|
import { SettingScope } from '../../config/settings.js';
|
||||||
|
import { AuthState } from '../types.js';
|
||||||
|
import { RadioButtonSelect } from '../components/shared/RadioButtonSelect.js';
|
||||||
|
import { useKeypress } from '../hooks/useKeypress.js';
|
||||||
|
import { validateAuthMethodWithSettings } from './useAuth.js';
|
||||||
|
import { runExitCleanup } from '../../utils/cleanup.js';
|
||||||
|
import { clearCachedCredentialFile } from '@google/gemini-cli-core';
|
||||||
|
import { Text } from 'ink';
|
||||||
|
|
||||||
|
// Mocks
|
||||||
|
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||||
|
const actual =
|
||||||
|
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
clearCachedCredentialFile: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock('../../utils/cleanup.js', () => ({
|
||||||
|
runExitCleanup: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('./useAuth.js', () => ({
|
||||||
|
validateAuthMethodWithSettings: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../hooks/useKeypress.js', () => ({
|
||||||
|
useKeypress: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../components/shared/RadioButtonSelect.js', () => ({
|
||||||
|
RadioButtonSelect: vi.fn(({ items, initialIndex }) => (
|
||||||
|
<>
|
||||||
|
{items.map((item: { value: string; label: string }, index: number) => (
|
||||||
|
<Text key={item.value}>
|
||||||
|
{index === initialIndex ? '(selected)' : '(not selected)'}{' '}
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockedUseKeypress = useKeypress as Mock;
|
||||||
|
const mockedRadioButtonSelect = RadioButtonSelect as Mock;
|
||||||
|
const mockedValidateAuthMethod = validateAuthMethodWithSettings as Mock;
|
||||||
|
const mockedRunExitCleanup = runExitCleanup as Mock;
|
||||||
|
const mockedClearCachedCredentialFile = clearCachedCredentialFile as Mock;
|
||||||
|
|
||||||
|
describe('AuthDialog', () => {
|
||||||
|
let props: {
|
||||||
|
config: Config;
|
||||||
|
settings: LoadedSettings;
|
||||||
|
setAuthState: (state: AuthState) => void;
|
||||||
|
authError: string | null;
|
||||||
|
onAuthError: (error: string) => void;
|
||||||
|
};
|
||||||
|
const originalEnv = { ...process.env };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
process.env = {};
|
||||||
|
|
||||||
|
props = {
|
||||||
|
config: {
|
||||||
|
isBrowserLaunchSuppressed: vi.fn().mockReturnValue(false),
|
||||||
|
} as unknown as Config,
|
||||||
|
settings: {
|
||||||
|
merged: {
|
||||||
|
security: {
|
||||||
|
auth: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setValue: vi.fn(),
|
||||||
|
} as unknown as LoadedSettings,
|
||||||
|
setAuthState: vi.fn(),
|
||||||
|
authError: null,
|
||||||
|
onAuthError: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.env = originalEnv;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows Cloud Shell option when in Cloud Shell environment', () => {
|
||||||
|
process.env['CLOUD_SHELL'] = 'true';
|
||||||
|
renderWithProviders(<AuthDialog {...props} />);
|
||||||
|
const items = mockedRadioButtonSelect.mock.calls[0][0].items;
|
||||||
|
expect(items).toContainEqual({
|
||||||
|
label: 'Use Cloud Shell user credentials',
|
||||||
|
value: AuthType.CLOUD_SHELL,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters auth types when enforcedType is set', () => {
|
||||||
|
props.settings.merged.security!.auth!.enforcedType = AuthType.USE_GEMINI;
|
||||||
|
renderWithProviders(<AuthDialog {...props} />);
|
||||||
|
const items = mockedRadioButtonSelect.mock.calls[0][0].items;
|
||||||
|
expect(items).toHaveLength(1);
|
||||||
|
expect(items[0].value).toBe(AuthType.USE_GEMINI);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets initial index to 0 when enforcedType is set', () => {
|
||||||
|
props.settings.merged.security!.auth!.enforcedType = AuthType.USE_GEMINI;
|
||||||
|
renderWithProviders(<AuthDialog {...props} />);
|
||||||
|
const { initialIndex } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||||
|
expect(initialIndex).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selects initial auth type from settings', () => {
|
||||||
|
props.settings.merged.security!.auth!.selectedType = AuthType.USE_VERTEX_AI;
|
||||||
|
renderWithProviders(<AuthDialog {...props} />);
|
||||||
|
const { items, initialIndex } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||||
|
expect(items[initialIndex].value).toBe(AuthType.USE_VERTEX_AI);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selects initial auth type from GEMINI_DEFAULT_AUTH_TYPE env var', () => {
|
||||||
|
process.env['GEMINI_DEFAULT_AUTH_TYPE'] = AuthType.USE_GEMINI;
|
||||||
|
renderWithProviders(<AuthDialog {...props} />);
|
||||||
|
const { items, initialIndex } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||||
|
expect(items[initialIndex].value).toBe(AuthType.USE_GEMINI);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selects initial auth type from GEMINI_API_KEY env var', () => {
|
||||||
|
process.env['GEMINI_API_KEY'] = 'test-key';
|
||||||
|
renderWithProviders(<AuthDialog {...props} />);
|
||||||
|
const { items, initialIndex } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||||
|
expect(items[initialIndex].value).toBe(AuthType.USE_GEMINI);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults to Login with Google', () => {
|
||||||
|
renderWithProviders(<AuthDialog {...props} />);
|
||||||
|
const { items, initialIndex } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||||
|
expect(items[initialIndex].value).toBe(AuthType.LOGIN_WITH_GOOGLE);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleAuthSelect', () => {
|
||||||
|
it('calls onAuthError if validation fails', () => {
|
||||||
|
mockedValidateAuthMethod.mockReturnValue('Invalid method');
|
||||||
|
renderWithProviders(<AuthDialog {...props} />);
|
||||||
|
const { onSelect: handleAuthSelect } =
|
||||||
|
mockedRadioButtonSelect.mock.calls[0][0];
|
||||||
|
handleAuthSelect(AuthType.USE_GEMINI);
|
||||||
|
|
||||||
|
expect(mockedValidateAuthMethod).toHaveBeenCalledWith(
|
||||||
|
AuthType.USE_GEMINI,
|
||||||
|
props.settings,
|
||||||
|
);
|
||||||
|
expect(props.onAuthError).toHaveBeenCalledWith('Invalid method');
|
||||||
|
expect(props.settings.setValue).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onSelect if validation passes', async () => {
|
||||||
|
mockedValidateAuthMethod.mockReturnValue(null);
|
||||||
|
renderWithProviders(<AuthDialog {...props} />);
|
||||||
|
const { onSelect: handleAuthSelect } =
|
||||||
|
mockedRadioButtonSelect.mock.calls[0][0];
|
||||||
|
await handleAuthSelect(AuthType.USE_GEMINI);
|
||||||
|
|
||||||
|
expect(mockedValidateAuthMethod).toHaveBeenCalledWith(
|
||||||
|
AuthType.USE_GEMINI,
|
||||||
|
props.settings,
|
||||||
|
);
|
||||||
|
expect(props.onAuthError).not.toHaveBeenCalled();
|
||||||
|
expect(mockedClearCachedCredentialFile).toHaveBeenCalled();
|
||||||
|
expect(props.settings.setValue).toHaveBeenCalledWith(
|
||||||
|
SettingScope.User,
|
||||||
|
'security.auth.selectedType',
|
||||||
|
AuthType.USE_GEMINI,
|
||||||
|
);
|
||||||
|
expect(props.setAuthState).toHaveBeenCalledWith(
|
||||||
|
AuthState.Unauthenticated,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exits process for Login with Google when browser is suppressed', async () => {
|
||||||
|
const exitSpy = vi
|
||||||
|
.spyOn(process, 'exit')
|
||||||
|
.mockImplementation(() => undefined as never);
|
||||||
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||||
|
vi.mocked(props.config.isBrowserLaunchSuppressed).mockReturnValue(true);
|
||||||
|
mockedValidateAuthMethod.mockReturnValue(null);
|
||||||
|
|
||||||
|
renderWithProviders(<AuthDialog {...props} />);
|
||||||
|
const { onSelect: handleAuthSelect } =
|
||||||
|
mockedRadioButtonSelect.mock.calls[0][0];
|
||||||
|
await handleAuthSelect(AuthType.LOGIN_WITH_GOOGLE);
|
||||||
|
|
||||||
|
expect(mockedRunExitCleanup).toHaveBeenCalled();
|
||||||
|
expect(logSpy).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('Please restart Gemini CLI'),
|
||||||
|
);
|
||||||
|
expect(exitSpy).toHaveBeenCalledWith(0);
|
||||||
|
|
||||||
|
exitSpy.mockRestore();
|
||||||
|
logSpy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays authError when provided', () => {
|
||||||
|
props.authError = 'Something went wrong';
|
||||||
|
const { lastFrame } = renderWithProviders(<AuthDialog {...props} />);
|
||||||
|
expect(lastFrame()).toContain('Something went wrong');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('useKeypress', () => {
|
||||||
|
it('does nothing on escape if authError is present', () => {
|
||||||
|
props.authError = 'Some error';
|
||||||
|
renderWithProviders(<AuthDialog {...props} />);
|
||||||
|
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
|
||||||
|
keypressHandler({ name: 'escape' });
|
||||||
|
expect(props.onAuthError).not.toHaveBeenCalled();
|
||||||
|
expect(props.setAuthState).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onAuthError on escape if no auth method is set', () => {
|
||||||
|
props.settings.merged.security!.auth!.selectedType = undefined;
|
||||||
|
renderWithProviders(<AuthDialog {...props} />);
|
||||||
|
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
|
||||||
|
keypressHandler({ name: 'escape' });
|
||||||
|
expect(props.onAuthError).toHaveBeenCalledWith(
|
||||||
|
'You must select an auth method to proceed. Press Ctrl+C twice to exit.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onSelect(undefined) on escape if auth method is set', () => {
|
||||||
|
props.settings.merged.security!.auth!.selectedType = AuthType.USE_GEMINI;
|
||||||
|
renderWithProviders(<AuthDialog {...props} />);
|
||||||
|
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
|
||||||
|
keypressHandler({ name: 'escape' });
|
||||||
|
expect(props.setAuthState).toHaveBeenCalledWith(
|
||||||
|
AuthState.Unauthenticated,
|
||||||
|
);
|
||||||
|
expect(props.settings.setValue).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+59
-54
@@ -5,63 +5,37 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { useState } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import { Colors } from '../colors.js';
|
import { Colors } from '../colors.js';
|
||||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
import { RadioButtonSelect } from '../components/shared/RadioButtonSelect.js';
|
||||||
import type { LoadedSettings } from '../../config/settings.js';
|
import type { LoadedSettings } from '../../config/settings.js';
|
||||||
import { SettingScope } from '../../config/settings.js';
|
import { SettingScope } from '../../config/settings.js';
|
||||||
import { AuthType } from '@google/gemini-cli-core';
|
import {
|
||||||
import { validateAuthMethod } from '../../config/auth.js';
|
AuthType,
|
||||||
|
clearCachedCredentialFile,
|
||||||
|
type Config,
|
||||||
|
} from '@google/gemini-cli-core';
|
||||||
import { useKeypress } from '../hooks/useKeypress.js';
|
import { useKeypress } from '../hooks/useKeypress.js';
|
||||||
|
import { AuthState } from '../types.js';
|
||||||
|
import { runExitCleanup } from '../../utils/cleanup.js';
|
||||||
|
import { validateAuthMethodWithSettings } from './useAuth.js';
|
||||||
|
|
||||||
interface AuthDialogProps {
|
interface AuthDialogProps {
|
||||||
onSelect: (authMethod: AuthType | undefined, scope: SettingScope) => void;
|
config: Config;
|
||||||
settings: LoadedSettings;
|
settings: LoadedSettings;
|
||||||
initialErrorMessage?: string | null;
|
setAuthState: (state: AuthState) => void;
|
||||||
}
|
authError: string | null;
|
||||||
|
onAuthError: (error: string) => void;
|
||||||
function parseDefaultAuthType(
|
|
||||||
defaultAuthType: string | undefined,
|
|
||||||
): AuthType | null {
|
|
||||||
if (
|
|
||||||
defaultAuthType &&
|
|
||||||
Object.values(AuthType).includes(defaultAuthType as AuthType)
|
|
||||||
) {
|
|
||||||
return defaultAuthType as AuthType;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AuthDialog({
|
export function AuthDialog({
|
||||||
onSelect,
|
config,
|
||||||
settings,
|
settings,
|
||||||
initialErrorMessage,
|
setAuthState,
|
||||||
|
authError,
|
||||||
|
onAuthError,
|
||||||
}: AuthDialogProps): React.JSX.Element {
|
}: AuthDialogProps): React.JSX.Element {
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(() => {
|
|
||||||
if (initialErrorMessage) {
|
|
||||||
return initialErrorMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultAuthType = parseDefaultAuthType(
|
|
||||||
process.env['GEMINI_DEFAULT_AUTH_TYPE'],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (process.env['GEMINI_DEFAULT_AUTH_TYPE'] && defaultAuthType === null) {
|
|
||||||
return (
|
|
||||||
`Invalid value for GEMINI_DEFAULT_AUTH_TYPE: "${process.env['GEMINI_DEFAULT_AUTH_TYPE']}". ` +
|
|
||||||
`Valid values are: ${Object.values(AuthType).join(', ')}.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
process.env['GEMINI_API_KEY'] &&
|
|
||||||
(!defaultAuthType || defaultAuthType === AuthType.USE_GEMINI)
|
|
||||||
) {
|
|
||||||
return 'Existing API key detected (GEMINI_API_KEY). Select "Gemini API Key" option to use it.';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
let items = [
|
let items = [
|
||||||
{
|
{
|
||||||
label: 'Login with Google',
|
label: 'Login with Google',
|
||||||
@@ -88,14 +62,20 @@ export function AuthDialog({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let defaultAuthType = null;
|
||||||
|
const defaultAuthTypeEnv = process.env['GEMINI_DEFAULT_AUTH_TYPE'];
|
||||||
|
if (
|
||||||
|
defaultAuthTypeEnv &&
|
||||||
|
Object.values(AuthType).includes(defaultAuthTypeEnv as AuthType)
|
||||||
|
) {
|
||||||
|
defaultAuthType = defaultAuthTypeEnv as AuthType;
|
||||||
|
}
|
||||||
|
|
||||||
let initialAuthIndex = items.findIndex((item) => {
|
let initialAuthIndex = items.findIndex((item) => {
|
||||||
if (settings.merged.security?.auth?.selectedType) {
|
if (settings.merged.security?.auth?.selectedType) {
|
||||||
return item.value === settings.merged.security.auth.selectedType;
|
return item.value === settings.merged.security.auth.selectedType;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultAuthType = parseDefaultAuthType(
|
|
||||||
process.env['GEMINI_DEFAULT_AUTH_TYPE'],
|
|
||||||
);
|
|
||||||
if (defaultAuthType) {
|
if (defaultAuthType) {
|
||||||
return item.value === defaultAuthType;
|
return item.value === defaultAuthType;
|
||||||
}
|
}
|
||||||
@@ -110,12 +90,37 @@ export function AuthDialog({
|
|||||||
initialAuthIndex = 0;
|
initialAuthIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onSelect = useCallback(
|
||||||
|
async (authType: AuthType | undefined, scope: SettingScope) => {
|
||||||
|
if (authType) {
|
||||||
|
await clearCachedCredentialFile();
|
||||||
|
|
||||||
|
settings.setValue(scope, 'security.auth.selectedType', authType);
|
||||||
|
if (
|
||||||
|
authType === AuthType.LOGIN_WITH_GOOGLE &&
|
||||||
|
config.isBrowserLaunchSuppressed()
|
||||||
|
) {
|
||||||
|
runExitCleanup();
|
||||||
|
console.log(
|
||||||
|
`
|
||||||
|
----------------------------------------------------------------
|
||||||
|
Logging in with Google... Please restart Gemini CLI to continue.
|
||||||
|
----------------------------------------------------------------
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setAuthState(AuthState.Unauthenticated);
|
||||||
|
},
|
||||||
|
[settings, config, setAuthState],
|
||||||
|
);
|
||||||
|
|
||||||
const handleAuthSelect = (authMethod: AuthType) => {
|
const handleAuthSelect = (authMethod: AuthType) => {
|
||||||
const error = validateAuthMethod(authMethod);
|
const error = validateAuthMethodWithSettings(authMethod, settings);
|
||||||
if (error) {
|
if (error) {
|
||||||
setErrorMessage(error);
|
onAuthError(error);
|
||||||
} else {
|
} else {
|
||||||
setErrorMessage(null);
|
|
||||||
onSelect(authMethod, SettingScope.User);
|
onSelect(authMethod, SettingScope.User);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -125,12 +130,12 @@ export function AuthDialog({
|
|||||||
if (key.name === 'escape') {
|
if (key.name === 'escape') {
|
||||||
// Prevent exit if there is an error message.
|
// Prevent exit if there is an error message.
|
||||||
// This means they user is not authenticated yet.
|
// This means they user is not authenticated yet.
|
||||||
if (errorMessage) {
|
if (authError) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (settings.merged.security?.auth?.selectedType === undefined) {
|
if (settings.merged.security?.auth?.selectedType === undefined) {
|
||||||
// Prevent exiting if no auth method is set
|
// Prevent exiting if no auth method is set
|
||||||
setErrorMessage(
|
onAuthError(
|
||||||
'You must select an auth method to proceed. Press Ctrl+C twice to exit.',
|
'You must select an auth method to proceed. Press Ctrl+C twice to exit.',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -160,9 +165,9 @@ export function AuthDialog({
|
|||||||
onSelect={handleAuthSelect}
|
onSelect={handleAuthSelect}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
{errorMessage && (
|
{authError && (
|
||||||
<Box marginTop={1}>
|
<Box marginTop={1}>
|
||||||
<Text color={Colors.AccentRed}>{errorMessage}</Text>
|
<Text color={Colors.AccentRed}>{authError}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
<Box marginTop={1}>
|
<Box marginTop={1}>
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import type { LoadedSettings } from '../../config/settings.js';
|
||||||
|
import { AuthType, type Config } from '@google/gemini-cli-core';
|
||||||
|
import { getErrorMessage } from '@google/gemini-cli-core';
|
||||||
|
import { AuthState } from '../types.js';
|
||||||
|
import { validateAuthMethod } from '../../config/auth.js';
|
||||||
|
|
||||||
|
export function validateAuthMethodWithSettings(
|
||||||
|
authType: AuthType,
|
||||||
|
settings: LoadedSettings,
|
||||||
|
): string | null {
|
||||||
|
const enforcedType = settings.merged.security?.auth?.enforcedType;
|
||||||
|
if (enforcedType && enforcedType !== authType) {
|
||||||
|
return `Authentication is enforced to be ${enforcedType}, but you are currently using ${authType}.`;
|
||||||
|
}
|
||||||
|
if (settings.merged.security?.auth?.useExternal) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return validateAuthMethod(authType);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuthCommand = (settings: LoadedSettings, config: Config) => {
|
||||||
|
const [authState, setAuthState] = useState<AuthState>(
|
||||||
|
AuthState.Unauthenticated,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [authError, setAuthError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const onAuthError = useCallback(
|
||||||
|
(error: string) => {
|
||||||
|
setAuthError(error);
|
||||||
|
setAuthState(AuthState.Updating);
|
||||||
|
},
|
||||||
|
[setAuthError, setAuthState],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
if (authState !== AuthState.Unauthenticated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authType = settings.merged.security?.auth?.selectedType;
|
||||||
|
if (!authType) {
|
||||||
|
if (process.env['GEMINI_API_KEY']) {
|
||||||
|
onAuthError(
|
||||||
|
'Existing API key detected (GEMINI_API_KEY). Select "Gemini API Key" option to use it.',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
onAuthError('No authentication method selected.');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const error = validateAuthMethodWithSettings(authType, settings);
|
||||||
|
if (error) {
|
||||||
|
onAuthError(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultAuthType = process.env['GEMINI_DEFAULT_AUTH_TYPE'];
|
||||||
|
if (
|
||||||
|
defaultAuthType &&
|
||||||
|
!Object.values(AuthType).includes(defaultAuthType as AuthType)
|
||||||
|
) {
|
||||||
|
onAuthError(
|
||||||
|
`Invalid value for GEMINI_DEFAULT_AUTH_TYPE: "${defaultAuthType}". ` +
|
||||||
|
`Valid values are: ${Object.values(AuthType).join(', ')}.`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await config.refreshAuth(authType);
|
||||||
|
|
||||||
|
console.log(`Authenticated via "${authType}".`);
|
||||||
|
setAuthError(null);
|
||||||
|
setAuthState(AuthState.Authenticated);
|
||||||
|
} catch (e) {
|
||||||
|
onAuthError(`Failed to login. Message: ${getErrorMessage(e)}`);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [settings, config, authState, setAuthState, setAuthError, onAuthError]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
authState,
|
||||||
|
setAuthState,
|
||||||
|
authError,
|
||||||
|
onAuthError,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,473 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2025 Google LLC
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
||||||
import { AuthDialog } from './AuthDialog.js';
|
|
||||||
import { LoadedSettings, SettingScope } from '../../config/settings.js';
|
|
||||||
import { AuthType } from '@google/gemini-cli-core';
|
|
||||||
import { renderWithProviders } from '../../test-utils/render.js';
|
|
||||||
|
|
||||||
describe('AuthDialog', () => {
|
|
||||||
const wait = (ms = 50) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
|
|
||||||
let originalEnv: NodeJS.ProcessEnv;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
originalEnv = { ...process.env };
|
|
||||||
process.env['GEMINI_API_KEY'] = '';
|
|
||||||
process.env['GEMINI_DEFAULT_AUTH_TYPE'] = '';
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
process.env = originalEnv;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show an error if the initial auth type is invalid', () => {
|
|
||||||
process.env['GEMINI_API_KEY'] = '';
|
|
||||||
|
|
||||||
const settings: LoadedSettings = new LoadedSettings(
|
|
||||||
{
|
|
||||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: {},
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: {
|
|
||||||
security: {
|
|
||||||
auth: {
|
|
||||||
selectedType: AuthType.USE_GEMINI,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
new Set(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { lastFrame } = renderWithProviders(
|
|
||||||
<AuthDialog
|
|
||||||
onSelect={() => {}}
|
|
||||||
settings={settings}
|
|
||||||
initialErrorMessage="GEMINI_API_KEY environment variable not found"
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(lastFrame()).toContain(
|
|
||||||
'GEMINI_API_KEY environment variable not found',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GEMINI_API_KEY environment variable', () => {
|
|
||||||
it('should detect GEMINI_API_KEY environment variable', () => {
|
|
||||||
process.env['GEMINI_API_KEY'] = 'foobar';
|
|
||||||
|
|
||||||
const settings: LoadedSettings = new LoadedSettings(
|
|
||||||
{
|
|
||||||
settings: {
|
|
||||||
security: { auth: { selectedType: undefined } },
|
|
||||||
ui: { customThemes: {} },
|
|
||||||
mcpServers: {},
|
|
||||||
},
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: {},
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
new Set(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { lastFrame } = renderWithProviders(
|
|
||||||
<AuthDialog onSelect={() => {}} settings={settings} />,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(lastFrame()).toContain(
|
|
||||||
'Existing API key detected (GEMINI_API_KEY)',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not show the GEMINI_API_KEY message if GEMINI_DEFAULT_AUTH_TYPE is set to something else', () => {
|
|
||||||
process.env['GEMINI_API_KEY'] = 'foobar';
|
|
||||||
process.env['GEMINI_DEFAULT_AUTH_TYPE'] = AuthType.LOGIN_WITH_GOOGLE;
|
|
||||||
|
|
||||||
const settings: LoadedSettings = new LoadedSettings(
|
|
||||||
{
|
|
||||||
settings: {
|
|
||||||
security: { auth: { selectedType: undefined } },
|
|
||||||
ui: { customThemes: {} },
|
|
||||||
mcpServers: {},
|
|
||||||
},
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: {},
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
new Set(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { lastFrame } = renderWithProviders(
|
|
||||||
<AuthDialog onSelect={() => {}} settings={settings} />,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(lastFrame()).not.toContain(
|
|
||||||
'Existing API key detected (GEMINI_API_KEY)',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show the GEMINI_API_KEY message if GEMINI_DEFAULT_AUTH_TYPE is set to use api key', () => {
|
|
||||||
process.env['GEMINI_API_KEY'] = 'foobar';
|
|
||||||
process.env['GEMINI_DEFAULT_AUTH_TYPE'] = AuthType.USE_GEMINI;
|
|
||||||
|
|
||||||
const settings: LoadedSettings = new LoadedSettings(
|
|
||||||
{
|
|
||||||
settings: {
|
|
||||||
security: { auth: { selectedType: undefined } },
|
|
||||||
ui: { customThemes: {} },
|
|
||||||
mcpServers: {},
|
|
||||||
},
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: {},
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
new Set(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { lastFrame } = renderWithProviders(
|
|
||||||
<AuthDialog onSelect={() => {}} settings={settings} />,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(lastFrame()).toContain(
|
|
||||||
'Existing API key detected (GEMINI_API_KEY)',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GEMINI_DEFAULT_AUTH_TYPE environment variable', () => {
|
|
||||||
it('should select the auth type specified by GEMINI_DEFAULT_AUTH_TYPE', () => {
|
|
||||||
process.env['GEMINI_DEFAULT_AUTH_TYPE'] = AuthType.LOGIN_WITH_GOOGLE;
|
|
||||||
|
|
||||||
const settings: LoadedSettings = new LoadedSettings(
|
|
||||||
{
|
|
||||||
settings: {
|
|
||||||
security: { auth: { selectedType: undefined } },
|
|
||||||
ui: { customThemes: {} },
|
|
||||||
mcpServers: {},
|
|
||||||
},
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: {},
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
new Set(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { lastFrame } = renderWithProviders(
|
|
||||||
<AuthDialog onSelect={() => {}} settings={settings} />,
|
|
||||||
);
|
|
||||||
|
|
||||||
// This is a bit brittle, but it's the best way to check which item is selected.
|
|
||||||
expect(lastFrame()).toContain('● 1. Login with Google');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fall back to default if GEMINI_DEFAULT_AUTH_TYPE is not set', () => {
|
|
||||||
const settings: LoadedSettings = new LoadedSettings(
|
|
||||||
{
|
|
||||||
settings: {
|
|
||||||
security: { auth: { selectedType: undefined } },
|
|
||||||
ui: { customThemes: {} },
|
|
||||||
mcpServers: {},
|
|
||||||
},
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: {},
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
new Set(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { lastFrame } = renderWithProviders(
|
|
||||||
<AuthDialog onSelect={() => {}} settings={settings} />,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Default is LOGIN_WITH_GOOGLE
|
|
||||||
expect(lastFrame()).toContain('● 1. Login with Google');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show an error and fall back to default if GEMINI_DEFAULT_AUTH_TYPE is invalid', () => {
|
|
||||||
process.env['GEMINI_DEFAULT_AUTH_TYPE'] = 'invalid-auth-type';
|
|
||||||
|
|
||||||
const settings: LoadedSettings = new LoadedSettings(
|
|
||||||
{
|
|
||||||
settings: {
|
|
||||||
security: { auth: { selectedType: undefined } },
|
|
||||||
ui: { customThemes: {} },
|
|
||||||
mcpServers: {},
|
|
||||||
},
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: {},
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
new Set(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { lastFrame } = renderWithProviders(
|
|
||||||
<AuthDialog onSelect={() => {}} settings={settings} />,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(lastFrame()).toContain(
|
|
||||||
'Invalid value for GEMINI_DEFAULT_AUTH_TYPE: "invalid-auth-type"',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Default is LOGIN_WITH_GOOGLE
|
|
||||||
expect(lastFrame()).toContain('● 1. Login with Google');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should prevent exiting when no auth method is selected and show error message', async () => {
|
|
||||||
const onSelect = vi.fn();
|
|
||||||
const settings: LoadedSettings = new LoadedSettings(
|
|
||||||
{
|
|
||||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: {},
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: {
|
|
||||||
security: { auth: { selectedType: undefined } },
|
|
||||||
ui: { customThemes: {} },
|
|
||||||
mcpServers: {},
|
|
||||||
},
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
new Set(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { lastFrame, stdin, unmount } = renderWithProviders(
|
|
||||||
<AuthDialog onSelect={onSelect} settings={settings} />,
|
|
||||||
);
|
|
||||||
await wait();
|
|
||||||
|
|
||||||
// Simulate pressing escape key
|
|
||||||
stdin.write('\u001b'); // ESC key
|
|
||||||
await wait();
|
|
||||||
|
|
||||||
// Should show error message instead of calling onSelect
|
|
||||||
expect(lastFrame()).toContain(
|
|
||||||
'You must select an auth method to proceed. Press Ctrl+C twice to exit.',
|
|
||||||
);
|
|
||||||
expect(onSelect).not.toHaveBeenCalled();
|
|
||||||
unmount();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not exit if there is already an error message', async () => {
|
|
||||||
const onSelect = vi.fn();
|
|
||||||
const settings: LoadedSettings = new LoadedSettings(
|
|
||||||
{
|
|
||||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: {},
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: {
|
|
||||||
security: { auth: { selectedType: undefined } },
|
|
||||||
ui: { customThemes: {} },
|
|
||||||
mcpServers: {},
|
|
||||||
},
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
new Set(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { lastFrame, stdin, unmount } = renderWithProviders(
|
|
||||||
<AuthDialog
|
|
||||||
onSelect={onSelect}
|
|
||||||
settings={settings}
|
|
||||||
initialErrorMessage="Initial error"
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
await wait();
|
|
||||||
|
|
||||||
expect(lastFrame()).toContain('Initial error');
|
|
||||||
|
|
||||||
// Simulate pressing escape key
|
|
||||||
stdin.write('\u001b'); // ESC key
|
|
||||||
await wait();
|
|
||||||
|
|
||||||
// Should not call onSelect
|
|
||||||
expect(onSelect).not.toHaveBeenCalled();
|
|
||||||
unmount();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should allow exiting when auth method is already selected', async () => {
|
|
||||||
const onSelect = vi.fn();
|
|
||||||
const settings: LoadedSettings = new LoadedSettings(
|
|
||||||
{
|
|
||||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: {},
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: {
|
|
||||||
security: { auth: { selectedType: AuthType.LOGIN_WITH_GOOGLE } },
|
|
||||||
ui: { customThemes: {} },
|
|
||||||
mcpServers: {},
|
|
||||||
},
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: { ui: { customThemes: {} }, mcpServers: {} },
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
new Set(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { stdin, unmount } = renderWithProviders(
|
|
||||||
<AuthDialog onSelect={onSelect} settings={settings} />,
|
|
||||||
);
|
|
||||||
await wait();
|
|
||||||
|
|
||||||
// Simulate pressing escape key
|
|
||||||
stdin.write('\u001b'); // ESC key
|
|
||||||
await wait();
|
|
||||||
|
|
||||||
// Should call onSelect with undefined to exit
|
|
||||||
expect(onSelect).toHaveBeenCalledWith(undefined, SettingScope.User);
|
|
||||||
unmount();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('enforcedAuthType', () => {
|
|
||||||
it('should display the enforced auth type message if enforcedAuthType is set', () => {
|
|
||||||
const settings: LoadedSettings = new LoadedSettings(
|
|
||||||
{
|
|
||||||
settings: {
|
|
||||||
security: {
|
|
||||||
auth: {
|
|
||||||
enforcedType: AuthType.USE_VERTEX_AI,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: {
|
|
||||||
security: {
|
|
||||||
auth: {
|
|
||||||
selectedType: AuthType.USE_VERTEX_AI,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: {},
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
settings: {},
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
new Set(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { lastFrame } = renderWithProviders(
|
|
||||||
<AuthDialog onSelect={() => {}} settings={settings} />,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(lastFrame()).toContain('1. Vertex AI');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -21,11 +21,12 @@ import {
|
|||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import { useSessionStats } from '../contexts/SessionContext.js';
|
import { useSessionStats } from '../contexts/SessionContext.js';
|
||||||
import { runExitCleanup } from '../../utils/cleanup.js';
|
import { runExitCleanup } from '../../utils/cleanup.js';
|
||||||
import type {
|
import {
|
||||||
Message,
|
type Message,
|
||||||
HistoryItemWithoutId,
|
type HistoryItemWithoutId,
|
||||||
HistoryItem,
|
type HistoryItem,
|
||||||
SlashCommandProcessorResult,
|
type SlashCommandProcessorResult,
|
||||||
|
AuthState,
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
import { MessageType } from '../types.js';
|
import { MessageType } from '../types.js';
|
||||||
import type { LoadedSettings } from '../../config/settings.js';
|
import type { LoadedSettings } from '../../config/settings.js';
|
||||||
@@ -47,7 +48,7 @@ export const useSlashCommandProcessor = (
|
|||||||
refreshStatic: () => void,
|
refreshStatic: () => void,
|
||||||
onDebugMessage: (message: string) => void,
|
onDebugMessage: (message: string) => void,
|
||||||
openThemeDialog: () => void,
|
openThemeDialog: () => void,
|
||||||
openAuthDialog: () => void,
|
setAuthState: (state: AuthState) => void,
|
||||||
openEditorDialog: () => void,
|
openEditorDialog: () => void,
|
||||||
toggleCorgiMode: () => void,
|
toggleCorgiMode: () => void,
|
||||||
setQuittingMessages: (message: HistoryItem[]) => void,
|
setQuittingMessages: (message: HistoryItem[]) => void,
|
||||||
@@ -375,7 +376,7 @@ export const useSlashCommandProcessor = (
|
|||||||
case 'dialog':
|
case 'dialog':
|
||||||
switch (result.dialog) {
|
switch (result.dialog) {
|
||||||
case 'auth':
|
case 'auth':
|
||||||
openAuthDialog();
|
setAuthState(AuthState.Updating);
|
||||||
return { type: 'handled' };
|
return { type: 'handled' };
|
||||||
case 'theme':
|
case 'theme':
|
||||||
openThemeDialog();
|
openThemeDialog();
|
||||||
@@ -554,7 +555,7 @@ export const useSlashCommandProcessor = (
|
|||||||
[
|
[
|
||||||
config,
|
config,
|
||||||
addItem,
|
addItem,
|
||||||
openAuthDialog,
|
setAuthState,
|
||||||
commands,
|
commands,
|
||||||
commandContext,
|
commandContext,
|
||||||
addMessage,
|
addMessage,
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2025 Google LLC
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useState, useCallback, useEffect } from 'react';
|
|
||||||
import type { LoadedSettings, SettingScope } from '../../config/settings.js';
|
|
||||||
import { AuthType, type Config } from '@google/gemini-cli-core';
|
|
||||||
import {
|
|
||||||
clearCachedCredentialFile,
|
|
||||||
getErrorMessage,
|
|
||||||
} from '@google/gemini-cli-core';
|
|
||||||
import { runExitCleanup } from '../../utils/cleanup.js';
|
|
||||||
|
|
||||||
export const useAuthCommand = (
|
|
||||||
settings: LoadedSettings,
|
|
||||||
setAuthError: (error: string | null) => void,
|
|
||||||
config: Config,
|
|
||||||
) => {
|
|
||||||
const [isAuthDialogOpen, setIsAuthDialogOpen] = useState(
|
|
||||||
settings.merged.security?.auth?.selectedType === undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
const openAuthDialog = useCallback(() => {
|
|
||||||
setIsAuthDialogOpen(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const authFlow = async () => {
|
|
||||||
const authType = settings.merged.security?.auth?.selectedType;
|
|
||||||
if (isAuthDialogOpen || !authType) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setIsAuthenticating(true);
|
|
||||||
await config.refreshAuth(authType);
|
|
||||||
console.log(`Authenticated via "${authType}".`);
|
|
||||||
} catch (e) {
|
|
||||||
setAuthError(`Failed to login. Message: ${getErrorMessage(e)}`);
|
|
||||||
openAuthDialog();
|
|
||||||
} finally {
|
|
||||||
setIsAuthenticating(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
void authFlow();
|
|
||||||
}, [isAuthDialogOpen, settings, config, setAuthError, openAuthDialog]);
|
|
||||||
|
|
||||||
const handleAuthSelect = useCallback(
|
|
||||||
async (authType: AuthType | undefined, scope: SettingScope) => {
|
|
||||||
if (authType) {
|
|
||||||
await clearCachedCredentialFile();
|
|
||||||
|
|
||||||
settings.setValue(scope, 'security.auth.selectedType', authType);
|
|
||||||
if (
|
|
||||||
authType === AuthType.LOGIN_WITH_GOOGLE &&
|
|
||||||
config.isBrowserLaunchSuppressed()
|
|
||||||
) {
|
|
||||||
runExitCleanup();
|
|
||||||
console.log(
|
|
||||||
`
|
|
||||||
----------------------------------------------------------------
|
|
||||||
Logging in with Google... Please restart Gemini CLI to continue.
|
|
||||||
----------------------------------------------------------------
|
|
||||||
`,
|
|
||||||
);
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setIsAuthDialogOpen(false);
|
|
||||||
setAuthError(null);
|
|
||||||
},
|
|
||||||
[settings, setAuthError, config],
|
|
||||||
);
|
|
||||||
|
|
||||||
const cancelAuthentication = useCallback(() => {
|
|
||||||
setIsAuthenticating(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
|
||||||
isAuthDialogOpen,
|
|
||||||
openAuthDialog,
|
|
||||||
handleAuthSelect,
|
|
||||||
isAuthenticating,
|
|
||||||
cancelAuthentication,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -95,7 +95,7 @@ export const useGeminiStream = (
|
|||||||
) => Promise<SlashCommandProcessorResult | false>,
|
) => Promise<SlashCommandProcessorResult | false>,
|
||||||
shellModeActive: boolean,
|
shellModeActive: boolean,
|
||||||
getPreferredEditor: () => EditorType | undefined,
|
getPreferredEditor: () => EditorType | undefined,
|
||||||
onAuthError: () => void,
|
onAuthError: (error: string) => void,
|
||||||
performMemoryRefresh: () => Promise<void>,
|
performMemoryRefresh: () => Promise<void>,
|
||||||
modelSwitchedFromQuotaError: boolean,
|
modelSwitchedFromQuotaError: boolean,
|
||||||
setModelSwitchedFromQuotaError: React.Dispatch<React.SetStateAction<boolean>>,
|
setModelSwitchedFromQuotaError: React.Dispatch<React.SetStateAction<boolean>>,
|
||||||
@@ -751,7 +751,7 @@ export const useGeminiStream = (
|
|||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (error instanceof UnauthorizedError) {
|
if (error instanceof UnauthorizedError) {
|
||||||
onAuthError();
|
onAuthError('Session expired or is unauthorized.');
|
||||||
} else if (!isNodeError(error) || error.name !== 'AbortError') {
|
} else if (!isNodeError(error) || error.name !== 'AbortError') {
|
||||||
addItem(
|
addItem(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,6 +11,15 @@ import type {
|
|||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import type { PartListUnion } from '@google/genai';
|
import type { PartListUnion } from '@google/genai';
|
||||||
|
|
||||||
|
export enum AuthState {
|
||||||
|
// Attemtping to authenticate or re-authenticate
|
||||||
|
Unauthenticated = 'unauthenticated',
|
||||||
|
// Auth dialog is open for user to select auth method
|
||||||
|
Updating = 'updating',
|
||||||
|
// Successfully authenticated
|
||||||
|
Authenticated = 'authenticated',
|
||||||
|
}
|
||||||
|
|
||||||
// Only defining the state enum needed by the UI
|
// Only defining the state enum needed by the UI
|
||||||
export enum StreamingState {
|
export enum StreamingState {
|
||||||
Idle = 'idle',
|
Idle = 'idle',
|
||||||
|
|||||||
Reference in New Issue
Block a user