windowsweep 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/AI-INTEGRATION-GUIDE.md +203 -0
  2. package/CHANGELOG.md +124 -0
  3. package/LICENSE +21 -21
  4. package/README.md +88 -49
  5. package/VERSION +1 -1
  6. package/lib/actions.ps1 +212 -209
  7. package/lib/config.ps1 +108 -99
  8. package/lib/constants.ps1 +103 -89
  9. package/lib/fs.ps1 +189 -180
  10. package/lib/log.ps1 +140 -140
  11. package/lib/safety.ps1 +541 -493
  12. package/lib/scan.ps1 +109 -94
  13. package/lib/ui.ps1 +275 -235
  14. package/modules/android_avd.ps1 +36 -36
  15. package/modules/app_caches.ps1 +58 -58
  16. package/modules/browsers.ps1 +33 -33
  17. package/modules/build_tools.ps1 +35 -35
  18. package/modules/crash_trap.ps1 +89 -89
  19. package/modules/disk_usage.ps1 +49 -49
  20. package/modules/docker.ps1 +93 -93
  21. package/modules/editors.ps1 +150 -150
  22. package/modules/globals_audit.ps1 +236 -0
  23. package/modules/health.ps1 +101 -101
  24. package/modules/installed_programs.ps1 +118 -0
  25. package/modules/menu.ps1 +63 -61
  26. package/modules/notify.ps1 +60 -0
  27. package/modules/orphaned_appdata.ps1 +189 -0
  28. package/modules/personal.ps1 +126 -124
  29. package/modules/pkg_managers.ps1 +73 -69
  30. package/modules/projects.ps1 +160 -143
  31. package/modules/release_helpers.ps1 +438 -434
  32. package/modules/reports.ps1 +212 -212
  33. package/modules/runner.ps1 +242 -190
  34. package/modules/self_test_extra.ps1 +537 -86
  35. package/modules/startup_audit.ps1 +126 -0
  36. package/modules/system_admin.ps1 +220 -220
  37. package/modules/test_runners.ps1 +24 -24
  38. package/modules/walkthrough.ps1 +80 -79
  39. package/modules/windows_user.ps1 +118 -112
  40. package/package.json +4 -3
  41. package/windowsweep.ps1 +322 -296
