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/CHANGELOG.md +44 -0
- package/LICENSE +21 -0
- package/README.md +482 -0
- package/SECURITY.md +46 -0
- package/VERSION +1 -0
- package/bin/windowsweep.js +83 -0
- package/lib/actions.ps1 +209 -0
- package/lib/config.ps1 +90 -0
- package/lib/constants.ps1 +89 -0
- package/lib/fs.ps1 +180 -0
- package/lib/log.ps1 +140 -0
- package/lib/safety.ps1 +493 -0
- package/lib/scan.ps1 +94 -0
- package/lib/ui.ps1 +232 -0
- package/modules/android_avd.ps1 +36 -0
- package/modules/app_caches.ps1 +58 -0
- package/modules/browsers.ps1 +33 -0
- package/modules/build_tools.ps1 +35 -0
- package/modules/crash_trap.ps1 +89 -0
- package/modules/disk_usage.ps1 +49 -0
- package/modules/docker.ps1 +93 -0
- package/modules/editors.ps1 +149 -0
- package/modules/health.ps1 +101 -0
- package/modules/menu.ps1 +61 -0
- package/modules/personal.ps1 +124 -0
- package/modules/pkg_managers.ps1 +69 -0
- package/modules/projects.ps1 +140 -0
- package/modules/release_helpers.ps1 +414 -0
- package/modules/reports.ps1 +212 -0
- package/modules/runner.ps1 +190 -0
- package/modules/system_admin.ps1 +220 -0
- package/modules/test_runners.ps1 +24 -0
- package/modules/walkthrough.ps1 +79 -0
- package/modules/windows_user.ps1 +112 -0
- package/package.json +73 -0
- package/windowsweep.cmd +6 -0
- package/windowsweep.ps1 +292 -0
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
|
+
}
|