Public/Get-WingetHistory.ps1

function Get-WingetHistory {
    <#
    .SYNOPSIS
        Display package installation history as a timeline.
 
    .DESCRIPTION
        Reads installation history from the Windows Registry and winget logs to
        display a chronological timeline of package installations, updates, and
        removals on the system.
 
        Supports filtering by date range, searching by package name, and exporting
        to HTML for audit purposes.
 
    .PARAMETER Days
        Show history from the last N days. Default: 30.
 
    .PARAMETER All
        Show all available history (no date filter).
 
    .PARAMETER Search
        Filter history entries by package name or ID.
 
    .PARAMETER ExportHtml
        Generate an HTML timeline report.
 
    .PARAMETER ExportJson
        Output raw history data as JSON for programmatic use.
 
    .EXAMPLE
        Get-WingetHistory
        Shows installations from the last 30 days.
 
    .EXAMPLE
        Get-WingetHistory -Days 7
        Shows what was installed in the last week.
 
    .EXAMPLE
        Get-WingetHistory -Search "python"
        Shows all Python-related installation events.
 
    .EXAMPLE
        Get-WingetHistory -All -ExportHtml
        Full history exported as an HTML report.
 
    .LINK
        https://github.com/thebubbsy/WingetBatch
    #>


    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateRange(1, 3650)]
        [int]$Days = 30,

        [Parameter()]
        [switch]$All,

        [Parameter()]
        [string]$Search,

        [Parameter()]
        [switch]$ExportHtml,

        [Parameter()]
        [switch]$ExportJson
    )

    begin {
        if (-not (Get-Module -Name PwshSpectreConsole)) {
            if (Get-Module -ListAvailable -Name PwshSpectreConsole) {
                Import-Module PwshSpectreConsole -ErrorAction SilentlyContinue
            }
        }
    }

    process {
        Write-Host ""
        Write-Host " Scanning installation history..." -ForegroundColor Cyan

        $history = [System.Collections.Generic.List[PSCustomObject]]::new()
        $cutoffDate = if ($All) { [DateTime]::MinValue } else { (Get-Date).AddDays(-$Days) }

        # Source 1: Windows Registry (Uninstall keys)
        $regPaths = @(
            'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
            'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
            'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
        )

        foreach ($regPath in $regPaths) {
            $entries = Get-ItemProperty -Path $regPath -ErrorAction SilentlyContinue
            foreach ($entry in $entries) {
                if (-not $entry.DisplayName) { continue }
                if (-not $entry.InstallDate) { continue }

                try {
                    $installDate = [DateTime]::ParseExact($entry.InstallDate, 'yyyyMMdd', $null)
                } catch {
                    try { $installDate = [DateTime]::Parse($entry.InstallDate) }
                    catch { continue }
                }

                if ($installDate -lt $cutoffDate) { continue }

                # Apply search filter
                if ($Search) {
                    $matchText = "$($entry.DisplayName) $($entry.Publisher) $($entry.PSChildName)"
                    if ($matchText -notlike "*$Search*") { continue }
                }

                $scope = if ($regPath -like 'HKCU:*') { 'User' } else { 'Machine' }

                $history.Add([PSCustomObject]@{
                    Date        = $installDate
                    Action      = 'Installed'
                    Name        = $entry.DisplayName
                    Version     = if ($entry.DisplayVersion) { $entry.DisplayVersion } else { '' }
                    Publisher   = if ($entry.Publisher) { $entry.Publisher } else { '' }
                    Scope       = $scope
                    Source      = 'Registry'
                    SizeMB      = if ($entry.EstimatedSize) { [Math]::Round($entry.EstimatedSize / 1024, 1) } else { $null }
                })
            }
        }

        # Source 2: Winget log files (if available)
        $wingetLogDir = "$env:LOCALAPPDATA\Packages\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\LocalState\DiagOutputDir"
        if (Test-Path $wingetLogDir) {
            $logFiles = Get-ChildItem -Path $wingetLogDir -Filter "*.log" -ErrorAction SilentlyContinue |
                Where-Object { $_.LastWriteTime -ge $cutoffDate } |
                Sort-Object LastWriteTime -Descending |
                Select-Object -First 20

            foreach ($logFile in $logFiles) {
                try {
                    $logContent = Get-Content $logFile.FullName -Tail 100 -ErrorAction SilentlyContinue
                    foreach ($line in $logContent) {
                        # Parse winget log entries for install/update operations
                        if ($line -match '(\d{4}-\d{2}-\d{2})\s+.*(?:Installing|Updating|Uninstalling)\s+.*?(\S+\.\S+)') {
                            $logDate = [DateTime]::Parse($Matches[1])
                            if ($logDate -lt $cutoffDate) { continue }

                            $action = if ($line -match 'Installing') { 'Installed' }
                                      elseif ($line -match 'Updating') { 'Updated' }
                                      elseif ($line -match 'Uninstalling') { 'Removed' }
                                      else { 'Unknown' }

                            $pkgId = $Matches[2]
                            if ($Search -and $pkgId -notlike "*$Search*") { continue }

                            $history.Add([PSCustomObject]@{
                                Date      = $logDate
                                Action    = $action
                                Name      = $pkgId
                                Version   = ''
                                Publisher = ''
                                Scope     = ''
                                Source    = 'WingetLog'
                                SizeMB    = $null
                            })
                        }
                    }
                } catch {}
            }
        }

        # Sort by date descending
        $history = [System.Collections.Generic.List[PSCustomObject]]($history | Sort-Object Date -Descending)

        if ($history.Count -eq 0) {
            Write-Host " No installation history found" -ForegroundColor Yellow -NoNewline
            if (-not $All) { Write-Host " in the last $Days days" -ForegroundColor Yellow -NoNewline }
            Write-Host "." -ForegroundColor Yellow
            Write-Host ""
            return
        }

        # JSON export
        if ($ExportJson) {
            $history | ConvertTo-Json -Depth 5
            return
        }

        # Display timeline
        $dateRange = if ($All) { "all time" } else { "last $Days days" }
        Write-Host " Found " -ForegroundColor White -NoNewline
        Write-Host "$($history.Count)" -ForegroundColor Green -NoNewline
        Write-Host " events ($dateRange)" -ForegroundColor White
        Write-Host ""

        # Group by date for timeline display
        $grouped = $history | Group-Object { $_.Date.ToString('yyyy-MM-dd') } | Sort-Object Name -Descending

        foreach ($dayGroup in $grouped) {
            $dateStr = $dayGroup.Name
            $dayOfWeek = ([DateTime]::Parse($dateStr)).DayOfWeek

            Write-Host " $dateStr" -ForegroundColor Cyan -NoNewline
            Write-Host " ($dayOfWeek)" -ForegroundColor DarkGray
            Write-Host " $('─' * 50)" -ForegroundColor DarkGray

            foreach ($event in $dayGroup.Group) {
                $actionColor = switch ($event.Action) {
                    'Installed' { 'Green' }
                    'Updated' { 'Yellow' }
                    'Removed' { 'Red' }
                    default { 'Gray' }
                }
                $actionIcon = switch ($event.Action) {
                    'Installed' { '+' }
                    'Updated' { '~' }
                    'Removed' { '-' }
                    default { '?' }
                }

                Write-Host " [$actionIcon] " -ForegroundColor $actionColor -NoNewline
                Write-Host $event.Name -ForegroundColor White -NoNewline

                if ($event.Version) {
                    Write-Host " v$($event.Version)" -ForegroundColor Green -NoNewline
                }
                if ($event.Publisher) {
                    Write-Host " ($($event.Publisher))" -ForegroundColor DarkGray -NoNewline
                }
                if ($event.SizeMB) {
                    Write-Host " [$($event.SizeMB) MB]" -ForegroundColor DarkGray -NoNewline
                }
                Write-Host ""
            }
            Write-Host ""
        }

        # Summary stats
        $installCount = ($history | Where-Object { $_.Action -eq 'Installed' }).Count
        $updateCount = ($history | Where-Object { $_.Action -eq 'Updated' }).Count
        $removeCount = ($history | Where-Object { $_.Action -eq 'Removed' }).Count
        $totalSize = ($history | Where-Object { $_.SizeMB } | Measure-Object -Property SizeMB -Sum).Sum

        Write-Host " Summary: " -ForegroundColor Cyan -NoNewline
        Write-Host "$installCount installed" -ForegroundColor Green -NoNewline
        Write-Host " | " -ForegroundColor DarkGray -NoNewline
        Write-Host "$updateCount updated" -ForegroundColor Yellow -NoNewline
        Write-Host " | " -ForegroundColor DarkGray -NoNewline
        Write-Host "$removeCount removed" -ForegroundColor Red
        if ($totalSize) {
            Write-Host " Total size: " -ForegroundColor Cyan -NoNewline
            Write-Host "$([Math]::Round($totalSize, 1)) MB" -ForegroundColor White
        }
        Write-Host ""

        # HTML Export
        if ($ExportHtml) {
            $timestamp = (Get-Date).ToString("yyyyMMdd_HHmmss")
            $exportPath = Join-Path $env:TEMP "WingetBatch_History_$timestamp.html"
            try {
                Export-WingetHtmlReport -Data $history -ReportTitle "Installation History ($dateRange)" -FilePath $exportPath
                if (Test-Path $exportPath) {
                    Write-Host " [OK] Report saved: $exportPath" -ForegroundColor Green
                    Invoke-Item $exportPath
                }
            } catch {
                Write-Host " [FAIL] Could not generate report: $_" -ForegroundColor Red
            }
        }
    }
}