/ utils / which.ts
which.ts
 1  import { execa } from 'execa'
 2  import { execSync_DEPRECATED } from './execSyncWrapper.js'
 3  
 4  async function whichNodeAsync(command: string): Promise<string | null> {
 5    if (process.platform === 'win32') {
 6      // On Windows, use where.exe and return the first result
 7      const result = await execa(`where.exe ${command}`, {
 8        shell: true,
 9        stderr: 'ignore',
10        reject: false,
11      })
12      if (result.exitCode !== 0 || !result.stdout) {
13        return null
14      }
15      // where.exe returns multiple paths separated by newlines, return the first
16      return result.stdout.trim().split(/\r?\n/)[0] || null
17    }
18  
19    // On POSIX systems (macOS, Linux, WSL), use which
20    // Cross-platform safe: Windows is handled above
21    // eslint-disable-next-line custom-rules/no-cross-platform-process-issues
22    const result = await execa(`which ${command}`, {
23      shell: true,
24      stderr: 'ignore',
25      reject: false,
26    })
27    if (result.exitCode !== 0 || !result.stdout) {
28      return null
29    }
30    return result.stdout.trim()
31  }
32  
33  function whichNodeSync(command: string): string | null {
34    if (process.platform === 'win32') {
35      try {
36        const result = execSync_DEPRECATED(`where.exe ${command}`, {
37          encoding: 'utf-8',
38          stdio: ['ignore', 'pipe', 'ignore'],
39        })
40        const output = result.toString().trim()
41        return output.split(/\r?\n/)[0] || null
42      } catch {
43        return null
44      }
45    }
46  
47    try {
48      const result = execSync_DEPRECATED(`which ${command}`, {
49        encoding: 'utf-8',
50        stdio: ['ignore', 'pipe', 'ignore'],
51      })
52      return result.toString().trim() || null
53    } catch {
54      return null
55    }
56  }
57  
58  const bunWhich =
59    typeof Bun !== 'undefined' && typeof Bun.which === 'function'
60      ? Bun.which
61      : null
62  
63  /**
64   * Finds the full path to a command executable.
65   * Uses Bun.which when running in Bun (fast, no process spawn),
66   * otherwise spawns the platform-appropriate command.
67   *
68   * @param command - The command name to look up
69   * @returns The full path to the command, or null if not found
70   */
71  export const which: (command: string) => Promise<string | null> = bunWhich
72    ? async command => bunWhich(command)
73    : whichNodeAsync
74  
75  /**
76   * Synchronous version of `which`.
77   *
78   * @param command - The command name to look up
79   * @returns The full path to the command, or null if not found
80   */
81  export const whichSync: (command: string) => string | null =
82    bunWhich ?? whichNodeSync