which.ts 2.3 KB

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