source dump of claude code
at main 107 lines 3.7 kB view raw
1import { realpath, stat } from 'fs/promises' 2import { getPlatform } from '../platform.js' 3import { which } from '../which.js' 4 5async function probePath(p: string): Promise<string | null> { 6 try { 7 return (await stat(p)).isFile() ? p : null 8 } catch { 9 return null 10 } 11} 12 13/** 14 * Attempts to find PowerShell on the system via PATH. 15 * Prefers pwsh (PowerShell Core 7+), falls back to powershell (5.1). 16 * 17 * On Linux, if PATH resolves to a snap launcher (/snap/…) — directly or 18 * via a symlink chain like /usr/bin/pwsh → /snap/bin/pwsh — probe known 19 * apt/rpm install locations instead: the snap launcher can hang in 20 * subprocesses while snapd initializes confinement, but the underlying 21 * binary at /opt/microsoft/powershell/7/pwsh is reliable. On 22 * Windows/macOS, PATH is sufficient. 23 */ 24export async function findPowerShell(): Promise<string | null> { 25 const pwshPath = await which('pwsh') 26 if (pwshPath) { 27 // Snap launcher hangs in subprocesses. Prefer the direct binary. 28 // Check both the resolved PATH entry and its symlink target: on 29 // some distros /usr/bin/pwsh is a symlink to /snap/bin/pwsh, which 30 // would bypass a naive startsWith('/snap/') on the which() result. 31 if (getPlatform() === 'linux') { 32 const resolved = await realpath(pwshPath).catch(() => pwshPath) 33 if (pwshPath.startsWith('/snap/') || resolved.startsWith('/snap/')) { 34 const direct = 35 (await probePath('/opt/microsoft/powershell/7/pwsh')) ?? 36 (await probePath('/usr/bin/pwsh')) 37 if (direct) { 38 const directResolved = await realpath(direct).catch(() => direct) 39 if ( 40 !direct.startsWith('/snap/') && 41 !directResolved.startsWith('/snap/') 42 ) { 43 return direct 44 } 45 } 46 } 47 } 48 return pwshPath 49 } 50 51 const powershellPath = await which('powershell') 52 if (powershellPath) { 53 return powershellPath 54 } 55 56 return null 57} 58 59let cachedPowerShellPath: Promise<string | null> | null = null 60 61/** 62 * Gets the cached PowerShell path. Returns a memoized promise that 63 * resolves to the PowerShell executable path or null. 64 */ 65export function getCachedPowerShellPath(): Promise<string | null> { 66 if (!cachedPowerShellPath) { 67 cachedPowerShellPath = findPowerShell() 68 } 69 return cachedPowerShellPath 70} 71 72export type PowerShellEdition = 'core' | 'desktop' 73 74/** 75 * Infers the PowerShell edition from the binary name without spawning. 76 * - `pwsh` / `pwsh.exe` → 'core' (PowerShell 7+: supports `&&`, `||`, `?:`, `??`) 77 * - `powershell` / `powershell.exe` → 'desktop' (Windows PowerShell 5.1: 78 * no pipeline chain operators, stderr-sets-$? bug, UTF-16 default encoding) 79 * 80 * PowerShell 6 (also `pwsh`, no `&&`) has been EOL since 2020 and is not 81 * a realistic install target, so 'core' safely implies 7+ semantics. 82 * 83 * Used by the tool prompt to give version-appropriate syntax guidance so 84 * the model doesn't emit `cmd1 && cmd2` on 5.1 (parser error) or avoid 85 * `&&` on 7+ where it's the correct short-circuiting operator. 86 */ 87export async function getPowerShellEdition(): Promise<PowerShellEdition | null> { 88 const p = await getCachedPowerShellPath() 89 if (!p) return null 90 // basename without extension, case-insensitive. Covers: 91 // C:\Program Files\PowerShell\7\pwsh.exe 92 // /opt/microsoft/powershell/7/pwsh 93 // C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe 94 const base = p 95 .split(/[/\\]/) 96 .pop()! 97 .toLowerCase() 98 .replace(/\.exe$/, '') 99 return base === 'pwsh' ? 'core' : 'desktop' 100} 101 102/** 103 * Resets the cached PowerShell path. Only for testing. 104 */ 105export function resetPowerShellCache(): void { 106 cachedPowerShellPath = null 107}