diff --git a/lib/auth/browser.ts b/lib/auth/browser.ts index 1024c28..651bc22 100644 --- a/lib/auth/browser.ts +++ b/lib/auth/browser.ts @@ -3,7 +3,7 @@ * Handles platform-specific browser opening */ -import { spawn } from "node:child_process"; +import { spawn, spawnSync } from "node:child_process"; import { PLATFORM_OPENERS } from "../constants.js"; /** @@ -17,6 +17,24 @@ export function getBrowserOpener(): string { return PLATFORM_OPENERS.linux; } +/** + * Checks if a command exists in PATH + * @param command - Command name to check + * @returns true if command exists, false otherwise + */ +function commandExists(command: string): boolean { + try { + const result = spawnSync( + process.platform === "win32" ? "where" : "which", + [command], + { stdio: "ignore" }, + ); + return result.status === 0; + } catch { + return false; + } +} + /** * Opens a URL in the default browser * Silently fails if browser cannot be opened (user can copy URL manually) @@ -25,6 +43,13 @@ export function getBrowserOpener(): string { export function openBrowserUrl(url: string): void { try { const opener = getBrowserOpener(); + + // Check if the opener command exists before attempting to spawn + // This prevents crashes in headless environments (Docker, WSL, CI, etc.) + if (!commandExists(opener)) { + return; + } + spawn(opener, [url], { stdio: "ignore", shell: process.platform === "win32", diff --git a/test/browser.test.ts b/test/browser.test.ts index 6fceab3..a0afb22 100644 --- a/test/browser.test.ts +++ b/test/browser.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { getBrowserOpener } from '../lib/auth/browser.js'; +import { getBrowserOpener, openBrowserUrl } from '../lib/auth/browser.js'; import { PLATFORM_OPENERS } from '../lib/constants.js'; describe('Browser Module', () => { @@ -32,4 +32,29 @@ describe('Browser Module', () => { Object.defineProperty(process, 'platform', { value: originalPlatform }); }); }); + + describe('openBrowserUrl', () => { + it('should not throw when browser opener command does not exist', () => { + // Temporarily set platform to use a non-existent command + const originalPlatform = process.platform; + const originalPath = process.env.PATH; + + // Clear PATH to ensure no opener command is found + process.env.PATH = ''; + Object.defineProperty(process, 'platform', { value: 'linux' }); + + // Should not throw even when xdg-open doesn't exist + expect(() => openBrowserUrl('https://example.com')).not.toThrow(); + + // Restore + process.env.PATH = originalPath; + Object.defineProperty(process, 'platform', { value: originalPlatform }); + }); + + it('should handle valid URL without throwing', () => { + // This test verifies the function doesn't throw for valid input + // The actual browser opening is not tested as it would open a real browser + expect(() => openBrowserUrl('https://example.com')).not.toThrow(); + }); + }); });