CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes48downloads
gemini.js854 linesDownload Raw Back to src
1/**2 * @license3 * Copyright 2025 Google LLC4 * SPDX-License-Identifier: Apache-2.05 */6import { AuthType, InputFormat, isDebugLoggingDegraded, isBareMode, logUserPrompt, QWEN_CODE_SIMPLE_ENV_VAR, Storage, SessionService, setStartupEventSink, createDebugLogger, persistSessionUsage, uiTelemetryService, } from '@qwen-code/qwen-code-core';7import dns from 'node:dns';8import os from 'node:os';9import path from 'node:path';10import v8 from 'node:v8';11import { validateAuthMethod } from './config/auth.js';12import * as cliConfig from './config/config.js';13import { buildDisabledSkillNamesProvider, loadCliConfig, parseArguments, } from './config/config.js';14import { ENV_CORRUPTED_PATH, ENV_WAS_RECOVERED, createMinimalSettings, getSettingsWarnings, loadSettings, preResolveHomeEnvOverrides, } from './config/settings.js';15import { SettingsWatcher } from './config/settingsWatcher.js';16import { registerMcpHotReload } from './config/hot-reload.js';17import { LspConfigWatcher } from './config/lsp-config-watcher.js';18import { initializeI18n, resolveLanguageSetting } from './i18n/index.js';19import { setupStartupWorktree, persistStartupWorktreeSidecar, buildStartupWorktreeNotice, } from './startup/worktreeStartup.js';20import { startEarlyStartupPrefetches } from './startup/startup-prefetch.js';21import { cleanupCheckpoints, registerCleanup, runExitCleanup, } from './utils/cleanup.js';22import { AppEvent, appEvents } from './utils/events.js';23import { readStdin } from './utils/readStdin.js';24import { profileCheckpoint, recordStartupEvent, setInteractiveMode, finalizeStartupProfile, isStartupProfilerEnabled, } from './utils/startupProfiler.js';25import { relaunchAppInChildProcess, relaunchOnExitCode, } from './utils/relaunch.js';26import { start_sandbox } from './utils/sandbox.js';27import { getStartupWarnings } from './utils/startupWarnings.js';28import { getUserStartupWarnings } from './utils/userStartupWarnings.js';29import { initializeWarningHandler } from './utils/warningHandler.js';30import { writeStderrLine } from './utils/stdioHelpers.js';31import { getHeadlessYoloSafetyWarning } from './utils/headlessSafetyWarnings.js';32import { initializeLlmOutputLanguage } from './utils/languageUtils.js';33const debugLogger = createDebugLogger('STARTUP');34function clearCorruptionEnvVars() {35    delete process.env[ENV_CORRUPTED_PATH];36    delete process.env[ENV_WAS_RECOVERED];37}38export function validateDnsResolutionOrder(order) {39    const defaultValue = 'ipv4first';40    if (order === undefined) {41        return defaultValue;42    }43    if (order === 'ipv4first' || order === 'verbatim') {44        return order;45    }46    // We don't want to throw here, just warn and use the default.47    writeStderrLine(`Invalid value for dnsResolutionOrder in settings: "${order}". Using default "${defaultValue}".`);48    return defaultValue;49}50function getNodeMemoryArgs(isDebugMode) {51    const totalMemoryMB = os.totalmem() / (1024 * 1024);52    const heapStats = v8.getHeapStatistics();53    const currentMaxOldSpaceSizeMb = Math.floor(heapStats.heap_size_limit / 1024 / 1024);54    // Set target to 50% of total memory55    const targetMaxOldSpaceSizeInMB = Math.floor(totalMemoryMB * 0.5);56    if (isDebugMode) {57        writeStderrLine(`Current heap size ${currentMaxOldSpaceSizeMb.toFixed(2)} MB`);58    }59    if (process.env['QWEN_CODE_NO_RELAUNCH']) {60        return [];61    }62    if (targetMaxOldSpaceSizeInMB > currentMaxOldSpaceSizeMb) {63        if (isDebugMode) {64            writeStderrLine(`Need to relaunch with more memory: ${targetMaxOldSpaceSizeInMB.toFixed(2)} MB`);65        }66        return [`--max-old-space-size=${targetMaxOldSpaceSizeInMB}`];67    }68    return [];69}70import { loadSandboxConfig } from './config/sandboxConfig.js';71export function setupUnhandledRejectionHandler() {72    let unhandledRejectionOccurred = false;73    process.on('unhandledRejection', (reason, _promise) => {74        const errorMessage = `=========================================75This is an unexpected error. Please file a bug report using the /bug tool.76CRITICAL: Unhandled Promise Rejection!77=========================================78Reason: ${reason}${reason instanceof Error && reason.stack79            ? `80Stack trace:81${reason.stack}`82            : ''}`;83        appEvents.emit(AppEvent.LogError, errorMessage);84        if (!unhandledRejectionOccurred) {85            unhandledRejectionOccurred = true;86            appEvents.emit(AppEvent.OpenDebugConsole);87        }88    });89}90function getSignalExitCode(signal) {91    return signal === 'SIGINT' ? 130 : 143;92}93function installInteractiveSignalHandlers(wasRaw) {94    let cleanupStarted = false;95    const handleSignal = (signal) => {96        if (process.stdin.isTTY) {97            process.stdin.setRawMode(wasRaw);98        }99        if (cleanupStarted) {100            return;101        }102        cleanupStarted = true;103        void runExitCleanup()104            .catch((error) => {105            debugLogger.error(`Error during ${signal} cleanup:`, error);106        })107            .finally(() => {108            process.exit(getSignalExitCode(signal));109        });110    };111    const handleSigterm = () => {112        handleSignal('SIGTERM');113    };114    const handleSigint = () => {115        handleSignal('SIGINT');116    };117    process.once('SIGTERM', handleSigterm);118    process.once('SIGINT', handleSigint);119    return () => {120        process.removeListener('SIGTERM', handleSigterm);121        process.removeListener('SIGINT', handleSigint);122    };123}124export async function main() {125    profileCheckpoint('main_entry');126    // Bridge core-package startup events (Config.initialize, MCP discovery,127    // GeminiClient.setTools) into the cli's startup profiler. Gated on128    // `isStartupProfilerEnabled()` so that when QWEN_CODE_PROFILE_STARTUP is129    // unset (the common case) every core-side `recordStartupEvent()` call130    // sees a null sink and short-circuits at the first comparison, instead131    // of going through this arrow wrapper and the profiler's own enabled132    // check.133    if (isStartupProfilerEnabled()) {134        setStartupEventSink((name, attrs) => recordStartupEvent(name, attrs));135    }136    setupUnhandledRejectionHandler();137    initializeWarningHandler();138    if (process.argv.includes('--bare')) {139        process.env[QWEN_CODE_SIMPLE_ENV_VAR] = '1';140    }141    // Run before yargs parses subcommands — handlers like `channel status`/`stop`142    // call `process.exit` before `loadSettings()` would otherwise bootstrap.143    preResolveHomeEnvOverrides();144    let argv = await parseArguments();145    profileCheckpoint('after_parse_arguments');146    if (isBareMode(argv.bare)) {147        process.env[QWEN_CODE_SIMPLE_ENV_VAR] = '1';148    }149    // Load user settings — bare mode uses minimal config, normal mode loads full.150    const settings = isBareMode(argv.bare)151        ? createMinimalSettings()152        : loadSettings();153    // Propagate corruption state to child process via env vars so154    // relaunchAppInChildProcess() doesn't lose the marker.155    if (settings.corruptedPath) {156        process.env[ENV_CORRUPTED_PATH] = settings.corruptedPath;157        process.env[ENV_WAS_RECOVERED] = settings.wasRecovered ? '1' : '0';158    }159    await cleanupCheckpoints();160    // Performance checkpoint161    profileCheckpoint('after_load_settings');162    // Emit settings warnings early so the parent process surfaces them163    // before relaunchAppInChildProcess() exits (the child has empty164    // migrationWarnings because the parent already renamed the file).165    const settingsWarnings = getSettingsWarnings(settings);166    for (const warning of settingsWarnings) {167        writeStderrLine(warning);168    }169    // Corruption notification no longer goes through migrationWarnings —170    // check corruptedPath directly to keep stderr visible in relaunch.171    if (settings.corruptedPath) {172        writeStderrLine('Warning: Settings file had invalid JSON and was reset. ' +173            'A copy of the corrupted file has been saved at: ' +174            settings.corruptedPath);175    }176    if (argv.listExtensions) {177        await initializeI18n(resolveLanguageSetting(settings.merged.general?.language));178        const { handleList: handleListExtensions } = await import('./commands/extensions/list.js');179        await handleListExtensions();180        process.exit(0);181    }182    // Check for invalid input combinations early to prevent crashes183    if (argv.promptInteractive && !process.stdin.isTTY) {184        writeStderrLine('Error: The --prompt-interactive flag cannot be used when input is piped from stdin.');185        process.exit(1);186    }187    const isDebugMode = cliConfig.isDebugMode(argv);188    dns.setDefaultResultOrder(validateDnsResolutionOrder(settings.merged.advanced?.dnsResolutionOrder));189    const { themeManager, AUTO_THEME_NAME } = await import('./ui/themes/theme-manager.js');190    // Load custom themes from settings191    themeManager.loadCustomThemes(settings.merged.ui?.customThemes);192    const configuredTheme = settings.merged.ui?.theme;193    if (configuredTheme && configuredTheme !== AUTO_THEME_NAME) {194        if (!themeManager.setActiveTheme(configuredTheme)) {195            // If the theme is not found during initial load, log a warning and continue.196            // The useThemeCommand hook in AppContainer.tsx will handle opening the dialog.197            writeStderrLine(`Warning: Theme "${configuredTheme}" not found.`);198        }199    }200    else {201        // 'auto' or unset: resolve a synchronous baseline (COLORFGBG + macOS)202        // so non-interactive runs and any pre-render UI (e.g. the --resume203        // session picker) already have a sensible theme. The interactive204        // startup block refines this with an OSC 11 probe later on, which is205        // intentionally deferred to run inside the early-capture window so206        // terminal response bytes cannot leak into the TUI input.207        themeManager.setActiveTheme(AUTO_THEME_NAME);208    }209    // hop into sandbox if we are outside and sandboxing is enabled210    if (!process.env['SANDBOX']) {211        const memoryArgs = settings.merged.advanced?.autoConfigureMemory212            ? getNodeMemoryArgs(isDebugMode)213            : [];214        const sandboxConfig = await loadSandboxConfig(settings.merged, argv);215        // We intentially omit the list of extensions here because extensions216        // should not impact auth or setting up the sandbox.217        // TODO(jacobr): refactor loadCliConfig so there is a minimal version218        // that only initializes enough config to enable refreshAuth or find219        // another way to decouple refreshAuth from requiring a config.220        if (sandboxConfig) {221            const partialConfig = await loadCliConfig(settings.merged, argv, undefined, [], 222            // Pass separated hooks for proper source attribution223            {224                userHooks: settings.getUserHooks(),225                projectHooks: settings.getProjectHooks(),226            }, buildDisabledSkillNamesProvider(settings));227            if (!settings.merged.security?.auth?.useExternal) {228                // Validate authentication here because the sandbox will interfere with the Oauth2 web redirect.229                try {230                    const authType = partialConfig.getModelsConfig().getCurrentAuthType();231                    // Fresh users may not have selected/persisted an authType yet.232                    // In that case, defer auth prompting/selection to the main interactive flow.233                    if (authType) {234                        const err = validateAuthMethod(authType, partialConfig);235                        if (err) {236                            throw new Error(err);237                        }238                        await partialConfig.refreshAuth(authType);239                    }240                }241                catch (err) {242                    writeStderrLine(`Error authenticating: ${err}`);243                    process.exit(1);244                }245            }246            // For stream-json and ACP modes, don't read stdin here — stdin carries247            // protocol data (not a user prompt) and should be forwarded to the sandbox248            // intact via stdio: 'inherit'.249            const inputFormat = argv.inputFormat;250            const isAcpMode = argv.acp || argv.experimentalAcp;251            let stdinData = '';252            if (!process.stdin.isTTY && inputFormat !== 'stream-json' && !isAcpMode) {253                stdinData = await readStdin();254            }255            // This function is a copy of the one from sandbox.ts256            // It is moved here to decouple sandbox.ts from the CLI's argument structure.257            const injectStdinIntoArgs = (args, stdinData) => {258                const finalArgs = [...args];259                if (stdinData) {260                    const promptIndex = finalArgs.findIndex((arg) => arg === '--prompt' || arg === '-p');261                    if (promptIndex > -1 && finalArgs.length > promptIndex + 1) {262                        // If there's a prompt argument, prepend stdin to it263                        finalArgs[promptIndex + 1] =264                            `${stdinData}\n\n${finalArgs[promptIndex + 1]}`;265                    }266                    else {267                        // If there's no prompt argument, add stdin as the prompt268                        finalArgs.push('--prompt', stdinData);269                    }270                }271                return finalArgs;272            };273            const injectSandboxSessionIdIntoArgs = (args, sessionId) => {274                const separatorIndex = args.indexOf('--');275                const cliArgs = separatorIndex < 0 ? args : args.slice(0, separatorIndex);276                const hasArg = (names) => cliArgs.some((arg) => names.some((name) => arg === name || arg.startsWith(`${name}=`)));277                if (hasArg(['--session-id', '--sandbox-session-id']) ||278                    hasArg(['--continue', '-c']) ||279                    hasArg(['--resume', '-r'])) {280                    return args;281                }282                const sessionArgs = ['--sandbox-session-id', sessionId];283                if (separatorIndex < 0) {284                    return [...args, ...sessionArgs];285                }286                return [...cliArgs, ...sessionArgs, ...args.slice(separatorIndex)];287            };288            const sessionId = partialConfig.getSessionId();289            const sandboxArgs = sessionId290                ? injectSandboxSessionIdIntoArgs(injectStdinIntoArgs(process.argv, stdinData), sessionId)291                : injectStdinIntoArgs(process.argv, stdinData);292            await relaunchOnExitCode(() => start_sandbox(sandboxConfig, memoryArgs, partialConfig, sandboxArgs));293            process.exit(0);294        }295        else {296            // Relaunch app so we always have a child process that can be internally297            // restarted if needed.298            await relaunchAppInChildProcess(memoryArgs, [], {299                afterSpawn: clearCorruptionEnvVars,300            });301        }302    }303    // When --worktree is going to chdir us into a worktree below, resolve304    // any relative-path argv fields to absolute paths now — BEFORE the305    // chdir. Otherwise downstream `fs.existsSync('./mcp.json')` calls in306    // `loadCliConfig` re-resolve against the worktree dir, where the file307    // doesn't exist. Only touches values that look like paths (mcpConfig308    // also accepts inline JSON — skip those).309    //310    // The list of fields below is hand-maintained. If you add a new311    // CLI flag that takes a relative path, register it here too,312    // otherwise --worktree silently breaks for that flag.313    if (argv.worktree !== undefined) {314        const launchCwdForPaths = process.cwd();315        const looksLikeInlineJson = (v) => {316            const t = v.trim();317            return t.startsWith('{') || t.startsWith('[');318        };319        const resolveIfPath = (v) => {320            if (typeof v !== 'string' || v.length === 0)321                return v;322            if (looksLikeInlineJson(v))323                return v;324            return path.resolve(launchCwdForPaths, v);325        };326        argv.mcpConfig = resolveIfPath(argv.mcpConfig);327        argv.openaiLoggingDir = resolveIfPath(argv.openaiLoggingDir);328        argv.jsonFile = resolveIfPath(argv.jsonFile);329        argv.inputFile = resolveIfPath(argv.inputFile);330        argv.telemetryOutfile = resolveIfPath(argv.telemetryOutfile);331        if (Array.isArray(argv.includeDirectories)) {332            argv.includeDirectories = argv.includeDirectories.map((d) => typeof d === 'string' && d.length > 0333                ? path.resolve(launchCwdForPaths, d)334                : d);335        }336        // `--json-schema` accepts either an inline schema or `@<path>`. The337        // `@`-prefixed form is read from disk inside `resolveJsonSchemaArg`338        // (`packages/cli/src/config/config.ts`), AFTER chdir, so a relative339        // value would resolve against the worktree — fix the prefix path340        // here.341        if (typeof argv.jsonSchema === 'string') {342            const trimmedSchema = argv.jsonSchema.trim();343            if (trimmedSchema.startsWith('@')) {344                const rel = trimmedSchema.slice(1);345                if (rel.length > 0 && !path.isAbsolute(rel)) {346                    argv.jsonSchema = '@' + path.resolve(launchCwdForPaths, rel);347                }348            }349        }350    }351    // Phase D-1: process --worktree before the resume picker so the picker352    // (which uses process.cwd() to scope its session search) finds sessions353    // saved inside the target worktree. Creates the worktree directory on354    // disk and chdirs into it; on failure we emit to stderr and exit before355    // any expensive initialization runs.356    //357    // ACP mode is exempt: the ACP host (Zed, etc.) supplies its own per-session358    // cwd, and the startup-level chdir would not propagate. Reject the359    // combination with a clear error rather than silently dropping --worktree.360    let startupWorktreeContext = null;361    if (argv.worktree !== undefined && (argv.acp || argv.experimentalAcp)) {362        writeStderrLine('--worktree cannot be combined with --acp / --experimental-acp. ' +363            'Pass the worktree path as the cwd of the ACP loadSession / newSession ' +364            'request instead.');365        process.exit(1);366    }367    {368        const startupRes = await setupStartupWorktree(argv.worktree, {369            symlinkDirectories: settings.merged.worktree?.symlinkDirectories,370        });371        if (startupRes !== null) {372            if (!startupRes.ok) {373                writeStderrLine(startupRes.error);374                process.exit(1);375            }376            startupWorktreeContext = startupRes.context;377        }378    }379    // Handle --resume without a session ID, or with a custom title, by showing380    // the session picker. Set the runtime output dir early so the picker can find381    // sessions stored under a custom runtimeOutputDir (setRuntimeBaseDir is382    // idempotent and will be called again inside loadCliConfig).383    if (argv.resume !== undefined) {384        Storage.setRuntimeBaseDir(settings.merged.advanced?.runtimeOutputDir, process.cwd());385        let resolvedSessionId;386        if (argv.resume === '') {387            // No argument — show picker388            const { showResumeSessionPicker } = await import('./ui/components/StandaloneSessionPicker.js');389            resolvedSessionId = await showResumeSessionPicker();390        }391        else if (!cliConfig.isValidSessionId(argv.resume)) {392            // Non-UUID argument — treat as custom title search393            const sessionService = new SessionService(process.cwd());394            const matches = await sessionService.findSessionsByTitle(argv.resume);395            if (matches.length === 1) {396                resolvedSessionId = matches[0].sessionId;397            }398            else if (matches.length > 1) {399                // Multiple matches — show picker to let user choose400                writeStderrLine(`Multiple sessions found with title "${argv.resume}". Please select one:`);401                const { showResumeSessionPicker } = await import('./ui/components/StandaloneSessionPicker.js');402                resolvedSessionId = await showResumeSessionPicker(process.cwd(), matches);403            }404            // matches.length === 0 → resolvedSessionId stays undefined, handled below405        }406        if (resolvedSessionId !== undefined) {407            argv = { ...argv, resume: resolvedSessionId };408        }409        else if (argv.resume === '' || !cliConfig.isValidSessionId(argv.resume)) {410            // User cancelled the picker or no sessions found for the title411            if (argv.resume !== '') {412                writeStderrLine(`No saved session found with title "${argv.resume}".`);413                process.exit(1);414            }415            else {416                process.exit(0);417            }418        }419        // else: argv.resume is already a valid UUID, pass through to loadCliConfig420    }421    // We are now past the logic handling potentially launching a child process422    // to run Qwen Code. It is now safe to perform expensive initialization that423    // may have side effects.424    profileCheckpoint('after_sandbox_check');425    // Initialize output language file before config loads to ensure it's included in context426    if (!isBareMode(argv.bare)) {427        initializeLlmOutputLanguage(settings.merged.general?.outputLanguage);428    }429    {430        // Start settings file watcher (skip in bare mode)431        const settingsWatcher = isBareMode(argv.bare)432            ? undefined433            : new SettingsWatcher(settings);434        settingsWatcher?.startWatching();435        const config = await loadCliConfig(settings.merged, argv, process.cwd(), argv.extensions, 436        // Pass separated hooks for proper source attribution437        {438            userHooks: settings.getUserHooks(),439            projectHooks: settings.getProjectHooks(),440        }, buildDisabledSkillNamesProvider(settings), undefined, settingsWatcher);441        profileCheckpoint('after_load_cli_config');442        // Subscribe the running Config to settings changes so MCP servers443        // reconnect / disconnect / restart without a session restart (#3696,444        // sub-task 3). Skipped in bare mode (no watcher).445        if (settingsWatcher) {446            const disposeMcpHotReload = registerMcpHotReload(settingsWatcher, settings, config, config.getTopTierMcpServers());447            registerCleanup(disposeMcpHotReload);448        }449        registerLspHotReload(config, registerCleanup);450        // Phase D-1: persist the WorktreeSession sidecar so Phase C's restore451        // machinery on a subsequent `--resume` picks the worktree back up, and452        // capture any override of a previously-resumed session's worktree so453        // we can emit a one-shot notice on the model's first prompt.454        //455        // The notice is set BEFORE the persist attempt and AGAIN inside the456        // try block (so the override addendum can be appended on success).457        // A persist failure must NOT silently drop the notice — the cwd is458        // already switched, and the model needs to know which worktree it's459        // operating in regardless of whether the sidecar landed.460        if (startupWorktreeContext) {461            config.setPendingStartupWorktreeNotice(buildStartupWorktreeNotice(startupWorktreeContext));462            try {463                const startupWorktreePersist = await persistStartupWorktreeSidecar(config, startupWorktreeContext);464                if (startupWorktreePersist.overrodeResumedWorktree) {465                    writeStderrLine(`--worktree overrode the resumed session's previous worktree ` +466                        `"${startupWorktreePersist.overriddenSlug ?? '(unknown)'}". ` +467                        `That worktree directory was left intact on disk.`);468                }469                // Refresh the notice with the override addendum (if any). When470                // there is no override this is a no-op text-wise; on override it471                // gives the model the "you overrode <previous-slug>" hint. TUI472                // and headless consume this via Config.consumePendingStartupWorktreeNotice();473                // ACP is excluded above (`--worktree` × `--acp` is mutually474                // exclusive — see the mutex check earlier in this function).475                config.setPendingStartupWorktreeNotice(buildStartupWorktreeNotice(startupWorktreeContext, startupWorktreePersist));476            }477            catch (error) {478                debugLogger.warn(`--worktree sidecar persist failed (non-fatal, notice preserved): ${error instanceof Error ? error.message : String(error)}`);479            }480        }481        // Persist session usage for cross-session reports (must run before482        // config.shutdown() which clears telemetry state).483        // sessionStartTime is read from uiTelemetryService so it stays correct484        // after /clear resets the session (reset() updates the internal timestamp).485        registerCleanup(() => {486            try {487                const metrics = uiTelemetryService.getMetrics();488                const hasActivity = Object.values(metrics.models).some((m) => m.api.totalRequests > 0);489                if (!hasActivity)490                    return;491                persistSessionUsage({492                    sessionId: config.getSessionId(),493                    startTime: uiTelemetryService.getSessionStartTime(),494                    endTime: new Date(),495                    project: config.getProjectRoot(),496                    metrics,497                });498            }499            catch {500                // Best-effort — don't block shutdown501            }502        });503        // Register cleanup for MCP clients as early as possible504        // This ensures MCP server subprocesses are properly terminated on exit505        registerCleanup(() => config.shutdown());506        startEarlyStartupPrefetches(config);507        const wasRaw = process.stdin.isRaw;508        let kittyProtocolDetectionComplete;509        let themeAutoDetectionComplete;510        if (config.isInteractive()) {511            registerCleanup(installInteractiveSignalHandlers(wasRaw));512        }513        if (config.isInteractive() && !wasRaw && process.stdin.isTTY) {514            const { startEarlyInputCapture, stopAndGetCapturedInput } = await import('./utils/earlyInputCapture.js');515            const { detectAndEnableKittyProtocol } = await import('./ui/utils/kittyProtocolDetector.js');516            // Set this as early as possible to avoid spurious characters from517            // input showing up in the output.518            process.stdin.setRawMode(true);519            // Startup optimization: start early input capture520            startEarlyInputCapture();521            // Ensure the stdin listener is removed on any exit path (error, signal, etc.)522            registerCleanup(() => stopAndGetCapturedInput());523            // Detect and enable Kitty keyboard protocol once at startup.524            kittyProtocolDetectionComplete = detectAndEnableKittyProtocol();525            // Auto-detect theme (OSC 11 + COLORFGBG + macOS) when the user has526            // opted into 'auto' or has not configured a theme at all. Kicked off527            // here without awaiting so the OSC 11 timeout overlaps with the528            // heavier startup work below (initializeApp, warnings) instead of529            // blocking the critical path. The synchronous baseline picked above530            // keeps the active theme valid in the meantime; this probe only531            // refines it. Running inside the early-capture window is deliberate:532            // the filter in startEarlyInputCapture absorbs the OSC 11 response533            // bytes so they cannot leak into the TUI input, even though our534            // probe attaches its own listener to parse the RGB value.535            if (!configuredTheme || configuredTheme === AUTO_THEME_NAME) {536                themeAutoDetectionComplete = themeManager537                    .resolveAutoThemeAsync()538                    .catch((err) => {539                    debugLogger.warn('Async theme auto-detection failed:', err);540                });541            }542        }543        if (config.isInteractive()) {544            const { setMaxSizedBoxDebugging } = await import('./ui/components/shared/MaxSizedBox.js');545            setMaxSizedBoxDebugging(isDebugMode);546        }547        // Check input format early to determine initialization flow548        // In TTY mode, ignore stream-json input format to prevent process from hanging549        const inputFormat = process.stdin.isTTY550            ? InputFormat.TEXT551            : typeof config.getInputFormat === 'function'552                ? config.getInputFormat()553                : InputFormat.TEXT;554        // For stream-json mode, defer config.initialize() until after the initialize control request555        // For other modes, initialize normally556        const { initializeApp } = await import('./core/initializer.js');557        let input = config.getQuestion();558        const hasRemoteInput = Boolean(config.getInputFile?.());559        const deferIdeConnection = config.isInteractive() &&560            !config.getExperimentalZedIntegration() &&561            !input &&562            !hasRemoteInput;563        const initializationResult = await initializeApp(config, settings, {564            deferIdeConnection,565        });566        profileCheckpoint('after_initialize_app');567        if (config.getExperimentalZedIntegration()) {568            const { runAcpAgent } = await import('./acp-integration/acpAgent.js');569            await runAcpAgent(config, settings, argv);570            // Clean up child processes and force exit, matching other non-interactive modes571            await runExitCleanup();572            process.exit(0);573        }574        const startupWarnings = [575            ...new Set([576                ...(config.isSafeMode()577                    ? [578                        '⚠ SAFE MODE — all customizations disabled (hooks, extensions, skills, MCP servers, QWEN.md). Restart without --safe-mode to resume normal operation.',579                    ]580                    : []),581                ...(await getStartupWarnings()),582                ...(await getUserStartupWarnings({583                    workspaceRoot: process.cwd(),584                    useRipgrep: settings.merged.tools?.useRipgrep ?? true,585                    useBuiltinRipgrep: settings.merged.tools?.useBuiltinRipgrep ?? true,586                })),587                ...getSettingsWarnings(settings),588                ...config.getWarnings(),589                ...(config.getModelsConfig().getCurrentAuthType() ===590                    AuthType.QWEN_OAUTH591                    ? [592                        'Qwen OAuth free tier was discontinued on 2026-04-15. Run /auth to switch to Coding Plan or another provider.',593                    ]594                    : []),595            ]),596        ];597        const emittedStartupWarnings = new Set(startupWarnings);598        // Surface critical startup warnings (corrupted settings, recovery, etc.)599        // to stderr so they are visible regardless of UI mode. In interactive600        // mode the TUI's Notifications component also renders them, but the601        // onboarding flow can obscure the notification area, leaving users602        // unaware that their settings were reset. Writing to stderr before603        // the TUI takes over ensures the message is visible in the terminal604        // scrollback. In non-interactive mode this is the *only* channel.605        for (const warning of startupWarnings) {606            writeStderrLine(warning);607        }608        // Render UI, passing necessary config values. Check that there is no command line question.609        profileCheckpoint('before_render');610        if (config.isInteractive()) {611            // --json-schema is a headless-only contract: the synthetic612            // structured_output tool only terminates the run inside613            // runNonInteractive's main/drain loops. In TUI mode the same call614            // would just emit "Structured output accepted." and keep the chat615            // alive, which silently strands the user's run. Parse-time gating616            // can't catch this case (`qwen --json-schema '...'` on a TTY with617            // no prompt routes to interactive only after stdin TTY detection),618            // so reject here before the UI launches.619            if (config.getJsonSchema?.()) {620                writeStderrLine('Error: --json-schema is a headless-only flag. Provide a one-shot prompt via -p / --prompt or pipe one in via stdin.');621                // Run cleanup so MCP subprocesses + telemetry exporters that the622                // earlier initializeApp() / loadCliConfig() registered get shut623                // down — process.exit() doesn't drain them on its own.624                await runExitCleanup();625                process.exit(1);626            }627            // For the interactive path, the profile is finalized by AppContainer628            // after `config.initialize()` and `input_enabled` are recorded — that's629            // the only way `first_paint`, `config_initialize_*`, `input_enabled`,630            // and the MCP events are captured. See AppContainer's mount effect.631            setInteractiveMode(true);632            // Need kitty detection to be complete before we can start the interactive UI.633            await kittyProtocolDetectionComplete;634            // Drain the auto-theme probe before render so the OSC 11 response is635            // absorbed by the early-capture filter (which is closed inside636            // startInteractiveUI) and so the first paint uses the refined theme637            // when the probe finishes in time.638            await themeAutoDetectionComplete;639            const { startInteractiveUI } = await import('./ui/startInteractiveUI.js');640            await startInteractiveUI(config, settings, startupWarnings, process.cwd(), initializationResult, {641                postRenderConnectIde: deferIdeConnection,642            });643            // Clean up corruption env vars so subsequent relaunch children644            // and subprocesses don't inherit stale state.645            clearCorruptionEnvVars();646            return;647        }648        // Also clean up env vars for non-interactive paths so that649        // subprocesses don't inherit stale state.650        clearCorruptionEnvVars();651        // Non-interactive: defer finalize until after `config.initialize()` runs652        // so MCP discovery events (mcp_first_tool_registered, mcp_all_servers_settled,653        // gemini_tools_updated) are captured in the profile.654        // Print debug mode notice to stderr for non-interactive mode655        if (config.getDebugMode()) {656            writeStderrLine('Debug mode enabled');657            writeStderrLine(`Logging to: ${Storage.getDebugLogPath(config.getSessionId())}`);658            if (isDebugLoggingDegraded()) {659                writeStderrLine('Warning: Debug logging is degraded (write failures occurred)');660            }661        }662        // Headless + YOLO without a sandbox lets the model auto-approve and663        // execute shell / write / edit tools at the current process's664        // privilege level. Emit a one-line stderr warning so unattended runs665        // have at least an observable signal. Interactive runs are excluded666        // because the user is at the keyboard and the TUI shows approval667        // state directly. See issue #4103.668        if (!config.isInteractive()) {669            const yoloWarning = getHeadlessYoloSafetyWarning(config);670            if (yoloWarning)671                writeStderrLine(yoloWarning);672        }673        // For non-stream-json mode, initialize config here. Stream-json defers674        // `config.initialize()` to inside `Session.ensureConfigInitialized`675        // because the initial control_request may register SDK MCP servers676        // that must be in place before discovery runs (see session.ts).677        if (inputFormat !== InputFormat.STREAM_JSON) {678            profileCheckpoint('config_initialize_start');679            await config.initialize();680            for (const warning of config.getWarnings()) {681                if (emittedStartupWarnings.has(warning))682                    continue;683                emittedStartupWarnings.add(warning);684                writeStderrLine(warning);685            }686            profileCheckpoint('config_initialize_end');687            // Non-interactive paths feed a prompt to the model immediately after688            // init. Under PR-A's progressive MCP availability,689            // `config.initialize()` returns BEFORE MCP servers settle, so690            // without this wait the first sendMessage would see only built-in691            // tools — a silent regression versus the legacy synchronous692            // behavior. Interactive paths skip this (AppContainer's batch-flush693            // subscriber updates the tool list as MCP servers come online).694            await config.waitForMcpReady();695            // Surface MCP server failures on stderr so non-interactive runs696            // (--prompt / piped stdin / scripts) don't silently regress to697            // built-in-tools-only when a server cannot connect. The legacy698            // synchronous MCP path was visibly noisy on failures because699            // per-server errors logged to stderr during the blocking700            // `discoverAllMcpTools` call; PR-A moves discovery to a701            // background promise whose per-server errors are caught inside702            // `discoverAllMcpToolsIncremental` and never reach a TTY. This703            // helper closes that gap without re-introducing blocking.704            // Defensive against tests that pass a stubbed Config without705            // `getFailedMcpServerNames` — the warning is best-effort visibility706            // and never gates startup.707            const failedMcpServers = typeof config.getFailedMcpServerNames === 'function'708                ? config.getFailedMcpServerNames()709                : [];710            if (failedMcpServers.length > 0) {711                writeStderrLine(`Warning: MCP server(s) failed to start: ${failedMcpServers.join(', ')}. ` +712                    `Continuing with built-in tools and any servers that did connect. ` +713                    `Re-run with QWEN_CODE_DEBUG=1 to see per-server reasons.`);714            }715            // Finalize the non-interactive startup profile here so MCP events716            // emitted during initialize() / waitForMcpReady() are captured.717            // Subsequent stdin reads / auth checks / prompt execution are not718            // part of the "first-screen" budget.719            //720            // For stream-json we deliberately do NOT finalize here: the profile721            // is finalized inside Session.ensureConfigInitialized() after MCP722            // settles, so its `config_initialize_*` and MCP events make it into723            // the file. Finalizing here would write an empty profile and the724            // module-level `finalized` guard would suppress every subsequent725            // event.726            finalizeStartupProfile(config.getSessionId());727        }728        // Only read stdin if NOT in stream-json mode729        // In stream-json mode, stdin is used for protocol messages (control requests, etc.)730        // and should be consumed by StreamJsonInputReader instead731        if (inputFormat !== InputFormat.STREAM_JSON && !process.stdin.isTTY) {732            const stdinData = await readStdin();733            if (stdinData) {734                input = `${stdinData}\n\n${input}`;735            }736        }737        const { validateNonInteractiveAuth } = await import('./validateNonInterActiveAuth.js');738        const nonInteractiveConfig = await validateNonInteractiveAuth(settings.merged.security?.auth?.useExternal, config, settings);739        const prompt_id = createNonInteractivePromptId(config.getSessionId());740        if (inputFormat === InputFormat.STREAM_JSON) {741            const trimmedInput = (input ?? '').trim();742            const { runNonInteractiveStreamJson } = await import('./nonInteractive/session.js');743            await runNonInteractiveStreamJson(nonInteractiveConfig, trimmedInput.length > 0 ? trimmedInput : '', settings);744            await runExitCleanup();745            // `runNonInteractiveStreamJson` doesn't return an explicit exit746            // code yet, so a cleanup task that mutates `process.exitCode`747            // could clobber a non-zero failure signal. This is currently safe748            // because `--json-schema` is rejected at parse time when combined749            // with `--input-format stream-json` (see the yargs `.check` in750            // resolveCliGenerationConfig), so structured-output failures751            // never reach this branch. If a future stream-json equivalent of752            // structured output is added, plumb the exit code through the753            // function's return value the way `runNonInteractive` below does.754            process.exit(process.exitCode ?? 0);755        }756        if (!input) {757            writeStderrLine(`No input provided via stdin. Input can be provided by piping data into gemini or using the --prompt option.`);758            process.exit(1);759        }760        logUserPrompt(config, {761            'event.name': 'user_prompt',762            'event.timestamp': new Date().toISOString(),763            prompt: input,764            prompt_id,765            auth_type: config.getContentGeneratorConfig()?.authType,766            prompt_length: input.length,767        });768        debugLogger.debug(`Session ID: ${config.getSessionId()}`);769        const { runNonInteractive } = await import('./nonInteractiveCli.js');770        const exitCode = await runNonInteractive(nonInteractiveConfig, settings, input, prompt_id);771        // Call cleanup before process.exit, which causes cleanup to not run.772        // Capture the exit code BEFORE cleanup so any cleanup task that773        // mutates process.exitCode can't silently turn a structured-output774        // failure (or other explicit non-zero return from runNonInteractive)775        // into a zero exit.776        await runExitCleanup();777        process.exit(exitCode);778    }779}780export function createNonInteractivePromptId(sessionId) {781    return `${sessionId}########0`;782}783/**784 * Watches `.lsp.json` for changes and reconciles running LSP servers785 * (add / remove / restart) without requiring a session restart.786 *787 * Silently no-ops when LSP is disabled or the active client does not788 * support runtime reinitialization.789 *790 * Emits {@link AppEvent.LspStatusChanged} after every successful reload791 * so the UI can reflect the new server state.792 */793export function registerLspHotReload(config, registerCleanup) {794    if (config.isLspEnabled?.() !== true ||795        !config.getLspClient?.()?.reinitialize) {796        return;797    }798    const lspConfigWatcher = new LspConfigWatcher(config.getProjectRoot());799    debugLogger.info(`Registering LSP config hot reload watcher for ${config.getProjectRoot()}`);800    lspConfigWatcher.startWatching(async (event) => {801        if (event.changeType === 'invalid') {802            debugLogger.warn(`Invalid LSP config file ${event.path}: ${event.error}`);803            appEvents.emit(AppEvent.LogError, event.error);804            return;805        }806        debugLogger.info(`Reloading LSP server settings: changeType=${event.changeType}, path=${event.path}`);807        let errorReported = false;808        try {809            const result = await config.reinitializeLsp();810            if (result) {811                const failedServers = getRuntimeReloadFailedNames(result.reconcile);812                debugLogger.info(`Reloaded LSP server settings: added=${formatRuntimeReloadNames(result.reconcile.added)}, removed=${formatRuntimeReloadNames(result.reconcile.removed)}, restarted=${formatRuntimeReloadNames(result.reconcile.restarted)}, unchanged=${formatRuntimeReloadNames(result.reconcile.unchanged)}, failed=${formatRuntimeReloadNames(failedServers)}, skipped=${formatRuntimeReloadNames(result.skipped.map((server) => server.name))}`);813                if (failedServers.length > 0) {814                    appEvents.emit(AppEvent.LspStatusChanged);815                    const changedServers = [816                        ...result.reconcile.added,817                        ...result.reconcile.removed,818                        ...result.reconcile.restarted,819                    ];820                    const message = `LSP reload partially completed: changed=${formatRuntimeReloadNames(changedServers)}, failed=${formatRuntimeReloadNames(failedServers)}. Run with --debug for details.`;821                    appEvents.emit(AppEvent.LogError, message);822                    errorReported = true;823                    throw new Error(message);824                }825            }826            else {827                debugLogger.info('Skipped LSP server settings reload because LSP is disabled or no client is available');828            }829            appEvents.emit(AppEvent.LspStatusChanged);830        }831        catch (error) {832            debugLogger.warn('Failed to reload LSP server settings:', error);833            if (!errorReported) {834                const message = error instanceof Error835                    ? `Failed to reload LSP server settings: ${error.message}. Some LSP servers may have been partially updated. Run with --debug for details.`836                    : 'Failed to reload LSP server settings; some LSP servers may have been partially updated. Run with --debug for details.';837                appEvents.emit(AppEvent.LogError, message);838            }839            throw error;840        }841    });842    registerCleanup(() => lspConfigWatcher.stopWatching());843}844function formatRuntimeReloadNames(names) {845    return names.length === 0 ? '<none>' : names.join(',');846}847/**848 * Reads the optional failed bucket defensively because the CLI may typecheck849 * against stale core dist declarations during local development.850 */851function getRuntimeReloadFailedNames(reconcile) {852    return reconcile.failed ?? [];853}854//# sourceMappingURL=gemini.js.map
basant307/AI_Governance_Project · CoolFace