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'] = (Get-WinGetVersion -ErrorAction Stop).ToString()
                $data['WingetHealthy'] = $true
            } catch {
                $data['WingetVersion'] = 'N/A'
                $data['WingetHealthy'] = $false
            }

            # Installed packages
            $installed = 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, AvailableVersion

            # 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
            $wingetDir = "$env:LOCALAPPDATA\Packages\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\LocalState\Microsoft.Winget.Source_8wekyb3d8bbwe\winget"
            if (Test-Path "$wingetDir\source.db") {
                $dbAge = (Get-Date) - (Get-Item "$wingetDir\source.db").LastWriteTime
                $data['IndexAge'] = "$([Math]::Floor($dbAge.TotalHours))h ago"
                $data['IndexStale'] = $dbAge.TotalDays -gt 7
            } else {
                $data['IndexAge'] = 'Unknown'
                $data['IndexStale'] = $false
            }

            # 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
            [Console]::Clear()

            $border = '═' * 62
            $thinBorder = '─' * 62

            Write-Host ""
            Write-Host " ╔$border╗" -ForegroundColor Cyan
            Write-Host " ║" -ForegroundColor Cyan -NoNewline
            Write-Host " WINGETBATCH LIVE DASHBOARD" -ForegroundColor White -NoNewline
            $padRight = 62 - 30
            Write-Host (" " * $padRight) -NoNewline
            Write-Host "║" -ForegroundColor Cyan
            Write-Host " ║" -ForegroundColor Cyan -NoNewline
            $timeStr = " Last refresh: $($data.Timestamp.ToString('HH:mm:ss'))"
            Write-Host $timeStr -ForegroundColor DarkGray -NoNewline
            $padRight2 = 62 - $timeStr.Length
            Write-Host (" " * $padRight2) -NoNewline
            Write-Host "║" -ForegroundColor Cyan
            Write-Host " ╠$border╣" -ForegroundColor Cyan

            # System Health Row
            Write-Host " ║" -ForegroundColor Cyan -NoNewline
            Write-Host " SYSTEM HEALTH" -ForegroundColor Yellow -NoNewline
            Write-Host (" " * (62 - 15)) -NoNewline
            Write-Host "║" -ForegroundColor Cyan

            $wingetStatus = if ($data.WingetHealthy) { "[OK]" } else { "[!!]" }
            $wingetColor = if ($data.WingetHealthy) { 'Green' } else { 'Red' }
            Write-Host " ║" -ForegroundColor Cyan -NoNewline
            Write-Host " Winget Engine: " -ForegroundColor Gray -NoNewline
            Write-Host "$wingetStatus v$($data.WingetVersion)" -ForegroundColor $wingetColor -NoNewline
            Write-Host (" " * (62 - 23 - $wingetStatus.Length - $data.WingetVersion.ToString().Length - 2)) -NoNewline
            Write-Host "║" -ForegroundColor Cyan

            $indexStatus = if ($data.IndexStale) { "[STALE]" } else { "[FRESH]" }
            $indexColor = if ($data.IndexStale) { 'Yellow' } else { 'Green' }
            Write-Host " ║" -ForegroundColor Cyan -NoNewline
            Write-Host " Source Index: " -ForegroundColor Gray -NoNewline
            Write-Host "$indexStatus ($($data.IndexAge))" -ForegroundColor $indexColor -NoNewline
            $idxLen = 23 + $indexStatus.Length + $data.IndexAge.Length + 3
            Write-Host (" " * [Math]::Max(0, 62 - $idxLen)) -NoNewline
            Write-Host "║" -ForegroundColor Cyan

            $authStatus = if ($data.GitHubAuth) { "[AUTHENTICATED]" } else { "[ANONYMOUS]" }
            $authColor = if ($data.GitHubAuth) { 'Green' } else { 'DarkGray' }
            Write-Host " ║" -ForegroundColor Cyan -NoNewline
            Write-Host " GitHub API: " -ForegroundColor Gray -NoNewline
            Write-Host $authStatus -ForegroundColor $authColor -NoNewline
            $authLen = 23 + $authStatus.Length
            Write-Host (" " * [Math]::Max(0, 62 - $authLen)) -NoNewline
            Write-Host "║" -ForegroundColor Cyan

            Write-Host " ║" -ForegroundColor Cyan -NoNewline
            Write-Host (" " * 62) -NoNewline
            Write-Host "║" -ForegroundColor Cyan

            # Package Stats Row
            Write-Host " ║" -ForegroundColor Cyan -NoNewline
            Write-Host " PACKAGE STATISTICS" -ForegroundColor Yellow -NoNewline
            Write-Host (" " * (62 - 20)) -NoNewline
            Write-Host "║" -ForegroundColor Cyan

            Write-Host " ║" -ForegroundColor Cyan -NoNewline
            Write-Host " Total Installed: " -ForegroundColor Gray -NoNewline
            Write-Host "$($data.TotalPackages)" -ForegroundColor White -NoNewline
            $totalLen = 23 + $data.TotalPackages.ToString().Length
            Write-Host (" " * [Math]::Max(0, 62 - $totalLen)) -NoNewline
            Write-Host "║" -ForegroundColor Cyan

            $updateColor = if ($data.UpdatesAvailable -gt 0) { 'Yellow' } else { 'Green' }
            Write-Host " ║" -ForegroundColor Cyan -NoNewline
            Write-Host " Updates Pending: " -ForegroundColor Gray -NoNewline
            Write-Host "$($data.UpdatesAvailable)" -ForegroundColor $updateColor -NoNewline
            $updLen = 23 + $data.UpdatesAvailable.ToString().Length
            Write-Host (" " * [Math]::Max(0, 62 - $updLen)) -NoNewline
            Write-Host "║" -ForegroundColor Cyan

            Write-Host " ║" -ForegroundColor Cyan -NoNewline
            Write-Host " Recent (7 days): " -ForegroundColor Gray -NoNewline
            Write-Host "$($data.RecentInstalls)" -ForegroundColor White -NoNewline
            $recLen = 23 + $data.RecentInstalls.ToString().Length
            Write-Host (" " * [Math]::Max(0, 62 - $recLen)) -NoNewline
            Write-Host "║" -ForegroundColor Cyan

            Write-Host " ║" -ForegroundColor Cyan -NoNewline
            Write-Host (" " * 62) -NoNewline
            Write-Host "║" -ForegroundColor Cyan

            # Source Breakdown
            Write-Host " ║" -ForegroundColor Cyan -NoNewline
            Write-Host " SOURCE BREAKDOWN" -ForegroundColor Yellow -NoNewline
            Write-Host (" " * (62 - 18)) -NoNewline
            Write-Host "║" -ForegroundColor Cyan

            foreach ($src in $data.Sources.Keys | Sort-Object) {
                $count = $data.Sources[$src]
                $srcColor = if ($src -eq 'msstore') { 'Magenta' } elseif ($src -eq 'winget') { 'Cyan' } else { 'Gray' }
                $line = " $($src): $($count)"
                Write-Host " ║" -ForegroundColor Cyan -NoNewline
                Write-Host " " -NoNewline
                Write-Host "$src" -ForegroundColor $srcColor -NoNewline
                Write-Host ": $count" -ForegroundColor White -NoNewline
                Write-Host (" " * [Math]::Max(0, 62 - $line.Length - 4)) -NoNewline
                Write-Host "║" -ForegroundColor Cyan
            }

            Write-Host " ║" -ForegroundColor Cyan -NoNewline
            Write-Host (" " * 62) -NoNewline
            Write-Host "║" -ForegroundColor Cyan

            # Pending Updates (top 5)
            if ($data.UpdatesAvailable -gt 0) {
                Write-Host " ║" -ForegroundColor Cyan -NoNewline
                Write-Host " PENDING UPDATES (top 5)" -ForegroundColor Yellow -NoNewline
                Write-Host (" " * (62 - 25)) -NoNewline
                Write-Host "║" -ForegroundColor Cyan

                $shown = 0
                foreach ($upd in $data.UpdateList) {
                    if ($shown -ge 5) { break }
                    $line = " $($upd.Id): $($upd.InstalledVersion) -> $($upd.AvailableVersion)"
                    if ($line.Length -gt 60) { $line = $line.Substring(0, 57) + "..." }
                    Write-Host " ║" -ForegroundColor Cyan -NoNewline
                    Write-Host $line -ForegroundColor DarkGray -NoNewline
                    Write-Host (" " * [Math]::Max(0, 62 - $line.Length)) -NoNewline
                    Write-Host "║" -ForegroundColor Cyan
                    $shown++
                }

                if ($data.UpdatesAvailable -gt 5) {
                    Write-Host " ║" -ForegroundColor Cyan -NoNewline
                    Write-Host " ... and $($data.UpdatesAvailable - 5) more" -ForegroundColor DarkGray -NoNewline
                    $moreLine = " ... and $($data.UpdatesAvailable - 5) more"
                    Write-Host (" " * [Math]::Max(0, 62 - $moreLine.Length)) -NoNewline
                    Write-Host "║" -ForegroundColor Cyan
                }

                Write-Host " ║" -ForegroundColor Cyan -NoNewline
                Write-Host (" " * 62) -NoNewline
                Write-Host "║" -ForegroundColor Cyan
            }

            # Footer
            Write-Host " ╠$border╣" -ForegroundColor Cyan
            Write-Host " ║" -ForegroundColor Cyan -NoNewline
            $footer = " Refresh: ${RefreshInterval}s | Ctrl+C to exit | Get-WingetUpdates to update"
            Write-Host $footer -ForegroundColor DarkGray -NoNewline
            Write-Host (" " * [Math]::Max(0, 62 - $footer.Length)) -NoNewline
            Write-Host "║" -ForegroundColor Cyan
            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)
    }
}