Public/Get-WingetUpdates.ps1

function Get-WingetUpdates {
    <#
    .SYNOPSIS
        Check for and install available winget package updates.
 
    .DESCRIPTION
        Displays a list of all installed winget packages that have updates available,
        with an interactive selection to choose which ones to update.
        Uses the Microsoft.WinGet.Client COM API for reliable package enumeration.
 
    .PARAMETER Force
        Skip the cache and force a fresh check for updates.
 
    .PARAMETER ListOnly
        Return the available updates as objects without prompting or installing.
        Use this from scripts, scheduled tasks and the REST server.
 
    .EXAMPLE
        Get-WingetUpdates
        Shows available updates and allows you to select which to install.
 
    .EXAMPLE
        Get-WingetUpdates -Force
        Forces a fresh check for updates.
 
    .EXAMPLE
        Get-WingetUpdates -ListOnly | Where-Object Source -eq 'winget'
        Lists pending updates as objects for further processing.
    #>


    [CmdletBinding()]
    param(
        [Parameter()]
        [switch]$Force,

        [Parameter()]
        [switch]$ListOnly,

        [Parameter()]
        [switch]$IWantToLiterallyUpdateAllFuckingResults,

        [Parameter()]
        [switch]$ExportHtml,

        [Parameter()]
        [ValidateSet("Default", "Silent", "Interactive")]
        [string]$Mode,

        [Parameter()]
        [ValidateSet("User", "Machine")]
        [string]$Scope,

        [Parameter()]
        [string]$Architecture,

        [Parameter()]
        [string]$Override,

        [Parameter()]
        [string]$Location,

        [Parameter()]
        [switch]$ForceInstall,

        [Parameter()]
        [switch]$SkipDependencies,

        [Parameter()]
        [switch]$AllowHashMismatch
    )

    # Ensure Microsoft.WinGet.Client is available
    if (-not (Get-Module -Name Microsoft.WinGet.Client)) {
        try {
            Import-Module Microsoft.WinGet.Client -ErrorAction Stop
        }
        catch {
            Write-Error "Microsoft.WinGet.Client module is required. Install it with: Install-Module Microsoft.WinGet.Client -Force"
            return
        }
    }

    if (-not $ListOnly) {
        Write-Host "Checking for winget package updates..." -ForegroundColor Cyan
    }

    # Check cache first
    $configDir = Get-WingetBatchConfigDir
    if (-not (Test-Path $configDir)) { New-Item -ItemType Directory -Path $configDir -Force | Out-Null }
    $cacheFile = Join-Path $configDir "update_cache.json"
    $useCache = $false
    $updatesAvailable = [System.Collections.Generic.List[PSCustomObject]]::new()

    if (-not $Force -and (Test-Path $cacheFile)) {
        try {
            $cache = Get-Content $cacheFile -Raw | ConvertFrom-Json
            $cacheAge = ((Get-Date) - [DateTime]::Parse($cache.LastChecked)).TotalMinutes

            # Only trust caches written in the current format (they carry AvailableVersion)
            if ($cacheAge -lt 30 -and @($cache.Updates).Count -gt 0 -and $null -ne @($cache.Updates)[0].AvailableVersion) {
                $useCache = $true
                foreach ($u in $cache.Updates) {
                    $updatesAvailable.Add([PSCustomObject]@{
                        Id = $u.Id; Name = $u.Name; InstalledVersion = $u.InstalledVersion
                        AvailableVersion = $u.AvailableVersion; Source = $u.Source
                    })
                }
                if (-not $ListOnly) {
                    Write-Host "Using cached results (checked $([Math]::Round($cacheAge, 0)) minutes ago, -Force to refresh)" -ForegroundColor DarkGray
                }
            }
        }
        catch {
            # Cache is corrupt, ignore
        }
    }

    if (-not $useCache) {
        if (-not $ListOnly) {
            Write-Host "Querying installed packages via COM API..." -ForegroundColor DarkGray
        }
        $installed = Microsoft.WinGet.Client\Get-WinGetPackage -ErrorAction SilentlyContinue

        foreach ($pkg in $installed) {
            if ($pkg.IsUpdateAvailable) {
                $updatesAvailable.Add([PSCustomObject]@{
                    Id               = $pkg.Id
                    Name             = $pkg.Name
                    InstalledVersion = $pkg.InstalledVersion
                    AvailableVersion = @($pkg.AvailableVersions)[0]
                    Source           = $pkg.Source
                })
            }
        }

        # Save to cache (also read by the profile update notification)
        try {
            $cacheData = @{
                LastChecked = (Get-Date).ToString('o')
                UpdateCount = $updatesAvailable.Count
                Updates     = @($updatesAvailable)
            } | ConvertTo-Json -Depth 5
            [System.IO.File]::WriteAllText($cacheFile, $cacheData, [System.Text.Encoding]::UTF8)
        }
        catch {
            Write-Verbose "Failed to save update cache: $_"
        }
    }

    if ($ListOnly) {
        return $updatesAvailable.ToArray()
    }

    if ($updatesAvailable.Count -eq 0) {
        Write-Host "[OK] All packages are up to date!" -ForegroundColor Green
        return
    }

    Write-Host ""
    Write-Host " - " -ForegroundColor Green -NoNewline
    Write-Host "$($updatesAvailable.Count)" -ForegroundColor White -NoNewline
    Write-Host " update(s) available" -ForegroundColor Green
    Write-Host ""

    # HTML Export
    if ($ExportHtml) {
        Write-Host "`n[HTML] Exporting HTML report..." -ForegroundColor Cyan
        $timestamp = (Get-Date).ToString("yyyyMMdd_HHmmss")
        $defaultPath = "C:\temp\WingetBatch_Updates_$timestamp.html"
        $exportPath = Read-Host "Enter path for HTML report [Default: $defaultPath]"
        if (-not $exportPath) { $exportPath = $defaultPath }
        if (-not $exportPath.EndsWith(".html")) { $exportPath += ".html" }

        try {
            Export-WingetHtmlReport -Data $updatesAvailable.ToArray() -ReportTitle "Updates" -FilePath $exportPath
            if (Test-Path $exportPath) {
                Write-Host "[OK] Report successfully saved to $exportPath" -ForegroundColor Green
                Invoke-Item $exportPath
            }
        } catch {
            Write-Host "[FAIL] Failed to generate HTML report: $_" -ForegroundColor Red
        }
    }

    # Interactive selection
    if ($IWantToLiterallyUpdateAllFuckingResults) {
        $selectedPackages = @($updatesAvailable)
    }
    else {
        try {
            $displayToPkg = @{}
            $displayLines = foreach ($u in $updatesAvailable) {
                $name = ConvertTo-SpectreEscaped $u.Name
                $id = ConvertTo-SpectreEscaped $u.Id
                $from = ConvertTo-SpectreEscaped ([string]$u.InstalledVersion)
                $to = ConvertTo-SpectreEscaped ([string]$u.AvailableVersion)
                $display = "$name ($id) [grey]$from[/] -> [green]$to[/]"
                if ($u.Source -and $u.Source -ne 'winget') { $display += " [magenta]$(ConvertTo-SpectreEscaped $u.Source)[/]" }
                $displayToPkg[$display] = $u
                $display
            }

            $selectedLines = Read-SpectreMultiSelection -Title "[cyan]Select packages to update (Space to toggle, Enter to confirm)[/]" `
                -Choices $displayLines `
                -PageSize 20 `
                -Color "Green"

            if (@($selectedLines).Count -eq 0) {
                Write-Host "No packages selected." -ForegroundColor Yellow
                return
            }

            $selectedPackages = @($selectedLines | ForEach-Object { $displayToPkg[$_] })
        }
        catch {
            Write-Warning "Interactive selection error: $_"
            Write-Host "Packages with updates available:" -ForegroundColor Cyan
            $updatesAvailable | ForEach-Object {
                Write-Host " - $($_.Id) ($($_.InstalledVersion) -> $($_.AvailableVersion))" -ForegroundColor White
            }
            Write-Host ""
            Write-Host "To update all: " -ForegroundColor Cyan -NoNewline
            Write-Host "Get-WingetUpdates -IWantToLiterallyUpdateAllFuckingResults" -ForegroundColor Yellow
            return
        }
    }

    Write-Host ""
    Write-Host "Updating " -ForegroundColor Cyan -NoNewline
    Write-Host "$($selectedPackages.Count)" -ForegroundColor White -NoNewline
    Write-Host " package(s)..." -ForegroundColor Cyan
    Write-Host ""

    $successCount = 0
    $failCount = 0
    $rebootNeeded = $false
    $failures = [System.Collections.Generic.List[PSCustomObject]]::new()

    # Default to silent updates unless the caller chose a mode
    $installOptions = @{
        Mode = $(if ($Mode) { $Mode } else { 'Silent' })
        Scope = $Scope; Architecture = $Architecture; Override = $Override; Location = $Location
        Force = [bool]$ForceInstall; SkipDependencies = [bool]$SkipDependencies; AllowHashMismatch = [bool]$AllowHashMismatch
    }

    Invoke-WingetAutoSnapshot -Reason 'Get-WingetUpdates'

    $i = 0
    foreach ($pkg in $selectedPackages) {
        $i++
        Write-Host ">>> [$i/$($selectedPackages.Count)] Updating: " -ForegroundColor Magenta -NoNewline
        Write-Host "$($pkg.Id) " -ForegroundColor White -NoNewline
        Write-Host "$($pkg.InstalledVersion) -> $($pkg.AvailableVersion)" -ForegroundColor DarkGray

        $result = Invoke-WingetPackageAction -Action Update -Id $pkg.Id -Source $pkg.Source -Options $installOptions

        if ($result.Succeeded) {
            Write-Host "[OK] Successfully updated " -ForegroundColor Green -NoNewline
            Write-Host $pkg.Id -ForegroundColor White
            if ($result.RebootRequired) { $rebootNeeded = $true }
            $successCount++
        }
        else {
            Write-Host "[FAIL] Failed to update " -ForegroundColor Red -NoNewline
            Write-Host $pkg.Id -ForegroundColor White -NoNewline
            Write-Host " ($($result.Message))" -ForegroundColor Red
            $failures.Add($result)
            $failCount++
        }
        Write-Host ""
    }

    Write-Host ("=" * 60) -ForegroundColor Green
    Write-Host "Update Complete" -ForegroundColor Green
    Write-Host ("=" * 60) -ForegroundColor Green
    Write-Host "Updated: " -ForegroundColor Green -NoNewline
    Write-Host $successCount -ForegroundColor White -NoNewline
    Write-Host " | Failed: " -ForegroundColor Red -NoNewline
    Write-Host $failCount -ForegroundColor White
    foreach ($f in $failures) {
        Write-Host " - $($f.Id): $($f.Message)" -ForegroundColor DarkGray
    }
    if ($rebootNeeded) {
        Write-Host "A restart is required to finish at least one update." -ForegroundColor Yellow
    }

    # Clear cache after updates
    if (Test-Path $cacheFile) {
        Remove-Item $cacheFile -Force
    }
}