bin/clean.ps1
|
# WinMole - Deep System Cleanup # Cleans temp files, browser caches, dev caches, app caches, Recycle Bin param( [switch]$DryRun, [switch]$Debug, [switch]$Whitelist ) $ErrorActionPreference = 'SilentlyContinue' . "$PSScriptRoot\..\lib\core.ps1" # ── Whitelist management ────────────────────────────────────────────────────── if ($Whitelist) { if (-not (Test-Path $script:WINMOLE_CONFIG)) { [void][System.IO.Directory]::CreateDirectory($script:WINMOLE_CONFIG) } if (-not (Test-Path $script:WHITELIST_FILE)) { "# WinMole Clean Whitelist - one glob pattern per line" | Set-Content $script:WHITELIST_FILE } Write-SpectreHost " [deepskyblue1]Opening whitelist:[/] $(Esc $script:WHITELIST_FILE)" Start-Process notepad $script:WHITELIST_FILE return } $wl = Get-Whitelist # ── Scan category builder ───────────────────────────────────────────────────── function New-Category { param([string]$Name, [string[]]$Paths, [switch]$RequiresAdmin) $exists = $Paths | Where-Object { Test-Path $_ } [pscustomobject]@{ Name = $Name Paths = @($exists) RequiresAdmin = $RequiresAdmin.IsPresent Size = [long]0 FileCount = 0 } } $isAdmin = Test-IsAdmin # ── Define all cleanup categories ───────────────────────────────────────────── $categories = @( [pscustomobject]@{ Name = 'User temp files' Paths = @( $env:TEMP, "$env:LOCALAPPDATA\Temp", "$env:WINDIR\Temp" ) RequiresAdmin = $false IsRecycleBin = $false Size = [long]0 FileCount = 0 } [pscustomobject]@{ Name = 'Browser cache (Chrome, Edge, Firefox, Brave)' Paths = @( "$env:LOCALAPPDATA\Google\Chrome\User Data\Default\Cache", "$env:LOCALAPPDATA\Google\Chrome\User Data\Default\Code Cache", "$env:LOCALAPPDATA\Google\Chrome\User Data\Default\GPUCache", "$env:LOCALAPPDATA\Microsoft\Edge\User Data\Default\Cache", "$env:LOCALAPPDATA\Microsoft\Edge\User Data\Default\Code Cache", "$env:LOCALAPPDATA\Microsoft\Edge\User Data\Default\GPUCache", "$env:APPDATA\Mozilla\Firefox\Profiles", "$env:LOCALAPPDATA\BraveSoftware\Brave-Browser\User Data\Default\Cache", "$env:LOCALAPPDATA\BraveSoftware\Brave-Browser\User Data\Default\Code Cache" ) RequiresAdmin = $false IsRecycleBin = $false Size = [long]0 FileCount = 0 } [pscustomobject]@{ Name = 'Developer tools (npm, pip, NuGet, Maven, Gradle, .NET)' Paths = @( "$env:APPDATA\npm-cache", "$env:LOCALAPPDATA\npm-cache", "$env:USERPROFILE\.npm\_cacache", "$env:USERPROFILE\.pip\cache", "$env:LOCALAPPDATA\pip\Cache", "$env:USERPROFILE\.nuget\packages\.cache", "$env:LOCALAPPDATA\NuGet\Cache", "$env:LOCALAPPDATA\NuGet\v3-cache", "$env:USERPROFILE\.m2\repository\.cache", "$env:USERPROFILE\.gradle\caches", "$env:USERPROFILE\.dotnet\toolResolverCache", "$env:LOCALAPPDATA\Microsoft\dotnet\toolResolverCache", "$env:USERPROFILE\.cargo\registry\src", "$env:USERPROFILE\.cache\yarn", "$env:LOCALAPPDATA\Yarn\Berry\cache", "$env:USERPROFILE\.pnpm-store", "$env:LOCALAPPDATA\pnpm\store" ) RequiresAdmin = $false IsRecycleBin = $false Size = [long]0 FileCount = 0 } [pscustomobject]@{ Name = 'Windows Update cache' Paths = @( "$env:WINDIR\SoftwareDistribution\Download", "$env:WINDIR\SoftwareDistribution\DataStore\Logs" ) RequiresAdmin = $true IsRecycleBin = $false Size = [long]0 FileCount = 0 } [pscustomobject]@{ Name = 'Windows logs and temp files' Paths = @( "$env:LOCALAPPDATA\CrashDumps", "$env:LOCALAPPDATA\Microsoft\Windows\WER", "$env:PROGRAMDATA\Microsoft\Windows\WER", "$env:WINDIR\Logs", "$env:WINDIR\Prefetch" ) RequiresAdmin = $true IsRecycleBin = $false Size = [long]0 FileCount = 0 } [pscustomobject]@{ Name = 'App-specific cache (Spotify, Slack, VS Code, Teams)' Paths = @( "$env:LOCALAPPDATA\Spotify\Storage", "$env:APPDATA\Spotify\Storage", "$env:APPDATA\Slack\Cache", "$env:APPDATA\Slack\Code Cache", "$env:APPDATA\Code\Cache", "$env:APPDATA\Code\Code Cache", "$env:APPDATA\Code\CachedData", "$env:APPDATA\Code\User\workspaceStorage", "$env:LOCALAPPDATA\Microsoft\Teams\Cache", "$env:LOCALAPPDATA\Microsoft\Teams\Code Cache", "$env:APPDATA\Discord\Cache", "$env:APPDATA\Discord\Code Cache" ) RequiresAdmin = $false IsRecycleBin = $false Size = [long]0 FileCount = 0 } [pscustomobject]@{ Name = 'Thumbnail and icon cache' Paths = @( "$env:LOCALAPPDATA\Microsoft\Windows\Explorer" ) RequiresAdmin = $false IsRecycleBin = $false Size = [long]0 FileCount = 0 } [pscustomobject]@{ Name = 'Recycle Bin' Paths = @() # handled specially RequiresAdmin = $false IsRecycleBin = $true Size = [long]0 FileCount = 0 } ) # ── Scan: calculate sizes in parallel ───────────────────────────────────────── Write-Header -Title 'WinMole Clean' -Sub $(if ($DryRun) { '(dry run)' } else { '' }) Write-Host " Scanning cache directories...`n" $allPaths = $categories | Where-Object { -not (Get-OptionalPropertyValue -InputObject $_ -Name 'IsRecycleBin' -Default $false) } | ForEach-Object { $_.Paths } | Where-Object { (Test-Path $_) -and (-not (Test-Whitelisted $_ $wl)) } $sizes = Get-DirectorySizes -Paths @($allPaths) foreach ($cat in $categories) { if (Get-OptionalPropertyValue -InputObject $cat -Name 'IsRecycleBin' -Default $false) { # Measure Recycle Bin via Shell try { $shell = New-Object -ComObject Shell.Application $bin = $shell.Namespace(0x0A) $sz = [long]0 foreach ($item in $bin.Items()) { try { $sz += $item.Size } catch {} } $cat.Size = $sz } catch { $cat.Size = 0 } continue } $cat.Size = ($cat.Paths | ForEach-Object { if ($sizes.ContainsKey($_)) { $sizes[$_] } else { 0 } } | Measure-Object -Sum).Sum } # ── Display scan results ─────────────────────────────────────────────────────── $totalSize = [long]0 foreach ($cat in $categories) { $skip = $cat.RequiresAdmin -and -not $isAdmin $tag = if ($skip) { ' (needs admin)' } else { '' } Write-Success -Message " $($cat.Name)$tag" -Right (Format-Bytes -Bytes $cat.Size) $totalSize += $cat.Size } Write-Host "" Write-Summary -Label 'Estimated space to free:' -Value (Format-Bytes -Bytes $totalSize) if ($DryRun) { if ($Debug) { Write-SpectreHost " [grey][[debug]] Paths that would be cleaned:[/]" foreach ($cat in $categories) { foreach ($p in $cat.Paths) { if (Test-Path $p) { Write-SpectreHost " [grey]$(Esc $p)[/]" } } } Write-Host "" } Write-Info "Dry run complete. No files were deleted." return } # ── Confirm ─────────────────────────────────────────────────────────────────── if (-not (Confirm-Action -Message 'Proceed with cleanup?')) { Write-SpectreHost " [grey]Cancelled.[/]" Write-Host "" return } # ── Execute cleanup ─────────────────────────────────────────────────────────── Write-Host "" $freed = [long]0 foreach ($cat in $categories) { if ($cat.RequiresAdmin -and -not $isAdmin) { Write-Warn "$($cat.Name) — skipped (requires admin)" continue } if (Get-OptionalPropertyValue -InputObject $cat -Name 'IsRecycleBin' -Default $false) { try { Clear-RecycleBin -Force -ErrorAction SilentlyContinue $freed += $cat.Size Write-Success -Message 'Recycle Bin' -Right (Format-Bytes -Bytes $cat.Size) Write-OpLog "Emptied Recycle Bin (~$(Format-Bytes $cat.Size))" } catch { Write-Warn "Recycle Bin — could not empty" } continue } $catFreed = [long]0 foreach ($path in $cat.Paths) { if (Test-Whitelisted $path $wl) { if ($Debug) { Write-Info "Whitelisted: $path" } continue } $catFreed += Remove-ItemSafe -Path $path -Recurse if ($Debug) { Write-Info "Removed: $path" } } Write-OpLog "Cleaned '$($cat.Name)': $(Format-Bytes $catFreed)" Write-Success -Message $cat.Name -Right (Format-Bytes -Bytes $catFreed) $freed += $catFreed } # ── Thumbnail cache: delete thumbcache_*.db files ───────────────────────────── $thumbDir = "$env:LOCALAPPDATA\Microsoft\Windows\Explorer" if (Test-Path $thumbDir) { $thumbSize = [long]0 foreach ($f in [System.IO.Directory]::EnumerateFiles($thumbDir, 'thumbcache_*.db')) { try { $info = [System.IO.FileInfo]::new($f) $thumbSize += $info.Length Remove-Item -LiteralPath $f -Force -ErrorAction SilentlyContinue } catch {} } if ($thumbSize -gt 0) { Write-Success -Message 'Thumbnail cache' -Right (Format-Bytes -Bytes $thumbSize) $freed += $thumbSize } } Write-Host "" $freeNow = [long]0 try { $drive = Split-Path -Qualifier $env:SystemDrive $di = [System.IO.DriveInfo]::new($drive) $freeNow = $di.AvailableFreeSpace } catch {} $summary = "Space freed: $(Format-Bytes $freed)" if ($freeNow -gt 0) { $summary += " | Free space now: $(Format-Bytes $freeNow)" } Write-Summary -Label $summary -Value '' |