/ utils / browser.ts
browser.ts
 1  import { execFileNoThrow } from './execFileNoThrow.js'
 2  
 3  function validateUrl(url: string): void {
 4    let parsedUrl: URL
 5  
 6    try {
 7      parsedUrl = new URL(url)
 8    } catch (_error) {
 9      throw new Error(`Invalid URL format: ${url}`)
10    }
11  
12    // Validate URL protocol for security
13    if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
14      throw new Error(
15        `Invalid URL protocol: must use http:// or https://, got ${parsedUrl.protocol}`,
16      )
17    }
18  }
19  
20  /**
21   * Open a file or folder path using the system's default handler.
22   * Uses `open` on macOS, `explorer` on Windows, `xdg-open` on Linux.
23   */
24  export async function openPath(path: string): Promise<boolean> {
25    try {
26      const platform = process.platform
27      if (platform === 'win32') {
28        const { code } = await execFileNoThrow('explorer', [path])
29        return code === 0
30      }
31      const command = platform === 'darwin' ? 'open' : 'xdg-open'
32      const { code } = await execFileNoThrow(command, [path])
33      return code === 0
34    } catch (_) {
35      return false
36    }
37  }
38  
39  export async function openBrowser(url: string): Promise<boolean> {
40    try {
41      // Parse and validate the URL
42      validateUrl(url)
43  
44      const browserEnv = process.env.BROWSER
45      const platform = process.platform
46  
47      if (platform === 'win32') {
48        if (browserEnv) {
49          // browsers require shell, else they will treat this as a file:/// handle
50          const { code } = await execFileNoThrow(browserEnv, [`"${url}"`])
51          return code === 0
52        }
53        const { code } = await execFileNoThrow(
54          'rundll32',
55          ['url,OpenURL', url],
56          {},
57        )
58        return code === 0
59      } else {
60        const command =
61          browserEnv || (platform === 'darwin' ? 'open' : 'xdg-open')
62        const { code } = await execFileNoThrow(command, [url])
63        return code === 0
64      }
65    } catch (_) {
66      return false
67    }
68  }