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.
package/lib/scan.ps1 ADDED
@@ -0,0 +1,94 @@
1
+ # scan.ps1 - read-only scanners: the target table, --list-targets and the pre-scan.
2
+ # Every module contributes its targets through a Get-TargetsNN function; nothing here deletes.
3
+
4
+ function New-Target {
5
+ <# .SYNOPSIS Describe one path the tool can touch and how it is treated.
6
+ Kind: dir | file | glob (expands to dirs) | fileglob (expands to files) | cmd (informational)
7
+ Mode: prune (files idle >= Days) | clear (everything inside) | clear-old (entries idle >= Days) |
8
+ units (each child is a version/unit; idle gate, -KeepNewest per group) | file (remove the file)
9
+ Dev: $true = a developer cache: pruned by the idle gate in developer mode, cleared otherwise.
10
+ Guard: process names; the target is skipped while any of them runs. #>
11
+ param([int] $Section, [string] $Label, [string] $Path, [string] $Kind = 'dir', [string] $Mode = 'prune',
12
+ [int] $Days = 0, [bool] $Dev = $false, [string[]] $Guard = @(), [bool] $KeepNewest = $false,
13
+ [scriptblock] $GroupBy = $null, [string[]] $ExcludeNames = @(), [string] $Note = '')
14
+ return [pscustomobject]@{ Section = $Section; Label = $Label; Path = $Path; Kind = $Kind; Mode = $Mode; Days = $Days
15
+ Dev = $Dev; Guard = $Guard; KeepNewest = $KeepNewest; GroupBy = $GroupBy; ExcludeNames = $ExcludeNames; Note = $Note }
16
+ }
17
+
18
+ function Get-AllTargets {
19
+ <# .SYNOPSIS Collect every declared target from the loaded modules (Get-Targets00 .. Get-Targets21). #>
20
+ $all = @()
21
+ for ($i = 0; $i -le 21; $i++) {
22
+ $fn = 'Get-Targets{0:00}' -f $i
23
+ if (Get-Command $fn -ErrorAction SilentlyContinue) { $all += @(& $fn) }
24
+ }
25
+ return $all
26
+ }
27
+
28
+ function Resolve-TargetPaths {
29
+ <# .SYNOPSIS Expand a glob target into concrete existing paths; a plain path returns itself when present. #>
30
+ param([pscustomobject] $Target)
31
+ if ($Target.Kind -eq 'cmd') { return @() }
32
+ if ($Target.Kind -in 'chromium', 'firefox', 'electron', 'editor') { return @(Get-TargetCachePaths $Target) }
33
+ if ($Target.Kind -eq 'glob') {
34
+ try { return @(Get-Item -Path $Target.Path -Force -ErrorAction SilentlyContinue | Where-Object { $_ -is [IO.DirectoryInfo] } | ForEach-Object { $_.FullName }) } catch { return @() }
35
+ }
36
+ if ($Target.Kind -eq 'fileglob') {
37
+ try { return @(Get-Item -Path $Target.Path -Force -ErrorAction SilentlyContinue | Where-Object { $_ -is [IO.FileInfo] } | ForEach-Object { $_.FullName }) } catch { return @() }
38
+ }
39
+ if (Test-PathPresent $Target.Path) { return @((Get-FullPath $Target.Path)) }
40
+ return @()
41
+ }
42
+
43
+ function Show-ScanTable {
44
+ <# .SYNOPSIS Read-only table of every target with its current size. Returns total bytes found. #>
45
+ param([int[]] $Sections = @())
46
+ $total = [long]0
47
+ $targets = @(Get-AllTargets)
48
+ if ($Sections.Count -gt 0) { $targets = @($targets | Where-Object { $Sections -contains $_.Section }) }
49
+ $bySection = $targets | Group-Object Section | Sort-Object { [int]$_.Name }
50
+ foreach ($g in $bySection) {
51
+ $sec = Get-Section ([int]$g.Name)
52
+ Write-Section ("[{0:00}] {1}" -f $sec.Id, $sec.Title)
53
+ Write-UiLine (" {0,-46} {1,10} {2}" -f 'ITEM', 'SIZE', 'PATH') 'White'
54
+ $secTotal = [long]0
55
+ foreach ($t in $g.Group) {
56
+ if ($t.Kind -eq 'cmd') { Write-UiLine (" {0,-46} {1,10} {2}" -f $t.Label, '(cmd)', $t.Path) 'DarkGray'; continue }
57
+ $paths = @(Resolve-TargetPaths $t)
58
+ if ($paths.Count -eq 0) { Write-UiLine (" {0,-46} {1,10} {2}" -f $t.Label, 'absent', $t.Path) 'DarkGray'; continue }
59
+ $bytes = [long]0
60
+ foreach ($p in $paths) { $bytes += Get-DirectoryBytes $p }
61
+ $secTotal += $bytes
62
+ $shown = $t.Path
63
+ if ($paths.Count -gt 1) { $shown = "$($t.Path) ($($paths.Count) matches)" }
64
+ Write-UiLine (" {0,-46} {1,10} {2}" -f $t.Label, (Format-Bytes $bytes), $shown) 'Gray'
65
+ }
66
+ Write-UiLine (" {0,-46} {1,10}" -f 'section total on disk', (Format-Bytes $secTotal)) 'Cyan'
67
+ $total += $secTotal
68
+ }
69
+ Write-UiLine '' 'Gray'
70
+ Write-Info ("Currently on disk across all listed targets: " + (Format-Bytes $total))
71
+ Write-Note 'These are sizes on disk, not what a run would delete: the idle gate keeps recently used files, and running apps are skipped.'
72
+ return $total
73
+ }
74
+
75
+ function Show-TargetList {
76
+ <# .SYNOPSIS --list-targets: every path the tool can touch, plus the protected lists. #>
77
+ Write-Box 'Cleanup targets' 'Every path this tool can touch, grouped by section (read-only)'
78
+ $targets = @(Get-AllTargets)
79
+ foreach ($g in ($targets | Group-Object Section | Sort-Object { [int]$_.Name })) {
80
+ $sec = Get-Section ([int]$g.Name)
81
+ Write-Section ("[{0:00}] {1}" -f $sec.Id, $sec.Title)
82
+ foreach ($t in $g.Group) {
83
+ $note = ''
84
+ if ($t.Note) { $note = " - $($t.Note)" }
85
+ Write-UiLine (" {0,-44} {1}{2}" -f $t.Label, $t.Path, $note) 'Gray'
86
+ }
87
+ }
88
+ Write-Section 'PROTECTED - never deleted, no flag bypasses this'
89
+ foreach ($s in $Script:WS_PROTECT.Subtrees) { Write-UiLine " $($Script:WS.Glyph.bullet) $s" 'DarkGray' }
90
+ Write-UiLine " $($Script:WS.Glyph.bullet) every drive root, Windows, System32, Program Files, ProgramData, the user profile root and AppData roots" 'DarkGray'
91
+ Write-UiLine " $($Script:WS.Glyph.bullet) browser profile data (Local Storage, IndexedDB, cookies, history, bookmarks, extensions, PWA CacheStorage)" 'DarkGray'
92
+ Write-UiLine " $($Script:WS.Glyph.bullet) editor user data (settings, globalStorage, local History); UWP LocalState; toolchains (nvm, npm globals, corepack, ...)" 'DarkGray'
93
+ Write-UiLine " $($Script:WS.Glyph.bullet) NTUSER.DAT, UsrClass.dat, hiberfil/pagefile/swapfile (only powercfg touches hiberfil), Prefetch, Windows\Installer, WinSxS (DISM only)" 'DarkGray'
94
+ }
package/lib/ui.ps1 ADDED
@@ -0,0 +1,232 @@
1
+ # ui.ps1 - console output, glyphs, colours and prompts. ASCII only; glyphs come from [char] codes.
2
+ # Every human-readable line goes through Write-UiLine so it is also logged and, in --json mode,
3
+ # routed to stderr (stdout then carries only the JSON summary).
4
+
5
+ function Initialize-Ui {
6
+ <# .SYNOPSIS Decide colour + glyph set from flags, environment and redirection state. #>
7
+ $ws = $Script:WS
8
+ $outRedirected = [Console]::IsOutputRedirected
9
+ $inRedirected = [Console]::IsInputRedirected
10
+ $noColorEnv = (-not [string]::IsNullOrEmpty($env:NO_COLOR)) -or ($env:WINDOWSWEEP_NO_COLOR -eq '1')
11
+ $ws.Color = (-not $ws.NoColor) -and (-not $noColorEnv) -and (-not $outRedirected) -and (-not $ws.JsonMode)
12
+ $ws.Interactive = (-not $inRedirected) -and (-not $ws.JsonMode)
13
+ $useUnicode = (-not $ws.Ascii) -and (-not $outRedirected) -and ($env:WINDOWSWEEP_ASCII -ne '1')
14
+ if ($useUnicode) {
15
+ $ws.Glyph = @{
16
+ ok = [string][char]0x2713; err = [string][char]0x2717; info = [string][char]0x2139; warn = '!'
17
+ bullet = [string][char]0x00B7; arrow = [string][char]0x2192; hline = [string][char]0x2500
18
+ dline = [string][char]0x2550; tl = [string][char]0x2554; tr = [string][char]0x2557
19
+ bl = [string][char]0x255A; br = [string][char]0x255D; vbar = [string][char]0x2551
20
+ }
21
+ try {
22
+ $ws.PrevOutputEncoding = [Console]::OutputEncoding
23
+ [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
24
+ } catch { $ws.PrevOutputEncoding = $null }
25
+ } else {
26
+ $ws.Glyph = @{ ok = '+'; err = 'x'; info = 'i'; warn = '!'; bullet = '*'; arrow = '->'; hline = '-'; dline = '='; tl = '+'; tr = '+'; bl = '+'; br = '+'; vbar = '|' }
27
+ }
28
+ }
29
+
30
+ function Restore-Ui {
31
+ <# .SYNOPSIS Put the console encoding back the way we found it. #>
32
+ $ws = $Script:WS
33
+ if ($ws.PrevOutputEncoding) { try { [Console]::OutputEncoding = $ws.PrevOutputEncoding } catch { $null = $_ } }
34
+ }
35
+
36
+ function Write-UiLine {
37
+ <# .SYNOPSIS Print one line (coloured when allowed) and append it to the log. #>
38
+ param([string] $Text = '', [string] $Color = 'Gray', [switch] $NoLog, [switch] $NoNewline)
39
+ $ws = $Script:WS
40
+ if (-not $NoLog) { Write-Log $Text }
41
+ if ($ws.Mute) { return }
42
+ if ($ws.JsonMode) { [Console]::Error.WriteLine($Text); return }
43
+ if ($ws.Color) {
44
+ if ($NoNewline) { Write-Host $Text -ForegroundColor $Color -NoNewline } else { Write-Host $Text -ForegroundColor $Color }
45
+ } else {
46
+ if ($NoNewline) { Write-Host $Text -NoNewline } else { Write-Host $Text }
47
+ }
48
+ }
49
+
50
+ function Write-Info {
51
+ param([string] $Text)
52
+ if ($Script:WS.Quiet) { Write-Log "INFO $Text"; return }
53
+ Write-UiLine ("$($Script:WS.Glyph.info) $Text") 'Cyan'
54
+ }
55
+
56
+ function Write-Ok {
57
+ param([string] $Text)
58
+ Write-UiLine ("$($Script:WS.Glyph.ok) $Text") 'Green'
59
+ }
60
+
61
+ function Write-Warn {
62
+ param([string] $Text)
63
+ Write-UiLine ("$($Script:WS.Glyph.warn) $Text") 'Yellow'
64
+ }
65
+
66
+ function Write-Err {
67
+ param([string] $Text)
68
+ Write-UiLine ("$($Script:WS.Glyph.err) $Text") 'Red'
69
+ }
70
+
71
+ function Write-Note {
72
+ param([string] $Text)
73
+ if ($Script:WS.Quiet) { Write-Log "NOTE $Text"; return }
74
+ Write-UiLine (" $Text") 'DarkGray'
75
+ }
76
+
77
+ function Write-Plain {
78
+ param([string] $Text)
79
+ Write-UiLine $Text 'Gray'
80
+ }
81
+
82
+ function Write-DryRun {
83
+ param([string] $Text)
84
+ Write-UiLine (" [dry-run] $Text") 'Magenta'
85
+ }
86
+
87
+ function Write-Section {
88
+ <# .SYNOPSIS A dashed section heading. #>
89
+ param([string] $Title)
90
+ $h = $Script:WS.Glyph.hline
91
+ Write-UiLine '' 'Gray'
92
+ Write-UiLine ("$h$h $Title $h$h") 'Blue'
93
+ }
94
+
95
+ function Write-Separator {
96
+ Write-UiLine ([string]::new([char]($Script:WS.Glyph.hline), 78)) 'DarkGray'
97
+ }
98
+
99
+ function Write-Box {
100
+ <# .SYNOPSIS Double-lined title box with an optional dim subtitle. #>
101
+ param([string] $Title, [string] $Subtitle = '')
102
+ $line = [string]::new([char]($Script:WS.Glyph.dline), 78)
103
+ Write-UiLine '' 'Gray'
104
+ Write-UiLine $line 'Blue'
105
+ Write-UiLine (" $Title") 'Blue'
106
+ if ($Subtitle) { Write-UiLine (" $Subtitle") 'DarkGray' }
107
+ Write-UiLine $line 'Blue'
108
+ }
109
+
110
+ function Write-Step {
111
+ <# .SYNOPSIS Walkthrough step header: [ STEP n/N ] Title #>
112
+ param([int] $N, [int] $Total, [string] $Title)
113
+ Write-UiLine '' 'Gray'
114
+ Write-Separator
115
+ Write-UiLine (" [ STEP $N/$Total ] $Title") 'Cyan'
116
+ Write-Separator
117
+ }
118
+
119
+ function Write-Kv {
120
+ param([string] $Key, [string] $Value)
121
+ Write-UiLine (" {0,-24} {1}" -f $Key, $Value) 'Gray'
122
+ }
123
+
124
+ function Write-Banner {
125
+ $g = $Script:WS.Glyph
126
+ $inner = " $Script:WS_NAME v$(Get-ToolVersion) - safe, developer-aware Windows cleanup "
127
+ $bar = [string]::new([char]($g.dline), $inner.Length)
128
+ Write-UiLine ("$($g.tl)$bar$($g.tr)") 'Blue'
129
+ Write-UiLine ("$($g.vbar)$inner$($g.vbar)") 'Blue'
130
+ Write-UiLine ("$($g.bl)$bar$($g.br)") 'Blue'
131
+ }
132
+
133
+ function Format-Bytes {
134
+ <# .SYNOPSIS Human-readable size (B, KB, MB, GB, TB). #>
135
+ param([double] $Bytes)
136
+ if ($Bytes -lt 0) { $Bytes = 0 }
137
+ $units = @('B', 'KB', 'MB', 'GB', 'TB')
138
+ $i = 0
139
+ while ($Bytes -ge 1024 -and $i -lt ($units.Length - 1)) { $Bytes = $Bytes / 1024; $i++ }
140
+ if ($i -eq 0) { return ("{0:N0} {1}" -f $Bytes, $units[$i]) }
141
+ return ("{0:N1} {1}" -f $Bytes, $units[$i])
142
+ }
143
+
144
+ function Format-Duration {
145
+ param([double] $Seconds)
146
+ $m = [math]::Floor($Seconds / 60); $s = [math]::Floor($Seconds % 60)
147
+ return ("{0}m {1:00}s" -f $m, $s)
148
+ }
149
+
150
+ function Read-Line {
151
+ <# .SYNOPSIS Read one line from the console, or $null when not interactive / at EOF. #>
152
+ param([string] $Prompt = '')
153
+ if (-not $Script:WS.Interactive) { return $null }
154
+ if ($Prompt) { Write-UiLine $Prompt 'White' -NoNewline -NoLog }
155
+ try { $line = [Console]::In.ReadLine() } catch { return $null }
156
+ if ($null -eq $line) { return $null }
157
+ Write-Log ("> $line")
158
+ return $line
159
+ }
160
+
161
+ function Confirm-Ui {
162
+ <# .SYNOPSIS y/N prompt. --yes auto-confirms (unless -NoAutoYes); non-interactive runs answer no. #>
163
+ param([string] $Prompt = 'Proceed?', [string] $Default = 'n', [switch] $NoAutoYes)
164
+ $ws = $Script:WS
165
+ if ($ws.Yes -and -not $NoAutoYes) { Write-UiLine (" [auto-yes] $Prompt") 'DarkGray'; return $true }
166
+ if (-not $ws.Interactive) {
167
+ Write-Note "non-interactive session: '$Prompt' answered no (pass --yes to confirm in batch)"
168
+ return $false
169
+ }
170
+ $hint = '[y/N]'
171
+ if ($Default -eq 'y') { $hint = '[Y/n]' }
172
+ $reply = Read-Line ("? $Prompt $hint ")
173
+ if ($null -eq $reply) { return $false }
174
+ $reply = $reply.Trim()
175
+ if ($reply -eq '') { $reply = $Default }
176
+ return ($reply -match '^(y|yes)$')
177
+ }
178
+
179
+ function Confirm-Typed {
180
+ <# .SYNOPSIS Critical confirmation: the user must type the literal word. Never auto-answered. #>
181
+ param([string] $Prompt, [string] $Word = 'yes')
182
+ if (-not $Script:WS.Interactive) { Write-Note "non-interactive session: '$Prompt' requires a typed '$Word'; skipped"; return $false }
183
+ Write-Warn $Prompt
184
+ $reply = Read-Line (" Type '$Word' to proceed: ")
185
+ if ($null -eq $reply) { return $false }
186
+ return ($reply.Trim() -eq $Word)
187
+ }
188
+
189
+ function Read-MultiSelect {
190
+ <# .SYNOPSIS Parse "1,3,5-7" / all / none into a sorted unique list of 1-based indexes. #>
191
+ param([int] $Total)
192
+ if ($Total -le 0) { return @() }
193
+ if ($Script:WS.Yes) { return @(1..$Total) }
194
+ Write-UiLine " Selection: 1,3,5-10 | all | none (Enter = none)" 'DarkGray' -NoLog
195
+ $reply = Read-Line (" Select items (1..$Total): ")
196
+ return (ConvertTo-IndexList -Text $reply -Total $Total)
197
+ }
198
+
199
+ function ConvertTo-IndexList {
200
+ <# .SYNOPSIS Pure parser behind Read-MultiSelect (kept separate so the self-test can exercise it). #>
201
+ param([string] $Text, [int] $Total)
202
+ if ([string]::IsNullOrWhiteSpace($Text)) { return @() }
203
+ $t = $Text.Trim().ToLowerInvariant()
204
+ if ($t -in @('n', 'none')) { return @() }
205
+ if ($t -in @('a', 'all')) { return @(1..$Total) }
206
+ $set = New-Object System.Collections.Generic.SortedSet[int]
207
+ foreach ($part in ($t -split ',')) {
208
+ $part = $part.Trim()
209
+ if ($part -match '^(\d+)-(\d+)$') {
210
+ $a = [int]$Matches[1]; $b = [int]$Matches[2]
211
+ if ($a -gt $b) { $tmp = $a; $a = $b; $b = $tmp }
212
+ for ($i = $a; $i -le $b; $i++) { if ($i -ge 1 -and $i -le $Total) { $null = $set.Add($i) } }
213
+ } elseif ($part -match '^\d+$') {
214
+ $i = [int]$part
215
+ if ($i -ge 1 -and $i -le $Total) { $null = $set.Add($i) }
216
+ }
217
+ }
218
+ return @($set)
219
+ }
220
+
221
+ function Read-Choice {
222
+ <# .SYNOPSIS One-key choice; returns the lower-cased key or the default. #>
223
+ param([string] $Prompt, [string] $Default = '')
224
+ $reply = Read-Line $Prompt
225
+ if ($null -eq $reply -or $reply.Trim() -eq '') { return $Default }
226
+ return $reply.Trim().ToLowerInvariant()
227
+ }
228
+
229
+ function Wait-Enter {
230
+ if (-not $Script:WS.Interactive) { return }
231
+ $null = Read-Line " - press Enter to continue - "
232
+ }
@@ -0,0 +1,36 @@
1
+ # android_avd.ps1 - section 4: Android emulators (AVDs). Slow to recreate, so per-AVD idle gate and never in --all.
2
+
3
+ function Get-Targets04 {
4
+ return @((New-Target 4 'Android AVDs (each .avd + .ini pair)' "$($Script:P.U)\.android\avd" -Mode units -Dev $true -Note "an AVD goes only when nothing inside it changed for $($Script:WS.Days)+ days"))
5
+ }
6
+
7
+ function Invoke-Section04 {
8
+ $ws = $Script:WS
9
+ $root = Join-Path $Script:P.U '.android\avd'
10
+ Write-SectionIntro @(
11
+ 'An emulator image is 5-15 GB and takes minutes to recreate. Each AVD is one unit: it is removed only when',
12
+ "every file inside it has been idle for $($ws.Days)+ days (booting an emulator updates its files)."
13
+ ) -Dev $true
14
+ if (-not (Test-DirPresent $root)) { Write-Info 'no AVDs found'; return }
15
+ if (Test-ProcessRunning @('qemu-system-x86_64', 'emulator', 'emulator64-crash-service')) { Write-Warn 'an Android emulator is running - section skipped'; return }
16
+ $avds = @()
17
+ foreach ($e in (Get-ChildEntries $root)) {
18
+ if (-not ($e -is [IO.DirectoryInfo]) -or $e.Name -notlike '*.avd') { continue }
19
+ $full = Remove-LongPrefix $e.FullName
20
+ $avds += [pscustomobject]@{ Name = $e.Name; Path = $full; Ini = (Join-Path $root ($e.Name -replace '\.avd$', '.ini')); Idle = (Get-IdleDays $full); Bytes = (Get-DirectoryBytes $full) }
21
+ }
22
+ if ($avds.Count -eq 0) { Write-Info 'no AVDs found'; return }
23
+ Write-UiLine (" {0,-36} {1,10} {2,8}" -f 'AVD', 'SIZE', 'IDLE') 'White'
24
+ foreach ($a in $avds) { Write-UiLine (" {0,-36} {1,10} {2,7}d" -f $a.Name, (Format-Bytes $a.Bytes), $a.Idle) 'Gray' }
25
+ $avds = @($avds)
26
+ $stale = @($avds | Where-Object { $_.Idle -ge $ws.Days -or $ws.PurgeAll })
27
+ if ($stale.Count -eq 0) { Write-Info "every AVD was used within the last $($ws.Days) days - nothing to prune"; return }
28
+ if (-not (Confirm-Section "Remove $($stale.Count) idle AVD(s)?" 'n')) { Write-Info 'skipped'; return }
29
+ foreach ($a in $stale) {
30
+ $r = Remove-PathSafe -Path $a.Path -Within $root -Label $a.Name
31
+ if ($r.Removed) {
32
+ if (Test-PathPresent $a.Ini) { $null = Remove-PathSafe -Path $a.Ini -Within $root -Label "$($a.Name) ini" }
33
+ if (-not $ws.DryRun) { Write-Ok ("removed AVD $($a.Name) ($($a.Idle)d idle, " + (Format-Bytes $r.Bytes) + ')') }
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,58 @@
1
+ # app_caches.ps1 - section 8: desktop-app caches (Electron / Chromium-based apps) and superseded Squirrel versions.
2
+
3
+ function Get-Targets08 {
4
+ $P = $Script:P
5
+ return @(
6
+ (New-Target 8 'Discord' "$($P.A)\discord" -Kind electron -Guard @('Discord'))
7
+ (New-Target 8 'Discord Canary' "$($P.A)\discordcanary" -Kind electron -Guard @('DiscordCanary'))
8
+ (New-Target 8 'Discord PTB' "$($P.A)\discordptb" -Kind electron -Guard @('DiscordPTB'))
9
+ (New-Target 8 'Slack' "$($P.A)\Slack" -Kind electron -Guard @('slack'))
10
+ (New-Target 8 'Slack (Store)' "$($P.L)\Packages\*slack*\LocalCache\Roaming\Slack" -Kind electron -Guard @('slack'))
11
+ (New-Target 8 'Microsoft Teams (classic)' "$($P.A)\Microsoft\Teams" -Kind electron -Guard @('Teams'))
12
+ (New-Target 8 'Microsoft Teams (new)' "$($P.L)\Packages\MSTeams_*\LocalCache\Microsoft\MSTeams\EBWebView\*" -Kind chromium -Guard @('ms-teams'))
13
+ (New-Target 8 'Zoom logs' "$($P.A)\Zoom\logs" -Mode clear-old -Days 30 -Guard @('Zoom'))
14
+ (New-Target 8 'Spotify streaming cache' "$($P.L)\Spotify\Storage" -Mode clear -Guard @('Spotify'))
15
+ (New-Target 8 'Spotify (Store) streaming cache' "$($P.L)\Packages\SpotifyAB.SpotifyMusic_*\LocalCache\Spotify\Storage" -Kind glob -Mode clear -Guard @('Spotify'))
16
+ (New-Target 8 'Postman' "$($P.A)\Postman" -Kind electron -Guard @('Postman'))
17
+ (New-Target 8 'Figma' "$($P.A)\Figma" -Kind electron -Guard @('Figma'))
18
+ (New-Target 8 'Notion' "$($P.A)\Notion" -Kind electron -Guard @('Notion'))
19
+ (New-Target 8 'Signal' "$($P.A)\Signal" -Kind electron -Guard @('Signal'))
20
+ (New-Target 8 'Skype' "$($P.A)\Microsoft\Skype for Desktop" -Kind electron -Guard @('Skype'))
21
+ (New-Target 8 'GitHub Desktop' "$($P.A)\GitHub Desktop" -Kind electron -Guard @('GitHubDesktop'))
22
+ (New-Target 8 'Obsidian' "$($P.A)\obsidian" -Kind electron -Guard @('Obsidian'))
23
+ (New-Target 8 'Claude desktop app' "$($P.A)\Claude" -Kind electron -Guard @('Claude'))
24
+ (New-Target 8 'Linear' "$($P.A)\Linear" -Kind electron -Guard @('Linear'))
25
+ (New-Target 8 'Granola' "$($P.A)\Granola" -Kind electron -Guard @('Granola'))
26
+ (New-Target 8 'Insomnia' "$($P.A)\Insomnia" -Kind electron -Guard @('Insomnia'))
27
+ (New-Target 8 'Steam web cache' "$($P.L)\Steam\htmlcache" -Mode clear -Guard @('steam'))
28
+ (New-Target 8 'Epic Games Launcher web cache' "$($P.L)\EpicGamesLauncher\Saved\webcache*" -Kind glob -Mode clear -Guard @('EpicGamesLauncher'))
29
+ (New-Target 8 'Epic Games Launcher logs' "$($P.L)\EpicGamesLauncher\Saved\Logs" -Mode clear-old -Days 30)
30
+ (New-Target 8 'Adobe media cache files' "$($P.A)\Adobe\Common\Media Cache Files" -Mode prune -Guard @('Adobe Premiere Pro', 'AfterFX', 'Adobe Audition') -Note 'Premiere / After Effects rebuild them on open')
31
+ (New-Target 8 'Adobe media cache database' "$($P.A)\Adobe\Common\Media Cache" -Mode prune -Guard @('Adobe Premiere Pro', 'AfterFX', 'Adobe Audition'))
32
+ (New-Target 8 'OBS Studio logs' "$($P.A)\obs-studio\logs" -Mode clear-old -Days 30)
33
+ (New-Target 8 'OBS Studio crash reports' "$($P.A)\obs-studio\crashes" -Mode clear-old -Days 30)
34
+ (New-Target 8 'Squirrel installer temp' "$($P.L)\SquirrelTemp" -Mode clear)
35
+ (New-Target 8 'Electron updater downloads' "$($P.L)\@*-updater" -Kind glob -Mode clear-old -Days 7 -Note 'pending update installers older than a week')
36
+ (New-Target 8 'Discord superseded versions' "$($P.L)\Discord" -Kind cmd -Guard @('Discord') -Note 'app-x.y.z folders other than the newest')
37
+ (New-Target 8 'Postman superseded versions' "$($P.L)\Postman" -Kind cmd -Guard @('Postman'))
38
+ (New-Target 8 'Figma superseded versions' "$($P.L)\Figma" -Kind cmd -Guard @('Figma'))
39
+ (New-Target 8 'GitHub Desktop superseded versions' "$($P.L)\GitHubDesktop" -Kind cmd -Guard @('GitHubDesktop'))
40
+ (New-Target 8 'Slack superseded versions' "$($P.L)\slack" -Kind cmd -Guard @('slack'))
41
+ )
42
+ }
43
+
44
+ function Invoke-Section08 {
45
+ Write-SectionIntro @(
46
+ 'Chat, design and API apps are browsers in disguise: their Cache, Code Cache and GPU caches rebuild on the next',
47
+ 'start, and installers that keep old app-x.y.z versions beside the current one waste space. Accounts, chat',
48
+ 'databases and settings are never touched; a running app is skipped.'
49
+ )
50
+ $targets = Get-Targets08
51
+ $null = Show-TargetSizes @($targets | Where-Object { $_.Kind -ne 'cmd' })
52
+ if (-not (Confirm-Section 'Clean desktop-app caches now?')) { Write-Info 'skipped'; return }
53
+ $null = Invoke-TargetList @($targets | Where-Object { $_.Kind -ne 'cmd' })
54
+ foreach ($t in @($targets | Where-Object { $_.Kind -eq 'cmd' })) {
55
+ if (-not (Test-DirPresent $t.Path)) { continue }
56
+ $null = Remove-SupersededVersions -Root $t.Path -Guard $t.Guard -Label $t.Label
57
+ }
58
+ }
@@ -0,0 +1,33 @@
1
+ # browsers.ps1 - section 7: browser caches for every profile. Profile data is never touched.
2
+
3
+ function Get-Targets07 {
4
+ $P = $Script:P
5
+ return @(
6
+ (New-Target 7 'Google Chrome' "$($P.L)\Google\Chrome\User Data" -Kind chromium -Guard @('chrome') -Note 'cache folders of every profile')
7
+ (New-Target 7 'Google Chrome Beta' "$($P.L)\Google\Chrome Beta\User Data" -Kind chromium -Guard @('chrome'))
8
+ (New-Target 7 'Google Chrome Dev' "$($P.L)\Google\Chrome Dev\User Data" -Kind chromium -Guard @('chrome'))
9
+ (New-Target 7 'Google Chrome Canary' "$($P.L)\Google\Chrome SxS\User Data" -Kind chromium -Guard @('chrome'))
10
+ (New-Target 7 'Microsoft Edge' "$($P.L)\Microsoft\Edge\User Data" -Kind chromium -Guard @('msedge'))
11
+ (New-Target 7 'Brave' "$($P.L)\BraveSoftware\Brave-Browser\User Data" -Kind chromium -Guard @('brave'))
12
+ (New-Target 7 'Vivaldi' "$($P.L)\Vivaldi\User Data" -Kind chromium -Guard @('vivaldi'))
13
+ (New-Target 7 'Opera' "$($P.A)\Opera Software\Opera Stable" -Kind chromium -Guard @('opera'))
14
+ (New-Target 7 'Opera GX' "$($P.A)\Opera Software\Opera GX Stable" -Kind chromium -Guard @('opera'))
15
+ (New-Target 7 'Chromium' "$($P.L)\Chromium\User Data" -Kind chromium -Guard @('chrome', 'chromium'))
16
+ (New-Target 7 'Arc' "$($P.L)\Packages\TheBrowserCompany.Arc_*\LocalCache\Local\Arc\User Data" -Kind chromium -Guard @('Arc'))
17
+ (New-Target 7 'Firefox' "$($P.L)\Mozilla\Firefox\Profiles" -Kind firefox -Guard @('firefox') -Note 'cache2, startupCache, thumbnails, jumpListCache')
18
+ (New-Target 7 'LibreWolf' "$($P.L)\LibreWolf\Profiles" -Kind firefox -Guard @('librewolf'))
19
+ (New-Target 7 'Waterfox' "$($P.L)\Waterfox\Profiles" -Kind firefox -Guard @('waterfox'))
20
+ )
21
+ }
22
+
23
+ function Invoke-Section07 {
24
+ Write-SectionIntro @(
25
+ 'Browser caches rebuild as you browse; the first page loads afterwards are a little slower. Cookies, logins,',
26
+ 'history, bookmarks, extensions, Local Storage, IndexedDB and PWA CacheStorage are never touched.',
27
+ 'A browser that is open right now is skipped - close it and run this section again.'
28
+ )
29
+ $targets = Get-Targets07
30
+ $null = Show-TargetSizes $targets
31
+ if (-not (Confirm-Section 'Clear browser caches now?')) { Write-Info 'skipped'; return }
32
+ $null = Invoke-TargetList $targets
33
+ }
@@ -0,0 +1,35 @@
1
+ # build_tools.ps1 - section 2: build-tool caches (Gradle, Maven, Android, Unity, JetBrains).
2
+
3
+ function Get-Targets02 {
4
+ $P = $Script:P
5
+ return @(
6
+ (New-Target 2 'Gradle caches' "$($P.U)\.gradle\caches" -Mode prune -Dev $true -Note 'Gradle re-downloads what a build needs')
7
+ (New-Target 2 'Gradle wrapper distributions' "$($P.U)\.gradle\wrapper\dists" -Mode units -Dev $true -KeepNewest $true -Note 'the newest distribution is always kept')
8
+ (New-Target 2 'Gradle daemon logs' "$($P.U)\.gradle\daemon" -Mode prune -Days 7)
9
+ (New-Target 2 'Gradle temp' "$($P.U)\.gradle\.tmp" -Mode clear)
10
+ (New-Target 2 'Maven local repository' "$($P.U)\.m2\repository" -Mode prune -Dev $true -Note 'restored by the next mvn build')
11
+ (New-Target 2 'Android SDK manager cache' "$($P.U)\.android\cache" -Mode clear)
12
+ (New-Target 2 'Android build cache' "$($P.U)\.android\build-cache" -Mode prune -Dev $true)
13
+ (New-Target 2 'Android SDK temp' "$($P.L)\Android\Sdk\.temp" -Mode clear)
14
+ (New-Target 2 'Android SDK download leftovers' "$($P.L)\Android\Sdk\.downloadIntermediates" -Mode clear)
15
+ (New-Target 2 'Unity cache' "$($P.L)\Unity\cache" -Mode prune -Dev $true)
16
+ (New-Target 2 'JetBrains IDE caches (idle IDE versions)' "$($P.L)\JetBrains" -Mode units -Dev $true -ExcludeNames @('Toolbox') -Note 'a whole IDE version idle N+ days; the Toolbox itself is protected')
17
+ (New-Target 2 '.NET telemetry storage' "$($P.U)\.dotnet\TelemetryStorageService" -Mode clear)
18
+ )
19
+ }
20
+
21
+ function Invoke-Section02 {
22
+ Write-SectionIntro @(
23
+ 'Build caches are re-created by the next build. In developer mode only idle files go, so a project you',
24
+ 'compiled last week keeps its warm cache. JetBrains caches are removed per IDE version, only when that whole',
25
+ 'version has been idle for the window (an IDE you no longer run).'
26
+ ) -Dev $true
27
+ $targets = Get-Targets02
28
+ $null = Show-TargetSizes $targets
29
+ if (-not (Confirm-Section 'Prune build-tool caches now?')) { Write-Info 'skipped'; return }
30
+ if ((Test-CommandPresent 'gradle') -and -not $Script:WS.DryRun) {
31
+ $null = Invoke-External -FilePath 'gradle' -ArgumentList @('--stop') -Quiet -Label 'gradle --stop'
32
+ Write-Note 'stopped idle Gradle daemons so their caches are not locked'
33
+ }
34
+ $null = Invoke-TargetList $targets
35
+ }
@@ -0,0 +1,89 @@
1
+ # crash_trap.ps1 - on an unexpected error, bundle the log + latest report + a manifest into a local zip.
2
+ # Nothing is ever sent anywhere; the user decides whether to attach it to a bug report.
3
+
4
+ function Initialize-CrashTrap {
5
+ $Script:WS.CrashArmed = $true
6
+ }
7
+
8
+ function Get-FeedbackDir {
9
+ $d = $Script:WS.FeedbackDir
10
+ if (-not (Test-Path -LiteralPath $d)) { New-Item -ItemType Directory -Force -Path $d | Out-Null }
11
+ return $d
12
+ }
13
+
14
+ function Get-LatestReportPath {
15
+ $dir = $Script:WS.ReportsDir
16
+ if (-not (Test-Path -LiteralPath $dir)) { return $null }
17
+ $f = Get-ChildItem -LiteralPath $dir -Filter 'report-*.json' -File -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1
18
+ if ($f) { return $f.FullName }
19
+ return $null
20
+ }
21
+
22
+ function New-SystemManifest {
23
+ <# .SYNOPSIS Non-sensitive environment facts for a bug report. #>
24
+ param([string] $Kind, [string] $Extra = '')
25
+ $ws = $Script:WS
26
+ $lines = @(
27
+ "$Script:WS_NAME v$(Get-ToolVersion) - $Kind"
28
+ "Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss zzz')"
29
+ ''
30
+ '=== System info ==='
31
+ "OS: $($ws.OsCaption) ($([Environment]::OSVersion.Version))"
32
+ "PowerShell: $($PSVersionTable.PSVersion) ($($PSVersionTable.PSEdition))"
33
+ "Elevated: $($ws.IsAdmin)"
34
+ "Launcher: $($ws.Launcher) npx: $($ws.Npx)"
35
+ "Mode: $($ws.Mode) dry-run: $($ws.DryRun) developer: $($ws.Developer)"
36
+ "Node: $(try { (& node --version 2>$null) } catch { 'not installed' })"
37
+ )
38
+ if ($Extra) { $lines += ''; $lines += $Extra }
39
+ $lines += ''
40
+ $lines += '=== Privacy note ==='
41
+ $lines += 'This bundle was generated locally and was NOT sent anywhere. Review every file before sharing;'
42
+ $lines += 'logs and reports contain paths from your machine and a snapshot of cache sizes.'
43
+ $lines += "Report bugs at: $Script:WS_ISSUES (or email $Script:WS_EMAIL)"
44
+ return $lines
45
+ }
46
+
47
+ function New-BundleZip {
48
+ <# .SYNOPSIS Zip the current log, the latest report and a manifest. Returns the zip path or $null. #>
49
+ param([string] $Prefix, [string[]] $Manifest)
50
+ $ws = $Script:WS
51
+ $stamp = Get-Date -Format 'yyyy-MM-dd_HHmmss'
52
+ $tmp = Join-Path $env:TEMP ("$Script:WS_NAME-bundle-" + [guid]::NewGuid().ToString('N'))
53
+ New-Item -ItemType Directory -Force -Path $tmp | Out-Null
54
+ try {
55
+ [IO.File]::WriteAllLines((Join-Path $tmp 'MANIFEST.txt'), $Manifest)
56
+ if ($ws.LogFile -and (Test-Path -LiteralPath $ws.LogFile)) { Copy-Item -LiteralPath $ws.LogFile -Destination $tmp -ErrorAction SilentlyContinue }
57
+ $rep = Get-LatestReportPath
58
+ if ($rep) { Copy-Item -LiteralPath $rep -Destination $tmp -ErrorAction SilentlyContinue }
59
+ $out = Join-Path (Get-FeedbackDir) ("$Prefix-$stamp.zip")
60
+ Compress-Archive -Path (Join-Path $tmp '*') -DestinationPath $out -Force -ErrorAction Stop
61
+ return $out
62
+ } catch {
63
+ Write-Warn "could not create bundle: $($_.Exception.Message)"
64
+ return $null
65
+ } finally {
66
+ try { Remove-Item -LiteralPath $tmp -Recurse -Force -ErrorAction SilentlyContinue } catch { $null = $_ }
67
+ }
68
+ }
69
+
70
+ function Invoke-CrashBundle {
71
+ <# .SYNOPSIS Called by the entry script when Invoke-Main throws. #>
72
+ param([System.Management.Automation.ErrorRecord] $ErrorRecord)
73
+ $ws = $Script:WS
74
+ $msg = $ErrorRecord.Exception.Message
75
+ $where = $ErrorRecord.InvocationInfo.PositionMessage
76
+ Write-Log "CRASH: $msg"
77
+ Write-Log $ErrorRecord.ScriptStackTrace
78
+ $extra = @("=== Error ===", $msg, $where, '', '=== Stack ===', $ErrorRecord.ScriptStackTrace) -join "`r`n"
79
+ $zip = New-BundleZip -Prefix 'crash' -Manifest (New-SystemManifest -Kind 'CRASH REPORT' -Extra $extra)
80
+ Write-UiLine '' 'Gray'
81
+ Write-Err "$Script:WS_NAME exited unexpectedly: $msg"
82
+ Write-Note $where
83
+ if ($zip) {
84
+ Write-Info "crash bundle: $zip"
85
+ Write-Note 'Review it (it contains paths from your machine), then attach it to an issue:'
86
+ Write-Note "$Script:WS_ISSUES or run: windowsweep --report-issue"
87
+ }
88
+ Write-Note 'Nothing was sent. windowsweep makes no network calls.'
89
+ }
@@ -0,0 +1,49 @@
1
+ # disk_usage.ps1 - section 21: read-only disk usage report (largest entries, disk images, drives).
2
+
3
+ function Get-Targets21 { return @() }
4
+
5
+ function Show-LargestEntries {
6
+ <# .SYNOPSIS Size every top-level entry of a folder and print the largest N. Returns the rows. #>
7
+ param([string] $Root, [int] $Top = 20, [string] $Title = '')
8
+ if (-not (Test-DirPresent $Root)) { return @() }
9
+ if (-not $Title) { $Title = $Root }
10
+ Write-Section "Largest entries in $Title"
11
+ Write-Note 'measuring...'
12
+ $rows = @()
13
+ foreach ($e in (Get-ChildEntries $Root)) {
14
+ $full = Remove-LongPrefix $e.FullName
15
+ $bytes = [long]0
16
+ if (Test-ReparsePoint $e) { continue }
17
+ if ($e -is [IO.DirectoryInfo]) { $bytes = Get-DirectoryBytes $full } else { $bytes = [long]$e.Length }
18
+ $rows += [pscustomobject]@{ Path = $full; Bytes = $bytes }
19
+ }
20
+ $rows = @($rows | Sort-Object Bytes -Descending | Select-Object -First $Top)
21
+ foreach ($r in $rows) {
22
+ $why = Get-ProtectionReason $r.Path
23
+ $tag = ''
24
+ if ($why) { $tag = ' [protected]' }
25
+ Write-UiLine (" {0,10} {1}{2}" -f (Format-Bytes $r.Bytes), $r.Path, $tag) 'Gray'
26
+ }
27
+ return $rows
28
+ }
29
+
30
+ function Invoke-Section21 {
31
+ $ws = $Script:WS
32
+ $P = $Script:P
33
+ Write-SectionIntro @('Where the space went. Read-only; entries marked [protected] are ones this tool will never delete.')
34
+ $lines = @("windowsweep disk usage report - $(Get-Date -Format 'yyyy-MM-dd HH:mm')", '')
35
+ Show-DriveTable
36
+ foreach ($d in (Get-DriveSnapshot)) { $lines += ("{0} size {1} free {2}" -f $d.drive, (Format-Bytes $d.size_bytes), (Format-Bytes $d.free_bytes)) }
37
+ $h = Get-HiberfilInfo
38
+ if ($h.Bytes -gt 0) { Write-Kv 'hiberfil.sys:' (Format-Bytes $h.Bytes); $lines += "hiberfil.sys $(Format-Bytes $h.Bytes)" }
39
+ foreach ($img in (Get-DiskImageFiles)) { Write-Kv 'disk image:' ("{0} {1}" -f (Format-Bytes $img.Length), $img.FullName); $lines += "vhdx $(Format-Bytes $img.Length) $($img.FullName)" }
40
+ foreach ($pair in @(@($P.U, 'your profile'), @($P.L, 'AppData\Local'), @($P.A, 'AppData\Roaming'), @("$($P.SD)\", "drive $($P.SD)"))) {
41
+ $rows = Show-LargestEntries -Root $pair[0] -Top 20 -Title $pair[1]
42
+ $lines += ''; $lines += "== $($pair[1]) =="
43
+ foreach ($r in $rows) { $lines += ("{0,10} {1}" -f (Format-Bytes $r.Bytes), $r.Path) }
44
+ }
45
+ if (-not $ws.NoReport) {
46
+ $out = Join-Path $ws.ReportsDir ('disk-usage-' + $ws.Stamp + '.txt')
47
+ try { [IO.File]::WriteAllLines($out, $lines); Write-Info "report written: $out" } catch { Write-Warn "could not write $out" }
48
+ }
49
+ }