Compare commits

...

2 Commits

Author SHA1 Message Date
Jarred Sumner
38c04dec3e Merge branch 'main' into claude/fix-windows-open-url-26231 2026-01-19 11:58:26 -08:00
Claude Bot
f16b4775db fix(windows): use cmd.exe to invoke start for opening URLs
On Windows, `start` is a cmd.exe built-in command, not an executable.
Attempting to spawn it directly causes "Executable not found" errors.

This fixes the "o + Enter" shortcut in the HTML dev server and other
places that use `openURL()` on Windows.

The fix invokes `cmd.exe /c start "" "<url>"` instead of `start <url>`.
The empty string argument is the window title. The URL is quoted to
prevent cmd.exe metacharacter injection (e.g., & | < > ^ in URLs).

Fixes #26231

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 03:55:55 +00:00
2 changed files with 26 additions and 4 deletions

View File

@@ -383,7 +383,9 @@ yourself with Bun.serve().
// TODO: copy the AppleScript from create-react-app or Vite.
Bun.spawn(["open", url]).exited.catch(() => {});
} else if (process.platform === "win32") {
Bun.spawn(["start", url]).exited.catch(() => {});
// "start" is a cmd.exe built-in command, not an executable.
// The empty string argument is the window title (required for URLs with special characters).
Bun.spawn(["cmd.exe", "/c", "start", "", url]).exited.catch(() => {});
} else {
Bun.spawn(["xdg-open", url]).exited.catch(() => {});
}

View File

@@ -1,6 +1,6 @@
pub const opener = switch (@import("builtin").target.os.tag) {
.macos => "/usr/bin/open",
.windows => "start",
.windows => "cmd.exe",
else => "xdg-open",
};
@@ -12,11 +12,31 @@ fn fallback(url: string) void {
pub fn openURL(url: stringZ) void {
if (comptime Environment.isWasi) return fallback(url);
var args_buf = [_]stringZ{ opener, url };
// On Windows, "start" is a cmd.exe built-in command, not an executable.
// We must invoke it via: cmd.exe /c start "" "<url>"
// The empty string is the window title. The URL must be quoted to prevent
// cmd.exe metacharacter injection (e.g., & | < > ^ in URLs).
const quoted_url: [:0]const u8 = if (Environment.isWindows) blk: {
const unquoted: []const u8 = bun.sliceTo(url, 0);
// Allocate space for quotes + content + null terminator
const buf = default_allocator.allocSentinel(u8, unquoted.len + 2, 0) catch return fallback(url);
buf[0] = '"';
@memcpy(buf[1..][0..unquoted.len], unquoted);
buf[unquoted.len + 1] = '"';
break :blk buf;
} else undefined;
defer if (Environment.isWindows) default_allocator.free(@as([]const u8, quoted_url));
var args_buf = if (Environment.isWindows)
[_]stringZ{ opener, "/c", "start", "", quoted_url }
else
[_]stringZ{ opener, url, undefined, undefined, undefined };
const argv_len: usize = if (Environment.isWindows) 5 else 2;
maybe_fallback: {
switch (bun.spawnSync(&.{
.argv = &args_buf,
.argv = args_buf[0..argv_len],
.envp = null,