fix(cli): pass node arguments via NODE_OPTIONS during relaunch to support SEA (#26130)

This commit is contained in:
Coco Sheng
2026-04-28 17:15:23 -04:00
committed by GitHub
parent 8cfebb9e31
commit 12a77da45c
6 changed files with 386 additions and 123 deletions
+98
View File
@@ -29,3 +29,101 @@ export async function relaunchApp(): Promise<void> {
await runExitCleanup();
process.exit(RELAUNCH_EXIT_CODE);
}
export interface ProcessWithSea extends NodeJS.Process {
isSea?: () => boolean;
}
/**
* Determines whether the current process is a "standard" SEA (Single Executable Application)
* where the user arguments start at index 1 instead of index 2.
* A relaunched SEA child will have process.argv[0] === process.argv[1] (because we inject execPath),
* so it will return false here and correctly slice from index 2.
*/
export function isStandardSea(): boolean {
return (
process.argv[0] !== process.argv[1] &&
(process.env['IS_BINARY'] === 'true' ||
(process as ProcessWithSea).isSea?.() === true)
);
}
/**
* Extracts the user-provided script arguments from process.argv,
* accounting for the differences in SEA execution modes.
*/
export function getScriptArgs(): string[] {
return process.argv.slice(isStandardSea() ? 1 : 2);
}
/**
* Determines if the current process is running in any SEA environment
* (either the initial launch or a relaunched child).
*/
export function isSeaEnvironment(): boolean {
return (
process.env['IS_BINARY'] === 'true' ||
(process as ProcessWithSea).isSea?.() === true ||
process.argv[0] === process.argv[1]
);
}
/**
* Constructs the arguments and environment for spawning a child process during relaunch.
* Handles differences between standard Node and SEA binary modes.
*/
export function getSpawnConfig(
nodeArgs: string[],
scriptArgs: string[],
): {
spawnArgs: string[];
env: NodeJS.ProcessEnv;
} {
const isBinary = isSeaEnvironment();
const newEnv: NodeJS.ProcessEnv = {
...process.env,
GEMINI_CLI_NO_RELAUNCH: 'true',
};
const finalSpawnArgs: string[] = [];
if (isBinary) {
// In SEA mode, Node flags must be passed via NODE_OPTIONS, as the binary
// passes all CLI arguments directly to the application.
// We only need to append the *new* nodeArgs (e.g., memory flags).
// Existing execArgv are inherited via the environment or baked into the binary.
if (nodeArgs.length > 0) {
for (const arg of nodeArgs) {
if (/[\s"'\\]/.test(arg)) {
throw new Error(
`Unsupported node argument for SEA relaunch: ${arg}. Complex escaping is not supported.`,
);
}
}
const existingNodeOptions = process.env['NODE_OPTIONS'] || '';
// nodeArgs in our codebase are simple flags like --max-old-space-size=X
// that do not contain spaces and do not require complex escaping.
newEnv['NODE_OPTIONS'] =
`${existingNodeOptions} ${nodeArgs.join(' ')}`.trim();
}
// Binary is its own entry point. To maintain the [node, script, ...args]
// structure expected by the application (which uses slice(2)),
// we must provide a placeholder for the script path.
// We explicitly use process.execPath to break the cycle and prevent
// compounding argument duplication on subsequent relaunches.
finalSpawnArgs.push(process.execPath, ...scriptArgs);
} else {
// Standard Node mode: pass all flags via command line.
finalSpawnArgs.push(
...process.execArgv,
...nodeArgs,
process.argv[1],
...scriptArgs,
);
}
return {
spawnArgs: finalSpawnArgs,
env: newEnv,
};
}