Public/Watch-WingetPackages.ps1

function Watch-WingetPackages {
    <#
    .SYNOPSIS
        Live terminal dashboard for monitoring winget package state.
 
    .DESCRIPTION
        Displays a real-time refreshing dashboard showing:
        - Total installed packages count
        - Packages with available updates
        - Recently installed packages (last 7 days)
        - Source health status
        - Disk usage by package source
        - System winget version and health
 
        Refreshes automatically at a configurable interval. Press Ctrl+C to exit.
 
    .PARAMETER RefreshInterval
        Seconds between dashboard refreshes. Default: 30.
 
    .PARAMETER Once
        Display the dashboard once and exit (no refresh loop).
 
    .EXAMPLE
        Watch-WingetPackages
        Shows the live dashboard, refreshing every 30 seconds.
 
    .EXAMPLE
        Watch-WingetPackages -RefreshInterval 60
        Refresh every 60 seconds.
 
    .EXAMPLE
        Watch-WingetPackages -Once
        Show dashboard once (useful for scripts/CI).
 
    .LINK
        https://github.com/thebubbsy/WingetBatch
    #>


    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateRange(5, 3600)]
        [int]$RefreshInterval = 30,

        [Parameter()]
        [switch]$Once
    )

    begin {
        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."
                return
            }
        }
        if (-not (Get-Module -Name PwshSpectreConsole)) {
            if (Get-Module -ListAvailable -Name PwshSpectreConsole) {
                Import-Module PwshSpectreConsole -ErrorAction SilentlyContinue
            }
        }
    }

    process {
        function Get-DashboardData {
            $data = @{}

            # Winget version
            try {
                $data['WingetVersion'] = (Microsoft.WinGet.Client\Get-WinGetVersion -ErrorAction Stop).ToString()
                $data['WingetHealthy'] = $true
            } catch {
                $data['WingetVersion'] = 'N/A'
                $data['WingetHealthy'] = $false
            }

            # Installed packages
            $installed = @(Microsoft.WinGet.Client\Get-WinGetPackage -ErrorAction SilentlyContinue)
            $data['TotalPackages'] = if ($installed) { $installed.Count } else { 0 }

            # Updates available
            $updates = @()
            if ($installed) {
                $updates = @($installed | Where-Object { $_.IsUpdateAvailable })
            }
            $data['UpdatesAvailable'] = $updates.Count
            $data['UpdateList'] = $updates | Select-Object -First 10 Id, Name, InstalledVersion, @{ Name = 'AvailableVersion'; Expression = { @($_.AvailableVersions)[0] } }

            # Source breakdown
            $sourceGroups = @{}
            if ($installed) {
                foreach ($pkg in $installed) {
                    $src = if ($pkg.Source) { $pkg.Source } else { 'unknown' }
                    if (-not $sourceGroups.ContainsKey($src)) { $sourceGroups[$src] = 0 }
                    $sourceGroups[$src]++
                }
            }
            $data['Sources'] = $sourceGroups

            # Recently installed (from registry - last 7 days)
            $recentCount = 0
            try {
                $regPaths = @(
                    'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
                    'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
                    'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
                )
                $sevenDaysAgo = (Get-Date).AddDays(-7)
                foreach ($regPath in $regPaths) {
                    $entries = Get-ItemProperty -Path $regPath -ErrorAction SilentlyContinue |
                        Where-Object { $_.InstallDate }
                    foreach ($entry in $entries) {
                        try {
                            $installDate = [DateTime]::ParseExact($entry.InstallDate, 'yyyyMMdd', $null)
                            if ($installDate -ge $sevenDaysAgo) { $recentCount++ }
                        } catch {}
                    }
                }
            } catch {}
            $data['RecentInstalls'] = $recentCount

            # Winget source index age
            $indexAge = Get-WingetSourceIndexAge
            if ($null -ne $indexAge) {
                $data['IndexAge'] = if ($indexAge.TotalHours -lt 48) { "$([Math]::Floor($indexAge.TotalHours))h ago" } else { "$([Math]::Floor($indexAge.TotalDays))d ago" }
                $data['IndexStale'] = $indexAge.TotalDays -gt 7
            } else {
                $data['IndexAge'] = 'Unknown'
                $data['IndexStale'] = $null
            }
            # GitHub token status
            $data['GitHubAuth'] = $false
            try {
                $token = Get-WingetBatchGitHubToken
                if ($token) { $data['GitHubAuth'] = $true }
            } catch {}

            $data['Timestamp'] = Get-Date
            return $data
        }

        function Show-Dashboard {
            param($data)

            # Clear screen between refreshes (not possible when output is redirected, e.g. -Once in CI)
            if (-not $Once -and -not [Console]::IsOutputRedirected) {
                try { [Console]::Clear() } catch { }
            }

            $width = 62
            $border = '═' * $width

            # One boxed row made of colored segments, padded to the box width
            function Write-Row {
                param([object[]]$Parts = @())
                Write-Host " ║" -ForegroundColor Cyan -NoNewline
                $len = 0
                for ($i = 0; $i -lt $Parts.Count; $i += 2) {
                    $text = [string]$Parts[$i]
                    $room = $width - $len
                    if ($room -le 0) { break }
                    if ($text.Length -gt $room) { $text = $text.Substring(0, [Math]::Max(0, $room - 3)) + '...' }
                    Write-Host $text -ForegroundColor $Parts[$i + 1] -NoNewline
                    $len += $text.Length
                }
                Write-Host (' ' * [Math]::Max(0, $width - $len)) -NoNewline
                Write-Host "║" -ForegroundColor Cyan
            }

            Write-Host ""
            Write-Host " ╔$border╗" -ForegroundColor Cyan
            Write-Row @(' WINGETBATCH LIVE DASHBOARD', 'White')
            Write-Row @(" Last refresh: $($data.Timestamp.ToString('HH:mm:ss'))", 'DarkGray')
            Write-Host " ╠$border╣" -ForegroundColor Cyan

            # System Health
            Write-Row @(' SYSTEM HEALTH', 'Yellow')
            $ver = ([string]$data.WingetVersion).TrimStart('v')
            if ($data.WingetHealthy) { Write-Row @(' Winget Engine: ', 'Gray', "[OK] v$ver", 'Green') }
            else { Write-Row @(' Winget Engine: ', 'Gray', '[!!] unavailable', 'Red') }
            $indexStatus = if ($null -eq $data.IndexStale) { "[?]" } elseif ($data.IndexStale) { "[STALE]" } else { "[FRESH]" }
            Write-Row @(' Source Index: ', 'Gray', "$indexStatus ($($data.IndexAge))", $(if ($data.IndexStale) { 'Yellow' } else { 'Green' }))
            Write-Row @(' GitHub API: ', 'Gray', $(if ($data.GitHubAuth) { '[AUTHENTICATED]' } else { '[ANONYMOUS]' }), $(if ($data.GitHubAuth) { 'Green' } else { 'DarkGray' }))
            Write-Row

            # Package Stats
            Write-Row @(' PACKAGE STATISTICS', 'Yellow')
            Write-Row @(' Total Installed: ', 'Gray', "$($data.TotalPackages)", 'White')
            Write-Row @(' Updates Pending: ', 'Gray', "$($data.UpdatesAvailable)", $(if ($data.UpdatesAvailable -gt 0) { 'Yellow' } else { 'Green' }))
            Write-Row @(' Recent (7 days): ', 'Gray', "$($data.RecentInstalls)", 'White')
            Write-Row

            # Source Breakdown
            Write-Row @(' SOURCE BREAKDOWN', 'Yellow')
            foreach ($src in $data.Sources.Keys | Sort-Object) {
                $srcColor = if ($src -eq 'msstore') { 'Magenta' } elseif ($src -eq 'winget') { 'Cyan' } else { 'Gray' }
                $label = if ($src -eq 'unknown') { 'no source (ARP/MSIX)' } else { $src }
                Write-Row @(' ', 'Gray', $label, $srcColor, ": $($data.Sources[$src])", 'White')
            }
            Write-Row

            # Pending Updates (top 5)
            if ($data.UpdatesAvailable -gt 0) {
                Write-Row @(' PENDING UPDATES (top 5)', 'Yellow')
                foreach ($upd in @($data.UpdateList) | Select-Object -First 5) {
                    Write-Row @(" $($upd.Id): $($upd.InstalledVersion) -> $($upd.AvailableVersion)", 'DarkGray')
                }
                if ($data.UpdatesAvailable -gt 5) {
                    Write-Row @(" ... and $($data.UpdatesAvailable - 5) more", 'DarkGray')
                }
                Write-Row
            }

            # Footer
            Write-Host " ╠$border╣" -ForegroundColor Cyan
            $footer = if ($Once) { " Get-WingetUpdates to install updates" } else { " Refresh: ${RefreshInterval}s | Ctrl+C to exit | Get-WingetUpdates" }
            Write-Row @($footer, 'DarkGray')
            Write-Host " ╚$border╝" -ForegroundColor Cyan
            Write-Host ""
        }

        # Main loop
        do {
            $dashboardData = Get-DashboardData
            Show-Dashboard -data $dashboardData

            if ($Once) { break }

            Start-Sleep -Seconds $RefreshInterval
        } while ($true)
    }
}