windowsweep 1.0.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.
@@ -0,0 +1,93 @@
1
+ # docker.ps1 - section 5 (Docker prune) and section 20 (disk image compaction).
2
+
3
+ function Get-Targets05 {
4
+ return @(
5
+ (New-Target 5 'docker image prune (dangling layers)' 'docker image prune -f' -Kind cmd)
6
+ (New-Target 5 'docker builder prune (build cache idle N+ days)' 'docker builder prune -f --filter until=<N>h' -Kind cmd)
7
+ (New-Target 5 'docker image prune (unused images older than N days)' 'docker image prune -a -f --filter until=<N>h' -Kind cmd)
8
+ )
9
+ }
10
+
11
+ function Test-DockerDaemon {
12
+ if (-not (Test-CommandPresent 'docker')) { return $false }
13
+ $r = Invoke-External -FilePath 'docker' -ArgumentList @('info', '--format', '{{.ServerVersion}}') -Quiet -Label 'docker info'
14
+ return ($r.Ran -and $r.ExitCode -eq 0)
15
+ }
16
+
17
+ function Invoke-DockerPrune {
18
+ param([string[]] $Arguments, [string] $Label)
19
+ $r = Invoke-External -FilePath 'docker' -ArgumentList $Arguments -Destructive -Quiet -Label $Label
20
+ if (-not $r.Ran) { return [long]0 }
21
+ $freed = [long]0
22
+ foreach ($l in $r.Output) { if ($l -match 'Total reclaimed space:\s*(.+)$') { $freed += ConvertFrom-SizeText $Matches[1] } }
23
+ if ($r.ExitCode -eq 0) { Write-Ok ("$Label - reclaimed " + (Format-Bytes $freed)) } else { Write-Warn "$Label - docker returned $($r.ExitCode)" }
24
+ Add-Freed $freed
25
+ return $freed
26
+ }
27
+
28
+ function Invoke-Section05 {
29
+ $ws = $Script:WS
30
+ Write-SectionIntro @(
31
+ 'Docker keeps every image layer and build step it ever produced. Dangling layers and the build cache are',
32
+ 'safe to drop; images no container uses go only when older than the idle window. Volumes are never touched',
33
+ '(review them yourself with: docker volume ls / docker volume prune).'
34
+ ) -Dev $true
35
+ if (-not (Test-CommandPresent 'docker')) { Write-Info 'docker CLI not found - skipped'; return }
36
+ if (-not (Test-DockerDaemon)) { Write-Warn 'Docker daemon is not running (start Docker Desktop) - skipped'; $ws.Hints += 'Start Docker Desktop and re-run: windowsweep --only 5 --yes'; return }
37
+ $df = Invoke-External -FilePath 'docker' -ArgumentList @('system', 'df') -Quiet -Label 'docker system df'
38
+ foreach ($l in $df.Output) { Write-Plain " $l" }
39
+ if (-not (Confirm-Section 'Prune Docker now?')) { Write-Info 'skipped'; return }
40
+ $hours = [int]($ws.Days * 24)
41
+ if ($ws.PurgeAll -or $ws.Developer -eq $false) {
42
+ if ($ws.Developer -ne $false -and -not (Confirm-Ui -Prompt 'FULL PURGE removes every image no container uses right now (they re-pull later). Continue?' -Default 'n')) { Write-Info 'full purge declined; doing the conservative prune instead' }
43
+ else { $null = Invoke-DockerPrune -Arguments @('system', 'prune', '-a', '-f') -Label 'docker system prune -a'; return }
44
+ }
45
+ $null = Invoke-DockerPrune -Arguments @('image', 'prune', '-f') -Label 'dangling image layers'
46
+ $null = Invoke-DockerPrune -Arguments @('builder', 'prune', '-f', '--filter', "until=${hours}h") -Label "build cache idle $($ws.Days)+ days"
47
+ $null = Invoke-DockerPrune -Arguments @('image', 'prune', '-a', '-f', '--filter', "until=${hours}h") -Label "unused images older than $($ws.Days) days"
48
+ Write-Note 'the Docker disk image (docker_data.vhdx) does not shrink by itself - section 20 compacts it (admin, stops Docker)'
49
+ }
50
+
51
+ # ---------------------------------------------------------------------------------------------
52
+ # Section 20 - disk image compaction
53
+ # ---------------------------------------------------------------------------------------------
54
+
55
+ function Get-Targets20 {
56
+ return @((New-Target 20 'Docker Desktop / WSL virtual disks (*.vhdx)' "$($Script:P.L)\Docker\wsl\*\*.vhdx and Packages\*\LocalState\*.vhdx" -Kind cmd -Note 'diskpart compact vdisk; Docker Desktop and WSL are stopped first'))
57
+ }
58
+
59
+ function Invoke-Section20 {
60
+ $ws = $Script:WS
61
+ Write-SectionIntro @(
62
+ 'A WSL/Docker virtual disk grows as data is written but never shrinks when it is deleted. Compaction hands the',
63
+ 'free space back to Windows. It needs Administrator rights and STOPS Docker Desktop and every WSL distro first;',
64
+ 'restart Docker Desktop afterwards.'
65
+ )
66
+ $images = @(Get-DiskImageFiles)
67
+ if ($images.Count -eq 0) { Write-Info 'no .vhdx disk images found'; return }
68
+ $i = 1
69
+ foreach ($img in $images) { Write-UiLine (" {0,3} {1,10} {2}" -f $i, (Format-Bytes $img.Length), $img.FullName) 'Gray'; $i++ }
70
+ if (-not (Test-CommandPresent 'diskpart.exe')) { Write-Warn 'diskpart.exe not found - skipped'; return }
71
+ $picks = @()
72
+ if ($ws.Yes) { $picks = @(1..$images.Count) } else { $picks = Read-MultiSelect -Total $images.Count }
73
+ if ($picks.Count -eq 0) { Write-Info 'nothing selected'; return }
74
+ if (-not (Confirm-Section 'Stop Docker Desktop and WSL now and compact the selected disks?' 'n')) { Write-Info 'skipped'; return }
75
+ if ($ws.DryRun) { foreach ($k in $picks) { Write-DryRun "would compact $($images[$k - 1].FullName)" }; return }
76
+ Write-Info 'stopping Docker Desktop and WSL...'
77
+ Get-Process -Name 'Docker Desktop' -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
78
+ $null = Invoke-External -FilePath 'wsl.exe' -ArgumentList @('--shutdown') -Destructive -Quiet -Label 'wsl --shutdown'
79
+ Start-Sleep -Seconds 8
80
+ foreach ($k in $picks) {
81
+ $img = $images[$k - 1]
82
+ $before = (Get-Item -LiteralPath $img.FullName).Length
83
+ $script = Join-Path $env:TEMP ("windowsweep-diskpart-" + [guid]::NewGuid().ToString('N') + '.txt')
84
+ [IO.File]::WriteAllLines($script, @("select vdisk file=`"$($img.FullName)`"", 'attach vdisk readonly', 'compact vdisk', 'detach vdisk', 'exit'))
85
+ $r = Invoke-External -FilePath 'diskpart.exe' -ArgumentList @('/s', $script) -Destructive -Quiet -Label "diskpart compact $($img.Name)"
86
+ Remove-Item -LiteralPath $script -Force -ErrorAction SilentlyContinue
87
+ $after = (Get-Item -LiteralPath $img.FullName).Length
88
+ $gain = [math]::Max([long]0, $before - $after)
89
+ if ($r.ExitCode -eq 0) { Write-Ok ("$($img.Name): " + (Format-Bytes $before) + ' -> ' + (Format-Bytes $after) + ' (freed ' + (Format-Bytes $gain) + ')') } else { Write-Warn "$($img.Name): diskpart returned $($r.ExitCode)"; foreach ($l in $r.Output) { Write-Note $l } }
90
+ Add-Freed $gain
91
+ }
92
+ Write-Info 'done - start Docker Desktop again when you need it'
93
+ }
@@ -0,0 +1,149 @@
1
+ # editors.ps1 - section 6: editor caches, stale workspace storage, superseded extension versions.
2
+
3
+ $Script:WS_EDITORS = @(
4
+ @{ Name = 'VS Code'; Root = 'Code'; Ext = '.vscode'; Proc = @('Code') }
5
+ @{ Name = 'VS Code Insiders'; Root = 'Code - Insiders'; Ext = '.vscode-insiders'; Proc = @('Code - Insiders') }
6
+ @{ Name = 'VSCodium'; Root = 'VSCodium'; Ext = '.vscode-oss'; Proc = @('VSCodium') }
7
+ @{ Name = 'Cursor'; Root = 'Cursor'; Ext = '.cursor'; Proc = @('Cursor') }
8
+ @{ Name = 'Windsurf'; Root = 'Windsurf'; Ext = '.windsurf'; Proc = @('Windsurf') }
9
+ )
10
+
11
+ function Get-Targets06 {
12
+ $P = $Script:P
13
+ $t = @()
14
+ foreach ($e in $Script:WS_EDITORS) {
15
+ $root = Join-Path $P.A $e.Root
16
+ $t += (New-Target 6 "$($e.Name) caches" $root -Kind editor -Guard $e.Proc -Note 'Cache, CachedData, Code Cache, GPUCache, VSIX cache, crash reports')
17
+ $t += (New-Target 6 "$($e.Name) logs" (Join-Path $root 'logs') -Mode clear-old -Days 7)
18
+ $t += (New-Target 6 "$($e.Name) stale workspace storage" (Join-Path $root 'User\workspaceStorage') -Kind cmd -Guard $e.Proc -Note 'entries whose folder no longer exists')
19
+ $t += (New-Target 6 "$($e.Name) superseded extension versions" (Join-Path $P.U "$($e.Ext)\extensions") -Kind cmd -Guard $e.Proc -Note 'older versions and .obsolete entries; newest kept')
20
+ }
21
+ $t += (New-Target 6 'Visual Studio ComponentModelCache' "$($P.L)\Microsoft\VisualStudio\*\ComponentModelCache" -Kind glob -Mode clear -Guard @('devenv'))
22
+ $t += (New-Target 6 'Visual Studio designer shadow cache' "$($P.L)\Microsoft\VisualStudio\*\Designer\ShadowCache" -Kind glob -Mode clear -Guard @('devenv'))
23
+ $t += (New-Target 6 'Visual Studio telemetry (AppInsights)' "$($P.L)\Microsoft\VSApplicationInsights" -Mode clear)
24
+ $t += (New-Target 6 'Visual Studio SQM logs' "$($P.L)\Microsoft\VSCommon\*\SQM" -Kind glob -Mode clear)
25
+ return $t
26
+ }
27
+
28
+ function Remove-StaleWorkspaceStorage {
29
+ <# .SYNOPSIS workspaceStorage entries pointing at a folder/workspace file that no longer exists. #>
30
+ param([string] $Root, [string] $Label)
31
+ if (-not (Test-DirPresent $Root)) { return }
32
+ $stale = @()
33
+ foreach ($e in (Get-ChildEntries $Root)) {
34
+ if (-not ($e -is [IO.DirectoryInfo])) { continue }
35
+ $json = Join-Path (Remove-LongPrefix $e.FullName) 'workspace.json'
36
+ if (-not (Test-PathPresent $json)) { continue }
37
+ try {
38
+ $w = Get-Content -LiteralPath $json -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
39
+ $uri = $w.folder
40
+ if (-not $uri) { $uri = $w.workspace }
41
+ if (-not $uri -or $uri -notmatch '^file:///') { continue }
42
+ $local = [Uri]::UnescapeDataString(($uri -replace '^file:///', '')) -replace '/', '\'
43
+ if (-not (Test-PathPresent $local)) { $stale += [pscustomobject]@{ Path = (Remove-LongPrefix $e.FullName); Target = $local } }
44
+ } catch { $null = $_ }
45
+ }
46
+ if ($stale.Count -eq 0) { Write-Info "$Label - none stale"; return }
47
+ Write-Info "$Label - $($stale.Count) entries point at folders that no longer exist:"
48
+ foreach ($s in $stale) { Write-Note " $($s.Target)" }
49
+ foreach ($s in $stale) { $null = Remove-PathSafe -Path $s.Path -Within $Root -Label $Label }
50
+ if (-not $Script:WS.DryRun) { Write-Ok "$Label - removed $($stale.Count) stale entries" }
51
+ }
52
+
53
+ function Read-JsonFile {
54
+ <# .SYNOPSIS Parse a JSON file. A top-level array always comes back as an array (PowerShell 5.1's ConvertFrom-Json
55
+ emits an array as ONE pipeline object, which silently turns 16 entries into a single bogus one when wrapped). #>
56
+ param([string] $Path)
57
+ $parsed = Get-Content -LiteralPath $Path -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
58
+ if ($parsed -is [array]) { return $parsed }
59
+ return @($parsed)
60
+ }
61
+
62
+ function Get-SupersededExtensionPlan {
63
+ <# .SYNOPSIS Decide which extension folders are leftovers. Pure logic (exercised by the self-test).
64
+ A folder is a victim only when the editor's extensions.json does NOT reference it AND either the .obsolete marker
65
+ names it or a different version of the same extension IS referenced. Without extensions.json: .obsolete entries
66
+ plus every version older than the newest one present. #>
67
+ param([string] $Root, [object[]] $Folders)
68
+ $victims = @()
69
+ $obsolete = @{}
70
+ $obsFile = Join-Path $Root '.obsolete'
71
+ if (Test-PathPresent $obsFile) {
72
+ try { $o = Get-Content -LiteralPath $obsFile -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop; foreach ($p in $o.PSObject.Properties) { if ($p.Value) { $obsolete[$p.Name] = $true } } } catch { $null = $_ }
73
+ }
74
+ $installed = $null
75
+ $manifest = Join-Path $Root 'extensions.json'
76
+ if (Test-PathPresent $manifest) {
77
+ try {
78
+ $installed = @{}
79
+ foreach ($entry in (Read-JsonFile $manifest)) {
80
+ $loc = $entry.relativeLocation
81
+ if ($loc -is [string] -and $loc) { $installed[$loc] = $true }
82
+ }
83
+ } catch { $installed = $null }
84
+ }
85
+ if ($null -ne $installed -and $installed.Count -gt 0) {
86
+ $installedIds = @{}
87
+ foreach ($f in $Folders) { if ($installed.ContainsKey($f.Name)) { $installedIds[$f.Id] = $true } }
88
+ foreach ($f in $Folders) {
89
+ if ($installed.ContainsKey($f.Name)) { continue }
90
+ if ($obsolete.ContainsKey($f.Name) -or $installedIds.ContainsKey($f.Id)) { $victims += $f }
91
+ }
92
+ $note = "extensions.json lists $($installed.Count) installed extension(s); $($victims.Count) leftover folder(s)"
93
+ } else {
94
+ foreach ($group in @($Folders | Where-Object { -not $obsolete.ContainsKey($_.Name) } | Group-Object Id)) {
95
+ if ($group.Count -lt 2) { continue }
96
+ $newest = $group.Group | Sort-Object Version -Descending | Select-Object -First 1
97
+ foreach ($v in $group.Group) { if ($v.Path -ne $newest.Path) { $victims += $v } }
98
+ }
99
+ foreach ($f in $Folders) { if ($obsolete.ContainsKey($f.Name)) { $victims += $f } }
100
+ $note = "no extensions.json; using .obsolete + newest-version rule: $($victims.Count) leftover folder(s)"
101
+ }
102
+ return [pscustomobject]@{ Victims = @($victims); Note = $note }
103
+ }
104
+
105
+ function Remove-SupersededExtensions {
106
+ <# .SYNOPSIS Extension folders the editor no longer uses.
107
+ The editor's own extensions.json is the source of truth: every versioned folder it does not reference is a leftover
108
+ (uninstalled, or superseded by the version it does reference). Without that file, fall back to the .obsolete marker
109
+ plus keep-the-newest-version-per-extension. #>
110
+ param([string] $Root, [string] $Label)
111
+ if (-not (Test-DirPresent $Root)) { return }
112
+ $folders = @()
113
+ foreach ($e in @(Get-ChildEntries $Root)) {
114
+ if (-not ($e -is [IO.DirectoryInfo]) -or (Test-ReparsePoint $e)) { continue }
115
+ if ($e.Name -match '^(.+?)-(\d+\.\d+\.\d+)([^\\]*)$') {
116
+ $ver = $null
117
+ try { $ver = [version]$Matches[2] } catch { continue }
118
+ $folders += [pscustomobject]@{ Name = $e.Name; Id = $Matches[1]; Path = (Remove-LongPrefix $e.FullName); Version = $ver }
119
+ }
120
+ }
121
+ if ($folders.Count -eq 0) { Write-Info "$Label - none superseded"; return }
122
+ $plan = Get-SupersededExtensionPlan -Root $Root -Folders $folders
123
+ Write-Note "$Label - $($plan.Note)"
124
+ $victims = @($plan.Victims)
125
+ if ($victims.Count -eq 0) { Write-Info "$Label - none superseded"; return }
126
+ foreach ($v in $victims) {
127
+ $r = Remove-PathSafe -Path $v.Path -Within $Root -Label $Label
128
+ if ($r.Removed -and -not $Script:WS.DryRun) { Write-Ok ("$Label - removed $($v.Name) (" + (Format-Bytes $r.Bytes) + ')') }
129
+ }
130
+ }
131
+
132
+ function Invoke-Section06 {
133
+ Write-SectionIntro @(
134
+ 'Editors rebuild their caches on the next start. Settings, keybindings, snippets, globalStorage and local',
135
+ 'History are protected. A running editor is left alone except for its VSIX download cache and old logs.'
136
+ )
137
+ $targets = Get-Targets06
138
+ $null = Show-TargetSizes $targets
139
+ if (-not (Confirm-Section 'Clean editor caches now?')) { Write-Info 'skipped'; return }
140
+ $null = Invoke-TargetList @($targets | Where-Object { $_.Kind -ne 'cmd' })
141
+ foreach ($e in $Script:WS_EDITORS) {
142
+ $root = Join-Path $Script:P.A $e.Root
143
+ $extRoot = Join-Path $Script:P.U "$($e.Ext)\extensions"
144
+ if (-not (Test-DirPresent $root) -and -not (Test-DirPresent $extRoot)) { continue }
145
+ if (Test-ProcessRunning $e.Proc) { Write-Warn "$($e.Name) is running - workspace storage and extension versions left alone"; continue }
146
+ Remove-StaleWorkspaceStorage -Root (Join-Path $root 'User\workspaceStorage') -Label "$($e.Name) workspace storage"
147
+ Remove-SupersededExtensions -Root $extRoot -Label "$($e.Name) extensions"
148
+ }
149
+ }
@@ -0,0 +1,101 @@
1
+ # health.ps1 - section 0: read-only system health report.
2
+
3
+ function Get-Targets00 { return @() }
4
+
5
+ function Get-HiberfilInfo {
6
+ $p = Join-Path $Script:P.SD '\hiberfil.sys'
7
+ $size = [long]0
8
+ # Get-Item cannot open the hibernation file without elevation, but FileInfo reads its size from the directory entry.
9
+ try { $f = New-Object IO.FileInfo($p); if ($f.Exists) { $size = [long]$f.Length } } catch { $size = 0 }
10
+ $state = 'unknown'
11
+ try {
12
+ $a = (& powercfg.exe /a 2>$null) -join ' '
13
+ if ($a -match 'Hibernate' -and $a -notmatch 'Hibernation has not been enabled') { $state = 'available' } else { $state = 'off' }
14
+ } catch { $null = $_ }
15
+ return [pscustomobject]@{ Path = $p; Bytes = $size; State = $state }
16
+ }
17
+
18
+ function Get-DiskImageFiles {
19
+ <# .SYNOPSIS Docker Desktop / WSL virtual disks (they grow but never shrink on their own). #>
20
+ $L = $Script:P.L
21
+ $out = @()
22
+ foreach ($root in @((Join-Path $L 'Docker\wsl'), (Join-Path $L 'Packages'))) {
23
+ if (-not (Test-DirPresent $root)) { continue }
24
+ try { $out += Get-ChildItem -LiteralPath $root -Recurse -Filter '*.vhdx' -File -Depth 4 -ErrorAction SilentlyContinue } catch { $null = $_ }
25
+ }
26
+ return $out
27
+ }
28
+
29
+ function Get-BlockingApps {
30
+ <# .SYNOPSIS Running apps whose caches this tool skips while they run. #>
31
+ $map = [ordered]@{ chrome = 'Chrome'; msedge = 'Edge'; brave = 'Brave'; firefox = 'Firefox'; vivaldi = 'Vivaldi'; opera = 'Opera'; Code = 'VS Code'; Cursor = 'Cursor'; Windsurf = 'Windsurf'; Discord = 'Discord'; slack = 'Slack'; 'ms-teams' = 'Teams'; Zoom = 'Zoom'; Spotify = 'Spotify'; Postman = 'Postman'; Figma = 'Figma'; Notion = 'Notion' }
32
+ $out = @()
33
+ foreach ($k in $map.Keys) { if (Test-ProcessRunning @($k)) { $out += $map[$k] } }
34
+ return $out
35
+ }
36
+
37
+ function Invoke-Section00 {
38
+ $ws = $Script:WS
39
+ Write-SectionIntro @('Read-only snapshot of the machine: nothing is changed here.')
40
+ try {
41
+ $os = Get-CimInstance Win32_OperatingSystem -ErrorAction Stop
42
+ Write-Kv 'Windows:' "$($os.Caption) $($os.Version) (build $($os.BuildNumber))"
43
+ $up = (Get-Date) - $os.LastBootUpTime
44
+ Write-Kv 'Uptime:' ("{0}d {1}h {2}m" -f $up.Days, $up.Hours, $up.Minutes)
45
+ Write-Kv 'RAM:' ("{0} total, {1} free" -f (Format-Bytes ($os.TotalVisibleMemorySize * 1KB)), (Format-Bytes ($os.FreePhysicalMemory * 1KB)))
46
+ } catch { Write-Kv 'Windows:' $ws.OsCaption }
47
+ Write-Kv 'PowerShell:' "$($PSVersionTable.PSVersion) ($($PSVersionTable.PSEdition))"
48
+ $adm = 'no'
49
+ if ($ws.IsAdmin) { $adm = 'yes' } elseif (Test-CanElevate) { $adm = 'no (your account can elevate: --elevate)' } else { $adm = 'no (standard user; admin sections need an administrator)' }
50
+ Write-Kv 'Elevated:' $adm
51
+ $dev = 'not decided yet'
52
+ if ($null -ne $ws.Developer) { if ($ws.Developer) { $dev = "on ($($ws.DeveloperSource))" } else { $dev = "off ($($ws.DeveloperSource))" } }
53
+ Write-Kv 'Developer mode:' $dev
54
+ $hints = @(Get-ToolchainHints)
55
+ if ($hints.Count -gt 0) { Write-Kv 'Dev tooling found:' ($hints -join ', ') }
56
+ Write-UiLine '' 'Gray'
57
+ Show-DriveTable
58
+ try {
59
+ $sys = New-Object IO.DriveInfo($Script:P.SD)
60
+ $pct = 100.0 * $sys.AvailableFreeSpace / $sys.TotalSize
61
+ if ($sys.AvailableFreeSpace -lt 10GB -or $pct -lt 10) { Write-Warn ("system drive has only {0} free ({1:N1}%) - Windows slows down badly below ~10%; this run should help" -f (Format-Bytes $sys.AvailableFreeSpace), $pct) }
62
+ } catch { $null = $_ }
63
+ Write-UiLine '' 'Gray'
64
+ $h = Get-HiberfilInfo
65
+ if ($h.Bytes -gt 0) { Write-Kv 'hiberfil.sys:' ("{0} (hibernation {1}) - section 15 can shrink or remove it (admin)" -f (Format-Bytes $h.Bytes), $h.State) } else { Write-Kv 'hiberfil.sys:' 'absent (hibernation off)' }
66
+ try {
67
+ foreach ($pf in (Get-CimInstance Win32_PageFileUsage -ErrorAction Stop)) { Write-Kv 'pagefile:' ("{0} {1} allocated" -f $pf.Name, (Format-Bytes ($pf.AllocatedBaseSize * 1MB))) }
68
+ } catch { $null = $_ }
69
+ foreach ($img in (Get-DiskImageFiles)) { Write-Kv 'disk image:' ("{0} {1} (section 20 compacts it, admin)" -f $img.FullName, (Format-Bytes $img.Length)) }
70
+ if (Test-CommandPresent 'wsl.exe') {
71
+ try {
72
+ $raw = & wsl.exe --list --verbose 2>$null
73
+ $txt = (($raw | ForEach-Object { [string]$_ }) -join "`n") -replace "`0", ''
74
+ $lines = @($txt -split "`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -and $_ -notmatch '^NAME' })
75
+ if ($lines.Count -gt 0) { Write-Kv 'WSL distros:' (($lines | ForEach-Object { ($_ -replace '\s+', ' ') }) -join ' | ') }
76
+ } catch { $null = $_ }
77
+ }
78
+ Write-UiLine '' 'Gray'
79
+ try { $n = @(Get-CimInstance Win32_StartupCommand -ErrorAction Stop).Count; Write-Kv 'Startup items:' "$n (review in Task Manager > Startup; this tool does not change them)" } catch { $null = $_ }
80
+ try {
81
+ $ss = (Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\StorageSense\Parameters\StoragePolicy' -ErrorAction Stop).'01'
82
+ $ssText = 'off'
83
+ if ($ss -eq 1) { $ssText = 'on' }
84
+ Write-Kv 'Storage Sense:' $ssText
85
+ } catch { $null = $_ }
86
+ try {
87
+ $la = (& fsutil.exe behavior query disablelastaccess 2>$null) -join ' '
88
+ if ($la -match '=\s*(\d)') {
89
+ $v = [int]$Matches[1]
90
+ $laText = 'disabled - idle age uses write/creation times (the safe reading)'
91
+ if ($v -eq 1 -or $v -eq 3) { $laText = 'enabled - last-access times are reliable' }
92
+ Write-Kv 'Last-access tracking:' $laText
93
+ }
94
+ } catch { $null = $_ }
95
+ $blocking = @(Get-BlockingApps)
96
+ if ($blocking.Count -gt 0) {
97
+ Write-UiLine '' 'Gray'
98
+ Write-Warn ("running now, their caches will be skipped until closed: " + ($blocking -join ', '))
99
+ }
100
+ Add-Freed 0
101
+ }
@@ -0,0 +1,61 @@
1
+ # menu.ps1 - jump-to menu: run one section at a time; toggle dry-run and auto-yes.
2
+
3
+ function Show-Menu {
4
+ $ws = $Script:WS
5
+ Write-UiLine '' 'Gray'
6
+ Write-Banner
7
+ $sys = New-Object IO.DriveInfo($Script:P.SD)
8
+ $dry = 'OFF'
9
+ if ($ws.DryRun) { $dry = 'ON' }
10
+ $yes = 'OFF'
11
+ if ($ws.Yes) { $yes = 'ON' }
12
+ $adm = 'no'
13
+ if ($ws.IsAdmin) { $adm = 'yes' }
14
+ Write-UiLine (" system drive free: {0} dry-run: {1} auto-yes: {2} elevated: {3}" -f (Format-Bytes $sys.AvailableFreeSpace), $dry, $yes, $adm) 'Cyan'
15
+ Write-UiLine '' 'Gray'
16
+ foreach ($s in $Script:WS_SECTIONS) {
17
+ $mark = ' '
18
+ if ($ws.Report.steps | Where-Object { $_.section -eq $s.Id -and $_.status -in 'ran', 'dry-run' }) { $mark = $ws.Glyph.ok }
19
+ $flags = ''
20
+ if ($s.Admin) { $flags += ' [admin]' }
21
+ if ($s.Batch -eq 'deep') { $flags += ' [deep]' }
22
+ if ($s.Batch -eq 'interactive') { $flags += ' [interactive]' }
23
+ $color = 'Gray'
24
+ if ($s.Admin -and -not $ws.IsAdmin) { $color = 'DarkGray' }
25
+ Write-UiLine (" {0} [{1,2}] {2}{3}" -f $mark, $s.Id, $s.Title, $flags) $color
26
+ }
27
+ Write-UiLine '' 'Gray'
28
+ Write-UiLine ' [A] run the safe batch [D] toggle dry-run [Y] toggle auto-yes [S] summary [Q] quit' 'White'
29
+ }
30
+
31
+ function Invoke-Menu {
32
+ $ws = $Script:WS
33
+ if (-not $ws.Interactive) {
34
+ Write-Err 'The menu needs an interactive console. For unattended runs use: windowsweep --all --yes'
35
+ $ws.ExitCode = $Script:WS_EXIT_USAGE
36
+ return
37
+ }
38
+ while ($true) {
39
+ Show-Menu
40
+ $choice = Read-Choice -Prompt ' Select [0-21 / A / D / Y / S / Q]: ' -Default ''
41
+ if ($choice -eq '') { continue }
42
+ switch -Regex ($choice) {
43
+ '^q' { Show-SessionSummary; return }
44
+ '^a' {
45
+ $ids = @($Script:WS_SAFE_BATCH)
46
+ if ($ws.IsAdmin) { $ids += $Script:WS_SAFE_BATCH_ADMIN }
47
+ $ws.BatchMode = $true
48
+ try { foreach ($id in $ids) { Invoke-SectionById -Id $id } } finally { $ws.BatchMode = $false }
49
+ Wait-Enter
50
+ }
51
+ '^d' { $ws.DryRun = -not $ws.DryRun }
52
+ '^y' { $ws.Yes = -not $ws.Yes }
53
+ '^s' { Show-SessionSummary; Wait-Enter }
54
+ '^\d+$' {
55
+ $id = [int]$choice
56
+ if (Get-Section $id) { Invoke-SectionById -Id $id; Wait-Enter } else { Write-Warn "no section $id" }
57
+ }
58
+ default { Write-Warn "unrecognised choice: $choice" }
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,124 @@
1
+ # personal.ps1 - sections 18 and 19: personal files in Downloads. Interactive only; the Recycle Bin is the destination.
2
+
3
+ $Script:WS_PARTIAL_PATTERNS = @('*.crdownload', '*.part', '*.partial', '*.fdmdownload', '*.opdownload', '*.!ut', '*.aria2', '*.download', '*.bc!', '*.tmp')
4
+
5
+ function Get-PersonalRoots {
6
+ $roots = @()
7
+ $d = Join-Path $Script:P.U 'Downloads'
8
+ if (Test-DirPresent $d) { $roots += (Get-FullPath $d) }
9
+ return $roots
10
+ }
11
+
12
+ function Get-Targets18 {
13
+ $t = @()
14
+ foreach ($r in (Get-PersonalRoots)) { $t += (New-Target 18 'Partial / orphan downloads' $r -Kind cmd -Note ($Script:WS_PARTIAL_PATTERNS -join ' ')) }
15
+ return $t
16
+ }
17
+
18
+ function Get-Targets19 {
19
+ $t = @()
20
+ foreach ($r in (Get-PersonalRoots)) { $t += (New-Target 19 "Files >= $($Script:WS.LargeFileMb) MB idle $($Script:WS.Days)+ days" $r -Kind cmd -Note 'moved to the Recycle Bin after you pick them') }
21
+ return $t
22
+ }
23
+
24
+ function Get-FilesUnder {
25
+ <# .SYNOPSIS Files under Root up to MaxDepth, never following links, never entering protected folders. #>
26
+ param([string] $Root, [int] $MaxDepth = 4)
27
+ $out = New-Object System.Collections.Generic.List[object]
28
+ $stack = New-Object System.Collections.Generic.Stack[object]
29
+ $stack.Push(@($Root, 0))
30
+ while ($stack.Count -gt 0) {
31
+ $item = $stack.Pop()
32
+ foreach ($e in (Get-ChildEntries ([string]$item[0]))) {
33
+ if (Test-ReparsePoint $e) { continue }
34
+ if ($e -is [IO.DirectoryInfo]) { if ([int]$item[1] -lt $MaxDepth) { $stack.Push(@((Remove-LongPrefix $e.FullName), [int]$item[1] + 1)) }; continue }
35
+ $out.Add($e)
36
+ }
37
+ }
38
+ return $out
39
+ }
40
+
41
+ function Get-PartialDownloads {
42
+ $list = @()
43
+ foreach ($root in (Get-PersonalRoots)) {
44
+ foreach ($f in (Get-FilesUnder -Root $root -MaxDepth 4)) {
45
+ foreach ($pat in $Script:WS_PARTIAL_PATTERNS) {
46
+ if ($f.Name -like $pat) { $list += [pscustomobject]@{ Path = (Remove-LongPrefix $f.FullName); Bytes = [long]$f.Length; Idle = [int][math]::Floor(([datetime]::UtcNow - (Get-NewestTimestampUtc $f)).TotalDays); Root = $root }; break }
47
+ }
48
+ }
49
+ }
50
+ return @($list | Sort-Object Bytes -Descending)
51
+ }
52
+
53
+ function Get-LargeStaleFiles {
54
+ $ws = $Script:WS
55
+ $min = [long]$ws.LargeFileMb * 1MB
56
+ $list = @()
57
+ foreach ($root in (Get-PersonalRoots)) {
58
+ foreach ($f in (Get-FilesUnder -Root $root -MaxDepth 4)) {
59
+ if ([long]$f.Length -lt $min) { continue }
60
+ $idle = [int][math]::Floor(([datetime]::UtcNow - (Get-NewestTimestampUtc $f)).TotalDays)
61
+ if ($idle -lt $ws.Days) { continue }
62
+ $list += [pscustomobject]@{ Path = (Remove-LongPrefix $f.FullName); Bytes = [long]$f.Length; Idle = $idle; Root = $root }
63
+ }
64
+ }
65
+ return @($list | Sort-Object Bytes -Descending | Select-Object -First 100)
66
+ }
67
+
68
+ function Show-FileTable {
69
+ param([object[]] $Rows)
70
+ Write-UiLine (" {0,3} {1,10} {2,6} {3}" -f '#', 'SIZE', 'IDLE', 'FILE') 'White'
71
+ $i = 1
72
+ foreach ($r in $Rows) { Write-UiLine (" {0,3} {1,10} {2,5}d {3}" -f $i, (Format-Bytes $r.Bytes), $r.Idle, $r.Path) 'Gray'; $i++ }
73
+ }
74
+
75
+ function Invoke-PersonalPicker {
76
+ param([object[]] $Rows, [string] $Prompt)
77
+ $ws = $Script:WS
78
+ $Rows = @($Rows)
79
+ if ($Rows.Count -eq 0) { Write-Info 'none found'; return }
80
+ Show-FileTable $Rows
81
+ $total = [long]0
82
+ foreach ($r in $Rows) { $total += $r.Bytes }
83
+ Write-Info ("total: " + (Format-Bytes $total))
84
+ $dest = 'the Recycle Bin'
85
+ if ($ws.Permanent) { $dest = 'PERMANENT deletion (--permanent)' }
86
+ Write-Note "selected files go to $dest"
87
+ $picks = Read-MultiSelect -Total $Rows.Count
88
+ if ($picks.Count -eq 0) { Write-Info 'nothing selected'; return }
89
+ if (-not $ws.DryRun) {
90
+ # Personal files never auto-confirm: --yes does not apply here.
91
+ if (-not (Confirm-Ui -Prompt "$Prompt ($($picks.Count) file(s))?" -Default 'n' -NoAutoYes)) { Write-Info 'skipped'; return }
92
+ }
93
+ foreach ($k in $picks) { $null = Send-ToRecycleBin -Path $Rows[$k - 1].Path -Within $Rows[$k - 1].Root }
94
+ }
95
+
96
+ function Invoke-Section18 {
97
+ Write-SectionIntro @(
98
+ 'Half-finished downloads left behind by browsers and download managers (.crdownload, .part, .tmp, ...).',
99
+ 'You pick what goes; it lands in the Recycle Bin. Desktop and cloud-synced folders are never scanned.'
100
+ )
101
+ if (@(Get-PersonalRoots).Count -eq 0) { Write-Info 'no Downloads folder found'; return }
102
+ Invoke-PersonalPicker -Rows @(Get-PartialDownloads) -Prompt 'Move the selected partial downloads to the Recycle Bin'
103
+ }
104
+
105
+ function Invoke-Section19 {
106
+ $ws = $Script:WS
107
+ Write-SectionIntro @(
108
+ "Files of $($ws.LargeFileMb) MB or more in Downloads that nothing has touched for $($ws.Days)+ days: old installers, ISOs,",
109
+ 'archives you already extracted. You pick what goes; it lands in the Recycle Bin.'
110
+ )
111
+ if (@(Get-PersonalRoots).Count -eq 0) { Write-Info 'no Downloads folder found'; return }
112
+ Invoke-PersonalPicker -Rows @(Get-LargeStaleFiles) -Prompt 'Move the selected files to the Recycle Bin'
113
+ }
114
+
115
+ function Show-PersonalScan {
116
+ <# .SYNOPSIS Read-only listing used by --scan. #>
117
+ Write-Box 'Personal files (read-only)' 'Sections 18 and 19 are interactive only - nothing here is deleted'
118
+ Write-Section 'Partial / orphan downloads'
119
+ $p = @(Get-PartialDownloads)
120
+ if ($p.Count -eq 0) { Write-Info 'none found' } else { Show-FileTable @($p | Select-Object -First 30) }
121
+ Write-Section "Files >= $($Script:WS.LargeFileMb) MB idle $($Script:WS.Days)+ days in Downloads"
122
+ $l = @(Get-LargeStaleFiles)
123
+ if ($l.Count -eq 0) { Write-Info 'none found' } else { Show-FileTable @($l | Select-Object -First 30) }
124
+ }
@@ -0,0 +1,69 @@
1
+ # pkg_managers.ps1 - section 1: package-manager caches. All regenerable; the idle gate keeps recent work fast.
2
+
3
+ function Get-Targets01 {
4
+ $P = $Script:P
5
+ $npm = Get-NpmCacheDir
6
+ return @(
7
+ (New-Target 1 'npm cache (_cacache)' (Join-Path $npm '_cacache') -Mode prune -Dev $true -Note 'regenerated on the next npm install')
8
+ (New-Target 1 'npx package cache (_npx)' (Join-Path $npm '_npx') -Mode prune -Dev $true)
9
+ (New-Target 1 'npm logs' (Join-Path $npm '_logs') -Mode clear-old -Days 7)
10
+ (New-Target 1 'Yarn v1 cache' "$($P.L)\Yarn\Cache" -Mode prune -Dev $true -Note 'yarn classic offline mirror')
11
+ (New-Target 1 'Yarn Berry global cache' "$($P.L)\Yarn\Berry\cache" -Mode prune -Dev $true)
12
+ (New-Target 1 'pnpm content store' "$($P.L)\pnpm\store" -Mode prune -Dev $true -Note 'pnpm store prune is used instead when pnpm is installed')
13
+ (New-Target 1 'pnpm metadata cache' "$($P.L)\pnpm-cache" -Mode prune -Dev $true)
14
+ (New-Target 1 'bun install cache' "$($P.U)\.bun\install\cache" -Mode prune -Dev $true -Note 'global bun packages stay')
15
+ (New-Target 1 'deno deps cache' "$($P.L)\deno\deps" -Mode prune -Dev $true -Note 'installed deno scripts (~\.deno\bin) stay')
16
+ (New-Target 1 'deno gen cache' "$($P.L)\deno\gen" -Mode prune -Dev $true)
17
+ (New-Target 1 'deno npm cache' "$($P.L)\deno\npm" -Mode prune -Dev $true)
18
+ (New-Target 1 'deno remote cache' "$($P.L)\deno\remote" -Mode prune -Dev $true)
19
+ (New-Target 1 'pip cache' "$($P.L)\pip\cache" -Mode prune -Dev $true)
20
+ (New-Target 1 'uv cache' "$($P.L)\uv\cache" -Mode prune -Dev $true -Note 'uv cache prune runs first when uv is installed')
21
+ (New-Target 1 'poetry cache' "$($P.L)\pypoetry\Cache" -Mode prune -Dev $true)
22
+ (New-Target 1 'Composer cache (files)' "$($P.L)\Composer\files" -Mode prune -Dev $true)
23
+ (New-Target 1 'Composer cache (roaming)' "$($P.A)\Composer\cache" -Mode prune -Dev $true)
24
+ (New-Target 1 'NuGet http cache' "$($P.L)\NuGet\v3-cache" -Mode clear)
25
+ (New-Target 1 'NuGet plugins cache' "$($P.L)\NuGet\plugins-cache" -Mode clear)
26
+ (New-Target 1 'NuGet scratch' "$($P.L)\Temp\NuGetScratch" -Mode clear)
27
+ (New-Target 1 'NuGet global packages' "$($P.U)\.nuget\packages" -Mode prune -Dev $true -Note 'restored on the next dotnet restore')
28
+ (New-Target 1 'Cargo registry cache' "$($P.U)\.cargo\registry\cache" -Mode prune -Dev $true -Note 'installed cargo binaries (~\.cargo\bin) stay')
29
+ (New-Target 1 'Cargo registry sources' "$($P.U)\.cargo\registry\src" -Mode prune -Dev $true)
30
+ (New-Target 1 'Cargo git checkouts' "$($P.U)\.cargo\git\checkouts" -Mode prune -Dev $true)
31
+ (New-Target 1 'Go build cache' "$($P.L)\go-build" -Mode prune -Dev $true)
32
+ (New-Target 1 'Go module download cache' "$($P.U)\go\pkg\mod\cache\download" -Mode prune -Dev $true)
33
+ (New-Target 1 'Dart/Flutter pub cache' "$($P.L)\Pub\Cache" -Mode prune -Dev $true -Note 'redownloaded by flutter pub get')
34
+ (New-Target 1 'Dart/Flutter pub cache (legacy)' "$($P.A)\Pub\Cache" -Mode prune -Dev $true)
35
+ (New-Target 1 'Dart analysis server cache' "$($P.U)\.dartServer" -Mode prune -Dev $true)
36
+ (New-Target 1 'Electron download cache' "$($P.L)\electron\Cache" -Mode prune -Dev $true)
37
+ (New-Target 1 'electron-builder cache' "$($P.L)\electron-builder\Cache" -Mode prune -Dev $true)
38
+ (New-Target 1 'node-gyp headers cache' "$($P.L)\node-gyp\Cache" -Mode prune -Dev $true)
39
+ (New-Target 1 'Scoop installer cache' "$($P.U)\scoop\cache" -Mode prune -Dev $true)
40
+ )
41
+ }
42
+
43
+ function Invoke-Section01 {
44
+ $ws = $Script:WS
45
+ Write-SectionIntro @(
46
+ 'Package-manager caches only re-download what a future install needs. Globally installed packages,',
47
+ 'toolchain managers (nvm, Volta, corepack, pnpm global, cargo bin, go bin) are protected and never touched.'
48
+ ) -Dev $true
49
+ $targets = Get-Targets01
50
+ $null = Show-TargetSizes $targets
51
+ if (-not (Confirm-Section 'Prune package-manager caches now?')) { Write-Info 'skipped'; return }
52
+ # Tool-native smart prunes first: they know which entries are still referenced.
53
+ if ((Test-CommandPresent 'pnpm') -and $ws.Developer -ne $false -and -not $ws.PurgeAll) {
54
+ Write-Info 'pnpm store prune - removes packages no project references'
55
+ $r = Invoke-External -FilePath 'pnpm' -ArgumentList @('store', 'prune') -Destructive -Quiet -Label 'pnpm store prune'
56
+ if ($r.Ran -and $r.ExitCode -eq 0) { Write-Ok 'pnpm store pruned' }
57
+ $targets = @($targets | Where-Object { $_.Label -ne 'pnpm content store' })
58
+ }
59
+ if ((Test-CommandPresent 'uv') -and $ws.Developer -ne $false -and -not $ws.PurgeAll) {
60
+ Write-Info 'uv cache prune - removes unreachable wheels and sources'
61
+ $r = Invoke-External -FilePath 'uv' -ArgumentList @('cache', 'prune') -Destructive -Quiet -Label 'uv cache prune'
62
+ if ($r.Ran -and $r.ExitCode -eq 0) { Write-Ok 'uv cache pruned' }
63
+ }
64
+ if ((Test-CommandPresent 'npm') -and $ws.PurgeAll) {
65
+ Write-Info 'npm cache clean --force'
66
+ $null = Invoke-External -FilePath 'npm' -ArgumentList @('cache', 'clean', '--force') -Destructive -Quiet -Label 'npm cache clean'
67
+ }
68
+ $null = Invoke-TargetList $targets
69
+ }