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 | /** Best-effort open the verification URL in the user's default browser. */ | 336 | /** Best-effort open the verification URL in the user's default browser. */ |
| 315 | export const openBrowser = (url: string): void => { | 337 | export const openBrowser = (url: string): void => { |
| 316 | const platform = process.platform; | 338 | const openCommand = browserOpenCommand(url); |
| 317 | const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open"; | 339 | if (!openCommand) return; |
| 318 | const args = platform === "win32" ? ["/c", "start", "", url] : [url]; | 340 | const [command, args] = openCommand; |
| 319 | try { | 341 | try { |
| 320 | const child = spawn(command, args, { stdio: "ignore", detached: true }); | 342 | const child = spawn(command, args, { stdio: "ignore", detached: true }); |
| 321 | // Swallow async spawn failures (e.g. the opener binary is missing), the | 343 | // Swallow async spawn failures (e.g. the opener binary is missing), the |
| @@ -2,7 +2,6 @@ · 1 unmodified line | |||
| 2 | // before any import (e.g. `@executor-js/local` → libSQL) eagerly loads them. | 2 | // before any import (e.g. `@executor-js/local` → libSQL) eagerly loads them. |
| 3 | import "./native-bindings"; | 3 | import "./native-bindings"; |
| 4 | 4 | ||
| 5 | import { execFile } from "node:child_process"; | ||
| 6 | import { randomUUID } from "node:crypto"; | 5 | import { randomUUID } from "node:crypto"; |
| 7 | import { existsSync, realpathSync } from "node:fs"; | 6 | import { existsSync, realpathSync } from "node:fs"; |
| 8 | import { dirname, join, resolve } from "node:path"; | 7 | import { dirname, join, resolve } from "node:path"; |
| @@ -2821,16 +2820,7 @@ const installCommand = Command.make( · 2812 unmodified lines | |||
| 2821 | */ | 2820 | */ |
| 2822 | const openInBrowser = (url: string): Effect.Effect<void> => | 2821 | const openInBrowser = (url: string): Effect.Effect<void> => |
| 2823 | Effect.sync(() => { | 2822 | Effect.sync(() => { |
| 2824 | const [cmd, args]: readonly [string, ReadonlyArray<string>] = | 2823 | openBrowser(url); |
| 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], () => {}); | ||
| 2834 | }); | 2824 | }); |
| 2835 | 2825 | ||
| 2836 | const printNoRunningLocalWebApp = (): void => { | 2826 | const printNoRunningLocalWebApp = (): void => { |