Skip to content
Closed
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
142 changes: 92 additions & 50 deletions packages/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,66 +6,108 @@
* 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 { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

// --- Global Entry Point ---
const VERSION_FLAGS = new Set(['-v', '--version']);

// Suppress known race condition error in node-pty on Windows
// Tracking bug: https://github.com/microsoft/node-pty/issues/827
process.on('uncaughtException', (error) => {
if (
process.platform === 'win32' &&
error instanceof Error &&
error.message === 'Cannot resize a pty that has already exited'
) {
// This error happens on Windows with node-pty when resizing a pty that has just exited.
// It is a race condition in node-pty that we cannot prevent, so we silence it.
return;
}

// 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');
} else {
writeToStderr(String(error) + '\n');
}
process.exit(1);
});
function isVersionOnlyRequest(args: string[]): boolean {
return args.length === 1 && VERSION_FLAGS.has(args[0] ?? '');
}

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);
function getCliVersionFromPackageJson(): string {
const __dirname = dirname(fileURLToPath(import.meta.url));
const packageJsonPath = join(__dirname, 'package.json');
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as {
version?: string;
};
return packageJson.version ?? 'unknown';
}

if (isVersionOnlyRequest(process.argv.slice(2))) {
try {
await runExitCleanup();
} catch (cleanupError) {
writeToStderr(
`Error during final cleanup: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}\n`,
process.stdout.write(`${getCliVersionFromPackageJson()}\n`);
process.exit(0);
} catch (error) {
process.stderr.write(
`Failed to read CLI version: ${error instanceof Error ? error.message : String(error)}\n`,
);
} finally {
clearTimeout(cleanupTimeout);
process.exit(1);
}
}

async function run() {
const [{ main }, coreModule, { runExitCleanup }] = await Promise.all([
import('./src/gemini.js'),
import('@google/gemini-cli-core'),
import('./src/utils/cleanup.js'),
]);
const { FatalError, writeToStderr } = coreModule;

if (error instanceof FatalError) {
let errorMessage = error.message;
if (!process.env['NO_COLOR']) {
errorMessage = `\x1b[31m${errorMessage}\x1b[0m`;
// Suppress known race condition error in node-pty on Windows
// Tracking bug: https://github.com/microsoft/node-pty/issues/827
process.on('uncaughtException', (error) => {
if (
process.platform === 'win32' &&
error instanceof Error &&
error.message === 'Cannot resize a pty that has already exited'
) {
// This error happens on Windows with node-pty when resizing a pty that has just exited.
// It is a race condition in node-pty that we cannot prevent, so we silence it.
return;
}
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');
}
// 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');
} else {
writeToStderr(String(error) + '\n');
}
process.exit(1);
});

await 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);
}

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);
});
}

void run().catch((error) => {
process.stderr.write(
`Failed to start Gemini CLI: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`,
);
process.exit(1);
});
2 changes: 1 addition & 1 deletion packages/cli/src/commands/extensions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ vi.mock('./extensions/validate.js', () => ({
}));

// Mock gemini.js
vi.mock('../gemini.js', () => ({
vi.mock('../utils/outputListeners.js', () => ({
initializeOutputListenersAndFlush: vi.fn(),
}));

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/extensions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import { linkCommand } from './extensions/link.js';
import { newCommand } from './extensions/new.js';
import { validateCommand } from './extensions/validate.js';
import { configureCommand } from './extensions/configure.js';
import { initializeOutputListenersAndFlush } from '../gemini.js';
import { defer } from '../deferred.js';
import { initializeOutputListenersAndFlush } from '../utils/outputListeners.js';

export const extensionsCommand: CommandModule = {
command: 'extensions <command>',
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import type { CommandModule } from 'yargs';
import { migrateCommand } from './hooks/migrate.js';
import { initializeOutputListenersAndFlush } from '../gemini.js';
import { initializeOutputListenersAndFlush } from '../utils/outputListeners.js';

export const hooksCommand: CommandModule = {
command: 'hooks <command>',
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import { addCommand } from './mcp/add.js';
import { removeCommand } from './mcp/remove.js';
import { listCommand } from './mcp/list.js';
import { enableCommand, disableCommand } from './mcp/enableDisable.js';
import { initializeOutputListenersAndFlush } from '../gemini.js';
import { defer } from '../deferred.js';
import { initializeOutputListenersAndFlush } from '../utils/outputListeners.js';

export const mcpCommand: CommandModule = {
command: 'mcp',
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/skills.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ vi.mock('./skills/disable.js', () => ({
disableCommand: { command: 'disable <name>' },
}));

vi.mock('../gemini.js', () => ({
vi.mock('../utils/outputListeners.js', () => ({
initializeOutputListenersAndFlush: vi.fn(),
}));

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/skills.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import { disableCommand } from './skills/disable.js';
import { installCommand } from './skills/install.js';
import { linkCommand } from './skills/link.js';
import { uninstallCommand } from './skills/uninstall.js';
import { initializeOutputListenersAndFlush } from '../gemini.js';
import { defer } from '../deferred.js';
import { initializeOutputListenersAndFlush } from '../utils/outputListeners.js';

export const skillsCommand: CommandModule = {
command: 'skills <command>',
Expand Down
Loading