Private/Test-DFToolAvailable.ps1

#Requires -Version 7.0

$script:DFToolAvailability = @{}

function Test-DFToolAvailable {
    <#
    .SYNOPSIS
        Checks whether a tool's executable or module is available; "available" is remembered for the session.
    .DESCRIPTION
        Wraps Get-Command (exe-type tools) / Get-Module -ListAvailable
        (module-type tools) with a session-scoped cache keyed by type and
        name. Only a positive answer is remembered: an available tool is probed
        once per session, but a missing one is probed again on every call,
        because an earlier tool can put it on PATH mid-load (fnm puts node,
        npm and inshellisense's `is` on PATH only when its companion runs) and
        an install can add it mid-session. A missing tool costs one lookup per
        call, and only missing tools pay it.
    .PARAMETER Executable
        The executable name (exe-type tools) or module name (module-type
        tools) to check.
    .PARAMETER Type
        'exe' or 'module'. Defaults to 'exe'.
    .PARAMETER Force
        Bypass the cache and re-probe.
    .EXAMPLE
        Test-DFToolAvailable -Executable 'ripgrep.exe'
        Returns $true if ripgrep.exe is on PATH.
    .EXAMPLE
        Test-DFToolAvailable -Executable 'PSFzf' -Type 'module'
        Returns $true if the PSFzf module is installed.
    .OUTPUTS
        [bool]
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory)]
        [string]$Executable,

        [ValidateSet('exe', 'module')]
        [string]$Type = 'exe',

        [switch]$Force
    )

    $key = "$Type|$Executable"
    if (-not $Force -and $script:DFToolAvailability.ContainsKey($key)) {
        return $script:DFToolAvailability[$key]
    }

    $available = if ($Type -eq 'module') { Test-DFModuleOnPath -Name $Executable } else { Test-DFExecutableOnPath -Name $Executable }

    # Never remember "not installed": see .DESCRIPTION.
    if ($available) { $script:DFToolAvailability[$key] = $true }
    return $available
}

function Test-DFExecutableOnPath {
    <#
    .SYNOPSIS
        Whether an executable is on PATH, by checking its exact candidate filenames in each PATH folder.
    .DESCRIPTION
        A cheaper stand-in for Get-Command when only "is it installed?"
        matters (about half the time per tool at startup). A name with an
        extension is looked up as is; a bare name also tries each PATHEXT
        extension (pipx is pipx.cmd from scoop, pipx.exe from pip). Folders
        that don't exist are skipped. A rooted path is checked directly.
    .PARAMETER Name
        The executable, e.g. 'rg.exe' or 'pipx'.
    .PARAMETER PathValue
        The PATH to search. Default: $Env:Path.
    .PARAMETER PathExt
        Extensions a bare name may have. Default: $Env:PATHEXT.
    .OUTPUTS
        System.Boolean.
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory)][string]$Name,
        [string]$PathValue = $Env:Path,
        [string]$PathExt = $Env:PATHEXT
    )
    if ([IO.Path]::IsPathRooted($Name)) { return [IO.File]::Exists($Name) }
    $names = if ([IO.Path]::HasExtension($Name)) { @($Name) }
             else { @($Name) + @("$PathExt" -split ';' | Where-Object { $_ } | ForEach-Object { $Name + $_ }) }
    foreach ($dir in "$PathValue" -split [IO.Path]::PathSeparator) {
        $dir = $dir.Trim().Trim('"')
        if (-not $dir) { continue }
        foreach ($n in $names) {
            if ([IO.File]::Exists([IO.Path]::Combine($dir, $n))) { return $true }
        }
    }
    $false
}

function Test-DFModuleOnPath {
    <#
    .SYNOPSIS
        Whether a PowerShell module is installed, by checking for its folder under each PSModulePath root.
    .DESCRIPTION
        A cheaper stand-in for Get-Module -ListAvailable (about a tenth of the
        time), which also reads every manifest it finds.
    .PARAMETER Name
        The module name.
    .PARAMETER ModulePath
        The module search path. Default: $Env:PSModulePath.
    .OUTPUTS
        System.Boolean.
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory)][string]$Name,
        [string]$ModulePath = $Env:PSModulePath
    )
    foreach ($root in "$ModulePath" -split [IO.Path]::PathSeparator) {
        $root = $root.Trim().Trim('"')
        if ($root -and [IO.Directory]::Exists([IO.Path]::Combine($root, $Name))) { return $true }
    }
    $false
}