2025-04-18 17:44:24 -07:00
/ * *
* @license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
2026-03-12 16:37:03 -04:00
import {
type StartupWarning ,
WarningPriority ,
type Config ,
type ResumedSessionData ,
2026-03-20 10:10:51 -04:00
type WorktreeInfo ,
2026-03-12 16:37:03 -04:00
type OutputPayload ,
type ConsoleLogPayload ,
type UserFeedbackPayload ,
2026-04-08 23:27:24 +00:00
createSessionId ,
2026-03-12 16:37:03 -04:00
logUserPrompt ,
AuthType ,
UserPromptEvent ,
coreEvents ,
CoreEvent ,
getOauthClient ,
patchStdio ,
writeToStdout ,
writeToStderr ,
shouldEnterAlternateScreen ,
startupProfiler ,
ExitCodes ,
SessionStartSource ,
SessionEndReason ,
ValidationCancelledError ,
ValidationRequiredError ,
type AdminControlsSettings ,
debugLogger ,
2026-03-25 06:46:00 -07:00
isHeadlessMode ,
2026-04-08 23:27:24 +00:00
Storage ,
2026-03-12 16:37:03 -04:00
} from '@google/gemini-cli-core' ;
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' ;
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' ;
2026-03-11 23:20:42 +05:30
import {
loadSettings ,
SettingScope ,
type DnsResolutionOrder ,
type LoadedSettings ,
} from './config/settings.js' ;
2025-12-23 10:26:37 -08:00
import {
loadTrustedFolders ,
type TrustedFoldersError ,
} from './config/trustedFolders.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 ,
2025-08-29 12:53:39 -04:00
} from './utils/cleanup.js' ;
2026-03-20 10:10:51 -04:00
import { setupWorktree } from './utils/worktreeSetup.js' ;
2026-01-29 15:20:11 -08:00
import {
cleanupToolOutputFiles ,
cleanupExpiredSessions ,
} from './utils/sessionCleanup.js' ;
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-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' ;
2026-03-12 16:37:03 -04:00
2026-04-08 20:17:32 -04:00
import { 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' ;
2026-02-27 07:55:02 -08:00
import { isAlternateBufferEnabled } from './ui/hooks/useAlternateBuffer.js' ;
2025-11-20 10:44:02 -08:00
2025-12-18 10:36:48 -08:00
import { setupTerminalAndTheme } from './utils/terminalTheme.js' ;
2026-01-20 12:52:11 -05:00
import { runDeferredCommand } from './deferred.js' ;
2026-03-10 17:13:20 -07:00
import { cleanupBackgroundLogs } from './utils/logCleanup.js' ;
2026-02-12 11:29:06 -05:00
import { SlashCommandConflictHandler } from './services/SlashCommandConflictHandler.js' ;
2025-04-23 22:00:40 +00:00
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 ;
}
2026-04-07 23:03:36 -04:00
const DEFAULT_EPT_SIZE = ( 256 * 1024 * 1024 ) . toString ( ) ;
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 [ ] ;
}
2026-04-07 23:03:36 -04:00
const args : string [ ] = [ ] ;
// Automatically expand the V8 External Pointer Table to 256MB to prevent
// out-of-memory crashes during high native-handle concurrency.
// Note: Only supported in specific Node.js versions compiled with V8 Sandbox enabled.
const eptFlag = ` --max-external-pointer-table-size= ${ DEFAULT_EPT_SIZE } ` ;
const isV8SandboxEnabled =
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion
( process . config ? . variables as any ) ? . v8_enable_sandbox === 1 ;
if (
isV8SandboxEnabled &&
! process . execArgv . some ( ( arg ) = >
arg . startsWith ( '--max-external-pointer-table-size' ) ,
)
) {
args . push ( eptFlag ) ;
}
2025-06-24 21:18:55 +00:00
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 ` ,
) ;
}
2026-04-07 23:03:36 -04:00
args . push ( ` --max-old-space-size= ${ targetMaxOldSpaceSizeInMB } ` ) ;
2025-06-24 21:18:55 +00:00
}
2026-04-07 23:03:36 -04:00
return args ;
2025-06-24 21:18:55 +00:00
}
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 ) ;
}
} ) ;
}
2026-04-08 23:27:24 +00:00
export async function resolveSessionId ( resumeArg : string | undefined ) : Promise < {
sessionId : string ;
resumedSessionData? : ResumedSessionData ;
} > {
if ( ! resumeArg ) {
return { sessionId : createSessionId ( ) } ;
}
const storage = new Storage ( process . cwd ( ) ) ;
await storage . initialize ( ) ;
try {
const { sessionData , sessionPath } = await new SessionSelector (
storage ,
) . resolveSession ( resumeArg ) ;
return {
sessionId : sessionData.sessionId ,
resumedSessionData : { conversation : sessionData , filePath : sessionPath } ,
} ;
} catch ( error ) {
if ( error instanceof SessionError && error . code === 'NO_SESSIONS_FOUND' ) {
coreEvents . emitFeedback ( 'warning' , error . message ) ;
return { sessionId : createSessionId ( ) } ;
}
coreEvents . emitFeedback (
'error' ,
` Error resuming session: ${ error instanceof Error ? error . message : 'Unknown error' } ` ,
) ;
await runExitCleanup ( ) ;
process . exit ( ExitCodes . FATAL_INPUT_ERROR ) ;
}
}
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
) {
2026-03-12 16:37:03 -04:00
// Dynamically import the heavy UI module so React/Ink are only parsed when needed
const { startInteractiveUI : doStartUI } = await import ( './interactiveCli.js' ) ;
await doStartUI (
config ,
settings ,
startupWarnings ,
workspaceRoot ,
resumedSessionData ,
initializationResult ,
2025-09-17 15:37:13 -07:00
) ;
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 ( ) ;
2026-03-20 10:10:51 -04:00
// If a worktree is requested and enabled, set it up early.
2026-03-23 12:26:56 -04:00
// This must be awaited before any other async tasks that depend on CWD (like loadCliConfig)
// because setupWorktree calls process.chdir().
2026-03-20 10:10:51 -04:00
const requestedWorktree = cliConfig . getRequestedWorktreeName ( settings ) ;
let worktreeInfo : WorktreeInfo | undefined ;
if ( requestedWorktree !== undefined ) {
2026-03-23 12:26:56 -04:00
const worktreeHandle = startupProfiler . start ( 'setup_worktree' ) ;
2026-03-20 10:10:51 -04:00
worktreeInfo = await setupWorktree ( requestedWorktree || undefined ) ;
2026-03-23 12:26:56 -04:00
worktreeHandle ? . end ( ) ;
2026-03-20 10:10:51 -04:00
}
2026-03-23 12:26:56 -04:00
const cleanupOpsHandle = startupProfiler . start ( 'cleanup_ops' ) ;
Promise . all ( [
cleanupCheckpoints ( ) ,
cleanupToolOutputFiles ( settings . merged ) ,
cleanupBackgroundLogs ( ) ,
] )
. catch ( ( e ) = > {
debugLogger . error ( 'Early cleanup failed:' , e ) ;
} )
. finally ( ( ) = > {
cleanupOpsHandle ? . end ( ) ;
} ) ;
const parseArgsHandle = startupProfiler . start ( 'parse_arguments' ) ;
const argvPromise = parseArguments ( settings . merged ) . finally ( ( ) = > {
parseArgsHandle ? . end ( ) ;
} ) ;
const rawStartupWarningsPromise = getStartupWarnings ( ) ;
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-03-23 12:26:56 -04:00
const argv = await argvPromise ;
2025-07-11 16:52:56 -07:00
2026-04-08 23:27:24 +00:00
const { sessionId , resumedSessionData } = await resolveSessionId ( argv . resume ) ;
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 ,
2026-03-25 06:46:00 -07:00
interactive : isHeadlessMode ( ) ? false : 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-03-23 13:34:09 -07:00
if ( ! settings . merged . security . auth . useExternal && ! argv . isCommand ) {
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-04-08 20:17:32 -04:00
if ( process . send ) {
process . send ( {
type : 'admin-settings-update' ,
settings : remoteAdminSettings ,
} ) ;
}
2026-01-09 17:04:57 -05:00
}
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
2026-03-23 13:34:09 -07:00
if ( ! process . env [ 'SANDBOX' ] && ! argv . isCommand ) {
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-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 ,
2026-03-20 10:10:51 -04:00
worktreeSettings : worktreeInfo ,
2025-12-23 16:10:46 -05:00
} ) ;
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
2026-03-23 12:26:56 -04:00
// Launch cleanup expired sessions as a background task
cleanupExpiredSessions ( config , settings . merged ) . catch ( ( e ) = > {
2025-11-24 23:11:46 +05:30
debugLogger . error ( 'Failed to cleanup expired sessions:' , e ) ;
2026-03-23 12:26:56 -04:00
} ) ;
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
2026-03-23 12:26:56 -04:00
const terminalHandle = startupProfiler . start ( 'setup_terminal' ) ;
2025-12-18 10:36:48 -08:00
await setupTerminalAndTheme ( config , settings ) ;
2026-03-23 12:26:56 -04:00
terminalHandle ? . end ( ) ;
2025-12-18 10:36:48 -08:00
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-03-23 12:26:56 -04:00
const rawStartupWarnings = await rawStartupWarningsPromise ;
2026-02-20 13:22:45 -05:00
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-12-01 10:06:13 -08:00
cliStartupHandle ? . end ( ) ;
2026-03-25 14:18:43 -04:00
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 ( ) ) {
2026-03-25 14:18:43 -04:00
// Earlier initialization phases (like TerminalCapabilityManager resolving
// or authWithWeb) may have added and removed 'data' listeners on process.stdin.
// When the listener count drops to 0, Node.js implicitly pauses the stream buffer.
// React Ink's useInput hooks will silently fail to receive keystrokes if the stream remains paused.
if ( process . stdin . isTTY ) {
process . stdin . resume ( ) ;
}
2025-09-23 17:06:31 -07:00
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
}
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
}
2026-03-17 14:42:40 -07:00
const prompt_id = sessionId ;
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-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 ) ;
} ,
} ;
}