From ea9486ae2dccce046ad03ff37d46aaec7fdbcc87 Mon Sep 17 00:00:00 2001 From: Roshan Singh Date: Mon, 12 Jan 2026 17:40:26 +0000 Subject: [PATCH] Fix: quote URLs when opening browser on Windows --- src/commands/onboard-helpers.test.ts | 43 ++++++++++++++++++++++++++++ src/commands/onboard-helpers.ts | 15 ++++++++-- 2 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 src/commands/onboard-helpers.test.ts diff --git a/src/commands/onboard-helpers.test.ts b/src/commands/onboard-helpers.test.ts new file mode 100644 index 000000000..5d058f51d --- /dev/null +++ b/src/commands/onboard-helpers.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it, vi } from "vitest"; + +import { openUrl, resolveBrowserOpenCommand } from "./onboard-helpers.js"; + +const mocks = vi.hoisted(() => ({ + runCommandWithTimeout: vi.fn(async () => ({ + stdout: "", + stderr: "", + code: 0, + signal: null, + killed: false, + })), +})); + +vi.mock("../process/exec.js", () => ({ + runCommandWithTimeout: mocks.runCommandWithTimeout, +})); + +describe("openUrl", () => { + it("quotes URLs on win32 so '&' is not treated as cmd separator", async () => { + vi.spyOn(process, "platform", "get").mockReturnValue("win32"); + + const url = + "https://accounts.google.com/o/oauth2/v2/auth?client_id=abc&response_type=code&redirect_uri=http%3A%2F%2Flocalhost"; + + const ok = await openUrl(url); + expect(ok).toBe(true); + + expect(mocks.runCommandWithTimeout).toHaveBeenCalledTimes(1); + const argv = mocks.runCommandWithTimeout.mock.calls[0]?.[0]; + expect(argv?.slice(0, 4)).toEqual(["cmd", "/c", "start", ""]); + expect(argv?.at(-1)).toBe(`"${url}"`); + }); +}); + +describe("resolveBrowserOpenCommand", () => { + it("marks win32 commands as quoteUrl=true", async () => { + vi.spyOn(process, "platform", "get").mockReturnValue("win32"); + const resolved = await resolveBrowserOpenCommand(); + expect(resolved.argv).toEqual(["cmd", "/c", "start", ""]); + expect(resolved.quoteUrl).toBe(true); + }); +}); diff --git a/src/commands/onboard-helpers.ts b/src/commands/onboard-helpers.ts index 3676f4dea..37f85e1f5 100644 --- a/src/commands/onboard-helpers.ts +++ b/src/commands/onboard-helpers.ts @@ -134,9 +134,14 @@ type BrowserOpenCommand = { argv: string[] | null; reason?: string; command?: string; + /** + * Whether the URL must be wrapped in quotes when appended to argv. + * Needed for Windows `cmd /c start` where `&` splits commands. + */ + quoteUrl?: boolean; }; -async function resolveBrowserOpenCommand(): Promise { +export async function resolveBrowserOpenCommand(): Promise { const platform = process.platform; const hasDisplay = Boolean( process.env.DISPLAY || process.env.WAYLAND_DISPLAY, @@ -151,7 +156,11 @@ async function resolveBrowserOpenCommand(): Promise { } if (platform === "win32") { - return { argv: ["cmd", "/c", "start", ""], command: "cmd" }; + return { + argv: ["cmd", "/c", "start", ""], + command: "cmd", + quoteUrl: true, + }; } if (platform === "darwin") { @@ -223,7 +232,7 @@ function resolveSshTargetHint(): string { export async function openUrl(url: string): Promise { const resolved = await resolveBrowserOpenCommand(); if (!resolved.argv) return false; - const command = [...resolved.argv, url]; + const command = [...resolved.argv, resolved.quoteUrl ? `"${url}"` : url]; try { await runCommandWithTimeout(command, { timeoutMs: 5_000 }); return true;