Private/Invoke-WingetPackageAction.ps1

function Invoke-WingetPackageAction {
    <#
    .SYNOPSIS
        Install, update or uninstall one package and report whether it really worked.
 
    .DESCRIPTION
        Single entry point for package changes. Always calls the Microsoft.WinGet.Client
        cmdlets by their module-qualified name (other modules such as Cobalt export
        commands with the same names), matches the ID exactly instead of by substring,
        and returns a ConvertTo-WingetActionResult object instead of assuming success
        when no exception is thrown.
 
    .PARAMETER Options
        Optional install settings: Mode, Scope, Architecture, Override, Location,
        Force, SkipDependencies, AllowHashMismatch. Keys the target cmdlet does not
        support are ignored.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateSet('Install', 'Update', 'Uninstall')]
        [string]$Action,

        [Parameter(Mandatory)]
        [string]$Id,

        [Parameter()]
        [string]$Version,

        [Parameter()]
        [string]$Source,

        [Parameter()]
        [hashtable]$Options = @{}
    )

    $cmd = Get-Command -Name "Microsoft.WinGet.Client\$Action-WinGetPackage" -ErrorAction SilentlyContinue
    if (-not $cmd) {
        return [PSCustomObject]@{
            Id = $Id; Action = $Action; Succeeded = $false; Status = 'ModuleMissing'
            RebootRequired = $false; Message = 'Microsoft.WinGet.Client is not available.'
        }
    }

    $params = @{
        Id          = $Id
        MatchOption = 'EqualsCaseInsensitive'
        ErrorAction = 'Stop'
    }
    if ($Version -and $Version -ne 'latest') { $params['Version'] = $Version }
    if ($Source) { $params['Source'] = $Source }

    foreach ($key in $Options.Keys) {
        $value = $Options[$key]
        if ($null -eq $value -or ($value -is [string] -and $value -eq '')) { continue }
        if (-not $cmd.Parameters.ContainsKey($key)) { continue }
        if ($cmd.Parameters[$key].SwitchParameter) {
            if ([bool]$value) { $params[$key] = $true }
        }
        else {
            $params[$key] = $value
        }
    }

    # Installed copies of this exact ID. ARP\... and MSIX\... IDs make the -Id search
    # throw CatalogError, so fall back to listing everything and filtering.
    $findInstalled = {
        try {
            @(Microsoft.WinGet.Client\Get-WinGetPackage -Id $Id -MatchOption EqualsCaseInsensitive -ErrorAction Stop)
        }
        catch {
            @(Microsoft.WinGet.Client\Get-WinGetPackage -ErrorAction SilentlyContinue | Where-Object { $_.Id -eq $Id })
        }
    }
    $countInstalled = { @(& $findInstalled).Count }
    $getVersion = { & $findInstalled | Select-Object -First 1 -ExpandProperty InstalledVersion }
    # Counted before an uninstall so we can confirm a copy went away
    $before = if ($Action -eq 'Uninstall') { & $countInstalled } else { 0 }
    $versionBefore = if ($Action -eq 'Update') { [string](& $getVersion) } else { $null }

    try {
        # Programs known only from Add/Remove Programs (ARP\...) or MSIX (MSIX\...) cannot
        # be found by ID search (CatalogError); act on the installed package object instead.
        $byObject = {
            $pkg = & $findInstalled | Select-Object -First 1
            if (-not $pkg) { throw "Package '$Id' is not installed." }
            $objParams = @{ PSCatalogPackage = $pkg; ErrorAction = 'Stop' }
            foreach ($k in $params.Keys) {
                if ($k -notin 'Id', 'MatchOption', 'Source', 'Version', 'ErrorAction') { $objParams[$k] = $params[$k] }
            }
            & $cmd @objParams
        }
        if ($Action -ne 'Install' -and $Id -match '^(ARP|MSIX)\\') {
            $result = & $byObject
        }
        else {
            try { $result = & $cmd @params }
            catch {
                if ($Action -ne 'Install' -and $_.Exception.Message -match 'CatalogError') { $result = & $byObject }
                else { throw }
            }
        }
        $normalized = ConvertTo-WingetActionResult -Result $result -Id $Id -Action $Action

        # Some uninstallers exit 0 without removing anything (WinGet then reports Ok),
        # and NSIS uninstallers finish a few seconds after returning. Confirm removal.
        if ($normalized.Succeeded -and $Action -eq 'Uninstall' -and $before -gt 0) {
            $deadline = (Get-Date).AddSeconds(30)
            while ((& $countInstalled) -ge $before -and (Get-Date) -lt $deadline) {
                Start-Sleep -Seconds 3
            }
            if ((& $countInstalled) -ge $before) {
                $normalized.Succeeded = $false
                $normalized.Status = 'StillInstalled'
                $normalized.Message = "WinGet reported success, but $Id is still installed (the uninstaller did not remove it)."
            }
        }
        # An update that "succeeds" but leaves the same version installed (installer
        # declined, or WinGet mis-orders versions like "1.5.11+hash" vs "1.5.11")
        if ($normalized.Succeeded -and $Action -eq 'Update' -and $normalized.Status -eq 'Ok' -and $versionBefore) {
            $deadline = (Get-Date).AddSeconds(20)
            $versionAfter = [string](& $getVersion)
            while ($versionAfter -eq $versionBefore -and (Get-Date) -lt $deadline) {
                Start-Sleep -Seconds 3
                $versionAfter = [string](& $getVersion)
            }
            if ($versionAfter -eq $versionBefore) {
                $normalized.Succeeded = $false
                $normalized.Status = 'NotUpdated'
                $normalized.Message = "WinGet reported success, but $Id is still at $versionBefore."
            }
        }
        $normalized
    }
    catch {
        [PSCustomObject]@{
            Id             = $Id
            Action         = $Action
            Succeeded      = $false
            Status         = 'Error'
            RebootRequired = $false
            Message        = $_.Exception.Message
        }
    }
}