Private/Invoke-DFInstallCommand.ps1

#Requires -Version 7.0

function Expand-DFInstallArgv {
    <#
    .SYNOPSIS
        Fills a manager's argv template: {id} becomes every id, other {name} tokens come from -Values.
    .PARAMETER Template
        The argv template from installs.command or installs.feeds. A token
        that is exactly {id} expands to every id; a token containing {id}
        (pkg@{id}) is repeated once per id.
    .PARAMETER Values
        ids (string[]) plus any other token values (name, url).
    .OUTPUTS
        System.String[].
    #>

    [CmdletBinding()]
    [OutputType([string[]])]
    param([Parameter(Mandatory)][string[]]$Template, [Parameter(Mandatory)][hashtable]$Values)
    [string[]]@(foreach ($t in $Template) {
        $fill = { param($text) [regex]::Replace($text, '\{(\w+)\}', { param($m) [string]$Values[$m.Groups[1].Value] }) }
        if ($t -eq '{id}') { @($Values.ids) }
        # {id} inside a token (node@{id}) gives one argument per id.
        elseif ($t.Contains('{id}')) { foreach ($i in @($Values.ids)) { & $fill $t.Replace('{id}', $i) } }
        else { & $fill $t }
    })
}

function Invoke-DFInstallCommand {
    <#
    .SYNOPSIS
        Runs one package-manager command and returns its exit code and output. The only place DotForge runs a manager.
    .DESCRIPTION
        An argv runs as a native command; a function (installs.function, e.g.
        Install-PSResource) is called with -Arguments splatted. -Elevate runs
        the argv through -ElevateWith (the elevator role's executable, e.g.
        gsudo). Tests mock this function.
    .PARAMETER Manager
        The manager's tool record (for messages).
    .PARAMETER Argv
        The command line, already expanded.
    .PARAMETER Function
        A PowerShell command to call instead of an argv.
    .PARAMETER Arguments
        Parameters for -Function.
    .PARAMETER Elevate
        Run through -ElevateWith.
    .PARAMETER ElevateWith
        The elevator's executable.
    .OUTPUTS
        PSCustomObject: ExitCode, Output.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory)][pscustomobject]$Manager,
        [string[]]$Argv = @(),
        [string]$Function,
        [hashtable]$Arguments = @{},
        [switch]$Elevate,
        [string]$ElevateWith
    )
    if ($Function) {
        try {
            $out = & $Function @Arguments -ErrorAction Stop 2>&1 | Out-String
            return [pscustomobject]@{ ExitCode = 0; Output = $out }
        } catch {
            return [pscustomobject]@{ ExitCode = 1; Output = $_.Exception.Message }
        }
    }
    $exe, $rest = if ($Elevate) { $ElevateWith, $Argv } else { $Argv[0], @($Argv | Select-Object -Skip 1) }
    # A manager can resolve to a .ps1 shim (scoop.ps1) that runs in-process and
    # never sets an exit code: don't read a stale one from an earlier command.
    $global:LASTEXITCODE = 0
    $out = & $exe @rest 2>&1 | Out-String
    [pscustomobject]@{ ExitCode = $LASTEXITCODE; Output = $out }
}