/ utils / platform.ts
platform.ts
  1  import { readdir, readFile } from 'fs/promises'
  2  import memoize from 'lodash-es/memoize.js'
  3  import { release as osRelease } from 'os'
  4  import { getFsImplementation } from './fsOperations.js'
  5  import { logError } from './log.js'
  6  
  7  export type Platform = 'macos' | 'windows' | 'wsl' | 'linux' | 'unknown'
  8  
  9  export const SUPPORTED_PLATFORMS: Platform[] = ['macos', 'wsl']
 10  
 11  export const getPlatform = memoize((): Platform => {
 12    try {
 13      if (process.platform === 'darwin') {
 14        return 'macos'
 15      }
 16  
 17      if (process.platform === 'win32') {
 18        return 'windows'
 19      }
 20  
 21      if (process.platform === 'linux') {
 22        // Check if running in WSL (Windows Subsystem for Linux)
 23        try {
 24          const procVersion = getFsImplementation().readFileSync(
 25            '/proc/version',
 26            { encoding: 'utf8' },
 27          )
 28          if (
 29            procVersion.toLowerCase().includes('microsoft') ||
 30            procVersion.toLowerCase().includes('wsl')
 31          ) {
 32            return 'wsl'
 33          }
 34        } catch (error) {
 35          // Error reading /proc/version, assume regular Linux
 36          logError(error)
 37        }
 38  
 39        // Regular Linux
 40        return 'linux'
 41      }
 42  
 43      // Unknown platform
 44      return 'unknown'
 45    } catch (error) {
 46      logError(error)
 47      return 'unknown'
 48    }
 49  })
 50  
 51  export const getWslVersion = memoize((): string | undefined => {
 52    // Only check for WSL on Linux systems
 53    if (process.platform !== 'linux') {
 54      return undefined
 55    }
 56    try {
 57      const procVersion = getFsImplementation().readFileSync('/proc/version', {
 58        encoding: 'utf8',
 59      })
 60  
 61      // First check for explicit WSL version markers (e.g., "WSL2", "WSL3", etc.)
 62      const wslVersionMatch = procVersion.match(/WSL(\d+)/i)
 63      if (wslVersionMatch && wslVersionMatch[1]) {
 64        return wslVersionMatch[1]
 65      }
 66  
 67      // If no explicit WSL version but contains Microsoft, assume WSL1
 68      // This handles the original WSL1 format: "4.4.0-19041-Microsoft"
 69      if (procVersion.toLowerCase().includes('microsoft')) {
 70        return '1'
 71      }
 72  
 73      // Not WSL or unable to determine version
 74      return undefined
 75    } catch (error) {
 76      logError(error)
 77      return undefined
 78    }
 79  })
 80  
 81  export type LinuxDistroInfo = {
 82    linuxDistroId?: string
 83    linuxDistroVersion?: string
 84    linuxKernel?: string
 85  }
 86  
 87  export const getLinuxDistroInfo = memoize(
 88    async (): Promise<LinuxDistroInfo | undefined> => {
 89      if (process.platform !== 'linux') {
 90        return undefined
 91      }
 92  
 93      const result: LinuxDistroInfo = {
 94        linuxKernel: osRelease(),
 95      }
 96  
 97      try {
 98        const content = await readFile('/etc/os-release', 'utf8')
 99        for (const line of content.split('\n')) {
100          const match = line.match(/^(ID|VERSION_ID)=(.*)$/)
101          if (match && match[1] && match[2]) {
102            const value = match[2].replace(/^"|"$/g, '')
103            if (match[1] === 'ID') {
104              result.linuxDistroId = value
105            } else {
106              result.linuxDistroVersion = value
107            }
108          }
109        }
110      } catch {
111        // /etc/os-release may not exist on all Linux systems
112      }
113  
114      return result
115    },
116  )
117  
118  const VCS_MARKERS: Array<[string, string]> = [
119    ['.git', 'git'],
120    ['.hg', 'mercurial'],
121    ['.svn', 'svn'],
122    ['.p4config', 'perforce'],
123    ['$tf', 'tfs'],
124    ['.tfvc', 'tfs'],
125    ['.jj', 'jujutsu'],
126    ['.sl', 'sapling'],
127  ]
128  
129  export async function detectVcs(dir?: string): Promise<string[]> {
130    const detected = new Set<string>()
131  
132    // Check for Perforce via env var
133    if (process.env.P4PORT) {
134      detected.add('perforce')
135    }
136  
137    try {
138      const targetDir = dir ?? getFsImplementation().cwd()
139      const entries = new Set(await readdir(targetDir))
140      for (const [marker, vcs] of VCS_MARKERS) {
141        if (entries.has(marker)) {
142          detected.add(vcs)
143        }
144      }
145    } catch {
146      // Directory may not be readable
147    }
148  
149    return [...detected]
150  }