From 6e0d850d6afcbbd57e46c439eb483f743d474d53 Mon Sep 17 00:00:00 2001 From: Bhadra Date: Thu, 8 Jan 2026 15:46:45 +0530 Subject: [PATCH] feat(windows): add Windows support Phase 2 - cleanup features Add cleanup modules and command scripts for Windows: - lib/clean: user, caches, dev, apps, system cleanup modules - bin/clean: deep cleanup orchestrator with dry-run and whitelist - bin/uninstall: interactive app uninstaller - bin/optimize: system optimization and health checks - bin/purge: project artifact cleanup All scripts support dry-run mode and follow safe deletion practices. --- .gitignore | 3 + windows/bin/clean.ps1 | 300 +++++++++++++++++ windows/bin/optimize.ps1 | 545 +++++++++++++++++++++++++++++++ windows/bin/purge.ps1 | 610 +++++++++++++++++++++++++++++++++++ windows/bin/uninstall.ps1 | 596 ++++++++++++++++++++++++++++++++++ windows/lib/clean/apps.ps1 | 429 ++++++++++++++++++++++++ windows/lib/clean/caches.ps1 | 385 ++++++++++++++++++++++ windows/lib/clean/dev.ps1 | 537 ++++++++++++++++++++++++++++++ windows/lib/clean/system.ps1 | 420 ++++++++++++++++++++++++ windows/lib/clean/user.ps1 | 349 ++++++++++++++++++++ 10 files changed, 4174 insertions(+) create mode 100644 windows/bin/clean.ps1 create mode 100644 windows/bin/optimize.ps1 create mode 100644 windows/bin/purge.ps1 create mode 100644 windows/bin/uninstall.ps1 create mode 100644 windows/lib/clean/apps.ps1 create mode 100644 windows/lib/clean/caches.ps1 create mode 100644 windows/lib/clean/dev.ps1 create mode 100644 windows/lib/clean/system.ps1 create mode 100644 windows/lib/clean/user.ps1 diff --git a/.gitignore b/.gitignore index 8319b94..574c7c8 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,6 @@ bin/status-darwin-* tests/tmp-*/ tests/*.tmp tests/*.log + +session.json +run_tests.ps1 diff --git a/windows/bin/clean.ps1 b/windows/bin/clean.ps1 new file mode 100644 index 0000000..4e6311c --- /dev/null +++ b/windows/bin/clean.ps1 @@ -0,0 +1,300 @@ +# Mole - Clean Command +# Deep cleanup for Windows with dry-run support and whitelist + +#Requires -Version 5.1 +[CmdletBinding()] +param( + [switch]$DryRun, + [switch]$System, + [switch]$DebugMode, + [switch]$Whitelist, + [switch]$ShowHelp +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +# Script location +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$libDir = Join-Path (Split-Path -Parent $scriptDir) "lib" + +# Import core modules +. "$libDir\core\base.ps1" +. "$libDir\core\log.ps1" +. "$libDir\core\ui.ps1" +. "$libDir\core\file_ops.ps1" + +# Import cleanup modules +. "$libDir\clean\user.ps1" +. "$libDir\clean\caches.ps1" +. "$libDir\clean\dev.ps1" +. "$libDir\clean\apps.ps1" +. "$libDir\clean\system.ps1" + +# ============================================================================ +# Configuration +# ============================================================================ + +$script:ExportListFile = "$env:USERPROFILE\.config\mole\clean-list.txt" + +# ============================================================================ +# Help +# ============================================================================ + +function Show-CleanHelp { + $esc = [char]27 + Write-Host "" + Write-Host "$esc[1;35mMole Clean$esc[0m - Deep cleanup for Windows" + Write-Host "" + Write-Host "$esc[33mUsage:$esc[0m mole clean [options]" + Write-Host "" + Write-Host "$esc[33mOptions:$esc[0m" + Write-Host " -DryRun Preview changes without deleting (recommended first run)" + Write-Host " -System Include system-level cleanup (requires admin)" + Write-Host " -Whitelist Manage protected paths" + Write-Host " -DebugMode Enable debug logging" + Write-Host " -ShowHelp Show this help message" + Write-Host "" + Write-Host "$esc[33mExamples:$esc[0m" + Write-Host " mole clean -DryRun # Preview what would be cleaned" + Write-Host " mole clean # Run standard cleanup" + Write-Host " mole clean -System # Include system cleanup (as admin)" + Write-Host "" +} + +# ============================================================================ +# Whitelist Management +# ============================================================================ + +function Edit-Whitelist { + $whitelistPath = $script:Config.WhitelistFile + $whitelistDir = Split-Path -Parent $whitelistPath + + # Ensure directory exists + if (-not (Test-Path $whitelistDir)) { + New-Item -ItemType Directory -Path $whitelistDir -Force | Out-Null + } + + # Create default whitelist if doesn't exist + if (-not (Test-Path $whitelistPath)) { + $defaultContent = @" +# Mole Whitelist - Paths listed here will never be cleaned +# Use full paths or patterns with wildcards (*) +# +# Examples: +# C:\Users\YourName\Documents\ImportantProject +# C:\Users\*\AppData\Local\MyApp +# $env:LOCALAPPDATA\CriticalApp +# +# Add your protected paths below: + +"@ + Set-Content -Path $whitelistPath -Value $defaultContent + } + + # Open in default editor + Write-Info "Opening whitelist file: $whitelistPath" + Start-Process notepad.exe -ArgumentList $whitelistPath -Wait + + Write-Success "Whitelist saved" +} + +# ============================================================================ +# Cleanup Summary +# ============================================================================ + +function Show-CleanupSummary { + param( + [hashtable]$Stats, + [bool]$IsDryRun + ) + + $esc = [char]27 + + Write-Host "" + Write-Host "$esc[1;35m" -NoNewline + if ($IsDryRun) { + Write-Host "Dry run complete - no changes made" -NoNewline + } + else { + Write-Host "Cleanup complete" -NoNewline + } + Write-Host "$esc[0m" + Write-Host "" + + if ($Stats.TotalSizeKB -gt 0) { + $sizeGB = [Math]::Round($Stats.TotalSizeKB / 1024 / 1024, 2) + + if ($IsDryRun) { + Write-Host " Potential space: $esc[32m${sizeGB}GB$esc[0m" + Write-Host " Items found: $($Stats.FilesCleaned)" + Write-Host " Categories: $($Stats.TotalItems)" + Write-Host "" + Write-Host " Detailed list: $esc[90m$($script:ExportListFile)$esc[0m" + Write-Host " Run without -DryRun to apply cleanup" + } + else { + Write-Host " Space freed: $esc[32m${sizeGB}GB$esc[0m" + Write-Host " Items cleaned: $($Stats.FilesCleaned)" + Write-Host " Categories: $($Stats.TotalItems)" + Write-Host "" + Write-Host " Free space now: $(Get-FreeSpace)" + } + } + else { + if ($IsDryRun) { + Write-Host " No significant reclaimable space detected." + } + else { + Write-Host " System was already clean; no additional space freed." + } + Write-Host " Free space now: $(Get-FreeSpace)" + } + + Write-Host "" +} + +# ============================================================================ +# Main Cleanup Flow +# ============================================================================ + +function Start-Cleanup { + param( + [bool]$IsDryRun, + [bool]$IncludeSystem + ) + + $esc = [char]27 + + # Clear screen + Clear-Host + Write-Host "" + Write-Host "$esc[1;35mClean Your Windows$esc[0m" + Write-Host "" + + # Show mode + if ($IsDryRun) { + Write-Host "$esc[33mDry Run Mode$esc[0m - Preview only, no deletions" + Write-Host "" + + # Prepare export file + $exportDir = Split-Path -Parent $script:ExportListFile + if (-not (Test-Path $exportDir)) { + New-Item -ItemType Directory -Path $exportDir -Force | Out-Null + } + + $header = @" +# Mole Cleanup Preview - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') +# +# How to protect files: +# 1. Copy any path below to $($script:Config.WhitelistFile) +# 2. Run: mole clean -Whitelist +# + +"@ + Set-Content -Path $script:ExportListFile -Value $header + } + else { + Write-Host "$esc[90m$($script:Icons.Solid) Use -DryRun to preview, -Whitelist to manage protected paths$esc[0m" + Write-Host "" + } + + # System cleanup confirmation + if ($IncludeSystem -and -not $IsDryRun) { + if (-not (Test-IsAdmin)) { + Write-Warning "System cleanup requires administrator privileges" + Write-Host " Run PowerShell as Administrator for full cleanup" + Write-Host "" + $IncludeSystem = $false + } + else { + Write-Host "$esc[32m$($script:Icons.Success)$esc[0m Running with Administrator privileges" + Write-Host "" + } + } + + # Show system info + $winVer = Get-WindowsVersion + Write-Host "$esc[34m$($script:Icons.Admin)$esc[0m $($winVer.Name) | Free space: $(Get-FreeSpace)" + Write-Host "" + + # Reset stats + Reset-CleanupStats + Set-DryRunMode -Enabled $IsDryRun + + # Run cleanup modules + try { + # User essentials (temp, logs, etc.) + Invoke-UserCleanup -TempDaysOld 7 -LogDaysOld 7 + + # Browser caches + Clear-BrowserCaches + + # Application caches + Clear-AppCaches + + # Developer tools + Invoke-DevToolsCleanup + + # Applications cleanup + Invoke-AppCleanup + + # System cleanup (if requested and admin) + if ($IncludeSystem -and (Test-IsAdmin)) { + Invoke-SystemCleanup + } + } + catch { + Write-Error "Cleanup error: $_" + } + + # Get final stats + $stats = Get-CleanupStats + + # Show summary + Show-CleanupSummary -Stats $stats -IsDryRun $IsDryRun +} + +# ============================================================================ +# Main Entry Point +# ============================================================================ + +function Main { + # Enable debug if requested + if ($DebugMode) { + $env:MOLE_DEBUG = "1" + $DebugPreference = "Continue" + } + + # Show help + if ($ShowHelp) { + Show-CleanHelp + return + } + + # Manage whitelist + if ($Whitelist) { + Edit-Whitelist + return + } + + # Set dry-run mode + if ($DryRun) { + $env:MOLE_DRY_RUN = "1" + } + else { + $env:MOLE_DRY_RUN = "0" + } + + # Run cleanup + try { + Start-Cleanup -IsDryRun $DryRun -IncludeSystem $System + } + finally { + # Cleanup temp files + Clear-TempFiles + } +} + +# Run main +Main diff --git a/windows/bin/optimize.ps1 b/windows/bin/optimize.ps1 new file mode 100644 index 0000000..5973ee9 --- /dev/null +++ b/windows/bin/optimize.ps1 @@ -0,0 +1,545 @@ +# Mole - Optimize Command +# System optimization and health checks for Windows + +#Requires -Version 5.1 +[CmdletBinding()] +param( + [switch]$DryRun, + [switch]$DebugMode, + [switch]$ShowHelp +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +# Script location +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$libDir = Join-Path (Split-Path -Parent $scriptDir) "lib" + +# Import core modules +. "$libDir\core\base.ps1" +. "$libDir\core\log.ps1" +. "$libDir\core\ui.ps1" +. "$libDir\core\file_ops.ps1" + +# ============================================================================ +# Configuration +# ============================================================================ + +$script:OptimizationsApplied = 0 +$script:IssuesFound = 0 +$script:IssuesFixed = 0 + +# ============================================================================ +# Help +# ============================================================================ + +function Show-OptimizeHelp { + $esc = [char]27 + Write-Host "" + Write-Host "$esc[1;35mMole Optimize$esc[0m - System optimization and health checks" + Write-Host "" + Write-Host "$esc[33mUsage:$esc[0m mole optimize [options]" + Write-Host "" + Write-Host "$esc[33mOptions:$esc[0m" + Write-Host " -DryRun Preview optimizations without applying" + Write-Host " -DebugMode Enable debug logging" + Write-Host " -ShowHelp Show this help message" + Write-Host "" + Write-Host "$esc[33mOptimizations:$esc[0m" + Write-Host " - Disk defragmentation/TRIM (SSD optimization)" + Write-Host " - Windows Search index optimization" + Write-Host " - DNS cache flush" + Write-Host " - Network optimization" + Write-Host " - Startup program analysis" + Write-Host " - System file verification" + Write-Host "" +} + +# ============================================================================ +# System Health Information +# ============================================================================ + +function Get-SystemHealth { + <# + .SYNOPSIS + Collect system health metrics + #> + + $health = @{} + + # Memory info + $os = Get-WmiObject Win32_OperatingSystem + $health.MemoryTotalGB = [Math]::Round($os.TotalVisibleMemorySize / 1MB, 1) + $health.MemoryUsedGB = [Math]::Round(($os.TotalVisibleMemorySize - $os.FreePhysicalMemory) / 1MB, 1) + $health.MemoryUsedPercent = [Math]::Round((($os.TotalVisibleMemorySize - $os.FreePhysicalMemory) / $os.TotalVisibleMemorySize) * 100, 0) + + # Disk info + $disk = Get-WmiObject Win32_LogicalDisk -Filter "DeviceID='$env:SystemDrive'" + $health.DiskTotalGB = [Math]::Round($disk.Size / 1GB, 0) + $health.DiskUsedGB = [Math]::Round(($disk.Size - $disk.FreeSpace) / 1GB, 0) + $health.DiskUsedPercent = [Math]::Round((($disk.Size - $disk.FreeSpace) / $disk.Size) * 100, 0) + + # Uptime + $uptime = (Get-Date) - (Get-CimInstance Win32_OperatingSystem).LastBootUpTime + $health.UptimeDays = [Math]::Round($uptime.TotalDays, 1) + + # CPU info + $cpu = Get-WmiObject Win32_Processor + $health.CPUName = $cpu.Name + $health.CPUCores = $cpu.NumberOfLogicalProcessors + + return $health +} + +function Show-SystemHealth { + param([hashtable]$Health) + + $esc = [char]27 + + Write-Host "$esc[34m$($script:Icons.Admin)$esc[0m System " -NoNewline + Write-Host "$($Health.MemoryUsedGB)/$($Health.MemoryTotalGB)GB RAM | " -NoNewline + Write-Host "$($Health.DiskUsedGB)/$($Health.DiskTotalGB)GB Disk | " -NoNewline + Write-Host "Uptime $($Health.UptimeDays)d" +} + +# ============================================================================ +# Optimization Tasks +# ============================================================================ + +function Optimize-DiskDrive { + <# + .SYNOPSIS + Optimize disk (defrag for HDD, TRIM for SSD) + #> + + $esc = [char]27 + + Write-Host "" + Write-Host "$esc[34m$($script:Icons.Arrow) Disk Optimization$esc[0m" + + if (-not (Test-IsAdmin)) { + Write-Host " $esc[33m$($script:Icons.Warning)$esc[0m Requires administrator privileges" + return + } + + if ($script:DryRun) { + Write-Host " $esc[33m$($script:Icons.DryRun)$esc[0m Would optimize $env:SystemDrive" + $script:OptimizationsApplied++ + return + } + + try { + # Check if SSD or HDD + $diskNumber = (Get-Partition -DriveLetter $env:SystemDrive[0]).DiskNumber + $mediaType = (Get-PhysicalDisk | Where-Object { $_.DeviceId -eq $diskNumber }).MediaType + + if ($mediaType -eq "SSD") { + Write-Host " Running TRIM on SSD..." + $null = Optimize-Volume -DriveLetter $env:SystemDrive[0] -ReTrim -ErrorAction Stop + Write-Host " $esc[32m$($script:Icons.Success)$esc[0m SSD TRIM completed" + } + else { + Write-Host " Running defragmentation on HDD..." + $null = Optimize-Volume -DriveLetter $env:SystemDrive[0] -Defrag -ErrorAction Stop + Write-Host " $esc[32m$($script:Icons.Success)$esc[0m Defragmentation completed" + } + $script:OptimizationsApplied++ + } + catch { + Write-Host " $esc[31m$($script:Icons.Error)$esc[0m Disk optimization failed: $_" + } +} + +function Optimize-SearchIndex { + <# + .SYNOPSIS + Rebuild Windows Search index if needed + #> + + $esc = [char]27 + + Write-Host "" + Write-Host "$esc[34m$($script:Icons.Arrow) Windows Search$esc[0m" + + $searchService = Get-Service -Name WSearch -ErrorAction SilentlyContinue + + if (-not $searchService) { + Write-Host " $esc[90mWindows Search service not found$esc[0m" + return + } + + if ($searchService.Status -ne 'Running') { + Write-Host " $esc[33m$($script:Icons.Warning)$esc[0m Windows Search service is not running" + + if ($script:DryRun) { + Write-Host " $esc[33m$($script:Icons.DryRun)$esc[0m Would start search service" + return + } + + try { + Start-Service -Name WSearch -ErrorAction Stop + Write-Host " $esc[32m$($script:Icons.Success)$esc[0m Started Windows Search service" + $script:OptimizationsApplied++ + } + catch { + Write-Host " $esc[31m$($script:Icons.Error)$esc[0m Could not start search service" + } + } + else { + Write-Host " $esc[32m$($script:Icons.Success)$esc[0m Search service running" + } +} + +function Clear-DnsCache { + <# + .SYNOPSIS + Clear DNS resolver cache + #> + + $esc = [char]27 + + Write-Host "" + Write-Host "$esc[34m$($script:Icons.Arrow) DNS Cache$esc[0m" + + if ($script:DryRun) { + Write-Host " $esc[33m$($script:Icons.DryRun)$esc[0m Would flush DNS cache" + $script:OptimizationsApplied++ + return + } + + try { + Clear-DnsClientCache -ErrorAction Stop + Write-Host " $esc[32m$($script:Icons.Success)$esc[0m DNS cache flushed" + $script:OptimizationsApplied++ + } + catch { + Write-Host " $esc[31m$($script:Icons.Error)$esc[0m Could not flush DNS cache: $_" + } +} + +function Optimize-Network { + <# + .SYNOPSIS + Network stack optimization + #> + + $esc = [char]27 + + Write-Host "" + Write-Host "$esc[34m$($script:Icons.Arrow) Network Optimization$esc[0m" + + if (-not (Test-IsAdmin)) { + Write-Host " $esc[33m$($script:Icons.Warning)$esc[0m Requires administrator privileges" + return + } + + if ($script:DryRun) { + Write-Host " $esc[33m$($script:Icons.DryRun)$esc[0m Would reset Winsock catalog" + Write-Host " $esc[33m$($script:Icons.DryRun)$esc[0m Would reset TCP/IP stack" + $script:OptimizationsApplied += 2 + return + } + + try { + # Reset Winsock + $null = netsh winsock reset 2>&1 + Write-Host " $esc[32m$($script:Icons.Success)$esc[0m Winsock catalog reset" + $script:OptimizationsApplied++ + } + catch { + Write-Host " $esc[31m$($script:Icons.Error)$esc[0m Winsock reset failed" + } + + try { + # Flush ARP cache + $null = netsh interface ip delete arpcache 2>&1 + Write-Host " $esc[32m$($script:Icons.Success)$esc[0m ARP cache cleared" + $script:OptimizationsApplied++ + } + catch { + Write-Host " $esc[31m$($script:Icons.Error)$esc[0m ARP cache clear failed" + } +} + +function Get-StartupPrograms { + <# + .SYNOPSIS + Analyze startup programs + #> + + $esc = [char]27 + + Write-Host "" + Write-Host "$esc[34m$($script:Icons.Arrow) Startup Programs$esc[0m" + + $startupPaths = @( + "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" + "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run" + ) + + $startupCount = 0 + + foreach ($path in $startupPaths) { + if (Test-Path $path) { + $items = Get-ItemProperty -Path $path -ErrorAction SilentlyContinue + $props = @($items.PSObject.Properties | Where-Object { + $_.Name -notin @('PSPath', 'PSParentPath', 'PSChildName', 'PSDrive', 'PSProvider') + }) + $startupCount += $props.Count + } + } + + # Also check startup folder + $startupFolder = "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup" + if (Test-Path $startupFolder) { + $startupFiles = @(Get-ChildItem -Path $startupFolder -File -ErrorAction SilentlyContinue) + $startupCount += $startupFiles.Count + } + + if ($startupCount -gt 10) { + Write-Host " $esc[33m$($script:Icons.Warning)$esc[0m $startupCount startup programs (high)" + Write-Host " $esc[90mConsider disabling unnecessary startup items in Task Manager$esc[0m" + $script:IssuesFound++ + } + elseif ($startupCount -gt 5) { + Write-Host " $esc[33m$($script:Icons.Warning)$esc[0m $startupCount startup programs (moderate)" + $script:IssuesFound++ + } + else { + Write-Host " $esc[32m$($script:Icons.Success)$esc[0m $startupCount startup programs" + } +} + +function Test-SystemFiles { + <# + .SYNOPSIS + Run System File Checker (SFC) + #> + + $esc = [char]27 + + Write-Host "" + Write-Host "$esc[34m$($script:Icons.Arrow) System File Verification$esc[0m" + + if (-not (Test-IsAdmin)) { + Write-Host " $esc[33m$($script:Icons.Warning)$esc[0m Requires administrator privileges" + return + } + + if ($script:DryRun) { + Write-Host " $esc[33m$($script:Icons.DryRun)$esc[0m Would run System File Checker" + return + } + + Write-Host " Running System File Checker (this may take several minutes)..." + + try { + $sfcResult = Start-Process -FilePath "sfc.exe" -ArgumentList "/scannow" ` + -Wait -PassThru -NoNewWindow -RedirectStandardOutput "$env:TEMP\sfc_output.txt" -ErrorAction Stop + + $output = Get-Content "$env:TEMP\sfc_output.txt" -ErrorAction SilentlyContinue + Remove-Item "$env:TEMP\sfc_output.txt" -Force -ErrorAction SilentlyContinue + + if ($output -match "did not find any integrity violations") { + Write-Host " $esc[32m$($script:Icons.Success)$esc[0m No integrity violations found" + } + elseif ($output -match "found corrupt files and successfully repaired") { + Write-Host " $esc[32m$($script:Icons.Success)$esc[0m Corrupt files were repaired" + $script:IssuesFixed++ + } + elseif ($output -match "found corrupt files but was unable to fix") { + Write-Host " $esc[31m$($script:Icons.Error)$esc[0m Found corrupt files that could not be repaired" + Write-Host " $esc[90mRun 'DISM /Online /Cleanup-Image /RestoreHealth' then retry SFC$esc[0m" + $script:IssuesFound++ + } + else { + Write-Host " $esc[32m$($script:Icons.Success)$esc[0m Scan completed" + } + } + catch { + Write-Host " $esc[31m$($script:Icons.Error)$esc[0m System File Checker failed: $_" + } +} + +function Test-DiskHealth { + <# + .SYNOPSIS + Check disk health status + #> + + $esc = [char]27 + + Write-Host "" + Write-Host "$esc[34m$($script:Icons.Arrow) Disk Health$esc[0m" + + try { + $disks = Get-PhysicalDisk -ErrorAction Stop + + foreach ($disk in $disks) { + $status = $disk.HealthStatus + $name = $disk.FriendlyName + + if ($status -eq "Healthy") { + Write-Host " $esc[32m$($script:Icons.Success)$esc[0m $name - Healthy" + } + elseif ($status -eq "Warning") { + Write-Host " $esc[33m$($script:Icons.Warning)$esc[0m $name - Warning" + Write-Host " $esc[90mDisk may have issues, consider backing up data$esc[0m" + $script:IssuesFound++ + } + else { + Write-Host " $esc[31m$($script:Icons.Error)$esc[0m $name - $status" + Write-Host " $esc[31mDisk has critical issues, back up data immediately!$esc[0m" + $script:IssuesFound++ + } + } + } + catch { + Write-Host " $esc[90mCould not check disk health$esc[0m" + } +} + +function Test-WindowsUpdate { + <# + .SYNOPSIS + Check Windows Update status + #> + + $esc = [char]27 + + Write-Host "" + Write-Host "$esc[34m$($script:Icons.Arrow) Windows Update$esc[0m" + + try { + $updateSession = New-Object -ComObject Microsoft.Update.Session + $updateSearcher = $updateSession.CreateUpdateSearcher() + + Write-Host " Checking for updates..." + $searchResult = $updateSearcher.Search("IsInstalled=0") + + $importantUpdates = $searchResult.Updates | Where-Object { + $_.MsrcSeverity -in @('Critical', 'Important') + } + + if ($importantUpdates.Count -gt 0) { + Write-Host " $esc[33m$($script:Icons.Warning)$esc[0m $($importantUpdates.Count) important updates available" + Write-Host " $esc[90mRun Windows Update to install$esc[0m" + $script:IssuesFound++ + } + elseif ($searchResult.Updates.Count -gt 0) { + Write-Host " $esc[90m$($script:Icons.List)$esc[0m $($searchResult.Updates.Count) optional updates available" + } + else { + Write-Host " $esc[32m$($script:Icons.Success)$esc[0m System is up to date" + } + } + catch { + Write-Host " $esc[90mCould not check Windows Update status$esc[0m" + } +} + +# ============================================================================ +# Summary +# ============================================================================ + +function Show-OptimizeSummary { + $esc = [char]27 + + Write-Host "" + Write-Host "$esc[1;35m" -NoNewline + if ($script:DryRun) { + Write-Host "Dry Run Complete - No Changes Made" -NoNewline + } + else { + Write-Host "Optimization Complete" -NoNewline + } + Write-Host "$esc[0m" + Write-Host "" + + if ($script:DryRun) { + Write-Host " Would apply $esc[33m$($script:OptimizationsApplied)$esc[0m optimizations" + Write-Host " Run without -DryRun to apply changes" + } + else { + Write-Host " Optimizations applied: $esc[32m$($script:OptimizationsApplied)$esc[0m" + + if ($script:IssuesFixed -gt 0) { + Write-Host " Issues fixed: $esc[32m$($script:IssuesFixed)$esc[0m" + } + + if ($script:IssuesFound -gt 0) { + Write-Host " Issues found: $esc[33m$($script:IssuesFound)$esc[0m" + } + else { + Write-Host " System health: $esc[32mGood$esc[0m" + } + } + + Write-Host "" +} + +# ============================================================================ +# Main Entry Point +# ============================================================================ + +function Main { + # Enable debug if requested + if ($DebugMode) { + $env:MOLE_DEBUG = "1" + $DebugPreference = "Continue" + } + + # Show help + if ($ShowHelp) { + Show-OptimizeHelp + return + } + + # Set dry-run mode + $script:DryRun = $DryRun + + # Clear screen + Clear-Host + + $esc = [char]27 + Write-Host "" + Write-Host "$esc[1;35mOptimize and Check$esc[0m" + Write-Host "" + + if ($script:DryRun) { + Write-Host "$esc[33m$($script:Icons.DryRun) DRY RUN MODE$esc[0m - No changes will be made" + Write-Host "" + } + + # Show system health + $health = Get-SystemHealth + Show-SystemHealth -Health $health + + # Run optimizations + Optimize-DiskDrive + Optimize-SearchIndex + Clear-DnsCache + Optimize-Network + + # Run health checks + Get-StartupPrograms + Test-DiskHealth + Test-WindowsUpdate + + # System file check is slow, ask first + if (-not $script:DryRun -and (Test-IsAdmin)) { + Write-Host "" + $runSfc = Read-Host "Run System File Checker? This may take several minutes (y/N)" + if ($runSfc -eq 'y' -or $runSfc -eq 'Y') { + Test-SystemFiles + } + } + + # Summary + Show-OptimizeSummary +} + +# Run main +Main diff --git a/windows/bin/purge.ps1 b/windows/bin/purge.ps1 new file mode 100644 index 0000000..883dcf6 --- /dev/null +++ b/windows/bin/purge.ps1 @@ -0,0 +1,610 @@ +# Mole - Purge Command +# Aggressive cleanup of project build artifacts + +#Requires -Version 5.1 +[CmdletBinding()] +param( + [switch]$DebugMode, + [switch]$Paths, + [switch]$ShowHelp +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +# Script location +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$libDir = Join-Path (Split-Path -Parent $scriptDir) "lib" + +# Import core modules +. "$libDir\core\base.ps1" +. "$libDir\core\log.ps1" +. "$libDir\core\ui.ps1" +. "$libDir\core\file_ops.ps1" + +# ============================================================================ +# Configuration +# ============================================================================ + +$script:DefaultSearchPaths = @( + "$env:USERPROFILE\Documents" + "$env:USERPROFILE\Projects" + "$env:USERPROFILE\Code" + "$env:USERPROFILE\Development" + "$env:USERPROFILE\workspace" + "$env:USERPROFILE\github" + "$env:USERPROFILE\repos" + "$env:USERPROFILE\src" + "D:\Projects" + "D:\Code" + "D:\Development" +) + +$script:ConfigFile = "$env:USERPROFILE\.config\mole\purge_paths.txt" + +# Artifact patterns to clean +$script:ArtifactPatterns = @( + @{ Name = "node_modules"; Type = "Directory"; Language = "JavaScript/Node.js" } + @{ Name = "vendor"; Type = "Directory"; Language = "PHP/Go" } + @{ Name = ".venv"; Type = "Directory"; Language = "Python" } + @{ Name = "venv"; Type = "Directory"; Language = "Python" } + @{ Name = "__pycache__"; Type = "Directory"; Language = "Python" } + @{ Name = ".pytest_cache"; Type = "Directory"; Language = "Python" } + @{ Name = "target"; Type = "Directory"; Language = "Rust/Java" } + @{ Name = "build"; Type = "Directory"; Language = "General" } + @{ Name = "dist"; Type = "Directory"; Language = "General" } + @{ Name = ".next"; Type = "Directory"; Language = "Next.js" } + @{ Name = ".nuxt"; Type = "Directory"; Language = "Nuxt.js" } + @{ Name = ".turbo"; Type = "Directory"; Language = "Turborepo" } + @{ Name = ".parcel-cache"; Type = "Directory"; Language = "Parcel" } + @{ Name = "bin"; Type = "Directory"; Language = ".NET" } + @{ Name = "obj"; Type = "Directory"; Language = ".NET" } + @{ Name = ".gradle"; Type = "Directory"; Language = "Java/Gradle" } + @{ Name = ".idea"; Type = "Directory"; Language = "JetBrains IDE" } + @{ Name = "*.log"; Type = "File"; Language = "Logs" } +) + +$script:TotalSizeCleaned = 0 +$script:ItemsCleaned = 0 + +# ============================================================================ +# Help +# ============================================================================ + +function Show-PurgeHelp { + $esc = [char]27 + Write-Host "" + Write-Host "$esc[1;35mMole Purge$esc[0m - Clean project build artifacts" + Write-Host "" + Write-Host "$esc[33mUsage:$esc[0m mole purge [options]" + Write-Host "" + Write-Host "$esc[33mOptions:$esc[0m" + Write-Host " -Paths Edit custom scan directories" + Write-Host " -DebugMode Enable debug logging" + Write-Host " -ShowHelp Show this help message" + Write-Host "" + Write-Host "$esc[33mDefault Search Paths:$esc[0m" + foreach ($path in $script:DefaultSearchPaths) { + if (Test-Path $path) { + Write-Host " $esc[32m+$esc[0m $path" + } + else { + Write-Host " $esc[90m-$esc[0m $path (not found)" + } + } + Write-Host "" + Write-Host "$esc[33mArtifacts Cleaned:$esc[0m" + Write-Host " node_modules, vendor, venv, target, build, dist, __pycache__, etc." + Write-Host "" +} + +# ============================================================================ +# Path Management +# ============================================================================ + +function Get-SearchPaths { + <# + .SYNOPSIS + Get list of paths to scan for projects + #> + + $paths = @() + + # Load custom paths if available + if (Test-Path $script:ConfigFile) { + $customPaths = Get-Content $script:ConfigFile -ErrorAction SilentlyContinue | + Where-Object { $_ -and -not $_.StartsWith('#') } | + ForEach-Object { $_.Trim() } + + foreach ($path in $customPaths) { + if (Test-Path $path) { + $paths += $path + } + } + } + + # Add default paths if no custom paths or custom paths don't exist + if ($paths.Count -eq 0) { + foreach ($path in $script:DefaultSearchPaths) { + if (Test-Path $path) { + $paths += $path + } + } + } + + return $paths +} + +function Edit-SearchPaths { + <# + .SYNOPSIS + Open search paths configuration for editing + #> + + $configDir = Split-Path -Parent $script:ConfigFile + + if (-not (Test-Path $configDir)) { + New-Item -ItemType Directory -Path $configDir -Force | Out-Null + } + + if (-not (Test-Path $script:ConfigFile)) { + $defaultContent = @" +# Mole Purge - Custom Search Paths +# Add directories to scan for project artifacts (one per line) +# Lines starting with # are ignored +# +# Examples: +# D:\MyProjects +# E:\Work\Code +# +# Default paths (used if this file is empty): +# $env:USERPROFILE\Documents +# $env:USERPROFILE\Projects +# $env:USERPROFILE\Code + +"@ + Set-Content -Path $script:ConfigFile -Value $defaultContent + } + + Write-Info "Opening paths configuration: $($script:ConfigFile)" + Start-Process notepad.exe -ArgumentList $script:ConfigFile -Wait + + Write-Success "Configuration saved" +} + +# ============================================================================ +# Project Discovery +# ============================================================================ + +function Find-Projects { + <# + .SYNOPSIS + Find all development projects in search paths + #> + param([string[]]$SearchPaths) + + $projects = @() + + # Project markers + $projectMarkers = @( + "package.json" # Node.js + "composer.json" # PHP + "Cargo.toml" # Rust + "go.mod" # Go + "pom.xml" # Java/Maven + "build.gradle" # Java/Gradle + "requirements.txt" # Python + "pyproject.toml" # Python + "*.csproj" # .NET + "*.sln" # .NET Solution + ) + + $esc = [char]27 + $pathCount = 0 + $totalPaths = $SearchPaths.Count + + foreach ($searchPath in $SearchPaths) { + $pathCount++ + Write-Progress -Activity "Scanning for projects" ` + -Status "Searching: $searchPath" ` + -PercentComplete (($pathCount / $totalPaths) * 100) + + foreach ($marker in $projectMarkers) { + try { + $found = Get-ChildItem -Path $searchPath -Filter $marker -Recurse -Depth 4 -ErrorAction SilentlyContinue + + foreach ($item in $found) { + $projectPath = Split-Path -Parent $item.FullName + + # Skip if already found or if it's inside node_modules, etc. + if ($projects.Path -contains $projectPath) { continue } + if ($projectPath -like "*\node_modules\*") { continue } + if ($projectPath -like "*\vendor\*") { continue } + if ($projectPath -like "*\.git\*") { continue } + + # Find artifacts in this project + $artifacts = @(Find-ProjectArtifacts -ProjectPath $projectPath) + + if ($artifacts.Count -gt 0) { + $totalSize = ($artifacts | Measure-Object -Property SizeKB -Sum).Sum + + $projects += [PSCustomObject]@{ + Path = $projectPath + Name = Split-Path -Leaf $projectPath + Marker = $marker + Artifacts = $artifacts + TotalSizeKB = $totalSize + TotalSizeHuman = Format-ByteSize -Bytes ($totalSize * 1024) + } + } + } + } + catch { + Write-Debug "Error scanning $searchPath for $marker : $_" + } + } + } + + Write-Progress -Activity "Scanning for projects" -Completed + + # Sort by size (largest first) + return $projects | Sort-Object -Property TotalSizeKB -Descending +} + +function Find-ProjectArtifacts { + <# + .SYNOPSIS + Find cleanable artifacts in a project directory + #> + param([string]$ProjectPath) + + $artifacts = @() + + foreach ($pattern in $script:ArtifactPatterns) { + $items = Get-ChildItem -Path $ProjectPath -Filter $pattern.Name -Force -ErrorAction SilentlyContinue + + foreach ($item in $items) { + if ($pattern.Type -eq "Directory" -and $item.PSIsContainer) { + $sizeKB = Get-PathSizeKB -Path $item.FullName + + $artifacts += [PSCustomObject]@{ + Path = $item.FullName + Name = $item.Name + Type = "Directory" + Language = $pattern.Language + SizeKB = $sizeKB + SizeHuman = Format-ByteSize -Bytes ($sizeKB * 1024) + } + } + elseif ($pattern.Type -eq "File" -and -not $item.PSIsContainer) { + $sizeKB = [Math]::Ceiling($item.Length / 1024) + + $artifacts += [PSCustomObject]@{ + Path = $item.FullName + Name = $item.Name + Type = "File" + Language = $pattern.Language + SizeKB = $sizeKB + SizeHuman = Format-ByteSize -Bytes ($sizeKB * 1024) + } + } + } + } + + return $artifacts +} + +# ============================================================================ +# Project Selection UI +# ============================================================================ + +function Show-ProjectSelectionMenu { + <# + .SYNOPSIS + Interactive menu for selecting projects to clean + #> + param([array]$Projects) + + if ($Projects.Count -eq 0) { + Write-Warning "No projects with cleanable artifacts found" + return @() + } + + $esc = [char]27 + $selectedIndices = @{} + $currentIndex = 0 + $pageSize = 12 + $pageStart = 0 + + [Console]::CursorVisible = $false + + try { + while ($true) { + Clear-Host + + # Header + Write-Host "" + Write-Host "$esc[1;35mSelect Projects to Clean$esc[0m" + Write-Host "" + Write-Host "$esc[90mUse: $($script:Icons.NavUp)$($script:Icons.NavDown) navigate | Space select | A select all | Enter confirm | Q quit$esc[0m" + Write-Host "" + + # Display projects + $pageEnd = [Math]::Min($pageStart + $pageSize, $Projects.Count) + + for ($i = $pageStart; $i -lt $pageEnd; $i++) { + $project = $Projects[$i] + $isSelected = $selectedIndices.ContainsKey($i) + $isCurrent = ($i -eq $currentIndex) + + $checkbox = if ($isSelected) { "$esc[32m[$($script:Icons.Success)]$esc[0m" } else { "[ ]" } + + if ($isCurrent) { + Write-Host "$esc[7m" -NoNewline + } + + $name = $project.Name + if ($name.Length -gt 30) { + $name = $name.Substring(0, 27) + "..." + } + + $artifactCount = $project.Artifacts.Count + + Write-Host (" {0} {1,-32} {2,10} ({3} items)" -f $checkbox, $name, $project.TotalSizeHuman, $artifactCount) -NoNewline + + if ($isCurrent) { + Write-Host "$esc[0m" + } + else { + Write-Host "" + } + } + + # Footer + Write-Host "" + $selectedCount = $selectedIndices.Count + if ($selectedCount -gt 0) { + $totalSize = 0 + foreach ($idx in $selectedIndices.Keys) { + $totalSize += $Projects[$idx].TotalSizeKB + } + $totalSizeHuman = Format-ByteSize -Bytes ($totalSize * 1024) + Write-Host "$esc[33mSelected:$esc[0m $selectedCount projects ($totalSizeHuman)" + } + + # Page indicator + $totalPages = [Math]::Ceiling($Projects.Count / $pageSize) + $currentPage = [Math]::Floor($pageStart / $pageSize) + 1 + Write-Host "$esc[90mPage $currentPage of $totalPages | Total: $($Projects.Count) projects$esc[0m" + + # Handle input + $key = [Console]::ReadKey($true) + + switch ($key.Key) { + 'UpArrow' { + if ($currentIndex -gt 0) { + $currentIndex-- + if ($currentIndex -lt $pageStart) { + $pageStart = [Math]::Max(0, $pageStart - $pageSize) + } + } + } + 'DownArrow' { + if ($currentIndex -lt $Projects.Count - 1) { + $currentIndex++ + if ($currentIndex -ge $pageStart + $pageSize) { + $pageStart += $pageSize + } + } + } + 'PageUp' { + $pageStart = [Math]::Max(0, $pageStart - $pageSize) + $currentIndex = $pageStart + } + 'PageDown' { + $pageStart = [Math]::Min($Projects.Count - $pageSize, $pageStart + $pageSize) + if ($pageStart -lt 0) { $pageStart = 0 } + $currentIndex = $pageStart + } + 'Spacebar' { + if ($selectedIndices.ContainsKey($currentIndex)) { + $selectedIndices.Remove($currentIndex) + } + else { + $selectedIndices[$currentIndex] = $true + } + } + 'A' { + # Select/deselect all + if ($selectedIndices.Count -eq $Projects.Count) { + $selectedIndices.Clear() + } + else { + for ($i = 0; $i -lt $Projects.Count; $i++) { + $selectedIndices[$i] = $true + } + } + } + 'Enter' { + if ($selectedIndices.Count -gt 0) { + $selected = @() + foreach ($idx in $selectedIndices.Keys) { + $selected += $Projects[$idx] + } + return $selected + } + } + 'Escape' { return @() } + 'Q' { return @() } + } + } + } + finally { + [Console]::CursorVisible = $true + } +} + +# ============================================================================ +# Cleanup +# ============================================================================ + +function Remove-ProjectArtifacts { + <# + .SYNOPSIS + Remove artifacts from selected projects + #> + param([array]$Projects) + + $esc = [char]27 + + Write-Host "" + Write-Host "$esc[1;35mCleaning Project Artifacts$esc[0m" + Write-Host "" + + foreach ($project in $Projects) { + Write-Host "$esc[34m$($script:Icons.Arrow)$esc[0m $($project.Name)" + + foreach ($artifact in $project.Artifacts) { + if (Test-Path $artifact.Path) { + try { + if ($artifact.Type -eq "Directory") { + Remove-Item -Path $artifact.Path -Recurse -Force -ErrorAction Stop + } + else { + Remove-Item -Path $artifact.Path -Force -ErrorAction Stop + } + + Write-Host " $esc[32m$($script:Icons.Success)$esc[0m $($artifact.Name) ($($artifact.SizeHuman))" + $script:TotalSizeCleaned += $artifact.SizeKB + $script:ItemsCleaned++ + } + catch { + Write-Host " $esc[31m$($script:Icons.Error)$esc[0m $($artifact.Name) - $_" + } + } + } + } +} + +# ============================================================================ +# Summary +# ============================================================================ + +function Show-PurgeSummary { + $esc = [char]27 + + Write-Host "" + Write-Host "$esc[1;35mPurge Complete$esc[0m" + Write-Host "" + + if ($script:TotalSizeCleaned -gt 0) { + $sizeGB = [Math]::Round($script:TotalSizeCleaned / 1024 / 1024, 2) + Write-Host " Space freed: $esc[32m${sizeGB}GB$esc[0m" + Write-Host " Items cleaned: $($script:ItemsCleaned)" + Write-Host " Free space now: $(Get-FreeSpace)" + } + else { + Write-Host " No artifacts to clean." + Write-Host " Free space now: $(Get-FreeSpace)" + } + + Write-Host "" +} + +# ============================================================================ +# Main Entry Point +# ============================================================================ + +function Main { + # Enable debug if requested + if ($DebugMode) { + $env:MOLE_DEBUG = "1" + $DebugPreference = "Continue" + } + + # Show help + if ($ShowHelp) { + Show-PurgeHelp + return + } + + # Edit paths + if ($Paths) { + Edit-SearchPaths + return + } + + # Clear screen + Clear-Host + + $esc = [char]27 + Write-Host "" + Write-Host "$esc[1;35mPurge Project Artifacts$esc[0m" + Write-Host "" + + # Get search paths + $searchPaths = Get-SearchPaths + + if ($searchPaths.Count -eq 0) { + Write-Warning "No valid search paths found" + Write-Host "Run 'mole purge -Paths' to configure search directories" + return + } + + Write-Info "Searching in $($searchPaths.Count) directories..." + + # Find projects + $projects = Find-Projects -SearchPaths $searchPaths + + if ($projects.Count -eq 0) { + Write-Host "" + Write-Host "$esc[32m$($script:Icons.Success)$esc[0m No cleanable artifacts found" + Write-Host "" + return + } + + $totalSize = ($projects | Measure-Object -Property TotalSizeKB -Sum).Sum + $totalSizeHuman = Format-ByteSize -Bytes ($totalSize * 1024) + + Write-Host "" + Write-Host "Found $esc[33m$($projects.Count)$esc[0m projects with $esc[33m$totalSizeHuman$esc[0m of artifacts" + Write-Host "" + + # Project selection + $selected = Show-ProjectSelectionMenu -Projects $projects + + if ($selected.Count -eq 0) { + Write-Info "No projects selected" + return + } + + # Confirm + Clear-Host + Write-Host "" + $selectedSize = ($selected | Measure-Object -Property TotalSizeKB -Sum).Sum + $selectedSizeHuman = Format-ByteSize -Bytes ($selectedSize * 1024) + + Write-Host "$esc[33mThe following will be cleaned ($selectedSizeHuman):$esc[0m" + Write-Host "" + + foreach ($project in $selected) { + Write-Host " $($script:Icons.List) $($project.Name) ($($project.TotalSizeHuman))" + foreach ($artifact in $project.Artifacts) { + Write-Host " $esc[90m$($artifact.Name) - $($artifact.SizeHuman)$esc[0m" + } + } + + Write-Host "" + $confirm = Read-Host "Continue? (y/N)" + + if ($confirm -eq 'y' -or $confirm -eq 'Y') { + Remove-ProjectArtifacts -Projects $selected + Show-PurgeSummary + } + else { + Write-Info "Cancelled" + } +} + +# Run main +Main diff --git a/windows/bin/uninstall.ps1 b/windows/bin/uninstall.ps1 new file mode 100644 index 0000000..ef9100c --- /dev/null +++ b/windows/bin/uninstall.ps1 @@ -0,0 +1,596 @@ +# Mole - Uninstall Command +# Interactive application uninstaller for Windows + +#Requires -Version 5.1 +[CmdletBinding()] +param( + [switch]$DebugMode, + [switch]$Rescan, + [switch]$ShowHelp +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +# Script location +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$libDir = Join-Path (Split-Path -Parent $scriptDir) "lib" + +# Import core modules +. "$libDir\core\base.ps1" +. "$libDir\core\log.ps1" +. "$libDir\core\ui.ps1" +. "$libDir\core\file_ops.ps1" + +# ============================================================================ +# Configuration +# ============================================================================ + +$script:CacheDir = "$env:USERPROFILE\.cache\mole" +$script:AppCacheFile = "$script:CacheDir\app_scan_cache.json" +$script:CacheTTLHours = 24 + +# ============================================================================ +# Help +# ============================================================================ + +function Show-UninstallHelp { + $esc = [char]27 + Write-Host "" + Write-Host "$esc[1;35mMole Uninstall$esc[0m - Interactive application uninstaller" + Write-Host "" + Write-Host "$esc[33mUsage:$esc[0m mole uninstall [options]" + Write-Host "" + Write-Host "$esc[33mOptions:$esc[0m" + Write-Host " -Rescan Force rescan of installed applications" + Write-Host " -DebugMode Enable debug logging" + Write-Host " -ShowHelp Show this help message" + Write-Host "" + Write-Host "$esc[33mFeatures:$esc[0m" + Write-Host " - Scans installed programs from registry and Windows Apps" + Write-Host " - Shows program size and last used date" + Write-Host " - Interactive selection with arrow keys" + Write-Host " - Cleans leftover files after uninstall" + Write-Host "" +} + +# ============================================================================ +# Protected Applications +# ============================================================================ + +$script:ProtectedApps = @( + "Microsoft Windows" + "Windows Feature Experience Pack" + "Microsoft Edge" + "Microsoft Edge WebView2" + "Windows Security" + "Microsoft Visual C++ *" + "Microsoft .NET *" + ".NET Desktop Runtime*" + "Microsoft Update Health Tools" + "NVIDIA Graphics Driver*" + "AMD Software*" + "Intel*Driver*" +) + +function Test-ProtectedApp { + param([string]$AppName) + + foreach ($pattern in $script:ProtectedApps) { + if ($AppName -like $pattern) { + return $true + } + } + return $false +} + +# ============================================================================ +# Application Discovery +# ============================================================================ + +function Get-InstalledApplications { + <# + .SYNOPSIS + Scan and return all installed applications + #> + param([switch]$ForceRescan) + + # Check cache + if (-not $ForceRescan -and (Test-Path $script:AppCacheFile)) { + $cacheInfo = Get-Item $script:AppCacheFile + $cacheAge = (Get-Date) - $cacheInfo.LastWriteTime + + if ($cacheAge.TotalHours -lt $script:CacheTTLHours) { + Write-Debug "Loading from cache..." + try { + $cached = Get-Content $script:AppCacheFile | ConvertFrom-Json + return $cached + } + catch { + Write-Debug "Cache read failed, rescanning..." + } + } + } + + Write-Info "Scanning installed applications..." + + $apps = @() + + # Registry paths for installed programs + $registryPaths = @( + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" + "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" + "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" + ) + + $count = 0 + $total = $registryPaths.Count + + foreach ($path in $registryPaths) { + $count++ + Write-Progress -Activity "Scanning applications" -Status "Registry path $count of $total" -PercentComplete (($count / $total) * 50) + + $items = Get-ItemProperty -Path $path -ErrorAction SilentlyContinue | + Where-Object { + $_.DisplayName -and + $_.UninstallString -and + -not (Test-ProtectedApp $_.DisplayName) + } + + foreach ($item in $items) { + # Calculate size + $sizeKB = 0 + if ($item.EstimatedSize) { + $sizeKB = [long]$item.EstimatedSize + } + elseif ($item.InstallLocation -and (Test-Path $item.InstallLocation)) { + $sizeKB = Get-PathSizeKB -Path $item.InstallLocation + } + + # Get install date + $installDate = $null + if ($item.InstallDate) { + try { + $installDate = [DateTime]::ParseExact($item.InstallDate, "yyyyMMdd", $null) + } + catch { } + } + + $apps += [PSCustomObject]@{ + Name = $item.DisplayName + Publisher = $item.Publisher + Version = $item.DisplayVersion + SizeKB = $sizeKB + SizeHuman = Format-ByteSize -Bytes ($sizeKB * 1024) + InstallLocation = $item.InstallLocation + UninstallString = $item.UninstallString + InstallDate = $installDate + Source = "Registry" + } + } + } + + # UWP / Store Apps + Write-Progress -Activity "Scanning applications" -Status "Scanning Windows Apps" -PercentComplete 75 + + try { + $uwpApps = Get-AppxPackage -ErrorAction SilentlyContinue | + Where-Object { + $_.IsFramework -eq $false -and + $_.SignatureKind -ne 'System' -and + -not (Test-ProtectedApp $_.Name) + } + + foreach ($uwp in $uwpApps) { + # Get friendly name + $name = $uwp.Name + try { + $manifest = Get-AppxPackageManifest -Package $uwp.PackageFullName -ErrorAction SilentlyContinue + if ($manifest.Package.Properties.DisplayName -and + -not $manifest.Package.Properties.DisplayName.StartsWith("ms-resource:")) { + $name = $manifest.Package.Properties.DisplayName + } + } + catch { } + + # Calculate size + $sizeKB = 0 + if ($uwp.InstallLocation -and (Test-Path $uwp.InstallLocation)) { + $sizeKB = Get-PathSizeKB -Path $uwp.InstallLocation + } + + $apps += [PSCustomObject]@{ + Name = $name + Publisher = $uwp.Publisher + Version = $uwp.Version + SizeKB = $sizeKB + SizeHuman = Format-ByteSize -Bytes ($sizeKB * 1024) + InstallLocation = $uwp.InstallLocation + UninstallString = $null + PackageFullName = $uwp.PackageFullName + InstallDate = $null + Source = "WindowsStore" + } + } + } + catch { + Write-Debug "Could not enumerate UWP apps: $_" + } + + Write-Progress -Activity "Scanning applications" -Completed + + # Sort by size (largest first) + $apps = $apps | Sort-Object -Property SizeKB -Descending + + # Cache results + if (-not (Test-Path $script:CacheDir)) { + New-Item -ItemType Directory -Path $script:CacheDir -Force | Out-Null + } + $apps | ConvertTo-Json -Depth 5 | Set-Content $script:AppCacheFile + + return $apps +} + +# ============================================================================ +# Application Selection UI +# ============================================================================ + +function Show-AppSelectionMenu { + <# + .SYNOPSIS + Interactive menu for selecting applications to uninstall + #> + param([array]$Apps) + + if ($Apps.Count -eq 0) { + Write-Warning "No applications found to uninstall" + return @() + } + + $esc = [char]27 + $selectedIndices = @{} + $currentIndex = 0 + $pageSize = 15 + $pageStart = 0 + $searchTerm = "" + $filteredApps = $Apps + + # Hide cursor + [Console]::CursorVisible = $false + + try { + while ($true) { + Clear-Host + + # Header + Write-Host "" + Write-Host "$esc[1;35mSelect Applications to Uninstall$esc[0m" + Write-Host "" + Write-Host "$esc[90mUse: $($script:Icons.NavUp)$($script:Icons.NavDown) navigate | Space select | Enter confirm | Q quit | / search$esc[0m" + Write-Host "" + + # Search indicator + if ($searchTerm) { + Write-Host "$esc[33mSearch:$esc[0m $searchTerm ($($filteredApps.Count) matches)" + Write-Host "" + } + + # Display apps + $pageEnd = [Math]::Min($pageStart + $pageSize, $filteredApps.Count) + + for ($i = $pageStart; $i -lt $pageEnd; $i++) { + $app = $filteredApps[$i] + $isSelected = $selectedIndices.ContainsKey($app.Name) + $isCurrent = ($i -eq $currentIndex) + + # Selection indicator + $checkbox = if ($isSelected) { "$esc[32m[$($script:Icons.Success)]$esc[0m" } else { "[ ]" } + + # Highlight current + if ($isCurrent) { + Write-Host "$esc[7m" -NoNewline # Reverse video + } + + # App info + $name = $app.Name + if ($name.Length -gt 40) { + $name = $name.Substring(0, 37) + "..." + } + + $size = $app.SizeHuman + if (-not $size -or $size -eq "0B") { + $size = "N/A" + } + + Write-Host (" {0} {1,-42} {2,10}" -f $checkbox, $name, $size) -NoNewline + + if ($isCurrent) { + Write-Host "$esc[0m" # Reset + } + else { + Write-Host "" + } + } + + # Footer + Write-Host "" + $selectedCount = $selectedIndices.Count + if ($selectedCount -gt 0) { + $totalSize = 0 + foreach ($key in $selectedIndices.Keys) { + $app = $Apps | Where-Object { $_.Name -eq $key } + if ($app.SizeKB) { + $totalSize += $app.SizeKB + } + } + $totalSizeHuman = Format-ByteSize -Bytes ($totalSize * 1024) + Write-Host "$esc[33mSelected:$esc[0m $selectedCount apps ($totalSizeHuman)" + } + + # Page indicator + $totalPages = [Math]::Ceiling($filteredApps.Count / $pageSize) + $currentPage = [Math]::Floor($pageStart / $pageSize) + 1 + Write-Host "$esc[90mPage $currentPage of $totalPages$esc[0m" + + # Handle input + $key = [Console]::ReadKey($true) + + switch ($key.Key) { + 'UpArrow' { + if ($currentIndex -gt 0) { + $currentIndex-- + if ($currentIndex -lt $pageStart) { + $pageStart = [Math]::Max(0, $pageStart - $pageSize) + } + } + } + 'DownArrow' { + if ($currentIndex -lt $filteredApps.Count - 1) { + $currentIndex++ + if ($currentIndex -ge $pageStart + $pageSize) { + $pageStart += $pageSize + } + } + } + 'PageUp' { + $pageStart = [Math]::Max(0, $pageStart - $pageSize) + $currentIndex = $pageStart + } + 'PageDown' { + $pageStart = [Math]::Min($filteredApps.Count - $pageSize, $pageStart + $pageSize) + if ($pageStart -lt 0) { $pageStart = 0 } + $currentIndex = $pageStart + } + 'Spacebar' { + $app = $filteredApps[$currentIndex] + if ($selectedIndices.ContainsKey($app.Name)) { + $selectedIndices.Remove($app.Name) + } + else { + $selectedIndices[$app.Name] = $true + } + } + 'Enter' { + if ($selectedIndices.Count -gt 0) { + # Return selected apps + $selected = $Apps | Where-Object { $selectedIndices.ContainsKey($_.Name) } + return $selected + } + } + 'Escape' { + return @() + } + 'Q' { + return @() + } + 'Oem2' { # Forward slash + # Search mode + Write-Host "" + Write-Host "Search: " -NoNewline + [Console]::CursorVisible = $true + $searchTerm = Read-Host + [Console]::CursorVisible = $false + + if ($searchTerm) { + $filteredApps = $Apps | Where-Object { $_.Name -like "*$searchTerm*" } + } + else { + $filteredApps = $Apps + } + $currentIndex = 0 + $pageStart = 0 + } + 'Backspace' { + if ($searchTerm) { + $searchTerm = "" + $filteredApps = $Apps + $currentIndex = 0 + $pageStart = 0 + } + } + } + } + } + finally { + [Console]::CursorVisible = $true + } +} + +# ============================================================================ +# Uninstallation +# ============================================================================ + +function Uninstall-SelectedApps { + <# + .SYNOPSIS + Uninstall the selected applications + #> + param([array]$Apps) + + $esc = [char]27 + + Write-Host "" + Write-Host "$esc[1;35mUninstalling Applications$esc[0m" + Write-Host "" + + $successCount = 0 + $failCount = 0 + + foreach ($app in $Apps) { + Write-Host "$esc[34m$($script:Icons.Arrow)$esc[0m Uninstalling: $($app.Name)" -NoNewline + + try { + if ($app.Source -eq "WindowsStore") { + # UWP app + if ($app.PackageFullName) { + Remove-AppxPackage -Package $app.PackageFullName -ErrorAction Stop + Write-Host " $esc[32m$($script:Icons.Success)$esc[0m" + $successCount++ + } + } + else { + # Registry app with uninstall string + $uninstallString = $app.UninstallString + + # Handle different uninstall types + if ($uninstallString -like "MsiExec.exe*") { + # MSI uninstall + $productCode = [regex]::Match($uninstallString, '\{[0-9A-F-]+\}').Value + if ($productCode) { + $process = Start-Process -FilePath "msiexec.exe" ` + -ArgumentList "/x", $productCode, "/qn", "/norestart" ` + -Wait -PassThru -NoNewWindow + + if ($process.ExitCode -eq 0 -or $process.ExitCode -eq 3010) { + Write-Host " $esc[32m$($script:Icons.Success)$esc[0m" + $successCount++ + } + else { + Write-Host " $esc[33m(requires interaction)$esc[0m" + # Fallback to interactive uninstall + Start-Process -FilePath "msiexec.exe" -ArgumentList "/x", $productCode -Wait + $successCount++ + } + } + } + else { + # Direct executable uninstall + # Try silent uninstall first + $silentArgs = @("/S", "/silent", "/quiet", "-s", "-silent", "-quiet", "/VERYSILENT") + $uninstalled = $false + + foreach ($arg in $silentArgs) { + try { + $process = Start-Process -FilePath "cmd.exe" ` + -ArgumentList "/c", "`"$uninstallString`"", $arg ` + -Wait -PassThru -NoNewWindow -ErrorAction SilentlyContinue + + if ($process.ExitCode -eq 0) { + Write-Host " $esc[32m$($script:Icons.Success)$esc[0m" + $successCount++ + $uninstalled = $true + break + } + } + catch { } + } + + if (-not $uninstalled) { + # Fallback to interactive + Write-Host " $esc[33m(launching uninstaller)$esc[0m" + Start-Process -FilePath "cmd.exe" -ArgumentList "/c", "`"$uninstallString`"" -Wait + $successCount++ + } + } + } + + # Clean leftover files + if ($app.InstallLocation -and (Test-Path $app.InstallLocation)) { + Write-Host " $esc[90mCleaning leftover files...$esc[0m" + Remove-SafeItem -Path $app.InstallLocation -Description "Leftover files" -Recurse + } + } + catch { + Write-Host " $esc[31m$($script:Icons.Error)$esc[0m" + Write-Debug "Uninstall failed: $_" + $failCount++ + } + } + + # Summary + Write-Host "" + Write-Host "$esc[1;35mUninstall Complete$esc[0m" + Write-Host " Successfully uninstalled: $esc[32m$successCount$esc[0m" + if ($failCount -gt 0) { + Write-Host " Failed: $esc[31m$failCount$esc[0m" + } + Write-Host "" + + # Clear cache + if (Test-Path $script:AppCacheFile) { + Remove-Item $script:AppCacheFile -Force -ErrorAction SilentlyContinue + } +} + +# ============================================================================ +# Main Entry Point +# ============================================================================ + +function Main { + # Enable debug if requested + if ($DebugMode) { + $env:MOLE_DEBUG = "1" + $DebugPreference = "Continue" + } + + # Show help + if ($ShowHelp) { + Show-UninstallHelp + return + } + + # Clear screen + Clear-Host + + # Get installed apps + $apps = Get-InstalledApplications -ForceRescan:$Rescan + + if ($apps.Count -eq 0) { + Write-Warning "No applications found" + return + } + + Write-Info "Found $($apps.Count) applications" + + # Show selection menu + $selected = Show-AppSelectionMenu -Apps $apps + + if ($selected.Count -eq 0) { + Write-Info "No applications selected" + return + } + + # Confirm uninstall + $esc = [char]27 + Clear-Host + Write-Host "" + Write-Host "$esc[33mThe following applications will be uninstalled:$esc[0m" + Write-Host "" + + foreach ($app in $selected) { + Write-Host " $($script:Icons.List) $($app.Name) ($($app.SizeHuman))" + } + + Write-Host "" + $confirm = Read-Host "Continue? (y/N)" + + if ($confirm -eq 'y' -or $confirm -eq 'Y') { + Uninstall-SelectedApps -Apps $selected + } + else { + Write-Info "Cancelled" + } +} + +# Run main +Main diff --git a/windows/lib/clean/apps.ps1 b/windows/lib/clean/apps.ps1 new file mode 100644 index 0000000..2ec6557 --- /dev/null +++ b/windows/lib/clean/apps.ps1 @@ -0,0 +1,429 @@ +# Mole - Application-Specific Cleanup Module +# Cleans leftover data from uninstalled apps and app-specific caches + +#Requires -Version 5.1 +Set-StrictMode -Version Latest + +# Prevent multiple sourcing +if ((Get-Variable -Name 'MOLE_CLEAN_APPS_LOADED' -Scope Script -ErrorAction SilentlyContinue) -and $script:MOLE_CLEAN_APPS_LOADED) { return } +$script:MOLE_CLEAN_APPS_LOADED = $true + +# Import dependencies +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +. "$scriptDir\..\core\base.ps1" +. "$scriptDir\..\core\log.ps1" +. "$scriptDir\..\core\file_ops.ps1" + +# ============================================================================ +# Orphaned App Data Detection +# ============================================================================ + +function Get-InstalledPrograms { + <# + .SYNOPSIS + Get list of installed programs from registry + #> + + $programs = @() + + $registryPaths = @( + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" + "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" + "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" + ) + + foreach ($path in $registryPaths) { + $items = Get-ItemProperty -Path $path -ErrorAction SilentlyContinue | + Where-Object { $_.DisplayName } | + Select-Object DisplayName, InstallLocation, Publisher + if ($items) { + $programs += $items + } + } + + # Also check UWP apps + try { + $uwpApps = Get-AppxPackage -ErrorAction SilentlyContinue | + Select-Object @{N='DisplayName';E={$_.Name}}, @{N='InstallLocation';E={$_.InstallLocation}}, Publisher + if ($uwpApps) { + $programs += $uwpApps + } + } + catch { + Write-Debug "Could not enumerate UWP apps: $_" + } + + return $programs +} + +function Find-OrphanedAppData { + <# + .SYNOPSIS + Find app data folders for apps that are no longer installed + #> + param([int]$DaysOld = 60) + + $installedPrograms = Get-InstalledPrograms + $installedNames = $installedPrograms | ForEach-Object { $_.DisplayName.ToLower() } + + $orphanedPaths = @() + $cutoffDate = (Get-Date).AddDays(-$DaysOld) + + # Check common app data locations + $appDataPaths = @( + @{ Path = $env:APPDATA; Type = "Roaming" } + @{ Path = $env:LOCALAPPDATA; Type = "Local" } + ) + + foreach ($location in $appDataPaths) { + if (-not (Test-Path $location.Path)) { continue } + + $folders = Get-ChildItem -Path $location.Path -Directory -ErrorAction SilentlyContinue + + foreach ($folder in $folders) { + # Skip system folders + $skipFolders = @('Microsoft', 'Windows', 'Packages', 'Programs', 'Temp', 'Roaming') + if ($folder.Name -in $skipFolders) { continue } + + # Skip if recently modified + if ($folder.LastWriteTime -gt $cutoffDate) { continue } + + # Check if app is installed + $isInstalled = $false + foreach ($name in $installedNames) { + if ($name -like "*$($folder.Name.ToLower())*" -or $folder.Name.ToLower() -like "*$name*") { + $isInstalled = $true + break + } + } + + if (-not $isInstalled) { + $orphanedPaths += @{ + Path = $folder.FullName + Name = $folder.Name + Type = $location.Type + Size = (Get-PathSize -Path $folder.FullName) + LastModified = $folder.LastWriteTime + } + } + } + } + + return $orphanedPaths +} + +function Clear-OrphanedAppData { + <# + .SYNOPSIS + Clean orphaned application data + #> + param([int]$DaysOld = 60) + + Start-Section "Orphaned app data" + + $orphaned = Find-OrphanedAppData -DaysOld $DaysOld + + if ($orphaned.Count -eq 0) { + Write-Info "No orphaned app data found" + Stop-Section + return + } + + # Filter by size (only clean if > 10MB to avoid noise) + $significantOrphans = $orphaned | Where-Object { $_.Size -gt 10MB } + + if ($significantOrphans.Count -gt 0) { + $totalSize = ($significantOrphans | Measure-Object -Property Size -Sum).Sum + $sizeHuman = Format-ByteSize -Bytes $totalSize + + Write-Info "Found $($significantOrphans.Count) orphaned folders ($sizeHuman)" + + foreach ($orphan in $significantOrphans) { + $orphanSize = Format-ByteSize -Bytes $orphan.Size + Remove-SafeItem -Path $orphan.Path -Description "$($orphan.Name) ($orphanSize)" -Recurse + } + } + + Stop-Section +} + +# ============================================================================ +# Specific Application Cleanup +# ============================================================================ + +function Clear-OfficeCache { + <# + .SYNOPSIS + Clean Microsoft Office caches and temp files + #> + + $officeCachePaths = @( + # Office 365 / 2019 / 2021 + "$env:LOCALAPPDATA\Microsoft\Office\16.0\OfficeFileCache" + "$env:LOCALAPPDATA\Microsoft\Office\16.0\Wef" + "$env:LOCALAPPDATA\Microsoft\Outlook\RoamCache" + "$env:LOCALAPPDATA\Microsoft\Outlook\Offline Address Books" + # Older Office versions + "$env:LOCALAPPDATA\Microsoft\Office\15.0\OfficeFileCache" + # Office temp files + "$env:APPDATA\Microsoft\Templates\*.tmp" + "$env:APPDATA\Microsoft\Word\*.tmp" + "$env:APPDATA\Microsoft\Excel\*.tmp" + "$env:APPDATA\Microsoft\PowerPoint\*.tmp" + ) + + foreach ($path in $officeCachePaths) { + if ($path -like "*.tmp") { + $parent = Split-Path -Parent $path + if (Test-Path $parent) { + $tmpFiles = Get-ChildItem -Path $parent -Filter "*.tmp" -File -ErrorAction SilentlyContinue + if ($tmpFiles) { + $paths = $tmpFiles | ForEach-Object { $_.FullName } + Remove-SafeItems -Paths $paths -Description "Office temp files" + } + } + } + elseif (Test-Path $path) { + Clear-DirectoryContents -Path $path -Description "Office $(Split-Path -Leaf $path)" + } + } +} + +function Clear-OneDriveCache { + <# + .SYNOPSIS + Clean OneDrive cache + #> + + $oneDriveCachePaths = @( + "$env:LOCALAPPDATA\Microsoft\OneDrive\logs" + "$env:LOCALAPPDATA\Microsoft\OneDrive\setup\logs" + ) + + foreach ($path in $oneDriveCachePaths) { + if (Test-Path $path) { + Remove-OldFiles -Path $path -DaysOld 7 -Description "OneDrive logs" + } + } +} + +function Clear-DroplboxCache { + <# + .SYNOPSIS + Clean Dropbox cache + #> + + # Dropbox cache is typically in the Dropbox folder itself + $dropboxInfoPath = "$env:LOCALAPPDATA\Dropbox\info.json" + + if (Test-Path $dropboxInfoPath) { + try { + $dropboxInfo = Get-Content $dropboxInfoPath | ConvertFrom-Json + $dropboxPath = $dropboxInfo.personal.path + + if ($dropboxPath) { + $dropboxCachePath = "$dropboxPath\.dropbox.cache" + if (Test-Path $dropboxCachePath) { + Clear-DirectoryContents -Path $dropboxCachePath -Description "Dropbox cache" + } + } + } + catch { + Write-Debug "Could not read Dropbox config: $_" + } + } +} + +function Clear-GoogleDriveCache { + <# + .SYNOPSIS + Clean Google Drive cache + #> + + $googleDriveCachePaths = @( + "$env:LOCALAPPDATA\Google\DriveFS\Logs" + "$env:LOCALAPPDATA\Google\DriveFS\*.tmp" + ) + + foreach ($path in $googleDriveCachePaths) { + if ($path -like "*.tmp") { + $parent = Split-Path -Parent $path + if (Test-Path $parent) { + $tmpFiles = Get-ChildItem -Path $parent -Filter "*.tmp" -ErrorAction SilentlyContinue + if ($tmpFiles) { + $paths = $tmpFiles | ForEach-Object { $_.FullName } + Remove-SafeItems -Paths $paths -Description "Google Drive temp" + } + } + } + elseif (Test-Path $path) { + Remove-OldFiles -Path $path -DaysOld 7 -Description "Google Drive logs" + } + } +} + +function Clear-AdobeData { + <# + .SYNOPSIS + Clean Adobe application caches and temp files + #> + + $adobeCachePaths = @( + "$env:APPDATA\Adobe\Common\Media Cache Files" + "$env:APPDATA\Adobe\Common\Peak Files" + "$env:APPDATA\Adobe\Common\Team Projects Cache" + "$env:LOCALAPPDATA\Adobe\*\Cache" + "$env:LOCALAPPDATA\Adobe\*\CameraRaw\Cache" + "$env:LOCALAPPDATA\Temp\Adobe" + ) + + foreach ($pattern in $adobeCachePaths) { + $paths = Resolve-Path $pattern -ErrorAction SilentlyContinue + foreach ($path in $paths) { + if (Test-Path $path.Path) { + Clear-DirectoryContents -Path $path.Path -Description "Adobe cache" + } + } + } +} + +function Clear-AutodeskData { + <# + .SYNOPSIS + Clean Autodesk application caches + #> + + $autodeskCachePaths = @( + "$env:LOCALAPPDATA\Autodesk\*\Cache" + "$env:APPDATA\Autodesk\*\cache" + ) + + foreach ($pattern in $autodeskCachePaths) { + $paths = Resolve-Path $pattern -ErrorAction SilentlyContinue + foreach ($path in $paths) { + if (Test-Path $path.Path) { + Clear-DirectoryContents -Path $path.Path -Description "Autodesk cache" + } + } + } +} + +# ============================================================================ +# Gaming Platform Cleanup +# ============================================================================ + +function Clear-GamingPlatformCaches { + <# + .SYNOPSIS + Clean gaming platform caches (Steam, Epic, Origin, etc.) + #> + + # Steam + $steamPaths = @( + "${env:ProgramFiles(x86)}\Steam\appcache\httpcache" + "${env:ProgramFiles(x86)}\Steam\appcache\librarycache" + "${env:ProgramFiles(x86)}\Steam\logs" + ) + foreach ($path in $steamPaths) { + if (Test-Path $path) { + Clear-DirectoryContents -Path $path -Description "Steam $(Split-Path -Leaf $path)" + } + } + + # Epic Games Launcher + $epicPaths = @( + "$env:LOCALAPPDATA\EpicGamesLauncher\Saved\webcache" + "$env:LOCALAPPDATA\EpicGamesLauncher\Saved\Logs" + ) + foreach ($path in $epicPaths) { + if (Test-Path $path) { + Clear-DirectoryContents -Path $path -Description "Epic Games $(Split-Path -Leaf $path)" + } + } + + # EA App (Origin replacement) + $eaPaths = @( + "$env:LOCALAPPDATA\Electronic Arts\EA Desktop\cache" + "$env:APPDATA\Origin\*\cache" + ) + foreach ($pattern in $eaPaths) { + $paths = Resolve-Path $pattern -ErrorAction SilentlyContinue + foreach ($path in $paths) { + if (Test-Path $path.Path) { + Clear-DirectoryContents -Path $path.Path -Description "EA/Origin cache" + } + } + } + + # GOG Galaxy + $gogPaths = @( + "$env:LOCALAPPDATA\GOG.com\Galaxy\webcache" + "$env:PROGRAMDATA\GOG.com\Galaxy\logs" + ) + foreach ($path in $gogPaths) { + if (Test-Path $path) { + Clear-DirectoryContents -Path $path -Description "GOG Galaxy $(Split-Path -Leaf $path)" + } + } + + # Ubisoft Connect + $ubiPaths = @( + "$env:LOCALAPPDATA\Ubisoft Game Launcher\cache" + "$env:LOCALAPPDATA\Ubisoft Game Launcher\logs" + ) + foreach ($path in $ubiPaths) { + if (Test-Path $path) { + Clear-DirectoryContents -Path $path -Description "Ubisoft $(Split-Path -Leaf $path)" + } + } + + # Battle.net + $battlenetPaths = @( + "$env:APPDATA\Battle.net\Cache" + "$env:APPDATA\Battle.net\Logs" + ) + foreach ($path in $battlenetPaths) { + if (Test-Path $path) { + Clear-DirectoryContents -Path $path -Description "Battle.net $(Split-Path -Leaf $path)" + } + } +} + +# ============================================================================ +# Main Application Cleanup Function +# ============================================================================ + +function Invoke-AppCleanup { + <# + .SYNOPSIS + Run all application-specific cleanup tasks + #> + param([switch]$IncludeOrphaned) + + Start-Section "Applications" + + # Productivity apps + Clear-OfficeCache + Clear-OneDriveCache + Clear-DroplboxCache + Clear-GoogleDriveCache + + # Creative apps + Clear-AdobeData + Clear-AutodeskData + + # Gaming platforms + Clear-GamingPlatformCaches + + Stop-Section + + # Orphaned app data (separate section) + if ($IncludeOrphaned) { + Clear-OrphanedAppData -DaysOld 60 + } +} + +# ============================================================================ +# Exports +# ============================================================================ +# Functions: Get-InstalledPrograms, Find-OrphanedAppData, Clear-OfficeCache, etc. diff --git a/windows/lib/clean/caches.ps1 b/windows/lib/clean/caches.ps1 new file mode 100644 index 0000000..bba2550 --- /dev/null +++ b/windows/lib/clean/caches.ps1 @@ -0,0 +1,385 @@ +# Mole - Cache Cleanup Module +# Cleans Windows and application caches + +#Requires -Version 5.1 +Set-StrictMode -Version Latest + +# Prevent multiple sourcing +if ((Get-Variable -Name 'MOLE_CLEAN_CACHES_LOADED' -Scope Script -ErrorAction SilentlyContinue) -and $script:MOLE_CLEAN_CACHES_LOADED) { return } +$script:MOLE_CLEAN_CACHES_LOADED = $true + +# Import dependencies +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +. "$scriptDir\..\core\base.ps1" +. "$scriptDir\..\core\log.ps1" +. "$scriptDir\..\core\file_ops.ps1" + +# ============================================================================ +# Windows System Caches +# ============================================================================ + +function Clear-WindowsUpdateCache { + <# + .SYNOPSIS + Clean Windows Update cache (requires admin) + #> + + if (-not (Test-IsAdmin)) { + Write-Debug "Skipping Windows Update cache - requires admin" + return + } + + $wuPath = "$env:WINDIR\SoftwareDistribution\Download" + + if (Test-Path $wuPath) { + # Stop Windows Update service first + if (Test-DryRunMode) { + Write-DryRun "Windows Update cache" + Set-SectionActivity + return + } + + try { + Stop-Service -Name wuauserv -Force -ErrorAction SilentlyContinue + Clear-DirectoryContents -Path $wuPath -Description "Windows Update cache" + Start-Service -Name wuauserv -ErrorAction SilentlyContinue + } + catch { + Write-Debug "Could not clear Windows Update cache: $_" + Start-Service -Name wuauserv -ErrorAction SilentlyContinue + } + } +} + +function Clear-DeliveryOptimizationCache { + <# + .SYNOPSIS + Clean Windows Delivery Optimization cache (requires admin) + #> + + if (-not (Test-IsAdmin)) { + Write-Debug "Skipping Delivery Optimization cache - requires admin" + return + } + + $doPath = "$env:WINDIR\ServiceProfiles\NetworkService\AppData\Local\Microsoft\Windows\DeliveryOptimization" + + if (Test-Path $doPath) { + if (Test-DryRunMode) { + Write-DryRun "Delivery Optimization cache" + Set-SectionActivity + return + } + + try { + Stop-Service -Name DoSvc -Force -ErrorAction SilentlyContinue + Clear-DirectoryContents -Path "$doPath\Cache" -Description "Delivery Optimization cache" + Start-Service -Name DoSvc -ErrorAction SilentlyContinue + } + catch { + Write-Debug "Could not clear Delivery Optimization cache: $_" + Start-Service -Name DoSvc -ErrorAction SilentlyContinue + } + } +} + +function Clear-FontCache { + <# + .SYNOPSIS + Clean Windows font cache (requires admin) + #> + + if (-not (Test-IsAdmin)) { + return + } + + $fontCachePath = "$env:LOCALAPPDATA\Microsoft\Windows\Fonts\FontCache" + + if (Test-Path $fontCachePath) { + Remove-SafeItem -Path $fontCachePath -Description "Font cache" + } +} + +# ============================================================================ +# Browser Caches +# ============================================================================ + +function Clear-BrowserCaches { + <# + .SYNOPSIS + Clean browser cache directories + #> + + Start-Section "Browser caches" + + # Chrome + $chromeCachePaths = @( + "$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\Google\Chrome\User Data\Default\Service Worker\CacheStorage" + "$env:LOCALAPPDATA\Google\Chrome\User Data\ShaderCache" + "$env:LOCALAPPDATA\Google\Chrome\User Data\GrShaderCache" + ) + + foreach ($path in $chromeCachePaths) { + if (Test-Path $path) { + Clear-DirectoryContents -Path $path -Description "Chrome $(Split-Path -Leaf $path)" + } + } + + # Edge + $edgeCachePaths = @( + "$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:LOCALAPPDATA\Microsoft\Edge\User Data\Default\Service Worker\CacheStorage" + "$env:LOCALAPPDATA\Microsoft\Edge\User Data\ShaderCache" + ) + + foreach ($path in $edgeCachePaths) { + if (Test-Path $path) { + Clear-DirectoryContents -Path $path -Description "Edge $(Split-Path -Leaf $path)" + } + } + + # Firefox + $firefoxProfiles = "$env:APPDATA\Mozilla\Firefox\Profiles" + if (Test-Path $firefoxProfiles) { + $profiles = Get-ChildItem -Path $firefoxProfiles -Directory -ErrorAction SilentlyContinue + foreach ($profile in $profiles) { + $firefoxCachePaths = @( + "$($profile.FullName)\cache2" + "$($profile.FullName)\startupCache" + "$($profile.FullName)\shader-cache" + ) + foreach ($path in $firefoxCachePaths) { + if (Test-Path $path) { + Clear-DirectoryContents -Path $path -Description "Firefox cache" + } + } + } + } + + # Brave + $braveCachePath = "$env:LOCALAPPDATA\BraveSoftware\Brave-Browser\User Data\Default\Cache" + if (Test-Path $braveCachePath) { + Clear-DirectoryContents -Path $braveCachePath -Description "Brave cache" + } + + # Opera + $operaCachePath = "$env:APPDATA\Opera Software\Opera Stable\Cache" + if (Test-Path $operaCachePath) { + Clear-DirectoryContents -Path $operaCachePath -Description "Opera cache" + } + + Stop-Section +} + +# ============================================================================ +# Application Caches +# ============================================================================ + +function Clear-AppCaches { + <# + .SYNOPSIS + Clean common application caches + #> + + Start-Section "Application caches" + + # Spotify + $spotifyCachePaths = @( + "$env:LOCALAPPDATA\Spotify\Data" + "$env:LOCALAPPDATA\Spotify\Storage" + ) + foreach ($path in $spotifyCachePaths) { + if (Test-Path $path) { + Clear-DirectoryContents -Path $path -Description "Spotify cache" + } + } + + # Discord + $discordCachePaths = @( + "$env:APPDATA\discord\Cache" + "$env:APPDATA\discord\Code Cache" + "$env:APPDATA\discord\GPUCache" + ) + foreach ($path in $discordCachePaths) { + if (Test-Path $path) { + Clear-DirectoryContents -Path $path -Description "Discord cache" + } + } + + # Slack + $slackCachePaths = @( + "$env:APPDATA\Slack\Cache" + "$env:APPDATA\Slack\Code Cache" + "$env:APPDATA\Slack\GPUCache" + "$env:APPDATA\Slack\Service Worker\CacheStorage" + ) + foreach ($path in $slackCachePaths) { + if (Test-Path $path) { + Clear-DirectoryContents -Path $path -Description "Slack cache" + } + } + + # Teams + $teamsCachePaths = @( + "$env:APPDATA\Microsoft\Teams\Cache" + "$env:APPDATA\Microsoft\Teams\blob_storage" + "$env:APPDATA\Microsoft\Teams\databases" + "$env:APPDATA\Microsoft\Teams\GPUCache" + "$env:APPDATA\Microsoft\Teams\IndexedDB" + "$env:APPDATA\Microsoft\Teams\Local Storage" + "$env:APPDATA\Microsoft\Teams\tmp" + ) + foreach ($path in $teamsCachePaths) { + if (Test-Path $path) { + Clear-DirectoryContents -Path $path -Description "Teams cache" + } + } + + # VS Code + $vscodeCachePaths = @( + "$env:APPDATA\Code\Cache" + "$env:APPDATA\Code\CachedData" + "$env:APPDATA\Code\CachedExtensions" + "$env:APPDATA\Code\CachedExtensionVSIXs" + "$env:APPDATA\Code\Code Cache" + "$env:APPDATA\Code\GPUCache" + ) + foreach ($path in $vscodeCachePaths) { + if (Test-Path $path) { + Clear-DirectoryContents -Path $path -Description "VS Code cache" + } + } + + # Zoom + $zoomCachePath = "$env:APPDATA\Zoom\data" + if (Test-Path $zoomCachePath) { + Clear-DirectoryContents -Path $zoomCachePath -Description "Zoom cache" + } + + # Adobe Creative Cloud + $adobeCachePaths = @( + "$env:LOCALAPPDATA\Adobe\*\Cache" + "$env:APPDATA\Adobe\Common\Media Cache Files" + "$env:APPDATA\Adobe\Common\Peak Files" + ) + foreach ($pattern in $adobeCachePaths) { + $paths = Resolve-Path $pattern -ErrorAction SilentlyContinue + foreach ($path in $paths) { + if (Test-Path $path) { + Clear-DirectoryContents -Path $path.Path -Description "Adobe cache" + } + } + } + + # Steam (download cache, not games) + $steamCachePath = "${env:ProgramFiles(x86)}\Steam\appcache" + if (Test-Path $steamCachePath) { + Clear-DirectoryContents -Path $steamCachePath -Description "Steam app cache" + } + + # Epic Games Launcher + $epicCachePath = "$env:LOCALAPPDATA\EpicGamesLauncher\Saved\webcache" + if (Test-Path $epicCachePath) { + Clear-DirectoryContents -Path $epicCachePath -Description "Epic Games cache" + } + + Stop-Section +} + +# ============================================================================ +# Windows Store / UWP App Caches +# ============================================================================ + +function Clear-StoreAppCaches { + <# + .SYNOPSIS + Clean Windows Store and UWP app caches + #> + + # Microsoft Store cache + $storeCache = "$env:LOCALAPPDATA\Microsoft\Windows\WCN" + if (Test-Path $storeCache) { + Clear-DirectoryContents -Path $storeCache -Description "Windows Store cache" + } + + # Store app temp files + $storeTemp = "$env:LOCALAPPDATA\Packages\Microsoft.WindowsStore_*\LocalCache" + $storePaths = Resolve-Path $storeTemp -ErrorAction SilentlyContinue + foreach ($path in $storePaths) { + if (Test-Path $path.Path) { + Clear-DirectoryContents -Path $path.Path -Description "Store LocalCache" + } + } +} + +# ============================================================================ +# .NET / Runtime Caches +# ============================================================================ + +function Clear-DotNetCaches { + <# + .SYNOPSIS + Clean .NET runtime caches + #> + + # .NET temp files + $dotnetTemp = "$env:LOCALAPPDATA\Temp\Microsoft.NET" + if (Test-Path $dotnetTemp) { + Clear-DirectoryContents -Path $dotnetTemp -Description ".NET temp files" + } + + # NGen cache (don't touch - managed by Windows) + # Assembly cache (don't touch - managed by CLR) +} + +# ============================================================================ +# Main Cache Cleanup Function +# ============================================================================ + +function Invoke-CacheCleanup { + <# + .SYNOPSIS + Run all cache cleanup tasks + #> + param( + [switch]$IncludeWindowsUpdate, + [switch]$IncludeBrowsers, + [switch]$IncludeApps + ) + + Start-Section "System caches" + + # Windows system caches (if admin) + if (Test-IsAdmin) { + if ($IncludeWindowsUpdate) { + Clear-WindowsUpdateCache + Clear-DeliveryOptimizationCache + } + Clear-FontCache + } + + Clear-StoreAppCaches + Clear-DotNetCaches + + Stop-Section + + # Browser caches + if ($IncludeBrowsers) { + Clear-BrowserCaches + } + + # Application caches + if ($IncludeApps) { + Clear-AppCaches + } +} + +# ============================================================================ +# Exports +# ============================================================================ +# Functions: Clear-WindowsUpdateCache, Clear-BrowserCaches, Clear-AppCaches, etc. diff --git a/windows/lib/clean/dev.ps1 b/windows/lib/clean/dev.ps1 new file mode 100644 index 0000000..5e85d5e --- /dev/null +++ b/windows/lib/clean/dev.ps1 @@ -0,0 +1,537 @@ +# Mole - Developer Tools Cleanup Module +# Cleans development tool caches and build artifacts + +#Requires -Version 5.1 +Set-StrictMode -Version Latest + +# Prevent multiple sourcing +if ((Get-Variable -Name 'MOLE_CLEAN_DEV_LOADED' -Scope Script -ErrorAction SilentlyContinue) -and $script:MOLE_CLEAN_DEV_LOADED) { return } +$script:MOLE_CLEAN_DEV_LOADED = $true + +# Import dependencies +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +. "$scriptDir\..\core\base.ps1" +. "$scriptDir\..\core\log.ps1" +. "$scriptDir\..\core\file_ops.ps1" + +# ============================================================================ +# Node.js / JavaScript Ecosystem +# ============================================================================ + +function Clear-NpmCache { + <# + .SYNOPSIS + Clean npm, pnpm, yarn, and bun caches + #> + + # npm cache + if (Get-Command npm -ErrorAction SilentlyContinue) { + if (Test-DryRunMode) { + Write-DryRun "npm cache" + } + else { + try { + $null = npm cache clean --force 2>&1 + Write-Success "npm cache" + Set-SectionActivity + } + catch { + Write-Debug "npm cache clean failed: $_" + } + } + } + + # npm cache directory (fallback) + $npmCachePath = "$env:APPDATA\npm-cache" + if (Test-Path $npmCachePath) { + Clear-DirectoryContents -Path $npmCachePath -Description "npm cache directory" + } + + # pnpm store + $pnpmStorePath = "$env:LOCALAPPDATA\pnpm\store" + if (Test-Path $pnpmStorePath) { + if (Get-Command pnpm -ErrorAction SilentlyContinue) { + if (Test-DryRunMode) { + Write-DryRun "pnpm store" + } + else { + try { + $null = pnpm store prune 2>&1 + Write-Success "pnpm store pruned" + Set-SectionActivity + } + catch { + Write-Debug "pnpm store prune failed: $_" + } + } + } + } + + # Yarn cache + $yarnCachePaths = @( + "$env:LOCALAPPDATA\Yarn\Cache" + "$env:USERPROFILE\.yarn\cache" + ) + foreach ($path in $yarnCachePaths) { + if (Test-Path $path) { + Clear-DirectoryContents -Path $path -Description "Yarn cache" + } + } + + # Bun cache + $bunCachePath = "$env:USERPROFILE\.bun\install\cache" + if (Test-Path $bunCachePath) { + Clear-DirectoryContents -Path $bunCachePath -Description "Bun cache" + } +} + +function Clear-NodeBuildCaches { + <# + .SYNOPSIS + Clean Node.js build-related caches + #> + + # node-gyp + $nodeGypPath = "$env:LOCALAPPDATA\node-gyp\Cache" + if (Test-Path $nodeGypPath) { + Clear-DirectoryContents -Path $nodeGypPath -Description "node-gyp cache" + } + + # Electron cache + $electronCachePath = "$env:LOCALAPPDATA\electron\Cache" + if (Test-Path $electronCachePath) { + Clear-DirectoryContents -Path $electronCachePath -Description "Electron cache" + } + + # TypeScript cache + $tsCachePath = "$env:LOCALAPPDATA\TypeScript" + if (Test-Path $tsCachePath) { + Clear-DirectoryContents -Path $tsCachePath -Description "TypeScript cache" + } +} + +# ============================================================================ +# Python Ecosystem +# ============================================================================ + +function Clear-PythonCaches { + <# + .SYNOPSIS + Clean Python and pip caches + #> + + # pip cache + if (Get-Command pip -ErrorAction SilentlyContinue) { + if (Test-DryRunMode) { + Write-DryRun "pip cache" + } + else { + try { + $null = pip cache purge 2>&1 + Write-Success "pip cache" + Set-SectionActivity + } + catch { + Write-Debug "pip cache purge failed: $_" + } + } + } + + # pip cache directory + $pipCachePath = "$env:LOCALAPPDATA\pip\Cache" + if (Test-Path $pipCachePath) { + Clear-DirectoryContents -Path $pipCachePath -Description "pip cache directory" + } + + # Python bytecode caches (__pycache__) + # Note: These are typically in project directories, cleaned by purge command + + # pyenv cache + $pyenvCachePath = "$env:USERPROFILE\.pyenv\cache" + if (Test-Path $pyenvCachePath) { + Clear-DirectoryContents -Path $pyenvCachePath -Description "pyenv cache" + } + + # Poetry cache + $poetryCachePath = "$env:LOCALAPPDATA\pypoetry\Cache" + if (Test-Path $poetryCachePath) { + Clear-DirectoryContents -Path $poetryCachePath -Description "Poetry cache" + } + + # conda packages + $condaCachePaths = @( + "$env:USERPROFILE\.conda\pkgs" + "$env:USERPROFILE\anaconda3\pkgs" + "$env:USERPROFILE\miniconda3\pkgs" + ) + foreach ($path in $condaCachePaths) { + if (Test-Path $path) { + # Only clean index and temp files, not actual packages + $tempFiles = Get-ChildItem -Path $path -Filter "*.tmp" -ErrorAction SilentlyContinue + if ($tempFiles) { + $paths = $tempFiles | ForEach-Object { $_.FullName } + Remove-SafeItems -Paths $paths -Description "Conda temp files" + } + } + } + + # Jupyter runtime + $jupyterRuntimePath = "$env:APPDATA\jupyter\runtime" + if (Test-Path $jupyterRuntimePath) { + Clear-DirectoryContents -Path $jupyterRuntimePath -Description "Jupyter runtime" + } + + # pytest cache + $pytestCachePath = "$env:USERPROFILE\.pytest_cache" + if (Test-Path $pytestCachePath) { + Remove-SafeItem -Path $pytestCachePath -Description "pytest cache" -Recurse + } +} + +# ============================================================================ +# .NET / C# Ecosystem +# ============================================================================ + +function Clear-DotNetDevCaches { + <# + .SYNOPSIS + Clean .NET development caches + #> + + # NuGet cache + $nugetCachePath = "$env:USERPROFILE\.nuget\packages" + # Don't clean packages by default - they're needed for builds + # Only clean http-cache and temp + + $nugetHttpCache = "$env:LOCALAPPDATA\NuGet\v3-cache" + if (Test-Path $nugetHttpCache) { + Clear-DirectoryContents -Path $nugetHttpCache -Description "NuGet HTTP cache" + } + + $nugetTempPath = "$env:LOCALAPPDATA\NuGet\plugins-cache" + if (Test-Path $nugetTempPath) { + Clear-DirectoryContents -Path $nugetTempPath -Description "NuGet plugins cache" + } + + # MSBuild temp files + $msbuildTemp = "$env:LOCALAPPDATA\Microsoft\MSBuild" + if (Test-Path $msbuildTemp) { + $tempDirs = Get-ChildItem -Path $msbuildTemp -Directory -Filter "*temp*" -ErrorAction SilentlyContinue + foreach ($dir in $tempDirs) { + Clear-DirectoryContents -Path $dir.FullName -Description "MSBuild temp" + } + } +} + +# ============================================================================ +# Go Ecosystem +# ============================================================================ + +function Clear-GoCaches { + <# + .SYNOPSIS + Clean Go build and module caches + #> + + if (Get-Command go -ErrorAction SilentlyContinue) { + if (Test-DryRunMode) { + Write-DryRun "Go cache" + } + else { + try { + $null = go clean -cache 2>&1 + Write-Success "Go build cache" + Set-SectionActivity + } + catch { + Write-Debug "go clean -cache failed: $_" + } + } + } + + # Go module cache + $goModCachePath = "$env:GOPATH\pkg\mod\cache" + if (-not $env:GOPATH) { + $goModCachePath = "$env:USERPROFILE\go\pkg\mod\cache" + } + if (Test-Path $goModCachePath) { + Clear-DirectoryContents -Path $goModCachePath -Description "Go module cache" + } +} + +# ============================================================================ +# Rust Ecosystem +# ============================================================================ + +function Clear-RustCaches { + <# + .SYNOPSIS + Clean Rust/Cargo caches + #> + + # Cargo registry cache + $cargoRegistryCache = "$env:USERPROFILE\.cargo\registry\cache" + if (Test-Path $cargoRegistryCache) { + Clear-DirectoryContents -Path $cargoRegistryCache -Description "Cargo registry cache" + } + + # Cargo git cache + $cargoGitCache = "$env:USERPROFILE\.cargo\git\checkouts" + if (Test-Path $cargoGitCache) { + Clear-DirectoryContents -Path $cargoGitCache -Description "Cargo git cache" + } + + # Rustup downloads + $rustupDownloads = "$env:USERPROFILE\.rustup\downloads" + if (Test-Path $rustupDownloads) { + Clear-DirectoryContents -Path $rustupDownloads -Description "Rustup downloads" + } +} + +# ============================================================================ +# Java / JVM Ecosystem +# ============================================================================ + +function Clear-JvmCaches { + <# + .SYNOPSIS + Clean JVM ecosystem caches (Gradle, Maven, etc.) + #> + + # Gradle caches + $gradleCachePaths = @( + "$env:USERPROFILE\.gradle\caches" + "$env:USERPROFILE\.gradle\daemon" + "$env:USERPROFILE\.gradle\wrapper\dists" + ) + foreach ($path in $gradleCachePaths) { + if (Test-Path $path) { + # Only clean temp and old daemon logs + $tempFiles = Get-ChildItem -Path $path -Recurse -Filter "*.lock" -ErrorAction SilentlyContinue + if ($tempFiles) { + $paths = $tempFiles | ForEach-Object { $_.FullName } + Remove-SafeItems -Paths $paths -Description "Gradle lock files" + } + } + } + + # Maven repository (only clean temp files) + $mavenRepoPath = "$env:USERPROFILE\.m2\repository" + if (Test-Path $mavenRepoPath) { + $tempFiles = Get-ChildItem -Path $mavenRepoPath -Recurse -Filter "*.lastUpdated" -ErrorAction SilentlyContinue + if ($tempFiles) { + $paths = $tempFiles | ForEach-Object { $_.FullName } + Remove-SafeItems -Paths $paths -Description "Maven update markers" + } + } +} + +# ============================================================================ +# Docker / Containers +# ============================================================================ + +function Clear-DockerCaches { + <# + .SYNOPSIS + Clean Docker build caches and unused data + #> + + if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { + return + } + + # Check if Docker daemon is running + $dockerRunning = $false + try { + $null = docker info 2>&1 + $dockerRunning = $true + } + catch { + Write-Debug "Docker daemon not running" + } + + if ($dockerRunning) { + if (Test-DryRunMode) { + Write-DryRun "Docker build cache" + } + else { + try { + $null = docker builder prune -af 2>&1 + Write-Success "Docker build cache" + Set-SectionActivity + } + catch { + Write-Debug "docker builder prune failed: $_" + } + } + } + + # Docker Desktop cache (Windows) + $dockerDesktopCache = "$env:LOCALAPPDATA\Docker\wsl\data" + # Note: Don't clean this - it's the WSL2 virtual disk +} + +# ============================================================================ +# Cloud CLI Tools +# ============================================================================ + +function Clear-CloudCliCaches { + <# + .SYNOPSIS + Clean cloud CLI tool caches (AWS, Azure, GCP) + #> + + # AWS CLI cache + $awsCachePath = "$env:USERPROFILE\.aws\cli\cache" + if (Test-Path $awsCachePath) { + Clear-DirectoryContents -Path $awsCachePath -Description "AWS CLI cache" + } + + # Azure CLI logs + $azureLogsPath = "$env:USERPROFILE\.azure\logs" + if (Test-Path $azureLogsPath) { + Clear-DirectoryContents -Path $azureLogsPath -Description "Azure CLI logs" + } + + # Google Cloud logs + $gcloudLogsPath = "$env:APPDATA\gcloud\logs" + if (Test-Path $gcloudLogsPath) { + Clear-DirectoryContents -Path $gcloudLogsPath -Description "gcloud logs" + } + + # Kubernetes cache + $kubeCachePath = "$env:USERPROFILE\.kube\cache" + if (Test-Path $kubeCachePath) { + Clear-DirectoryContents -Path $kubeCachePath -Description "Kubernetes cache" + } + + # Terraform plugin cache + $terraformCachePath = "$env:APPDATA\terraform.d\plugin-cache" + if (Test-Path $terraformCachePath) { + Clear-DirectoryContents -Path $terraformCachePath -Description "Terraform plugin cache" + } +} + +# ============================================================================ +# IDE Caches +# ============================================================================ + +function Clear-IdeCaches { + <# + .SYNOPSIS + Clean IDE caches (VS, VSCode, JetBrains, etc.) + #> + + # Visual Studio cache + $vsCachePaths = @( + "$env:LOCALAPPDATA\Microsoft\VisualStudio\*\ComponentModelCache" + "$env:LOCALAPPDATA\Microsoft\VisualStudio\*\ImageCache" + ) + foreach ($pattern in $vsCachePaths) { + $paths = Resolve-Path $pattern -ErrorAction SilentlyContinue + foreach ($path in $paths) { + if (Test-Path $path.Path) { + Clear-DirectoryContents -Path $path.Path -Description "Visual Studio cache" + } + } + } + + # JetBrains IDEs caches + $jetbrainsBasePaths = @( + "$env:LOCALAPPDATA\JetBrains" + "$env:APPDATA\JetBrains" + ) + foreach ($basePath in $jetbrainsBasePaths) { + if (Test-Path $basePath) { + $ideFolders = Get-ChildItem -Path $basePath -Directory -ErrorAction SilentlyContinue + foreach ($ideFolder in $ideFolders) { + $cacheFolders = @("caches", "index", "tmp") + foreach ($cacheFolder in $cacheFolders) { + $cachePath = Join-Path $ideFolder.FullName $cacheFolder + if (Test-Path $cachePath) { + Clear-DirectoryContents -Path $cachePath -Description "$($ideFolder.Name) $cacheFolder" + } + } + } + } + } +} + +# ============================================================================ +# Git Caches +# ============================================================================ + +function Clear-GitCaches { + <# + .SYNOPSIS + Clean Git temporary files and lock files + #> + + # Git config locks (stale) + $gitConfigLock = "$env:USERPROFILE\.gitconfig.lock" + if (Test-Path $gitConfigLock) { + Remove-SafeItem -Path $gitConfigLock -Description "Git config lock" + } + + # GitHub CLI cache + $ghCachePath = "$env:APPDATA\GitHub CLI" + if (Test-Path $ghCachePath) { + $cacheFiles = Get-ChildItem -Path $ghCachePath -Filter "*.json" -ErrorAction SilentlyContinue | + Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-7) } + if ($cacheFiles) { + $paths = $cacheFiles | ForEach-Object { $_.FullName } + Remove-SafeItems -Paths $paths -Description "GitHub CLI cache" + } + } +} + +# ============================================================================ +# Main Developer Tools Cleanup Function +# ============================================================================ + +function Invoke-DevToolsCleanup { + <# + .SYNOPSIS + Run all developer tools cleanup tasks + #> + + Start-Section "Developer tools" + + # JavaScript ecosystem + Clear-NpmCache + Clear-NodeBuildCaches + + # Python ecosystem + Clear-PythonCaches + + # .NET ecosystem + Clear-DotNetDevCaches + + # Go ecosystem + Clear-GoCaches + + # Rust ecosystem + Clear-RustCaches + + # JVM ecosystem + Clear-JvmCaches + + # Containers + Clear-DockerCaches + + # Cloud CLI tools + Clear-CloudCliCaches + + # IDEs + Clear-IdeCaches + + # Git + Clear-GitCaches + + Stop-Section +} + +# ============================================================================ +# Exports +# ============================================================================ +# Functions: Clear-NpmCache, Clear-PythonCaches, Clear-DockerCaches, etc. diff --git a/windows/lib/clean/system.ps1 b/windows/lib/clean/system.ps1 new file mode 100644 index 0000000..4e86e20 --- /dev/null +++ b/windows/lib/clean/system.ps1 @@ -0,0 +1,420 @@ +# Mole - System Cleanup Module +# Cleans Windows system files that require administrator access + +#Requires -Version 5.1 +Set-StrictMode -Version Latest + +# Prevent multiple sourcing +if ((Get-Variable -Name 'MOLE_CLEAN_SYSTEM_LOADED' -Scope Script -ErrorAction SilentlyContinue) -and $script:MOLE_CLEAN_SYSTEM_LOADED) { return } +$script:MOLE_CLEAN_SYSTEM_LOADED = $true + +# Import dependencies +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +. "$scriptDir\..\core\base.ps1" +. "$scriptDir\..\core\log.ps1" +. "$scriptDir\..\core\file_ops.ps1" + +# ============================================================================ +# System Temp Files +# ============================================================================ + +function Clear-SystemTempFiles { + <# + .SYNOPSIS + Clean system-level temporary files (requires admin) + #> + + if (-not (Test-IsAdmin)) { + Write-Debug "Skipping system temp cleanup - requires admin" + return + } + + # Windows Temp folder + $winTemp = "$env:WINDIR\Temp" + if (Test-Path $winTemp) { + Remove-OldFiles -Path $winTemp -DaysOld 7 -Description "Windows temp files" + } + + # System temp (different from Windows temp) + $systemTemp = "$env:SYSTEMROOT\Temp" + if ((Test-Path $systemTemp) -and ($systemTemp -ne $winTemp)) { + Remove-OldFiles -Path $systemTemp -DaysOld 7 -Description "System temp files" + } +} + +# ============================================================================ +# Windows Logs +# ============================================================================ + +function Clear-WindowsLogs { + <# + .SYNOPSIS + Clean Windows log files (requires admin) + #> + param([int]$DaysOld = 7) + + if (-not (Test-IsAdmin)) { + Write-Debug "Skipping Windows logs cleanup - requires admin" + return + } + + # Windows Logs directory + $logPaths = @( + "$env:WINDIR\Logs\CBS" + "$env:WINDIR\Logs\DISM" + "$env:WINDIR\Logs\DPX" + "$env:WINDIR\Logs\WindowsUpdate" + "$env:WINDIR\Logs\SIH" + "$env:WINDIR\Logs\waasmedia" + "$env:WINDIR\Debug" + "$env:WINDIR\Panther" + "$env:PROGRAMDATA\Microsoft\Windows\WER\ReportQueue" + "$env:PROGRAMDATA\Microsoft\Windows\WER\ReportArchive" + ) + + foreach ($path in $logPaths) { + if (Test-Path $path) { + Remove-OldFiles -Path $path -DaysOld $DaysOld -Description "$(Split-Path -Leaf $path) logs" + } + } + + # Setup logs (*.log files in Windows directory) + $setupLogs = Get-ChildItem -Path "$env:WINDIR\*.log" -File -ErrorAction SilentlyContinue | + Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$DaysOld) } + if ($setupLogs) { + $paths = $setupLogs | ForEach-Object { $_.FullName } + Remove-SafeItems -Paths $paths -Description "Windows setup logs" + } +} + +# ============================================================================ +# Windows Update Cleanup +# ============================================================================ + +function Clear-WindowsUpdateFiles { + <# + .SYNOPSIS + Clean Windows Update download cache (requires admin) + #> + + if (-not (Test-IsAdmin)) { + Write-Debug "Skipping Windows Update cleanup - requires admin" + return + } + + # Stop Windows Update service + $wuService = Get-Service -Name wuauserv -ErrorAction SilentlyContinue + $wasRunning = $wuService.Status -eq 'Running' + + if ($wasRunning) { + if (Test-DryRunMode) { + Write-DryRun "Windows Update cache (service would be restarted)" + return + } + + try { + Stop-Service -Name wuauserv -Force -ErrorAction Stop + } + catch { + Write-Debug "Could not stop Windows Update service: $_" + return + } + } + + # Clean download cache + $wuDownloadPath = "$env:WINDIR\SoftwareDistribution\Download" + if (Test-Path $wuDownloadPath) { + Clear-DirectoryContents -Path $wuDownloadPath -Description "Windows Update download cache" + } + + # Clean DataStore (old update history - be careful!) + # Only clean temp files, not the actual database + $wuDataStore = "$env:WINDIR\SoftwareDistribution\DataStore\Logs" + if (Test-Path $wuDataStore) { + Clear-DirectoryContents -Path $wuDataStore -Description "Windows Update logs" + } + + # Restart service if it was running + if ($wasRunning) { + Start-Service -Name wuauserv -ErrorAction SilentlyContinue + } +} + +# ============================================================================ +# Installer Cleanup +# ============================================================================ + +function Clear-InstallerCache { + <# + .SYNOPSIS + Clean Windows Installer cache (orphaned patches) + #> + + if (-not (Test-IsAdmin)) { + return + } + + # Windows Installer patch cache + # WARNING: Be very careful here - only clean truly orphaned files + $installerPath = "$env:WINDIR\Installer" + + # Only clean .tmp files and very old .msp files that are likely orphaned + if (Test-Path $installerPath) { + $tmpFiles = Get-ChildItem -Path $installerPath -Filter "*.tmp" -File -ErrorAction SilentlyContinue + if ($tmpFiles) { + $paths = $tmpFiles | ForEach-Object { $_.FullName } + Remove-SafeItems -Paths $paths -Description "Installer temp files" + } + } + + # Installer logs in temp + $installerLogs = Get-ChildItem -Path $env:TEMP -Filter "MSI*.LOG" -File -ErrorAction SilentlyContinue | + Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-30) } + if ($installerLogs) { + $paths = $installerLogs | ForEach-Object { $_.FullName } + Remove-SafeItems -Paths $paths -Description "Old MSI logs" + } +} + +# ============================================================================ +# Component Store Cleanup +# ============================================================================ + +function Invoke-ComponentStoreCleanup { + <# + .SYNOPSIS + Run Windows Component Store cleanup (DISM) + #> + + if (-not (Test-IsAdmin)) { + Write-Debug "Skipping component store cleanup - requires admin" + return + } + + if (Test-DryRunMode) { + Write-DryRun "Component Store cleanup (DISM)" + Set-SectionActivity + return + } + + try { + Write-Info "Running Component Store cleanup (this may take a while)..." + + # Run DISM cleanup + $result = Start-Process -FilePath "dism.exe" ` + -ArgumentList "/Online", "/Cleanup-Image", "/StartComponentCleanup" ` + -Wait -PassThru -NoNewWindow -ErrorAction Stop + + if ($result.ExitCode -eq 0) { + Write-Success "Component Store cleanup" + Set-SectionActivity + } + else { + Write-Debug "DISM returned exit code: $($result.ExitCode)" + } + } + catch { + Write-Debug "Component Store cleanup failed: $_" + } +} + +# ============================================================================ +# Memory Dump Cleanup +# ============================================================================ + +function Clear-MemoryDumps { + <# + .SYNOPSIS + Clean Windows memory dumps + #> + + $dumpPaths = @( + "$env:WINDIR\MEMORY.DMP" + "$env:WINDIR\Minidump" + "$env:LOCALAPPDATA\CrashDumps" + ) + + foreach ($path in $dumpPaths) { + if (Test-Path $path -PathType Leaf) { + # Single file (MEMORY.DMP) + Remove-SafeItem -Path $path -Description "Memory dump" + } + elseif (Test-Path $path -PathType Container) { + # Directory (Minidump, CrashDumps) + Clear-DirectoryContents -Path $path -Description "$(Split-Path -Leaf $path)" + } + } +} + +# ============================================================================ +# Font Cache +# ============================================================================ + +function Clear-SystemFontCache { + <# + .SYNOPSIS + Clear Windows font cache (requires admin and may need restart) + #> + + if (-not (Test-IsAdmin)) { + return + } + + $fontCacheService = Get-Service -Name "FontCache" -ErrorAction SilentlyContinue + + if ($fontCacheService) { + if (Test-DryRunMode) { + Write-DryRun "System font cache" + return + } + + try { + # Stop font cache service + Stop-Service -Name "FontCache" -Force -ErrorAction SilentlyContinue + + # Clear font cache files + $fontCachePath = "$env:WINDIR\ServiceProfiles\LocalService\AppData\Local\FontCache" + if (Test-Path $fontCachePath) { + Clear-DirectoryContents -Path $fontCachePath -Description "System font cache" + } + + # Restart font cache service + Start-Service -Name "FontCache" -ErrorAction SilentlyContinue + } + catch { + Write-Debug "Font cache cleanup failed: $_" + Start-Service -Name "FontCache" -ErrorAction SilentlyContinue + } + } +} + +# ============================================================================ +# Disk Cleanup Tool Integration +# ============================================================================ + +function Invoke-DiskCleanupTool { + <# + .SYNOPSIS + Run Windows built-in Disk Cleanup tool with predefined settings + #> + param([switch]$Full) + + if (-not (Test-IsAdmin)) { + Write-Debug "Skipping Disk Cleanup tool - requires admin for full cleanup" + } + + if (Test-DryRunMode) { + Write-DryRun "Windows Disk Cleanup tool" + return + } + + # Set up registry keys for automated cleanup + $cleanupKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches" + + $cleanupItems = @( + "Active Setup Temp Folders" + "Downloaded Program Files" + "Internet Cache Files" + "Old ChkDsk Files" + "Recycle Bin" + "Setup Log Files" + "System error memory dump files" + "System error minidump files" + "Temporary Files" + "Temporary Setup Files" + "Thumbnail Cache" + "Windows Error Reporting Archive Files" + "Windows Error Reporting Queue Files" + "Windows Error Reporting System Archive Files" + "Windows Error Reporting System Queue Files" + ) + + if ($Full -and (Test-IsAdmin)) { + $cleanupItems += @( + "Previous Installations" + "Temporary Windows installation files" + "Update Cleanup" + "Windows Defender" + "Windows Upgrade Log Files" + ) + } + + # Enable cleanup items in registry + foreach ($item in $cleanupItems) { + $itemPath = Join-Path $cleanupKey $item + if (Test-Path $itemPath) { + Set-ItemProperty -Path $itemPath -Name "StateFlags0100" -Value 2 -Type DWord -ErrorAction SilentlyContinue + } + } + + try { + # Run disk cleanup + $process = Start-Process -FilePath "cleanmgr.exe" ` + -ArgumentList "/sagerun:100" ` + -Wait -PassThru -NoNewWindow -ErrorAction Stop + + if ($process.ExitCode -eq 0) { + Write-Success "Windows Disk Cleanup" + Set-SectionActivity + } + } + catch { + Write-Debug "Disk Cleanup failed: $_" + } +} + +# ============================================================================ +# Main System Cleanup Function +# ============================================================================ + +function Invoke-SystemCleanup { + <# + .SYNOPSIS + Run all system-level cleanup tasks (requires admin for full effect) + #> + param( + [switch]$IncludeComponentStore, + [switch]$IncludeDiskCleanup + ) + + Start-Section "System cleanup" + + if (-not (Test-IsAdmin)) { + Write-Warning "Running without admin - some cleanup tasks will be skipped" + } + + # System temp files + Clear-SystemTempFiles + + # Windows logs + Clear-WindowsLogs -DaysOld 7 + + # Windows Update cache + Clear-WindowsUpdateFiles + + # Installer cache + Clear-InstallerCache + + # Memory dumps + Clear-MemoryDumps + + # Font cache + Clear-SystemFontCache + + # Optional: Component Store (can take a long time) + if ($IncludeComponentStore) { + Invoke-ComponentStoreCleanup + } + + # Optional: Windows Disk Cleanup tool + if ($IncludeDiskCleanup) { + Invoke-DiskCleanupTool -Full + } + + Stop-Section +} + +# ============================================================================ +# Exports +# ============================================================================ +# Functions: Clear-SystemTempFiles, Clear-WindowsLogs, Invoke-SystemCleanup, etc. diff --git a/windows/lib/clean/user.ps1 b/windows/lib/clean/user.ps1 new file mode 100644 index 0000000..61aef59 --- /dev/null +++ b/windows/lib/clean/user.ps1 @@ -0,0 +1,349 @@ +# Mole - User Cleanup Module +# Cleans user-level temporary files, caches, and downloads + +#Requires -Version 5.1 +Set-StrictMode -Version Latest + +# Prevent multiple sourcing +if ((Get-Variable -Name 'MOLE_CLEAN_USER_LOADED' -Scope Script -ErrorAction SilentlyContinue) -and $script:MOLE_CLEAN_USER_LOADED) { return } +$script:MOLE_CLEAN_USER_LOADED = $true + +# Import dependencies +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +. "$scriptDir\..\core\base.ps1" +. "$scriptDir\..\core\log.ps1" +. "$scriptDir\..\core\file_ops.ps1" + +# ============================================================================ +# Windows Temp Files Cleanup +# ============================================================================ + +function Clear-UserTempFiles { + <# + .SYNOPSIS + Clean user temporary files + #> + param([int]$DaysOld = 7) + + Start-Section "User temp files" + + # User temp directory + $userTemp = $env:TEMP + if (Test-Path $userTemp) { + Remove-OldFiles -Path $userTemp -DaysOld $DaysOld -Description "User temp files" + } + + # Windows Temp (if accessible) + $winTemp = "$env:WINDIR\Temp" + if ((Test-Path $winTemp) -and (Test-IsAdmin)) { + Remove-OldFiles -Path $winTemp -DaysOld $DaysOld -Description "Windows temp files" + } + + Stop-Section +} + +# ============================================================================ +# Downloads Folder Cleanup +# ============================================================================ + +function Clear-OldDownloads { + <# + .SYNOPSIS + Clean old files from Downloads folder (with user confirmation pattern) + #> + param([int]$DaysOld = 30) + + $downloadsPath = [Environment]::GetFolderPath('UserProfile') + '\Downloads' + + if (-not (Test-Path $downloadsPath)) { + return + } + + # Find old installers and archives + $patterns = @('*.exe', '*.msi', '*.zip', '*.7z', '*.rar', '*.tar.gz', '*.iso') + $cutoffDate = (Get-Date).AddDays(-$DaysOld) + + $oldFiles = @() + foreach ($pattern in $patterns) { + $files = Get-ChildItem -Path $downloadsPath -Filter $pattern -File -ErrorAction SilentlyContinue | + Where-Object { $_.LastWriteTime -lt $cutoffDate } + if ($files) { + $oldFiles += $files + } + } + + if ($oldFiles.Count -gt 0) { + $paths = $oldFiles | ForEach-Object { $_.FullName } + Remove-SafeItems -Paths $paths -Description "Old downloads (>${DaysOld}d)" + } +} + +# ============================================================================ +# Recycle Bin Cleanup +# ============================================================================ + +function Clear-RecycleBin { + <# + .SYNOPSIS + Empty the Recycle Bin + #> + + if (Test-DryRunMode) { + Write-DryRun "Recycle Bin (would empty)" + Set-SectionActivity + return + } + + try { + # Use Shell.Application COM object + $shell = New-Object -ComObject Shell.Application + $recycleBin = $shell.Namespace(0xA) # Recycle Bin + $items = $recycleBin.Items() + + if ($items.Count -gt 0) { + # Calculate size + $totalSize = 0 + foreach ($item in $items) { + $totalSize += $item.Size + } + + # Clear using Clear-RecycleBin cmdlet (Windows 10+) + Clear-RecycleBin -Force -ErrorAction SilentlyContinue + + $sizeHuman = Format-ByteSize -Bytes $totalSize + Write-Success "Recycle Bin $($script:Colors.Green)($sizeHuman)$($script:Colors.NC)" + Set-SectionActivity + } + } + catch { + Write-Debug "Could not clear Recycle Bin: $_" + } +} + +# ============================================================================ +# Recent Files Cleanup +# ============================================================================ + +function Clear-RecentFiles { + <# + .SYNOPSIS + Clean old recent file shortcuts + #> + param([int]$DaysOld = 30) + + $recentPath = "$env:APPDATA\Microsoft\Windows\Recent" + + if (Test-Path $recentPath) { + Remove-OldFiles -Path $recentPath -DaysOld $DaysOld -Filter "*.lnk" -Description "Old recent shortcuts" + } + + # AutomaticDestinations (jump lists) + $autoDestPath = "$recentPath\AutomaticDestinations" + if (Test-Path $autoDestPath) { + Remove-OldFiles -Path $autoDestPath -DaysOld $DaysOld -Description "Old jump list entries" + } +} + +# ============================================================================ +# Thumbnail Cache Cleanup +# ============================================================================ + +function Clear-ThumbnailCache { + <# + .SYNOPSIS + Clean Windows thumbnail cache + #> + + $thumbCachePath = "$env:LOCALAPPDATA\Microsoft\Windows\Explorer" + + if (-not (Test-Path $thumbCachePath)) { + return + } + + # Thumbnail cache files (thumbcache_*.db) + $thumbFiles = Get-ChildItem -Path $thumbCachePath -Filter "thumbcache_*.db" -File -ErrorAction SilentlyContinue + + if ($thumbFiles) { + $paths = $thumbFiles | ForEach-Object { $_.FullName } + Remove-SafeItems -Paths $paths -Description "Thumbnail cache" + } + + # Icon cache + $iconCache = "$env:LOCALAPPDATA\IconCache.db" + if (Test-Path $iconCache) { + Remove-SafeItem -Path $iconCache -Description "Icon cache" + } +} + +# ============================================================================ +# Windows Error Reports Cleanup +# ============================================================================ + +function Clear-ErrorReports { + <# + .SYNOPSIS + Clean Windows Error Reporting files + #> + param([int]$DaysOld = 7) + + $werPaths = @( + "$env:LOCALAPPDATA\Microsoft\Windows\WER" + "$env:LOCALAPPDATA\CrashDumps" + "$env:USERPROFILE\AppData\Local\Microsoft\Windows\WER" + ) + + foreach ($path in $werPaths) { + if (Test-Path $path) { + $items = Get-ChildItem -Path $path -Recurse -Force -ErrorAction SilentlyContinue + if ($items) { + $paths = $items | ForEach-Object { $_.FullName } + Remove-SafeItems -Paths $paths -Description "Error reports" + } + } + } + + # Memory dumps + $dumpPaths = @( + "$env:LOCALAPPDATA\CrashDumps" + "$env:USERPROFILE\*.dmp" + ) + + foreach ($path in $dumpPaths) { + $dumps = Get-ChildItem -Path $path -Filter "*.dmp" -ErrorAction SilentlyContinue + if ($dumps) { + $paths = $dumps | ForEach-Object { $_.FullName } + Remove-SafeItems -Paths $paths -Description "Memory dumps" + } + } +} + +# ============================================================================ +# Windows Prefetch Cleanup (requires admin) +# ============================================================================ + +function Clear-Prefetch { + <# + .SYNOPSIS + Clean Windows Prefetch files (requires admin) + #> + param([int]$DaysOld = 14) + + if (-not (Test-IsAdmin)) { + Write-Debug "Skipping Prefetch cleanup - requires admin" + return + } + + $prefetchPath = "$env:WINDIR\Prefetch" + + if (Test-Path $prefetchPath) { + Remove-OldFiles -Path $prefetchPath -DaysOld $DaysOld -Description "Prefetch files" + } +} + +# ============================================================================ +# Log Files Cleanup +# ============================================================================ + +function Clear-UserLogs { + <# + .SYNOPSIS + Clean old log files from common locations + #> + param([int]$DaysOld = 7) + + $logLocations = @( + "$env:LOCALAPPDATA\Temp\*.log" + "$env:APPDATA\*.log" + "$env:USERPROFILE\*.log" + ) + + foreach ($location in $logLocations) { + $parent = Split-Path -Parent $location + $filter = Split-Path -Leaf $location + + if (Test-Path $parent) { + $logs = Get-ChildItem -Path $parent -Filter $filter -File -ErrorAction SilentlyContinue | + Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$DaysOld) } + + if ($logs) { + $paths = $logs | ForEach-Object { $_.FullName } + Remove-SafeItems -Paths $paths -Description "Old log files" + } + } + } +} + +# ============================================================================ +# Clipboard History Cleanup +# ============================================================================ + +function Clear-ClipboardHistory { + <# + .SYNOPSIS + Clear Windows clipboard history + #> + + if (Test-DryRunMode) { + Write-DryRun "Clipboard history (would clear)" + return + } + + try { + # Clear current clipboard + [System.Windows.Forms.Clipboard]::Clear() + + # Clear clipboard history (Windows 10 1809+) + $clipboardPath = "$env:LOCALAPPDATA\Microsoft\Windows\Clipboard" + if (Test-Path $clipboardPath) { + Clear-DirectoryContents -Path $clipboardPath -Description "Clipboard history" + } + } + catch { + Write-Debug "Could not clear clipboard: $_" + } +} + +# ============================================================================ +# Main User Cleanup Function +# ============================================================================ + +function Invoke-UserCleanup { + <# + .SYNOPSIS + Run all user-level cleanup tasks + #> + param( + [int]$TempDaysOld = 7, + [int]$DownloadsDaysOld = 30, + [int]$LogDaysOld = 7, + [switch]$IncludeDownloads, + [switch]$IncludeRecycleBin + ) + + Start-Section "User essentials" + + # Always clean these + Clear-UserTempFiles -DaysOld $TempDaysOld + Clear-RecentFiles -DaysOld 30 + Clear-ThumbnailCache + Clear-ErrorReports -DaysOld 7 + Clear-UserLogs -DaysOld $LogDaysOld + Clear-Prefetch -DaysOld 14 + + # Optional: Downloads cleanup + if ($IncludeDownloads) { + Clear-OldDownloads -DaysOld $DownloadsDaysOld + } + + # Optional: Recycle Bin + if ($IncludeRecycleBin) { + Clear-RecycleBin + } + + Stop-Section +} + +# ============================================================================ +# Exports +# ============================================================================ +# Functions: Clear-UserTempFiles, Clear-OldDownloads, Clear-RecycleBin, etc.