Private/Get-DFCachedCommandOutput.ps1

#Requires -Version 7.0

function Get-DFCachedCommandOutput {
    <#
    .SYNOPSIS
        Returns the cached stdout of a deterministic external command,
        regenerating only when the resolved executable itself has changed.
    .DESCRIPTION
        For companions like carapace/zoxide/mdcat/scoop-search whose init
        output is a pure function of the tool's own build -- verified
        byte-identical across repeated runs (see
        docs/superpowers/specs/2026-09-05-startup-perf-audit.md) -- with no
        session input to fingerprint on (unlike vivid's theme name, see
        Tools/vivid.ps1). The fingerprint is the resolved executable's path
        plus its LastWriteTimeUtc: a file stat, not a process spawn, so the
        cache-hit path never pays for a version check. A tool upgrade
        (which rewrites the file) or a switch to a differently-located
        binary both correctly invalidate the cache.

        Falls back to always calling -Generate, uncached, when
        $Env:XDG_CACHE_HOME is unset, -Executable does not resolve, or it
        resolves to something with no backing file (a function or alias
        stand-in, e.g. how tests/scoop.Tests.ps1 stubs scoop-search --
        Get-Command's .Source on a function is not a usable file path) --
        these companions register real functionality (completions, cd
        hooks), so degrading to "slower but correct" beats "skip it
        entirely" the way Tools/vivid.ps1's cosmetic LS_COLORS cache does.

        Never caches a falsy result (empty string or $null) -- a transient
        failure is retried next session rather than remembered.
    .PARAMETER Name
        Cache key, distinct per companion (e.g. 'carapace-init'). Backs the
        files $XDG_CACHE_HOME/dotforge/<Name>.txt and <Name>.key.
    .PARAMETER Executable
        The command name to resolve and fingerprint (e.g. 'carapace').
    .PARAMETER Generate
        Scriptblock producing the real output on a cache miss.
    .PARAMETER Force
        Bypass the cache and regenerate unconditionally.
    .EXAMPLE
        Get-DFCachedCommandOutput -Name 'carapace-init' -Executable 'carapace' -Generate {
            carapace _carapace powershell | Out-String
        }
    .OUTPUTS
        [string]
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)][string]$Name,
        [Parameter(Mandatory)][string]$Executable,
        [Parameter(Mandatory)][scriptblock]$Generate,
        [switch]$Force
    )

    $cmd = Get-Command $Executable -ErrorAction Ignore
    if (-not $Env:XDG_CACHE_HOME -or -not $cmd -or -not $cmd.Source -or -not (Test-Path $cmd.Source -PathType Leaf)) {
        return & $Generate
    }

    $cacheDir  = Join-Path $Env:XDG_CACHE_HOME 'dotforge'
    $cacheFile = Join-Path $cacheDir "$Name.txt"
    $keyFile   = Join-Path $cacheDir "$Name.key"
    $fingerprint = "$($cmd.Source)|$((Get-Item $cmd.Source).LastWriteTimeUtc.Ticks)"

    $cacheValid = -not $Force -and (Test-Path $cacheFile -PathType Leaf) -and (Test-Path $keyFile -PathType Leaf) -and
                  ((Get-Content $keyFile -Raw).Trim() -eq $fingerprint)

    if ($cacheValid) {
        return (Get-Content $cacheFile -Raw).Trim()
    }

    $value = (& $Generate)
    if ($value) { $value = $value.Trim() }
    if ($value) {
        New-DFDirectory $cacheDir
        Set-Content -Path $keyFile   -Value $fingerprint -Encoding UTF8
        Set-Content -Path $cacheFile -Value $value        -Encoding UTF8
    }
    return $value
}