diff --git a/windows/bin/uninstall.ps1 b/windows/bin/uninstall.ps1 index 85b148c..bd1a325 100644 --- a/windows/bin/uninstall.ps1 +++ b/windows/bin/uninstall.ps1 @@ -523,10 +523,10 @@ function Uninstall-SelectedApps { } if (-not $uninstalled) { - # Fallback to interactive - Write-Host " $esc[33m(launching uninstaller)$esc[0m" + # Fallback to interactive - don't count as automatic success + Write-Host " $esc[33m(launching uninstaller - verify completion manually)$esc[0m" Start-Process -FilePath "cmd.exe" -ArgumentList "/c", "`"$uninstallString`"" -Wait - $successCount++ + # Note: Not incrementing $successCount since we can't verify if user completed or cancelled } } } diff --git a/windows/cmd/analyze/analyze.exe b/windows/cmd/analyze/analyze.exe new file mode 100644 index 0000000..88661c1 Binary files /dev/null and b/windows/cmd/analyze/analyze.exe differ diff --git a/windows/cmd/analyze/main.go b/windows/cmd/analyze/main.go index 4251932..60cc9bc 100644 --- a/windows/cmd/analyze/main.go +++ b/windows/cmd/analyze/main.go @@ -89,6 +89,59 @@ var skipPatterns = map[string]bool{ "Config.Msi": true, } +// Protected paths that should NEVER be deleted +var protectedPaths = []string{ + `C:\Windows`, + `C:\Program Files`, + `C:\Program Files (x86)`, + `C:\ProgramData`, + `C:\Users\Default`, + `C:\Users\Public`, + `C:\Recovery`, + `C:\System Volume Information`, +} + +// isProtectedPath checks if a path is protected from deletion +func isProtectedPath(path string) bool { + absPath, err := filepath.Abs(path) + if err != nil { + return true // If we can't resolve the path, treat it as protected + } + absPath = strings.ToLower(absPath) + + // Check against protected paths + for _, protected := range protectedPaths { + protectedLower := strings.ToLower(protected) + if absPath == protectedLower || strings.HasPrefix(absPath, protectedLower+`\`) { + return true + } + } + + // Check against skip patterns (system directories) + baseName := strings.ToLower(filepath.Base(absPath)) + for pattern := range skipPatterns { + if strings.ToLower(pattern) == baseName { + // Only protect if it's at a root level (e.g., C:\Windows, not C:\Projects\Windows) + parent := filepath.Dir(absPath) + if len(parent) <= 3 { // e.g., "C:\" + return true + } + } + } + + // Protect Windows directory itself + winDir := strings.ToLower(os.Getenv("WINDIR")) + sysRoot := strings.ToLower(os.Getenv("SYSTEMROOT")) + if winDir != "" && (absPath == winDir || strings.HasPrefix(absPath, winDir+`\`)) { + return true + } + if sysRoot != "" && (absPath == sysRoot || strings.HasPrefix(absPath, sysRoot+`\`)) { + return true + } + + return false +} + // Entry types type dirEntry struct { Name string @@ -462,9 +515,17 @@ func (m model) scanPath(path string) tea.Cmd { } } -// deletePath deletes a file or directory +// deletePath deletes a file or directory with protection checks func (m model) deletePath(path string) tea.Cmd { return func() tea.Msg { + // Safety check: never delete protected paths + if isProtectedPath(path) { + return deleteCompleteMsg{ + path: path, + err: fmt.Errorf("cannot delete protected system path: %s", path), + } + } + err := os.RemoveAll(path) return deleteCompleteMsg{path: path, err: err} } diff --git a/windows/lib/clean/apps.ps1 b/windows/lib/clean/apps.ps1 index ceb9460..36ed9c7 100644 --- a/windows/lib/clean/apps.ps1 +++ b/windows/lib/clean/apps.ps1 @@ -88,10 +88,23 @@ function Find-OrphanedAppData { # Skip if recently modified if ($folder.LastWriteTime -gt $cutoffDate) { continue } - # Check if app is installed + # Check if app is installed using stricter matching + # Require exact match or that folder name is a clear prefix/suffix of app name $isInstalled = $false + $folderLower = $folder.Name.ToLower() foreach ($name in $installedNames) { - if ($name -like "*$($folder.Name.ToLower())*" -or $folder.Name.ToLower() -like "*$name*") { + # Exact match + if ($name -eq $folderLower) { + $isInstalled = $true + break + } + # Folder is prefix of app name (e.g., "chrome" matches "chrome browser") + if ($name.StartsWith($folderLower) -and $folderLower.Length -ge 4) { + $isInstalled = $true + break + } + # App name is prefix of folder (e.g., "vscode" matches "vscode-data") + if ($folderLower.StartsWith($name) -and $name.Length -ge 4) { $isInstalled = $true break } diff --git a/windows/lib/clean/system.ps1 b/windows/lib/clean/system.ps1 index 4e86e20..035590f 100644 --- a/windows/lib/clean/system.ps1 +++ b/windows/lib/clean/system.ps1 @@ -121,22 +121,25 @@ function Clear-WindowsUpdateFiles { } } - # Clean download cache - $wuDownloadPath = "$env:WINDIR\SoftwareDistribution\Download" - if (Test-Path $wuDownloadPath) { - Clear-DirectoryContents -Path $wuDownloadPath -Description "Windows Update download cache" + try { + # 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" + } } - - # 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 + finally { + # Always restart service if it was running, even if cleanup failed + if ($wasRunning) { + Start-Service -Name wuauserv -ErrorAction SilentlyContinue + } } }