2025-04-18 17:44:24 -07:00
/ * *
* @license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
2025-09-17 15:37:13 -07:00
import React from 'react' ;
2025-11-03 13:41:58 -08:00
import { render } from 'ink' ;
2025-09-06 01:39:02 -04:00
import { AppContainer } from './ui/AppContainer.js' ;
2025-08-07 14:19:06 -07:00
import { loadCliConfig , parseArguments } from './config/config.js' ;
2025-09-22 19:48:25 -07:00
import * as cliConfig from './config/config.js' ;
2025-04-21 17:41:44 -07:00
import { readStdin } from './utils/readStdin.js' ;
2025-06-10 18:34:36 -07:00
import { basename } from 'node:path' ;
2026-02-20 13:22:45 -05:00
import { createHash } from 'node:crypto' ;
2025-06-24 21:18:55 +00:00
import v8 from 'node:v8' ;
import os from 'node:os' ;
2025-08-01 12:30:39 -07:00
import dns from 'node:dns' ;
2025-06-18 10:01:00 -07:00
import { start_sandbox } from './utils/sandbox.js' ;
2025-08-26 00:04:53 +02:00
import type { DnsResolutionOrder , LoadedSettings } from './config/settings.js' ;
2025-12-23 10:26:37 -08:00
import {
loadTrustedFolders ,
type TrustedFoldersError ,
} from './config/trustedFolders.js' ;
2026-01-09 14:34:23 -08:00
import { loadSettings , SettingScope } from './config/settings.js' ;
2025-05-14 00:12:04 +00:00
import { getStartupWarnings } from './utils/startupWarnings.js' ;
2025-07-05 23:27:00 -07:00
import { getUserStartupWarnings } from './utils/userStartupWarnings.js' ;
2025-08-15 04:25:27 +00:00
import { ConsolePatcher } from './ui/utils/ConsolePatcher.js' ;
2025-06-01 16:11:37 -07:00
import { runNonInteractive } from './nonInteractiveCli.js' ;
2025-08-29 12:53:39 -04:00
import {
cleanupCheckpoints ,
registerCleanup ,
2025-11-20 10:44:02 -08:00
registerSyncCleanup ,
2025-08-29 12:53:39 -04:00
runExitCleanup ,
2025-12-03 09:04:13 -08:00
registerTelemetryConfig ,
2026-02-27 04:47:09 +05:30
setupSignalHandlers ,
setupTtyCheck ,
2025-08-29 12:53:39 -04:00
} from './utils/cleanup.js' ;
2026-01-29 15:20:11 -08:00
import {
cleanupToolOutputFiles ,
cleanupExpiredSessions ,
} from './utils/sessionCleanup.js' ;
2025-06-01 16:11:37 -07:00
import {
2026-02-20 13:22:45 -05:00
type StartupWarning ,
WarningPriority ,
2025-11-21 08:31:47 -08:00
type Config ,
type ResumedSessionData ,
type OutputPayload ,
type ConsoleLogPayload ,
2025-12-29 15:46:10 -05:00
type UserFeedbackPayload ,
2025-06-11 04:46:39 +00:00
sessionId ,
2025-06-13 03:44:17 -04:00
logUserPrompt ,
2025-06-19 16:52:22 -07:00
AuthType ,
2025-07-10 18:59:02 -07:00
getOauthClient ,
2025-10-09 16:02:58 -07:00
UserPromptEvent ,
2025-10-20 18:16:47 -04:00
debugLogger ,
2025-10-31 15:51:05 -04:00
recordSlowRender ,
2025-11-20 10:44:02 -08:00
coreEvents ,
CoreEvent ,
2025-12-02 15:08:25 -08:00
createWorkingStdio ,
2025-11-20 14:16:46 -08:00
patchStdio ,
writeToStdout ,
writeToStderr ,
2025-11-21 08:31:47 -08:00
disableMouseEvents ,
enableMouseEvents ,
disableLineWrapping ,
2026-02-11 09:29:18 -08:00
enableLineWrapping ,
2025-11-21 08:31:47 -08:00
shouldEnterAlternateScreen ,
2025-12-01 10:06:13 -08:00
startupProfiler ,
2025-11-26 08:13:21 +05:30
ExitCodes ,
2025-12-03 09:04:13 -08:00
SessionStartSource ,
SessionEndReason ,
2025-12-09 16:38:33 -08:00
getVersion ,
2026-01-26 06:31:19 -08:00
ValidationCancelledError ,
ValidationRequiredError ,
2026-02-03 16:19:14 -05:00
type AdminControlsSettings ,
2025-06-25 05:41:11 -07:00
} from '@google/gemini-cli-core' ;
2025-09-06 01:39:02 -04:00
import {
initializeApp ,
type InitializationResult ,
} from './core/initializer.js' ;
2025-06-19 16:52:22 -07:00
import { validateAuthMethod } from './config/auth.js' ;
2026-03-05 14:57:28 -05:00
import { runAcpClient } from './acp/acpClient.js' ;
2025-07-22 10:52:40 -04:00
import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js' ;
2025-07-28 17:56:52 -07:00
import { checkForUpdates } from './ui/utils/updateCheck.js' ;
import { handleAutoUpdate } from './utils/handleAutoUpdate.js' ;
2025-07-25 17:35:26 -07:00
import { appEvents , AppEvent } from './utils/events.js' ;
2026-03-06 11:02:33 -08:00
import { SessionError , SessionSelector } from './utils/sessionUtils.js' ;
2025-08-20 12:49:15 -07:00
import { SettingsContext } from './ui/contexts/SettingsContext.js' ;
2025-11-03 13:41:58 -08:00
import { MouseProvider } from './ui/contexts/MouseContext.js' ;
2026-01-12 17:18:14 -08:00
import { StreamingState } from './ui/types.js' ;
import { computeTerminalTitle } from './utils/windowTitle.js' ;
2025-09-17 00:16:05 +09:00
2025-09-06 01:39:02 -04:00
import { SessionStatsProvider } from './ui/contexts/SessionContext.js' ;
import { VimModeProvider } from './ui/contexts/VimModeContext.js' ;
import { KeypressProvider } from './ui/contexts/KeypressContext.js' ;
import { useKittyKeyboardProtocol } from './ui/hooks/useKittyKeyboardProtocol.js' ;
2025-09-22 19:48:25 -07:00
import {
relaunchAppInChildProcess ,
relaunchOnExitCode ,
} from './utils/relaunch.js' ;
2025-10-06 13:34:00 -06:00
import { loadSandboxConfig } from './config/sandboxConfig.js' ;
2025-11-10 18:31:00 -07:00
import { deleteSession , listSessions } from './utils/sessions.js' ;
2025-10-21 11:45:33 -07:00
import { createPolicyUpdater } from './config/policy.js' ;
2025-11-04 16:21:00 -08:00
import { ScrollProvider } from './ui/contexts/ScrollProvider.js' ;
2026-02-02 16:39:17 -08:00
import { TerminalProvider } from './ui/contexts/TerminalContext.js' ;
2026-02-27 07:55:02 -08:00
import { isAlternateBufferEnabled } from './ui/hooks/useAlternateBuffer.js' ;
2026-02-20 16:26:11 -08:00
import { OverflowProvider } from './ui/contexts/OverflowContext.js' ;
2025-11-20 10:44:02 -08:00
2025-12-18 10:36:48 -08:00
import { setupTerminalAndTheme } from './utils/terminalTheme.js' ;
2025-11-20 10:44:02 -08:00
import { profiler } from './ui/components/DebugProfiler.js' ;
2026-01-20 12:52:11 -05:00
import { runDeferredCommand } from './deferred.js' ;
2026-02-12 11:29:06 -05:00
import { SlashCommandConflictHandler } from './services/SlashCommandConflictHandler.js' ;
2025-04-23 22:00:40 +00:00
2025-10-31 15:51:05 -04:00
const SLOW_RENDER_MS = 200 ;
2025-08-01 12:30:39 -07:00
export function validateDnsResolutionOrder (
order : string | undefined ,
) : DnsResolutionOrder {
const defaultValue : DnsResolutionOrder = 'ipv4first' ;
if ( order === undefined ) {
return defaultValue ;
}
if ( order === 'ipv4first' || order === 'verbatim' ) {
return order ;
}
// We don't want to throw here, just warn and use the default.
2025-10-20 18:16:47 -04:00
debugLogger . warn (
2025-08-01 12:30:39 -07:00
` Invalid value for dnsResolutionOrder in settings: " ${ order } ". Using default " ${ defaultValue } ". ` ,
) ;
return defaultValue ;
}
2025-11-24 23:11:46 +05:30
export function getNodeMemoryArgs ( isDebugMode : boolean ) : string [ ] {
2025-06-24 21:18:55 +00:00
const totalMemoryMB = os . totalmem ( ) / ( 1024 * 1024 ) ;
const heapStats = v8 . getHeapStatistics ( ) ;
const currentMaxOldSpaceSizeMb = Math . floor (
heapStats . heap_size_limit / 1024 / 1024 ,
) ;
// Set target to 50% of total memory
const targetMaxOldSpaceSizeInMB = Math . floor ( totalMemoryMB * 0.5 ) ;
2025-09-22 19:48:25 -07:00
if ( isDebugMode ) {
2025-10-20 18:16:47 -04:00
debugLogger . debug (
2025-06-24 21:18:55 +00:00
` Current heap size ${ currentMaxOldSpaceSizeMb . toFixed ( 2 ) } MB ` ,
) ;
}
2025-08-17 12:43:21 -04:00
if ( process . env [ 'GEMINI_CLI_NO_RELAUNCH' ] ) {
2025-06-24 21:18:55 +00:00
return [ ] ;
}
if ( targetMaxOldSpaceSizeInMB > currentMaxOldSpaceSizeMb ) {
2025-09-22 19:48:25 -07:00
if ( isDebugMode ) {
2025-10-20 18:16:47 -04:00
debugLogger . debug (
2025-06-24 21:18:55 +00:00
` Need to relaunch with more memory: ${ targetMaxOldSpaceSizeInMB . toFixed ( 2 ) } MB ` ,
) ;
}
return [ ` --max-old-space-size= ${ targetMaxOldSpaceSizeInMB } ` ] ;
}
return [ ] ;
}
2025-07-25 17:35:26 -07:00
export function setupUnhandledRejectionHandler() {
let unhandledRejectionOccurred = false ;
process . on ( 'unhandledRejection' , ( reason , _promise ) = > {
const errorMessage = ` =========================================
This is an unexpected error . Please file a bug report using the / bug tool .
CRITICAL : Unhandled Promise Rejection !
=== === === === === === === === === === === === === ==
Reason : $ { reason } $ {
reason instanceof Error && reason . stack
? `
Stack trace :
$ { reason . stack } `
: ''
} ` ;
2025-11-20 10:44:02 -08:00
debugLogger . error ( errorMessage ) ;
2025-07-25 17:35:26 -07:00
if ( ! unhandledRejectionOccurred ) {
unhandledRejectionOccurred = true ;
appEvents . emit ( AppEvent . OpenDebugConsole ) ;
}
} ) ;
}
2025-08-23 12:43:03 +08:00
export async function startInteractiveUI (
config : Config ,
settings : LoadedSettings ,
2026-02-20 13:22:45 -05:00
startupWarnings : StartupWarning [ ] ,
2025-09-03 10:41:53 -07:00
workspaceRoot : string = process . cwd ( ) ,
2025-11-10 18:31:00 -07:00
resumedSessionData : ResumedSessionData | undefined ,
2025-09-06 01:39:02 -04:00
initializationResult : InitializationResult ,
2025-08-23 12:43:03 +08:00
) {
2025-11-13 09:45:03 -08:00
// Never enter Ink alternate buffer mode when screen reader mode is enabled
// as there is no benefit of alternate buffer mode when using a screen reader
// and the Ink alternate buffer mode requires line wrapping harmful to
// screen readers.
2025-11-21 08:31:47 -08:00
const useAlternateBuffer = shouldEnterAlternateScreen (
2026-02-27 07:55:02 -08:00
isAlternateBufferEnabled ( config ) ,
2025-11-21 08:31:47 -08:00
config . getScreenReader ( ) ,
) ;
2025-11-12 21:17:46 -08:00
const mouseEventsEnabled = useAlternateBuffer ;
2025-11-03 13:41:58 -08:00
if ( mouseEventsEnabled ) {
enableMouseEvents ( ) ;
2025-11-13 09:45:03 -08:00
registerCleanup ( ( ) = > {
2025-11-03 13:41:58 -08:00
disableMouseEvents ( ) ;
2025-11-13 09:45:03 -08:00
} ) ;
}
2025-11-03 13:41:58 -08:00
2025-12-09 16:38:33 -08:00
const version = await getVersion ( ) ;
2025-08-23 12:43:03 +08:00
setWindowTitle ( basename ( workspaceRoot ) , settings ) ;
2025-09-06 01:39:02 -04:00
2025-11-20 10:44:02 -08:00
const consolePatcher = new ConsolePatcher ( {
onNewMessage : ( msg ) = > {
coreEvents . emitConsoleLog ( msg . type , msg . content ) ;
} ,
debugMode : config.getDebugMode ( ) ,
} ) ;
consolePatcher . patch ( ) ;
registerCleanup ( consolePatcher . cleanup ) ;
2025-12-02 15:08:25 -08:00
const { stdout : inkStdout , stderr : inkStderr } = createWorkingStdio ( ) ;
2025-11-20 10:44:02 -08:00
2026-02-11 09:29:18 -08:00
const isShpool = ! ! process . env [ 'SHPOOL_SESSION_NAME' ] ;
2025-09-06 01:39:02 -04:00
// Create wrapper component to use hooks inside render
const AppWrapper = ( ) = > {
2025-11-09 08:45:04 -08:00
useKittyKeyboardProtocol ( ) ;
2026-02-11 09:29:18 -08:00
2025-09-06 01:39:02 -04:00
return (
2025-08-23 12:43:03 +08:00
< SettingsContext.Provider value = { settings } >
2025-09-06 01:39:02 -04:00
< KeypressProvider
2025-08-23 12:43:03 +08:00
config = { config }
2026-01-15 09:26:10 -08:00
debugKeystrokeLogging = { settings . merged . general . debugKeystrokeLogging }
2025-09-06 01:39:02 -04:00
>
2025-11-03 13:41:58 -08:00
< MouseProvider
mouseEventsEnabled = { mouseEventsEnabled }
debugKeystrokeLogging = {
2026-01-15 09:26:10 -08:00
settings . merged . general . debugKeystrokeLogging
2025-11-03 13:41:58 -08:00
}
>
2026-02-02 16:39:17 -08:00
< TerminalProvider >
< ScrollProvider >
2026-02-20 16:26:11 -08:00
< OverflowProvider >
< SessionStatsProvider >
2026-03-02 13:30:58 -08:00
< VimModeProvider >
2026-02-20 16:26:11 -08:00
< AppContainer
config = { config }
startupWarnings = { startupWarnings }
version = { version }
resumedSessionData = { resumedSessionData }
initializationResult = { initializationResult }
/ >
< / VimModeProvider >
< / SessionStatsProvider >
< / OverflowProvider >
2026-02-02 16:39:17 -08:00
< / ScrollProvider >
< / TerminalProvider >
2025-11-03 13:41:58 -08:00
< / MouseProvider >
2025-09-06 01:39:02 -04:00
< / KeypressProvider >
2025-08-23 12:43:03 +08:00
< / SettingsContext.Provider >
2025-09-06 01:39:02 -04:00
) ;
} ;
2026-02-11 09:29:18 -08:00
if ( isShpool ) {
// Wait a moment for shpool to stabilize terminal size and state.
// shpool is a persistence tool that restores terminal state by replaying it.
// This delay gives shpool time to finish its restoration replay and send
// the actual terminal size (often via an immediate SIGWINCH) before we
// render the first TUI frame. Without this, the first frame may be
// garbled or rendered at an incorrect size, which disabling incremental
// rendering alone cannot fix for the initial frame.
await new Promise ( ( resolve ) = > setTimeout ( resolve , 100 ) ) ;
}
2025-09-17 15:37:13 -07:00
const instance = render (
process . env [ 'DEBUG' ] ? (
< React.StrictMode >
< AppWrapper / >
< / React.StrictMode >
) : (
< AppWrapper / >
) ,
{
2025-11-20 10:44:02 -08:00
stdout : inkStdout ,
stderr : inkStderr ,
stdin : process.stdin ,
2025-09-17 15:37:13 -07:00
exitOnCtrlC : false ,
isScreenReaderEnabled : config.getScreenReader ( ) ,
2025-10-31 15:51:05 -04:00
onRender : ( { renderTime } : { renderTime : number } ) = > {
if ( renderTime > SLOW_RENDER_MS ) {
recordSlowRender ( config , renderTime ) ;
}
2025-11-20 10:44:02 -08:00
profiler . reportFrameRendered ( ) ;
2025-10-31 15:51:05 -04:00
} ,
2025-11-20 10:44:02 -08:00
patchConsole : false ,
2025-11-12 21:17:46 -08:00
alternateBuffer : useAlternateBuffer ,
2025-11-13 09:45:03 -08:00
incrementalRendering :
2026-02-11 09:29:18 -08:00
settings . merged . ui . incrementalRendering !== false &&
useAlternateBuffer &&
! isShpool ,
2025-11-03 13:41:58 -08:00
} ,
2025-09-17 15:37:13 -07:00
) ;
2025-08-23 12:43:03 +08:00
2026-02-11 09:29:18 -08:00
if ( useAlternateBuffer ) {
disableLineWrapping ( ) ;
registerCleanup ( ( ) = > {
enableLineWrapping ( ) ;
} ) ;
}
2025-10-17 12:44:31 -07:00
checkForUpdates ( settings )
2025-08-23 12:43:03 +08:00
. then ( ( info ) = > {
handleAutoUpdate ( info , settings , config . getProjectRoot ( ) ) ;
} )
. catch ( ( err ) = > {
// Silently ignore update check errors.
if ( config . getDebugMode ( ) ) {
2025-10-20 18:16:47 -04:00
debugLogger . warn ( 'Update check failed:' , err ) ;
2025-08-23 12:43:03 +08:00
}
} ) ;
registerCleanup ( ( ) = > instance . unmount ( ) ) ;
2026-02-27 04:47:09 +05:30
registerCleanup ( setupTtyCheck ( ) ) ;
2025-08-23 12:43:03 +08:00
}
2025-06-06 09:56:45 -07:00
export async function main() {
2025-12-01 10:06:13 -08:00
const cliStartupHandle = startupProfiler . start ( 'cli_startup' ) ;
2026-01-16 15:24:53 -05:00
// Listen for admin controls from parent process (IPC) in non-sandbox mode. In
// sandbox mode, we re-fetch the admin controls from the server once we enter
// the sandbox.
// TODO: Cache settings in sandbox mode as well.
const adminControlsListner = setupAdminControlsListener ( ) ;
registerCleanup ( adminControlsListner . cleanup ) ;
2025-11-20 10:44:02 -08:00
const cleanupStdio = patchStdio ( ) ;
registerSyncCleanup ( ( ) = > {
// This is needed to ensure we don't lose any buffered output.
initializeOutputListenersAndFlush ( ) ;
cleanupStdio ( ) ;
} ) ;
2025-07-25 17:35:26 -07:00
setupUnhandledRejectionHandler ( ) ;
2026-02-12 11:29:06 -05:00
2026-02-27 04:47:09 +05:30
setupSignalHandlers ( ) ;
2026-02-12 11:29:06 -05:00
const slashCommandConflictHandler = new SlashCommandConflictHandler ( ) ;
slashCommandConflictHandler . start ( ) ;
registerCleanup ( ( ) = > slashCommandConflictHandler . stop ( ) ) ;
2025-12-01 10:06:13 -08:00
const loadSettingsHandle = startupProfiler . start ( 'load_settings' ) ;
2025-09-03 10:41:53 -07:00
const settings = loadSettings ( ) ;
2025-12-01 10:06:13 -08:00
loadSettingsHandle ? . end ( ) ;
2025-12-23 10:26:37 -08:00
// Report settings errors once during startup
settings . errors . forEach ( ( error ) = > {
coreEvents . emitFeedback ( 'warning' , error . message ) ;
} ) ;
const trustedFolders = loadTrustedFolders ( ) ;
trustedFolders . errors . forEach ( ( error : TrustedFoldersError ) = > {
coreEvents . emitFeedback (
'warning' ,
` Error in ${ error . path } : ${ error . message } ` ,
) ;
} ) ;
2026-01-29 15:20:11 -08:00
await Promise . all ( [
cleanupCheckpoints ( ) ,
cleanupToolOutputFiles ( settings . merged ) ,
] ) ;
2025-06-06 09:56:45 -07:00
2025-12-01 10:06:13 -08:00
const parseArgsHandle = startupProfiler . start ( 'parse_arguments' ) ;
2025-08-25 17:02:10 +00:00
const argv = await parseArguments ( settings . merged ) ;
2025-12-01 10:06:13 -08:00
parseArgsHandle ? . end ( ) ;
2025-07-11 16:52:56 -07:00
2026-02-11 16:49:48 -08:00
if (
( argv . allowedTools && argv . allowedTools . length > 0 ) ||
( settings . merged . tools ? . allowed && settings . merged . tools . allowed . length > 0 )
) {
coreEvents . emitFeedback (
'warning' ,
'Warning: --allowed-tools cli argument and tools.allowed in settings.json are deprecated and will be removed in 1.0: Migrate to Policy Engine: https://geminicli.com/docs/core/policy-engine/' ,
) ;
}
if (
settings . merged . tools ? . exclude &&
settings . merged . tools . exclude . length > 0
) {
coreEvents . emitFeedback (
'warning' ,
'Warning: tools.exclude in settings.json is deprecated and will be removed in 1.0. Migrate to Policy Engine: https://geminicli.com/docs/core/policy-engine/' ,
) ;
}
2026-01-23 05:08:53 +05:30
if ( argv . startupMessages ) {
argv . startupMessages . forEach ( ( msg ) = > {
coreEvents . emitFeedback ( 'info' , msg ) ;
} ) ;
}
2025-09-16 06:56:25 +08:00
// Check for invalid input combinations early to prevent crashes
if ( argv . promptInteractive && ! process . stdin . isTTY ) {
2025-11-20 10:44:02 -08:00
writeToStderr (
'Error: The --prompt-interactive flag cannot be used when input is piped from stdin.\n' ,
2025-09-16 06:56:25 +08:00
) ;
2025-11-20 10:44:02 -08:00
await runExitCleanup ( ) ;
2025-11-26 08:13:21 +05:30
process . exit ( ExitCodes . FATAL_INPUT_ERROR ) ;
2025-09-16 06:56:25 +08:00
}
2025-09-22 19:48:25 -07:00
const isDebugMode = cliConfig . isDebugMode ( argv ) ;
2025-08-15 04:25:27 +00:00
const consolePatcher = new ConsolePatcher ( {
stderr : true ,
2025-09-22 19:48:25 -07:00
debugMode : isDebugMode ,
2025-11-20 10:44:02 -08:00
onNewMessage : ( msg ) = > {
coreEvents . emitConsoleLog ( msg . type , msg . content ) ;
} ,
2025-08-15 04:25:27 +00:00
} ) ;
consolePatcher . patch ( ) ;
registerCleanup ( consolePatcher . cleanup ) ;
2025-08-01 12:30:39 -07:00
dns . setDefaultResultOrder (
2026-01-15 09:26:10 -08:00
validateDnsResolutionOrder ( settings . merged . advanced . dnsResolutionOrder ) ,
2025-08-01 12:30:39 -07:00
) ;
2025-11-14 11:39:11 -05:00
// Set a default auth type if one isn't set or is set to a legacy type
if (
2026-01-15 09:26:10 -08:00
! settings . merged . security . auth . selectedType ||
settings . merged . security . auth . selectedType === AuthType . LEGACY_CLOUD_SHELL
2025-11-14 11:39:11 -05:00
) {
if (
process . env [ 'CLOUD_SHELL' ] === 'true' ||
process . env [ 'GEMINI_CLI_USE_COMPUTE_ADC' ] === 'true'
) {
2025-07-07 15:02:13 -07:00
settings . setValue (
SettingScope . User ,
2026-01-29 15:21:17 -05:00
'security.auth.selectedType' ,
2025-11-14 11:39:11 -05:00
AuthType . COMPUTE_ADC ,
2025-07-07 15:02:13 -07:00
) ;
}
2025-06-20 01:36:33 -07:00
}
2026-01-09 17:04:57 -05:00
const partialConfig = await loadCliConfig ( settings . merged , sessionId , argv , {
projectHooks : settings.workspace.settings.hooks ,
} ) ;
2026-01-16 15:24:53 -05:00
adminControlsListner . setConfig ( partialConfig ) ;
2026-01-09 17:04:57 -05:00
// Refresh auth to fetch remote admin settings from CCPA and before entering
// the sandbox because the sandbox will interfere with the Oauth2 web
// redirect.
2026-01-22 11:44:55 -05:00
let initialAuthFailed = false ;
2026-01-21 12:38:20 -05:00
if ( ! settings . merged . security . auth . useExternal ) {
2026-01-09 17:04:57 -05:00
try {
2026-01-21 12:38:20 -05:00
if (
partialConfig . isInteractive ( ) &&
settings . merged . security . auth . selectedType
) {
2026-01-09 17:04:57 -05:00
const err = validateAuthMethod (
settings . merged . security . auth . selectedType ,
) ;
if ( err ) {
throw new Error ( err ) ;
}
await partialConfig . refreshAuth (
settings . merged . security . auth . selectedType ,
) ;
2026-01-21 12:38:20 -05:00
} else if ( ! partialConfig . isInteractive ( ) ) {
2026-01-09 17:04:57 -05:00
const authType = await validateNonInteractiveAuth (
2026-01-15 09:26:10 -08:00
settings . merged . security . auth . selectedType ,
settings . merged . security . auth . useExternal ,
2026-01-09 17:04:57 -05:00
partialConfig ,
settings ,
) ;
await partialConfig . refreshAuth ( authType ) ;
}
} catch ( err ) {
2026-01-26 06:31:19 -08:00
if ( err instanceof ValidationCancelledError ) {
// User cancelled verification, exit immediately.
await runExitCleanup ( ) ;
process . exit ( ExitCodes . SUCCESS ) ;
}
// If validation is required, we don't treat it as a fatal failure.
// We allow the app to start, and the React-based ValidationDialog
// will handle it.
if ( ! ( err instanceof ValidationRequiredError ) ) {
debugLogger . error ( 'Error authenticating:' , err ) ;
initialAuthFailed = true ;
}
2026-01-09 17:04:57 -05:00
}
}
const remoteAdminSettings = partialConfig . getRemoteAdminSettings ( ) ;
// Set remote admin settings if returned from CCPA.
if ( remoteAdminSettings ) {
settings . setRemoteAdminSettings ( remoteAdminSettings ) ;
}
2026-01-20 12:52:11 -05:00
// Run deferred command now that we have admin settings.
await runDeferredCommand ( settings . merged ) ;
2025-04-29 10:52:05 -07:00
// hop into sandbox if we are outside and sandboxing is enabled
2025-08-17 12:43:21 -04:00
if ( ! process . env [ 'SANDBOX' ] ) {
2026-01-15 09:26:10 -08:00
const memoryArgs = settings . merged . advanced . autoConfigureMemory
2025-09-22 19:48:25 -07:00
? getNodeMemoryArgs ( isDebugMode )
2025-07-07 15:01:59 -07:00
: [ ] ;
2025-09-22 19:48:25 -07:00
const sandboxConfig = await loadSandboxConfig ( settings . merged , argv ) ;
2025-10-20 16:15:23 -07:00
// We intentionally omit the list of extensions here because extensions
2025-09-23 17:06:31 -07:00
// should not impact auth or setting up the sandbox.
// TODO(jacobr): refactor loadCliConfig so there is a minimal version
// that only initializes enough config to enable refreshAuth or find
// another way to decouple refreshAuth from requiring a config.
2025-09-22 19:48:25 -07:00
2025-06-18 10:01:00 -07:00
if ( sandboxConfig ) {
2026-01-22 11:44:55 -05:00
if ( initialAuthFailed ) {
await runExitCleanup ( ) ;
process . exit ( ExitCodes . FATAL_AUTHENTICATION_ERROR ) ;
}
2025-08-20 16:52:27 -07:00
let stdinData = '' ;
if ( ! process . stdin . isTTY ) {
stdinData = await readStdin ( ) ;
}
// This function is a copy of the one from sandbox.ts
// It is moved here to decouple sandbox.ts from the CLI's argument structure.
const injectStdinIntoArgs = (
args : string [ ] ,
stdinData? : string ,
) : string [ ] = > {
const finalArgs = [ . . . args ] ;
if ( stdinData ) {
const promptIndex = finalArgs . findIndex (
( arg ) = > arg === '--prompt' || arg === '-p' ,
) ;
if ( promptIndex > - 1 && finalArgs . length > promptIndex + 1 ) {
// If there's a prompt argument, prepend stdin to it
finalArgs [ promptIndex + 1 ] =
` ${ stdinData } \ n \ n ${ finalArgs [ promptIndex + 1 ] } ` ;
} else {
// If there's no prompt argument, add stdin as the prompt
finalArgs . push ( '--prompt' , stdinData ) ;
}
}
return finalArgs ;
} ;
const sandboxArgs = injectStdinIntoArgs ( process . argv , stdinData ) ;
2025-09-17 13:05:40 -07:00
await relaunchOnExitCode ( ( ) = >
2025-09-23 17:06:31 -07:00
start_sandbox ( sandboxConfig , memoryArgs , partialConfig , sandboxArgs ) ,
2025-09-17 13:05:40 -07:00
) ;
2025-11-20 10:44:02 -08:00
await runExitCleanup ( ) ;
2025-11-26 08:13:21 +05:30
process . exit ( ExitCodes . SUCCESS ) ;
2025-06-24 21:18:55 +00:00
} else {
2025-09-17 13:05:40 -07:00
// Relaunch app so we always have a child process that can be internally
// restarted if needed.
2026-01-16 15:24:53 -05:00
await relaunchAppInChildProcess ( memoryArgs , [ ] , remoteAdminSettings ) ;
2025-04-29 10:52:05 -07:00
}
2025-04-24 08:58:47 -07:00
}
2025-07-10 18:59:02 -07:00
2025-09-22 19:48:25 -07:00
// We are now past the logic handling potentially launching a child process
// to run Gemini CLI. It is now safe to perform expensive initialization that
// may have side effects.
2025-09-23 17:06:31 -07:00
{
2025-12-01 10:06:13 -08:00
const loadConfigHandle = startupProfiler . start ( 'load_cli_config' ) ;
2025-12-23 16:10:46 -05:00
const config = await loadCliConfig ( settings . merged , sessionId , argv , {
projectHooks : settings.workspace.settings.hooks ,
} ) ;
2025-12-01 10:06:13 -08:00
loadConfigHandle ? . end ( ) ;
2026-02-06 13:33:13 -08:00
// Initialize storage immediately after loading config to ensure that
// storage-related operations (like listing or resuming sessions) have
// access to the project identifier.
await config . storage . initialize ( ) ;
2026-01-16 15:24:53 -05:00
adminControlsListner . setConfig ( config ) ;
2025-09-22 19:48:25 -07:00
2026-02-09 14:03:10 -08:00
if ( config . isInteractive ( ) && settings . merged . general . devtools ) {
2026-02-10 08:54:23 -08:00
const { setupInitialActivityLogger } = await import (
2026-02-09 14:03:10 -08:00
'./utils/devtoolsService.js'
2026-01-11 21:22:49 +08:00
) ;
2026-02-10 08:54:23 -08:00
await setupInitialActivityLogger ( config ) ;
2026-01-11 21:22:49 +08:00
}
2025-12-03 09:04:13 -08:00
// Register config for telemetry shutdown
// This ensures telemetry (including SessionEnd hooks) is properly flushed on exit
registerTelemetryConfig ( config ) ;
2025-10-21 11:45:33 -07:00
const policyEngine = config . getPolicyEngine ( ) ;
const messageBus = config . getMessageBus ( ) ;
2026-02-20 14:07:20 -08:00
createPolicyUpdater ( policyEngine , messageBus , config . storage ) ;
2025-10-21 11:45:33 -07:00
2025-12-03 09:04:13 -08:00
// Register SessionEnd hook to fire on graceful exit
// This runs before telemetry shutdown in runExitCleanup()
2026-01-08 00:41:49 +05:30
registerCleanup ( async ( ) = > {
await config . getHookSystem ( ) ? . fireSessionEndEvent ( SessionEndReason . Exit ) ;
} ) ;
2025-12-03 09:04:13 -08:00
2025-10-06 13:34:00 -06:00
// Cleanup sessions after config initialization
2025-11-24 23:11:46 +05:30
try {
await cleanupExpiredSessions ( config , settings . merged ) ;
} catch ( e ) {
debugLogger . error ( 'Failed to cleanup expired sessions:' , e ) ;
}
2025-10-06 13:34:00 -06:00
2025-09-23 17:06:31 -07:00
if ( config . getListExtensions ( ) ) {
2025-10-20 18:16:47 -04:00
debugLogger . log ( 'Installed extensions:' ) ;
2025-10-28 09:04:30 -07:00
for ( const extension of config . getExtensions ( ) ) {
2025-10-20 18:16:47 -04:00
debugLogger . log ( ` - ${ extension . name } ` ) ;
2025-09-23 17:06:31 -07:00
}
2025-11-20 10:44:02 -08:00
await runExitCleanup ( ) ;
2025-11-26 08:13:21 +05:30
process . exit ( ExitCodes . SUCCESS ) ;
2025-09-22 19:48:25 -07:00
}
2025-11-10 18:31:00 -07:00
// Handle --list-sessions flag
if ( config . getListSessions ( ) ) {
2025-12-09 22:25:22 -05:00
// Attempt auth for summary generation (gracefully skips if not configured)
2026-01-15 09:26:10 -08:00
const authType = settings . merged . security . auth . selectedType ;
2025-12-09 22:25:22 -05:00
if ( authType ) {
try {
await config . refreshAuth ( authType ) ;
} catch ( e ) {
// Auth failed - continue without summary generation capability
debugLogger . debug (
'Auth failed for --list-sessions, summaries may not be generated:' ,
e ,
) ;
}
}
2025-11-10 18:31:00 -07:00
await listSessions ( config ) ;
2025-11-20 10:44:02 -08:00
await runExitCleanup ( ) ;
2025-11-26 08:13:21 +05:30
process . exit ( ExitCodes . SUCCESS ) ;
2025-11-10 18:31:00 -07:00
}
// Handle --delete-session flag
const sessionToDelete = config . getDeleteSession ( ) ;
if ( sessionToDelete ) {
await deleteSession ( config , sessionToDelete ) ;
2025-11-20 10:44:02 -08:00
await runExitCleanup ( ) ;
2025-11-26 08:13:21 +05:30
process . exit ( ExitCodes . SUCCESS ) ;
2025-11-10 18:31:00 -07:00
}
2025-09-23 17:06:31 -07:00
const wasRaw = process . stdin . isRaw ;
if ( config . isInteractive ( ) && ! wasRaw && process . stdin . isTTY ) {
// Set this as early as possible to avoid spurious characters from
// input showing up in the output.
process . stdin . setRawMode ( true ) ;
// This cleanup isn't strictly needed but may help in certain situations.
2026-02-27 04:47:09 +05:30
registerSyncCleanup ( ( ) = > {
2026-02-11 09:29:18 -08:00
process . stdin . setRawMode ( wasRaw ) ;
} ) ;
2025-09-23 17:06:31 -07:00
}
2025-09-22 19:48:25 -07:00
2025-12-18 10:36:48 -08:00
await setupTerminalAndTheme ( config , settings ) ;
2025-12-01 10:06:13 -08:00
const initAppHandle = startupProfiler . start ( 'initialize_app' ) ;
2025-09-23 17:06:31 -07:00
const initializationResult = await initializeApp ( config , settings ) ;
2025-12-01 10:06:13 -08:00
initAppHandle ? . end ( ) ;
2025-09-22 19:48:25 -07:00
2025-09-23 17:06:31 -07:00
if (
2026-01-15 09:26:10 -08:00
settings . merged . security . auth . selectedType ===
2025-09-23 17:06:31 -07:00
AuthType . LOGIN_WITH_GOOGLE &&
config . isBrowserLaunchSuppressed ( )
) {
// Do oauth before app renders to make copying the link possible.
await getOauthClient ( settings . merged . security . auth . selectedType , config ) ;
}
2025-09-22 19:48:25 -07:00
2026-03-05 14:57:28 -05:00
if ( config . getAcpMode ( ) ) {
return runAcpClient ( config , settings , argv ) ;
2025-09-23 17:06:31 -07:00
}
2025-09-22 19:48:25 -07:00
2025-09-23 17:06:31 -07:00
let input = config . getQuestion ( ) ;
2026-02-20 13:06:35 -08:00
const useAlternateBuffer = shouldEnterAlternateScreen (
2026-02-27 07:55:02 -08:00
isAlternateBufferEnabled ( config ) ,
2026-02-20 13:06:35 -08:00
config . getScreenReader ( ) ,
) ;
2026-02-20 13:22:45 -05:00
const rawStartupWarnings = await getStartupWarnings ( ) ;
const startupWarnings : StartupWarning [ ] = [
. . . rawStartupWarnings . map ( ( message ) = > ( {
id : ` startup- ${ createHash ( 'sha256' ) . update ( message ) . digest ( 'hex' ) . substring ( 0 , 16 ) } ` ,
message ,
priority : WarningPriority.High ,
} ) ) ,
2026-02-20 13:06:35 -08:00
. . . ( await getUserStartupWarnings ( settings . merged , undefined , {
isAlternateBuffer : useAlternateBuffer ,
} ) ) ,
2025-09-23 17:06:31 -07:00
] ;
2025-11-10 18:31:00 -07:00
// Handle --resume flag
let resumedSessionData : ResumedSessionData | undefined = undefined ;
if ( argv . resume ) {
const sessionSelector = new SessionSelector ( config ) ;
try {
const result = await sessionSelector . resolveSession ( argv . resume ) ;
resumedSessionData = {
conversation : result.sessionData ,
filePath : result.sessionPath ,
} ;
// Use the existing session ID to continue recording to the same session
config . setSessionId ( resumedSessionData . conversation . sessionId ) ;
} catch ( error ) {
2026-03-06 11:02:33 -08:00
if (
error instanceof SessionError &&
error . code === 'NO_SESSIONS_FOUND'
) {
// No sessions to resume — start a fresh session with a warning
startupWarnings . push ( {
id : 'resume-no-sessions' ,
message : error.message ,
priority : WarningPriority.High ,
} ) ;
} else {
coreEvents . emitFeedback (
'error' ,
` Error resuming session: ${ error instanceof Error ? error . message : 'Unknown error' } ` ,
) ;
await runExitCleanup ( ) ;
process . exit ( ExitCodes . FATAL_INPUT_ERROR ) ;
}
2025-11-10 18:31:00 -07:00
}
}
2025-12-01 10:06:13 -08:00
cliStartupHandle ? . end ( ) ;
2025-09-23 17:06:31 -07:00
// Render UI, passing necessary config values. Check that there is no command line question.
if ( config . isInteractive ( ) ) {
await startInteractiveUI (
config ,
settings ,
startupWarnings ,
process . cwd ( ) ,
2025-11-10 18:31:00 -07:00
resumedSessionData ,
2025-09-23 17:06:31 -07:00
initializationResult ,
) ;
return ;
}
2025-07-10 18:59:02 -07:00
2025-09-23 17:06:31 -07:00
await config . initialize ( ) ;
2025-12-01 10:06:13 -08:00
startupProfiler . flush ( config ) ;
2025-07-17 16:25:23 -06:00
2026-01-05 13:27:53 -08:00
// If not a TTY, read from stdin
// This is for cases where the user pipes input directly into the command
let stdinData : string | undefined = undefined ;
if ( ! process . stdin . isTTY ) {
stdinData = await readStdin ( ) ;
if ( stdinData ) {
input = input ? ` ${ stdinData } \ n \ n ${ input } ` : stdinData ;
}
}
2025-12-03 09:04:13 -08:00
// Fire SessionStart hook through MessageBus (only if hooks are enabled)
// Must be called AFTER config.initialize() to ensure HookRegistry is loaded
2026-01-08 00:41:49 +05:30
const sessionStartSource = resumedSessionData
? SessionStartSource . Resume
: SessionStartSource . Startup ;
2026-01-20 08:55:43 +05:30
const hookSystem = config ? . getHookSystem ( ) ;
if ( hookSystem ) {
const result = await hookSystem . fireSessionStartEvent ( sessionStartSource ) ;
if ( result ) {
if ( result . systemMessage ) {
writeToStderr ( result . systemMessage + '\n' ) ;
}
const additionalContext = result . getAdditionalContext ( ) ;
if ( additionalContext ) {
// Prepend context to input (System Context -> Stdin -> Question)
2026-01-21 21:11:45 -05:00
const wrappedContext = ` <hook_context> ${ additionalContext } </hook_context> ` ;
input = input ? ` ${ wrappedContext } \ n \ n ${ input } ` : wrappedContext ;
2026-01-20 08:55:43 +05:30
}
2026-01-05 13:27:53 -08:00
}
2025-12-03 09:04:13 -08:00
}
2026-01-08 00:41:49 +05:30
// Register SessionEnd hook for graceful exit
registerCleanup ( async ( ) = > {
await config . getHookSystem ( ) ? . fireSessionEndEvent ( SessionEndReason . Exit ) ;
} ) ;
2025-09-23 17:06:31 -07:00
if ( ! input ) {
2025-10-20 18:16:47 -04:00
debugLogger . error (
2025-09-23 17:06:31 -07:00
` No input provided via stdin. Input can be provided by piping data into gemini or using the --prompt option. ` ,
) ;
2025-11-20 10:44:02 -08:00
await runExitCleanup ( ) ;
2025-11-26 08:13:21 +05:30
process . exit ( ExitCodes . FATAL_INPUT_ERROR ) ;
2025-09-23 17:06:31 -07:00
}
const prompt_id = Math . random ( ) . toString ( 16 ) . slice ( 2 ) ;
2025-10-09 16:02:58 -07:00
logUserPrompt (
config ,
new UserPromptEvent (
input . length ,
prompt_id ,
config . getContentGeneratorConfig ( ) ? . authType ,
input ,
) ,
) ;
2025-09-23 17:06:31 -07:00
2025-12-30 11:09:00 -05:00
const authType = await validateNonInteractiveAuth (
2026-01-15 09:26:10 -08:00
settings . merged . security . auth . selectedType ,
settings . merged . security . auth . useExternal ,
2025-09-06 01:39:02 -04:00
config ,
settings ,
) ;
2025-12-30 11:09:00 -05:00
await config . refreshAuth ( authType ) ;
2025-09-08 16:37:36 -07:00
2025-09-23 17:06:31 -07:00
if ( config . getDebugMode ( ) ) {
2025-10-20 18:16:47 -04:00
debugLogger . log ( 'Session ID: %s' , sessionId ) ;
2025-08-14 20:08:59 +02:00
}
2025-06-13 03:44:17 -04:00
2025-11-20 10:44:02 -08:00
initializeOutputListenersAndFlush ( ) ;
2025-10-29 17:54:40 -04:00
await runNonInteractive ( {
2025-12-30 11:09:00 -05:00
config ,
2025-10-29 17:54:40 -04:00
settings ,
input ,
prompt_id ,
2025-11-10 18:31:00 -07:00
resumedSessionData ,
2025-10-29 17:54:40 -04:00
} ) ;
2025-09-23 17:06:31 -07:00
// Call cleanup before process.exit, which causes cleanup to not run
await runExitCleanup ( ) ;
2025-11-26 08:13:21 +05:30
process . exit ( ExitCodes . SUCCESS ) ;
2025-08-25 17:21:00 -05:00
}
Initial commit of Gemini Code CLI
This commit introduces the initial codebase for the Gemini Code CLI, a command-line interface designed to facilitate interaction with the Gemini API for software engineering tasks.
The code was migrated from a previous git repository as a single squashed commit.
Core Features & Components:
* **Gemini Integration:** Leverages the `@google/genai` SDK to interact with the Gemini models, supporting chat history, streaming responses, and function calling (tools).
* **Terminal UI:** Built with Ink (React for CLIs) providing an interactive chat interface within the terminal, including input prompts, message display, loading indicators, and tool interaction elements.
* **Tooling Framework:** Implements a robust tool system allowing Gemini to interact with the local environment. Includes tools for:
* File system listing (`ls`)
* File reading (`read-file`)
* Content searching (`grep`)
* File globbing (`glob`)
* File editing (`edit`)
* File writing (`write-file`)
* Executing bash commands (`terminal`)
* **State Management:** Handles the streaming state of Gemini responses and manages the conversation history.
* **Configuration:** Parses command-line arguments (`yargs`) and loads environment variables (`dotenv`) for setup.
* **Project Structure:** Organized into `core`, `ui`, `tools`, `config`, and `utils` directories using TypeScript. Includes basic build (`tsc`) and start scripts.
This initial version establishes the foundation for a powerful CLI tool enabling developers to use Gemini for coding assistance directly in their terminal environment.
---
Created by yours truly: __Gemini Code__
2025-04-15 21:41:08 -07:00
}
2025-06-10 11:54:51 +08:00
function setWindowTitle ( title : string , settings : LoadedSettings ) {
2026-01-15 09:26:10 -08:00
if ( ! settings . merged . ui . hideWindowTitle ) {
2026-01-12 17:18:14 -08:00
// Initial state before React loop starts
const windowTitle = computeTerminalTitle ( {
streamingState : StreamingState.Idle ,
isConfirming : false ,
2026-01-21 14:31:24 -08:00
isSilentWorking : false ,
2026-01-12 17:18:14 -08:00
folderName : title ,
2026-01-15 09:26:10 -08:00
showThoughts : ! ! settings . merged . ui . showStatusInTitle ,
useDynamicTitle : settings.merged.ui.dynamicWindowTitle ,
2026-01-12 17:18:14 -08:00
} ) ;
2026-01-12 20:14:01 -08:00
writeToStdout ( ` \ x1b]0; ${ windowTitle } \ x07 ` ) ;
2025-06-10 11:54:51 +08:00
process . on ( 'exit' , ( ) = > {
2026-01-12 20:14:01 -08:00
writeToStdout ( ` \ x1b]0; \ x07 ` ) ;
2025-11-20 10:44:02 -08:00
} ) ;
}
}
2025-11-21 21:08:06 -05:00
export function initializeOutputListenersAndFlush() {
2025-11-20 10:44:02 -08:00
// If there are no listeners for output, make sure we flush so output is not
// lost.
if ( coreEvents . listenerCount ( CoreEvent . Output ) === 0 ) {
// In non-interactive mode, ensure we drain any buffered output or logs to stderr
coreEvents . on ( CoreEvent . Output , ( payload : OutputPayload ) = > {
if ( payload . isStderr ) {
writeToStderr ( payload . chunk , payload . encoding ) ;
} else {
writeToStdout ( payload . chunk , payload . encoding ) ;
}
} ) ;
2025-12-29 15:46:10 -05:00
if ( coreEvents . listenerCount ( CoreEvent . ConsoleLog ) === 0 ) {
coreEvents . on ( CoreEvent . ConsoleLog , ( payload : ConsoleLogPayload ) = > {
if ( payload . type === 'error' || payload . type === 'warn' ) {
writeToStderr ( payload . content ) ;
} else {
writeToStdout ( payload . content ) ;
}
} ) ;
}
if ( coreEvents . listenerCount ( CoreEvent . UserFeedback ) === 0 ) {
coreEvents . on ( CoreEvent . UserFeedback , ( payload : UserFeedbackPayload ) = > {
if ( payload . severity === 'error' || payload . severity === 'warning' ) {
writeToStderr ( payload . message ) ;
} else {
writeToStdout ( payload . message ) ;
}
} ) ;
}
2025-06-10 11:54:51 +08:00
}
2025-11-20 10:44:02 -08:00
coreEvents . drainBacklogs ( ) ;
2025-06-10 11:54:51 +08:00
}
2026-01-16 15:24:53 -05:00
function setupAdminControlsListener() {
2026-02-03 16:19:14 -05:00
let pendingSettings : AdminControlsSettings | undefined ;
2026-01-16 15:24:53 -05:00
let config : Config | undefined ;
const messageHandler = ( msg : unknown ) = > {
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2026-01-16 15:24:53 -05:00
const message = msg as {
type ? : string ;
2026-02-03 16:19:14 -05:00
settings? : AdminControlsSettings ;
2026-01-16 15:24:53 -05:00
} ;
if ( message ? . type === 'admin-settings' && message . settings ) {
if ( config ) {
config . setRemoteAdminSettings ( message . settings ) ;
} else {
pendingSettings = message . settings ;
}
}
} ;
process . on ( 'message' , messageHandler ) ;
return {
setConfig : ( newConfig : Config ) = > {
config = newConfig ;
if ( pendingSettings ) {
config . setRemoteAdminSettings ( pendingSettings ) ;
}
} ,
cleanup : ( ) = > {
process . off ( 'message' , messageHandler ) ;
} ,
} ;
}