powershellDetection.ts
utils/shell/powershellDetection.ts
108
Lines
3714
Bytes
5
Exports
3
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with shell-safety. It contains 108 lines, 3 detected imports, and 5 detected exports.
Important relationships
Detected exports
findPowerShellgetCachedPowerShellPathPowerShellEditiongetPowerShellEditionresetPowerShellCache
Keywords
powershellpwshsnappathdirectwhichpromisepwshpathcorestartswith
Detected imports
fs/promises../platform.js../which.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
import { realpath, stat } from 'fs/promises'
import { getPlatform } from '../platform.js'
import { which } from '../which.js'
async function probePath(p: string): Promise<string | null> {
try {
return (await stat(p)).isFile() ? p : null
} catch {
return null
}
}
/**
* Attempts to find PowerShell on the system via PATH.
* Prefers pwsh (PowerShell Core 7+), falls back to powershell (5.1).
*
* On Linux, if PATH resolves to a snap launcher (/snap/…) — directly or
* via a symlink chain like /usr/bin/pwsh → /snap/bin/pwsh — probe known
* apt/rpm install locations instead: the snap launcher can hang in
* subprocesses while snapd initializes confinement, but the underlying
* binary at /opt/microsoft/powershell/7/pwsh is reliable. On
* Windows/macOS, PATH is sufficient.
*/
export async function findPowerShell(): Promise<string | null> {
const pwshPath = await which('pwsh')
if (pwshPath) {
// Snap launcher hangs in subprocesses. Prefer the direct binary.
// Check both the resolved PATH entry and its symlink target: on
// some distros /usr/bin/pwsh is a symlink to /snap/bin/pwsh, which
// would bypass a naive startsWith('/snap/') on the which() result.
if (getPlatform() === 'linux') {
const resolved = await realpath(pwshPath).catch(() => pwshPath)
if (pwshPath.startsWith('/snap/') || resolved.startsWith('/snap/')) {
const direct =
(await probePath('/opt/microsoft/powershell/7/pwsh')) ??
(await probePath('/usr/bin/pwsh'))
if (direct) {
const directResolved = await realpath(direct).catch(() => direct)
if (
!direct.startsWith('/snap/') &&
!directResolved.startsWith('/snap/')
) {
return direct
}
}
}
}
return pwshPath
}
const powershellPath = await which('powershell')
if (powershellPath) {
return powershellPath
}
return null
}
let cachedPowerShellPath: Promise<string | null> | null = null
/**
* Gets the cached PowerShell path. Returns a memoized promise that
* resolves to the PowerShell executable path or null.
*/
export function getCachedPowerShellPath(): Promise<string | null> {
if (!cachedPowerShellPath) {
cachedPowerShellPath = findPowerShell()
}
return cachedPowerShellPath
}
export type PowerShellEdition = 'core' | 'desktop'
/**
* Infers the PowerShell edition from the binary name without spawning.
* - `pwsh` / `pwsh.exe` → 'core' (PowerShell 7+: supports `&&`, `||`, `?:`, `??`)
* - `powershell` / `powershell.exe` → 'desktop' (Windows PowerShell 5.1:
* no pipeline chain operators, stderr-sets-$? bug, UTF-16 default encoding)
*
* PowerShell 6 (also `pwsh`, no `&&`) has been EOL since 2020 and is not
* a realistic install target, so 'core' safely implies 7+ semantics.
*
* Used by the tool prompt to give version-appropriate syntax guidance so
* the model doesn't emit `cmd1 && cmd2` on 5.1 (parser error) or avoid
* `&&` on 7+ where it's the correct short-circuiting operator.
*/
export async function getPowerShellEdition(): Promise<PowerShellEdition | null> {
const p = await getCachedPowerShellPath()
if (!p) return null
// basename without extension, case-insensitive. Covers:
// C:\Program Files\PowerShell\7\pwsh.exe
// /opt/microsoft/powershell/7/pwsh
// C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
const base = p
.split(/[/\\]/)
.pop()!
.toLowerCase()
.replace(/\.exe$/, '')
return base === 'pwsh' ? 'core' : 'desktop'
}
/**
* Resets the cached PowerShell path. Only for testing.
*/
export function resetPowerShellCache(): void {
cachedPowerShellPath = null
}