From 6e6896510a4b861c5cc0daee3aaa60eae9f44e4f Mon Sep 17 00:00:00 2001 From: robobun Date: Wed, 14 Jan 2026 13:10:53 -0800 Subject: [PATCH] fix(cli): prevent --version/--help interception in standalone executables with compile-exec-argv (#26083) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes https://github.com/oven-sh/bun/issues/26082 - Fixes a bug where standalone executables compiled with `--compile-exec-argv` would intercept `--version`, `-v`, `--help`, and `-h` flags before user code could handle them - CLI applications using libraries like `commander` can now properly implement their own version and help commands ## Root Cause When `--compile-exec-argv` is used, `Command.init` was being called with `.AutoCommand`, which parses ALL arguments (including user arguments). The `Arguments.parse` function intercepts `--version`/`--help` flags for `AutoCommand`, preventing them from reaching user code. ## Fix Temporarily set `bun.argv` to only include the executable name + embedded exec argv options when calling `Command.init`. This ensures: 1. Bun's embedded options (like `--smol`, `--use-system-ca`) are properly parsed 2. User arguments (including `--version`/`--help`) are NOT intercepted by Bun's parser 3. User arguments are properly passed through to user code ## Test plan - [x] Added tests for `--version`, `-v`, `--help`, and `-h` flags in `compile-argv.test.ts` - [x] Verified tests fail with `USE_SYSTEM_BUN=1` (proving the bug exists) - [x] Verified tests pass with debug build - [x] Verified existing compile-argv tests still pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Bot Co-authored-by: Claude Opus 4.5 --- src/cli.zig | 19 +++++- test/bundler/compile-argv.test.ts | 101 ++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 3 deletions(-) diff --git a/src/cli.zig b/src/cli.zig index e2a62330cc..5f6f7026cc 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -690,13 +690,26 @@ pub const Command = struct { const original_argv_len = bun.argv.len; var argv_list = std.array_list.Managed([:0]const u8).fromOwnedSlice(bun.default_allocator, bun.argv); try bun.appendOptionsEnv(graph.compile_exec_argv, &argv_list, bun.default_allocator); - bun.argv = argv_list.items; + + // Store the full argv including user arguments + const full_argv = argv_list.items; + const num_exec_argv_options = full_argv.len -| original_argv_len; // Calculate offset: skip executable name + all exec argv options - offset_for_passthrough = if (bun.argv.len > 1) 1 + (bun.argv.len -| original_argv_len) else 0; + offset_for_passthrough = if (full_argv.len > 1) 1 + num_exec_argv_options else 0; + + // Temporarily set bun.argv to only include executable name + exec_argv options. + // This prevents user arguments like --version/--help from being intercepted + // by Bun's argument parser (they should be passed through to user code). + bun.argv = full_argv[0..@min(1 + num_exec_argv_options, full_argv.len)]; // Handle actual options to parse. - break :brk try Command.init(allocator, log, .AutoCommand); + const result = try Command.init(allocator, log, .AutoCommand); + + // Restore full argv so passthrough calculation works correctly + bun.argv = full_argv; + + break :brk result; } context_data = .{ diff --git a/test/bundler/compile-argv.test.ts b/test/bundler/compile-argv.test.ts index 6d7ea8d20f..50b32fa5fb 100644 --- a/test/bundler/compile-argv.test.ts +++ b/test/bundler/compile-argv.test.ts @@ -175,4 +175,105 @@ describe("bundler", () => { stdout: /SUCCESS: user arguments properly passed with exec argv present/, }, }); + + // Test that --version and --help flags are passed through to user code (issue #26082) + // When compile-exec-argv is used, user flags like --version should NOT be intercepted by Bun + itBundled("compile/CompileExecArgvVersionHelpPassthrough", { + compile: { + execArgv: ["--smol"], + }, + backend: "cli", + files: { + "/entry.ts": /* js */ ` + // Test that --version and --help are passed through to user code, not intercepted by Bun + const args = process.argv.slice(2); + console.log("User args:", JSON.stringify(args)); + + if (args.includes("--version")) { + console.log("APP_VERSION:1.0.0"); + } else if (args.includes("-v")) { + console.log("APP_VERSION:1.0.0"); + } else if (args.includes("--help")) { + console.log("APP_HELP:This is my app help"); + } else if (args.includes("-h")) { + console.log("APP_HELP:This is my app help"); + } else { + console.log("NO_FLAG_MATCHED"); + } + `, + }, + run: { + args: ["--version"], + stdout: /APP_VERSION:1\.0\.0/, + }, + }); + + // Test with -v short flag + itBundled("compile/CompileExecArgvShortVersionPassthrough", { + compile: { + execArgv: ["--smol"], + }, + backend: "cli", + files: { + "/entry.ts": /* js */ ` + const args = process.argv.slice(2); + if (args.includes("-v")) { + console.log("APP_VERSION:1.0.0"); + } else { + console.log("FAIL: -v not found in args:", args); + process.exit(1); + } + `, + }, + run: { + args: ["-v"], + stdout: /APP_VERSION:1\.0\.0/, + }, + }); + + // Test with --help flag + itBundled("compile/CompileExecArgvHelpPassthrough", { + compile: { + execArgv: ["--smol"], + }, + backend: "cli", + files: { + "/entry.ts": /* js */ ` + const args = process.argv.slice(2); + if (args.includes("--help")) { + console.log("APP_HELP:my custom help"); + } else { + console.log("FAIL: --help not found in args:", args); + process.exit(1); + } + `, + }, + run: { + args: ["--help"], + stdout: /APP_HELP:my custom help/, + }, + }); + + // Test with -h short flag + itBundled("compile/CompileExecArgvShortHelpPassthrough", { + compile: { + execArgv: ["--smol"], + }, + backend: "cli", + files: { + "/entry.ts": /* js */ ` + const args = process.argv.slice(2); + if (args.includes("-h")) { + console.log("APP_HELP:my custom help"); + } else { + console.log("FAIL: -h not found in args:", args); + process.exit(1); + } + `, + }, + run: { + args: ["-h"], + stdout: /APP_HELP:my custom help/, + }, + }); });