Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 150 additions & 34 deletions packages/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { main } from './src/gemini.js';
import { FatalError, writeToStderr } from '@google/gemini-cli-core';
import { runExitCleanup } from './src/utils/cleanup.js';
import { spawn } from 'node:child_process';
import os from 'node:os';
import v8 from 'node:v8';

// --- Global Entry Point ---

Expand All @@ -28,44 +28,160 @@ process.on('uncaughtException', (error) => {
// For other errors, we rely on the default behavior, but since we attached a listener,
// we must manually replicate it.
if (error instanceof Error) {
writeToStderr(error.stack + '\n');
process.stderr.write(error.stack + '\n');
} else {
writeToStderr(String(error) + '\n');
process.stderr.write(String(error) + '\n');
}
process.exit(1);
});

main().catch(async (error) => {
// Set a timeout to force exit if cleanup hangs
const cleanupTimeout = setTimeout(() => {
writeToStderr('Cleanup timed out, forcing exit...\n');
process.exit(1);
}, 5000);

try {
await runExitCleanup();
} catch (cleanupError) {
writeToStderr(
`Error during final cleanup: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}\n`,
);
} finally {
clearTimeout(cleanupTimeout);
}
async function run() {
if (!process.env['GEMINI_CLI_NO_RELAUNCH'] && !process.env['SANDBOX']) {
// --- Lightweight Parent Process / Daemon ---
// We avoid importing heavy dependencies here to save ~1.5s of startup time.

// Check memory arguments (lightweight version)
const nodeArgs: string[] = [...process.execArgv];
const scriptArgs = process.argv.slice(2);
const isCommand =
scriptArgs.length > 0 &&
[
'mcp',
'extensions',
'extension',
'skills',
'skill',
'hooks',
'hook',
].includes(scriptArgs[0]);

if (error instanceof FatalError) {
let errorMessage = error.message;
if (!process.env['NO_COLOR']) {
errorMessage = `\x1b[31m${errorMessage}\x1b[0m`;
if (!isCommand) {
let autoConfigureMemory = true;
try {
const { readFileSync } = await import('node:fs');
const { join } = await import('node:path');
const homedir = os.homedir();
const settingsPath = join(homedir, '.gemini', 'settings.json');
const rawSettings = readFileSync(settingsPath, 'utf8');
const settings = JSON.parse(rawSettings);
if (settings?.advanced?.autoConfigureMemory === false) {
autoConfigureMemory = false;
}
} catch {
// ignore
}

if (autoConfigureMemory) {
// We allocate 50% of system memory because parsing large codebases, handling
// huge contexts (like massive prompts or full repo context), and maintaining
// terminal UI states in Ink requires significantly more memory than the V8 default
// (which is ~2GB-4GB). Without this, the app easily crashes with Out of Memory errors.
const totalMemoryMB = os.totalmem() / (1024 * 1024);
const heapStats = v8.getHeapStatistics();
const currentMaxOldSpaceSizeMb = Math.floor(
heapStats.heap_size_limit / 1024 / 1024,
);
const targetMaxOldSpaceSizeInMB = Math.floor(totalMemoryMB * 0.5);

if (targetMaxOldSpaceSizeInMB > currentMaxOldSpaceSizeMb) {
nodeArgs.push(`--max-old-space-size=${targetMaxOldSpaceSizeInMB}`);
}
}
}
writeToStderr(errorMessage + '\n');
process.exit(error.exitCode);
}

writeToStderr('An unexpected critical error occurred:');
if (error instanceof Error) {
writeToStderr(error.stack + '\n');
const script = process.argv[1];
nodeArgs.push(script);
nodeArgs.push(...scriptArgs);

const newEnv = { ...process.env, GEMINI_CLI_NO_RELAUNCH: 'true' };
const RELAUNCH_EXIT_CODE = 199;
let latestAdminSettings: unknown = undefined;

const runner = () => {
process.stdin.pause();

const child = spawn(process.execPath, nodeArgs, {
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
env: newEnv,
});

if (latestAdminSettings) {
child.send({ type: 'admin-settings', settings: latestAdminSettings });
}

child.on('message', (msg: { type?: string; settings?: unknown }) => {
if (msg.type === 'admin-settings-update' && msg.settings) {
latestAdminSettings = msg.settings;
}
});

return new Promise<number>((resolve) => {
child.on('error', () => resolve(1));
child.on('close', (code) => {
process.stdin.resume();
resolve(code ?? 1);
});
});
};

while (true) {
try {
const exitCode = await runner();
if (exitCode !== RELAUNCH_EXIT_CODE) {
process.exit(exitCode);
}
} catch (error: unknown) {
process.stdin.resume();
process.stderr.write(
`Fatal error: Failed to relaunch the CLI process.\n${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`,
);
process.exit(1);
}
}
} else {
writeToStderr(String(error) + '\n');
// --- Heavy Child Process ---
// Now we can safely import everything.
const { main } = await import('./src/gemini.js');
const { FatalError, writeToStderr } = await import(
'@google/gemini-cli-core'
);
const { runExitCleanup } = await import('./src/utils/cleanup.js');

main().catch(async (error: unknown) => {
// Set a timeout to force exit if cleanup hangs
const cleanupTimeout = setTimeout(() => {
writeToStderr('Cleanup timed out, forcing exit...\n');
process.exit(1);
}, 5000);

try {
await runExitCleanup();
} catch (cleanupError: unknown) {
writeToStderr(
`Error during final cleanup: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}\n`,
);
} finally {
clearTimeout(cleanupTimeout);
}

if (error instanceof FatalError) {
let errorMessage = error.message;
if (!process.env['NO_COLOR']) {
errorMessage = `\x1b[31m${errorMessage}\x1b[0m`;
}
writeToStderr(errorMessage + '\n');
process.exit(error.exitCode);
}

writeToStderr('An unexpected critical error occurred:');
if (error instanceof Error) {
writeToStderr(error.stack + '\n');
} else {
writeToStderr(String(error) + '\n');
}
process.exit(1);
});
}
process.exit(1);
});
}

run();
15 changes: 7 additions & 8 deletions packages/cli/src/gemini.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,7 @@ import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js';
import { appEvents, AppEvent } from './utils/events.js';
import { SessionError, SessionSelector } from './utils/sessionUtils.js';

import {
relaunchAppInChildProcess,
relaunchOnExitCode,
} from './utils/relaunch.js';
import { relaunchOnExitCode } from './utils/relaunch.js';
import { loadSandboxConfig } from './config/sandboxConfig.js';
import { deleteSession, listSessions } from './utils/sessions.js';
import { createPolicyUpdater } from './config/policy.js';
Expand Down Expand Up @@ -382,6 +379,12 @@ export async function main() {
// Set remote admin settings if returned from CCPA.
if (remoteAdminSettings) {
settings.setRemoteAdminSettings(remoteAdminSettings);
if (process.send) {
process.send({
type: 'admin-settings-update',
settings: remoteAdminSettings,
});
}
}

// Run deferred command now that we have admin settings.
Expand Down Expand Up @@ -439,10 +442,6 @@ export async function main() {
);
await runExitCleanup();
process.exit(ExitCodes.SUCCESS);
} else {
// Relaunch app so we always have a child process that can be internally
// restarted if needed.
await relaunchAppInChildProcess(memoryArgs, [], remoteAdminSettings);
}
}

Expand Down
Loading