Public/Remove-WingetRecent.ps1
|
function Remove-WingetRecent { <# .SYNOPSIS Uninstall recently installed winget packages. .DESCRIPTION Shows packages installed in the last X days and allows interactive selection of which packages to uninstall. Install dates come from the Windows uninstall registry. Each installed package (from the WinGet COM API) is matched to its registry entry by registry key for Add/Remove Programs entries, then by exact display name, then by a cautious prefix match. Packages that cannot be matched confidently are left out rather than guessed, because this list feeds an uninstall. .PARAMETER Days Number of days to look back for recently installed packages. Default is 1 day. .EXAMPLE Remove-WingetRecent Shows packages installed in the last day and allows you to select which to uninstall. .EXAMPLE Remove-WingetRecent -Days 7 Shows packages installed in the last 7 days. #> [CmdletBinding()] param( [Parameter()] [ValidateRange(1, 3650)] [int]$Days = 1 ) $plural = if ($Days -ne 1) { 's' } else { '' } Write-Host "Searching for packages installed in the last " -ForegroundColor Cyan -NoNewline Write-Host "$Days day$plural..." -ForegroundColor Yellow # --- Registry install dates --- $uninstallPaths = @( 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' ) $datesByKey = @{} # registry key name (PSChildName) -> date $datesByName = @{} # DisplayName -> date (case-insensitive) foreach ($path in $uninstallPaths) { foreach ($entry in (Get-ItemProperty $path -ErrorAction SilentlyContinue)) { if (-not $entry.DisplayName -or -not $entry.InstallDate) { continue } try { $date = [DateTime]::ParseExact([string]$entry.InstallDate, 'yyyyMMdd', [Globalization.CultureInfo]::InvariantCulture) } catch { continue } if (-not $datesByKey.ContainsKey($entry.PSChildName)) { $datesByKey[$entry.PSChildName] = $date } if (-not $datesByName.ContainsKey($entry.DisplayName)) { $datesByName[$entry.DisplayName] = $date } } } Write-Host "Found installation dates for $($datesByName.Count) programs" -ForegroundColor DarkGray Write-Host "" # --- Installed packages (COM API: full names, no console-width truncation, any locale) --- try { $installed = @(Microsoft.WinGet.Client\Get-WinGetPackage -ErrorAction Stop) } catch { Write-Error "Failed to get installed packages: $_" return } $cutoffDate = (Get-Date).AddDays(-$Days).Date $installedPackages = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($pkg in $installed) { if (-not $pkg.Id) { continue } $installDate = $null # 1. ARP\<scope>\<arch>\<registry key> IDs point straight at the registry entry if ($pkg.Id -match '^ARP\\.+\\([^\\]+)$' -and $datesByKey.ContainsKey($Matches[1])) { $installDate = $datesByKey[$Matches[1]] } # 2. Exact display name elseif ($pkg.Name -and $datesByName.ContainsKey($pkg.Name)) { $installDate = $datesByName[$pkg.Name] } # 3. Registry name starts with the package name ("7-Zip" -> "7-Zip 24.08 (x64)"). # Short names ("Go", "Git") are skipped: too likely to hit an unrelated program. elseif ($pkg.Name -and $pkg.Name.Length -ge 5) { $candidates = @($datesByName.Keys | Where-Object { $_.StartsWith($pkg.Name, [StringComparison]::OrdinalIgnoreCase) }) if ($candidates.Count -eq 1) { $installDate = $datesByName[$candidates[0]] } } if ($installDate -and $installDate -ge $cutoffDate) { $installedPackages.Add([PSCustomObject]@{ Id = $pkg.Id Name = $pkg.Name Version = $pkg.InstalledVersion Source = $pkg.Source InstallDate = $installDate }) } } if ($installedPackages.Count -eq 0) { Write-Warning "No packages installed in the last $Days day$plural." Write-Host "Note: Only packages with registry install dates can be tracked." -ForegroundColor DarkGray return } $installedPackages = @($installedPackages | Sort-Object -Property InstallDate -Descending) Write-Host "Found " -ForegroundColor Green -NoNewline Write-Host "$($installedPackages.Count)" -ForegroundColor White -NoNewline Write-Host " package(s) installed in the last $Days day$plural" -ForegroundColor Green Write-Host "" try { $displayToPkg = @{} $displayLines = foreach ($p in $installedPackages) { $display = "($($p.InstallDate.ToString('yyyy-MM-dd'))) $(ConvertTo-SpectreEscaped $p.Name) [grey]$(ConvertTo-SpectreEscaped $p.Id)[/]" $displayToPkg[$display] = $p $display } $selectedLines = Read-SpectreMultiSelection -Title "[red]Select packages to UNINSTALL (Space to toggle, Enter to confirm)[/]" ` -Choices $displayLines ` -PageSize 20 ` -Color "Red" } catch { Write-Warning "Interactive selection error: $_" Write-Host "Recently installed packages:" -ForegroundColor Cyan $installedPackages | ForEach-Object { Write-Host " - ($($_.InstallDate.ToString('yyyy-MM-dd'))) $($_.Name) [$($_.Id)]" -ForegroundColor White } return } if (@($selectedLines).Count -eq 0) { Write-Host "No packages selected." -ForegroundColor Yellow return } $selectedPackages = @($selectedLines | ForEach-Object { $displayToPkg[$_] }) Write-Host "" Write-Host "WARNING: " -ForegroundColor Red -NoNewline Write-Host "You are about to UNINSTALL " -ForegroundColor Yellow -NoNewline Write-Host "$($selectedPackages.Count)" -ForegroundColor White -NoNewline Write-Host " package(s):" -ForegroundColor Yellow Write-Host "" foreach ($p in $selectedPackages) { Write-Host " - " -ForegroundColor Red -NoNewline Write-Host "$($p.Name) ($($p.Id))" -ForegroundColor White } Write-Host "" Write-Host "Type " -NoNewline -ForegroundColor Yellow Write-Host "YES" -NoNewline -ForegroundColor Red Write-Host " to confirm uninstallation, or anything else to cancel: " -NoNewline -ForegroundColor Yellow $confirmation = Read-Host if ($confirmation -cne "YES") { Write-Host "Uninstallation cancelled." -ForegroundColor Green return } Invoke-WingetAutoSnapshot -Reason 'Remove-WingetRecent' Write-Host "" Write-Host ("=" * 60) -ForegroundColor Red Write-Host "Starting Uninstallation Process" -ForegroundColor Red Write-Host ("=" * 60) -ForegroundColor Red Write-Host "" $successCount = 0 $failCount = 0 foreach ($p in $selectedPackages) { Write-Host ">>> Uninstalling: " -ForegroundColor Magenta -NoNewline Write-Host $p.Id -ForegroundColor White $result = Invoke-WingetPackageAction -Action Uninstall -Id $p.Id -Source $p.Source if ($result.Succeeded) { Write-Host "[OK] Successfully uninstalled " -ForegroundColor Green -NoNewline Write-Host $p.Id -ForegroundColor White $successCount++ } else { Write-Host "[FAIL] Failed to uninstall " -ForegroundColor Red -NoNewline Write-Host $p.Id -ForegroundColor White -NoNewline Write-Host " ($($result.Message))" -ForegroundColor Red $failCount++ } Write-Host "" } Write-Host ("=" * 60) -ForegroundColor Green Write-Host "Uninstallation Complete" -ForegroundColor Green Write-Host ("=" * 60) -ForegroundColor Green Write-Host "Success: " -ForegroundColor Green -NoNewline Write-Host $successCount -ForegroundColor White -NoNewline Write-Host " | Failed: " -ForegroundColor Red -NoNewline Write-Host $failCount -ForegroundColor White } |