/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import React from 'react'; import { render } from 'ink'; import { AppWrapper } from './ui/App.js'; import { loadCliConfig } from './config/config.js'; import { readStdin } from './utils/readStdin.js'; import { basename } from 'node:path'; import v8 from 'node:v8'; import os from 'node:os'; import { spawn } from 'node:child_process'; import { start_sandbox } from './utils/sandbox.js'; import { LoadedSettings, loadSettings, SettingScope, } from './config/settings.js'; import { themeManager } from './ui/themes/theme-manager.js'; import { getStartupWarnings } from './utils/startupWarnings.js'; import { runNonInteractive } from './nonInteractiveCli.js'; import { loadExtensions, Extension } from './config/extension.js'; import { cleanupCheckpoints } from './utils/cleanup.js'; import { ApprovalMode, Config, EditTool, ShellTool, WriteFileTool, sessionId, logUserPrompt, AuthType, } from '@gemini-cli/core'; import { validateAuthMethod } from './config/auth.js'; import { setMaxSizedBoxDebugging } from './ui/components/shared/MaxSizedBox.js'; function getNodeMemoryArgs(config: Config): string[] { 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); if (config.getDebugMode()) { console.debug( `Current heap size ${currentMaxOldSpaceSizeMb.toFixed(2)} MB`, ); } if (process.env.GEMINI_CLI_NO_RELAUNCH) { return []; } if (targetMaxOldSpaceSizeInMB > currentMaxOldSpaceSizeMb) { if (config.getDebugMode()) { console.debug( `Need to relaunch with more memory: ${targetMaxOldSpaceSizeInMB.toFixed(2)} MB`, ); } return [`--max-old-space-size=${targetMaxOldSpaceSizeInMB}`]; } return []; } async function relaunchWithAdditionalArgs(additionalArgs: string[]) { const nodeArgs = [...additionalArgs, ...process.argv.slice(1)]; const newEnv = { ...process.env, GEMINI_CLI_NO_RELAUNCH: 'true' }; const child = spawn(process.execPath, nodeArgs, { stdio: 'inherit', env: newEnv, }); await new Promise((resolve) => child.on('close', resolve)); process.exit(0); } export async function main() { const workspaceRoot = process.cwd(); const settings = loadSettings(workspaceRoot); await cleanupCheckpoints(); if (settings.errors.length > 0) { for (const error of settings.errors) { let errorMessage = `Error in ${error.path}: ${error.message}`; if (!process.env.NO_COLOR) { errorMessage = `\x1b[31m${errorMessage}\x1b[0m`; } console.error(errorMessage); console.error(`Please fix ${error.path} and try again.`); } process.exit(1); } const extensions = loadExtensions(workspaceRoot); const config = await loadCliConfig(settings.merged, extensions, sessionId); // set default fallback to gemini api key // this has to go after load cli becuase thats where the env is set if (!settings.merged.selectedAuthType && process.env.GEMINI_API_KEY) { settings.setValue( SettingScope.User, 'selectedAuthType', AuthType.USE_GEMINI, ); } setMaxSizedBoxDebugging(config.getDebugMode()); // Initialize centralized FileDiscoveryService config.getFileService(); if (config.getCheckpointingEnabled()) { try { await config.getGitService(); } catch { // For now swallow the error, later log it. } } if (settings.merged.theme) { if (!themeManager.setActiveTheme(settings.merged.theme)) { // If the theme is not found during initial load, log a warning and continue. // The useThemeCommand hook in App.tsx will handle opening the dialog. console.warn(`Warning: Theme "${settings.merged.theme}" not found.`); } } const memoryArgs = settings.merged.autoConfigureMaxOldSpaceSize ? getNodeMemoryArgs(config) : []; // hop into sandbox if we are outside and sandboxing is enabled if (!process.env.SANDBOX) { const sandboxConfig = config.getSandbox(); if (sandboxConfig) { if (settings.merged.selectedAuthType) { // Validate authentication here because the sandbox will interfere with the Oauth2 web redirect. const err = validateAuthMethod(settings.merged.selectedAuthType); if (err) { console.error(err); process.exit(1); } await config.refreshAuth(settings.merged.selectedAuthType); } await start_sandbox(sandboxConfig, memoryArgs); process.exit(0); } else { // Not in a sandbox and not entering one, so relaunch with additional // arguments to control memory usage if needed. if (memoryArgs.length > 0) { await relaunchWithAdditionalArgs(memoryArgs); process.exit(0); } } } let input = config.getQuestion(); const startupWarnings = await getStartupWarnings(); // Render UI, passing necessary config values. Check that there is no command line question. if (process.stdin.isTTY && input?.length === 0) { setWindowTitle(basename(workspaceRoot), settings); render( , { exitOnCtrlC: false }, ); return; } // If not a TTY, read from stdin // This is for cases where the user pipes input directly into the command if (!process.stdin.isTTY) { input += await readStdin(); } if (!input) { console.error('No input provided via stdin.'); process.exit(1); } logUserPrompt(config, { 'event.name': 'user_prompt', 'event.timestamp': new Date().toISOString(), prompt: input, prompt_length: input.length, }); // Non-interactive mode handled by runNonInteractive const nonInteractiveConfig = await loadNonInteractiveConfig( config, extensions, settings, ); await runNonInteractive(nonInteractiveConfig, input); process.exit(0); } function setWindowTitle(title: string, settings: LoadedSettings) { if (!settings.merged.hideWindowTitle) { process.stdout.write(`\x1b]2; Gemini - ${title} \x07`); process.on('exit', () => { process.stdout.write(`\x1b]2;\x07`); }); } } // --- Global Unhandled Rejection Handler --- process.on('unhandledRejection', (reason, _promise) => { // Log other unexpected unhandled rejections as critical errors console.error('========================================='); console.error('CRITICAL: Unhandled Promise Rejection!'); console.error('========================================='); console.error('Reason:', reason); console.error('Stack trace may follow:'); if (!(reason instanceof Error)) { console.error(reason); } // Exit for genuinely unhandled errors process.exit(1); }); async function loadNonInteractiveConfig( config: Config, extensions: Extension[], settings: LoadedSettings, ) { let finalConfig = config; if (config.getApprovalMode() !== ApprovalMode.YOLO) { // Everything is not allowed, ensure that only read-only tools are configured. const existingExcludeTools = settings.merged.excludeTools || []; const interactiveTools = [ ShellTool.Name, EditTool.Name, WriteFileTool.Name, ]; const newExcludeTools = [ ...new Set([...existingExcludeTools, ...interactiveTools]), ]; const nonInteractiveSettings = { ...settings.merged, excludeTools: newExcludeTools, }; finalConfig = await loadCliConfig( nonInteractiveSettings, extensions, config.getSessionId(), ); } return await validateNonInterActiveAuth( settings.merged.selectedAuthType, finalConfig, ); } async function validateNonInterActiveAuth( selectedAuthType: AuthType | undefined, nonInteractiveConfig: Config, ) { // making a special case for the cli. many headless environments might not have a settings.json set // so if GEMINI_API_KEY is set, we'll use that. However since the oauth things are interactive anyway, we'll // still expect that exists if (!selectedAuthType && !process.env.GEMINI_API_KEY) { console.error( 'Please set an Auth method in your .gemini/settings.json OR specify GEMINI_API_KEY env variable file before running', ); process.exit(1); } selectedAuthType = selectedAuthType || AuthType.USE_GEMINI; const err = validateAuthMethod(selectedAuthType); if (err != null) { console.error(err); process.exit(1); } await nonInteractiveConfig.refreshAuth(selectedAuthType); return nonInteractiveConfig; }