@@ -0,0 +1,126 @@
1
+ # startup_audit.ps1 - section 25: everything that starts with the user or the machine. Report only.
2
+ #
3
+ # The tool shows; the user decides in Task Manager. That non-goal is deliberate: disabling a startup item is
4
+ # a system configuration change, not cleanup, and a wrong one is invisible until the next boot.
5
+
6
+ function Get-StartupApprovedState {
7
+ <# .SYNOPSIS enabled | disabled | unknown from an Explorer StartupApproved value.
8
+ The value is 12 bytes: a DWORD of flags then a FILETIME. Explorer stamps that FILETIME when the item is
9
+ DISABLED and zeroes it when the item is enabled, so a non-zero tail is the reliable signal.
10
+ Measured on Windows 10 19045 (2026-09-04): five items carried a real FILETIME under a leading 0x01, and
11
+ two enabled ones (0x01 and 0x04) carried an all-zero tail - the widely quoted "byte 0 is 02/03" table
12
+ does not hold there, so the tail is read first and 03 / 07 are kept only as a fallback marker.
13
+ A value shorter than 12 bytes is reported as unknown; it is never guessed. #>
14
+ param([byte[]] $Value)
15
+ if ($null -eq $Value -or $Value.Length -lt 12) { return 'unknown' }
16
+ for ($i = 4; $i -lt 12; $i++) { if ($Value[$i] -ne 0) { return 'disabled' } }
17
+ if ($Value[0] -eq 3 -or $Value[0] -eq 7) { return 'disabled' }
18
+ return 'enabled'
19
+ }
20
+
21
+ function Get-StartupApprovedMap {
22
+ <# .SYNOPSIS name -> enabled|disabled|unknown, from the StartupApproved keys Explorer maintains. #>
23
+ $map = @{}
24
+ $keys = @(
25
+ 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run',
26
+ 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run32',
27
+ 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\StartupFolder',
28
+ 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run',
29
+ 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\StartupFolder'
30
+ )
31
+ foreach ($k in $keys) {
32
+ $props = $null
33
+ try { $props = Get-ItemProperty -LiteralPath $k -ErrorAction Stop } catch { continue }
34
+ foreach ($prop in $props.PSObject.Properties) {
35
+ if ($prop.Name.StartsWith('PS')) { continue }
36
+ if (-not ($prop.Value -is [byte[]])) { continue }
37
+ $map[$prop.Name] = Get-StartupApprovedState -Value $prop.Value
38
+ }
39
+ }
40
+ return $map
41
+ }
42
+
43
+ function Get-StartupItems {
44
+ <# .SYNOPSIS Every startup entry the user can see, from all five places Windows keeps them. #>
45
+ $P = $Script:P
46
+ $approved = Get-StartupApprovedMap
47
+ $items = @()
48
+ $runKeys = @(
49
+ @('HKCU Run', 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'),
50
+ @('HKCU RunOnce', 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce'),
51
+ @('HKLM Run', 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'),
52
+ @('HKLM RunOnce', 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce'),
53
+ @('HKLM Run (32)', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run'),
54
+ @('HKLM RunOnce (32)', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\RunOnce')
55
+ )
56
+ foreach ($rk in $runKeys) {
57
+ # NOT $p: PowerShell variable names are case-insensitive, so $p would silently null the $P roots table.
58
+ $props = $null
59
+ try { $props = Get-ItemProperty -LiteralPath $rk[1] -ErrorAction Stop } catch { continue }
60
+ foreach ($prop in $props.PSObject.Properties) {
61
+ if ($prop.Name.StartsWith('PS')) { continue }
62
+ $state = 'enabled'
63
+ if ($approved.ContainsKey($prop.Name)) { $state = $approved[$prop.Name] }
64
+ $items += [pscustomobject]@{ Source = $rk[0]; Name = $prop.Name; State = $state; Command = [string]$prop.Value }
65
+ }
66
+ }
67
+ foreach ($folder in @(@('Startup (user)', (Join-Path $P.A 'Microsoft\Windows\Start Menu\Programs\Startup')), @('Startup (all users)', (Join-Path $P.PD 'Microsoft\Windows\Start Menu\Programs\Startup')))) {
68
+ if (-not (Test-DirPresent $folder[1])) { continue }
69
+ foreach ($e in (Get-ChildEntries $folder[1])) {
70
+ if ($e -is [IO.DirectoryInfo] -or $e.Name -eq 'desktop.ini') { continue }
71
+ $state = 'enabled'
72
+ if ($approved.ContainsKey($e.Name)) { $state = $approved[$e.Name] }
73
+ $items += [pscustomobject]@{ Source = $folder[0]; Name = $e.Name; State = $state; Command = (Remove-LongPrefix $e.FullName) }
74
+ }
75
+ }
76
+ try {
77
+ foreach ($t in (Get-ScheduledTask -ErrorAction Stop)) {
78
+ if (-not (@($t.Triggers) | Where-Object { $_.CimClass.CimClassName -eq 'MSFT_TaskLogonTrigger' })) { continue }
79
+ $state = 'enabled'
80
+ if ($t.State -eq 'Disabled') { $state = 'disabled' }
81
+ $cmd = ''
82
+ foreach ($a in @($t.Actions)) { if ($a.Execute) { $cmd = [string]$a.Execute } }
83
+ $items += [pscustomobject]@{ Source = 'Scheduled task (logon)'; Name = $t.TaskName; State = $state; Command = $cmd }
84
+ }
85
+ } catch { $null = $_ }
86
+ try {
87
+ foreach ($c in (Get-CimInstance Win32_StartupCommand -ErrorAction Stop)) {
88
+ if (@($items | Where-Object { $_.Name -eq $c.Name }).Count -gt 0) { continue }
89
+ $state = 'enabled'
90
+ if ($approved.ContainsKey([string]$c.Name)) { $state = $approved[[string]$c.Name] }
91
+ $items += [pscustomobject]@{ Source = "WMI ($($c.Location))"; Name = [string]$c.Name; State = $state; Command = [string]$c.Command }
92
+ }
93
+ } catch { $null = $_ }
94
+ return @($items | Sort-Object Source, Name)
95
+ }
96
+
97
+ function Get-Targets25 {
98
+ <# .SYNOPSIS Informational only. Section 25 changes nothing: it never disables or removes a startup item. #>
99
+ return @((New-Target 25 'Startup items (registry, folders, tasks)' 'Run keys + Startup folders + logon tasks' -Kind cmd -Note 'report only; change them yourself in Task Manager > Startup'))
100
+ }
101
+
102
+ function Invoke-Section25 {
103
+ $ws = $Script:WS
104
+ Write-SectionIntro @(
105
+ 'Everything that launches when you sign in or the machine boots: Run and RunOnce keys, both Startup',
106
+ 'folders, scheduled tasks with a logon trigger, and what Explorer records as enabled or disabled.',
107
+ 'This section only shows them - change them in Task Manager > Startup, where you can undo it.'
108
+ )
109
+ $items = @(Get-StartupItems)
110
+ if ($items.Count -eq 0) { Write-Info 'no startup items found'; return }
111
+ Write-UiLine (" {0,-22} {1,-30} {2,-9} {3}" -f 'SOURCE', 'NAME', 'STATE', 'COMMAND') 'White'
112
+ $lines = @("startup items - $(Get-Date -Format 'yyyy-MM-dd HH:mm')", '')
113
+ foreach ($it in $items) {
114
+ $color = 'Gray'
115
+ if ($it.State -eq 'disabled') { $color = 'DarkGray' }
116
+ $line = " {0,-22} {1,-30} {2,-9} {3}" -f $it.Source, $it.Name, $it.State, $it.Command
117
+ Write-UiLine $line $color
118
+ $lines += $line.Trim()
119
+ }
120
+ $on = @($items | Where-Object { $_.State -eq 'enabled' }).Count
121
+ Write-Info "$($items.Count) startup item(s), $on enabled"
122
+ if (-not $ws.NoReport) {
123
+ $out = Join-Path $ws.ReportsDir ('startup-items-' + $ws.Stamp + '.txt')
124
+ try { [IO.File]::WriteAllLines($out, $lines); Write-Note "list saved: $out" } catch { $null = $_ }
125
+ }
126
+ }
@@ -1,220 +1,220 @@
1
- # system_admin.ps1 - sections 12-16: Windows Update cache, Disk Cleanup engine, DISM, hibernation file, event logs.
2
- # Every section here needs Administrator rights; the runner skips them (with the --elevate hint) when not elevated.
3
-
4
- function Get-Targets12 {
5
- $SR = $Script:P.SR; $PD = $Script:P.PD
6
- return @(
7
- (New-Target 12 'Windows Update download cache' "$SR\SoftwareDistribution\Download" -Mode clear -Note 'wuauserv and bits are stopped for the wipe and restarted')
8
- (New-Target 12 'Delivery Optimization cache' 'Delete-DeliveryOptimizationCache -Force' -Kind cmd)
9
- (New-Target 12 'Windows temp' "$SR\Temp" -Mode prune -Days $Script:WS.TempDays)
10
- (New-Target 12 'LocalService temp' "$SR\ServiceProfiles\LocalService\AppData\Local\Temp" -Mode prune -Days $Script:WS.TempDays)
11
- (New-Target 12 'NetworkService temp' "$SR\ServiceProfiles\NetworkService\AppData\Local\Temp" -Mode prune -Days $Script:WS.TempDays)
12
- (New-Target 12 'CBS servicing logs' "$SR\Logs\CBS" -Mode prune -Days 30 -Note 'the live CBS.log is open and skipped')
13
- (New-Target 12 'DISM logs' "$SR\Logs\DISM" -Mode prune -Days 30)
14
- (New-Target 12 'Windows Update logs' "$SR\Logs\WindowsUpdate" -Mode prune -Days 30)
15
- (New-Target 12 'Live kernel reports' "$SR\LiveKernelReports" -Mode clear-old -Days 7)
16
- (New-Target 12 'Windows Error Reporting queue (system)' "$PD\Microsoft\Windows\WER\ReportQueue" -Mode clear)
17
- (New-Target 12 'Windows Error Reporting archive (system)' "$PD\Microsoft\Windows\WER\ReportArchive" -Mode clear)
18
- (New-Target 12 'Windows Error Reporting temp (system)' "$PD\Microsoft\Windows\WER\Temp" -Mode clear)
19
- )
20
- }
21
-
22
- function Set-ServiceState {
23
- <# .SYNOPSIS Stop or start a service through the dry-run-aware wrapper. Returns $true when it was running before a stop. #>
24
- param([string] $Name, [string] $Action)
25
- $svc = Get-Service -Name $Name -ErrorAction SilentlyContinue
26
- if (-not $svc) { return $false }
27
- if ($Action -eq 'stop') {
28
- if ($svc.Status -ne 'Running') { return $false }
29
- if ($Script:WS.DryRun) { Write-DryRun "would stop service $Name"; return $true }
30
- try { Stop-Service -Name $Name -Force -ErrorAction Stop; Write-Note "stopped $Name" } catch { Write-Warn "could not stop $Name : $($_.Exception.Message)" }
31
- return $true
32
- }
33
- if ($Script:WS.DryRun) { Write-DryRun "would start service $Name"; return $true }
34
- try { Start-Service -Name $Name -ErrorAction Stop; Write-Note "started $Name" } catch { Write-Warn "could not start $Name : $($_.Exception.Message)" }
35
- return $true
36
- }
37
-
38
- function Invoke-Section12 {
39
- $ws = $Script:WS
40
- Write-SectionIntro @(
41
- 'Windows Update keeps every downloaded package after installing it; Delivery Optimization keeps peer-share',
42
- 'copies; C:\Windows\Temp and the servicing logs grow forever. All of it is rebuilt on demand.'
43
- )
44
- if (-not $ws.IsAdmin) { Write-Warn 'needs Administrator - skipped'; return }
45
- $targets = Get-Targets12
46
- $null = Show-TargetSizes $targets
47
- if (-not (Confirm-Section 'Clear Windows Update and system temp caches now?')) { Write-Info 'skipped'; return }
48
- $wuWasRunning = Set-ServiceState -Name 'wuauserv' -Action 'stop'
49
- $bitsWasRunning = Set-ServiceState -Name 'bits' -Action 'stop'
50
- try {
51
- $null = Invoke-TargetList $targets
52
- } finally {
53
- if ($wuWasRunning) { $null = Set-ServiceState -Name 'wuauserv' -Action 'start' }
54
- if ($bitsWasRunning) { $null = Set-ServiceState -Name 'bits' -Action 'start' }
55
- }
56
- if (Get-Command Delete-DeliveryOptimizationCache -ErrorAction SilentlyContinue) {
57
- if ($ws.DryRun) { Write-DryRun 'would run Delete-DeliveryOptimizationCache -Force' }
58
- else {
59
- try { Delete-DeliveryOptimizationCache -Force -ErrorAction Stop; Write-Ok 'Delivery Optimization cache cleared' } catch { Write-Warn "Delivery Optimization: $($_.Exception.Message)" }
60
- }
61
- }
62
- }
63
-
64
- # ---------------------------------------------------------------------------------------------
65
- # Section 13 - Disk Cleanup engine (cleanmgr)
66
- # ---------------------------------------------------------------------------------------------
67
-
68
- # Handlers this tool enables. Deliberately absent: DownloadsFolder (your files), Recycle Bin (section 11 asks
69
- # separately), User file versions (File History), Windows ESD installation files (needed for Reset this PC),
70
- # Language Pack (removes installed languages).
71
- $Script:WS_CLEANMGR_HANDLERS = @(
72
- 'Active Setup Temp Folders', 'BranchCache', 'Content Indexer Cleaner', 'D3D Shader Cache', 'Delivery Optimization Files',
73
- 'Device Driver Packages', 'Diagnostic Data Viewer database files', 'Downloaded Program Files', 'Internet Cache Files',
74
- 'Offline Pages Files', 'Old ChkDsk Files', 'Previous Installations', 'RetailDemo Offline Content', 'Setup Log Files',
75
- 'System error memory dump files', 'System error minidump files', 'Temporary Files', 'Temporary Setup Files',
76
- 'Temporary Sync Files', 'Thumbnail Cache', 'Update Cleanup', 'Upgrade Discarded Files', 'Windows Defender',
77
- 'Windows Error Reporting Files', 'Windows Upgrade Log Files'
78
- )
79
- $Script:WS_CLEANMGR_KEY = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches'
80
- $Script:WS_CLEANMGR_FLAG = 'StateFlags0077'
81
-
82
- function Get-Targets13 {
83
- return @((New-Target 13 'cleanmgr /sagerun:77 with curated handlers' ($Script:WS_CLEANMGR_HANDLERS -join ', ') -Kind cmd -Note 'never Downloads, Recycle Bin, File History, ESD files or language packs'))
84
- }
85
-
86
- function Invoke-Section13 {
87
- $ws = $Script:WS
88
- Write-SectionIntro @(
89
- "Windows' own Disk Cleanup engine, driven with a curated handler list: temporary files, Update Cleanup,",
90
- 'thumbnail cache, memory dumps, setup and upgrade logs, previous Windows installations. It never touches',
91
- 'your Downloads folder, the Recycle Bin, File History or the files Reset-this-PC needs.'
92
- )
93
- if (-not $ws.IsAdmin) { Write-Warn 'needs Administrator - skipped'; return }
94
- if (-not (Test-CommandPresent 'cleanmgr.exe')) { Write-Warn 'cleanmgr.exe not found on this edition of Windows - skipped'; return }
95
- $present = @()
96
- foreach ($h in $Script:WS_CLEANMGR_HANDLERS) { if (Test-Path -LiteralPath (Join-Path $Script:WS_CLEANMGR_KEY $h)) { $present += $h } }
97
- Write-Info "handlers available on this machine: $($present.Count) of $($Script:WS_CLEANMGR_HANDLERS.Count)"
98
- foreach ($h in $present) { Write-Note $h }
99
- if ($ws.DryRun) { Write-DryRun 'would run cleanmgr.exe /sagerun:77 with those handlers (cleanmgr cannot preview sizes)'; return }
100
- if (-not (Confirm-Section 'Run Disk Cleanup with these handlers now? (a progress window may appear; it can take several minutes)')) { Write-Info 'skipped'; return }
101
- $before = Get-SystemDriveFree
102
- try {
103
- foreach ($h in $present) { Set-ItemProperty -LiteralPath (Join-Path $Script:WS_CLEANMGR_KEY $h) -Name $Script:WS_CLEANMGR_FLAG -Value 2 -Type DWord -ErrorAction Stop }
104
- $r = Invoke-External -FilePath 'cleanmgr.exe' -ArgumentList @('/sagerun:77') -Destructive -Quiet -Label 'cleanmgr /sagerun:77'
105
- if ($r.ExitCode -ne 0) { Write-Warn "cleanmgr returned $($r.ExitCode)" }
106
- } catch {
107
- Write-Err "could not configure Disk Cleanup: $($_.Exception.Message)"
108
- } finally {
109
- foreach ($h in $present) { Remove-ItemProperty -LiteralPath (Join-Path $Script:WS_CLEANMGR_KEY $h) -Name $Script:WS_CLEANMGR_FLAG -ErrorAction SilentlyContinue }
110
- }
111
- $gain = [math]::Max([long]0, (Get-SystemDriveFree) - $before)
112
- Add-Freed $gain
113
- Write-Ok ("Disk Cleanup finished - system drive gained " + (Format-Bytes $gain))
114
- }
115
-
116
- # ---------------------------------------------------------------------------------------------
117
- # Section 14 - DISM component store
118
- # ---------------------------------------------------------------------------------------------
119
-
120
- function Get-Targets14 {
121
- $note = 'superseded component versions in WinSxS'
122
- if ($Script:WS.ResetBase) { $note += '; --reset-base also removes the ability to uninstall installed updates' }
123
- return @((New-Target 14 'Dism /Online /Cleanup-Image /StartComponentCleanup' "$($Script:P.SR)\WinSxS" -Kind cmd -Note $note))
124
- }
125
-
126
- function Invoke-Section14 {
127
- $ws = $Script:WS
128
- Write-SectionIntro @(
129
- 'The component store (WinSxS) keeps superseded versions of Windows components. DISM removes the ones no',
130
- 'installed update needs. It is slow (5-20 minutes) and safe; /ResetBase (opt-in) also drops the ability to',
131
- 'uninstall currently installed updates.'
132
- )
133
- if (-not $ws.IsAdmin) { Write-Warn 'needs Administrator - skipped'; return }
134
- if (-not (Test-CommandPresent 'Dism.exe')) { Write-Warn 'Dism.exe not found - skipped'; return }
135
- Write-Info 'analyzing the component store (read-only, about a minute)...'
136
- $a = Invoke-External -FilePath 'Dism.exe' -ArgumentList @('/Online', '/Cleanup-Image', '/AnalyzeComponentStore') -Quiet -Label 'dism analyze'
137
- foreach ($l in $a.Output) { if ($l -match 'Size|Cleanup|Reclaimable|Date' ) { Write-Note ($l.Trim()) } }
138
- $dismArgs = @('/Online', '/Cleanup-Image', '/StartComponentCleanup')
139
- if ($ws.ResetBase) { $dismArgs += '/ResetBase'; Write-Warn '/ResetBase requested: installed updates can no longer be uninstalled afterwards' }
140
- if (-not (Confirm-Section 'Run the component store cleanup now?')) { Write-Info 'skipped'; return }
141
- $before = Get-SystemDriveFree
142
- $r = Invoke-External -FilePath 'Dism.exe' -ArgumentList $dismArgs -Destructive -Quiet -Label 'dism StartComponentCleanup'
143
- if ($r.Ran) {
144
- $gain = [math]::Max([long]0, (Get-SystemDriveFree) - $before)
145
- Add-Freed $gain
146
- if ($r.ExitCode -eq 0) { Write-Ok ("component store cleanup finished - system drive gained " + (Format-Bytes $gain)) } else { Write-Warn "DISM returned $($r.ExitCode) - see the log" }
147
- }
148
- }
149
-
150
- # ---------------------------------------------------------------------------------------------
151
- # Section 15 - hibernation file
152
- # ---------------------------------------------------------------------------------------------
153
-
154
- function Get-Targets15 {
155
- return @((New-Target 15 'hiberfil.sys (powercfg /hibernate off | /type reduced)' (Join-Path $Script:P.SD '\hiberfil.sys') -Kind cmd -Note 'off frees the whole file; reduced keeps Fast Startup at roughly 40% of RAM'))
156
- }
157
-
158
- function Invoke-Section15 {
159
- $ws = $Script:WS
160
- $h = Get-HiberfilInfo
161
- Write-SectionIntro @(
162
- 'hiberfil.sys holds a copy of RAM for Hibernate and Fast Startup, so it is about 40% of your RAM - permanently.',
163
- ' off removes it. Sleep still works; Hibernate, Fast Startup and hibernate-on-critical-battery do not.',
164
- ' reduced keeps Fast Startup only, at a smaller file (Windows decides the size, usually about half).',
165
- 'Running speed is unaffected either way; this is purely disk space.'
166
- )
167
- if ($h.Bytes -gt 0) { Write-Kv 'hiberfil.sys now:' ("{0} (hibernation {1})" -f (Format-Bytes $h.Bytes), $h.State) } else { Write-Info 'hiberfil.sys is absent - hibernation is already off'; return }
168
- if (-not $ws.IsAdmin) { Write-Warn 'needs Administrator - skipped'; return }
169
- $choice = $ws.Hiberfil
170
- if (-not $choice) {
171
- if (-not $ws.Interactive) { Write-Warn 'pass --hiberfil off|reduced|keep to run this section unattended'; return }
172
- $choice = Read-Choice -Prompt ' [o] turn hibernation off [r] reduced (keep Fast Startup) [k] keep as is > ' -Default 'k'
173
- switch ($choice) { 'o' { $choice = 'off' } 'r' { $choice = 'reduced' } default { $choice = 'keep' } }
174
- }
175
- if ($choice -eq 'keep') { Write-Info 'left unchanged'; return }
176
- $pcArgs = @('/hibernate', 'off')
177
- if ($choice -eq 'reduced') { $pcArgs = @('/hibernate', '/type', 'reduced') }
178
- if ($ws.DryRun) { Write-DryRun ("would run powercfg $($pcArgs -join ' ') (frees up to " + (Format-Bytes $h.Bytes) + ')'); Add-Freed $h.Bytes; return }
179
- if (-not (Confirm-Ui -Prompt "Run powercfg $($pcArgs -join ' ') now?" -Default 'n')) { Write-Info 'skipped'; return }
180
- $r = Invoke-External -FilePath 'powercfg.exe' -ArgumentList $pcArgs -Destructive -Quiet -Label "powercfg $($pcArgs -join ' ')"
181
- Start-Sleep -Seconds 2
182
- $after = Get-HiberfilInfo
183
- $gain = [math]::Max([long]0, $h.Bytes - $after.Bytes)
184
- Add-Freed $gain
185
- if ($r.ExitCode -eq 0) { Write-Ok ("hibernation set to '$choice' - hiberfil.sys " + (Format-Bytes $h.Bytes) + ' -> ' + (Format-Bytes $after.Bytes) + ' (freed ' + (Format-Bytes $gain) + ')') } else { Write-Warn "powercfg returned $($r.ExitCode)" }
186
- }
187
-
188
- # ---------------------------------------------------------------------------------------------
189
- # Section 16 - event logs
190
- # ---------------------------------------------------------------------------------------------
191
-
192
- function Get-Targets16 {
193
- return @((New-Target 16 'Windows Event Logs (wevtutil cl for every log)' "$($Script:P.SR)\System32\winevt\Logs" -Kind cmd -Note 'PERMANENT - troubleshooting history is lost'))
194
- }
195
-
196
- function Invoke-Section16 {
197
- $ws = $Script:WS
198
- $logDir = Join-Path $Script:P.SR 'System32\winevt\Logs'
199
- Write-SectionIntro @(
200
- 'Clears every Windows Event Log (Application, System, Security and the hundreds of per-component logs).',
201
- 'This is permanent and removes the history a technician would use to diagnose a problem. Only do this on',
202
- 'a machine that is behaving normally.'
203
- )
204
- if (-not $ws.IsAdmin) { Write-Warn 'needs Administrator - skipped'; return }
205
- $before = Get-DirectoryBytes $logDir
206
- Write-Kv 'event log store:' (Format-Bytes $before)
207
- if ($ws.DryRun) { Write-DryRun ("would clear every event log (about " + (Format-Bytes $before) + ')'); Add-Freed $before; return }
208
- if (-not (Confirm-Ui -Prompt 'Permanently clear ALL Windows Event Logs?' -Default 'n')) { Write-Info 'skipped'; return }
209
- $list = Invoke-External -FilePath 'wevtutil.exe' -ArgumentList @('el') -Quiet -Label 'wevtutil el'
210
- $ok = 0; $failed = 0
211
- foreach ($name in $list.Output) {
212
- $n = ([string]$name).Trim()
213
- if (-not $n) { continue }
214
- $r = Invoke-External -FilePath 'wevtutil.exe' -ArgumentList @('cl', $n) -Destructive -Quiet -Label "wevtutil cl $n"
215
- if ($r.ExitCode -eq 0) { $ok++ } else { $failed++ }
216
- }
217
- $gain = [math]::Max([long]0, $before - (Get-DirectoryBytes $logDir))
218
- Add-Freed $gain
219
- Write-Ok ("cleared $ok logs ($failed could not be cleared - analytic/debug channels), freed " + (Format-Bytes $gain))
220
- }
1
+ # system_admin.ps1 - sections 12-16: Windows Update cache, Disk Cleanup engine, DISM, hibernation file, event logs.
2
+ # Every section here needs Administrator rights; the runner skips them (with the --elevate hint) when not elevated.
3
+
4
+ function Get-Targets12 {
5
+ $SR = $Script:P.SR; $PD = $Script:P.PD
6
+ return @(
7
+ (New-Target 12 'Windows Update download cache' "$SR\SoftwareDistribution\Download" -Mode clear -Note 'wuauserv and bits are stopped for the wipe and restarted')
8
+ (New-Target 12 'Delivery Optimization cache' 'Delete-DeliveryOptimizationCache -Force' -Kind cmd)
9
+ (New-Target 12 'Windows temp' "$SR\Temp" -Mode prune -Days $Script:WS.TempDays)
10
+ (New-Target 12 'LocalService temp' "$SR\ServiceProfiles\LocalService\AppData\Local\Temp" -Mode prune -Days $Script:WS.TempDays)
11
+ (New-Target 12 'NetworkService temp' "$SR\ServiceProfiles\NetworkService\AppData\Local\Temp" -Mode prune -Days $Script:WS.TempDays)
12
+ (New-Target 12 'CBS servicing logs' "$SR\Logs\CBS" -Mode prune -Days 30 -Note 'the live CBS.log is open and skipped')
13
+ (New-Target 12 'DISM logs' "$SR\Logs\DISM" -Mode prune -Days 30)
14
+ (New-Target 12 'Windows Update logs' "$SR\Logs\WindowsUpdate" -Mode prune -Days 30)
15
+ (New-Target 12 'Live kernel reports' "$SR\LiveKernelReports" -Mode clear-old -Days 7)
16
+ (New-Target 12 'Windows Error Reporting queue (system)' "$PD\Microsoft\Windows\WER\ReportQueue" -Mode clear)
17
+ (New-Target 12 'Windows Error Reporting archive (system)' "$PD\Microsoft\Windows\WER\ReportArchive" -Mode clear)
18
+ (New-Target 12 'Windows Error Reporting temp (system)' "$PD\Microsoft\Windows\WER\Temp" -Mode clear)
19
+ )
20
+ }
21
+
22
+ function Set-ServiceState {
23
+ <# .SYNOPSIS Stop or start a service through the dry-run-aware wrapper. Returns $true when it was running before a stop. #>
24
+ param([string] $Name, [string] $Action)
25
+ $svc = Get-Service -Name $Name -ErrorAction SilentlyContinue
26
+ if (-not $svc) { return $false }
27
+ if ($Action -eq 'stop') {
28
+ if ($svc.Status -ne 'Running') { return $false }
29
+ if ($Script:WS.DryRun) { Write-DryRun "would stop service $Name"; return $true }
30
+ try { Stop-Service -Name $Name -Force -ErrorAction Stop; Write-Note "stopped $Name" } catch { Write-Warn "could not stop $Name : $($_.Exception.Message)" }
31
+ return $true
32
+ }
33
+ if ($Script:WS.DryRun) { Write-DryRun "would start service $Name"; return $true }
34
+ try { Start-Service -Name $Name -ErrorAction Stop; Write-Note "started $Name" } catch { Write-Warn "could not start $Name : $($_.Exception.Message)" }
35
+ return $true
36
+ }
37
+
38
+ function Invoke-Section12 {
39
+ $ws = $Script:WS
40
+ Write-SectionIntro @(
41
+ 'Windows Update keeps every downloaded package after installing it; Delivery Optimization keeps peer-share',
42
+ 'copies; C:\Windows\Temp and the servicing logs grow forever. All of it is rebuilt on demand.'
43
+ )
44
+ if (-not $ws.IsAdmin) { Write-Warn 'needs Administrator - skipped'; return }
45
+ $targets = Get-Targets12
46
+ $null = Show-TargetSizes $targets
47
+ if (-not (Confirm-Section 'Clear Windows Update and system temp caches now?')) { Write-Info 'skipped'; return }
48
+ $wuWasRunning = Set-ServiceState -Name 'wuauserv' -Action 'stop'
49
+ $bitsWasRunning = Set-ServiceState -Name 'bits' -Action 'stop'
50
+ try {
51
+ $null = Invoke-TargetList $targets
52
+ } finally {
53
+ if ($wuWasRunning) { $null = Set-ServiceState -Name 'wuauserv' -Action 'start' }
54
+ if ($bitsWasRunning) { $null = Set-ServiceState -Name 'bits' -Action 'start' }
55
+ }
56
+ if (Get-Command Delete-DeliveryOptimizationCache -ErrorAction SilentlyContinue) {
57
+ if ($ws.DryRun) { Write-DryRun 'would run Delete-DeliveryOptimizationCache -Force' }
58
+ else {
59
+ try { Delete-DeliveryOptimizationCache -Force -ErrorAction Stop; Write-Ok 'Delivery Optimization cache cleared' } catch { Write-Warn "Delivery Optimization: $($_.Exception.Message)" }
60
+ }
61
+ }
62
+ }
63
+
64
+ # ---------------------------------------------------------------------------------------------
65
+ # Section 13 - Disk Cleanup engine (cleanmgr)
66
+ # ---------------------------------------------------------------------------------------------
67
+
68
+ # Handlers this tool enables. Deliberately absent: DownloadsFolder (your files), Recycle Bin (section 11 asks
69
+ # separately), User file versions (File History), Windows ESD installation files (needed for Reset this PC),
70
+ # Language Pack (removes installed languages).
71
+ $Script:WS_CLEANMGR_HANDLERS = @(
72
+ 'Active Setup Temp Folders', 'BranchCache', 'Content Indexer Cleaner', 'D3D Shader Cache', 'Delivery Optimization Files',
73
+ 'Device Driver Packages', 'Diagnostic Data Viewer database files', 'Downloaded Program Files', 'Internet Cache Files',
74
+ 'Offline Pages Files', 'Old ChkDsk Files', 'Previous Installations', 'RetailDemo Offline Content', 'Setup Log Files',
75
+ 'System error memory dump files', 'System error minidump files', 'Temporary Files', 'Temporary Setup Files',
76
+ 'Temporary Sync Files', 'Thumbnail Cache', 'Update Cleanup', 'Upgrade Discarded Files', 'Windows Defender',
77
+ 'Windows Error Reporting Files', 'Windows Upgrade Log Files'
78
+ )
79
+ $Script:WS_CLEANMGR_KEY = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches'
80
+ $Script:WS_CLEANMGR_FLAG = 'StateFlags0077'
81
+
82
+ function Get-Targets13 {
83
+ return @((New-Target 13 'cleanmgr /sagerun:77 with curated handlers' ($Script:WS_CLEANMGR_HANDLERS -join ', ') -Kind cmd -Note 'never Downloads, Recycle Bin, File History, ESD files or language packs'))
84
+ }
85
+
86
+ function Invoke-Section13 {
87
+ $ws = $Script:WS
88
+ Write-SectionIntro @(
89
+ "Windows' own Disk Cleanup engine, driven with a curated handler list: temporary files, Update Cleanup,",
90
+ 'thumbnail cache, memory dumps, setup and upgrade logs, previous Windows installations. It never touches',
91
+ 'your Downloads folder, the Recycle Bin, File History or the files Reset-this-PC needs.'
92
+ )
93
+ if (-not $ws.IsAdmin) { Write-Warn 'needs Administrator - skipped'; return }
94
+ if (-not (Test-CommandPresent 'cleanmgr.exe')) { Write-Warn 'cleanmgr.exe not found on this edition of Windows - skipped'; return }
95
+ $present = @()
96
+ foreach ($h in $Script:WS_CLEANMGR_HANDLERS) { if (Test-Path -LiteralPath (Join-Path $Script:WS_CLEANMGR_KEY $h)) { $present += $h } }
97
+ Write-Info "handlers available on this machine: $($present.Count) of $($Script:WS_CLEANMGR_HANDLERS.Count)"
98
+ foreach ($h in $present) { Write-Note $h }
99
+ if ($ws.DryRun) { Write-DryRun 'would run cleanmgr.exe /sagerun:77 with those handlers (cleanmgr cannot preview sizes)'; return }
100
+ if (-not (Confirm-Section 'Run Disk Cleanup with these handlers now? (a progress window may appear; it can take several minutes)')) { Write-Info 'skipped'; return }
101
+ $before = Get-SystemDriveFree
102
+ try {
103
+ foreach ($h in $present) { Set-ItemProperty -LiteralPath (Join-Path $Script:WS_CLEANMGR_KEY $h) -Name $Script:WS_CLEANMGR_FLAG -Value 2 -Type DWord -ErrorAction Stop }
104
+ $r = Invoke-External -FilePath 'cleanmgr.exe' -ArgumentList @('/sagerun:77') -Destructive -Quiet -Label 'cleanmgr /sagerun:77'
105
+ if ($r.ExitCode -ne 0) { Write-Warn "cleanmgr returned $($r.ExitCode)" }
106
+ } catch {
107
+ Write-Err "could not configure Disk Cleanup: $($_.Exception.Message)"
108
+ } finally {
109
+ foreach ($h in $present) { Remove-ItemProperty -LiteralPath (Join-Path $Script:WS_CLEANMGR_KEY $h) -Name $Script:WS_CLEANMGR_FLAG -ErrorAction SilentlyContinue }
110
+ }
111
+ $gain = [math]::Max([long]0, (Get-SystemDriveFree) - $before)
112
+ Add-Freed $gain
113
+ Write-Ok ("Disk Cleanup finished - system drive gained " + (Format-Bytes $gain))
114
+ }
115
+
116
+ # ---------------------------------------------------------------------------------------------
117
+ # Section 14 - DISM component store
118
+ # ---------------------------------------------------------------------------------------------
119
+
120
+ function Get-Targets14 {
121
+ $note = 'superseded component versions in WinSxS'
122
+ if ($Script:WS.ResetBase) { $note += '; --reset-base also removes the ability to uninstall installed updates' }
123
+ return @((New-Target 14 'Dism /Online /Cleanup-Image /StartComponentCleanup' "$($Script:P.SR)\WinSxS" -Kind cmd -Note $note))
124
+ }
125
+
126
+ function Invoke-Section14 {
127
+ $ws = $Script:WS
128
+ Write-SectionIntro @(
129
+ 'The component store (WinSxS) keeps superseded versions of Windows components. DISM removes the ones no',
130
+ 'installed update needs. It is slow (5-20 minutes) and safe; /ResetBase (opt-in) also drops the ability to',
131
+ 'uninstall currently installed updates.'
132
+ )
133
+ if (-not $ws.IsAdmin) { Write-Warn 'needs Administrator - skipped'; return }
134
+ if (-not (Test-CommandPresent 'Dism.exe')) { Write-Warn 'Dism.exe not found - skipped'; return }
135
+ Write-Info 'analyzing the component store (read-only, about a minute)...'
136
+ $a = Invoke-External -FilePath 'Dism.exe' -ArgumentList @('/Online', '/Cleanup-Image', '/AnalyzeComponentStore') -Quiet -Label 'dism analyze'
137
+ foreach ($l in $a.Output) { if ($l -match 'Size|Cleanup|Reclaimable|Date' ) { Write-Note ($l.Trim()) } }
138
+ $dismArgs = @('/Online', '/Cleanup-Image', '/StartComponentCleanup')
139
+ if ($ws.ResetBase) { $dismArgs += '/ResetBase'; Write-Warn '/ResetBase requested: installed updates can no longer be uninstalled afterwards' }
140
+ if (-not (Confirm-Section 'Run the component store cleanup now?')) { Write-Info 'skipped'; return }
141
+ $before = Get-SystemDriveFree
142
+ $r = Invoke-External -FilePath 'Dism.exe' -ArgumentList $dismArgs -Destructive -Quiet -Label 'dism StartComponentCleanup'
143
+ if ($r.Ran) {
144
+ $gain = [math]::Max([long]0, (Get-SystemDriveFree) - $before)
145
+ Add-Freed $gain
146
+ if ($r.ExitCode -eq 0) { Write-Ok ("component store cleanup finished - system drive gained " + (Format-Bytes $gain)) } else { Write-Warn "DISM returned $($r.ExitCode) - see the log" }
147
+ }
148
+ }
149
+
150
+ # ---------------------------------------------------------------------------------------------
151
+ # Section 15 - hibernation file
152
+ # ---------------------------------------------------------------------------------------------
153
+
154
+ function Get-Targets15 {
155
+ return @((New-Target 15 'hiberfil.sys (powercfg /hibernate off | /type reduced)' (Join-Path $Script:P.SD '\hiberfil.sys') -Kind cmd -Note 'off frees the whole file, about 40% of RAM; reduced keeps Fast Startup at roughly half that'))
156
+ }
157
+
158
+ function Invoke-Section15 {
159
+ $ws = $Script:WS
160
+ $h = Get-HiberfilInfo
161
+ Write-SectionIntro @(
162
+ 'hiberfil.sys holds a copy of RAM for Hibernate and Fast Startup, so it is about 40% of your RAM - permanently.',
163
+ ' off removes it. Sleep still works; Hibernate, Fast Startup and hibernate-on-critical-battery do not.',
164
+ ' reduced keeps Fast Startup only, at a smaller file (Windows decides the size, usually about half).',
165
+ 'Running speed is unaffected either way; this is purely disk space.'
166
+ )
167
+ if ($h.Bytes -gt 0) { Write-Kv 'hiberfil.sys now:' ("{0} (hibernation {1})" -f (Format-Bytes $h.Bytes), $h.State) } else { Write-Info 'hiberfil.sys is absent - hibernation is already off'; return }
168
+ if (-not $ws.IsAdmin) { Write-Warn 'needs Administrator - skipped'; return }
169
+ $choice = $ws.Hiberfil
170
+ if (-not $choice) {
171
+ if (-not $ws.Interactive) { Write-Warn 'pass --hiberfil off|reduced|keep to run this section unattended'; return }
172
+ $choice = Read-Choice -Prompt ' [o] turn hibernation off [r] reduced (keep Fast Startup) [k] keep as is > ' -Default 'k'
173
+ switch ($choice) { 'o' { $choice = 'off' } 'r' { $choice = 'reduced' } default { $choice = 'keep' } }
174
+ }
175
+ if ($choice -eq 'keep') { Write-Info 'left unchanged'; return }
176
+ $pcArgs = @('/hibernate', 'off')
177
+ if ($choice -eq 'reduced') { $pcArgs = @('/hibernate', '/type', 'reduced') }
178
+ if ($ws.DryRun) { Write-DryRun ("would run powercfg $($pcArgs -join ' ') (frees up to " + (Format-Bytes $h.Bytes) + ')'); Add-Freed $h.Bytes; return }
179
+ if (-not (Confirm-Ui -Prompt "Run powercfg $($pcArgs -join ' ') now?" -Default 'n')) { Write-Info 'skipped'; return }
180
+ $r = Invoke-External -FilePath 'powercfg.exe' -ArgumentList $pcArgs -Destructive -Quiet -Label "powercfg $($pcArgs -join ' ')"
181
+ Start-Sleep -Seconds 2
182
+ $after = Get-HiberfilInfo
183
+ $gain = [math]::Max([long]0, $h.Bytes - $after.Bytes)
184
+ Add-Freed $gain
185
+ if ($r.ExitCode -eq 0) { Write-Ok ("hibernation set to '$choice' - hiberfil.sys " + (Format-Bytes $h.Bytes) + ' -> ' + (Format-Bytes $after.Bytes) + ' (freed ' + (Format-Bytes $gain) + ')') } else { Write-Warn "powercfg returned $($r.ExitCode)" }
186
+ }
187
+
188
+ # ---------------------------------------------------------------------------------------------
189
+ # Section 16 - event logs
190
+ # ---------------------------------------------------------------------------------------------
191
+
192
+ function Get-Targets16 {
193
+ return @((New-Target 16 'Windows Event Logs (wevtutil cl for every log)' "$($Script:P.SR)\System32\winevt\Logs" -Kind cmd -Note 'PERMANENT - troubleshooting history is lost'))
194
+ }
195
+
196
+ function Invoke-Section16 {
197
+ $ws = $Script:WS
198
+ $logDir = Join-Path $Script:P.SR 'System32\winevt\Logs'
199
+ Write-SectionIntro @(
200
+ 'Clears every Windows Event Log (Application, System, Security and the hundreds of per-component logs).',
201
+ 'This is permanent and removes the history a technician would use to diagnose a problem. Only do this on',
202
+ 'a machine that is behaving normally.'
203
+ )
204
+ if (-not $ws.IsAdmin) { Write-Warn 'needs Administrator - skipped'; return }
205
+ $before = Get-DirectoryBytes $logDir
206
+ Write-Kv 'event log store:' (Format-Bytes $before)
207
+ if ($ws.DryRun) { Write-DryRun ("would clear every event log (about " + (Format-Bytes $before) + ')'); Add-Freed $before; return }
208
+ if (-not (Confirm-Ui -Prompt 'Permanently clear ALL Windows Event Logs?' -Default 'n')) { Write-Info 'skipped'; return }
209
+ $list = Invoke-External -FilePath 'wevtutil.exe' -ArgumentList @('el') -Quiet -Label 'wevtutil el'
210
+ $ok = 0; $failed = 0
211
+ foreach ($name in $list.Output) {
212
+ $n = ([string]$name).Trim()
213
+ if (-not $n) { continue }
214
+ $r = Invoke-External -FilePath 'wevtutil.exe' -ArgumentList @('cl', $n) -Destructive -Quiet -Label "wevtutil cl $n"
215
+ if ($r.ExitCode -eq 0) { $ok++ } else { $failed++ }
216
+ }
217
+ $gain = [math]::Max([long]0, $before - (Get-DirectoryBytes $logDir))
218
+ Add-Freed $gain
219
+ Write-Ok ("cleared $ok logs ($failed could not be cleared - analytic/debug channels), freed " + (Format-Bytes $gain))
220
+ }
@@ -1,24 +1,24 @@
1
- # test_runners.ps1 - section 3: downloaded test browsers (Cypress, Playwright, Puppeteer). Keep the newest of each.
2
-
3
- $Script:WS_GROUP_BY_FAMILY = { param($name) ($name -replace '[-_]?v?\d[\d.]*$', '') }
4
-
5
- function Get-Targets03 {
6
- $P = $Script:P
7
- return @(
8
- (New-Target 3 'Cypress binary versions' "$($P.L)\Cypress\Cache" -Mode units -Dev $true -KeepNewest $true -Note 'one folder per Cypress version; newest kept')
9
- (New-Target 3 'Playwright browsers' "$($P.L)\ms-playwright" -Mode units -Dev $true -KeepNewest $true -GroupBy $Script:WS_GROUP_BY_FAMILY -ExcludeNames @('.links', 'daemon') -Note 'newest build of each browser kept')
10
- (New-Target 3 'Playwright-Go browsers' "$($P.L)\ms-playwright-go" -Mode units -Dev $true -KeepNewest $true -GroupBy $Script:WS_GROUP_BY_FAMILY)
11
- (New-Target 3 'Puppeteer browsers' "$($P.U)\.cache\puppeteer\*" -Kind glob -Mode units -Dev $true -KeepNewest $true -Note 'per browser family: newest build kept')
12
- )
13
- }
14
-
15
- function Invoke-Section03 {
16
- Write-SectionIntro @(
17
- 'Test runners download whole browsers per version. The newest build of each browser is always kept; older',
18
- 'builds go once idle for the window. A missing build is re-downloaded by the next install or "playwright install".'
19
- ) -Dev $true
20
- $targets = Get-Targets03
21
- $null = Show-TargetSizes $targets
22
- if (-not (Confirm-Section 'Prune superseded test-runner browsers now?')) { Write-Info 'skipped'; return }
23
- $null = Invoke-TargetList $targets
24
- }
1
+ # test_runners.ps1 - section 3: downloaded test browsers (Cypress, Playwright, Puppeteer). Keep the newest of each.
2
+
3
+ $Script:WS_GROUP_BY_FAMILY = { param($name) ($name -replace '[-_]?v?\d[\d.]*$', '') }
4
+
5
+ function Get-Targets03 {
6
+ $P = $Script:P
7
+ return @(
8
+ (New-Target 3 'Cypress binary versions' "$($P.L)\Cypress\Cache" -Mode units -Dev $true -KeepNewest $true -Note 'one folder per Cypress version; newest kept')
9
+ (New-Target 3 'Playwright browsers' "$($P.L)\ms-playwright" -Mode units -Dev $true -KeepNewest $true -GroupBy $Script:WS_GROUP_BY_FAMILY -ExcludeNames @('.links', 'daemon') -Note 'newest build of each browser kept')
10
+ (New-Target 3 'Playwright-Go browsers' "$($P.L)\ms-playwright-go" -Mode units -Dev $true -KeepNewest $true -GroupBy $Script:WS_GROUP_BY_FAMILY)
11
+ (New-Target 3 'Puppeteer browsers' "$($P.U)\.cache\puppeteer\*" -Kind glob -Mode units -Dev $true -KeepNewest $true -Note 'per browser family: newest build kept')
12
+ )
13
+ }
14
+
15
+ function Invoke-Section03 {
16
+ Write-SectionIntro @(
17
+ 'Test runners download whole browsers per version. The newest build of each browser is always kept; older',
18
+ 'builds go once idle for the window. A missing build is re-downloaded by the next install or "playwright install".'
19
+ ) -Dev $true
20
+ $targets = Get-Targets03
21
+ $null = Show-TargetSizes $targets
22
+ if (-not (Confirm-Section 'Prune superseded test-runner browsers now?')) { Write-Info 'skipped'; return }
23
+ $null = Invoke-TargetList $targets
24
+ }