/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { execFile } from 'child_process'; import React from 'react'; import { render } from 'ink'; import { AppWrapper } from './ui/App.js'; import { loadCliConfig, parseArguments } 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 dns from 'node:dns'; import { spawn } from 'node:child_process'; import { start_sandbox } from './utils/sandbox.js'; import { DnsResolutionOrder, LoadedSettings, loadSettings, SettingScope, } from './config/settings.js'; import { themeManager } from './ui/themes/theme-manager.js'; import { getStartupWarnings } from './utils/startupWarnings.js'; import { getUserStartupWarnings } from './utils/userStartupWarnings.js'; import { ConsolePatcher } from './ui/utils/ConsolePatcher.js'; import { runNonInteractive } from './nonInteractiveCli.js'; import { loadExtensions } from './config/extension.js'; import { cleanupCheckpoints, registerCleanup } from './utils/cleanup.js'; import { getCliVersion } from './utils/version.js'; import { Config, sessionId, logUserPrompt, AuthType, getOauthClient, logIdeConnection, IdeConnectionEvent, IdeConnectionType, } from '@google/gemini-cli-core'; import { validateAuthMethod } from './config/auth.js'; import { setMaxSizedBoxDebugging } from './ui/components/shared/MaxSizedBox.js'; import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js'; import { detectAndEnableKittyProtocol } from './ui/utils/kittyProtocolDetector.js'; import { checkForUpdates } from './ui/utils/updateCheck.js'; import { handleAutoUpdate } from './utils/handleAutoUpdate.js'; import { appEvents, AppEvent } from './utils/events.js'; import { SettingsContext } from './ui/contexts/SettingsContext.js'; 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. console.warn( `Invalid value for dnsResolutionOrder in settings: "${order}". Using default "${defaultValue}".`, ); return defaultValue; } 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); } import { runZedIntegration } from './zed-integration/zedIntegration.js'; 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}` : '' }`; appEvents.emit(AppEvent.LogError, errorMessage); if (!unhandledRejectionOccurred) { unhandledRejectionOccurred = true; appEvents.emit(AppEvent.OpenDebugConsole); } }); } export async function main() { if (!process.env['SANDBOX']) { console.log("regex --output 'startup'"); } setupUnhandledRejectionHandler(); 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 argv = await parseArguments(); const extensions = loadExtensions(workspaceRoot); const config = await loadCliConfig( settings.merged, extensions, sessionId, argv, ); // Immediately run regex --stats with the new session ID const command = '/home/jcarr/go/bin/regex'; const args = ['--stats', sessionId, '{}']; execFile(command, args, (error, stdout, stderr) => { if (error) { // Using console.error might be noisy, but it's good for debugging. // This will appear in the gemini-cli debug log if it's enabled. console.error(`[startup-stats] execFile error: ${error.message}`); return; } if (stderr) { console.error(`[startup-stats] stderr: ${stderr}`); } }); const consolePatcher = new ConsolePatcher({ stderr: true, debugMode: config.getDebugMode(), }); consolePatcher.patch(); registerCleanup(consolePatcher.cleanup); dns.setDefaultResultOrder( validateDnsResolutionOrder(settings.merged.dnsResolutionOrder), ); if (argv.promptInteractive && !process.stdin.isTTY) { console.error( 'Error: The --prompt-interactive flag is not supported when piping input from stdin.', ); process.exit(1); } if (config.getListExtensions()) { console.log('Installed extensions:'); for (const extension of extensions) { console.log(`- ${extension.config.name}`); } process.exit(0); } // Set a default auth type if one isn't set. if (!settings.merged.selectedAuthType) { if (process.env['CLOUD_SHELL'] === 'true') { settings.setValue( SettingScope.User, 'selectedAuthType', AuthType.CLOUD_SHELL, ); } } setMaxSizedBoxDebugging(config.getDebugMode()); await config.initialize(); if (config.getIdeMode()) { await config.getIdeClient().connect(); logIdeConnection(config, new IdeConnectionEvent(IdeConnectionType.START)); } // Load custom themes from settings themeManager.loadCustomThemes(settings.merged.customThemes); 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.`); } } // hop into sandbox if we are outside and sandboxing is enabled if (!process.env['SANDBOX']) { const memoryArgs = settings.merged.autoConfigureMaxOldSpaceSize ? getNodeMemoryArgs(config) : []; const sandboxConfig = config.getSandbox(); if (sandboxConfig) { if ( settings.merged.selectedAuthType && !settings.merged.useExternalAuth ) { // Validate authentication here because the sandbox will interfere with the Oauth2 web redirect. try { const err = validateAuthMethod(settings.merged.selectedAuthType); if (err) { throw new Error(err); } await config.refreshAuth(settings.merged.selectedAuthType); } catch (err) { console.error('Error authenticating:', err); process.exit(1); } } 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); await start_sandbox(sandboxConfig, memoryArgs, config, sandboxArgs); 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); } } } if ( settings.merged.selectedAuthType === AuthType.LOGIN_WITH_GOOGLE && config.isBrowserLaunchSuppressed() ) { // Do oauth before app renders to make copying the link possible. await getOauthClient(settings.merged.selectedAuthType, config); } if (config.getExperimentalZedIntegration()) { return runZedIntegration(config, settings, extensions, argv); } let input = config.getQuestion(); const startupWarnings = [ ...(await getStartupWarnings()), ...(await getUserStartupWarnings(workspaceRoot)), ]; // Render UI, passing necessary config values. Check that there is no command line question. if (config.isInteractive()) { const version = await getCliVersion(); // Detect and enable Kitty keyboard protocol once at startup await detectAndEnableKittyProtocol(); setWindowTitle(basename(workspaceRoot), settings); const instance = render( , { exitOnCtrlC: false, isScreenReaderEnabled: config.getScreenReader() }, ); checkForUpdates() .then((info) => { handleAutoUpdate(info, settings, config.getProjectRoot()); }) .catch((err) => { // Silently ignore update check errors. if (config.getDebugMode()) { console.error('Update check failed:', err); } }); registerCleanup(() => instance.unmount()); 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) { const stdinData = await readStdin(); if (stdinData) { input = `${stdinData}\n\n${input}`; } } if (!input) { console.error('No input provided via stdin.'); process.exit(1); } const prompt_id = Math.random().toString(16).slice(2); logUserPrompt(config, { 'event.name': 'user_prompt', 'event.timestamp': new Date().toISOString(), prompt: input, prompt_id, auth_type: config.getContentGeneratorConfig()?.authType, prompt_length: input.length, }); const nonInteractiveConfig = await validateNonInteractiveAuth( settings.merged.selectedAuthType, settings.merged.useExternalAuth, config, ); await runNonInteractive(nonInteractiveConfig, input, prompt_id); process.exit(0); } function setWindowTitle(title: string, settings: LoadedSettings) { if (!settings.merged.hideWindowTitle) { const windowTitle = ( process.env['CLI_TITLE'] || `Gemini - ${title}` ).replace( // eslint-disable-next-line no-control-regex /[\x00-\x1F\x7F]/g, '', ); process.stdout.write(`\x1b]2;${windowTitle}\x07`); process.on('exit', () => { process.stdout.write(`\x1b]2;\x07`); }); } }