/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import type { Config } from '@qwen-code/qwen-code-core'; import { AuthType, getOauthClient, InputFormat, logUserPrompt, } from '@qwen-code/qwen-code-core'; import { render } from 'ink'; import dns from 'node:dns'; import os from 'node:os'; import { basename } from 'node:path'; import v8 from 'node:v8'; import React from 'react'; import { validateAuthMethod } from './config/auth.js'; import * as cliConfig from './config/config.js'; import { loadCliConfig, parseArguments } from './config/config.js'; import { ExtensionStorage, loadExtensions } from './config/extension.js'; import type { DnsResolutionOrder, LoadedSettings } from './config/settings.js'; import { loadSettings, migrateDeprecatedSettings } from './config/settings.js'; import { initializeApp, type InitializationResult, } from './core/initializer.js'; import { runNonInteractive } from './nonInteractiveCli.js'; import { runNonInteractiveStreamJson } from './nonInteractive/session.js'; import { AppContainer } from './ui/AppContainer.js'; import { setMaxSizedBoxDebugging } from './ui/components/shared/MaxSizedBox.js'; import { KeypressProvider } from './ui/contexts/KeypressContext.js'; import { SessionStatsProvider } from './ui/contexts/SessionContext.js'; import { SettingsContext } from './ui/contexts/SettingsContext.js'; import { VimModeProvider } from './ui/contexts/VimModeContext.js'; import { useKittyKeyboardProtocol } from './ui/hooks/useKittyKeyboardProtocol.js'; import { themeManager } from './ui/themes/theme-manager.js'; import { ConsolePatcher } from './ui/utils/ConsolePatcher.js'; import { detectAndEnableKittyProtocol } from './ui/utils/kittyProtocolDetector.js'; import { checkForUpdates } from './ui/utils/updateCheck.js'; import { cleanupCheckpoints, registerCleanup, runExitCleanup, } from './utils/cleanup.js'; import { AppEvent, appEvents } from './utils/events.js'; import { handleAutoUpdate } from './utils/handleAutoUpdate.js'; import { readStdin } from './utils/readStdin.js'; import { relaunchAppInChildProcess, relaunchOnExitCode, } from './utils/relaunch.js'; import { start_sandbox } from './utils/sandbox.js'; import { getStartupWarnings } from './utils/startupWarnings.js'; import { getUserStartupWarnings } from './utils/userStartupWarnings.js'; import { getCliVersion } from './utils/version.js'; import { computeWindowTitle } from './utils/windowTitle.js'; import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js'; import { showResumeSessionPicker } from './ui/components/ResumeSessionPicker.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(isDebugMode: boolean): 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 (isDebugMode) { console.debug( `Current heap size ${currentMaxOldSpaceSizeMb.toFixed(2)} MB`, ); } if (process.env['GEMINI_CLI_NO_RELAUNCH']) { return []; } if (targetMaxOldSpaceSizeInMB > currentMaxOldSpaceSizeMb) { if (isDebugMode) { console.debug( `Need to relaunch with more memory: ${targetMaxOldSpaceSizeInMB.toFixed(2)} MB`, ); } return [`--max-old-space-size=${targetMaxOldSpaceSizeInMB}`]; } return []; } import { ExtensionEnablementManager } from './config/extensions/extensionEnablement.js'; import { loadSandboxConfig } from './config/sandboxConfig.js'; import { runAcpAgent } from './acp-integration/acpAgent.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 startInteractiveUI( config: Config, settings: LoadedSettings, startupWarnings: string[], workspaceRoot: string = process.cwd(), initializationResult: InitializationResult, ) { const version = await getCliVersion(); setWindowTitle(basename(workspaceRoot), settings); // Create wrapper component to use hooks inside render const AppWrapper = () => { const kittyProtocolStatus = useKittyKeyboardProtocol(); const nodeMajorVersion = parseInt(process.versions.node.split('.')[0], 10); return ( ); }; const instance = render( process.env['DEBUG'] ? ( ) : ( ), { 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()); } export async function main() { setupUnhandledRejectionHandler(); const settings = loadSettings(); migrateDeprecatedSettings(settings); await cleanupCheckpoints(); let argv = await parseArguments(settings.merged); // Check for invalid input combinations early to prevent crashes if (argv.promptInteractive && !process.stdin.isTTY) { console.error( 'Error: The --prompt-interactive flag cannot be used when input is piped from stdin.', ); process.exit(1); } const isDebugMode = cliConfig.isDebugMode(argv); dns.setDefaultResultOrder( validateDnsResolutionOrder(settings.merged.advanced?.dnsResolutionOrder), ); // Load custom themes from settings themeManager.loadCustomThemes(settings.merged.ui?.customThemes); if (settings.merged.ui?.theme) { if (!themeManager.setActiveTheme(settings.merged.ui?.theme)) { // If the theme is not found during initial load, log a warning and continue. // The useThemeCommand hook in AppContainer.tsx will handle opening the dialog. console.warn(`Warning: Theme "${settings.merged.ui?.theme}" not found.`); } } // hop into sandbox if we are outside and sandboxing is enabled if (!process.env['SANDBOX']) { const memoryArgs = settings.merged.advanced?.autoConfigureMemory ? getNodeMemoryArgs(isDebugMode) : []; const sandboxConfig = await loadSandboxConfig(settings.merged, argv); // We intentially omit the list of extensions here because extensions // 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. if (sandboxConfig) { const partialConfig = await loadCliConfig( settings.merged, [], new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()), argv, ); if ( settings.merged.security?.auth?.selectedType && !settings.merged.security?.auth?.useExternal ) { // Validate authentication here because the sandbox will interfere with the Oauth2 web redirect. try { const err = validateAuthMethod( settings.merged.security.auth.selectedType, ); if (err) { throw new Error(err); } await partialConfig.refreshAuth( settings.merged.security.auth.selectedType, ); } 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 relaunchOnExitCode(() => start_sandbox(sandboxConfig, memoryArgs, partialConfig, sandboxArgs), ); process.exit(0); } else { // Relaunch app so we always have a child process that can be internally // restarted if needed. await relaunchAppInChildProcess(memoryArgs, []); } } // Handle --resume without a session ID by showing the session picker if (argv.resume === '') { const selectedSessionId = await showResumeSessionPicker(); if (!selectedSessionId) { // User cancelled or no sessions available process.exit(0); } // Update argv with the selected session ID argv = { ...argv, resume: selectedSessionId }; } // 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. { const extensionEnablementManager = new ExtensionEnablementManager( ExtensionStorage.getUserExtensionsDir(), argv.extensions, ); const extensions = loadExtensions(extensionEnablementManager); const config = await loadCliConfig( settings.merged, extensions, extensionEnablementManager, argv, ); if (config.getListExtensions()) { console.log('Installed extensions:'); for (const extension of extensions) { console.log(`- ${extension.config.name}`); } process.exit(0); } // Setup unified ConsolePatcher based on interactive mode const isInteractive = config.isInteractive(); const consolePatcher = new ConsolePatcher({ stderr: isInteractive, debugMode: isDebugMode, }); consolePatcher.patch(); registerCleanup(consolePatcher.cleanup); const wasRaw = process.stdin.isRaw; let kittyProtocolDetectionComplete: Promise | undefined; 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. process.on('SIGTERM', () => { process.stdin.setRawMode(wasRaw); }); process.on('SIGINT', () => { process.stdin.setRawMode(wasRaw); }); // Detect and enable Kitty keyboard protocol once at startup. kittyProtocolDetectionComplete = detectAndEnableKittyProtocol(); } setMaxSizedBoxDebugging(isDebugMode); // Check input format early to determine initialization flow const inputFormat = typeof config.getInputFormat === 'function' ? config.getInputFormat() : InputFormat.TEXT; // For stream-json mode, defer config.initialize() until after the initialize control request // For other modes, initialize normally let initializationResult: InitializationResult | undefined; if (inputFormat !== InputFormat.STREAM_JSON) { initializationResult = await initializeApp(config, settings); } if ( settings.merged.security?.auth?.selectedType === 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); } if (config.getExperimentalZedIntegration()) { return runAcpAgent(config, settings, extensions, argv); } let input = config.getQuestion(); const startupWarnings = [ ...(await getStartupWarnings()), ...(await getUserStartupWarnings({ workspaceRoot: process.cwd(), useRipgrep: settings.merged.tools?.useRipgrep ?? true, useBuiltinRipgrep: settings.merged.tools?.useBuiltinRipgrep ?? true, })), ]; // Render UI, passing necessary config values. Check that there is no command line question. if (config.isInteractive()) { // Need kitty detection to be complete before we can start the interactive UI. await kittyProtocolDetectionComplete; await startInteractiveUI( config, settings, startupWarnings, process.cwd(), initializationResult!, ); return; } // For non-stream-json mode, initialize config here if (inputFormat !== InputFormat.STREAM_JSON) { await config.initialize(); } // Only read stdin if NOT in stream-json mode // In stream-json mode, stdin is used for protocol messages (control requests, etc.) // and should be consumed by StreamJsonInputReader instead if (inputFormat !== InputFormat.STREAM_JSON && !process.stdin.isTTY) { const stdinData = await readStdin(); if (stdinData) { input = `${stdinData}\n\n${input}`; } } const nonInteractiveConfig = await validateNonInteractiveAuth( (argv.authType as AuthType) || settings.merged.security?.auth?.selectedType, settings.merged.security?.auth?.useExternal, config, settings, ); const prompt_id = Math.random().toString(16).slice(2); if (inputFormat === InputFormat.STREAM_JSON) { const trimmedInput = (input ?? '').trim(); await runNonInteractiveStreamJson( nonInteractiveConfig, trimmedInput.length > 0 ? trimmedInput : '', ); await runExitCleanup(); process.exit(0); } if (!input) { console.error( `No input provided via stdin. Input can be provided by piping data into gemini or using the --prompt option.`, ); process.exit(1); } logUserPrompt(config, { 'event.name': 'user_prompt', 'event.timestamp': new Date().toISOString(), prompt: input, prompt_id, auth_type: config.getContentGeneratorConfig()?.authType, prompt_length: input.length, }); if (config.getDebugMode()) { console.log('Session ID: %s', config.getSessionId()); } await runNonInteractive(nonInteractiveConfig, settings, input, prompt_id); // Call cleanup before process.exit, which causes cleanup to not run await runExitCleanup(); process.exit(0); } } function setWindowTitle(title: string, settings: LoadedSettings) { if (!settings.merged.ui?.hideWindowTitle) { const windowTitle = computeWindowTitle(title); process.stdout.write(`\x1b]2;${windowTitle}\x07`); process.on('exit', () => { process.stdout.write(`\x1b]2;\x07`); }); } }