From de4ad981ae03bc0ad4565704f1b89791fc0bf509 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 31 Aug 2026 02:14:11 -0700 Subject: [PATCH] fix(linux): respect CLI flag value boundaries --- src/main/startup/cli-launch-redirect.test.ts | 25 +++++++++ src/main/startup/cli-launch-redirect.ts | 58 +++++++++++++++++--- src/main/startup/serve-mode-argv.ts | 3 +- 3 files changed, 78 insertions(+), 8 deletions(-) diff --git a/src/main/startup/cli-launch-redirect.test.ts b/src/main/startup/cli-launch-redirect.test.ts index 4ac2394c455..975e15a5d7c 100644 --- a/src/main/startup/cli-launch-redirect.test.ts +++ b/src/main/startup/cli-launch-redirect.test.ts @@ -126,6 +126,31 @@ describe('CLI launch redirect: command form', () => { ]) }) + it.each(['--user-data-dir', '--proxy-server', '--unknown-desktop-switch'])( + 'does not treat a value of %s as a CLI early-exit flag', + (flag) => { + expect( + getCliLaunchArgs([linux.execPath, flag, 'help'], linux.cliEntryPath, linuxOptions) + ).toBeNull() + } + ) + + it('does not treat a serve option value as a help request', () => { + expect( + getCliLaunchArgs( + [linux.execPath, 'serve', '--project-root', 'help'], + linux.cliEntryPath, + linuxOptions + ) + ).toBeNull() + }) + + it('does not reinterpret help after the argument terminator', () => { + expect( + getCliLaunchArgs([linux.execPath, 'serve', '--', '--help'], linux.cliEntryPath, linuxOptions) + ).toBeNull() + }) + it('leaves a plain desktop launch alone', () => { expect(getCliLaunchArgs([linux.execPath], linux.cliEntryPath, linuxOptions)).toBeNull() expect( diff --git a/src/main/startup/cli-launch-redirect.ts b/src/main/startup/cli-launch-redirect.ts index c033bfa1f59..e9b3be31348 100644 --- a/src/main/startup/cli-launch-redirect.ts +++ b/src/main/startup/cli-launch-redirect.ts @@ -1,8 +1,9 @@ import { existsSync } from 'node:fs' import { posix, win32 } from 'node:path' import { runProcessSync } from '../../shared/child-process/run-process' -import { CLI_GLOBAL_VALUE_FLAGS, findCliCommandIndex } from '../../shared/cli-argument-boundary' +import { CLI_BOOLEAN_FLAGS, findCliCommandIndex } from '../../shared/cli-argument-boundary' import { CLI_COMMAND_NAMES } from './cli-command-names' +import { VALUE_TAKING_FLAGS } from './serve-mode-argv' export type CliLaunchRedirectResult = { redirected: false } | { redirected: true; status: number } @@ -20,6 +21,7 @@ export type CliLaunchRedirectOptions = { const CLI_EARLY_EXIT_FLAGS = new Set(['--help', '-h', 'help', '--version', '-v']) const DESKTOP_FLAGS = new Set(['--no-sandbox', '--disable-gpu']) +const CLI_LAUNCH_VALUE_FLAG_NAMES = [...VALUE_TAKING_FLAGS].map((flag) => flag.slice(2)) // Fence recursion if a wrapper drops ELECTRON_RUN_AS_NODE again. const REDIRECT_ATTEMPT_ENV = 'ORCA_CLI_LAUNCH_REDIRECTED' @@ -114,17 +116,59 @@ function getCommandLaunchArgs( return null } const commandPaths = options.commandNames.map((name) => [name]) - const commandIndex = findCliCommandIndex(args, commandPaths, CLI_GLOBAL_VALUE_FLAGS) + const commandIndex = findCliCommandIndex(args, commandPaths, CLI_LAUNCH_VALUE_FLAG_NAMES) const cliArgs = args.filter( (arg, index) => (commandIndex !== -1 && index > commandIndex) || !DESKTOP_FLAGS.has(arg) ) - if (cliArgs.some((arg) => CLI_EARLY_EXIT_FLAGS.has(arg))) { - return cliArgs - } - const command = commandIndex === -1 ? null : args[commandIndex] // Keep direct serve in-process so signals reach its full child tree. - return command && command !== 'serve' ? cliArgs : null + if (command && command !== 'serve') { + return cliArgs + } + return hasCliEarlyExitArg(args, commandIndex) ? cliArgs : null +} + +function hasCliEarlyExitArg(args: readonly string[], commandIndex: number): boolean { + let index = 0 + let positionalCount = 0 + while (index < args.length) { + const token = args[index]! + if (token === '--') { + return false + } + if ( + CLI_EARLY_EXIT_FLAGS.has(token) && + (token !== 'help' || positionalCount === 0 || commandIndex !== -1) + ) { + return true + } + if (!token.startsWith('-')) { + if (token === 'help' && (positionalCount === 0 || commandIndex !== -1)) { + return true + } + positionalCount += 1 + } + index += 1 + if (takesLaunchValue(token, args[index])) { + index += 1 + } + } + return false +} + +function takesLaunchValue(token: string, next: string | undefined): boolean { + if ( + !next || + next.startsWith('-') || + !token.startsWith('-') || + token.includes('=') || + CLI_EARLY_EXIT_FLAGS.has(token) || + DESKTOP_FLAGS.has(token) + ) { + return false + } + const flagName = token.startsWith('--') ? token.slice(2) : token.replace(/^-+/, '') + return !CLI_BOOLEAN_FLAGS.has(flagName) } function buildPackagedCliEntryPath(platform: NodeJS.Platform, resourcesPath: string): string { diff --git a/src/main/startup/serve-mode-argv.ts b/src/main/startup/serve-mode-argv.ts index 3ba25fd8d85..70159e35057 100644 --- a/src/main/startup/serve-mode-argv.ts +++ b/src/main/startup/serve-mode-argv.ts @@ -26,12 +26,13 @@ const CLI_TO_SERVE_VALUE_FLAG = new Map([ * Residual class: a flag outside this list whose space-separated value is literally `serve` would * read as the subcommand. Chromium switches are `--flag=value` only, so no real launch does that. */ -const VALUE_TAKING_FLAGS = new Set([ +export const VALUE_TAKING_FLAGS = new Set([ ...CLI_TO_SERVE_VALUE_FLAG.keys(), '--serve-port', '--serve-pairing-address', '--serve-project-root', '--user-data-dir', + '--proxy-server', '--environment', '--pairing-code' ])