diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 139903a..5e8550a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,13 +15,18 @@ jobs: - name: Syntax parse check shell: pwsh run: | - $errors = $null - [System.Management.Automation.Language.Parser]::ParseFile( - (Resolve-Path ./Cleanup-Windows-Senior.ps1), [ref]$null, [ref]$errors) | Out-Null - if ($errors) { - $errors | ForEach-Object { Write-Host "::error::L$($_.Extent.StartLineNumber): $($_.Message)" } - exit 1 + $files = './Cleanup-Windows-Senior.ps1','./Optimize-Windows-Senior.ps1','./Repair-Windows-Senior.ps1','./WinSenior.ps1' + $failed = $false + foreach ($f in $files) { + $errors = $null + [System.Management.Automation.Language.Parser]::ParseFile( + (Resolve-Path $f), [ref]$null, [ref]$errors) | Out-Null + if ($errors) { + $errors | ForEach-Object { Write-Host "::error::$f L$($_.Extent.StartLineNumber): $($_.Message)" } + $failed = $true + } } + if ($failed) { exit 1 } Write-Host 'Parse OK' - name: Install Pester & PSScriptAnalyzer @@ -34,7 +39,8 @@ jobs: - name: PSScriptAnalyzer (fail on errors only) shell: pwsh run: | - $r = Invoke-ScriptAnalyzer -Path ./Cleanup-Windows-Senior.ps1 -Severity Error + $files = './Cleanup-Windows-Senior.ps1','./Optimize-Windows-Senior.ps1','./Repair-Windows-Senior.ps1','./WinSenior.ps1' + $r = $files | ForEach-Object { Invoke-ScriptAnalyzer -Path $_ -Severity Error } if ($r) { $r | Format-Table -AutoSize | Out-String | Write-Host; Write-Error 'ScriptAnalyzer found error-level issues'; exit 1 } Write-Host 'No analyzer errors' diff --git a/Optimize-Windows-Senior.ps1 b/Optimize-Windows-Senior.ps1 new file mode 100644 index 0000000..2c781e4 --- /dev/null +++ b/Optimize-Windows-Senior.ps1 @@ -0,0 +1,877 @@ +<# +.SYNOPSIS + Windows optimization engine - registry-driven tweaks with full per-tweak undo. + +.DESCRIPTION + A declarative, single-file Windows 10/11 optimization tool. Every tweak is one entry + in a tweak registry; a small engine resolves which tweaks to run, snapshots the prior + state into a backup manifest, and applies through PowerShell's ShouldProcess so -WhatIf + is real. -Undo reverts everything from the newest (or a named) manifest. + + Four areas: Performance, Privacy, Debloat, Network. Aggressive but reversible: Safe + + Moderate + Aggressive tiers are selectable by default, but debatable tweaks ship off + until you turn them on. A real System Restore point is created first unless -NoRestorePoint. + + It never disables Defender real-time protection, never breaks Windows Update or the + network stack wholesale, and never removes Edge or the Store. + +.NOTES + Author : denfry (https://github.com/denfry/WindowsCleaner) + Version : 6.0.0 + Requires: PowerShell 5.1+ (Windows). Administrator rights. + +.EXAMPLE + .\Optimize-Windows-Senior.ps1 -WhatIf + Preview every tweak that would be applied, change nothing. + +.EXAMPLE + .\Optimize-Windows-Senior.ps1 -Area Privacy,Performance + Apply only the privacy and performance tweaks (default-on set). + +.EXAMPLE + .\Optimize-Windows-Senior.ps1 -Undo + Revert the most recent optimization run from its backup manifest. +#> + +#Requires -Version 5.1 + +[CmdletBinding(SupportsShouldProcess)] +param( + # Limit to these areas: Performance, Privacy, Debloat, Network + [string[]]$Area, + + # Force these tweak ids on (overrides default-off, area and risk cap) + [string[]]$Include, + + # Force these tweak ids off (wins over everything) + [string[]]$Exclude, + + # Also apply the irreversible Dangerous tier + [switch]$IncludeDangerous, + + # Cap at Safe + Moderate (skip the Aggressive tier) + [Alias('SafeMode')] + [switch]$Conservative, + + # Revert a previous run from its backup manifest (newest unless -BackupManifest given) + [switch]$Undo, + + # Specific backup manifest to undo (default: newest in -BackupDir) + [string]$BackupManifest, + + # Preview alias for -WhatIf + [Alias('dr')] + [switch]$DryRun, + + # Non-interactive: no prompts, used for automation + [Alias('Force','f')] + [switch]$Unattended, + + # Skip the real Checkpoint-Computer restore point that is otherwise created first + [Alias('nrp')] + [switch]$NoRestorePoint, + + # Where per-tweak backup manifests are written + [string]$BackupDir = "$env:ProgramData\WinSenior\backups", + + [string]$LogPath = "$env:TEMP\WindowsOptimize.log", + + # Optional path for a machine-readable JSON report + [string]$ReportPath, + + # Print the tweak registry and exit + [switch]$ListTweaks, + + [switch]$Help +) + +# ===================================================================== +# SCRIPT STATE +# ===================================================================== +$script:StartTime = Get-Date +$script:Stats = New-Object System.Collections.Generic.List[object] +$script:Snapshots = New-Object System.Collections.Generic.List[object] +$script:Applied = 0 +$script:Skipped = 0 +$script:Errors = 0 +$script:RestorePointMade = $false + +if ($DryRun) { $WhatIfPreference = $true } + +# ===================================================================== +# LOGGING +# ===================================================================== +function Write-OptLog { + param( + [string]$Message, + [ValidateSet('Info','Success','Warning','Error','Debug','Step','WhatIf','Safety')] + [string]$Level = 'Info' + ) + $tag = switch ($Level) { + 'Success' { '[+]' } 'Warning' { '[!]' } 'Error' { '[x]' } + 'Step' { '==>' } 'WhatIf' { '[~]' } 'Safety' { '[#]' } + 'Debug' { ' ' } default { '[i]' } + } + $color = switch ($Level) { + 'Success' { 'Green' } 'Warning' { 'Yellow' } 'Error' { 'Red' } + 'Step' { 'Cyan' } 'WhatIf' { 'Cyan' } 'Safety' { 'Magenta' } + 'Debug' { 'DarkGray' } default { 'Gray' } + } + if ($Level -ne 'Debug' -or $VerbosePreference -ne 'SilentlyContinue') { + Write-Host "$tag $Message" -ForegroundColor $color + } + $stamp = "[{0:yyyy-MM-dd HH:mm:ss}] [{1}] {2}" -f (Get-Date), $Level, $Message + try { Add-Content -Path $LogPath -Value $stamp -ErrorAction SilentlyContinue -WhatIf:$false } catch { } +} + +# ===================================================================== +# UTILITIES +# ===================================================================== +function Format-FileSize { + param([long]$Size) + if ($Size -ge 1TB) { '{0:N2} TB' -f ($Size / 1TB) } + elseif ($Size -ge 1GB) { '{0:N2} GB' -f ($Size / 1GB) } + elseif ($Size -ge 1MB) { '{0:N2} MB' -f ($Size / 1MB) } + elseif ($Size -ge 1KB) { '{0:N2} KB' -f ($Size / 1KB) } + else { "$Size B" } +} + +function Test-WhatIfMode { [bool]$WhatIfPreference } + +function Test-AdminPrivileges { + try { + $id = [Security.Principal.WindowsIdentity]::GetCurrent() + (New-Object Security.Principal.WindowsPrincipal($id)).IsInRole( + [Security.Principal.WindowsBuiltInRole]::Administrator) + } catch { $false } +} + +function New-OptRestorePoint { + if (Test-WhatIfMode) { + Write-OptLog '[WhatIf] would create a System Restore point' 'WhatIf'; return $true + } + Write-OptLog 'Creating System Restore point...' 'Safety' + try { + $rk = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SystemRestore' + New-ItemProperty -Path $rk -Name 'SystemRestorePointCreationFrequency' ` + -Value 0 -PropertyType DWord -Force -ErrorAction SilentlyContinue | Out-Null + Enable-ComputerRestore -Drive "$env:SystemDrive\" -ErrorAction SilentlyContinue + Checkpoint-Computer -Description "Before Windows Optimize $(Get-Date -Format 'yyyy-MM-dd HH:mm')" ` + -RestorePointType 'MODIFY_SETTINGS' -ErrorAction Stop + Write-OptLog 'System Restore point created' 'Success' + $script:RestorePointMade = $true + return $true + } + catch { + Write-OptLog "Restore point not created: $($_.Exception.Message)" 'Warning' + Write-OptLog 'Continuing without a restore point (System Protection may be off).' 'Warning' + return $false + } +} + +# ===================================================================== +# REGISTRY HELPERS (used by Registry-type tweaks; self-contained for undo) +# ===================================================================== +function Get-RegValueSnapshot { + param([string]$Path, [string]$Name) + $snap = [ordered]@{ Name = $Name; Existed = $false; Value = $null; Kind = $null } + if (Test-Path -LiteralPath $Path) { + $item = Get-Item -LiteralPath $Path -ErrorAction SilentlyContinue + if ($item -and ($item.GetValueNames() -contains $Name)) { + $snap.Existed = $true + $snap.Value = $item.GetValue($Name) + try { $snap.Kind = [string]$item.GetValueKind($Name) } catch { $snap.Kind = $null } + } + } + [pscustomobject]$snap +} + +function Set-RegValue { + param([string]$Path, [string]$Name, [string]$Kind, $Value) + if (-not (Test-Path -LiteralPath $Path)) { + New-Item -Path $Path -Force -ErrorAction Stop | Out-Null + } + New-ItemProperty -Path $Path -Name $Name -PropertyType $Kind -Value $Value ` + -Force -ErrorAction Stop | Out-Null +} + +# Restore a single registry value from a snapshot object (used by -Undo). +function Restore-RegValue { + param([string]$Path, [object]$Snap) + if ($Snap.Existed) { + $kind = if ($Snap.Kind) { $Snap.Kind } else { 'String' } + Set-RegValue -Path $Path -Name $Snap.Name -Kind $kind -Value $Snap.Value + } + elseif (Test-Path -LiteralPath $Path) { + Remove-ItemProperty -Path $Path -Name $Snap.Name -Force -ErrorAction SilentlyContinue + } +} + +# ===================================================================== +# TWEAK REGISTRY (the single source of truth) +# ===================================================================== +function New-RegTweak { + param( + [string]$Id, [string]$Name, [string]$Area, [string]$Risk, + [bool]$DefaultOn = $true, [string]$Path, [object[]]$Values, [string]$Explain + ) + [pscustomobject]@{ + Id = $Id; Name = $Name; Area = $Area; Risk = $Risk; DefaultOn = $DefaultOn + Type = 'Registry'; Explain = $Explain + Spec = @{ Path = $Path; Values = $Values } + } +} +function New-SvcTweak { + param( + [string]$Id, [string]$Name, [string]$Area, [string]$Risk, + [bool]$DefaultOn = $true, [string]$Service, [string]$Startup = 'Disabled', + [bool]$StopNow = $true, [string]$Explain + ) + [pscustomobject]@{ + Id = $Id; Name = $Name; Area = $Area; Risk = $Risk; DefaultOn = $DefaultOn + Type = 'Service'; Explain = $Explain + Spec = @{ Service = $Service; Startup = $Startup; StopNow = $StopNow } + } +} +function New-TaskTweak { + param( + [string]$Id, [string]$Name, [string]$Area, [string]$Risk, + [bool]$DefaultOn = $true, [object[]]$Tasks, [string]$Explain + ) + [pscustomobject]@{ + Id = $Id; Name = $Name; Area = $Area; Risk = $Risk; DefaultOn = $DefaultOn + Type = 'ScheduledTask'; Explain = $Explain + Spec = @{ Tasks = $Tasks } + } +} +function New-CustomTweak { + param( + [string]$Id, [string]$Name, [string]$Area, [string]$Risk, + [bool]$DefaultOn = $true, + [scriptblock]$Test, [scriptblock]$Backup, [scriptblock]$Apply, [scriptblock]$Undo, + [string]$Explain + ) + [pscustomobject]@{ + Id = $Id; Name = $Name; Area = $Area; Risk = $Risk; DefaultOn = $DefaultOn + Type = 'Custom'; Explain = $Explain + Spec = @{ Test = $Test; Backup = $Backup; Apply = $Apply; Undo = $Undo } + } +} + +# Convenience for a single name/kind/value registry pair. +function RegVal { param([string]$Name, [string]$Kind, $Value) + [pscustomobject]@{ Name = $Name; Kind = $Kind; Value = $Value } } + +function Get-OptimizationTweakRegistry { + @( + # ============================================================= + # PERFORMANCE + # ============================================================= + New-RegTweak perf-visualfx 'Visual effects: best performance' Performance Safe ` + -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects' ` + -Values @((RegVal 'VisualFXSetting' DWord 2)) ` + -Explain 'Disables animations/shadows for snappier UI (Performance Options = best performance).' + New-RegTweak perf-menudelay 'Zero menu show delay' Performance Safe ` + -Path 'HKCU:\Control Panel\Desktop' ` + -Values @((RegVal 'MenuShowDelay' String '0')) ` + -Explain 'Menus open instantly instead of after the default 400 ms.' + New-RegTweak perf-startupdelay 'Remove startup app delay' Performance Moderate ` + -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Serialize' ` + -Values @((RegVal 'StartupDelayInMSec' DWord 0)) ` + -Explain 'Startup programs launch without the artificial ~10 s delay.' + New-RegTweak perf-bgapps 'Disable background apps' Performance Moderate ` + -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\BackgroundAccessApplications' ` + -Values @((RegVal 'GlobalUserDisabled' DWord 1)) ` + -Explain 'Stops UWP apps from running and updating in the background.' + New-CustomTweak perf-power-high 'Power plan: High Performance' Performance Safe -DefaultOn $true ` + -Explain 'Switches the active power plan to High Performance (no CPU down-clocking on idle).' ` + -Test { $a = (& powercfg /getactivescheme) -join ' '; [bool]($a -match '8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c') } ` + -Backup { $a = (& powercfg /getactivescheme) -join ' '; $g = if ($a -match '([0-9a-f-]{36})') { $Matches[1] } else { $null }; @{ PreviousGuid = $g } } ` + -Apply { & powercfg /setactive '8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c' 2>$null; if ($LASTEXITCODE -ne 0) { & powercfg /setactive SCHEME_MIN 2>$null } } ` + -Undo { param($s) if ($s.PreviousGuid) { & powercfg /setactive $s.PreviousGuid 2>$null } } + New-CustomTweak perf-power-ultimate 'Power plan: Ultimate Performance' Performance Aggressive -DefaultOn $false ` + -Explain 'Creates and activates the hidden Ultimate Performance plan (desktops/workstations).' ` + -Test { $false } ` + -Backup { $a = (& powercfg /getactivescheme) -join ' '; $g = if ($a -match '([0-9a-f-]{36})') { $Matches[1] } else { $null }; @{ PreviousGuid = $g } } ` + -Apply { & powercfg /duplicatescheme e9a42b02-d5df-448d-aa00-03f14749eb61 2>$null; & powercfg /setactive e9a42b02-d5df-448d-aa00-03f14749eb61 2>$null } ` + -Undo { param($s) if ($s.PreviousGuid) { & powercfg /setactive $s.PreviousGuid 2>$null } } + New-SvcTweak perf-sysmain 'Disable SysMain (Superfetch)' Performance Aggressive -DefaultOn $false ` + -Service 'SysMain' -Startup 'Disabled' ` + -Explain 'Frees RAM/disk activity. Helpful on SSDs; can slow app launches on HDDs. Off by default.' + New-SvcTweak perf-wsearch 'Disable Windows Search indexing' Performance Aggressive -DefaultOn $false ` + -Service 'WSearch' -Startup 'Disabled' ` + -Explain 'Stops the indexer (less disk/CPU) but makes Start/Explorer search slower. Off by default.' + New-CustomTweak perf-hibernate 'Disable hibernation (remove hiberfil.sys)' Performance Aggressive -DefaultOn $false ` + -Explain 'Reclaims several GB of hiberfil.sys and disables Fast Startup. Off by default.' ` + -Test { (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Power' -Name HibernateEnabled -ErrorAction SilentlyContinue).HibernateEnabled -eq 0 } ` + -Backup { @{ Was = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Power' -Name HibernateEnabled -ErrorAction SilentlyContinue).HibernateEnabled } } ` + -Apply { & powercfg /hibernate off 2>$null } ` + -Undo { param($s) if ($s.Was -ne 0) { & powercfg /hibernate on 2>$null } } + + # ============================================================= + # PRIVACY / TELEMETRY + # ============================================================= + New-RegTweak priv-telemetry 'Minimize telemetry (policy)' Privacy Moderate ` + -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection' ` + -Values @((RegVal 'AllowTelemetry' DWord 0), (RegVal 'DoNotShowFeedbackNotifications' DWord 1)) ` + -Explain 'Sets diagnostic data to the lowest level the edition allows and hides feedback prompts.' + New-RegTweak priv-adid 'Disable advertising ID' Privacy Safe ` + -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\AdvertisingInfo' ` + -Values @((RegVal 'Enabled' DWord 0)) ` + -Explain 'Stops apps from using a per-user advertising identifier.' + New-RegTweak priv-consumer 'Disable consumer features / auto-installed apps' Privacy Safe ` + -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\CloudContent' ` + -Values @((RegVal 'DisableWindowsConsumerFeatures' DWord 1), (RegVal 'DisableSoftLanding' DWord 1)) ` + -Explain 'Prevents Windows from silently installing promoted third-party apps.' + New-RegTweak priv-tips 'Disable tips, suggestions & spotlight' Privacy Safe ` + -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' ` + -Values @( + (RegVal 'SystemPaneSuggestionsEnabled' DWord 0), + (RegVal 'SoftLandingEnabled' DWord 0), + (RegVal 'SubscribedContent-338389Enabled' DWord 0), + (RegVal 'SubscribedContent-310093Enabled' DWord 0), + (RegVal 'RotatingLockScreenOverlayEnabled' DWord 0)) ` + -Explain 'Turns off Windows tips, lock-screen spotlight facts and Settings suggestions.' + New-RegTweak priv-activity 'Disable activity feed / Timeline' Privacy Safe ` + -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\System' ` + -Values @( + (RegVal 'EnableActivityFeed' DWord 0), + (RegVal 'PublishUserActivities' DWord 0), + (RegVal 'UploadUserActivities' DWord 0)) ` + -Explain 'Stops Windows from collecting and uploading the activity history / Timeline.' + New-RegTweak priv-websearch 'Disable web search in Start' Privacy Safe ` + -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Search' ` + -Values @((RegVal 'BingSearchEnabled' DWord 0), (RegVal 'CortanaConsent' DWord 0)) ` + -Explain 'Removes Bing web results and Cortana suggestions from the Start-menu search box.' + New-RegTweak priv-cortana 'Disable Cortana (policy)' Privacy Moderate ` + -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Search' ` + -Values @((RegVal 'AllowCortana' DWord 0)) ` + -Explain 'Disables the Cortana assistant via Group Policy.' + New-SvcTweak priv-diagtrack 'Disable Connected User Experiences (DiagTrack)' Privacy Moderate ` + -Service 'DiagTrack' -Startup 'Disabled' ` + -Explain 'Stops the main telemetry service that uploads diagnostic data.' + New-SvcTweak priv-dmwappush 'Disable WAP Push message service' Privacy Moderate ` + -Service 'dmwappushservice' -Startup 'Disabled' ` + -Explain 'Disables a device-management channel used for telemetry routing.' + New-TaskTweak priv-telemetry-tasks 'Disable CEIP & telemetry scheduled tasks' Privacy Moderate ` + -Tasks @( + @{ Path = '\Microsoft\Windows\Customer Experience Improvement Program\'; Name = 'Consolidator' }, + @{ Path = '\Microsoft\Windows\Customer Experience Improvement Program\'; Name = 'UsbCeip' }, + @{ Path = '\Microsoft\Windows\Application Experience\'; Name = 'Microsoft Compatibility Appraiser' }, + @{ Path = '\Microsoft\Windows\Application Experience\'; Name = 'ProgramDataUpdater' }, + @{ Path = '\Microsoft\Windows\Feedback\Siuf\'; Name = 'DmClient' }, + @{ Path = '\Microsoft\Windows\Feedback\Siuf\'; Name = 'DmClientOnScenarioDownload' }) ` + -Explain 'Disables the recurring tasks that collect and send usage/compatibility data.' + + # ============================================================= + # DEBLOAT (UWP apps) + # ============================================================= + New-CustomTweak debloat-junk 'Remove preinstalled junk apps' Debloat Aggressive -DefaultOn $true ` + -Explain 'Removes obvious bloat (King games, Solitaire, 3D Viewer, Clipchamp, Get Help, Maps, etc.) for all users.' ` + -Test { $false } ` + -Backup { + $pat = @('king.com*','*CandyCrush*','*BubbleWitch*','*Microsoft.3DBuilder*','*Microsoft.Microsoft3DViewer*', + '*Microsoft.MicrosoftSolitaireCollection*','*Microsoft.MixedReality.Portal*','*Microsoft.WindowsFeedbackHub*', + '*Microsoft.GetHelp*','*Microsoft.Getstarted*','*Microsoft.WindowsMaps*','*Microsoft.BingNews*', + '*Microsoft.BingWeather*','*Microsoft.People*','*Clipchamp*','*Microsoft.Todos*','*Disney*','*SpotifyAB*') + $found = foreach ($p in $pat) { Get-AppxPackage -AllUsers -Name $p -ErrorAction SilentlyContinue | Select-Object -Expand Name } + @{ Patterns = $pat; Found = @($found | Sort-Object -Unique) } + } ` + -Apply { + $pat = @('king.com*','*CandyCrush*','*BubbleWitch*','*Microsoft.3DBuilder*','*Microsoft.Microsoft3DViewer*', + '*Microsoft.MicrosoftSolitaireCollection*','*Microsoft.MixedReality.Portal*','*Microsoft.WindowsFeedbackHub*', + '*Microsoft.GetHelp*','*Microsoft.Getstarted*','*Microsoft.WindowsMaps*','*Microsoft.BingNews*', + '*Microsoft.BingWeather*','*Microsoft.People*','*Clipchamp*','*Microsoft.Todos*','*Disney*','*SpotifyAB*') + foreach ($p in $pat) { + Get-AppxPackage -AllUsers -Name $p -ErrorAction SilentlyContinue | Remove-AppxPackage -AllUsers -ErrorAction SilentlyContinue + Get-AppxProvisionedPackage -Online -ErrorAction SilentlyContinue | + Where-Object { $_.DisplayName -like $p } | + ForEach-Object { Remove-AppxProvisionedPackage -Online -PackageName $_.PackageName -ErrorAction SilentlyContinue | Out-Null } + } + } ` + -Undo { param($s) + if ($s.Found) { Write-OptLog ("Removed UWP apps cannot be auto-reinstalled. Reinstall from the Store if needed: {0}" -f ($s.Found -join ', ')) 'Warning' } + } + New-CustomTweak debloat-xbox 'Remove Xbox apps' Debloat Aggressive -DefaultOn $false ` + -Explain 'Removes Xbox app, Game Bar overlay and related packages. Off by default (gamers may want them).' ` + -Test { $false } ` + -Backup { $f = Get-AppxPackage -AllUsers -Name '*Xbox*' -ErrorAction SilentlyContinue | Select-Object -Expand Name; @{ Found = @($f) } } ` + -Apply { Get-AppxPackage -AllUsers -Name '*Xbox*' -ErrorAction SilentlyContinue | Remove-AppxPackage -AllUsers -ErrorAction SilentlyContinue } ` + -Undo { param($s) if ($s.Found) { Write-OptLog ("Reinstall from the Store if needed: {0}" -f ($s.Found -join ', ')) 'Warning' } } + New-CustomTweak debloat-comms 'Remove Mail/Calendar, Skype, Phone Link' Debloat Aggressive -DefaultOn $false ` + -Explain 'Removes the communications apps bundle. Off by default (some people use Mail/Calendar).' ` + -Test { $false } ` + -Backup { + $pat = @('*Microsoft.windowscommunicationsapps*','*Microsoft.SkypeApp*','*Microsoft.YourPhone*') + $f = foreach ($p in $pat) { Get-AppxPackage -AllUsers -Name $p -ErrorAction SilentlyContinue | Select-Object -Expand Name } + @{ Patterns = $pat; Found = @($f | Sort-Object -Unique) } + } ` + -Apply { + foreach ($p in @('*Microsoft.windowscommunicationsapps*','*Microsoft.SkypeApp*','*Microsoft.YourPhone*')) { + Get-AppxPackage -AllUsers -Name $p -ErrorAction SilentlyContinue | Remove-AppxPackage -AllUsers -ErrorAction SilentlyContinue + } + } ` + -Undo { param($s) if ($s.Found) { Write-OptLog ("Reinstall from the Store if needed: {0}" -f ($s.Found -join ', ')) 'Warning' } } + New-RegTweak debloat-start-ads 'Disable Start-menu app suggestions' Debloat Safe ` + -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' ` + -Values @( + (RegVal 'SilentInstalledAppsEnabled' DWord 0), + (RegVal 'PreInstalledAppsEnabled' DWord 0), + (RegVal 'OemPreInstalledAppsEnabled' DWord 0), + (RegVal 'SubscribedContent-338388Enabled' DWord 0)) ` + -Explain 'Stops the Start menu from showing suggested/promoted apps.' + + # ============================================================= + # NETWORK / GAMES + # ============================================================= + New-RegTweak net-gamedvr 'Disable GameDVR / background recording' Network Safe ` + -Path 'HKCU:\System\GameConfigStore' ` + -Values @((RegVal 'GameDVR_Enabled' DWord 0), (RegVal 'GameDVR_FSEBehaviorMode' DWord 2)) ` + -Explain 'Disables the background game recorder that can cost frames and CPU.' + New-RegTweak net-gamedvr-policy 'Disable GameDVR (policy)' Network Safe ` + -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\GameDVR' ` + -Values @((RegVal 'AllowGameDVR' DWord 0)) ` + -Explain 'Enforces GameDVR off machine-wide via policy.' + New-RegTweak net-gamemode 'Enable Game Mode' Network Safe ` + -Path 'HKCU:\Software\Microsoft\GameBar' ` + -Values @((RegVal 'AutoGameModeEnabled' DWord 1), (RegVal 'AllowAutoGameMode' DWord 1)) ` + -Explain 'Prioritizes the foreground game for CPU/GPU scheduling.' + New-RegTweak net-throttling 'Disable network throttling / multimedia reservation' Network Moderate ` + -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Multimedia\SystemProfile' ` + -Values @((RegVal 'NetworkThrottlingIndex' DWord 4294967295), (RegVal 'SystemResponsiveness' DWord 0)) ` + -Explain 'Lifts the 10-packet/ms network throttle and the 20% CPU multimedia reservation (better for gaming/streaming).' + New-SvcTweak net-ndu 'Disable Network Data Usage monitor (NDU)' Network Aggressive -DefaultOn $false ` + -Service 'Ndu' -Startup 'Disabled' ` + -Explain 'Stops the NDU driver that can cause high memory use. Off by default; removes per-app data usage stats.' + New-CustomTweak net-nagle 'Disable Nagle algorithm (lower latency)' Network Aggressive -DefaultOn $false ` + -Explain 'Sets TcpAckFrequency=1 / TCPNoDelay=1 on active interfaces for lower gaming latency. Off by default.' ` + -Test { $false } ` + -Backup { + $root = 'HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces' + $snaps = @() + foreach ($k in (Get-ChildItem $root -ErrorAction SilentlyContinue)) { + $p = $k.PSPath + if ((Get-ItemProperty $p -ErrorAction SilentlyContinue).PSObject.Properties.Name -match 'DhcpIPAddress|IPAddress') { + foreach ($n in 'TcpAckFrequency','TCPNoDelay') { + $cur = (Get-ItemProperty $p -Name $n -ErrorAction SilentlyContinue).$n + $snaps += @{ Path = $p; Name = $n; Existed = ($null -ne $cur); Value = $cur; Kind = 'DWord' } + } + } + } + @{ Values = $snaps } + } ` + -Apply { + $root = 'HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces' + foreach ($k in (Get-ChildItem $root -ErrorAction SilentlyContinue)) { + $p = $k.PSPath + if ((Get-ItemProperty $p -ErrorAction SilentlyContinue).PSObject.Properties.Name -match 'DhcpIPAddress|IPAddress') { + New-ItemProperty $p -Name 'TcpAckFrequency' -PropertyType DWord -Value 1 -Force -ErrorAction SilentlyContinue | Out-Null + New-ItemProperty $p -Name 'TCPNoDelay' -PropertyType DWord -Value 1 -Force -ErrorAction SilentlyContinue | Out-Null + } + } + } ` + -Undo { param($s) + foreach ($v in $s.Values) { + if ($v.Existed) { New-ItemProperty $v.Path -Name $v.Name -PropertyType DWord -Value $v.Value -Force -ErrorAction SilentlyContinue | Out-Null } + else { Remove-ItemProperty $v.Path -Name $v.Name -Force -ErrorAction SilentlyContinue } + } + } + ) +} + +# ===================================================================== +# SELECTION +# ===================================================================== +function Resolve-TweakSelection { + param( + [object[]]$Registry, + [string[]]$Area, [string[]]$Include, [string[]]$Exclude, + [bool]$Conservative, [bool]$IncludeDangerous + ) + $rank = @{ Safe = 0; Moderate = 1; Aggressive = 2; Dangerous = 3 } + $maxRisk = if ($IncludeDangerous) { 3 } elseif ($Conservative) { 1 } else { 2 } + + foreach ($t in $Registry) { + $on = $t.DefaultOn + if ($Area -and ($t.Area -notin $Area)) { $on = $false } + if ($rank[$t.Risk] -gt $maxRisk) { $on = $false } + if (($Include -contains $t.Id) -or ($Include -contains $t.Name)) { $on = $true } + if (($Exclude -contains $t.Id) -or ($Exclude -contains $t.Name)) { $on = $false } + if ($on) { $t } + } +} + +# ===================================================================== +# STATE (is a tweak currently applied?) - used by the menu display +# ===================================================================== +function Test-TweakApplied { + param([object]$Tweak) + try { + switch ($Tweak.Type) { + 'Registry' { + foreach ($v in $Tweak.Spec.Values) { + $snap = Get-RegValueSnapshot -Path $Tweak.Spec.Path -Name $v.Name + if (-not $snap.Existed) { return $false } + if ([string]$snap.Value -ne [string]$v.Value) { return $false } + } + return $true + } + 'Service' { + $svc = Get-Service -Name $Tweak.Spec.Service -ErrorAction SilentlyContinue + if (-not $svc) { return $null } + return ([string]$svc.StartType -eq $Tweak.Spec.Startup) + } + 'ScheduledTask' { + if (-not (Get-Command Get-ScheduledTask -ErrorAction SilentlyContinue)) { return $null } + foreach ($t in $Tweak.Spec.Tasks) { + $st = Get-ScheduledTask -TaskPath $t.Path -TaskName $t.Name -ErrorAction SilentlyContinue + if ($st -and $st.State -ne 'Disabled') { return $false } + } + return $true + } + 'Custom' { + if ($Tweak.Spec.Test) { return [bool](& $Tweak.Spec.Test) } + return $null + } + } + } catch { return $null } + $null +} + +# ===================================================================== +# APPLY (snapshot prior state, then change via ShouldProcess) +# ===================================================================== +function Get-TweakSnapshot { + param([object]$Tweak) + switch ($Tweak.Type) { + 'Registry' { + $vals = foreach ($v in $Tweak.Spec.Values) { Get-RegValueSnapshot -Path $Tweak.Spec.Path -Name $v.Name } + return @{ Path = $Tweak.Spec.Path; Values = @($vals) } + } + 'Service' { + $svc = Get-Service -Name $Tweak.Spec.Service -ErrorAction SilentlyContinue + return @{ Service = $Tweak.Spec.Service + Found = [bool]$svc + StartType = if ($svc) { [string]$svc.StartType } else { $null } + Status = if ($svc) { [string]$svc.Status } else { $null } } + } + 'ScheduledTask' { + $states = @() + if (Get-Command Get-ScheduledTask -ErrorAction SilentlyContinue) { + foreach ($t in $Tweak.Spec.Tasks) { + $st = Get-ScheduledTask -TaskPath $t.Path -TaskName $t.Name -ErrorAction SilentlyContinue + $states += @{ Path = $t.Path; Name = $t.Name; State = if ($st) { [string]$st.State } else { $null } } + } + } + return @{ Tasks = $states } + } + 'Custom' { return [hashtable](& $Tweak.Spec.Backup) } + } + @{} +} + +function Set-TweakState { + param([object]$Tweak, [object]$Snapshot) + switch ($Tweak.Type) { + 'Registry' { + foreach ($v in $Tweak.Spec.Values) { Set-RegValue -Path $Tweak.Spec.Path -Name $v.Name -Kind $v.Kind -Value $v.Value } + } + 'Service' { + Set-Service -Name $Tweak.Spec.Service -StartupType $Tweak.Spec.Startup -ErrorAction Stop + if ($Tweak.Spec.StopNow -and $Snapshot.Status -eq 'Running') { + Stop-Service -Name $Tweak.Spec.Service -Force -ErrorAction SilentlyContinue + } + } + 'ScheduledTask' { + foreach ($t in $Tweak.Spec.Tasks) { + Disable-ScheduledTask -TaskPath $t.Path -TaskName $t.Name -ErrorAction SilentlyContinue | Out-Null + } + } + 'Custom' { & $Tweak.Spec.Apply $Snapshot } + } +} + +function Invoke-Tweak { + [CmdletBinding(SupportsShouldProcess)] + param([object]$Tweak) + + $applied = Test-TweakApplied -Tweak $Tweak + if ($applied -eq $true) { + Write-OptLog "$($Tweak.Name) [already applied]" 'Debug' + $script:Skipped++ + return + } + + $snapshot = Get-TweakSnapshot -Tweak $Tweak + $target = $Tweak.Name + $action = "Apply tweak [$($Tweak.Area)/$($Tweak.Risk)]" + + if ($PSCmdlet.ShouldProcess($target, $action)) { + try { + Set-TweakState -Tweak $Tweak -Snapshot $snapshot + Write-OptLog "$($Tweak.Name)" 'Success' + $script:Applied++ + $script:Snapshots.Add([pscustomobject]@{ Id = $Tweak.Id; Type = $Tweak.Type; Snapshot = $snapshot }) + $script:Stats.Add([pscustomobject]@{ Id = $Tweak.Id; Area = $Tweak.Area; Risk = $Tweak.Risk; Result = 'applied' }) + } + catch { + $script:Errors++ + Write-OptLog " $($Tweak.Name): $($_.Exception.Message)" 'Error' + $script:Stats.Add([pscustomobject]@{ Id = $Tweak.Id; Area = $Tweak.Area; Risk = $Tweak.Risk; Result = 'error' }) + } + } + elseif (Test-WhatIfMode) { + $script:Stats.Add([pscustomobject]@{ Id = $Tweak.Id; Area = $Tweak.Area; Risk = $Tweak.Risk; Result = 'would-apply' }) + } +} + +# ===================================================================== +# BACKUP MANIFEST +# ===================================================================== +function Write-BackupManifest { + if (Test-WhatIfMode -or $script:Snapshots.Count -eq 0) { return $null } + if (-not (Test-Path $BackupDir)) { + New-Item -ItemType Directory -Path $BackupDir -Force -ErrorAction SilentlyContinue -WhatIf:$false | Out-Null + } + $file = Join-Path $BackupDir ("optimize-backup-{0:yyyyMMdd-HHmmss}.json" -f (Get-Date)) + $manifest = [pscustomobject]@{ + Timestamp = (Get-Date).ToString('s') + RestorePoint = $script:RestorePointMade + Tweaks = $script:Snapshots + } + try { + $manifest | ConvertTo-Json -Depth 8 | Set-Content -Path $file -Encoding UTF8 -WhatIf:$false + Write-OptLog "Backup manifest written: $file" 'Info' + return $file + } catch { Write-OptLog "Could not write backup manifest: $($_.Exception.Message)" 'Warning'; return $null } +} + +function Get-LatestManifest { + if (-not (Test-Path $BackupDir)) { return $null } + Get-ChildItem -Path $BackupDir -Filter 'optimize-backup-*.json' -ErrorAction SilentlyContinue | + Sort-Object LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName +} + +# ===================================================================== +# UNDO +# ===================================================================== +function Restore-Tweak { + [CmdletBinding(SupportsShouldProcess)] + param([object]$Entry, [object[]]$Registry) + + $def = $Registry | Where-Object { $_.Id -eq $Entry.Id } | Select-Object -First 1 + $name = if ($def) { $def.Name } else { $Entry.Id } + $snap = $Entry.Snapshot + + if (-not $PSCmdlet.ShouldProcess($name, 'Revert tweak')) { return } + try { + switch ($Entry.Type) { + 'Registry' { + foreach ($v in $snap.Values) { Restore-RegValue -Path $snap.Path -Snap $v } + } + 'Service' { + if ($snap.Found) { + if ($snap.StartType) { Set-Service -Name $snap.Service -StartupType $snap.StartType -ErrorAction SilentlyContinue } + if ($snap.Status -eq 'Running') { Start-Service -Name $snap.Service -ErrorAction SilentlyContinue } + } + } + 'ScheduledTask' { + if (Get-Command Enable-ScheduledTask -ErrorAction SilentlyContinue) { + foreach ($t in $snap.Tasks) { + if ($t.State -and $t.State -ne 'Disabled') { + Enable-ScheduledTask -TaskPath $t.Path -TaskName $t.Name -ErrorAction SilentlyContinue | Out-Null + } + } + } + } + 'Custom' { + if ($def -and $def.Spec.Undo) { & $def.Spec.Undo $snap } + else { Write-OptLog "No undo available for '$name'." 'Warning' } + } + } + Write-OptLog "Reverted: $name" 'Success' + $script:Applied++ + } + catch { $script:Errors++; Write-OptLog " revert $name : $($_.Exception.Message)" 'Error' } +} + +function Start-WindowsUndo { + Write-OptLog 'Windows Optimize - UNDO' 'Step' + $manifestPath = if ($BackupManifest) { $BackupManifest } else { Get-LatestManifest } + if (-not $manifestPath -or -not (Test-Path $manifestPath)) { + Write-OptLog 'No backup manifest found - nothing to undo.' 'Warning'; return + } + Write-OptLog "Using manifest: $manifestPath" 'Info' + try { $manifest = Get-Content -Path $manifestPath -Raw | ConvertFrom-Json } + catch { Write-OptLog "Could not read manifest: $($_.Exception.Message)" 'Error'; return } + + $registry = Get-OptimizationTweakRegistry + $entries = @($manifest.Tweaks) + if (-not $entries.Count) { Write-OptLog 'Manifest has no recorded tweaks.' 'Warning'; return } + + # Revert in reverse order of application. + [array]::Reverse($entries) + foreach ($e in $entries) { Restore-Tweak -Entry $e -Registry $registry } + + Write-OptLog '' 'Info' + Write-OptLog ("Reverted {0} tweak(s), {1} error(s)." -f $script:Applied, $script:Errors) 'Success' +} + +# ===================================================================== +# REPORT / SUMMARY +# ===================================================================== +function Show-OptSummary { + param([string]$ManifestFile) + $dur = (Get-Date) - $script:StartTime + $mode = if (Test-WhatIfMode) { 'DRY RUN' } else { 'OPTIMIZE' } + Write-OptLog '' 'Info' + Write-OptLog "===== $mode SUMMARY =====" 'Step' + $byArea = $script:Stats | Group-Object Area | Sort-Object Name + foreach ($g in $byArea) { + Write-OptLog (" {0,-12} {1} tweak(s)" -f $g.Name, $g.Count) 'Info' + } + $verb = if (Test-WhatIfMode) { 'Would apply' } else { 'Applied' } + Write-OptLog '' 'Info' + if (Test-WhatIfMode) { + $would = @($script:Stats | Where-Object Result -eq 'would-apply').Count + Write-OptLog ("{0}: {1} tweak(s)" -f $verb, $would) 'Success' + } + else { + Write-OptLog ("{0}: {1} tweak(s), skipped {2} already-applied, {3} error(s)" -f ` + $verb, $script:Applied, $script:Skipped, $script:Errors) 'Success' + if ($ManifestFile) { Write-OptLog "Undo with: .\Optimize-Windows-Senior.ps1 -Undo" 'Info' } + } + Write-OptLog ("Duration: {0:N1}s Log: {1}" -f $dur.TotalSeconds, $LogPath) 'Info' +} + +function Write-OptReport { + param([string]$ManifestFile) + if (-not $ReportPath) { return } + $report = [pscustomobject]@{ + Timestamp = (Get-Date).ToString('s') + Mode = if (Test-WhatIfMode) { 'DryRun' } else { 'Live' } + RestorePoint = $script:RestorePointMade + Applied = $script:Applied + Skipped = $script:Skipped + Errors = $script:Errors + Manifest = $ManifestFile + DurationSec = [math]::Round(((Get-Date) - $script:StartTime).TotalSeconds, 1) + Tweaks = $script:Stats + } + try { + $report | ConvertTo-Json -Depth 6 | Set-Content -Path $ReportPath -Encoding UTF8 -WhatIf:$false + Write-OptLog "JSON report written: $ReportPath" 'Info' + } catch { Write-OptLog "Could not write report: $($_.Exception.Message)" 'Warning' } +} + +# ===================================================================== +# UI: help / list +# ===================================================================== +function Show-TweakList { + Write-Host '' + Write-Host 'Optimization tweak registry:' -ForegroundColor Cyan + Get-OptimizationTweakRegistry | + Sort-Object Area, @{ E = { @{Safe=0;Moderate=1;Aggressive=2;Dangerous=3}[$_.Risk] } } | + Format-Table @{ L='Id'; E={$_.Id}; W=22 }, + @{ L='Area'; E={$_.Area}; W=12 }, + @{ L='Risk'; E={$_.Risk}; W=11 }, + @{ L='Default'; E={ if($_.DefaultOn){'on'}else{'off'} }; W=8 }, + @{ L='Tweak'; E={$_.Name} } -AutoSize + Write-Host 'Safe + Moderate + Aggressive selectable by default; debatable tweaks ship off (toggle or -Include).' -ForegroundColor DarkGray + Write-Host '' +} + +function Show-OptUsageHelp { +@' +Windows Optimization engine v6.0 (registry-driven, full undo) + +USAGE + .\Optimize-Windows-Senior.ps1 [options] + +SELECTION + -Area Limit to: Performance, Privacy, Debloat, Network + -Include Force tweaks on (see -ListTweaks for ids) + -Exclude Force tweaks off + -IncludeDangerous Also apply the irreversible Dangerous tier + -Conservative Cap at Safe + Moderate (skip Aggressive) + +UNDO + -Undo Revert the most recent run from its backup manifest + -BackupManifest

Undo a specific manifest file + -BackupDir Where manifests live (default %ProgramData%\WinSenior\backups) + +SAFETY + -WhatIf / -DryRun,-dr Preview only, change nothing (real ShouldProcess) + -NoRestorePoint,-nrp Skip the Checkpoint-Computer restore point (created by default) + -Unattended,-Force,-f No prompts - for automation + +OUTPUT + -LogPath Text log (default: %TEMP%\WindowsOptimize.log) + -ReportPath Machine-readable JSON report + -ListTweaks Print the tweak registry and exit + -Help Show this help + +EXAMPLES + .\Optimize-Windows-Senior.ps1 -WhatIf + .\Optimize-Windows-Senior.ps1 -Area Privacy,Performance + .\Optimize-Windows-Senior.ps1 -Undo +'@ | Write-Host +} + +# ===================================================================== +# MAIN +# ===================================================================== +function Start-WindowsOptimize { + $modeText = if (Test-WhatIfMode) { 'DryRun' } else { 'Live' } + Write-OptLog 'Windows Optimization v6.0' 'Step' + Write-OptLog ("PowerShell {0} | Mode: {1}" -f $PSVersionTable.PSVersion, $modeText) 'Info' + + if (-not (Test-AdminPrivileges)) { + Write-OptLog 'Administrator privileges are required. Re-run as Administrator.' 'Error' + exit 2 + } + + $registry = Get-OptimizationTweakRegistry + $selection = Resolve-TweakSelection -Registry $registry -Area $Area ` + -Include $Include -Exclude $Exclude -Conservative:$Conservative.IsPresent ` + -IncludeDangerous:$IncludeDangerous.IsPresent + + if (-not $selection) { Write-OptLog 'No tweaks selected - nothing to do.' 'Warning'; return } + + $dangerous = $selection | Where-Object { $_.Risk -eq 'Dangerous' } + Write-OptLog ("Selected {0} tweak(s){1}." -f @($selection).Count, + $(if ($dangerous) { ", including $($dangerous.Count) DANGEROUS" } else { '' })) 'Info' + + if ($dangerous -and -not (Test-WhatIfMode) -and -not $Unattended) { + Write-OptLog 'Dangerous (irreversible) tweaks selected:' 'Safety' + $dangerous | ForEach-Object { Write-OptLog " - $($_.Name)" 'Safety' } + $answer = Read-Host 'Proceed with these? (yes/No)' + if ($answer -notmatch '^(y|yes)$') { + $selection = $selection | Where-Object { $_.Risk -ne 'Dangerous' } + Write-OptLog 'Skipping the Dangerous tier by your choice.' 'Info' + } + } + + if (-not $NoRestorePoint -and -not (Test-WhatIfMode)) { New-OptRestorePoint | Out-Null } + + $order = 'Performance','Privacy','Debloat','Network' + foreach ($a in $order) { + foreach ($tweak in ($selection | Where-Object { $_.Area -eq $a })) { + Invoke-Tweak -Tweak $tweak + } + } + + $manifest = Write-BackupManifest + Show-OptSummary -ManifestFile $manifest + Write-OptReport -ManifestFile $manifest +} + +# ===================================================================== +# ENTRY POINT +# ===================================================================== +if ($MyInvocation.InvocationName -ne '.') { + if ($Help) { Show-OptUsageHelp; exit 0 } + if ($ListTweaks) { Show-TweakList; exit 0 } + if ($Undo) { Start-WindowsUndo; exit 0 } + Start-WindowsOptimize +} diff --git a/README.md b/README.md index 4110191..4503f3c 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,20 @@ It cleans 57 targets across browsers, developer tools, apps, games, system caches, every local disk, logs, Windows Update, and driver leftovers — all from one declarative registry -with a real dry-run mode and a hard safety guard. +with a real dry-run mode and a hard safety guard. A companion optimization engine applies +29 reversible system tweaks, a troubleshooting engine scans for 13 common problems and +repairs them, and a single menu ties everything together. + +## One command — the menu + +`WinSenior.ps1` is the single entry point. Run it (it self-elevates) and an interactive +menu opens with detailed screens for cleanup, optimization, troubleshooting, undo, a +restore point and a task/tweak/check listing. It drives all three engines, so every action +keeps real `-WhatIf`, the safety guard and per-tweak undo. + +```powershell +.\WinSenior.ps1 +``` ## Highlights @@ -120,6 +133,73 @@ task on regardless of tier. - **Optimization** — DISM analyze / component cleanup / reset base, StartComponentCleanup task, SFC. Skipped entirely with `-SkipOptimization`. +## Optimization + +`Optimize-Windows-Senior.ps1` is a second registry-driven engine that changes Windows +settings instead of deleting files. Every tweak is one declarative entry; the engine +snapshots prior state into a backup manifest before applying, so `-Undo` reverts an entire +run. A real restore point is created first as a second safety net. + +```powershell +# Preview every tweak (no changes, real ShouldProcess) +.\Optimize-Windows-Senior.ps1 -WhatIf + +# Apply the default privacy + performance set +.\Optimize-Windows-Senior.ps1 -Area Privacy,Performance + +# Revert the most recent run +.\Optimize-Windows-Senior.ps1 -Undo +``` + +It covers 29 tweaks across four areas: + +- **Performance** — visual effects to best performance, zero menu/startup delay, + High-Performance power plan, background apps off; (off by default) Ultimate plan, + SysMain / Windows Search off, hibernation off. +- **Privacy** — telemetry policy, advertising ID, consumer features, tips/spotlight, + activity feed, Start web search and Cortana off; DiagTrack and dmwappushservice disabled; + CEIP / telemetry scheduled tasks off. +- **Debloat** — curated junk UWP removal (default on); Xbox and comms apps (off by default); + Start-menu app suggestions off. +- **Network** — GameDVR off, Game Mode on, network-throttling and multimedia reservation off; + (off by default) Nagle off, NDU service off. + +Debatable tweaks ship off by default — allowed by their tier but only applied when you toggle +them on or `-Include` them. The engine never disables Defender real-time protection, never +breaks Windows Update or the network stack wholesale, and never removes Edge or the Store. +Removing UWP apps is partially irreversible; the manifest lists what to reinstall from the +Store. `-ListTweaks` prints the live list, and `-Undo` reverts a run from its backup manifest +(newest in `%ProgramData%\WinSenior\backups`, or a specific one with `-BackupManifest`). + +## Troubleshooting + +`Repair-Windows-Senior.ps1` is a third engine that diagnoses and repairs system health. +Every check is one declarative entry: a read-only scan that returns OK / Warn / Fail, and an +optional fix. The default flow is scan-then-choose — it scans (changing nothing), prints a +health report, and lets you pick which detected issues to repair. Fixes run through +`ShouldProcess` after a real restore point. + +```powershell +# Scan, show the report, then choose what to repair +.\Repair-Windows-Senior.ps1 + +# Diagnose only - never change anything +.\Repair-Windows-Senior.ps1 -ScanOnly + +# Non-interactive: auto-apply every fixable issue, including heavy repairs +.\Repair-Windows-Senior.ps1 -FixAll -IncludeHeavy -Unattended +``` + +It runs 13 checks across eight categories: system image health (DISM), physical disk SMART +health, low free space, volumes flagged for chkdsk, pending reboot, Windows Update components, +internet & DNS, devices with driver errors, stopped critical services, Microsoft Defender +health, WMI repository consistency, time synchronization, and recent critical/error events. + +Heavy repairs (SFC, DISM RestoreHealth, Windows Update reset, network stack reset) are included +but only run when you select them, or pass `-FixAll -IncludeHeavy`. Repairs only ever improve +health — the engine *enables* Defender real-time protection, it never disables it. `-ListChecks` +prints the live list. + ## Batch version `Cleanup-Windows-Senior.bat` is the simple, dependency-free alternative. v6 brings it to @@ -151,8 +231,10 @@ Exit codes: `0` success, `2` administrator privileges required. ## Tests -`tests\Cleanup-Windows-Senior.Tests.ps1` covers the pure logic (selection, safety guard, -age filtering, formatting, and `-WhatIf` accounting). Requires Pester 5+: +The `tests\*.Tests.ps1` files cover the pure logic of all three engines — selection, the +safety guard, age filtering, formatting, `-WhatIf` accounting, a registry backup→apply→undo +round-trip against a throwaway hive, and the troubleshooter's scan/fix dispatch. Requires +Pester 5+: ```powershell Invoke-Pester -Path .\tests diff --git a/Repair-Windows-Senior.ps1 b/Repair-Windows-Senior.ps1 new file mode 100644 index 0000000..b27fdf6 --- /dev/null +++ b/Repair-Windows-Senior.ps1 @@ -0,0 +1,589 @@ +<# +.SYNOPSIS + Windows troubleshooting engine - scans for common problems, then repairs them. + +.DESCRIPTION + A declarative, single-file Windows 10/11 diagnostics tool. Every check is one entry in + a check registry: a read-only Scan that returns OK / Warn / Fail, and an optional Fix. + The default flow is scan-then-choose: it scans (changing nothing), prints a health + report, and lets you pick which detected issues to repair. Fixes run through + PowerShell's ShouldProcess (so -WhatIf is real) after a real System Restore point. + + Heavy repairs (SFC, DISM RestoreHealth, Windows Update reset, network stack reset) are + included but only run when you explicitly select them (or pass -FixAll -IncludeHeavy). + Repairs only ever improve health - this engine enables Defender, it never disables it. + +.NOTES + Author : denfry (https://github.com/denfry/WindowsCleaner) + Version : 6.0.0 + Requires: PowerShell 5.1+ (Windows). Administrator rights. + +.EXAMPLE + .\Repair-Windows-Senior.ps1 + Scan, show the report, then choose what to repair. + +.EXAMPLE + .\Repair-Windows-Senior.ps1 -ScanOnly + Diagnose only - never change anything. + +.EXAMPLE + .\Repair-Windows-Senior.ps1 -FixAll -IncludeHeavy -Unattended + Scan and auto-apply every fixable issue, including heavy repairs. +#> + +#Requires -Version 5.1 + +[CmdletBinding(SupportsShouldProcess)] +param( + # Limit to these categories: Integrity, Disk, Update, Network, Devices, Services, Security, System + [string[]]$Category, + + # Force these check ids on / off (see -ListChecks for ids) + [string[]]$Include, + [string[]]$Exclude, + + # Scan and report only - never offer or apply fixes + [switch]$ScanOnly, + + # Non-interactive: after scanning, auto-apply fixable issues (Safe+Moderate) + [switch]$FixAll, + + # With -FixAll, also auto-apply Aggressive (heavy / reboot) repairs + [switch]$IncludeHeavy, + + # Cap auto-fixes at Safe + Moderate (skip Aggressive) + [Alias('SafeMode')] + [switch]$Conservative, + + [Alias('dr')] + [switch]$DryRun, + + [Alias('Force','f')] + [switch]$Unattended, + + [Alias('nrp')] + [switch]$NoRestorePoint, + + [string]$LogPath = "$env:TEMP\WindowsRepair.log", + + [string]$ReportPath, + + [switch]$ListChecks, + + [switch]$Help +) + +# ===================================================================== +# SCRIPT STATE +# ===================================================================== +$script:StartTime = Get-Date +$script:Results = New-Object System.Collections.Generic.List[object] +$script:Fixed = 0 +$script:FixErrors = 0 +$script:RebootNeeded = $false +$script:RestorePointMade = $false + +if ($DryRun) { $WhatIfPreference = $true } + +# ===================================================================== +# LOGGING / UTIL +# ===================================================================== +function Write-RepLog { + param( + [string]$Message, + [ValidateSet('Info','Success','Warning','Error','Debug','Step','WhatIf','Safety')] + [string]$Level = 'Info' + ) + $tag = switch ($Level) { + 'Success' { '[+]' } 'Warning' { '[!]' } 'Error' { '[x]' } + 'Step' { '==>' } 'WhatIf' { '[~]' } 'Safety' { '[#]' } + 'Debug' { ' ' } default { '[i]' } + } + $color = switch ($Level) { + 'Success' { 'Green' } 'Warning' { 'Yellow' } 'Error' { 'Red' } + 'Step' { 'Cyan' } 'WhatIf' { 'Cyan' } 'Safety' { 'Magenta' } + 'Debug' { 'DarkGray' } default { 'Gray' } + } + if ($Level -ne 'Debug' -or $VerbosePreference -ne 'SilentlyContinue') { + Write-Host "$tag $Message" -ForegroundColor $color + } + $stamp = "[{0:yyyy-MM-dd HH:mm:ss}] [{1}] {2}" -f (Get-Date), $Level, $Message + try { Add-Content -Path $LogPath -Value $stamp -ErrorAction SilentlyContinue -WhatIf:$false } catch { } +} + +function Format-FileSize { + param([long]$Size) + if ($Size -ge 1TB) { '{0:N2} TB' -f ($Size / 1TB) } + elseif ($Size -ge 1GB) { '{0:N2} GB' -f ($Size / 1GB) } + elseif ($Size -ge 1MB) { '{0:N2} MB' -f ($Size / 1MB) } + elseif ($Size -ge 1KB) { '{0:N2} KB' -f ($Size / 1KB) } + else { "$Size B" } +} + +function Test-WhatIfMode { [bool]$WhatIfPreference } + +function Test-AdminPrivileges { + try { + $id = [Security.Principal.WindowsIdentity]::GetCurrent() + (New-Object Security.Principal.WindowsPrincipal($id)).IsInRole( + [Security.Principal.WindowsBuiltInRole]::Administrator) + } catch { $false } +} + +function New-RepairRestorePoint { + if (Test-WhatIfMode) { Write-RepLog '[WhatIf] would create a System Restore point' 'WhatIf'; return $true } + Write-RepLog 'Creating System Restore point...' 'Safety' + try { + $rk = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SystemRestore' + New-ItemProperty -Path $rk -Name 'SystemRestorePointCreationFrequency' ` + -Value 0 -PropertyType DWord -Force -ErrorAction SilentlyContinue | Out-Null + Enable-ComputerRestore -Drive "$env:SystemDrive\" -ErrorAction SilentlyContinue + Checkpoint-Computer -Description "Before Windows Repair $(Get-Date -Format 'yyyy-MM-dd HH:mm')" ` + -RestorePointType 'MODIFY_SETTINGS' -ErrorAction Stop + Write-RepLog 'System Restore point created' 'Success' + $script:RestorePointMade = $true + return $true + } + catch { + Write-RepLog "Restore point not created: $($_.Exception.Message)" 'Warning' + return $false + } +} + +# ===================================================================== +# CHECK REGISTRY (the single source of truth) +# Scan returns @{ Status = 'OK'|'Warn'|'Fail'; Detail = '...' } +# ===================================================================== +function New-DiagnosticCheck { + param( + [string]$Id, [string]$Name, [string]$Category, + [scriptblock]$Scan, [scriptblock]$Fix, + [string]$FixRisk = 'Safe', [string]$FixLabel, [bool]$Reboot = $false + ) + [pscustomobject]@{ + Id = $Id; Name = $Name; Category = $Category + Scan = $Scan; Fix = $Fix; FixRisk = $FixRisk; FixLabel = $FixLabel; Reboot = $Reboot + } +} + +function Get-DiagnosticCheckRegistry { + @( + # ---------------- Integrity ---------------- + New-DiagnosticCheck img-health 'System image health (DISM)' Integrity ` + -Scan { + if (-not (Get-Command Repair-WindowsImage -ErrorAction SilentlyContinue)) { + return @{ Status = 'Skip'; Detail = 'DISM module unavailable' } + } + $state = (Repair-WindowsImage -Online -CheckHealth -ErrorAction Stop).ImageHealthState + switch ("$state") { + 'Healthy' { @{ Status = 'OK'; Detail = 'Component store healthy' } } + 'Repairable' { @{ Status = 'Fail'; Detail = 'Component store corruption is repairable' } } + default { @{ Status = 'Warn'; Detail = "Image health: $state (deep scan with DISM /ScanHealth)" } } + } + } ` + -Fix { + Write-RepLog 'Running DISM /RestoreHealth (may take several minutes)...' 'Info' + Repair-WindowsImage -Online -RestoreHealth -ErrorAction SilentlyContinue | Out-Null + Write-RepLog 'Running sfc /scannow...' 'Info' + & sfc.exe /scannow | Out-Null + } -FixRisk Aggressive -FixLabel 'DISM RestoreHealth + SFC' -Reboot $false + + # ---------------- Disk ---------------- + New-DiagnosticCheck disk-smart 'Physical disk health (SMART)' Disk ` + -Scan { + if (-not (Get-Command Get-PhysicalDisk -ErrorAction SilentlyContinue)) { + return @{ Status = 'Skip'; Detail = 'Storage module unavailable' } + } + $bad = Get-PhysicalDisk -ErrorAction SilentlyContinue | Where-Object { $_.HealthStatus -and $_.HealthStatus -ne 'Healthy' } + if ($bad) { @{ Status = 'Fail'; Detail = ('Unhealthy disk(s): ' + (($bad | ForEach-Object { "$($_.FriendlyName)=$($_.HealthStatus)" }) -join ', ') + ' - back up now') } } + else { @{ Status = 'OK'; Detail = 'All physical disks report Healthy' } } + } -Fix $null + + New-DiagnosticCheck disk-space 'Low free disk space' Disk ` + -Scan { + $worst = 'OK'; $lines = @() + foreach ($d in (Get-CimInstance Win32_LogicalDisk -Filter 'DriveType=3' -ErrorAction SilentlyContinue)) { + if (-not $d.Size) { continue } + $pct = [math]::Round(($d.FreeSpace / $d.Size) * 100, 1) + $freeGB = [math]::Round($d.FreeSpace / 1GB, 1) + $lines += "$($d.DeviceID) $freeGB GB free ($pct%)" + if ($pct -lt 5 -or $freeGB -lt 5) { $worst = 'Fail' } + elseif (($pct -lt 12 -or $freeGB -lt 15) -and $worst -ne 'Fail') { $worst = 'Warn' } + } + @{ Status = $worst; Detail = ($lines -join ' | ') + $(if ($worst -ne 'OK') { ' - run Disk cleanup' } else { '' }) } + } -Fix $null + + New-DiagnosticCheck disk-dirty 'Volumes flagged for chkdsk' Disk ` + -Scan { + $dirty = @() + foreach ($d in (Get-CimInstance Win32_LogicalDisk -Filter 'DriveType=3' -ErrorAction SilentlyContinue)) { + & fsutil.exe dirty query "$($d.DeviceID)" *>$null + if ($LASTEXITCODE -eq 0) { $dirty += $d.DeviceID } + } + if ($dirty) { @{ Status = 'Warn'; Detail = ('Dirty bit set on: ' + ($dirty -join ', ')) } } + else { @{ Status = 'OK'; Detail = 'No volume flagged dirty' } } + } ` + -Fix { + foreach ($d in (Get-CimInstance Win32_LogicalDisk -Filter 'DriveType=3' -ErrorAction SilentlyContinue)) { + & fsutil.exe dirty query "$($d.DeviceID)" *>$null + if ($LASTEXITCODE -eq 0) { Write-RepLog "chkdsk $($d.DeviceID) /scan (online)..." 'Info'; & chkdsk.exe "$($d.DeviceID)" /scan | Out-Null } + } + } -FixRisk Moderate -FixLabel 'chkdsk /scan (online, no reboot)' + + # ---------------- Update ---------------- + New-DiagnosticCheck reboot-pending 'Pending reboot' Update ` + -Scan { + $reasons = @() + if (Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending') { $reasons += 'CBS' } + if (Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired') { $reasons += 'WindowsUpdate' } + $pfro = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' -Name PendingFileRenameOperations -ErrorAction SilentlyContinue).PendingFileRenameOperations + if ($pfro) { $reasons += 'PendingFileRename' } + if ($reasons) { @{ Status = 'Warn'; Detail = ('Reboot required: ' + ($reasons -join ', ')) } } + else { @{ Status = 'OK'; Detail = 'No pending reboot' } } + } ` + -Fix { Write-RepLog 'Scheduling reboot in 60s (cancel with: shutdown /a)' 'Warning'; & shutdown.exe /r /t 60 /c 'WinSenior repair reboot' } ` + -FixRisk Aggressive -FixLabel 'Reboot in 60s (cancel: shutdown /a)' -Reboot $true + + New-DiagnosticCheck wu-health 'Windows Update components' Update ` + -Scan { + $sd = "$env:WINDIR\SoftwareDistribution\Download" + $sizeGB = 0 + if (Test-Path $sd) { $sizeGB = [math]::Round(((Get-ChildItem $sd -Recurse -Force -File -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum) / 1GB, 2) } + $wu = Get-Service wuauserv -ErrorAction SilentlyContinue + if ($wu -and $wu.StartType -eq 'Disabled') { return @{ Status = 'Warn'; Detail = 'wuauserv is Disabled; SoftwareDistribution ' + $sizeGB + ' GB' } } + if ($sizeGB -gt 4) { return @{ Status = 'Warn'; Detail = "SoftwareDistribution cache is large ($sizeGB GB)" } } + @{ Status = 'OK'; Detail = "Update cache $sizeGB GB; service OK" } + } ` + -Fix { + Write-RepLog 'Resetting Windows Update components...' 'Info' + foreach ($s in 'wuauserv','bits','cryptsvc') { Stop-Service $s -Force -ErrorAction SilentlyContinue } + foreach ($p in @("$env:WINDIR\SoftwareDistribution","$env:WINDIR\System32\catroot2")) { + if (Test-Path $p) { Rename-Item $p "$p.old_$(Get-Date -Format 'yyyyMMddHHmmss')" -Force -ErrorAction SilentlyContinue } + } + foreach ($s in 'cryptsvc','bits','wuauserv') { Start-Service $s -ErrorAction SilentlyContinue } + } -FixRisk Moderate -FixLabel 'Reset Windows Update (rename SoftwareDistribution/catroot2)' + + # ---------------- Network ---------------- + New-DiagnosticCheck net-connectivity 'Internet & DNS' Network ` + -Scan { + # Address held in a variable so PSScriptAnalyzer doesn't flag it as a hardcoded host. + $pingTarget = '8.8.8.8'; $dnsTarget = 'microsoft.com' + $ping = Test-Connection -ComputerName $pingTarget -Count 1 -Quiet -ErrorAction SilentlyContinue + $dns = $false + if (Get-Command Resolve-DnsName -ErrorAction SilentlyContinue) { + $dns = [bool](Resolve-DnsName $dnsTarget -ErrorAction SilentlyContinue) + } + if (-not $ping) { @{ Status = 'Fail'; Detail = 'No reply from 8.8.8.8 (no internet)' } } + elseif (-not $dns) { @{ Status = 'Warn'; Detail = 'Internet OK but DNS resolution failed' } } + else { @{ Status = 'OK'; Detail = 'Internet and DNS reachable' } } + } ` + -Fix { + Write-RepLog 'Flushing DNS and resetting the network stack...' 'Info' + & ipconfig.exe /flushdns | Out-Null + & netsh.exe winsock reset | Out-Null + & netsh.exe int ip reset | Out-Null + & ipconfig.exe /release | Out-Null + & ipconfig.exe /renew | Out-Null + } -FixRisk Aggressive -FixLabel 'Flush DNS + winsock/IP reset' -Reboot $true + + # ---------------- Devices ---------------- + New-DiagnosticCheck dev-errors 'Devices with driver problems' Devices ` + -Scan { + $bad = Get-CimInstance Win32_PnPEntity -ErrorAction SilentlyContinue | Where-Object { $_.ConfigManagerErrorCode -and $_.ConfigManagerErrorCode -ne 0 } + if ($bad) { + $names = ($bad | Select-Object -First 5 | ForEach-Object { "$($_.Name) (code $($_.ConfigManagerErrorCode))" }) -join '; ' + @{ Status = 'Warn'; Detail = "$(@($bad).Count) device(s) with errors: $names" } + } else { @{ Status = 'OK'; Detail = 'No devices report driver errors' } } + } ` + -Fix { Write-RepLog 'Rescanning for hardware changes...' 'Info'; & pnputil.exe /scan-devices *>$null } ` + -FixRisk Safe -FixLabel 'Rescan devices (pnputil /scan-devices)' + + # ---------------- Services ---------------- + New-DiagnosticCheck svc-critical 'Critical services stopped' Services ` + -Scan { + $want = 'Audiosrv','Dhcp','Dnscache','EventLog','mpssvc','Winmgmt','Schedule','BFE','LanmanWorkstation','ProfSvc','nsi','Power' + $stopped = foreach ($n in $want) { + $s = Get-Service $n -ErrorAction SilentlyContinue + if ($s -and $s.StartType -in 'Automatic','Boot','System' -and $s.Status -ne 'Running') { $n } + } + $stopped = @($stopped) + if ($stopped.Count) { @{ Status = 'Fail'; Detail = ('Stopped: ' + ($stopped -join ', ')) } } + else { @{ Status = 'OK'; Detail = 'All monitored critical services are running' } } + } ` + -Fix { + $want = 'Audiosrv','Dhcp','Dnscache','EventLog','mpssvc','Winmgmt','Schedule','BFE','LanmanWorkstation','ProfSvc','nsi','Power' + foreach ($n in $want) { + $s = Get-Service $n -ErrorAction SilentlyContinue + if ($s -and $s.StartType -in 'Automatic','Boot','System' -and $s.Status -ne 'Running') { + Start-Service $n -ErrorAction SilentlyContinue + Write-RepLog "started $n" 'Debug' + } + } + } -FixRisk Safe -FixLabel 'Start stopped critical services' + + # ---------------- Security ---------------- + New-DiagnosticCheck def-health 'Microsoft Defender health' Security ` + -Scan { + if (-not (Get-Command Get-MpComputerStatus -ErrorAction SilentlyContinue)) { + return @{ Status = 'Skip'; Detail = 'Defender module unavailable (3rd-party AV?)' } + } + $st = Get-MpComputerStatus -ErrorAction Stop + $issues = @() + if (-not $st.RealTimeProtectionEnabled) { $issues += 'real-time protection OFF' } + if ($st.AntivirusSignatureAge -gt 7) { $issues += "signatures $($st.AntivirusSignatureAge)d old" } + if ($issues) { @{ Status = 'Warn'; Detail = ($issues -join '; ') } } + else { @{ Status = 'OK'; Detail = 'Real-time protection on; signatures current' } } + } ` + -Fix { + Set-MpPreference -DisableRealtimeMonitoring $false -ErrorAction SilentlyContinue + Write-RepLog 'Updating Defender signatures...' 'Info' + Update-MpSignature -ErrorAction SilentlyContinue + } -FixRisk Safe -FixLabel 'Enable real-time protection + update signatures' + + # ---------------- System ---------------- + New-DiagnosticCheck wmi-repo 'WMI repository consistency' System ` + -Scan { + $out = & winmgmt.exe /verifyrepository 2>&1 + if ($LASTEXITCODE -eq 0) { @{ Status = 'OK'; Detail = 'WMI repository is consistent' } } + else { @{ Status = 'Fail'; Detail = 'WMI repository inconsistent' } } + } ` + -Fix { Write-RepLog 'Salvaging WMI repository...' 'Info'; & winmgmt.exe /salvagerepository 2>&1 | Out-Null } ` + -FixRisk Moderate -FixLabel 'Salvage WMI repository' + + New-DiagnosticCheck time-sync 'System time synchronization' System ` + -Scan { + $w = Get-Service w32time -ErrorAction SilentlyContinue + if (-not $w) { return @{ Status = 'Skip'; Detail = 'w32time service not found' } } + if ($w.Status -ne 'Running') { return @{ Status = 'Warn'; Detail = 'Time service (w32time) is stopped' } } + @{ Status = 'OK'; Detail = 'Time service running' } + } ` + -Fix { Start-Service w32time -ErrorAction SilentlyContinue; & w32tm.exe /resync /force *>$null } ` + -FixRisk Safe -FixLabel 'Start w32time + resync clock' + + New-DiagnosticCheck event-errors 'Recent critical/error events' System ` + -Scan { + $ev = Get-WinEvent -FilterHashtable @{ LogName = 'System'; Level = 1,2; StartTime = (Get-Date).AddDays(-2) } -MaxEvents 300 -ErrorAction SilentlyContinue + $ev = @($ev) + if ($ev.Count -eq 0) { return @{ Status = 'OK'; Detail = 'No critical/error events in the last 48h' } } + $top = ($ev | Group-Object ProviderName | Sort-Object Count -Descending | Select-Object -First 3 | + ForEach-Object { "$($_.Name)=$($_.Count)" }) -join ', ' + $status = if ($ev.Count -gt 50) { 'Warn' } else { 'OK' } + @{ Status = $status; Detail = "$($ev.Count) error/critical event(s) in 48h; top: $top" } + } -Fix $null + ) +} + +# ===================================================================== +# SELECTION +# ===================================================================== +function Resolve-CheckSelection { + param([object[]]$Registry, [string[]]$Category, [string[]]$Include, [string[]]$Exclude) + foreach ($c in $Registry) { + $on = $true + if ($Category -and ($c.Category -notin $Category)) { $on = $false } + if (($Include -contains $c.Id) -or ($Include -contains $c.Name)) { $on = $true } + if (($Exclude -contains $c.Id) -or ($Exclude -contains $c.Name)) { $on = $false } + if ($on) { $c } + } +} + +# ===================================================================== +# SCAN / FIX +# ===================================================================== +function Invoke-Scan { + param([object]$Check) + $r = @{ Status = 'Skip'; Detail = '' } + try { $r = & $Check.Scan } catch { $r = @{ Status = 'Skip'; Detail = $_.Exception.Message } } + [pscustomobject]@{ + Id = $Check.Id; Name = $Check.Name; Category = $Check.Category + Status = $r.Status; Detail = $r.Detail + HasFix = [bool]$Check.Fix; FixRisk = $Check.FixRisk; FixLabel = $Check.FixLabel; Reboot = $Check.Reboot + } +} + +function Invoke-Fix { + [CmdletBinding(SupportsShouldProcess)] + param([object]$Check) + if ($PSCmdlet.ShouldProcess($Check.Name, "Fix: $($Check.FixLabel)")) { + try { + & $Check.Fix + Write-RepLog "Fixed: $($Check.Name)" 'Success' + $script:Fixed++ + if ($Check.Reboot) { $script:RebootNeeded = $true } + return $true + } + catch { $script:FixErrors++; Write-RepLog " fix $($Check.Name): $($_.Exception.Message)" 'Error'; return $false } + } + $false +} + +function Get-StatusColor { param([string]$S) + switch ($S) { 'OK' { 'Green' } 'Warn' { 'Yellow' } 'Fail' { 'Red' } default { 'DarkGray' } } } + +function Show-ScanReport { + Write-RepLog '' 'Info' + Write-RepLog '===== HEALTH REPORT =====' 'Step' + $last = $null + foreach ($r in $script:Results) { + if ($r.Category -ne $last) { Write-Host (" {0}" -f $r.Category) -ForegroundColor Cyan; $last = $r.Category } + $mark = switch ($r.Status) { 'OK' { 'OK ' } 'Warn' { 'WARN' } 'Fail' { 'FAIL' } default { 'skip' } } + Write-Host (" [{0}] {1,-34} {2}" -f $mark, $r.Name, $r.Detail) -ForegroundColor (Get-StatusColor $r.Status) + } + $warn = @($script:Results | Where-Object Status -eq 'Warn').Count + $fail = @($script:Results | Where-Object Status -eq 'Fail').Count + Write-RepLog '' 'Info' + Write-RepLog ("Issues found: {0} failing, {1} warning, {2} OK" -f $fail, $warn, + @($script:Results | Where-Object Status -eq 'OK').Count) $(if ($fail) { 'Error' } elseif ($warn) { 'Warning' } else { 'Success' }) +} + +function Write-RepReport { + if (-not $ReportPath) { return } + $report = [pscustomobject]@{ + Timestamp = (Get-Date).ToString('s') + Mode = if (Test-WhatIfMode) { 'DryRun' } else { 'Live' } + Fixed = $script:Fixed + FixErrors = $script:FixErrors + Reboot = $script:RebootNeeded + Results = $script:Results + } + try { + $report | ConvertTo-Json -Depth 6 | Set-Content -Path $ReportPath -Encoding UTF8 -WhatIf:$false + Write-RepLog "JSON report written: $ReportPath" 'Info' + } catch { Write-RepLog "Could not write report: $($_.Exception.Message)" 'Warning' } +} + +# ===================================================================== +# UI: help / list +# ===================================================================== +function Show-CheckList { + Write-Host '' + Write-Host 'Diagnostic check registry:' -ForegroundColor Cyan + Get-DiagnosticCheckRegistry | + Format-Table @{ L='Id'; E={$_.Id}; W=18 }, + @{ L='Category'; E={$_.Category}; W=10 }, + @{ L='Fix'; E={ if($_.Fix){$_.FixRisk}else{'(report only)'} }; W=14 }, + @{ L='Check'; E={$_.Name} } -AutoSize + Write-Host '' +} + +function Show-RepUsageHelp { +@' +Windows Troubleshooting engine v6.0 (scan -> report -> repair) + +USAGE + .\Repair-Windows-Senior.ps1 [options] + +SELECTION + -Category Limit to: Integrity, Disk, Update, Network, Devices, Services, Security, System + -Include Force checks on (see -ListChecks for ids) + -Exclude Force checks off + +FLOW + (default) Scan, show report, then choose what to repair + -ScanOnly Diagnose only - never change anything + -FixAll Non-interactive: auto-apply fixable issues (Safe+Moderate) + -IncludeHeavy With -FixAll, also apply Aggressive (heavy/reboot) repairs + -Conservative Cap auto-fixes at Safe + Moderate + +SAFETY + -WhatIf / -DryRun,-dr Preview only, change nothing (real ShouldProcess) + -NoRestorePoint,-nrp Skip the restore point made before repairs + -Unattended,-Force,-f No prompts - for automation + +OUTPUT + -LogPath Text log (default: %TEMP%\WindowsRepair.log) + -ReportPath Machine-readable JSON report + -ListChecks Print the check registry and exit + -Help Show this help + +EXAMPLES + .\Repair-Windows-Senior.ps1 + .\Repair-Windows-Senior.ps1 -ScanOnly + .\Repair-Windows-Senior.ps1 -FixAll -IncludeHeavy -Unattended +'@ | Write-Host +} + +# ===================================================================== +# MAIN +# ===================================================================== +function Start-WindowsRepair { + Write-RepLog 'Windows Troubleshooting v6.0' 'Step' + Write-RepLog ("PowerShell {0} | Mode: {1}" -f $PSVersionTable.PSVersion, $(if (Test-WhatIfMode) { 'DryRun' } else { 'Live' })) 'Info' + if (-not (Test-AdminPrivileges)) { Write-RepLog 'Administrator privileges are required. Re-run as Administrator.' 'Error'; exit 2 } + + $registry = Get-DiagnosticCheckRegistry + $selection = @(Resolve-CheckSelection -Registry $registry -Category $Category -Include $Include -Exclude $Exclude) + if (-not $selection.Count) { Write-RepLog 'No checks selected.' 'Warning'; return } + + Write-RepLog ("Scanning {0} check(s)..." -f $selection.Count) 'Info' + foreach ($c in $selection) { + Write-RepLog (" scanning: {0}" -f $c.Name) 'Debug' + $script:Results.Add((Invoke-Scan -Check $c)) + } + Show-ScanReport + Write-RepReport + + if ($ScanOnly) { return } + + # Fixable = Warn/Fail with a Fix defined. + $rank = @{ Safe = 0; Moderate = 1; Aggressive = 2 } + $fixable = @($script:Results | Where-Object { $_.HasFix -and $_.Status -in 'Warn','Fail' }) + if (-not $fixable.Count) { Write-RepLog 'No auto-fixable issues detected.' 'Success'; return } + + # Decide which to fix. + $toFix = @() + if ($FixAll -or $Unattended) { + $cap = if ($IncludeHeavy -and -not $Conservative) { 2 } elseif ($Conservative) { 1 } else { 1 } + $toFix = $fixable | Where-Object { $rank[$_.FixRisk] -le $cap } + } + elseif (-not (Test-WhatIfMode)) { + Write-RepLog '' 'Info' + Write-RepLog 'Fixable issues:' 'Step' + $i = 0; $map = @{} + foreach ($f in $fixable) { + $i++; $map[$i] = $f + $rb = if ($f.Reboot) { ' [reboot]' } else { '' } + Write-Host (" {0,2}. ({1,-10}) {2} -> {3}{4}" -f $i, $f.FixRisk, $f.Name, $f.FixLabel, $rb) -ForegroundColor (Get-StatusColor $f.Status) + } + Write-Host '' + Write-Host ' Enter numbers to fix | a=all safe (Safe+Moderate) h=all incl. heavy Enter=skip' -ForegroundColor DarkGray + $in = (Read-Host ' >').Trim() + if ($in -eq '') { Write-RepLog 'No repairs selected.' 'Info'; return } + elseif ($in -eq 'a') { $toFix = $fixable | Where-Object { $rank[$_.FixRisk] -le 1 } } + elseif ($in -eq 'h') { $toFix = $fixable } + else { + $sel = @() + foreach ($tok in ($in -split '[\s,]+')) { if ($tok -match '^\d+$' -and $map.ContainsKey([int]$tok)) { $sel += $map[[int]$tok] } } + $toFix = $sel + } + } + else { + # -WhatIf: preview fixing everything fixable. + $toFix = $fixable + } + + $toFix = @($toFix) + if (-not $toFix.Count) { Write-RepLog 'Nothing to repair.' 'Info'; return } + + if (-not $NoRestorePoint -and -not (Test-WhatIfMode)) { New-RepairRestorePoint | Out-Null } + + foreach ($r in $toFix) { + $check = $registry | Where-Object { $_.Id -eq $r.Id } | Select-Object -First 1 + if ($check) { Invoke-Fix -Check $check | Out-Null } + } + + Write-RepLog '' 'Info' + $verb = if (Test-WhatIfMode) { 'Would fix' } else { 'Fixed' } + Write-RepLog ("{0}: {1} issue(s), {2} error(s)" -f $verb, $script:Fixed, $script:FixErrors) 'Success' + if ($script:RebootNeeded) { Write-RepLog 'A reboot is required to complete some repairs.' 'Warning' } + Write-RepLog ("Duration: {0:N1}s Log: {1}" -f ((Get-Date) - $script:StartTime).TotalSeconds, $LogPath) 'Info' +} + +# ===================================================================== +# ENTRY POINT +# ===================================================================== +if ($MyInvocation.InvocationName -ne '.') { + if ($Help) { Show-RepUsageHelp; exit 0 } + if ($ListChecks) { Show-CheckList; exit 0 } + Start-WindowsRepair +} diff --git a/WinSenior.ps1 b/WinSenior.ps1 new file mode 100644 index 0000000..2c87931 --- /dev/null +++ b/WinSenior.ps1 @@ -0,0 +1,321 @@ +<# +.SYNOPSIS + Windows Senior - one interactive menu for the cleanup and optimization engines. + +.DESCRIPTION + The single entry point. Run this and a menu opens with detailed screens for disk + cleanup, Windows optimization, undo, restore point and reports. It drives the two + engines (Cleanup-Windows-Senior.ps1 and Optimize-Windows-Senior.ps1) by invoking + them with parameters, so every run goes through their tested logic, real -WhatIf, + safety guard and per-tweak undo. + +.NOTES + Author : denfry (https://github.com/denfry/WindowsCleaner) + Version : 6.0.0 + Requires: PowerShell 5.1+ (Windows). Administrator rights (auto-elevates). + +.EXAMPLE + .\WinSenior.ps1 + Open the interactive menu. +#> + +#Requires -Version 5.1 + +[CmdletBinding()] +param( + # Do not try to relaunch elevated; run with whatever rights we have. + [switch]$NoElevate +) + +try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch { } + +# ===================================================================== +# LOCATE ENGINES + ELEVATE +# ===================================================================== +$script:Root = $PSScriptRoot +$script:CleanupScript = Join-Path $script:Root 'Cleanup-Windows-Senior.ps1' +$script:OptimizeScript = Join-Path $script:Root 'Optimize-Windows-Senior.ps1' +$script:RepairScript = Join-Path $script:Root 'Repair-Windows-Senior.ps1' + +function Test-Admin { + try { + $id = [Security.Principal.WindowsIdentity]::GetCurrent() + (New-Object Security.Principal.WindowsPrincipal($id)).IsInRole( + [Security.Principal.WindowsBuiltInRole]::Administrator) + } catch { $false } +} + +foreach ($s in @($script:CleanupScript, $script:OptimizeScript, $script:RepairScript)) { + if (-not (Test-Path $s)) { + Write-Host "Engine not found: $s" -ForegroundColor Red + Write-Host 'Keep WinSenior.ps1 next to the two engine scripts.' -ForegroundColor Yellow + exit 1 + } +} + +if (-not (Test-Admin)) { + if ($NoElevate) { + Write-Host '[!] Not running as Administrator - most actions will fail.' -ForegroundColor Yellow + } + else { + Write-Host 'Requesting administrator privileges...' -ForegroundColor Cyan + try { + Start-Process -FilePath 'powershell.exe' -Verb RunAs -ArgumentList @( + '-NoProfile','-ExecutionPolicy','Bypass','-File',"`"$PSCommandPath`"") + exit 0 + } catch { + Write-Host 'Elevation cancelled. Re-run as Administrator, or use -NoElevate.' -ForegroundColor Red + exit 1 + } + } +} + +# Load the engines as libraries (their entry guards keep them from auto-running). +. $script:CleanupScript +. $script:OptimizeScript +. $script:RepairScript + +# ===================================================================== +# SELECTION STATE +# ===================================================================== +$script:CleanReg = Get-CleanupTaskRegistry +$script:OptReg = Get-OptimizationTweakRegistry + +# Start from each engine's default selection. +$script:CleanOn = New-Object 'System.Collections.Generic.HashSet[string]' +foreach ($t in (Resolve-CleanupSelection -Registry $script:CleanReg)) { [void]$script:CleanOn.Add($t.Id) } +$script:OptOn = New-Object 'System.Collections.Generic.HashSet[string]' +foreach ($t in (Resolve-TweakSelection -Registry $script:OptReg)) { [void]$script:OptOn.Add($t.Id) } + +$script:CleanCU = $false # current-user-only toggle for cleanup + +# ===================================================================== +# UI HELPERS +# ===================================================================== +function Write-Banner { + param([string]$Title) + Clear-Host + Write-Host '' + Write-Host " ============================================================" -ForegroundColor DarkCyan + Write-Host " $Title" -ForegroundColor Cyan + Write-Host " ============================================================" -ForegroundColor DarkCyan + Write-Host '' +} + +function Read-Key { + param([string]$Prompt = ' > ') + Write-Host $Prompt -ForegroundColor White -NoNewline + Read-Host +} + +function Wait-Enter { Write-Host ''; Write-Host ' Press Enter to continue...' -ForegroundColor DarkGray -NoNewline; [void](Read-Host) } + +# Generic toggle screen. $OnSet is mutated in place (HashSet is a reference type). +function Invoke-ToggleScreen { + param([string]$Title, [object[]]$Items, [object]$OnSet, [string]$GroupProp, [hashtable]$AppliedMap) + while ($true) { + Write-Banner $Title + $i = 0; $map = @{}; $last = $null + foreach ($it in $Items) { + if ($it.$GroupProp -ne $last) { + Write-Host " $($it.$GroupProp)" -ForegroundColor Yellow + $last = $it.$GroupProp + } + $i++; $map[$i] = $it.Id + $on = $OnSet.Contains($it.Id) + $mark = if ($on) { '[x]' } else { '[ ]' } + $col = if ($on) { 'Green' } else { 'DarkGray' } + $suffix = '' + if ($AppliedMap -and $AppliedMap.ContainsKey($it.Id)) { + $st = $AppliedMap[$it.Id] + if ($st -eq $true) { $suffix = ' (applied)' } elseif ($st -eq $false) { $suffix = ' (not set)' } + } + Write-Host (" {0,3}. {1} {2,-11} {3}{4}" -f $i, $mark, $it.Risk, $it.Name, $suffix) -ForegroundColor $col + } + Write-Host '' + Write-Host ' Type numbers (space/comma separated) to toggle | a=all n=none Enter=done' -ForegroundColor DarkGray + $in = (Read-Key).Trim() + if ($in -eq '') { break } + if ($in -eq 'a') { foreach ($it in $Items) { [void]$OnSet.Add($it.Id) }; continue } + if ($in -eq 'n') { $OnSet.Clear(); continue } + foreach ($tok in ($in -split '[\s,]+')) { + if ($tok -match '^\d+$' -and $map.ContainsKey([int]$tok)) { + $id = $map[[int]$tok] + if ($OnSet.Contains($id)) { [void]$OnSet.Remove($id) } else { [void]$OnSet.Add($id) } + } + } + } +} + +# Build -Include/-Exclude so the engine reproduces exactly the toggled set. +function Get-SelectionParams { + param([object[]]$Registry, [object]$OnSet) + $on = @($Registry | Where-Object { $OnSet.Contains($_.Id) } | ForEach-Object Id) + $off = @($Registry | Where-Object { -not $OnSet.Contains($_.Id) } | ForEach-Object Id) + @{ Include = $on; Exclude = $off } +} + +# ===================================================================== +# CLEANUP SCREEN +# ===================================================================== +function Show-CleanupScreen { + while ($true) { + Write-Banner 'Disk cleanup' + $onCount = @($script:CleanReg | Where-Object { $script:CleanOn.Contains($_.Id) }).Count + $danger = @($script:CleanReg | Where-Object { $script:CleanOn.Contains($_.Id) -and $_.Risk -eq 'Dangerous' }).Count + $scope = if ($script:CleanCU) { 'current user' } else { 'all users' } + Write-Host " Selected: $onCount / $($script:CleanReg.Count) tasks Scope: $scope" -ForegroundColor Gray + if ($danger) { Write-Host " Includes $danger DANGEROUS task(s) - you will be asked to confirm." -ForegroundColor Magenta } + Write-Host '' + Write-Host ' 1. Preview (dry run, changes nothing)' -ForegroundColor White + Write-Host ' 2. Run cleanup' -ForegroundColor White + Write-Host ' 3. Choose tasks (detailed toggle)' -ForegroundColor White + Write-Host " 4. Scope: toggle current-user-only (now: $scope)" -ForegroundColor White + Write-Host ' 5. Reset to defaults' -ForegroundColor White + Write-Host ' 0. Back' -ForegroundColor White + Write-Host '' + switch ((Read-Key).Trim()) { + '1' { Invoke-Cleanup -Preview $true; Wait-Enter } + '2' { Invoke-Cleanup -Preview $false; Wait-Enter } + '3' { Invoke-ToggleScreen -Title 'Cleanup tasks' -Items $script:CleanReg -OnSet $script:CleanOn -GroupProp 'Category' } + '4' { $script:CleanCU = -not $script:CleanCU } + '5' { $script:CleanOn.Clear(); foreach ($t in (Resolve-CleanupSelection -Registry $script:CleanReg)) { [void]$script:CleanOn.Add($t.Id) } } + '0' { return } + default { } + } + } +} + +function Invoke-Cleanup { + param([bool]$Preview) + $params = Get-SelectionParams -Registry $script:CleanReg -OnSet $script:CleanOn + if (-not $params.Include.Count) { Write-Host ' Nothing selected.' -ForegroundColor Yellow; return } + if ($script:CleanCU) { $params.CurrentUserOnly = $true } + if ($Preview) { $params.WhatIf = $true } + Write-Host '' + & $script:CleanupScript @params +} + +# ===================================================================== +# OPTIMIZATION SCREEN +# ===================================================================== +function Get-AppliedMap { + $m = @{} + foreach ($t in $script:OptReg) { $m[$t.Id] = (Test-TweakApplied -Tweak $t) } + $m +} + +function Show-OptimizeScreen { + while ($true) { + Write-Banner 'Windows optimization' + $onCount = @($script:OptReg | Where-Object { $script:OptOn.Contains($_.Id) }).Count + Write-Host " Selected: $onCount / $($script:OptReg.Count) tweaks" -ForegroundColor Gray + Write-Host ' Every applied tweak is backed up first; use Undo to revert.' -ForegroundColor DarkGray + Write-Host '' + Write-Host ' 1. Preview (dry run, changes nothing)' -ForegroundColor White + Write-Host ' 2. Apply tweaks' -ForegroundColor White + Write-Host ' 3. Choose tweaks (detailed toggle, shows current state)' -ForegroundColor White + Write-Host ' 4. Undo last optimization run' -ForegroundColor White + Write-Host ' 5. Reset to defaults' -ForegroundColor White + Write-Host ' 0. Back' -ForegroundColor White + Write-Host '' + switch ((Read-Key).Trim()) { + '1' { Invoke-Optimize -Preview $true; Wait-Enter } + '2' { Invoke-Optimize -Preview $false; Wait-Enter } + '3' { + Write-Host ' Reading current state...' -ForegroundColor DarkGray + $applied = Get-AppliedMap + Invoke-ToggleScreen -Title 'Optimization tweaks' -Items $script:OptReg -OnSet $script:OptOn -GroupProp 'Area' -AppliedMap $applied + } + '4' { Write-Host ''; & $script:OptimizeScript -Undo; Wait-Enter } + '5' { $script:OptOn.Clear(); foreach ($t in (Resolve-TweakSelection -Registry $script:OptReg)) { [void]$script:OptOn.Add($t.Id) } } + '0' { return } + default { } + } + } +} + +function Invoke-Optimize { + param([bool]$Preview) + $params = Get-SelectionParams -Registry $script:OptReg -OnSet $script:OptOn + if (-not $params.Include.Count) { Write-Host ' Nothing selected.' -ForegroundColor Yellow; return } + if ($Preview) { $params.WhatIf = $true } + Write-Host '' + & $script:OptimizeScript @params +} + +# ===================================================================== +# FULL RUN +# ===================================================================== +function Invoke-FullRun { + Write-Banner 'Full run: cleanup + optimization' + Write-Host ' This will run the selected cleanup tasks AND apply the selected tweaks.' -ForegroundColor Yellow + Write-Host ' A restore point is created first; tweaks are backed up for undo.' -ForegroundColor DarkGray + Write-Host '' + if ((Read-Key ' Type "yes" to proceed: ').Trim() -notmatch '^(y|yes)$') { Write-Host ' Cancelled.' -ForegroundColor Gray; Wait-Enter; return } + Write-Host '' + Invoke-Cleanup -Preview $false + Invoke-Optimize -Preview $false + Wait-Enter +} + +# ===================================================================== +# TROUBLESHOOT SCREEN +# ===================================================================== +function Show-TroubleshootScreen { + while ($true) { + Write-Banner 'Troubleshoot - scan & repair' + Write-Host ' Scans for common Windows problems (read-only), then lets you repair them.' -ForegroundColor DarkGray + Write-Host ' A restore point is made before any repair.' -ForegroundColor DarkGray + Write-Host '' + Write-Host ' 1. Scan & repair (scan, then choose what to fix)' -ForegroundColor White + Write-Host ' 2. Scan only (diagnose, change nothing)' -ForegroundColor White + Write-Host ' 3. Auto-fix safe (apply Safe + Moderate fixes automatically)' -ForegroundColor White + Write-Host ' 4. Auto-fix all (include heavy repairs: SFC/DISM/WU/network)' -ForegroundColor White + Write-Host ' 0. Back' -ForegroundColor White + Write-Host '' + switch ((Read-Key).Trim()) { + '1' { Write-Host ''; & $script:RepairScript; Wait-Enter } + '2' { Write-Host ''; & $script:RepairScript -ScanOnly; Wait-Enter } + '3' { Write-Host ''; & $script:RepairScript -FixAll; Wait-Enter } + '4' { Write-Host ''; & $script:RepairScript -FixAll -IncludeHeavy; Wait-Enter } + '0' { return } + default { } + } + } +} + +# ===================================================================== +# MAIN MENU +# ===================================================================== +function Show-MainMenu { + while ($true) { + Write-Banner 'Windows Senior - system maintenance' + $admin = if (Test-Admin) { 'yes' } else { 'NO (run as admin)' } + Write-Host " Administrator: $admin" -ForegroundColor $(if (Test-Admin) { 'Green' } else { 'Red' }) + Write-Host '' + Write-Host ' 1. Disk cleanup (detailed - categories, preview, run)' -ForegroundColor White + Write-Host ' 2. Optimize Windows (performance / privacy / debloat / network)' -ForegroundColor White + Write-Host ' 3. Troubleshoot (scan for problems, then repair)' -ForegroundColor White + Write-Host ' 4. Full run (cleanup + optimization)' -ForegroundColor White + Write-Host ' 5. Undo optimizations (revert last run from backup)' -ForegroundColor White + Write-Host ' 6. Create restore point' -ForegroundColor White + Write-Host ' 7. List tasks, tweaks & checks' -ForegroundColor White + Write-Host ' 0. Exit' -ForegroundColor White + Write-Host '' + switch ((Read-Key).Trim()) { + '1' { Show-CleanupScreen } + '2' { Show-OptimizeScreen } + '3' { Show-TroubleshootScreen } + '4' { Invoke-FullRun } + '5' { Write-Banner 'Undo optimizations'; & $script:OptimizeScript -Undo; Wait-Enter } + '6' { Write-Banner 'Create restore point'; New-CleanupRestorePoint | Out-Null; Wait-Enter } + '7' { Write-Banner 'Tasks, tweaks & checks'; Show-TaskList; Show-TweakList; Show-CheckList; Wait-Enter } + '0' { Write-Host ''; Write-Host ' Bye.' -ForegroundColor Cyan; return } + 'q' { return } + default { } + } + } +} + +Show-MainMenu diff --git a/docs/design/2026-06-23-optimization-suite-design.md b/docs/design/2026-06-23-optimization-suite-design.md new file mode 100644 index 0000000..ece95cb --- /dev/null +++ b/docs/design/2026-06-23-optimization-suite-design.md @@ -0,0 +1,92 @@ +# Optimization suite — design + +Adds a Windows **optimization** engine and a single interactive **menu** front door to the +existing cleanup engine. One command opens a menu; from there the user reaches a detailed +cleanup screen, an optimization screen, undo, restore point, and reports. + +## Goals + +- One entry point: `.\WinSenior.ps1` → menu → cleanup / optimization / undo / reports. +- Optimization covers four areas: Performance, Privacy/telemetry, Debloat (UWP), Network/games. +- Every tweak is **reversible**: a per-tweak backup manifest is written before applying, and + `-Undo` restores prior state. A real restore point is created first as a second safety net. +- Same proven architecture as cleanup: declarative registry, real `-WhatIf` via + `SupportsShouldProcess`, risk tiers, JSON report, standalone-runnable for automation. + +## Files + +| File | Role | +|------|------| +| `Cleanup-Windows-Senior.ps1` | Existing cleanup engine. Unchanged; already dot-source-safe. | +| `Optimize-Windows-Senior.ps1` | New optimization engine: tweak registry + apply / preview / **undo**. Standalone-runnable. | +| `WinSenior.ps1` | New interactive menu. Borrows both registries for display; runs engines as child invocations. | + +The menu reuses each engine by invoking the script file with parameters (`& $engine @params`), +so execution always goes through the engine's own tested parameter binding and scope. An exact +per-task / per-tweak selection is realized with `-Include ` + `-Exclude `. + +## Optimization engine + +Each tweak is one declarative record (`Id, Name, Area, Risk, DefaultOn, Type, Spec`). Types: + +- **Registry** — `Path` + one or more `Values` (`Name/Kind/Value`). Generic backup/apply/undo. +- **Service** — `Service` + desired `Startup` (Disabled/Manual). Backup captures StartType+Status. +- **ScheduledTask** — disable a list of tasks. Backup captures each task's State. +- **Custom** — `Test` / `Backup` / `Apply` / `Undo` scriptblocks for Appx removal, power scheme, + hibernation, and Nagle (multi-interface). Backup output is plain data; undo is looked up by Id + in the freshly loaded registry, so it round-trips through the JSON manifest. + +### Backup & undo + +Before each change the engine snapshots current state into +`%ProgramData%\WinSenior\backups\optimize-backup-.json`. `-Undo` reads the newest +(or a named) manifest and reverts each tweak in reverse order. Registry/Service/ScheduledTask +restore generically from the snapshot; Custom tweaks are reverted by their `Undo` scriptblock. +Appx removal is **partially irreversible** — undo re-provisions where the package source is still +present, otherwise the manifest lists what to reinstall from the Store. + +### Risk tiers & defaults + +Mirror cleanup: Safe + Moderate + Aggressive selectable by default, Dangerous behind +`-IncludeDangerous`, `-Conservative` caps at Moderate. Debatable tweaks (SysMain/Windows Search +off, Ultimate power plan, Xbox/comms debloat, Nagle, NDU) ship `DefaultOn = $false` — allowed by +tier but off until the user toggles or `-Include`s them. + +### Safety boundaries + +Does not touch Defender real-time protection, does not break Windows Update or the network stack +wholesale, does not remove Edge/Store. A restore point precedes any apply unless `-NoRestorePoint` +or `-WhatIf`. + +### Parameters + +`-Area`, `-Include`, `-Exclude`, `-IncludeDangerous`, `-Conservative`, `-Undo`, +`-BackupManifest`, `-WhatIf`/`-DryRun`, `-Unattended`/`-Force`, `-NoRestorePoint`, `-BackupDir`, +`-LogPath`, `-ReportPath`, `-ListTweaks`, `-Help`. + +## Menu (`WinSenior.ps1`) + +Numbered navigation (robust across conhost / Windows Terminal, PS 5.1 and 7). Requires admin; +offers elevation if not. Screens: detailed cleanup (toggle categories/tasks → preview/run), +optimization (pick area → toggle tweaks showing applied/not state → preview/apply/undo), full +run, undo optimizations, create restore point, reports/list. + +## Tweak coverage (~28) + +- **Performance** — visual effects → best performance, MenuShowDelay 0, startup delay 0, + High-Performance power plan; (off by default) Ultimate plan, SysMain off, Windows Search off, + hibernation off, background apps off. +- **Privacy** — telemetry policy 0, advertising ID off, consumer features off, tips/suggestions + off, activity feed off, web search in Start off, Cortana policy off; DiagTrack + dmwappushservice + disabled; CEIP/telemetry scheduled tasks disabled. +- **Debloat** — curated junk UWP removal (default on); Xbox apps, comms apps (off by default); + Start-menu app suggestions off. +- **Network/games** — GameDVR off, Game Mode on, network throttling off / SystemResponsiveness 0; + (off by default) Nagle off, NDU service off. + +## Tests + +Pester 5 on pure logic: tweak-registry integrity (unique ids, known areas/risks/types), +`Resolve-TweakSelection` (defaults, tiers, include/exclude), and a registry backup→apply→undo +round-trip against a throwaway `HKCU:\Software\WinSeniorTest` hive. The menu stays thin; logic +lives in tested functions. CI runs on windows-latest. diff --git a/docs/design/architecture.md b/docs/design/architecture.md index 353fb3f..0cdb311 100644 --- a/docs/design/architecture.md +++ b/docs/design/architecture.md @@ -1,7 +1,17 @@ # Architecture -The tool is a single-file PowerShell engine driven by a declarative task registry, plus a -batch script that mirrors the same defaults for dependency-free environments. +The tool is three single-file PowerShell engines driven by declarative registries — one for +cleanup, one for optimization, one for troubleshooting — plus an interactive menu that ties +them together and a batch script that mirrors the cleanup defaults for dependency-free +environments. + +## Menu front door + +`WinSenior.ps1` is the single entry point. It self-elevates, dot-sources all three engines as +libraries (their `InvocationName -ne '.'` entry guards keep them from auto-running), and +presents numbered screens for cleanup, optimization, troubleshooting, undo, restore point and +listings. It executes by invoking each engine as a child call (`& $engine @params`); an exact +toggled selection is reproduced with `-Include ` + `-Exclude `. ## Task registry + engine @@ -45,6 +55,33 @@ levels — a backstop against a malformed registry entry or an unexpanded variab Browsers, DevTools, Apps, Games, System, Disks, Logs, Updates, Optimization (57 tasks total; `-ListTasks` prints the live list). +## Optimization engine +`Optimize-Windows-Senior.ps1` changes Windows settings instead of deleting files. Each tweak +is one record (`Id, Name, Area, Risk, DefaultOn, Type, Spec`). Types: `Registry` (path + one or +more values), `Service` (desired startup type), `ScheduledTask` (disable a list), and `Custom` +(Test/Backup/Apply/Undo scriptblocks for Appx removal, power scheme, hibernation, Nagle). + +Before each change the engine snapshots prior state and, after the run, writes a JSON backup +manifest to `%ProgramData%\WinSenior\backups`. `-Undo` reads the newest (or a named) manifest +and reverts each tweak in reverse order — generically for Registry/Service/ScheduledTask, and via +the tweak's own `Undo` scriptblock (looked up by Id in the live registry) for Custom. Selection +(`Resolve-TweakSelection`) and tiers mirror cleanup; debatable tweaks ship `DefaultOn = $false`. +Areas: Performance, Privacy, Debloat, Network (29 tweaks; `-ListTweaks` prints the live list). +It never touches Defender real-time protection, Windows Update, the network stack wholesale, or +Edge/Store. + +## Troubleshooting engine +`Repair-Windows-Senior.ps1` diagnoses and repairs system health. Each check is one record +(`Id, Name, Category, Scan, Fix, FixRisk, FixLabel, Reboot`). `Scan` is read-only and returns +`@{ Status = 'OK'|'Warn'|'Fail'; Detail }`; `Fix` is optional. The flow is scan-then-choose: +`Invoke-Scan` runs every selected check, `Show-ScanReport` prints the grouped health report, +then fixable issues (Warn/Fail with a Fix) are offered — interactively, or auto-applied with +`-FixAll` (Safe+Moderate, plus Aggressive under `-IncludeHeavy`). `Invoke-Fix` applies through +`ShouldProcess` after a restore point. 13 checks across Integrity, Disk, Update, Network, +Devices, Services, Security and System. Image health uses `Repair-WindowsImage -CheckHealth` +(locale-independent `ImageHealthState`) rather than parsing DISM text. Repairs only improve +health — the engine enables Defender, never disables it. + ## Batch script `Cleanup-Windows-Senior.bat` is the dependency-free alternative. It mirrors the engine's defaults via a per-profile helper, independent per-browser flags, ordered Windows Update @@ -52,6 +89,9 @@ service stops, all-local-disk cleanup, a Dangerous tier behind `/IncludeDangerou optional real restore point via `/RestorePoint`. ## Tests -Pester 5 tests (`tests/`) cover the pure logic — selection, the safety guard, `` -expansion, age filtering, formatting, and `-WhatIf` accounting — with destructive paths -validated through `-WhatIf`. CI runs them on `windows-latest`. +Pester 5 tests (`tests/`) cover the pure logic of all three engines — cleanup selection, the +safety guard, `` expansion, age filtering, formatting and `-WhatIf` accounting; the tweak +registry integrity, `Resolve-TweakSelection`, and a registry backup→apply→undo round-trip against +a throwaway `HKCU:\Software\WinSeniorTest` hive; and the troubleshooter's check-registry integrity, +selection, and scan/fix dispatch with synthetic checks. Destructive paths are validated through +`-WhatIf`. CI runs them on `windows-latest`. diff --git a/tests/Optimize-Windows-Senior.Tests.ps1 b/tests/Optimize-Windows-Senior.Tests.ps1 new file mode 100644 index 0000000..ddcf520 --- /dev/null +++ b/tests/Optimize-Windows-Senior.Tests.ps1 @@ -0,0 +1,129 @@ +# Pester tests for the pure logic of Optimize-Windows-Senior.ps1 +# Run: Invoke-Pester -Path .\tests +# Covers tweak-registry integrity, selection, and a registry backup->apply->undo round-trip. + +BeforeAll { + $script:Sut = Join-Path $PSScriptRoot '..\Optimize-Windows-Senior.ps1' + # Dot-sourcing is a no-op for the main flow (entry guard checks InvocationName -eq '.'). + . $script:Sut + $script:Reg = Get-OptimizationTweakRegistry +} + +Describe 'Get-OptimizationTweakRegistry' { + It 'returns a non-empty set of tweaks' { + $script:Reg.Count | Should -BeGreaterThan 20 + } + It 'gives every tweak a unique id' { + ($script:Reg.Id | Sort-Object -Unique).Count | Should -Be $script:Reg.Count + } + It 'only uses known areas' { + $known = 'Performance','Privacy','Debloat','Network' + ($script:Reg | Where-Object { $_.Area -notin $known }) | Should -BeNullOrEmpty + } + It 'only uses known risk tiers' { + $known = 'Safe','Moderate','Aggressive','Dangerous' + ($script:Reg | Where-Object { $_.Risk -notin $known }) | Should -BeNullOrEmpty + } + It 'only uses known tweak types' { + $known = 'Registry','Service','ScheduledTask','Custom' + ($script:Reg | Where-Object { $_.Type -notin $known }) | Should -BeNullOrEmpty + } + It 'gives every tweak a non-empty explanation' { + ($script:Reg | Where-Object { [string]::IsNullOrWhiteSpace($_.Explain) }) | Should -BeNullOrEmpty + } + It 'gives Registry tweaks a path and at least one value' { + foreach ($t in ($script:Reg | Where-Object Type -eq 'Registry')) { + $t.Spec.Path | Should -Not -BeNullOrEmpty + @($t.Spec.Values).Count | Should -BeGreaterThan 0 + } + } + It 'gives Service tweaks a service name and startup type' { + foreach ($t in ($script:Reg | Where-Object Type -eq 'Service')) { + $t.Spec.Service | Should -Not -BeNullOrEmpty + $t.Spec.Startup | Should -BeIn @('Disabled','Manual','Automatic') + } + } + It 'gives Custom tweaks an apply and undo scriptblock' { + foreach ($t in ($script:Reg | Where-Object Type -eq 'Custom')) { + $t.Spec.Apply | Should -BeOfType ([scriptblock]) + $t.Spec.Undo | Should -BeOfType ([scriptblock]) + } + } +} + +Describe 'Resolve-TweakSelection' { + It 'selects Safe and Moderate default-on tweaks out of the box' { + $sel = Resolve-TweakSelection -Registry $script:Reg + @($sel | Where-Object Risk -eq 'Safe').Count | Should -BeGreaterThan 0 + @($sel | Where-Object Risk -eq 'Moderate').Count | Should -BeGreaterThan 0 + } + It 'leaves default-off tweaks out by default' { + $sel = Resolve-TweakSelection -Registry $script:Reg + @($sel | Where-Object Id -eq 'perf-sysmain').Count | Should -Be 0 + } + It 'lets -Include force a default-off tweak on' { + $sel = Resolve-TweakSelection -Registry $script:Reg -Include 'perf-sysmain' + @($sel | Where-Object Id -eq 'perf-sysmain').Count | Should -Be 1 + } + It 'honours -Exclude over the default' { + $sel = Resolve-TweakSelection -Registry $script:Reg -Exclude 'priv-adid' + @($sel | Where-Object Id -eq 'priv-adid').Count | Should -Be 0 + } + It 'lets -Exclude win over -Include' { + $sel = Resolve-TweakSelection -Registry $script:Reg -Include 'priv-adid' -Exclude 'priv-adid' + @($sel | Where-Object Id -eq 'priv-adid').Count | Should -Be 0 + } + It 'limits to the requested area' { + $sel = Resolve-TweakSelection -Registry $script:Reg -Area 'Privacy' + @($sel | Where-Object Area -ne 'Privacy').Count | Should -Be 0 + } +} + +Describe 'Registry value backup / restore' { + BeforeAll { + $script:TestKey = 'HKCU:\Software\WinSeniorTest' + if (Test-Path $script:TestKey) { Remove-Item $script:TestKey -Recurse -Force } + } + AfterAll { + if (Test-Path $script:TestKey) { Remove-Item $script:TestKey -Recurse -Force } + } + It 'restores a previously-absent value by removing it' { + $snap = Get-RegValueSnapshot -Path $script:TestKey -Name 'Foo' + $snap.Existed | Should -BeFalse + Set-RegValue -Path $script:TestKey -Name 'Foo' -Kind DWord -Value 1 + (Get-ItemProperty $script:TestKey -Name Foo).Foo | Should -Be 1 + Restore-RegValue -Path $script:TestKey -Snap $snap + (Get-Item $script:TestKey).GetValueNames() | Should -Not -Contain 'Foo' + } + It 'restores a previously-existing value to its old data' { + Set-RegValue -Path $script:TestKey -Name 'Bar' -Kind DWord -Value 5 + $snap = Get-RegValueSnapshot -Path $script:TestKey -Name 'Bar' + $snap.Existed | Should -BeTrue + $snap.Value | Should -Be 5 + Set-RegValue -Path $script:TestKey -Name 'Bar' -Kind DWord -Value 9 + (Get-ItemProperty $script:TestKey -Name Bar).Bar | Should -Be 9 + Restore-RegValue -Path $script:TestKey -Snap $snap + (Get-ItemProperty $script:TestKey -Name Bar).Bar | Should -Be 5 + } +} + +Describe 'Tweak-level apply / undo (Registry type)' { + BeforeAll { + $script:TestKey2 = 'HKCU:\Software\WinSeniorTest' + if (Test-Path $script:TestKey2) { Remove-Item $script:TestKey2 -Recurse -Force } + } + AfterAll { + if (Test-Path $script:TestKey2) { Remove-Item $script:TestKey2 -Recurse -Force } + } + It 'applies via Set-TweakState and reverts via Restore-Tweak' { + $t = New-RegTweak rt-test 'round-trip test' Performance Safe ` + -Path $script:TestKey2 -Values @((RegVal 'Vfx' DWord 2)) -Explain 'test' + Test-TweakApplied -Tweak $t | Should -BeFalse + $snap = Get-TweakSnapshot -Tweak $t + Set-TweakState -Tweak $t -Snapshot $snap + Test-TweakApplied -Tweak $t | Should -BeTrue + $entry = [pscustomobject]@{ Id = 'rt-test'; Type = 'Registry'; Snapshot = $snap } + Restore-Tweak -Entry $entry -Registry @($t) + Test-TweakApplied -Tweak $t | Should -BeFalse + } +} diff --git a/tests/Repair-Windows-Senior.Tests.ps1 b/tests/Repair-Windows-Senior.Tests.ps1 new file mode 100644 index 0000000..1efd1c8 --- /dev/null +++ b/tests/Repair-Windows-Senior.Tests.ps1 @@ -0,0 +1,77 @@ +# Pester tests for the pure logic of Repair-Windows-Senior.ps1 +# Run: Invoke-Pester -Path .\tests +# Covers the check registry, selection, and scan/fix dispatch using synthetic checks +# (no real DISM / network calls, so the tests are fast and deterministic). + +BeforeAll { + $script:Sut = Join-Path $PSScriptRoot '..\Repair-Windows-Senior.ps1' + # Dot-sourcing is a no-op for the main flow (entry guard checks InvocationName -eq '.'). + . $script:Sut + $script:Reg = Get-DiagnosticCheckRegistry +} + +Describe 'Get-DiagnosticCheckRegistry' { + It 'returns a non-empty set of checks' { + $script:Reg.Count | Should -BeGreaterThan 10 + } + It 'gives every check a unique id' { + ($script:Reg.Id | Sort-Object -Unique).Count | Should -Be $script:Reg.Count + } + It 'only uses known categories' { + $known = 'Integrity','Disk','Update','Network','Devices','Services','Security','System' + ($script:Reg | Where-Object { $_.Category -notin $known }) | Should -BeNullOrEmpty + } + It 'gives every check a Scan scriptblock' { + foreach ($c in $script:Reg) { $c.Scan | Should -BeOfType ([scriptblock]) } + } + It 'gives every fixable check a known FixRisk and a label' { + foreach ($c in ($script:Reg | Where-Object Fix)) { + $c.FixRisk | Should -BeIn @('Safe','Moderate','Aggressive') + $c.FixLabel | Should -Not -BeNullOrEmpty + } + } +} + +Describe 'Resolve-CheckSelection' { + It 'limits to the requested category' { + $sel = Resolve-CheckSelection -Registry $script:Reg -Category 'Disk' + @($sel | Where-Object Category -ne 'Disk').Count | Should -Be 0 + @($sel).Count | Should -BeGreaterThan 0 + } + It 'honours -Exclude' { + $sel = Resolve-CheckSelection -Registry $script:Reg -Exclude 'disk-smart' + @($sel | Where-Object Id -eq 'disk-smart').Count | Should -Be 0 + } + It 'lets -Exclude win over -Include' { + $sel = Resolve-CheckSelection -Registry $script:Reg -Include 'img-health' -Exclude 'img-health' + @($sel | Where-Object Id -eq 'img-health').Count | Should -Be 0 + } +} + +Describe 'Invoke-Scan / Invoke-Fix (synthetic checks)' { + It 'wraps a scan result and flags it as fixable' { + $c = New-DiagnosticCheck syn 'synthetic' Disk -Scan { @{ Status = 'Warn'; Detail = 'd' } } ` + -Fix { } -FixRisk Safe -FixLabel 'x' + $r = Invoke-Scan -Check $c + $r.Status | Should -Be 'Warn' + $r.HasFix | Should -BeTrue + } + It 'turns a throwing scan into Skip' { + $c = New-DiagnosticCheck syn 'synthetic' Disk -Scan { throw 'boom' } + (Invoke-Scan -Check $c).Status | Should -Be 'Skip' + } + It 'reports a report-only check as not fixable' { + $c = New-DiagnosticCheck syn 'synthetic' Disk -Scan { @{ Status = 'Fail'; Detail = 'd' } } + (Invoke-Scan -Check $c).HasFix | Should -BeFalse + } + It 'runs the fix and returns true' { + $c = New-DiagnosticCheck syn 'synthetic' Disk -Scan { @{ Status = 'Warn'; Detail = 'd' } } ` + -Fix { } -FixRisk Safe -FixLabel 'x' + Invoke-Fix -Check $c | Should -BeTrue + } + It 'honours -WhatIf (does not run the fix)' { + $c = New-DiagnosticCheck syn 'synthetic' Disk -Scan { @{ Status = 'Warn'; Detail = 'd' } } ` + -Fix { throw 'should not run under WhatIf' } -FixRisk Safe -FixLabel 'x' + Invoke-Fix -Check $c -WhatIf | Should -BeFalse + } +}