Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion lib/auth/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand All @@ -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)
Expand All @@ -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",
Expand Down
27 changes: 26 additions & 1 deletion test/browser.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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();
});
});
});