<#PSScriptInfo .VERSION 2.13 .GUID 8f7c3b2a-1d4e-5f6a-9b8c-0d1e2f3a4b5c .AUTHOR bivlked .COMPANYNAME .COPYRIGHT (c) 2026 bivlked. MIT License. .TAGS Windows Cleanup Maintenance PowerShell Windows11 DevTools Docker WSL npm pip nuget .LICENSEURI https://github.com/bivlked/WinClean/blob/main/LICENSE .PROJECTURI https://github.com/bivlked/WinClean .ICONURI https://raw.githubusercontent.com/bivlked/WinClean/main/assets/logo.svg .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES v2.13: Statistics accuracy fixes, efficiency improvements, registry cleanup v2.12: PS 7.4+ compatibility, improved statistics (Docker/WSL/RecycleBin), ReportOnly accuracy v2.11: Added timeouts for winget/DISM operations, fixed version display, improved reliability v2.10: Added auto-update check at startup (checks PSGallery for new version) v2.9: Fixed PSWindowsUpdate installation hanging (TLS 1.2, timeouts) .PRIVATEDATA #> <# .SYNOPSIS WinClean - Ultimate Windows 11 Maintenance Script v2.13 .DESCRIPTION Комплексный скрипт для обновления и очистки Windows 11: - Обновление Windows (включая драйверы) - Обновление приложений через winget - Очистка системы, браузеров, кэшей разработчика - Очистка Docker/WSL - Очистка Visual Studio - Очистка DNS кэша и истории Run - Опциональная блокировка телеметрии Windows - Параллельное выполнение для максимальной скорости - Подробный цветной вывод + лог-файл .NOTES Author: biv Version: 2.13 Requires: PowerShell 7.1+, Windows 11, Administrator rights Changes in 2.13: - Fixed Docker prune output parsing (supports "Total reclaimed space:" format) - Fixed WarningsCount not incrementing for event log failures - Fixed false "success" when Windows Update returns null results - Optimized Get-FolderSize with -File flag for better performance - Fixed temp path deduplication to avoid duplicate cleanups - Removed redundant docker builder prune (already included in system prune) - Fixed potential negative freed space in browser cache statistics - Added fallback for Recycle Bin size calculation via GetDetailsOf - Added registry cleanup for Disk Cleanup StateFlags after execution Changes in 2.12: - Fixed PS 7.4+ compatibility (removed deprecated -UseBasicParsing) - Fixed DISM ReportOnly to show /ResetBase and warning - Fixed AppUpdatesCount to only count successful updates - Added statistics for Docker, WSL, Recycle Bin, npm cache Changes in 2.11: - Fixed version display bugs (banner and log showed v2.9 instead of current version) - Added timeouts for winget/DISM operations to prevent script hangs - Added force stop for Storage Sense when timeout exceeded - Improved Docker detection and browser cache statistics reliability Changes in 2.10: - Added auto-update check: script checks PSGallery for newer version at startup - Added Test-ScriptUpdate function: compares local version with PSGallery - Added Invoke-ScriptUpdate function: prompts user and performs update if confirmed - Update check runs after reboot check, before main operations - Shows manual update instructions if script was downloaded manually (not via PSGallery) - Respects ReportOnly mode and non-interactive environments Changes in 2.9: - Fixed PSWindowsUpdate installation hanging: added TLS 1.2 enforcement - Added Test-PSGalleryConnection function: pre-checks PowerShell Gallery availability - Added Install-ModuleWithTimeout function: 120-second timeout for Install-Module - Added Install-PackageProviderWithTimeout function: 60-second timeout for NuGet provider - Improved error messages with manual installation instructions - Clear Write-Progress before module installation to prevent UI artifacts Changes in 2.8: - Fixed Disk Cleanup timeout: reduced from 10 minutes to 7 minutes - Fixed Disk Cleanup: replaced -NoNewWindow with -WindowStyle Hidden (more reliable) - Added progress logging every minute while Disk Cleanup is running - Replaced Wait-Process with explicit HasExited loop for better control Changes in 2.7: - Fixed UI: header frame (╔═╗║║) now uses Cyan like the rest of the frame - Status text (COMPLETED SUCCESSFULLY) remains colored (Green/Yellow/Red) for visual feedback Changes in 2.6: - Fixed UI: final statistics frame now uses consistent Cyan color throughout - Fixed UI: added 2-space gap between label and value (prevents "installed:Windows:" merging) - Fixed UI: category names (Temp, System) now right-aligned with PadLeft to match main labels - Refactored: $labelWidth moved to parent scope for reuse in category alignment Changes in 2.5: - Fixed UI: subsection gray lines now match TITLE frame width (70 chars) - Fixed UI: final statistics window alignment (emoji replaced with ASCII) - Fixed UI: Write-StatLine width formula corrected (-5 → -3) Changes in 2.4: - UI improvements: consistent left indent (2 spaces) throughout the script - UI improvements: major sections now have full frame (like banner) - UI improvements: subsections keep original style (┌─ Title / └────) - UI improvements: enhanced final statistics with status icons and colors - UI improvements: header color reflects completion status (green/yellow/red) - Removed 60-second auto-close timeout - window now waits indefinitely for keypress Changes in 2.3: - Fixed critical bug: TotalFreedBytes always showed 0 in final statistics Root cause: Interlocked.Add doesn't work with hashtable elements via [ref] in PowerShell Solution: Use simple += operator (synchronized hashtable handles thread-safety) Changes in 2.2: - Fixed TcpClient resource leak: now properly closed in finally block (prevents socket exhaustion) - Fixed code region markers: 8 misplaced #region tags corrected for proper IDE navigation - Fixed banner ASCII art: now displays "CLEAN" instead of incorrect "DREAM" Changes in 2.1: - Fixed Clear-EventLogs: exact match for Security log only (not all logs containing "Security") - Fixed browser cache cleanup: additional profiles now get full cache set (Code Cache, GPUCache, etc.) - Fixed Update-Applications: ErrorsCount++ now incremented when no internet - Fixed Roslyn Temp cleanup: file patterns now handled correctly (not just directories) - Fixed winget update count: works with custom sources (not just winget/msstore) - Fixed interactive prompts: safe defaults in non-console environments (Scheduled Tasks, ISE) - Fixed telemetry edition detection: uses EditionID registry (language-independent) - Fixed final statistics: consistent box width (no visual glitches) Changes in 2.0: - Fixed Test-InternetConnection: uses TcpClient with 3s timeout (no VPN hangs) - Fixed Clear-EventLogs: now checks $LASTEXITCODE for each wevtutil call - Fixed winget ExitCode: strict check (any non-zero = error, not just empty output) - Fixed Storage Sense: uses Get-ScheduledTask (language-independent status) - Fixed Storage Sense: detects actual completion (wasRunning -> Ready transition) - Fixed ReportOnly: no longer installs PSWindowsUpdate/NuGet modules - Removed unused DriverUpdatesCount field from Stats Changes in 1.9: - Fixed progress bar: TotalSteps now calculated dynamically based on skip flags - Fixed winget: source update skipped in ReportOnly mode, added ExitCode check - Fixed winget: --include-unknown now used consistently for count and upgrade - Fixed browser cache statistics: now measures actual freed space (before/after) - Fixed Storage Sense: now waits for task completion instead of fixed sleep - Fixed DNS flush: logs warning on unexpected result instead of false success - Fixed WSL/Docker VHDX: now compacts all VHDX files regardless of distro list - Moved Update-Progress calls after skip flag checks for accurate progress Changes in 1.8: - Fixed critical bug: $LogPath vs $script:LogPath in Start-WinClean and Show-FinalStatistics - Fixed version inconsistency: unified all version references to single source - Added browser cache size tracking to freed space statistics - Fixed TotalSteps count (was 12, actual 7 steps) - Improved winget update detection: language-independent parsing Changes in 1.7: - Improved internet connectivity check: HTTPS endpoints + ICMP fallback - Fixed Show-Banner to display correct log path ($script:LogPath) - Fixed Clear-SystemCaches: ReportOnly mode and size tracking for single files Changes in 1.6: - Added pause at end: window stays open 60 sec or until key press (prevents window from closing before user can read results) Changes in 1.5: - Fixed visual glitch: clear progress bar before DISM output to prevent overlay Changes in 1.4: - Fixed Clear-PrivacyTraces: added -Recurse to Remove-Item to prevent confirmation prompts when cleaning Recent folder (AutomaticDestinations, CustomDestinations) Changes in 1.3: - CRITICAL FIX: Clear-RecycleBin renamed to Clear-WinCleanRecycleBin to avoid infinite recursion (stack overflow) caused by name collision with built-in cmdlet Changes in 1.2: - Fixed $script:LogPath scope (logging now works correctly) - Fixed Clear-BrowserCaches respecting ReportOnly mode - Fixed Windows.old path to use $env:SystemDrive instead of hardcoded C: - Fixed NuGet: removed packages folder (not cache), kept only metadata caches - Fixed Gradle: only delete safe build caches, not downloaded dependencies - Fixed Windows Update services now properly restart via try/finally - Fixed WSL --list output UTF-16LE parsing (removes null characters) .PARAMETER SkipUpdates Пропустить все обновления (Windows + winget) .PARAMETER SkipCleanup Пропустить очистку системы .PARAMETER SkipRestore Пропустить создание точки восстановления .PARAMETER SkipDevCleanup Пропустить очистку кэшей разработчика (npm, pip, nuget) .PARAMETER SkipDockerCleanup Пропустить очистку Docker/WSL .PARAMETER SkipVSCleanup Пропустить очистку Visual Studio .PARAMETER DisableTelemetry Отключить телеметрию Windows (через групповую политику) .PARAMETER ReportOnly Только показать, что будет сделано (без выполнения) .PARAMETER LogPath Путь к файлу лога (по умолчанию: $env:TEMP\WinClean_.log) #> #Requires -Version 7.1 #Requires -RunAsAdministrator [CmdletBinding()] param( [switch]$SkipUpdates, [switch]$SkipCleanup, [switch]$SkipRestore, [switch]$SkipDevCleanup, [switch]$SkipDockerCleanup, [switch]$SkipVSCleanup, [switch]$DisableTelemetry, [switch]$ReportOnly, [string]$LogPath ) #region ═══════════════════════════════════════════════════════════════════════ # INITIALIZATION #═══════════════════════════════════════════════════════════════════════════════ # Ensure UTF-8 encoding [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8 $PSDefaultParameterValues['*:Encoding'] = 'utf8' # Statistics storage (synchronized hashtable for safe concurrent access) $script:Stats = [hashtable]::Synchronized(@{ TotalFreedBytes = [long]0 FreedByCategory = @{} WindowsUpdatesCount = 0 AppUpdatesCount = 0 WarningsCount = 0 ErrorsCount = 0 RebootRequired = $false StartTime = Get-Date CurrentStep = 0 TotalSteps = 0 # Calculated dynamically in Start-WinClean }) # Initialize log path (script scope for access in functions) if (-not $LogPath) { $script:LogPath = Join-Path $env:TEMP "WinClean_$((Get-Date).ToString('yyyyMMdd_HHmmss')).log" } else { $script:LogPath = $LogPath } # Script version (single source of truth for version checking) $script:Version = "2.13" # Protected paths that should never be deleted $script:ProtectedPaths = @( $env:SystemRoot, "$env:SystemRoot\System32", $env:ProgramFiles, ${env:ProgramFiles(x86)}, $env:USERPROFILE, "$env:SystemDrive\Users", "$env:SystemDrive\Program Files", "$env:SystemDrive\Program Files (x86)" ) #endregion #region ═══════════════════════════════════════════════════════════════════════ # LOGGING FUNCTIONS #═══════════════════════════════════════════════════════════════════════════════ function Write-Log { <# .SYNOPSIS Writes colored output to console and plain text to log file #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Message, [ValidateSet('INFO', 'SUCCESS', 'WARNING', 'ERROR', 'TITLE', 'SECTION', 'DETAIL')] [string]$Level = 'INFO', [switch]$NoNewLine, [switch]$NoTimestamp, [switch]$NoLog ) # Consistent left indent for all output (matches banner style) $indent = " " $boxWidth = 70 # Inner width for framed sections $timestamp = (Get-Date).ToString('HH:mm:ss') $logMessage = "[$timestamp] [$Level] $Message" # Write to log file if (-not $NoLog) { try { $logMessage | Out-File -FilePath $script:LogPath -Append -Encoding utf8 -ErrorAction SilentlyContinue } catch { } } # Console output with colors $colors = @{ INFO = @{ Tag = 'Cyan'; Message = 'White' } SUCCESS = @{ Tag = 'Green'; Message = 'White' } WARNING = @{ Tag = 'Yellow'; Message = 'Yellow' } ERROR = @{ Tag = 'Red'; Message = 'Red' } TITLE = @{ Tag = 'Magenta'; Message = 'Magenta' } SECTION = @{ Tag = 'Cyan'; Message = 'Cyan' } DETAIL = @{ Tag = 'DarkGray';Message = 'Gray' } } $tagColors = $colors[$Level] switch ($Level) { 'TITLE' { # Full frame for major sections (like banner style, but Magenta) $titleText = $Message.ToUpper() $padding = [math]::Max(0, $boxWidth - $titleText.Length) $leftPad = [math]::Floor($padding / 2) $rightPad = $padding - $leftPad $centeredTitle = (" " * $leftPad) + $titleText + (" " * $rightPad) Write-Host "" Write-Host "$indent╔$("═" * $boxWidth)╗" -ForegroundColor $tagColors.Tag Write-Host "$indent║$centeredTitle║" -ForegroundColor $tagColors.Tag Write-Host "$indent╚$("═" * $boxWidth)╝" -ForegroundColor $tagColors.Tag } 'SECTION' { # Subsection header (keep original style with indent) Write-Host "" Write-Host "$indent┌─ " -NoNewline -ForegroundColor DarkGray Write-Host $Message -ForegroundColor $tagColors.Message Write-Host "$indent└$("─" * 70)" -ForegroundColor DarkGray } 'DETAIL' { # Detail line with vertical bar Write-Host "$indent │ " -NoNewline -ForegroundColor DarkGray Write-Host $Message -ForegroundColor $tagColors.Message -NoNewline:$NoNewLine if (-not $NoNewLine) { Write-Host "" } } default { # Standard log line with timestamp and tag Write-Host $indent -NoNewline if (-not $NoTimestamp) { Write-Host "[$timestamp] " -NoNewline -ForegroundColor DarkGray } $tagText = switch ($Level) { 'INFO' { '[INFO] ' } 'SUCCESS' { '[OK] ' } 'WARNING' { '[WARN] ' } 'ERROR' { '[ERROR] ' } } Write-Host $tagText -NoNewline -ForegroundColor $tagColors.Tag Write-Host $Message -ForegroundColor $tagColors.Message -NoNewline:$NoNewLine if (-not $NoNewLine) { Write-Host "" } } } } function Update-Progress { <# .SYNOPSIS Updates progress bar and step counter #> param( [string]$Activity, [string]$Status = "Processing..." ) $script:Stats.CurrentStep++ $percent = [math]::Min(100, [math]::Round(($script:Stats.CurrentStep / $script:Stats.TotalSteps) * 100)) Write-Progress -Activity $Activity -Status $Status -PercentComplete $percent } #endregion #region ═══════════════════════════════════════════════════════════════════════ # HELPER FUNCTIONS #═══════════════════════════════════════════════════════════════════════════════ function Test-InteractiveConsole { <# .SYNOPSIS Checks if running in an interactive console environment .DESCRIPTION Returns $false for Scheduled Tasks, ISE, remote sessions, etc. Used to safely skip [Console]::KeyAvailable calls that would throw exceptions #> try { # Check if we're in ConsoleHost and have a valid console window if ($Host.Name -ne 'ConsoleHost') { return $false } # Try to access console properties - will throw in non-console environments $null = [Console]::WindowWidth return $true } catch { return $false } } function Test-InternetConnection { <# .SYNOPSIS Проверяет доступ к интернету через TCP-соединения с таймаутом .DESCRIPTION Использует TcpClient с явным таймаутом (3 сек) вместо Test-NetConnection, который может зависать на 20-30 секунд при VPN или нестабильном соединении #> $targets = @( @{ Host = 'www.microsoft.com'; Port = 443 } @{ Host = 'api.github.com'; Port = 443 } @{ Host = 'winget.azureedge.net'; Port = 443 } ) $timeoutMs = 3000 # 3 секунды таймаут на каждое соединение foreach ($target in $targets) { $tcpClient = $null try { $tcpClient = New-Object System.Net.Sockets.TcpClient $connect = $tcpClient.BeginConnect($target.Host, $target.Port, $null, $null) $success = $connect.AsyncWaitHandle.WaitOne($timeoutMs, $false) if ($success -and $tcpClient.Connected) { $tcpClient.EndConnect($connect) return $true } } catch { } finally { # Always close TcpClient to prevent resource leaks (fixed in v2.2) if ($tcpClient) { $tcpClient.Close() } } } # Запасной вариант: ICMP (может быть заблокирован в некоторых сетях) $dnsServers = @('8.8.8.8', '1.1.1.1', '208.67.222.222') foreach ($dns in $dnsServers) { if (Test-Connection -ComputerName $dns -Count 1 -Quiet -TimeoutSeconds 2 -ErrorAction SilentlyContinue) { return $true } } return $false } function Test-PSGalleryConnection { <# .SYNOPSIS Проверяет доступность PowerShell Gallery перед установкой модулей .DESCRIPTION Использует Invoke-WebRequest с коротким таймаутом для проверки доступности powershellgallery.com. Более специфичная проверка чем общий Test-InternetConnection. .OUTPUTS [bool] $true если PowerShell Gallery доступен, $false в противном случае #> try { # Check PSGallery API endpoint (faster than main page) # Note: -UseBasicParsing removed - it was deprecated in PS 6.0 and removed in PS 7.4+ $response = Invoke-WebRequest -Uri "https://www.powershellgallery.com/api/v2" ` -TimeoutSec 10 -ErrorAction Stop return $response.StatusCode -eq 200 } catch { return $false } } function Test-ScriptUpdate { <# .SYNOPSIS Проверяет наличие обновлений WinClean в PowerShell Gallery .DESCRIPTION Сравнивает текущую версию скрипта с последней версией в PowerShell Gallery. Проверяет, был ли скрипт установлен через PSGallery (для возможности автообновления). .OUTPUTS [hashtable] с информацией об обновлении или $null если обновление не требуется #> # Check if we can reach PSGallery if (-not (Test-PSGalleryConnection)) { return $null } try { $currentVersion = [Version]$script:Version # Query PSGallery for latest version $galleryScript = Find-Script -Name "WinClean" -Repository PSGallery -ErrorAction Stop $latestVersion = [Version]$galleryScript.Version if ($latestVersion -gt $currentVersion) { # Check if installed via PSGallery (for auto-update capability) $installedScript = Get-InstalledScript -Name "WinClean" -ErrorAction SilentlyContinue return @{ CurrentVersion = $currentVersion.ToString() LatestVersion = $latestVersion.ToString() IsInstalled = $null -ne $installedScript ReleaseNotes = $galleryScript.ReleaseNotes } } } catch { # Silently fail - update check is not critical Write-Log "Update check failed: $_" -Level WARNING } return $null } function Invoke-ScriptUpdate { <# .SYNOPSIS Предлагает пользователю обновить WinClean и выполняет обновление при подтверждении .PARAMETER UpdateInfo Хэштаблица с информацией об обновлении от Test-ScriptUpdate #> param( [Parameter(Mandatory)] [hashtable]$UpdateInfo ) Write-Host "" Write-Host " ╔══════════════════════════════════════════════════════════════════╗" -ForegroundColor Cyan Write-Host " ║ " -NoNewline -ForegroundColor Cyan Write-Host "UPDATE AVAILABLE" -NoNewline -ForegroundColor Yellow Write-Host " ║" -ForegroundColor Cyan Write-Host " ╚══════════════════════════════════════════════════════════════════╝" -ForegroundColor Cyan Write-Host "" Write-Host " Current version: " -NoNewline -ForegroundColor Gray Write-Host "v$($UpdateInfo.CurrentVersion)" -ForegroundColor White Write-Host " Latest version: " -NoNewline -ForegroundColor Gray Write-Host "v$($UpdateInfo.LatestVersion)" -NoNewline -ForegroundColor Green Write-Host " (new)" -ForegroundColor DarkGreen Write-Host "" Write-Log "Update available: v$($UpdateInfo.CurrentVersion) -> v$($UpdateInfo.LatestVersion)" -Level INFO # In ReportOnly mode, just inform and continue if ($ReportOnly) { Write-Host " ReportOnly mode - skipping update" -ForegroundColor DarkGray Write-Host "" return } # Check if interactive console is available if (-not (Test-InteractiveConsole)) { Write-Host " Non-interactive mode - skipping update prompt" -ForegroundColor DarkGray Write-Host " To update manually: Update-Script -Name WinClean" -ForegroundColor Gray Write-Host "" return } if ($UpdateInfo.IsInstalled) { # Installed via PSGallery - can auto-update Write-Host " Update now? (" -NoNewline -ForegroundColor Gray Write-Host "Y" -NoNewline -ForegroundColor Green Write-Host "/n): " -NoNewline -ForegroundColor Gray $response = Read-Host if ($response -eq '' -or $response -imatch '^[YyДд]') { Write-Host "" Write-Host " Updating WinClean..." -ForegroundColor Cyan try { Update-Script -Name WinClean -Force -ErrorAction Stop Write-Log "Update successful" -Level SUCCESS Write-Host "" Write-Host " ✓ Update complete!" -ForegroundColor Green Write-Host " Please run WinClean again to use the new version." -ForegroundColor Gray Write-Host "" Write-Host " Press any key to exit..." -ForegroundColor DarkGray $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") exit 0 } catch { Write-Log "Update failed: $_" -Level ERROR Write-Host " ✗ Update failed: $_" -ForegroundColor Red Write-Host " Continuing with current version..." -ForegroundColor Yellow Write-Host "" } } else { Write-Log "Update skipped by user" -Level INFO Write-Host " Update skipped. Continuing with current version..." -ForegroundColor DarkGray Write-Host "" } } else { # Not installed via PSGallery - show manual instructions Write-Host " WinClean was not installed via PowerShell Gallery." -ForegroundColor Yellow Write-Host " To enable auto-updates, install with:" -ForegroundColor Gray Write-Host "" Write-Host " Install-Script -Name WinClean -Scope CurrentUser -Force" -ForegroundColor White Write-Host "" Write-Host " Press any key to continue with current version..." -ForegroundColor DarkGray $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") Write-Host "" } } function Install-ModuleWithTimeout { <# .SYNOPSIS Устанавливает PowerShell модуль с таймаутом .DESCRIPTION Использует Background Job для установки модуля с возможностью прервать операцию по таймауту. Решает проблему бесконечного зависания Install-Module. .PARAMETER ModuleName Имя модуля для установки .PARAMETER TimeoutSeconds Таймаут в секундах (по умолчанию 120) .OUTPUTS [bool] $true если модуль успешно установлен, $false при ошибке/таймауте #> param( [Parameter(Mandatory)] [string]$ModuleName, [int]$TimeoutSeconds = 120 ) $job = Start-Job -ScriptBlock { param($moduleName) # Set TLS 1.2 in the job process as well [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Install-Module -Name $moduleName -Force -Scope CurrentUser -AllowClobber -SkipPublisherCheck -ErrorAction Stop } -ArgumentList $ModuleName $completed = Wait-Job $job -Timeout $TimeoutSeconds if ($completed) { $jobState = $job.State $jobError = $null try { Receive-Job $job -ErrorAction Stop } catch { $jobError = $_ } Remove-Job $job -Force if ($jobState -eq 'Completed' -and -not $jobError) { return $true } else { if ($jobError) { Write-Log "Module installation failed: $jobError" -Level ERROR } return $false } } else { # Timeout - kill the job Stop-Job $job -ErrorAction SilentlyContinue Remove-Job $job -Force Write-Log "Module installation timed out after $TimeoutSeconds seconds" -Level ERROR return $false } } function Install-PackageProviderWithTimeout { <# .SYNOPSIS Устанавливает PackageProvider с таймаутом .DESCRIPTION Аналогично Install-ModuleWithTimeout, но для Install-PackageProvider .PARAMETER ProviderName Имя провайдера (обычно NuGet) .PARAMETER TimeoutSeconds Таймаут в секундах (по умолчанию 60) .OUTPUTS [bool] $true если провайдер успешно установлен, $false при ошибке/таймауте #> param( [Parameter(Mandatory)] [string]$ProviderName, [string]$MinimumVersion = "2.8.5.201", [int]$TimeoutSeconds = 60 ) $job = Start-Job -ScriptBlock { param($providerName, $minVersion) [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Install-PackageProvider -Name $providerName -MinimumVersion $minVersion -Force -ErrorAction Stop } -ArgumentList $ProviderName, $MinimumVersion $completed = Wait-Job $job -Timeout $TimeoutSeconds if ($completed) { $jobState = $job.State $jobError = $null try { Receive-Job $job -ErrorAction Stop | Out-Null } catch { $jobError = $_ } Remove-Job $job -Force if ($jobState -eq 'Completed' -and -not $jobError) { return $true } else { if ($jobError) { Write-Log "Package provider installation failed: $jobError" -Level ERROR } return $false } } else { Stop-Job $job -ErrorAction SilentlyContinue Remove-Job $job -Force Write-Log "Package provider installation timed out after $TimeoutSeconds seconds" -Level ERROR return $false } } function Test-PendingReboot { <# .SYNOPSIS Checks if Windows has a pending reboot from previous operations .DESCRIPTION Checks multiple registry locations and system flags to determine if a reboot is pending from Windows Update, CBS, file rename operations, etc. #> $rebootRequired = $false $reasons = @() # Check Windows Update reboot flag $wuKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" if (Test-Path $wuKey) { $rebootRequired = $true $reasons += "Windows Update" } # Check Component-Based Servicing $cbsKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" if (Test-Path $cbsKey) { $rebootRequired = $true $reasons += "Component Servicing" } # Check Pending File Rename Operations $pfroKey = "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" try { $pfroValue = Get-ItemProperty -Path $pfroKey -Name PendingFileRenameOperations -ErrorAction SilentlyContinue if ($pfroValue.PendingFileRenameOperations) { $rebootRequired = $true $reasons += "File Rename Operations" } } catch { } # Check if Computer Rename is pending $compNameKey = "HKLM:\SYSTEM\CurrentControlSet\Control\ComputerName" try { $activeName = (Get-ItemProperty "$compNameKey\ActiveComputerName" -ErrorAction SilentlyContinue).ComputerName $pendingName = (Get-ItemProperty "$compNameKey\ComputerName" -ErrorAction SilentlyContinue).ComputerName if ($activeName -ne $pendingName) { $rebootRequired = $true $reasons += "Computer Rename" } } catch { } return @{ RebootRequired = $rebootRequired Reasons = $reasons } } function Get-FolderSize { <# .SYNOPSIS Calculates folder size in bytes #> param([string]$Path) if (-not (Test-Path -LiteralPath $Path -ErrorAction SilentlyContinue)) { return 0 } try { $size = (Get-ChildItem -LiteralPath $Path -Recurse -Force -File -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum return [long]($size ?? 0) } catch { return 0 } } function Format-FileSize { <# .SYNOPSIS Formats bytes to human-readable size #> param([long]$Bytes) if ($Bytes -ge 1GB) { return "{0:N2} GB" -f ($Bytes / 1GB) } if ($Bytes -ge 1MB) { return "{0:N2} MB" -f ($Bytes / 1MB) } if ($Bytes -ge 1KB) { return "{0:N2} KB" -f ($Bytes / 1KB) } return "$Bytes B" } function ConvertFrom-HumanReadableSize { <# .SYNOPSIS Converts human-readable size string to bytes (inverse of Format-FileSize) .EXAMPLE ConvertFrom-HumanReadableSize "2.5 GB" # Returns 2684354560 ConvertFrom-HumanReadableSize "512MB" # Returns 536870912 #> param([string]$SizeString) if (-not $SizeString) { return 0 } # Handle formats: "2.5 GB", "2.5GB", "512 MB", "100.5MB" if ($SizeString -match '^([\d.,]+)\s*([KMGT]?B)$') { $value = [double]($Matches[1] -replace ',', '.') $unit = $Matches[2].ToUpper() $multiplier = switch ($unit) { 'B' { 1 } 'KB' { 1KB } 'MB' { 1MB } 'GB' { 1GB } 'TB' { 1TB } default { 1 } } return [long]($value * $multiplier) } return 0 } function Test-PathProtected { <# .SYNOPSIS Checks if path is in protected list #> param([string]$Path) $normalizedPath = $Path.TrimEnd('\', '/') foreach ($protected in $script:ProtectedPaths) { $normalizedProtected = $protected.TrimEnd('\', '/') if ($normalizedPath -ieq $normalizedProtected) { return $true } } return $false } function Remove-FolderContent { <# .SYNOPSIS Safely removes folder contents with size tracking #> param( [Parameter(Mandatory)] [string]$Path, [Parameter(Mandatory)] [string]$Category, [string]$Description, [switch]$RemoveFolder ) # Safety check if (Test-PathProtected -Path $Path) { Write-Log "Protected path skipped: $Path" -Level WARNING return } if (-not (Test-Path -LiteralPath $Path -ErrorAction SilentlyContinue)) { return } if ($ReportOnly) { $size = Get-FolderSize -Path $Path if ($size -gt 0 -and $Description) { Write-Log "Would clean: $Description - $(Format-FileSize $size)" -Level DETAIL } return } $sizeBefore = Get-FolderSize -Path $Path try { if ($RemoveFolder) { Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction SilentlyContinue } else { Get-ChildItem -LiteralPath $Path -Force -ErrorAction SilentlyContinue | ForEach-Object { try { # Handle read-only files if ($_.Attributes -band [System.IO.FileAttributes]::ReadOnly) { $_.Attributes = $_.Attributes -band (-bnot [System.IO.FileAttributes]::ReadOnly) } Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction SilentlyContinue } catch { } } } $sizeAfter = Get-FolderSize -Path $Path $freed = $sizeBefore - $sizeAfter if ($freed -gt 0) { # Update statistics (synchronized hashtable handles thread-safety) $script:Stats.TotalFreedBytes += $freed # Update category (not thread-safe, but acceptable for reporting) if (-not $script:Stats.FreedByCategory.ContainsKey($Category)) { $script:Stats.FreedByCategory[$Category] = 0 } $script:Stats.FreedByCategory[$Category] += $freed if ($Description) { Write-Log "$Description - $(Format-FileSize $freed)" -Level SUCCESS } } } catch { Write-Log "Error cleaning $Path`: $_" -Level WARNING $script:Stats.WarningsCount++ } } function Remove-FilesByPattern { <# .SYNOPSIS Removes files matching a pattern with size tracking .DESCRIPTION Handles file patterns (like *.roslynobjectin) that Remove-FolderContent can't handle #> param( [Parameter(Mandatory)] [string]$Pattern, [Parameter(Mandatory)] [string]$Category, [string]$Description ) $files = Get-Item -Path $Pattern -ErrorAction SilentlyContinue | Where-Object { -not $_.PSIsContainer } if (-not $files) { return } $totalSize = ($files | Measure-Object -Property Length -Sum).Sum $totalSize = [long]($totalSize ?? 0) if ($ReportOnly) { if ($totalSize -gt 0 -and $Description) { Write-Log "Would clean: $Description - $(Format-FileSize $totalSize)" -Level DETAIL } return } $freedSize = 0 foreach ($file in $files) { try { $fileSize = $file.Length Remove-Item -LiteralPath $file.FullName -Force -ErrorAction SilentlyContinue if (-not (Test-Path -LiteralPath $file.FullName)) { $freedSize += $fileSize } } catch { } } if ($freedSize -gt 0) { $script:Stats.TotalFreedBytes += $freedSize if (-not $script:Stats.FreedByCategory.ContainsKey($Category)) { $script:Stats.FreedByCategory[$Category] = 0 } $script:Stats.FreedByCategory[$Category] += $freedSize if ($Description) { Write-Log "$Description - $(Format-FileSize $freedSize)" -Level SUCCESS } } } function New-SystemRestorePoint { <# .SYNOPSIS Creates system restore point using Windows PowerShell (for compatibility) #> param([string]$Description = "WinClean Maintenance") if ($SkipRestore) { Write-Log "Restore point creation skipped (parameter)" -Level INFO return $true } if ($ReportOnly) { Write-Log "Would create restore point: $Description" -Level INFO return $true } Write-Log "Creating system restore point..." -Level INFO try { # Checkpoint-Computer doesn't work in PowerShell 7, use Windows PowerShell $scriptBlock = @" try { Enable-ComputerRestore -Drive "$env:SystemDrive" -ErrorAction SilentlyContinue Checkpoint-Computer -Description "$Description" -RestorePointType MODIFY_SETTINGS -ErrorAction Stop Write-Output "SUCCESS" } catch { Write-Output "ERROR: `$_" } "@ # Use Windows PowerShell 5.1 (Checkpoint-Computer not available in PS7) $result = & powershell.exe ` -NoProfile -NoLogo -ExecutionPolicy Bypass -Command $scriptBlock 2>&1 if ($result -like "SUCCESS*") { Write-Log "Restore point created: $Description" -Level SUCCESS return $true } else { throw $result } } catch { Write-Log "Failed to create restore point: $_" -Level WARNING $script:Stats.WarningsCount++ return $false } } #endregion #region ═══════════════════════════════════════════════════════════════════════ # UPDATE FUNCTIONS #═══════════════════════════════════════════════════════════════════════════════ function Update-WindowsSystem { <# .SYNOPSIS Updates Windows including optional driver updates #> Write-Log "WINDOWS UPDATE" -Level TITLE Update-Progress -Activity "Windows Update" -Status "Checking for updates..." if ($SkipUpdates) { Write-Log "Windows Update skipped (parameter)" -Level INFO return } # Early exit for ReportOnly - don't install modules or modify system if ($ReportOnly) { Write-Log "Would check and install: Windows Updates and Drivers" -Level DETAIL return } if (-not (Test-InternetConnection)) { Write-Log "No internet connection - skipping Windows Update" -Level ERROR $script:Stats.ErrorsCount++ return } # Check Windows Update service $wuService = Get-Service -Name wuauserv -ErrorAction SilentlyContinue if (-not $wuService) { Write-Log "Windows Update service not found!" -Level ERROR $script:Stats.ErrorsCount++ return } if ($wuService.Status -ne 'Running') { Write-Log "Starting Windows Update service..." -Level INFO try { Start-Service wuauserv -ErrorAction Stop } catch { Write-Log "Failed to start Windows Update service: $_" -Level ERROR $script:Stats.ErrorsCount++ return } } try { # Install PSWindowsUpdate if needed if (-not (Get-Module -ListAvailable -Name PSWindowsUpdate)) { # Clear any lingering progress bar before module installation Write-Progress -Activity "Windows Update" -Completed -ErrorAction SilentlyContinue # Check PowerShell Gallery availability first Write-Log "Checking PowerShell Gallery availability..." -Level INFO if (-not (Test-PSGalleryConnection)) { Write-Log "PowerShell Gallery is unavailable" -Level ERROR Write-Log "Please check your internet connection or install PSWindowsUpdate manually:" -Level INFO Write-Log " Install-Module PSWindowsUpdate -Force -Scope CurrentUser" -Level INFO $script:Stats.ErrorsCount++ return } Write-Log "Installing PSWindowsUpdate module..." -Level INFO # Ensure NuGet provider with timeout $nuget = Get-PackageProvider -Name NuGet -ErrorAction SilentlyContinue if (-not $nuget -or $nuget.Version -lt [version]"2.8.5.201") { Write-Log "Installing NuGet provider..." -Level INFO if (-not (Install-PackageProviderWithTimeout -ProviderName "NuGet" -TimeoutSeconds 60)) { Write-Log "Failed to install NuGet provider - Windows Update skipped" -Level ERROR Write-Log "Try manual installation: Install-PackageProvider -Name NuGet -Force" -Level INFO $script:Stats.ErrorsCount++ return } Write-Log "NuGet provider installed" -Level SUCCESS } # Install PSWindowsUpdate module with timeout if (-not (Install-ModuleWithTimeout -ModuleName "PSWindowsUpdate" -TimeoutSeconds 120)) { Write-Log "Failed to install PSWindowsUpdate - Windows Update skipped" -Level ERROR Write-Log "Try manual installation: Install-Module PSWindowsUpdate -Force -Scope CurrentUser" -Level INFO $script:Stats.ErrorsCount++ return } Write-Log "PSWindowsUpdate installed" -Level SUCCESS } Import-Module PSWindowsUpdate -ErrorAction Stop $moduleVersion = (Get-Module PSWindowsUpdate).Version Write-Log "PSWindowsUpdate v$moduleVersion loaded" -Level INFO # Register Microsoft Update service $muService = Get-WUServiceManager -ErrorAction SilentlyContinue | Where-Object { $_.Name -eq "Microsoft Update" } if (-not $muService) { Write-Log "Registering Microsoft Update service..." -Level INFO Add-WUServiceManager -MicrosoftUpdate -Confirm:$false -ErrorAction SilentlyContinue | Out-Null } # Search for updates Write-Log "Searching for updates..." -Level INFO Write-Log "System Updates" -Level SECTION $systemUpdates = @(Get-WindowsUpdate -MicrosoftUpdate -NotCategory "Drivers" -ErrorAction SilentlyContinue) Write-Log "Driver Updates" -Level SECTION $driverUpdates = @(Get-WindowsUpdate -MicrosoftUpdate -Category "Drivers" -ErrorAction SilentlyContinue) $totalUpdates = $systemUpdates.Count + $driverUpdates.Count if ($totalUpdates -eq 0) { Write-Log "Windows is up to date" -Level SUCCESS return } Write-Log "Found $($systemUpdates.Count) system updates, $($driverUpdates.Count) driver updates" -Level INFO # Display updates if ($systemUpdates.Count -gt 0) { Write-Host "" Write-Host " System Updates:" -ForegroundColor Cyan foreach ($update in $systemUpdates) { $size = if ($update.Size) { " ($(Format-FileSize $update.Size))" } else { "" } Write-Host " - " -NoNewline -ForegroundColor DarkGray Write-Host "$($update.KB)" -NoNewline -ForegroundColor Yellow Write-Host " $($update.Title)$size" -ForegroundColor Gray } } if ($driverUpdates.Count -gt 0) { Write-Host "" Write-Host " Driver Updates:" -ForegroundColor Cyan foreach ($update in $driverUpdates) { Write-Host " - " -NoNewline -ForegroundColor DarkGray Write-Host "$($update.Title)" -ForegroundColor Gray } } Write-Host "" # Install updates Write-Log "Installing updates..." -Level INFO $installParams = @{ MicrosoftUpdate = $true AcceptAll = $true IgnoreReboot = $true ErrorAction = 'SilentlyContinue' } # Check module version for parameter compatibility if ($moduleVersion -ge [version]"2.3.0") { $installParams.Remove('IgnoreReboot') $installParams['AutoReboot'] = $false } $results = Install-WindowsUpdate @installParams # Handle null/empty results (possible silent error) if (-not $results) { Write-Log "Windows Update returned no results (possible error)" -Level WARNING $script:Stats.WarningsCount++ return } # Count installed updates $installed = @($results | Where-Object { $_.Result -in @('Installed', 'Downloaded') }).Count $failed = @($results | Where-Object { $_.Result -eq 'Failed' }).Count $script:Stats.WindowsUpdatesCount = $installed if ($failed -gt 0) { Write-Log "Installed: $installed, Failed: $failed" -Level WARNING $script:Stats.WarningsCount += $failed } else { Write-Log "All $installed updates installed successfully" -Level SUCCESS } # Check reboot status if (Get-WURebootStatus -Silent -ErrorAction SilentlyContinue) { $script:Stats.RebootRequired = $true Write-Log "Reboot required to complete updates" -Level WARNING } } catch { Write-Log "Windows Update error: $_" -Level ERROR $script:Stats.ErrorsCount++ } } function Update-Applications { <# .SYNOPSIS Updates applications via winget #> Write-Log "APPLICATION UPDATES (WINGET)" -Level TITLE Update-Progress -Activity "Application Updates" -Status "Checking winget..." if ($SkipUpdates) { Write-Log "Application updates skipped (parameter)" -Level INFO return } if (-not (Test-InternetConnection)) { Write-Log "No internet connection - skipping app updates" -Level ERROR $script:Stats.ErrorsCount++ return } # Find winget $wingetPath = $null $wingetCmd = Get-Command winget.exe -ErrorAction SilentlyContinue if ($wingetCmd) { $wingetPath = $wingetCmd.Source } if (-not $wingetPath) { $standardPath = "$env:LOCALAPPDATA\Microsoft\WindowsApps\winget.exe" if (Test-Path $standardPath) { $wingetPath = $standardPath } } if (-not $wingetPath) { Write-Log "Winget not found - install App Installer from Microsoft Store" -Level ERROR $script:Stats.ErrorsCount++ return } try { # Update sources only if not in ReportOnly mode (source update modifies state) if (-not $ReportOnly) { Write-Log "Updating winget sources..." -Level INFO # Run with timeout to prevent hanging $job = Start-Job -ScriptBlock { param($path) & $path source update 2>&1 } -ArgumentList $wingetPath $completed = $job | Wait-Job -Timeout 120 # 2 minutes timeout if (-not $completed) { $job | Stop-Job Write-Log "Winget source update timed out" -Level WARNING } $job | Remove-Job -Force -ErrorAction SilentlyContinue } # Get available updates (use --include-unknown to match actual upgrade behavior) Write-Log "Checking for app updates..." -Level INFO $tempFile = [System.IO.Path]::GetTempFileName() $tempErrorFile = [System.IO.Path]::GetTempFileName() $process = Start-Process -FilePath $wingetPath -ArgumentList "upgrade", "--include-unknown" ` -NoNewWindow -RedirectStandardOutput $tempFile -RedirectStandardError $tempErrorFile -PassThru # Wait with timeout (5 minutes for check operation) $timeoutMs = 300000 if (-not $process.WaitForExit($timeoutMs)) { $process.Kill() Write-Log "Winget upgrade check timed out after 5 minutes" -Level WARNING $script:Stats.WarningsCount++ Remove-Item $tempFile, $tempErrorFile -Force -ErrorAction SilentlyContinue return } $output = Get-Content $tempFile -Raw -Encoding UTF8 -ErrorAction SilentlyContinue $errorOutput = Get-Content $tempErrorFile -Raw -Encoding UTF8 -ErrorAction SilentlyContinue Remove-Item $tempFile -Force -ErrorAction SilentlyContinue Remove-Item $tempErrorFile -Force -ErrorAction SilentlyContinue # Check if winget command failed (any non-zero exit code is an error) if ($process.ExitCode -ne 0) { Write-Log "Winget upgrade check failed (exit code: $($process.ExitCode))" -Level ERROR if ($errorOutput) { Write-Log "Error: $errorOutput" -Level ERROR } $script:Stats.ErrorsCount++ return } # Parse output for update count (language-independent approach) # Uses table separator "---" as marker, then counts all data lines $updateCount = 0 $lines = $output -split "`n" $foundSeparator = $false foreach ($line in $lines) { # Look for table separator line (works in any language) if ($line -match "^-{10,}") { $foundSeparator = $true continue } # Count lines after separator that look like package entries # Must have multiple columns (name, id, version, available, source) if ($foundSeparator) { $trimmed = $line.Trim() # Skip empty lines, footer text ("X upgrades available"), and lines with too few columns if ($trimmed -and $trimmed -notmatch "^\d+\s+(upgrade|обновлен)" -and $trimmed -notmatch "^(No |Нет )" -and ($trimmed -split '\s{2,}').Count -ge 3) { $updateCount++ } } } if ($updateCount -eq 0) { Write-Log "All applications are up to date" -Level SUCCESS return } Write-Log "Available Updates" -Level SECTION Write-Host $output if ($ReportOnly) { Write-Log "Report mode - $updateCount updates available but not installed" -Level INFO return } Write-Log "Installing $updateCount application updates..." -Level INFO Write-Log "This may take several minutes..." -Level INFO # Run upgrade (--include-unknown matches the check above) $upgradeArgs = @( "upgrade", "--all", "--accept-source-agreements", "--accept-package-agreements", "--disable-interactivity", "--include-unknown" ) $upgradeProcess = Start-Process -FilePath $wingetPath -ArgumentList $upgradeArgs ` -NoNewWindow -PassThru # Wait with timeout (20 minutes for upgrade operation - can take long with many updates) $timeoutMs = 1200000 if (-not $upgradeProcess.WaitForExit($timeoutMs)) { $upgradeProcess.Kill() Write-Log "Winget upgrade timed out after 20 minutes" -Level WARNING $script:Stats.WarningsCount++ return } if ($upgradeProcess.ExitCode -eq 0) { $script:Stats.AppUpdatesCount = $updateCount Write-Log "Application updates completed successfully" -Level SUCCESS } else { # Don't count as successful updates if winget returned an error Write-Log "Application updates completed with code: $($upgradeProcess.ExitCode)" -Level WARNING $script:Stats.WarningsCount++ } } catch { Write-Log "Application update error: $_" -Level ERROR $script:Stats.ErrorsCount++ } } #endregion #region ═══════════════════════════════════════════════════════════════════════ # CLEANUP FUNCTIONS #═══════════════════════════════════════════════════════════════════════════════ function Clear-TempFiles { <# .SYNOPSIS Cleans temporary files and system caches #> Write-Log "Temporary Files" -Level SECTION # Define temp paths and remove duplicates (e.g., $env:TEMP often equals $env:LOCALAPPDATA\Temp) $tempPaths = @( @{ Path = $env:TEMP; Desc = "User Temp" } @{ Path = "$env:SystemRoot\Temp"; Desc = "Windows Temp" } @{ Path = "$env:LOCALAPPDATA\Temp"; Desc = "Local Temp" } ) | ForEach-Object { $_.Path = [System.IO.Path]::GetFullPath($_.Path) $_ } | Group-Object Path | ForEach-Object { $_.Group[0] } foreach ($item in $tempPaths) { Remove-FolderContent -Path $item.Path -Category "Temp" -Description $item.Desc } } function Clear-BrowserCaches { <# .SYNOPSIS Cleans browser caches (Edge, Chrome, Firefox, Yandex) #> Write-Log "Browser Caches" -Level SECTION # Define browser cache paths $browsers = @{ "Edge" = @( "$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" ) "Chrome" = @( "$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" ) "Firefox" = @() # Handled separately due to profile structure "Yandex" = @( "$env:LOCALAPPDATA\Yandex\YandexBrowser\User Data\Default\Cache" "$env:LOCALAPPDATA\Yandex\YandexBrowser\User Data\Default\Code Cache" "$env:LOCALAPPDATA\Yandex\YandexBrowser\User Data\Default\GPUCache" ) "Opera" = @( "$env:APPDATA\Opera Software\Opera Stable\Cache" "$env:APPDATA\Opera Software\Opera Stable\Code Cache" "$env:APPDATA\Opera Software\Opera Stable\GPUCache" ) "Brave" = @( "$env:LOCALAPPDATA\BraveSoftware\Brave-Browser\User Data\Default\Cache" "$env:LOCALAPPDATA\BraveSoftware\Brave-Browser\User Data\Default\Code Cache" "$env:LOCALAPPDATA\BraveSoftware\Brave-Browser\User Data\Default\GPUCache" ) } # Clean standard browsers in parallel $allPaths = @() foreach ($browser in $browsers.Keys) { foreach ($path in $browsers[$browser]) { if (Test-Path -LiteralPath $path -ErrorAction SilentlyContinue) { $allPaths += @{ Browser = $browser; Path = $path } } } } # Also check for additional Chrome/Edge profiles (with full cache set) foreach ($browser in @("Chrome", "Edge")) { $basePath = if ($browser -eq "Chrome") { "$env:LOCALAPPDATA\Google\Chrome\User Data" } else { "$env:LOCALAPPDATA\Microsoft\Edge\User Data" } if (Test-Path $basePath) { Get-ChildItem -Path $basePath -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -like "Profile *" } | ForEach-Object { # Add same cache types as Default profile (fixed in v2.1) $profileCacheTypes = @("Cache", "Code Cache", "GPUCache", "Service Worker\CacheStorage") foreach ($cacheType in $profileCacheTypes) { $profileCache = Join-Path $_.FullName $cacheType if (Test-Path $profileCache) { $allPaths += @{ Browser = "$browser $($_.Name)"; Path = $profileCache } } } } } } # Clean in parallel (with ReportOnly check) if ($allPaths.Count -gt 0) { # Get browser names for logging $browserNames = ($allPaths | Select-Object -ExpandProperty Browser -Unique) -join ', ' # Measure size before cleanup $sizeBefore = ($allPaths | ForEach-Object { Get-FolderSize -Path $_.Path } | Measure-Object -Sum).Sum if ($ReportOnly) { # In ReportOnly mode, just show what would be cleaned Write-Log "Would clean browser caches ($browserNames) - $(Format-FileSize $sizeBefore)" -Level DETAIL } else { # Actual cleanup $allPaths | ForEach-Object -Parallel { $item = $_ $path = $item.Path if (Test-Path -LiteralPath $path) { try { Get-ChildItem -LiteralPath $path -Force -ErrorAction SilentlyContinue | ForEach-Object { Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction SilentlyContinue } } catch { } } } -ThrottleLimit 8 # Measure size after cleanup to get actual freed space $sizeAfter = ($allPaths | ForEach-Object { Get-FolderSize -Path $_.Path } | Measure-Object -Sum).Sum $sizeAfter = [long]($sizeAfter ?? 0) # Ensure non-null value # Protect against negative values (can happen if browser recreates files during cleanup) $freedSpace = [math]::Max(0, $sizeBefore - $sizeAfter) # Update statistics with actual freed space (not estimated) if ($freedSpace -gt 0) { $script:Stats.TotalFreedBytes += $freedSpace if (-not $script:Stats.FreedByCategory.ContainsKey("Browser")) { $script:Stats.FreedByCategory["Browser"] = 0 } $script:Stats.FreedByCategory["Browser"] += $freedSpace Write-Log "Browser caches cleaned ($browserNames) - $(Format-FileSize $freedSpace)" -Level SUCCESS } else { Write-Log "Browser caches cleaned ($browserNames)" -Level SUCCESS } } } # Handle Firefox profiles separately $firefoxProfiles = "$env:APPDATA\Mozilla\Firefox\Profiles" if (Test-Path $firefoxProfiles) { Get-ChildItem -Path $firefoxProfiles -Directory -ErrorAction SilentlyContinue | ForEach-Object { Remove-FolderContent -Path "$($_.FullName)\cache2" -Category "Browser" -Description "Firefox cache" Remove-FolderContent -Path "$($_.FullName)\startupCache" -Category "Browser" } } } function Clear-WindowsUpdateCache { <# .SYNOPSIS Cleans Windows Update download cache #> Write-Log "Windows Update Cache" -Level SECTION if ($ReportOnly) { $size = Get-FolderSize -Path "$env:SystemRoot\SoftwareDistribution\Download" Write-Log "Would clean: Windows Update cache - $(Format-FileSize $size)" -Level DETAIL return } # Stop services with try/finally to ensure they restart Write-Log "Stopping Windows Update services..." -Level DETAIL -NoLog $servicesStopped = $false try { Stop-Service -Name wuauserv, bits -Force -ErrorAction SilentlyContinue $servicesStopped = $true # Clean Remove-FolderContent -Path "$env:SystemRoot\SoftwareDistribution\Download" -Category "WinUpdate" -Description "Windows Update cache" } finally { # Always restart services if ($servicesStopped) { Start-Service -Name wuauserv, bits -ErrorAction SilentlyContinue } } } function Get-RecycleBinSize { <# .SYNOPSIS Gets the total size of items in the Recycle Bin #> $totalSize = [long]0 try { $shell = New-Object -ComObject Shell.Application $recycleBin = $shell.Namespace(0xA) foreach ($item in $recycleBin.Items()) { try { # Try ExtendedProperty first (more reliable) $itemSize = $item.ExtendedProperty("System.Size") if ($itemSize) { $totalSize += [long]$itemSize } else { # Fallback: try GetDetailsOf (index 2 = Size column) $sizeStr = $recycleBin.GetDetailsOf($item, 2) if ($sizeStr) { $totalSize += ConvertFrom-HumanReadableSize $sizeStr } } } catch { # Ignore errors for individual items } } } catch { # Return 0 if we can't access recycle bin } return $totalSize } function Clear-WinCleanRecycleBin { <# .SYNOPSIS Empties the Recycle Bin with size tracking #> Write-Log "Recycle Bin" -Level SECTION # Measure size before cleanup $sizeBefore = Get-RecycleBinSize if ($ReportOnly) { if ($sizeBefore -gt 0) { Write-Log "Would clean: Recycle Bin - $(Format-FileSize $sizeBefore)" -Level DETAIL } else { Write-Log "Recycle Bin is empty" -Level DETAIL } return } if ($sizeBefore -eq 0) { Write-Log "Recycle Bin is already empty" -Level INFO return } try { # Use full cmdlet path to explicitly call the built-in cmdlet Microsoft.PowerShell.Management\Clear-RecycleBin -Force -ErrorAction Stop # Update statistics $script:Stats.TotalFreedBytes += $sizeBefore if (-not $script:Stats.FreedByCategory.ContainsKey("Recycle Bin")) { $script:Stats.FreedByCategory["Recycle Bin"] = 0 } $script:Stats.FreedByCategory["Recycle Bin"] += $sizeBefore Write-Log "Recycle Bin emptied - $(Format-FileSize $sizeBefore)" -Level SUCCESS } catch { # Fallback to COM method try { $shell = New-Object -ComObject Shell.Application $recycleBin = $shell.Namespace(0xA) $items = $recycleBin.Items() $count = $items.Count $items | ForEach-Object { Remove-Item -LiteralPath $_.Path -Recurse -Force -ErrorAction SilentlyContinue } # Update statistics even for fallback method $script:Stats.TotalFreedBytes += $sizeBefore if (-not $script:Stats.FreedByCategory.ContainsKey("Recycle Bin")) { $script:Stats.FreedByCategory["Recycle Bin"] = 0 } $script:Stats.FreedByCategory["Recycle Bin"] += $sizeBefore Write-Log "Recycle Bin emptied ($count items) - $(Format-FileSize $sizeBefore)" -Level SUCCESS } catch { Write-Log "Could not empty Recycle Bin: $_" -Level WARNING $script:Stats.WarningsCount++ } } } function Clear-SystemCaches { <# .SYNOPSIS Cleans various Windows system caches #> Write-Log "System Caches" -Level SECTION $systemPaths = @( @{ Path = "$env:SystemRoot\Prefetch"; Desc = "Prefetch" } @{ Path = "$env:LOCALAPPDATA\IconCache.db"; Desc = "Icon cache"; File = $true } @{ Path = "$env:LOCALAPPDATA\Microsoft\Windows\Explorer"; Desc = "Thumbnail cache" } @{ Path = "$env:LOCALAPPDATA\Microsoft\Windows\WER"; Desc = "Error reports (local)" } @{ Path = "$env:ProgramData\Microsoft\Windows\WER"; Desc = "Error reports (system)" } @{ Path = "$env:ProgramData\USOShared\Logs"; Desc = "Update logs" } @{ Path = "$env:SystemDrive\ProgramData\Microsoft\Windows\DeliveryOptimization"; Desc = "Delivery Optimization" } @{ Path = "$env:LOCALAPPDATA\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalCache"; Desc = "Windows Store cache" } ) foreach ($item in $systemPaths) { if ($item.File) { if (Test-Path -LiteralPath $item.Path -ErrorAction SilentlyContinue) { $fileSize = (Get-Item -LiteralPath $item.Path -ErrorAction SilentlyContinue).Length $fileSize = [long]($fileSize ?? 0) if ($ReportOnly) { Write-Log "Would clean: $($item.Desc) - $(Format-FileSize $fileSize)" -Level DETAIL } else { Remove-Item -LiteralPath $item.Path -Force -ErrorAction SilentlyContinue if ($fileSize -gt 0) { $script:Stats.TotalFreedBytes += $fileSize if (-not $script:Stats.FreedByCategory.ContainsKey("System")) { $script:Stats.FreedByCategory["System"] = 0 } $script:Stats.FreedByCategory["System"] += $fileSize } Write-Log "$($item.Desc) cleaned" -Level DETAIL } } } else { Remove-FolderContent -Path $item.Path -Category "System" -Description $item.Desc } } } function Clear-EventLogs { <# .SYNOPSIS Clears Windows Event Logs (excluding critical ones) #> Write-Log "Event Logs" -Level SECTION if ($ReportOnly) { Write-Log "Would clean: Windows Event Logs" -Level DETAIL return } try { $logs = wevtutil el 2>$null | Where-Object { $_ -notmatch 'Analytic' -and $_ -notmatch 'Debug' -and $_ -ne 'Security' # Keep only the main Security log (exact match, fixed in v2.1) } $clearedCount = 0 $failedCount = 0 foreach ($log in $logs) { try { wevtutil cl $log 2>$null if ($LASTEXITCODE -eq 0) { $clearedCount++ } else { $failedCount++ } } catch { $failedCount++ } } if ($failedCount -gt 0) { Write-Log "Event logs cleared: $clearedCount, failed: $failedCount" -Level WARNING $script:Stats.WarningsCount++ } else { Write-Log "Event logs cleared ($clearedCount logs)" -Level SUCCESS } } catch { Write-Log "Error clearing event logs: $_" -Level WARNING $script:Stats.WarningsCount++ } } function Clear-DNSCache { <# .SYNOPSIS Flushes DNS resolver cache .DESCRIPTION Clears the DNS client cache to resolve potential DNS issues and free up memory used by cached DNS entries #> Write-Log "DNS Cache" -Level SECTION if ($ReportOnly) { Write-Log "Would flush: DNS resolver cache" -Level DETAIL return } try { # Flush DNS cache using ipconfig $result = ipconfig /flushdns 2>&1 $exitCode = $LASTEXITCODE if ($exitCode -eq 0 -or $result -match "Successfully flushed|успешно") { Write-Log "DNS cache flushed successfully" -Level SUCCESS } else { # Command completed but may have failed - log as warning Write-Log "DNS cache flush returned unexpected result (exit code: $exitCode)" -Level WARNING $script:Stats.WarningsCount++ } # Also clear DNS client cache via cmdlet if available try { Clear-DnsClientCache -ErrorAction SilentlyContinue } catch { } } catch { Write-Log "Error flushing DNS cache: $_" -Level WARNING $script:Stats.WarningsCount++ } } function Clear-PrivacyTraces { <# .SYNOPSIS Clears privacy-related traces (Run history, recent files, etc.) .DESCRIPTION Removes various Windows usage traces including: - Run dialog history (Win+R) - Recent documents MRU - Explorer search history #> Write-Log "Privacy Traces" -Level SECTION if ($ReportOnly) { Write-Log "Would clean: Run dialog history, Recent documents MRU" -Level DETAIL return } $clearedItems = @() # Clear Run dialog history (RunMRU) $runMruKey = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\RunMRU" if (Test-Path $runMruKey) { try { # Get current values before clearing $mruValues = Get-ItemProperty -Path $runMruKey -ErrorAction SilentlyContinue $valueCount = ($mruValues.PSObject.Properties | Where-Object { $_.Name -match '^[a-z]$' }).Count # Remove the key and recreate it empty Remove-Item -Path $runMruKey -Force -ErrorAction SilentlyContinue New-Item -Path $runMruKey -Force -ErrorAction SilentlyContinue | Out-Null if ($valueCount -gt 0) { $clearedItems += "Run history ($valueCount entries)" } } catch { Write-Log "Could not clear Run history: $_" -Level WARNING } } # Clear TypedPaths (Explorer address bar history) $typedPathsKey = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\TypedPaths" if (Test-Path $typedPathsKey) { try { Remove-Item -Path $typedPathsKey -Force -ErrorAction SilentlyContinue New-Item -Path $typedPathsKey -Force -ErrorAction SilentlyContinue | Out-Null $clearedItems += "Explorer typed paths" } catch { } } # Clear WordWheelQuery (Explorer search history) $searchKey = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\WordWheelQuery" if (Test-Path $searchKey) { try { Remove-Item -Path $searchKey -Force -ErrorAction SilentlyContinue New-Item -Path $searchKey -Force -ErrorAction SilentlyContinue | Out-Null $clearedItems += "Explorer search history" } catch { } } # Clear Recent documents folder $recentFolder = [Environment]::GetFolderPath('Recent') if (Test-Path $recentFolder) { try { $recentCount = (Get-ChildItem -Path $recentFolder -Force -ErrorAction SilentlyContinue).Count Get-ChildItem -Path $recentFolder -Force -ErrorAction SilentlyContinue | ForEach-Object { Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction SilentlyContinue } if ($recentCount -gt 0) { $clearedItems += "Recent documents ($recentCount items)" } } catch { } } if ($clearedItems.Count -gt 0) { Write-Log "Privacy traces cleared: $($clearedItems -join ', ')" -Level SUCCESS } else { Write-Log "No privacy traces found to clear" -Level INFO } } function Set-WindowsTelemetry { <# .SYNOPSIS Configures Windows telemetry settings .DESCRIPTION Disables or minimizes Windows telemetry data collection by setting appropriate registry values and group policies #> param( [switch]$Disable ) if (-not $Disable) { return } Write-Log "WINDOWS TELEMETRY CONFIGURATION" -Level TITLE if ($ReportOnly) { Write-Log "Would configure: Disable Windows telemetry" -Level DETAIL return } $changesApplied = @() try { # Set telemetry level to Security (0 = Security, 1 = Basic, 2 = Enhanced, 3 = Full) $dataCollectionKey = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection" if (-not (Test-Path $dataCollectionKey)) { New-Item -Path $dataCollectionKey -Force -ErrorAction SilentlyContinue | Out-Null } # AllowTelemetry = 0 (Security - minimum telemetry, only for Enterprise/Education) # For other editions, setting to 1 (Basic) is the minimum allowed # Use EditionID from registry (language-independent, fixed in v2.1) $editionId = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name EditionID -ErrorAction SilentlyContinue).EditionID $telemetryLevel = if ($editionId -match "Enterprise|Education") { 0 } else { 1 } Set-ItemProperty -Path $dataCollectionKey -Name "AllowTelemetry" -Value $telemetryLevel -Type DWord -Force $changesApplied += "Telemetry level set to $telemetryLevel" # Disable Customer Experience Improvement Program Set-ItemProperty -Path $dataCollectionKey -Name "DoNotShowFeedbackNotifications" -Value 1 -Type DWord -Force $changesApplied += "Feedback notifications disabled" # Disable Application Telemetry $appCompatKey = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\AppCompat" if (-not (Test-Path $appCompatKey)) { New-Item -Path $appCompatKey -Force -ErrorAction SilentlyContinue | Out-Null } Set-ItemProperty -Path $appCompatKey -Name "AITEnable" -Value 0 -Type DWord -Force $changesApplied += "Application telemetry disabled" # Disable Advertising ID $advertisingKey = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\AdvertisingInfo" if (-not (Test-Path $advertisingKey)) { New-Item -Path $advertisingKey -Force -ErrorAction SilentlyContinue | Out-Null } Set-ItemProperty -Path $advertisingKey -Name "DisabledByGroupPolicy" -Value 1 -Type DWord -Force $changesApplied += "Advertising ID disabled" # Disable Windows Error Reporting $werKey = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Error Reporting" if (-not (Test-Path $werKey)) { New-Item -Path $werKey -Force -ErrorAction SilentlyContinue | Out-Null } Set-ItemProperty -Path $werKey -Name "Disabled" -Value 1 -Type DWord -Force $changesApplied += "Windows Error Reporting disabled" Write-Log "Telemetry settings applied:" -Level SUCCESS foreach ($change in $changesApplied) { Write-Log " - $change" -Level DETAIL } Write-Log "Note: Some changes may require a system restart to take effect" -Level INFO } catch { Write-Log "Error configuring telemetry: $_" -Level ERROR $script:Stats.ErrorsCount++ } } function Clear-WindowsOld { <# .SYNOPSIS Removes Windows.old folder with user confirmation #> $windowsOldPath = "$env:SystemDrive\Windows.old" if (-not (Test-Path $windowsOldPath)) { return } Write-Log "Previous Windows Installation" -Level SECTION $size = Get-FolderSize -Path $windowsOldPath $sizeFormatted = Format-FileSize $size Write-Log "Found Windows.old folder ($sizeFormatted)" -Level WARNING if ($ReportOnly) { Write-Log "Would clean: Windows.old - $sizeFormatted" -Level DETAIL return } # Check if running in interactive console (fixed in v2.1) if (-not (Test-InteractiveConsole)) { # Non-interactive: skip with safe default (don't delete without confirmation) Write-Log "Non-interactive mode - skipping Windows.old deletion (requires user confirmation)" -Level INFO return } # Interactive prompt with timeout Write-Host "" Write-Host " This folder contains files from a previous Windows installation." -ForegroundColor DarkGray Write-Host " Delete Windows.old? (" -NoNewline -ForegroundColor Yellow Write-Host "Y" -NoNewline -ForegroundColor Green Write-Host "/n, default " -NoNewline -ForegroundColor Yellow Write-Host "Y" -NoNewline -ForegroundColor Green Write-Host " in 15 sec): " -NoNewline -ForegroundColor Yellow $timeout = 15 $startTime = Get-Date $response = "" # Clear keyboard buffer while ([Console]::KeyAvailable) { [Console]::ReadKey($true) | Out-Null } while ((Get-Date) -lt $startTime.AddSeconds($timeout)) { if ([Console]::KeyAvailable) { $key = [Console]::ReadKey($true) if ($key.Key -eq "Enter" -or $key.KeyChar -match "[YyДд]") { $response = "Y" Write-Host "Y" -ForegroundColor Green break } elseif ($key.KeyChar -match "[NnНн]") { $response = "N" Write-Host "N" -ForegroundColor Red break } } $remaining = $timeout - [int]((Get-Date) - $startTime).TotalSeconds Write-Host "`r Delete Windows.old? (Y/n, default Y in $remaining sec): " -NoNewline -ForegroundColor Yellow Start-Sleep -Milliseconds 100 } if ($response -eq "" -or $response -eq "Y") { if ($response -eq "") { Write-Host "Y" -ForegroundColor Green } Write-Log "Removing Windows.old..." -Level INFO try { # Take ownership and remove $null = takeown /F $windowsOldPath /A /R /D Y 2>&1 $null = icacls $windowsOldPath /grant Administrators:F /T /C /Q 2>&1 Remove-Item -Path $windowsOldPath -Recurse -Force -ErrorAction SilentlyContinue if (-not (Test-Path $windowsOldPath)) { Write-Log "Windows.old removed - $sizeFormatted freed" -Level SUCCESS $script:Stats.TotalFreedBytes += $size $script:Stats.FreedByCategory["Windows.old"] = $size } else { Write-Log "Could not fully remove Windows.old" -Level WARNING $script:Stats.WarningsCount++ } } catch { Write-Log "Error removing Windows.old: $_" -Level ERROR $script:Stats.ErrorsCount++ } } else { Write-Log "Windows.old removal cancelled by user" -Level INFO } } #endregion #region ═══════════════════════════════════════════════════════════════════════ # DEVELOPER CLEANUP FUNCTIONS #═══════════════════════════════════════════════════════════════════════════════ function Clear-DeveloperCaches { <# .SYNOPSIS Cleans developer tool caches (npm, pip, nuget, composer, etc.) #> if ($SkipDevCleanup) { Write-Log "Developer cache cleanup skipped (parameter)" -Level INFO return } Write-Log "DEVELOPER CACHES" -Level TITLE Update-Progress -Activity "Developer Cleanup" -Status "Cleaning caches..." # NPM Cache Write-Log "npm Cache" -Level SECTION $npmCache = "$env:APPDATA\npm-cache" if (Test-Path $npmCache) { if ($ReportOnly) { $size = Get-FolderSize $npmCache Write-Log "Would clean: npm cache - $(Format-FileSize $size)" -Level DETAIL } else { # Use npm cache clean if available $npm = Get-Command npm -ErrorAction SilentlyContinue if ($npm) { try { $sizeBefore = Get-FolderSize $npmCache & npm cache clean --force 2>&1 | Out-Null $sizeAfter = Get-FolderSize $npmCache $freed = $sizeBefore - $sizeAfter if ($freed -gt 0) { $script:Stats.TotalFreedBytes += $freed if (-not $script:Stats.FreedByCategory.ContainsKey("Developer")) { $script:Stats.FreedByCategory["Developer"] = 0 } $script:Stats.FreedByCategory["Developer"] += $freed Write-Log "npm cache cleaned - $(Format-FileSize $freed)" -Level SUCCESS } else { Write-Log "npm cache cleaned (via npm)" -Level SUCCESS } } catch { Remove-FolderContent -Path $npmCache -Category "Developer" -Description "npm cache" } } else { Remove-FolderContent -Path $npmCache -Category "Developer" -Description "npm cache" } } } # pip Cache Write-Log "pip Cache" -Level SECTION $pipCaches = @( "$env:LOCALAPPDATA\pip\Cache" "$env:APPDATA\pip\cache" "$env:USERPROFILE\.cache\pip" ) foreach ($pipCache in $pipCaches) { Remove-FolderContent -Path $pipCache -Category "Developer" -Description "pip cache" } # NuGet Cache (only metadata caches, not packages!) Write-Log "NuGet Cache" -Level SECTION $nugetCaches = @( "$env:LOCALAPPDATA\NuGet\v3-cache" # Metadata cache "$env:LOCALAPPDATA\NuGet\plugins-cache" # Plugin cache "$env:LOCALAPPDATA\NuGet\http-cache" # HTTP cache # Note: $env:USERPROFILE\.nuget\packages is NOT cache - it's the global packages folder ) foreach ($cache in $nugetCaches) { Remove-FolderContent -Path $cache -Category "Developer" -Description "NuGet cache" } # Composer Cache Write-Log "Composer Cache" -Level SECTION $composerCache = "$env:LOCALAPPDATA\Composer\cache" Remove-FolderContent -Path $composerCache -Category "Developer" -Description "Composer cache" # Gradle (only safe cache directories, not full repositories!) Write-Log "Gradle Cache" -Level SECTION $gradleCaches = @( "$env:USERPROFILE\.gradle\caches\build-cache-1" # Build cache "$env:USERPROFILE\.gradle\caches\transforms-*" # Transform cache "$env:USERPROFILE\.gradle\daemon" # Daemon logs # Note: .gradle\caches\modules-* contains downloaded dependencies - dangerous to delete! # Note: .m2\repository is Maven local repo - do NOT delete! ) foreach ($pattern in $gradleCaches) { Get-ChildItem -Path (Split-Path $pattern -Parent) -Filter (Split-Path $pattern -Leaf) -Directory -ErrorAction SilentlyContinue | ForEach-Object { Remove-FolderContent -Path $_.FullName -Category "Developer" -Description "Gradle cache" } } # yarn Cache Write-Log "yarn Cache" -Level SECTION $yarnCaches = @( "$env:LOCALAPPDATA\Yarn\Cache" "$env:USERPROFILE\.cache\yarn" ) foreach ($cache in $yarnCaches) { Remove-FolderContent -Path $cache -Category "Developer" -Description "yarn cache" } # pnpm Cache Write-Log "pnpm Cache" -Level SECTION $pnpmCache = "$env:LOCALAPPDATA\pnpm-cache" Remove-FolderContent -Path $pnpmCache -Category "Developer" -Description "pnpm cache" # Go Cache Write-Log "Go Cache" -Level SECTION $goCache = "$env:LOCALAPPDATA\go-build" Remove-FolderContent -Path $goCache -Category "Developer" -Description "Go build cache" # Rust/Cargo Cache Write-Log "Cargo Cache" -Level SECTION $cargoCaches = @( "$env:USERPROFILE\.cargo\registry\cache" "$env:USERPROFILE\.cargo\git\db" ) foreach ($cache in $cargoCaches) { Remove-FolderContent -Path $cache -Category "Developer" -Description "Cargo cache" } } #endregion #region ═══════════════════════════════════════════════════════════════════════ # DOCKER/WSL CLEANUP FUNCTIONS #═══════════════════════════════════════════════════════════════════════════════ function Clear-DockerWSL { <# .SYNOPSIS Cleans Docker images, containers, and WSL2 disk #> if ($SkipDockerCleanup) { Write-Log "Docker/WSL cleanup skipped (parameter)" -Level INFO return } Write-Log "DOCKER & WSL CLEANUP" -Level TITLE Update-Progress -Activity "Docker/WSL Cleanup" -Status "Checking Docker..." # Docker Cleanup $docker = Get-Command docker -ErrorAction SilentlyContinue if ($docker) { Write-Log "Docker Cleanup" -Level SECTION # Check if Docker is running $dockerRunning = $false try { $dockerInfo = docker info 2>&1 $exitCode = $LASTEXITCODE # Capture immediately after command $dockerRunning = $exitCode -eq 0 } catch { # Docker command failed to execute (not installed or path issue) } if ($dockerRunning) { if ($ReportOnly) { Write-Log "Would run: docker system prune" -Level DETAIL } else { try { # Remove unused data (stopped containers, unused networks, dangling images, build cache) Write-Log "Running docker system prune..." -Level INFO $result = docker system prune -f 2>&1 # Parse reclaimed space and add to statistics # Supports both "reclaimed 1.23GB" and "Total reclaimed space: 1.23GB" formats if ($result -match "reclaimed\s+(?:space:\s*)?([\d.,]+\s*[KMGT]?B)") { $reclaimedStr = $Matches[1] $reclaimedBytes = ConvertFrom-HumanReadableSize $reclaimedStr Write-Log "Docker cleanup: $reclaimedStr reclaimed" -Level SUCCESS if ($reclaimedBytes -gt 0) { $script:Stats.TotalFreedBytes += $reclaimedBytes if (-not $script:Stats.FreedByCategory.ContainsKey("Docker")) { $script:Stats.FreedByCategory["Docker"] = 0 } $script:Stats.FreedByCategory["Docker"] += $reclaimedBytes } } else { Write-Log "Docker cleanup completed" -Level SUCCESS } # Note: docker system prune -f already includes build cache cleanup } catch { Write-Log "Docker cleanup error: $_" -Level WARNING $script:Stats.WarningsCount++ } } } else { Write-Log "Docker is not running - skipping cleanup" -Level INFO } } else { Write-Log "Docker not installed" -Level INFO } # WSL2 Disk Compaction Write-Log "WSL2 Disk Optimization" -Level SECTION $wsl = Get-Command wsl -ErrorAction SilentlyContinue if ($wsl) { try { # Define all possible VHDX locations (including Docker) $wslPaths = @( "$env:LOCALAPPDATA\Packages\*CanonicalGroupLimited*\LocalState" "$env:LOCALAPPDATA\Packages\*MicrosoftCorporationII.WindowsSubsystemForLinux*\LocalState" "$env:LOCALAPPDATA\Docker\wsl\data" "$env:LOCALAPPDATA\Docker\wsl\distro" ) # Find all VHDX files first (don't depend on WSL distro list) $vhdxFiles = @() foreach ($pattern in $wslPaths) { $vhdxFiles += Get-ChildItem -Path $pattern -Filter "*.vhdx" -Recurse -ErrorAction SilentlyContinue } if ($vhdxFiles.Count -gt 0) { if ($ReportOnly) { $totalSize = ($vhdxFiles | Measure-Object -Property Length -Sum).Sum Write-Log "Would optimize $($vhdxFiles.Count) WSL2/Docker disk(s) - Total: $(Format-FileSize $totalSize)" -Level DETAIL } else { # Shutdown WSL first (this also stops Docker WSL backends) Write-Log "Shutting down WSL..." -Level INFO wsl --shutdown Start-Sleep -Seconds 2 # Compact each VHDX file foreach ($vhdxFile in $vhdxFiles) { $vhdx = $vhdxFile.FullName $sizeBefore = $vhdxFile.Length Write-Log "Compacting $($vhdxFile.Name)..." -Level DETAIL try { # Use diskpart to compact $diskpartScript = @" select vdisk file="$vhdx" compact vdisk exit "@ $diskpartScript | diskpart | Out-Null $sizeAfter = (Get-Item $vhdx).Length $saved = $sizeBefore - $sizeAfter if ($saved -gt 0) { Write-Log "Compacted $($vhdxFile.Name): $(Format-FileSize $saved) saved" -Level SUCCESS $script:Stats.TotalFreedBytes += $saved if (-not $script:Stats.FreedByCategory.ContainsKey("WSL")) { $script:Stats.FreedByCategory["WSL"] = 0 } $script:Stats.FreedByCategory["WSL"] += $saved } else { Write-Log "Compacted $($vhdxFile.Name): no space saved" -Level INFO } } catch { Write-Log "Could not compact $($vhdxFile.Name): $_" -Level WARNING } } } } else { Write-Log "No WSL2/Docker VHDX files found" -Level INFO } } catch { Write-Log "WSL optimization error: $_" -Level WARNING $script:Stats.WarningsCount++ } } else { Write-Log "WSL not installed" -Level INFO } } #endregion #region ═══════════════════════════════════════════════════════════════════════ # VISUAL STUDIO CLEANUP FUNCTIONS #═══════════════════════════════════════════════════════════════════════════════ function Clear-VisualStudio { <# .SYNOPSIS Cleans Visual Studio caches and temporary files #> if ($SkipVSCleanup) { Write-Log "Visual Studio cleanup skipped (parameter)" -Level INFO return } Write-Log "VISUAL STUDIO CLEANUP" -Level TITLE Update-Progress -Activity "Visual Studio Cleanup" -Status "Cleaning caches..." # VS 2019/2022 caches (directories) $vsCacheDirs = @( @{ Path = "$env:LOCALAPPDATA\Microsoft\VisualStudio\*\ComponentModelCache"; Desc = "Component Model Cache" } @{ Path = "$env:LOCALAPPDATA\Microsoft\VisualStudio\*\ImageCacheRoot"; Desc = "Image Cache" } @{ Path = "$env:LOCALAPPDATA\Microsoft\VisualStudio\*\DesignTimeBuild"; Desc = "Design Time Build" } @{ Path = "$env:LOCALAPPDATA\Microsoft\VSCommon\*\SQM"; Desc = "SQM Data" } @{ Path = "$env:LOCALAPPDATA\Microsoft\VisualStudio\Packages\_Instances"; Desc = "Package Instances" } ) # VS file patterns (handled separately, fixed in v2.1) $vsFilePatterns = @( @{ Pattern = "$env:APPDATA\Microsoft\VisualStudio\*\*.roslynobjectin"; Desc = "Roslyn Temp" } ) Write-Log "Visual Studio Caches" -Level SECTION # Process directory caches foreach ($item in $vsCacheDirs) { $paths = Resolve-Path -Path $item.Path -ErrorAction SilentlyContinue foreach ($path in $paths) { Remove-FolderContent -Path $path.Path -Category "VS" -Description $item.Desc } } # Process file patterns (fixed in v2.1 - files were not being deleted before) foreach ($item in $vsFilePatterns) { Remove-FilesByPattern -Pattern $item.Pattern -Category "VS" -Description $item.Desc } # MEF Cache Write-Log "MEF Cache" -Level SECTION $mefPath = "$env:LOCALAPPDATA\Microsoft\VisualStudio" if (Test-Path $mefPath) { Get-ChildItem -Path $mefPath -Directory -ErrorAction SilentlyContinue | ForEach-Object { $mefCache = Join-Path $_.FullName "MEFCacheAssembly" Remove-FolderContent -Path $mefCache -Category "VS" -Description "MEF Cache" } } # VS Code caches Write-Log "VS Code Caches" -Level SECTION $vscodeCaches = @( "$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" "$env:APPDATA\Code - Insiders\Cache" "$env:APPDATA\Code - Insiders\CachedData" ) foreach ($cache in $vscodeCaches) { Remove-FolderContent -Path $cache -Category "VS Code" -Description "VS Code cache" } # JetBrains IDEs Write-Log "JetBrains IDE Caches" -Level SECTION $jetbrainsBase = "$env:LOCALAPPDATA\JetBrains" if (Test-Path $jetbrainsBase) { Get-ChildItem -Path $jetbrainsBase -Directory -ErrorAction SilentlyContinue | ForEach-Object { $cacheDirs = @("caches", "index", "tmp", "log") foreach ($cacheDir in $cacheDirs) { $fullPath = Join-Path $_.FullName $cacheDir Remove-FolderContent -Path $fullPath -Category "JetBrains" -Description "JetBrains $cacheDir" } } } } #endregion #region ═══════════════════════════════════════════════════════════════════════ # SYSTEM CLEANUP FUNCTIONS #═══════════════════════════════════════════════════════════════════════════════ function Invoke-DISMCleanup { <# .SYNOPSIS Runs DISM component cleanup #> # Clear any existing progress bar before DISM outputs to console Write-Progress -Activity "Cleanup" -Completed Write-Log "Windows Component Cleanup (DISM)" -Level SECTION if ($ReportOnly) { Write-Log "Would run: DISM /Online /Cleanup-Image /StartComponentCleanup /ResetBase" -Level DETAIL Write-Log "Note: /ResetBase removes ability to uninstall updates" -Level WARNING return } Write-Log "Running DISM cleanup (this may take several minutes)..." -Level INFO try { $dismProcess = Start-Process -FilePath "$env:SystemRoot\System32\Dism.exe" ` -ArgumentList "/Online", "/Cleanup-Image", "/StartComponentCleanup", "/ResetBase" ` -NoNewWindow -PassThru # Wait with timeout (15 minutes for DISM operation) $timeoutMs = 900000 if (-not $dismProcess.WaitForExit($timeoutMs)) { $dismProcess.Kill() Write-Log "DISM cleanup timed out after 15 minutes" -Level WARNING $script:Stats.WarningsCount++ return } switch ($dismProcess.ExitCode) { 0 { Write-Log "DISM cleanup completed successfully" -Level SUCCESS } 87 { Write-Log "DISM cleanup not needed" -Level INFO } default { Write-Log "DISM completed with code: $($dismProcess.ExitCode)" -Level WARNING } } } catch { Write-Log "DISM error: $_" -Level WARNING $script:Stats.WarningsCount++ } } function Invoke-StorageSense { <# .SYNOPSIS Runs Storage Sense cleanup #> Write-Log "Storage Sense" -Level SECTION if ($ReportOnly) { Write-Log "Would run: Storage Sense" -Level DETAIL return } # Try Storage Sense first (Windows 11) # Use Get-ScheduledTask for language-independent status checking $ssTaskPath = "\Microsoft\Windows\DiskCleanup\" $ssTaskName = "StorageSense" $task = Get-ScheduledTask -TaskPath $ssTaskPath -TaskName $ssTaskName -ErrorAction SilentlyContinue if ($task) { Write-Log "Running Storage Sense..." -Level INFO # Record time before running to compare with LastRunTime $startTime = Get-Date Start-ScheduledTask -TaskPath $ssTaskPath -TaskName $ssTaskName -ErrorAction SilentlyContinue # Wait for task to complete with timeout $timeout = 120 # 2 minutes max $elapsed = 0 $checkInterval = 5 $wasRunning = $false while ($elapsed -lt $timeout) { Start-Sleep -Seconds $checkInterval $elapsed += $checkInterval # Get current task state (language-independent: Ready, Running, Disabled) $task = Get-ScheduledTask -TaskPath $ssTaskPath -TaskName $ssTaskName -ErrorAction SilentlyContinue if ($task) { $state = $task.State if ($state -eq 'Running') { $wasRunning = $true } elseif ($wasRunning -and $state -eq 'Ready') { # Task was running and now finished Write-Log "Storage Sense completed" -Level SUCCESS break } elseif (-not $wasRunning -and $elapsed -ge 10) { # Task didn't start running within 10 seconds - check LastRunTime $taskInfo = Get-ScheduledTaskInfo -TaskPath $ssTaskPath -TaskName $ssTaskName -ErrorAction SilentlyContinue if ($taskInfo -and $taskInfo.LastRunTime -gt $startTime) { Write-Log "Storage Sense completed" -Level SUCCESS break } } } } if ($elapsed -ge $timeout) { Write-Log "Storage Sense timed out after $timeout seconds" -Level WARNING # Force stop the task if still running $task = Get-ScheduledTask -TaskPath $ssTaskPath -TaskName $ssTaskName -ErrorAction SilentlyContinue if ($task -and $task.State -eq 'Running') { Stop-ScheduledTask -TaskPath $ssTaskPath -TaskName $ssTaskName -ErrorAction SilentlyContinue Write-Log "Storage Sense task stopped" -Level INFO } $script:Stats.WarningsCount++ } } else { # Fallback to cleanmgr Write-Log "Storage Sense task not found, using Disk Cleanup..." -Level INFO # Configure cleanup categories $sageset = 9999 $regPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches" $categories = @( "Active Setup Temp Folders", "BranchCache", "Downloaded Program Files", "Internet Cache Files", "Memory Dump Files", "Old ChkDsk Files", "Previous Installations", "Recycle Bin", "Setup Log Files", "System error memory dump files", "System error minidump files", "Temporary Files", "Temporary Setup Files", "Thumbnail Cache", "Update Cleanup", "Upgrade Discarded Files", "User file versions", "Windows Error Reporting Archive Files", "Windows Error Reporting Queue Files", "Windows Upgrade Log Files", "Windows ESD installation files" ) try { # Set StateFlags for cleanup categories foreach ($category in $categories) { $categoryPath = Join-Path $regPath $category if (Test-Path $categoryPath) { Set-ItemProperty -Path $categoryPath -Name "StateFlags$sageset" -Value 2 -Type DWord -Force -ErrorAction SilentlyContinue } } # Run cleanmgr with progress feedback and reasonable timeout $cleanmgr = Start-Process -FilePath "cleanmgr.exe" -ArgumentList "/sagerun:$sageset" ` -WindowStyle Hidden -PassThru $maxWait = 420 # 7 minutes max (was 10 minutes) $elapsed = 0 $checkInterval = 10 while (-not $cleanmgr.HasExited -and $elapsed -lt $maxWait) { Start-Sleep -Seconds $checkInterval $elapsed += $checkInterval # Log progress every minute if ($elapsed % 60 -eq 0) { Write-Log "Disk Cleanup still running... ($elapsed seconds)" -Level INFO } } if (-not $cleanmgr.HasExited) { $cleanmgr | Stop-Process -Force -ErrorAction SilentlyContinue Write-Log "Disk Cleanup timed out after $maxWait seconds" -Level WARNING $script:Stats.WarningsCount++ } else { Write-Log "Disk Cleanup completed" -Level SUCCESS } } finally { # Cleanup: remove StateFlags to avoid leaving traces in registry foreach ($category in $categories) { $categoryPath = Join-Path $regPath $category if (Test-Path $categoryPath) { Remove-ItemProperty -Path $categoryPath -Name "StateFlags$sageset" -Force -ErrorAction SilentlyContinue } } } } } #endregion #region ═══════════════════════════════════════════════════════════════════════ # MAIN EXECUTION #═══════════════════════════════════════════════════════════════════════════════ function Show-Banner { try { Clear-Host } catch { } $banner = @" ╔══════════════════════════════════════════════════════════════════════╗ ║ ║ ║ ██████╗██╗ ███████╗ █████╗ ███╗ ██╗ ║ ║ ██╔════╝██║ ██╔════╝██╔══██╗████╗ ██║ ║ ║ ██║ ██║ █████╗ ███████║██╔██╗ ██║ ║ ║ ██║ ██║ ██╔══╝ ██╔══██║██║╚██╗██║ ║ ║ ╚██████╗███████╗███████╗██║ ██║██║ ╚████║ ║ ║ ╚═════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝ ║ ║ ║ ║ Ultimate Windows 11 Maintenance Script v$($script:Version) ║ ║ ║ ╚══════════════════════════════════════════════════════════════════════╝ "@ Write-Host $banner -ForegroundColor Cyan # System info $os = Get-CimInstance -ClassName Win32_OperatingSystem $osVersion = $os.Caption $osBuild = $os.BuildNumber Write-Host " System: $osVersion (Build $osBuild)" -ForegroundColor DarkGray Write-Host " PowerShell: $($PSVersionTable.PSVersion)" -ForegroundColor DarkGray Write-Host " Started: $(Get-Date -Format 'dd.MM.yyyy HH:mm:ss')" -ForegroundColor DarkGray Write-Host " Log: $script:LogPath" -ForegroundColor DarkGray if ($ReportOnly) { Write-Host "" Write-Host " >>> REPORT MODE - No changes will be made <<<" -ForegroundColor Yellow } Write-Host "" } function Show-FinalStatistics { $elapsed = (Get-Date) - $script:Stats.StartTime $elapsedStr = "{0:D2}:{1:D2}:{2:D2}" -f [int]$elapsed.Hours, $elapsed.Minutes, $elapsed.Seconds # Get disk info $drive = Get-PSDrive -Name $env:SystemDrive.Replace(':', '') $freeSpace = [math]::Round($drive.Free / 1GB, 2) $totalSize = [math]::Round(($drive.Used + $drive.Free) / 1GB, 2) $freePercent = [math]::Round(($drive.Free / ($drive.Used + $drive.Free)) * 100, 1) Write-Progress -Activity "Complete" -Completed # Box dimensions $boxWidth = 70 # Inner width (matches banner) $labelWidth = 18 # Width for label column (e.g., "Updates installed:") # Determine overall status $hasErrors = $script:Stats.ErrorsCount -gt 0 $hasWarnings = $script:Stats.WarningsCount -gt 0 $statusIcon = if ($hasErrors) { "✗" } elseif ($hasWarnings) { "⚠" } else { "✓" } $statusText = if ($hasErrors) { "COMPLETED WITH ERRORS" } elseif ($hasWarnings) { "COMPLETED WITH WARNINGS" } else { "COMPLETED SUCCESSFULLY" } $headerColor = if ($hasErrors) { "Red" } elseif ($hasWarnings) { "Yellow" } else { "Green" } Write-Host "" # Header with Cyan frame, status-colored text $titlePadding = [math]::Max(0, $boxWidth - $statusText.Length) $leftPad = [math]::Floor($titlePadding / 2) $rightPad = $titlePadding - $leftPad Write-Host " ╔$("═" * $boxWidth)╗" -ForegroundColor Cyan Write-Host " ║" -NoNewline -ForegroundColor Cyan Write-Host (" " * $leftPad) -NoNewline Write-Host $statusText -NoNewline -ForegroundColor $headerColor Write-Host (" " * $rightPad) -NoNewline Write-Host "║" -ForegroundColor Cyan Write-Host " ╠$("═" * $boxWidth)╣" -ForegroundColor Cyan # Helper function for consistent line formatting with icons function Write-StatLine { param( [string]$Icon, [string]$Label, [string]$Value, [string]$IconColor = "Cyan", [string]$ValueColor = "Green" ) # $labelWidth is inherited from parent scope (18) # Layout: space(1) + icon(1) + space(1) + label(18) + gap(2) + value(47) = 70 $valueWidth = $boxWidth - $labelWidth - 5 # 5 = icon(1) + spaces(2) + gap(2) $labelPadded = $Label.PadRight($labelWidth) $valuePadded = $Value.PadRight($valueWidth) Write-Host " ║ " -NoNewline -ForegroundColor Cyan Write-Host "$Icon " -NoNewline -ForegroundColor $IconColor Write-Host "$labelPadded " -NoNewline -ForegroundColor White # 2 spaces after label Write-Host $valuePadded -NoNewline -ForegroundColor $ValueColor Write-Host "║" -ForegroundColor Cyan } # Duration Write-StatLine -Icon ">" -Label "Duration:" -Value $elapsedStr -IconColor "DarkGray" -ValueColor "White" # Updates $totalUpdates = $script:Stats.WindowsUpdatesCount + $script:Stats.AppUpdatesCount if ($totalUpdates -gt 0) { $updatesStr = "Windows: $($script:Stats.WindowsUpdatesCount), Apps: $($script:Stats.AppUpdatesCount)" Write-StatLine -Icon "↑" -Label "Updates installed:" -Value $updatesStr -IconColor "Green" -ValueColor "Green" } # Space freed (highlight if significant) $freedStr = Format-FileSize $script:Stats.TotalFreedBytes $freedColor = if ($script:Stats.TotalFreedBytes -gt 1GB) { "Green" } elseif ($script:Stats.TotalFreedBytes -gt 100MB) { "Yellow" } else { "White" } Write-StatLine -Icon ">" -Label "Space freed:" -Value $freedStr -IconColor $freedColor -ValueColor $freedColor # Freed by category (if any) if ($script:Stats.FreedByCategory.Count -gt 0) { Write-Host " ╟$("─" * $boxWidth)╢" -ForegroundColor Cyan foreach ($cat in ($script:Stats.FreedByCategory.GetEnumerator() | Sort-Object -Property Value -Descending | Select-Object -First 5)) { if ($cat.Value -gt 0) { # Right-align category name so colon aligns with "Updates installed:" $catLabel = "$($cat.Key):".PadLeft($labelWidth) $catValue = Format-FileSize $cat.Value Write-StatLine -Icon " " -Label $catLabel -Value $catValue -ValueColor "DarkGray" } } } Write-Host " ╠$("═" * $boxWidth)╣" -ForegroundColor Cyan # Disk space $diskStr = "$freeSpace GB / $totalSize GB ($freePercent% free)" $diskColor = if ($freePercent -lt 10) { "Red" } elseif ($freePercent -lt 20) { "Yellow" } else { "White" } Write-StatLine -Icon ">" -Label "Disk space:" -Value $diskStr -IconColor $diskColor -ValueColor $diskColor # Warnings/Errors (if any) if ($hasWarnings -or $hasErrors) { $issueStr = "$($script:Stats.WarningsCount) warnings, $($script:Stats.ErrorsCount) errors" $issueIcon = if ($hasErrors) { "✗" } else { "⚠" } $issueColor = if ($hasErrors) { "Red" } else { "Yellow" } Write-StatLine -Icon $issueIcon -Label "Issues:" -Value $issueStr -IconColor $issueColor -ValueColor $issueColor } Write-Host " ╚$("═" * $boxWidth)╝" -ForegroundColor Cyan # Reboot notification if ($script:Stats.RebootRequired) { Write-Host "" Write-Host " ⚠ " -NoNewline -ForegroundColor Yellow Write-Host "Reboot required to complete Windows updates!" -ForegroundColor Yellow if (Test-InteractiveConsole) { Write-Host "" Write-Host " Reboot now? (y/N): " -NoNewline -ForegroundColor Yellow $response = Read-Host if ($response -match "^[YyДд]") { Write-Host " Rebooting in 10 seconds... Press Ctrl+C to cancel" -ForegroundColor Yellow Start-Sleep -Seconds 10 Restart-Computer -Force } else { Write-Host " Remember to reboot later!" -ForegroundColor Yellow } } else { Write-Host " Please reboot manually to complete updates." -ForegroundColor Yellow } } Write-Host "" Write-Host " Log: $script:LogPath" -ForegroundColor DarkGray Write-Host "" # Wait for keypress before closing (no timeout - window stays open) if (Test-InteractiveConsole) { Write-Host " Press any key to exit..." -ForegroundColor DarkGray # Clear keyboard buffer first while ([Console]::KeyAvailable) { [Console]::ReadKey($true) | Out-Null } # Wait indefinitely for keypress [Console]::ReadKey($true) | Out-Null } else { Write-Host " Non-interactive mode - exiting automatically." -ForegroundColor DarkGray } } function Start-WinClean { # Initialize log "WinClean v$($script:Version) - Started at $(Get-Date)" | Out-File -FilePath $script:LogPath -Encoding utf8 "=" * 70 | Out-File -FilePath $script:LogPath -Append -Encoding utf8 # Enable TLS 1.2 for all HTTPS connections (required by PowerShell Gallery, NuGet, etc.) # This must be set before any network operations [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # Calculate TotalSteps dynamically based on skip flags $script:Stats.TotalSteps = 0 if (-not $SkipUpdates) { $script:Stats.TotalSteps += 2 } # Windows Update + App Updates if (-not $SkipCleanup) { $script:Stats.TotalSteps += 2 } # System Cleanup + Deep Cleanup if (-not $SkipDevCleanup) { $script:Stats.TotalSteps += 1 } # Developer Caches if (-not $SkipDockerCleanup) { $script:Stats.TotalSteps += 1 } # Docker/WSL if (-not $SkipVSCleanup) { $script:Stats.TotalSteps += 1 } # Visual Studio # Ensure at least 1 step to avoid division by zero if ($script:Stats.TotalSteps -eq 0) { $script:Stats.TotalSteps = 1 } Show-Banner # Check for pending reboot before starting $pendingReboot = Test-PendingReboot if ($pendingReboot.RebootRequired) { Write-Host "" Write-Host " " -NoNewline Write-Host "WARNING: " -NoNewline -ForegroundColor Red Write-Host "Pending reboot detected!" -ForegroundColor Yellow Write-Host " Reasons: $($pendingReboot.Reasons -join ', ')" -ForegroundColor DarkYellow Write-Host "" Write-Host " It is recommended to reboot before running maintenance." -ForegroundColor Gray # Check if interactive console available (fixed in v2.1) if (Test-InteractiveConsole) { Write-Host " Continue anyway? (y/N): " -NoNewline -ForegroundColor Yellow $response = Read-Host if ($response -notmatch "^[YyДд]") { Write-Host "" Write-Host " Operation cancelled. Please reboot and run again." -ForegroundColor Yellow Write-Host "" return } } else { Write-Host " Non-interactive mode - continuing despite pending reboot." -ForegroundColor Yellow } Write-Host "" } # Check for script updates $updateInfo = Test-ScriptUpdate if ($updateInfo) { Invoke-ScriptUpdate -UpdateInfo $updateInfo } try { # Phase 1: Preparation $null = New-SystemRestorePoint -Description "WinClean $(Get-Date -Format 'yyyy-MM-dd HH:mm')" # Phase 2: Updates if (-not $SkipUpdates) { Update-WindowsSystem Update-Applications } # Phase 3: Cleanup if (-not $SkipCleanup) { Write-Log "SYSTEM CLEANUP" -Level TITLE Update-Progress -Activity "System Cleanup" -Status "Cleaning temporary files..." Clear-TempFiles Clear-BrowserCaches Clear-WindowsUpdateCache Clear-WinCleanRecycleBin Clear-SystemCaches Clear-EventLogs Clear-DNSCache Clear-PrivacyTraces } # Phase 4: Developer Cleanup Clear-DeveloperCaches # Phase 5: Docker/WSL Cleanup Clear-DockerWSL # Phase 6: Visual Studio Cleanup Clear-VisualStudio # Phase 7: System Cleanup if (-not $SkipCleanup) { Write-Log "DEEP SYSTEM CLEANUP" -Level TITLE Update-Progress -Activity "Deep Cleanup" -Status "Running system cleanup..." Invoke-DISMCleanup Invoke-StorageSense Clear-WindowsOld } # Phase 8: Telemetry Configuration (if requested) if ($DisableTelemetry) { Set-WindowsTelemetry -Disable } } catch { Write-Log "Critical error: $_" -Level ERROR $script:Stats.ErrorsCount++ } finally { Show-FinalStatistics } } # Entry point if ($MyInvocation.InvocationName -ne '.') { Start-WinClean } #endregion