diff --git a/packages/cli/index.ts b/packages/cli/index.ts index d857831fb7..b1af282ce4 100644 --- a/packages/cli/index.ts +++ b/packages/cli/index.ts @@ -9,6 +9,7 @@ import { spawn } from 'node:child_process'; import os from 'node:os'; import v8 from 'node:v8'; +import { buildRelaunchSpawnSpec } from './src/utils/relaunchSpawnSpec.js'; // --- Global Entry Point --- @@ -74,17 +75,14 @@ async function run() { // --- Lightweight Parent Process / Daemon --- // We avoid importing heavy dependencies here to save ~1.5s of startup time. - const nodeArgs: string[] = [...process.execArgv]; - const scriptArgs = process.argv.slice(2); - const memoryArgs = await getMemoryNodeArgs(); - nodeArgs.push(...memoryArgs); - - const script = process.argv[1]; - nodeArgs.push(script); - nodeArgs.push(...scriptArgs); - - const newEnv = { ...process.env, GEMINI_CLI_NO_RELAUNCH: 'true' }; + const { args: relaunchArgs, env: newEnv } = buildRelaunchSpawnSpec({ + additionalNodeArgs: memoryArgs, + additionalScriptArgs: [], + argv: process.argv, + env: process.env, + execArgv: process.execArgv, + }); const RELAUNCH_EXIT_CODE = 199; let latestAdminSettings: unknown = undefined; @@ -97,7 +95,7 @@ async function run() { const runner = () => { process.stdin.pause(); - const child = spawn(process.execPath, nodeArgs, { + const child = spawn(process.execPath, relaunchArgs, { stdio: ['inherit', 'inherit', 'inherit', 'ipc'], env: newEnv, }); diff --git a/packages/cli/src/utils/relaunch.ts b/packages/cli/src/utils/relaunch.ts index 7e287e4565..77b8dadda9 100644 --- a/packages/cli/src/utils/relaunch.ts +++ b/packages/cli/src/utils/relaunch.ts @@ -10,6 +10,7 @@ import { writeToStderr, type AdminControlsSettings, } from '@google/gemini-cli-core'; +import { buildRelaunchSpawnSpec } from './relaunchSpawnSpec.js'; export async function relaunchOnExitCode(runner: () => Promise) { while (true) { @@ -43,19 +44,13 @@ export async function relaunchAppInChildProcess( let latestAdminSettings = remoteAdminSettings; const runner = () => { - // process.argv is [node, script, ...args] - // We want to construct [ ...nodeArgs, script, ...scriptArgs] - const script = process.argv[1]; - const scriptArgs = process.argv.slice(2); - - const nodeArgs = [ - ...process.execArgv, - ...additionalNodeArgs, - script, - ...additionalScriptArgs, - ...scriptArgs, - ]; - const newEnv = { ...process.env, GEMINI_CLI_NO_RELAUNCH: 'true' }; + const { args: nodeArgs, env: newEnv } = buildRelaunchSpawnSpec({ + additionalNodeArgs, + additionalScriptArgs, + argv: process.argv, + env: process.env, + execArgv: process.execArgv, + }); // The parent process should not be reading from stdin while the child is running. process.stdin.pause(); diff --git a/packages/cli/src/utils/relaunchSpawnSpec.test.ts b/packages/cli/src/utils/relaunchSpawnSpec.test.ts new file mode 100644 index 0000000000..6b30aed286 --- /dev/null +++ b/packages/cli/src/utils/relaunchSpawnSpec.test.ts @@ -0,0 +1,57 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, expect, it } from 'vitest'; +import { buildRelaunchSpawnSpec } from './relaunchSpawnSpec.js'; + +describe('buildRelaunchSpawnSpec', () => { + it('preserves node-style relaunch arguments for source installs', () => { + const { args, env } = buildRelaunchSpawnSpec({ + additionalNodeArgs: ['--max-old-space-size=4096'], + additionalScriptArgs: ['--model', 'gemini-2.5-pro'], + argv: ['/usr/bin/node', '/app/cli.js', 'command', '--verbose'], + env: { PATH: '/usr/bin' }, + execArgv: ['--inspect=9229'], + }); + + expect(args).toEqual([ + '--inspect=9229', + '--max-old-space-size=4096', + '/app/cli.js', + '--model', + 'gemini-2.5-pro', + 'command', + '--verbose', + ]); + expect(env).toMatchObject({ + GEMINI_CLI_NO_RELAUNCH: 'true', + PATH: '/usr/bin', + }); + expect(env['NODE_OPTIONS']).toBeUndefined(); + }); + + it('moves node flags into NODE_OPTIONS for standalone binaries', () => { + const { args, env } = buildRelaunchSpawnSpec({ + additionalNodeArgs: ['--max-old-space-size=4096'], + additionalScriptArgs: ['--model', 'gemini-2.5-pro'], + argv: ['/tmp/gemini', '/tmp/gemini', '--verbose'], + env: { + IS_BINARY: 'true', + NODE_OPTIONS: '--trace-warnings', + PATH: '/usr/bin', + }, + execArgv: ['--inspect=9229'], + }); + + expect(args).toEqual(['--model', 'gemini-2.5-pro', '--verbose']); + expect(env).toMatchObject({ + GEMINI_CLI_NO_RELAUNCH: 'true', + IS_BINARY: 'true', + NODE_OPTIONS: '--trace-warnings --max-old-space-size=4096', + PATH: '/usr/bin', + }); + }); +}); diff --git a/packages/cli/src/utils/relaunchSpawnSpec.ts b/packages/cli/src/utils/relaunchSpawnSpec.ts new file mode 100644 index 0000000000..3b6a23afbd --- /dev/null +++ b/packages/cli/src/utils/relaunchSpawnSpec.ts @@ -0,0 +1,58 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export interface BuildRelaunchSpawnSpecParams { + additionalNodeArgs: string[]; + additionalScriptArgs: string[]; + argv: string[]; + env: NodeJS.ProcessEnv; + execArgv: string[]; +} + +export interface RelaunchSpawnSpec { + args: string[]; + env: NodeJS.ProcessEnv; +} + +export function buildRelaunchSpawnSpec({ + additionalNodeArgs, + additionalScriptArgs, + argv, + env, + execArgv, +}: BuildRelaunchSpawnSpecParams): RelaunchSpawnSpec { + const scriptArgs = argv.slice(2); + const newEnv: NodeJS.ProcessEnv = { + ...env, + GEMINI_CLI_NO_RELAUNCH: 'true', + }; + + if (env['IS_BINARY'] === 'true') { + if (additionalNodeArgs.length > 0) { + newEnv['NODE_OPTIONS'] = [newEnv['NODE_OPTIONS'], ...additionalNodeArgs] + .filter(Boolean) + .join(' '); + } + + return { + args: [...additionalScriptArgs, ...scriptArgs], + env: newEnv, + }; + } + + const script = argv[1]; + + return { + args: [ + ...execArgv, + ...additionalNodeArgs, + script, + ...additionalScriptArgs, + ...scriptArgs, + ], + env: newEnv, + }; +}