Unified · Split
| @@ -0,0 +1,34 @@ | ||
| 1 | +import { describe, expect, it } from "@effect/vitest"; | |
| 2 | + | |
| 3 | +import { browserOpenCommand } from "./device-login"; | |
| 4 | + | |
| 5 | +describe("browserOpenCommand", () => { | |
| 6 | + it("opens Windows browser URLs without cmd.exe", () => { | |
| 7 | + const command = browserOpenCommand( | |
| 8 | + "https://executor.example/login?next=a%20b&token=abc123", | |
| 9 | + "win32", | |
| 10 | + ); | |
| 11 | + | |
| 12 | + expect(command).toEqual([ | |
| 13 | + "rundll32.exe", | |
| 14 | + ["url.dll,FileProtocolHandler", "https://executor.example/login?next=a%20b&token=abc123"], | |
| 15 | + ]); | |
| 16 | + }); | |
| 17 | + | |
| 18 | + it("passes the browser URL as one argument on every platform", () => { | |
| 19 | + expect(browserOpenCommand("https://executor.example/login?x=1&y=2", "darwin")).toEqual([ | |
| 20 | + "open", | |
| 21 | + ["https://executor.example/login?x=1&y=2"], | |
| 22 | + ]); | |
| 23 | + expect(browserOpenCommand("https://executor.example/login?x=1&y=2", "linux")).toEqual([ | |
| 24 | + "xdg-open", | |
| 25 | + ["https://executor.example/login?x=1&y=2"], | |
| 26 | + ]); | |
| 27 | + }); | |
| 28 | + | |
| 29 | + it("refuses non-browser URL schemes", () => { | |
| 30 | + expect(browserOpenCommand("javascript:alert(1)", "win32")).toBeUndefined(); | |
| 31 | + expect(browserOpenCommand("file:///C:/Windows/System32/calc.exe", "win32")).toBeUndefined(); | |
| 32 | + expect(browserOpenCommand("not a url", "win32")).toBeUndefined(); | |
| 33 | + }); | |
| 34 | +}); | |
| @@ -311,11 +311,33 @@ export const refreshDeviceTokens = async (input: { · 310 unmodified lines | ||
| 311 | 311 | }; |
| 312 | 312 | }; |
| 313 | 313 | |
| 314 | +export type BrowserOpenCommand = readonly [command: string, args: ReadonlyArray<string>]; | |
| 315 | + | |
| 316 | +const normalizeBrowserOpenUrl = (url: string): string | undefined => { | |
| 317 | + try { | |
| 318 | + const parsed = new URL(url); | |
| 319 | + return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.href : undefined; | |
| 320 | + } catch { | |
| 321 | + return undefined; | |
| 322 | + } | |
| 323 | +}; | |
| 324 | + | |
| 325 | +export const browserOpenCommand = ( | |
| 326 | + url: string, | |
| 327 | + platform: typeof process.platform = process.platform, | |
| 328 | +): BrowserOpenCommand | undefined => { | |
| 329 | + const normalizedUrl = normalizeBrowserOpenUrl(url); | |
| 330 | + if (!normalizedUrl) return undefined; | |
| 331 | + if (platform === "darwin") return ["open", [normalizedUrl]]; | |
| 332 | + if (platform === "win32") return ["rundll32.exe", ["url.dll,FileProtocolHandler", normalizedUrl]]; | |
| 333 | + return ["xdg-open", [normalizedUrl]]; | |
| 334 | +}; | |
| 335 | + | |
| 314 | 336 | /** Best-effort open the verification URL in the user's default browser. */ |
| 315 | 337 | export const openBrowser = (url: string): void => { |
| 316 | − const platform = process.platform; | |
| 317 | − const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open"; | |
| 318 | − const args = platform === "win32" ? ["/c", "start", "", url] : [url]; | |
| 338 | + const openCommand = browserOpenCommand(url); | |
| 339 | + if (!openCommand) return; | |
| 340 | + const [command, args] = openCommand; | |
| 319 | 341 | try { |
| 320 | 342 | const child = spawn(command, args, { stdio: "ignore", detached: true }); |
| 321 | 343 | // Swallow async spawn failures (e.g. the opener binary is missing), the |
| @@ -2,7 +2,6 @@ · 1 unmodified line | ||
| 2 | 2 | // before any import (e.g. `@executor-js/local` → libSQL) eagerly loads them. |
| 3 | 3 | import "./native-bindings"; |
| 4 | 4 | |
| 5 | −import { execFile } from "node:child_process"; | |
| 6 | 5 | import { randomUUID } from "node:crypto"; |
| 7 | 6 | import { existsSync, realpathSync } from "node:fs"; |
| 8 | 7 | import { dirname, join, resolve } from "node:path"; |
| @@ -2821,16 +2820,7 @@ const installCommand = Command.make( · 2812 unmodified lines | ||
| 2821 | 2820 | */ |
| 2822 | 2821 | const openInBrowser = (url: string): Effect.Effect<void> => |
| 2823 | 2822 | Effect.sync(() => { |
| 2824 | − const [cmd, args]: readonly [string, ReadonlyArray<string>] = | |
| 2825 | − process.platform === "darwin" | |
| 2826 | − ? ["open", [url]] | |
| 2827 | − : process.platform === "win32" | |
| 2828 | − ? ["cmd", ["/c", "start", "", url]] | |
| 2829 | − : ["xdg-open", [url]]; | |
| 2830 | − // best-effort: ignore failures (e.g. no opener on headless Linux). The URL | |
| 2831 | − // was printed above for manual open; execFile's callback absorbs the error, | |
| 2832 | − // so there's no unhandled 'error' event to crash the CLI. | |
| 2833 | − execFile(cmd, [...args], () => {}); | |
| 2823 | + openBrowser(url); | |
| 2834 | 2824 | }); |
| 2835 | 2825 | |
| 2836 | 2826 | const printNoRunningLocalWebApp = (): void => { |