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
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# reports.ps1 - list, view, export (Markdown / HTML) and summarise past JSON reports. No external tools needed.
|
|
2
|
+
|
|
3
|
+
function Get-ReportFiles {
|
|
4
|
+
$dir = $Script:WS.ReportsDir
|
|
5
|
+
if (-not (Test-Path -LiteralPath $dir)) { return @() }
|
|
6
|
+
return @(Get-ChildItem -LiteralPath $dir -Filter 'report-*.json' -File -ErrorAction SilentlyContinue | Sort-Object Name -Descending)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function Read-Report {
|
|
10
|
+
param([string] $Path)
|
|
11
|
+
try { return (Get-Content -LiteralPath $Path -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop) } catch { return $null }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function Show-ReportList {
|
|
15
|
+
$files = @(Get-ReportFiles)
|
|
16
|
+
if ($files.Count -eq 0) { Write-Info 'no reports yet - run a cleanup first'; return @() }
|
|
17
|
+
Write-UiLine (" {0,3} {1,-34} {2,8} {3,-17} {4,-12} {5}" -f '#', 'REPORT', 'SIZE', 'STARTED', 'RECLAIMED', 'MODE') 'White'
|
|
18
|
+
$i = 1
|
|
19
|
+
foreach ($f in $files) {
|
|
20
|
+
$r = Read-Report $f.FullName
|
|
21
|
+
$started = '?'; $rec = '?'; $mode = '?'
|
|
22
|
+
if ($r) {
|
|
23
|
+
$started = ([string]$r.meta.started_at).Substring(0, [math]::Min(16, ([string]$r.meta.started_at).Length)).Replace('T', ' ')
|
|
24
|
+
$rec = $r.totals.total_reclaimed_human
|
|
25
|
+
if ($r.meta.dry_run) { $rec = "~$($r.totals.total_estimated_human) (dry)" }
|
|
26
|
+
$mode = $r.meta.mode
|
|
27
|
+
}
|
|
28
|
+
Write-UiLine (" {0,3} {1,-34} {2,8} {3,-17} {4,-12} {5}" -f $i, $f.Name, (Format-Bytes $f.Length), $started, $rec, $mode) 'Gray'
|
|
29
|
+
$i++
|
|
30
|
+
}
|
|
31
|
+
return $files
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function ConvertTo-HtmlText { param([string] $Text) return [System.Net.WebUtility]::HtmlEncode([string]$Text) }
|
|
35
|
+
|
|
36
|
+
function Convert-ReportToMarkdown {
|
|
37
|
+
param([string] $Json, [string] $Out = '')
|
|
38
|
+
$r = Read-Report $Json
|
|
39
|
+
if (-not $r) { Write-Err "cannot read $Json"; return $null }
|
|
40
|
+
if (-not $Out) { $Out = [IO.Path]::ChangeExtension($Json, '.md') }
|
|
41
|
+
$dry = 'no'
|
|
42
|
+
if ($r.meta.dry_run) { $dry = 'yes' }
|
|
43
|
+
$l = @()
|
|
44
|
+
$l += "# $Script:WS_NAME session report"; $l += ''
|
|
45
|
+
$l += "_Generated from ``$([IO.Path]::GetFileName($Json))``_"; $l += ''
|
|
46
|
+
$l += '## Overview'; $l += ''; $l += '| Field | Value |'; $l += '|---|---|'
|
|
47
|
+
$l += "| Started | $($r.meta.started_at) |"; $l += "| Finished | $($r.meta.finished_at) |"; $l += "| Duration | $($r.meta.duration_seconds) s |"
|
|
48
|
+
$l += "| Host | $($r.meta.host) |"; $l += "| User | $($r.meta.user) |"; $l += "| Windows | $($r.meta.os) |"; $l += "| Mode | $($r.meta.mode) |"
|
|
49
|
+
$l += "| Dry-run | $dry |"; $l += "| Elevated | $($r.meta.elevated) |"; $l += "| Developer mode | $($r.meta.developer_mode) |"; $l += "| Idle window | $($r.meta.idle_days) days |"
|
|
50
|
+
$l += ''; $l += '## Result'; $l += ''
|
|
51
|
+
if ($r.meta.dry_run) { $l += "**Would free (estimate): $($r.totals.total_estimated_human)** " } else { $l += "**Reclaimed: $($r.totals.total_reclaimed_human)** ($($r.totals.total_reclaimed_bytes) bytes) " }
|
|
52
|
+
$l += "Sections run: $($r.totals.steps_run) - skipped/refused: $($r.totals.steps_skipped)"; $l += ''
|
|
53
|
+
$l += '## Drives'; $l += ''; $l += '| Drive | Before free | After free | Size |'; $l += '|---|---|---|---|'
|
|
54
|
+
foreach ($b in $r.disk.before) {
|
|
55
|
+
$a = $r.disk.after | Where-Object { $_.drive -eq $b.drive } | Select-Object -First 1
|
|
56
|
+
$afterFree = '?'
|
|
57
|
+
if ($a) { $afterFree = Format-Bytes $a.free_bytes }
|
|
58
|
+
$l += "| $($b.drive) | $(Format-Bytes $b.free_bytes) | $afterFree | $(Format-Bytes $b.size_bytes) |"
|
|
59
|
+
}
|
|
60
|
+
$l += ''; $l += '## Steps'; $l += ''; $l += '| # | Section | Title | Status | Freed |'; $l += '|---:|---:|---|---|---:|'
|
|
61
|
+
foreach ($s in $r.steps) { $l += "| $($s.n) | $($s.section) | $($s.title) | $($s.status) | $(Format-Bytes $s.freed_bytes) |" }
|
|
62
|
+
$l += ''; $l += "_${Script:WS_NAME} v$($r.meta.tool_version) by $($r.credits.author.name) - $($r.credits.tool_homepage)_"
|
|
63
|
+
[IO.File]::WriteAllLines($Out, $l)
|
|
64
|
+
Write-Ok "wrote $Out"
|
|
65
|
+
return $Out
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function Convert-ReportToHtml {
|
|
69
|
+
param([string] $Json, [string] $Out = '')
|
|
70
|
+
$r = Read-Report $Json
|
|
71
|
+
if (-not $r) { Write-Err "cannot read $Json"; return $null }
|
|
72
|
+
if (-not $Out) { $Out = [IO.Path]::ChangeExtension($Json, '.html') }
|
|
73
|
+
$headline = "Reclaimed"; $big = $r.totals.total_reclaimed_human
|
|
74
|
+
if ($r.meta.dry_run) { $headline = 'Would free (dry-run estimate)'; $big = $r.totals.total_estimated_human }
|
|
75
|
+
$rows = ''
|
|
76
|
+
foreach ($s in $r.steps) {
|
|
77
|
+
$cls = ([string]$s.status) -replace '[^a-z-]', ''
|
|
78
|
+
$rows += "<tr class=`"row-$cls`"><td>$($s.n)</td><td>$($s.section)</td><td>$(ConvertTo-HtmlText $s.title)</td><td><span class=`"badge $cls`">$(ConvertTo-HtmlText $s.status)</span></td><td class=`"num`">$(ConvertTo-HtmlText (Format-Bytes $s.freed_bytes))</td></tr>`n"
|
|
79
|
+
}
|
|
80
|
+
$drives = ''
|
|
81
|
+
foreach ($b in $r.disk.before) {
|
|
82
|
+
$a = $r.disk.after | Where-Object { $_.drive -eq $b.drive } | Select-Object -First 1
|
|
83
|
+
$afterFree = '?'
|
|
84
|
+
if ($a) { $afterFree = Format-Bytes $a.free_bytes }
|
|
85
|
+
$drives += "<tr><td>$(ConvertTo-HtmlText $b.drive)</td><td class=`"num`">$(Format-Bytes $b.size_bytes)</td><td class=`"num`">$(Format-Bytes $b.free_bytes)</td><td class=`"num`">$afterFree</td></tr>`n"
|
|
86
|
+
}
|
|
87
|
+
$html = @"
|
|
88
|
+
<!doctype html>
|
|
89
|
+
<html lang="en">
|
|
90
|
+
<head>
|
|
91
|
+
<meta charset="utf-8">
|
|
92
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
93
|
+
<title>$Script:WS_NAME report - $(ConvertTo-HtmlText $r.meta.started_at)</title>
|
|
94
|
+
<style>
|
|
95
|
+
:root{--fg:#1c1c1f;--bg:#fafafa;--muted:#7a7a85;--ok:#0a7d54;--warn:#b8860b;--line:#e6e6ea;--card:#fff;--accent:#0b6bcb}
|
|
96
|
+
@media (prefers-color-scheme:dark){:root{--fg:#eaeaea;--bg:#16161a;--muted:#9d9da8;--line:#2a2a30;--card:#1f1f25;--accent:#5aa9ff}}
|
|
97
|
+
*{box-sizing:border-box}html,body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.55 'Segoe UI',system-ui,sans-serif}
|
|
98
|
+
.wrap{max-width:960px;margin:2rem auto;padding:0 1.25rem}h1{font-size:1.6rem;margin:0 0 .3rem}
|
|
99
|
+
h2{font-size:1.05rem;margin:2.2rem 0 .7rem;color:var(--muted);text-transform:uppercase;letter-spacing:.06em}
|
|
100
|
+
.subtitle{color:var(--muted);margin:0 0 1.5rem}.totals{display:flex;align-items:baseline;gap:1rem;padding:1.5rem 1.75rem;border:1px solid var(--line);border-radius:12px;background:var(--card);margin:1rem 0}
|
|
101
|
+
.totals .big{font-size:2.5rem;font-weight:800;color:var(--ok);line-height:1}.totals .label{color:var(--muted);text-transform:uppercase;font-size:.75rem;letter-spacing:.08em}
|
|
102
|
+
dl{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:.6rem 1.5rem;margin:0;padding:1rem 1.25rem;border:1px solid var(--line);border-radius:10px;background:var(--card)}
|
|
103
|
+
dt{color:var(--muted);font-size:.75rem;text-transform:uppercase;letter-spacing:.05em}dd{margin:.1rem 0 0;font-weight:600;word-break:break-all}
|
|
104
|
+
table{width:100%;border-collapse:collapse;background:var(--card);border:1px solid var(--line);border-radius:10px;overflow:hidden}
|
|
105
|
+
th,td{text-align:left;padding:.55rem .85rem;border-bottom:1px solid var(--line)}tr:last-child td{border-bottom:none}
|
|
106
|
+
th{font-size:.72rem;color:var(--muted);text-transform:uppercase;letter-spacing:.07em;background:var(--bg)}td.num{text-align:right;font-variant-numeric:tabular-nums}
|
|
107
|
+
.badge{display:inline-block;padding:.12em .5em;border-radius:999px;font-size:.75em;font-weight:600;text-transform:uppercase}
|
|
108
|
+
.badge.ran,.badge.dry-run{background:rgba(10,125,84,.12);color:var(--ok)}.badge.skipped,.badge.refused{background:rgba(184,134,11,.12);color:var(--warn)}.badge.failed{background:rgba(192,57,43,.12);color:#c0392b}
|
|
109
|
+
footer{margin:3rem 0 1rem;color:var(--muted);font-size:.85rem;text-align:center}a{color:var(--accent)}
|
|
110
|
+
</style>
|
|
111
|
+
</head>
|
|
112
|
+
<body><div class="wrap">
|
|
113
|
+
<h1>$Script:WS_NAME session report</h1>
|
|
114
|
+
<p class="subtitle">$(ConvertTo-HtmlText $r.meta.started_at) to $(ConvertTo-HtmlText $r.meta.finished_at) - $($r.meta.duration_seconds)s - $(ConvertTo-HtmlText $r.meta.host)</p>
|
|
115
|
+
<div class="totals"><div><div class="label">$headline</div><div class="big">$(ConvertTo-HtmlText $big)</div></div>
|
|
116
|
+
<div style="margin-left:auto;text-align:right;color:var(--muted)"><div><strong>$($r.totals.steps_run)</strong> sections run</div><div><strong>$($r.totals.steps_skipped)</strong> skipped</div></div></div>
|
|
117
|
+
<h2>Run</h2>
|
|
118
|
+
<dl><div><dt>Mode</dt><dd>$(ConvertTo-HtmlText $r.meta.mode)</dd></div><div><dt>Dry-run</dt><dd>$($r.meta.dry_run)</dd></div><div><dt>Elevated</dt><dd>$($r.meta.elevated)</dd></div>
|
|
119
|
+
<div><dt>Developer mode</dt><dd>$($r.meta.developer_mode)</dd></div><div><dt>Idle window</dt><dd>$($r.meta.idle_days) days</dd></div><div><dt>Windows</dt><dd>$(ConvertTo-HtmlText $r.meta.os)</dd></div>
|
|
120
|
+
<div><dt>Log file</dt><dd><code>$(ConvertTo-HtmlText $r.meta.log_file)</code></dd></div></dl>
|
|
121
|
+
<h2>Sections</h2>
|
|
122
|
+
<table><thead><tr><th>#</th><th>Section</th><th>Title</th><th>Status</th><th class="num">Freed</th></tr></thead><tbody>
|
|
123
|
+
$rows </tbody></table>
|
|
124
|
+
<h2>Drives</h2>
|
|
125
|
+
<table><thead><tr><th>Drive</th><th class="num">Size</th><th class="num">Free before</th><th class="num">Free after</th></tr></thead><tbody>
|
|
126
|
+
$drives </tbody></table>
|
|
127
|
+
<footer>Generated from <code>$(ConvertTo-HtmlText ([IO.Path]::GetFileName($Json)))</code> by <a href="$Script:WS_REPO">$Script:WS_NAME</a> v$(ConvertTo-HtmlText $r.meta.tool_version) - $(ConvertTo-HtmlText $r.credits.author.name)</footer>
|
|
128
|
+
</div></body></html>
|
|
129
|
+
"@
|
|
130
|
+
[IO.File]::WriteAllText($Out, $html, (New-Object System.Text.UTF8Encoding($false)))
|
|
131
|
+
Write-Ok "wrote $Out"
|
|
132
|
+
return $Out
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function Export-Reports {
|
|
136
|
+
<# .SYNOPSIS --export FMT ID. Returns an exit code. #>
|
|
137
|
+
param([string] $Format = 'both', [string] $Id = 'latest')
|
|
138
|
+
if ($Format -notin 'md', 'html', 'both') { Write-Err 'format must be md, html or both'; return $Script:WS_EXIT_USAGE }
|
|
139
|
+
$files = @(Get-ReportFiles)
|
|
140
|
+
if ($files.Count -eq 0) { Write-Err "no reports in $($Script:WS.ReportsDir)"; return $Script:WS_EXIT_FAIL }
|
|
141
|
+
$picked = @()
|
|
142
|
+
switch -Regex ($Id) {
|
|
143
|
+
'^all$' { $picked = $files }
|
|
144
|
+
'^latest$' { $picked = @($files[0]) }
|
|
145
|
+
'^\d+$' { $n = [int]$Id; if ($n -lt 1 -or $n -gt $files.Count) { Write-Err "index $Id out of range (1..$($files.Count))"; return $Script:WS_EXIT_USAGE }; $picked = @($files[$n - 1]) }
|
|
146
|
+
default { Write-Err 'ID must be a number, latest or all'; return $Script:WS_EXIT_USAGE }
|
|
147
|
+
}
|
|
148
|
+
foreach ($f in $picked) {
|
|
149
|
+
if ($Format -in 'md', 'both') { $null = Convert-ReportToMarkdown -Json $f.FullName }
|
|
150
|
+
if ($Format -in 'html', 'both') { $null = Convert-ReportToHtml -Json $f.FullName }
|
|
151
|
+
}
|
|
152
|
+
return $Script:WS_EXIT_OK
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function Invoke-ReportsManager {
|
|
156
|
+
Write-Box 'Reports manager' $Script:WS.ReportsDir
|
|
157
|
+
$files = @(Show-ReportList)
|
|
158
|
+
if ($files.Count -eq 0) { return }
|
|
159
|
+
if (-not $Script:WS.Interactive) { Write-Note 'non-interactive: use --export md|html|both N|latest|all'; return }
|
|
160
|
+
Write-UiLine '' 'Gray'
|
|
161
|
+
Write-Plain ' cm N markdown ch N html cb N both all export everything v N view JSON o N open HTML d N delete q quit'
|
|
162
|
+
$answer = Read-Line ' > '
|
|
163
|
+
if ($null -eq $answer) { return }
|
|
164
|
+
$parts = $answer.Trim() -split '\s+'
|
|
165
|
+
$cmd = $parts[0].ToLowerInvariant()
|
|
166
|
+
$n = 0
|
|
167
|
+
if ($parts.Count -gt 1 -and $parts[1] -match '^\d+$') { $n = [int]$parts[1] }
|
|
168
|
+
$target = $null
|
|
169
|
+
if ($n -ge 1 -and $n -le $files.Count) { $target = $files[$n - 1].FullName }
|
|
170
|
+
switch ($cmd) {
|
|
171
|
+
'cm' { if ($target) { $null = Convert-ReportToMarkdown -Json $target } else { Write-Err 'invalid index' } }
|
|
172
|
+
'ch' { if ($target) { $null = Convert-ReportToHtml -Json $target } else { Write-Err 'invalid index' } }
|
|
173
|
+
'cb' { if ($target) { $null = Convert-ReportToMarkdown -Json $target; $null = Convert-ReportToHtml -Json $target } else { Write-Err 'invalid index' } }
|
|
174
|
+
'all' { foreach ($f in $files) { $null = Convert-ReportToMarkdown -Json $f.FullName; $null = Convert-ReportToHtml -Json $f.FullName } }
|
|
175
|
+
'v' { if ($target) { Get-Content -LiteralPath $target | ForEach-Object { Write-Plain $_ } } else { Write-Err 'invalid index' } }
|
|
176
|
+
'o' {
|
|
177
|
+
if ($target) {
|
|
178
|
+
$html = [IO.Path]::ChangeExtension($target, '.html')
|
|
179
|
+
if (-not (Test-Path -LiteralPath $html)) { $null = Convert-ReportToHtml -Json $target }
|
|
180
|
+
try { Start-Process $html | Out-Null; Write-Ok "opened $html" } catch { Write-Warn "could not open $html" }
|
|
181
|
+
} else { Write-Err 'invalid index' }
|
|
182
|
+
}
|
|
183
|
+
'd' {
|
|
184
|
+
if ($target) {
|
|
185
|
+
if (Confirm-Ui -Prompt "Delete $([IO.Path]::GetFileName($target)) and its .md/.html siblings?" -Default 'n') {
|
|
186
|
+
foreach ($ext in '.json', '.md', '.html') { $p = [IO.Path]::ChangeExtension($target, $ext); if (Test-Path -LiteralPath $p) { Remove-Item -LiteralPath $p -Force -ErrorAction SilentlyContinue } }
|
|
187
|
+
Write-Ok 'deleted'
|
|
188
|
+
}
|
|
189
|
+
} else { Write-Err 'invalid index' }
|
|
190
|
+
}
|
|
191
|
+
'q' { return }
|
|
192
|
+
default { Write-Warn "unknown action: $cmd" }
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function Show-Stats {
|
|
197
|
+
Write-Box 'Run history' $Script:WS.Home
|
|
198
|
+
$files = @(Get-ReportFiles)
|
|
199
|
+
if ($files.Count -eq 0) { Write-Info 'no runs recorded yet'; return }
|
|
200
|
+
$total = [long]0; $runs = 0; $dry = 0
|
|
201
|
+
foreach ($f in $files) {
|
|
202
|
+
$r = Read-Report $f.FullName
|
|
203
|
+
if (-not $r) { continue }
|
|
204
|
+
if ($r.meta.dry_run) { $dry++ } else { $runs++; $total += [long]$r.totals.total_reclaimed_bytes }
|
|
205
|
+
}
|
|
206
|
+
Write-Kv 'Real runs:' $runs
|
|
207
|
+
Write-Kv 'Dry-runs:' $dry
|
|
208
|
+
Write-Kv 'Reclaimed overall:' (Format-Bytes $total)
|
|
209
|
+
Write-Kv 'Latest report:' $files[0].FullName
|
|
210
|
+
$logs = Join-Path $Script:WS.Home 'logs'
|
|
211
|
+
if (Test-Path -LiteralPath $logs) { Write-Kv 'Logs on disk:' (Format-Bytes (Get-DirectoryBytes $logs)) }
|
|
212
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# runner.ps1 - section dispatch, batch policy enforcement, the read-only scan mode and the summary.
|
|
2
|
+
|
|
3
|
+
function Get-SectionIdList {
|
|
4
|
+
<# .SYNOPSIS Parse "1,3,5-7" into section ids; validates against the catalogue. #>
|
|
5
|
+
param([string] $Text)
|
|
6
|
+
$ids = New-Object System.Collections.Generic.SortedSet[int]
|
|
7
|
+
foreach ($part in ($Text -split ',')) {
|
|
8
|
+
$part = $part.Trim()
|
|
9
|
+
if (-not $part) { continue }
|
|
10
|
+
if ($part -match '^(\d+)-(\d+)$') {
|
|
11
|
+
$a = [int]$Matches[1]; $b = [int]$Matches[2]
|
|
12
|
+
if ($a -gt $b) { $t = $a; $a = $b; $b = $t }
|
|
13
|
+
for ($i = $a; $i -le $b; $i++) { if (Get-Section $i) { $null = $ids.Add($i) } else { Write-Warn "no section $i - ignored" } }
|
|
14
|
+
} elseif ($part -match '^\d+$') {
|
|
15
|
+
$i = [int]$part
|
|
16
|
+
if (Get-Section $i) { $null = $ids.Add($i) } else { Write-Warn "no section $i - ignored" }
|
|
17
|
+
} else {
|
|
18
|
+
Write-Warn "cannot parse section '$part' - ignored"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return @($ids)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function Get-RequestedSections {
|
|
25
|
+
<# .SYNOPSIS Resolve --all / --only / --profile / --exclude into the ordered list to run. #>
|
|
26
|
+
$ws = $Script:WS
|
|
27
|
+
$ids = @()
|
|
28
|
+
if ($ws.Mode -eq 'all') {
|
|
29
|
+
$ids = @($Script:WS_SAFE_BATCH)
|
|
30
|
+
if ($ws.IsAdmin) { $ids += $Script:WS_SAFE_BATCH_ADMIN }
|
|
31
|
+
} elseif ($ws.OnlyList) {
|
|
32
|
+
$ids = Get-SectionIdList $ws.OnlyList
|
|
33
|
+
} elseif ($ws.Profile) {
|
|
34
|
+
$ids = @($Script:WS_PROFILES[$ws.Profile])
|
|
35
|
+
Write-Info "profile '$($ws.Profile)' = sections $($ids -join ',')"
|
|
36
|
+
}
|
|
37
|
+
if ($ws.Exclude) {
|
|
38
|
+
$ex = Get-SectionIdList $ws.Exclude
|
|
39
|
+
$ids = @($ids | Where-Object { $ex -notcontains $_ })
|
|
40
|
+
Write-Info "after --exclude: sections $($ids -join ',')"
|
|
41
|
+
}
|
|
42
|
+
return @($ids | Sort-Object -Unique)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function Invoke-SectionById {
|
|
46
|
+
<# .SYNOPSIS Run one section under the batch, admin and developer policies; records the report step. #>
|
|
47
|
+
param([int] $Id)
|
|
48
|
+
$ws = $Script:WS
|
|
49
|
+
$sec = Get-Section $Id
|
|
50
|
+
if (-not $sec) { Write-Warn "no section $Id"; return }
|
|
51
|
+
$title = "[{0:00}] {1}" -f $sec.Id, $sec.Title
|
|
52
|
+
Write-Box $title
|
|
53
|
+
# Batch policy: deep and interactive-only sections never run unattended without an explicit opt-in.
|
|
54
|
+
if ($ws.BatchMode -and -not $ws.DryRun) {
|
|
55
|
+
if ($sec.Batch -eq 'interactive') {
|
|
56
|
+
Write-Warn "section $Id is interactive-only: it needs a person at the keyboard. Run it from the walkthrough or the menu."
|
|
57
|
+
$ws.Refusals += "section $Id (interactive-only)"
|
|
58
|
+
Add-ReportStep -Section $Id -Title $sec.Title -Status 'refused' -Note 'interactive-only section in batch mode'
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
if ($sec.Batch -eq 'deep' -and -not $ws.Deep) {
|
|
62
|
+
Write-Warn "section $Id is deep (irreversible or system-changing): refused in batch mode without --i-understand-deep."
|
|
63
|
+
$ws.Refusals += "section $Id (deep, add --i-understand-deep)"
|
|
64
|
+
Add-ReportStep -Section $Id -Title $sec.Title -Status 'refused' -Note 'deep section without --i-understand-deep'
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if ($sec.Admin -and -not $ws.IsAdmin) {
|
|
69
|
+
Write-Warn "section $Id needs Administrator rights - skipped."
|
|
70
|
+
Write-Note "run it elevated: windowsweep --only $Id --yes --elevate"
|
|
71
|
+
$ws.Hints += "Admin section $Id skipped: windowsweep --only $Id --yes --elevate"
|
|
72
|
+
Add-ReportStep -Section $Id -Title $sec.Title -Status 'skipped' -Note 'needs Administrator'
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
if ($sec.Dev -and $ws.Developer -eq $false -and $Id -in @(4, 17, 20)) {
|
|
76
|
+
Write-Info "developer mode is off - section $Id (developer-only data) skipped."
|
|
77
|
+
Add-ReportStep -Section $Id -Title $sec.Title -Status 'skipped' -Note 'developer mode off'
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
$ws.SectionFreed = [long]0
|
|
81
|
+
$before = Get-SystemDriveFree
|
|
82
|
+
$status = 'ran'
|
|
83
|
+
if ($ws.DryRun) { $status = 'dry-run' }
|
|
84
|
+
try {
|
|
85
|
+
& $sec.Fn
|
|
86
|
+
} catch {
|
|
87
|
+
$status = 'failed'
|
|
88
|
+
Write-Err "section $Id failed: $($_.Exception.Message)"
|
|
89
|
+
Write-Log ("section $Id error: " + $_.ScriptStackTrace)
|
|
90
|
+
$ws.ExitCode = $Script:WS_EXIT_FAIL
|
|
91
|
+
}
|
|
92
|
+
$after = Get-SystemDriveFree
|
|
93
|
+
$delta = $after - $before
|
|
94
|
+
if ($delta -lt 0) { $delta = 0 }
|
|
95
|
+
$freed = $ws.SectionFreed
|
|
96
|
+
if ($ws.DryRun) {
|
|
97
|
+
if ($freed -gt 0) { Write-UiLine (" [dry-run] this section would free about {0}" -f (Format-Bytes $freed)) 'Magenta' }
|
|
98
|
+
} elseif ($freed -gt 0 -or $delta -gt 0) {
|
|
99
|
+
Write-UiLine (" {0} this section freed {1} (system drive gained {2}) running total: {3}" -f $ws.Glyph.ok, (Format-Bytes $freed), (Format-Bytes $delta), (Format-Bytes $ws.TotalFreed)) 'Green'
|
|
100
|
+
} else {
|
|
101
|
+
Write-Note 'no measurable change in this section'
|
|
102
|
+
}
|
|
103
|
+
Add-ReportStep -Section $Id -Title $sec.Title -Status $status -Freed $freed
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function Invoke-BatchMode {
|
|
107
|
+
<# .SYNOPSIS --all / --only / --profile #>
|
|
108
|
+
$ws = $Script:WS
|
|
109
|
+
$ids = @(Get-RequestedSections)
|
|
110
|
+
if ($ids.Count -eq 0) { Write-Err 'no sections to run'; $ws.ExitCode = $Script:WS_EXIT_USAGE; return }
|
|
111
|
+
Write-Banner
|
|
112
|
+
Show-RunHeader
|
|
113
|
+
Write-Info "running sections: $($ids -join ',')"
|
|
114
|
+
foreach ($id in $ids) { Invoke-SectionById -Id $id }
|
|
115
|
+
Show-SessionSummary
|
|
116
|
+
if ($ws.Refusals.Count -gt 0 -and $ws.Mode -eq 'only' -and $ws.ExitCode -eq 0) { $ws.ExitCode = $Script:WS_EXIT_REFUSED }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function Show-RunHeader {
|
|
120
|
+
$ws = $Script:WS
|
|
121
|
+
$strategy = "prune files idle $($ws.Days)+ days (temp: $($ws.TempDays)+ days)"
|
|
122
|
+
if ($ws.PurgeAll) { $strategy = 'FULL PURGE of cache targets' }
|
|
123
|
+
$dev = 'off'
|
|
124
|
+
if ($ws.Developer) { $dev = "on ($($ws.DeveloperSource))" }
|
|
125
|
+
$adm = 'no'
|
|
126
|
+
if ($ws.IsAdmin) { $adm = 'yes' }
|
|
127
|
+
Write-Info "mode: $($ws.Mode) strategy: $strategy developer: $dev elevated: $adm auto-yes: $($ws.Yes)"
|
|
128
|
+
if ($ws.DryRun) { Write-UiLine ' DRY-RUN: nothing will be deleted; sizes below are estimates of what a real run would remove.' 'Magenta' }
|
|
129
|
+
Write-Info "log: $($ws.LogFile)"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function Invoke-ScanMode {
|
|
133
|
+
<# .SYNOPSIS --scan: health report + every target's size + the read-only personal scanners. Deletes nothing. #>
|
|
134
|
+
$ws = $Script:WS
|
|
135
|
+
Write-Banner
|
|
136
|
+
Write-Info 'read-only scan - nothing is deleted'
|
|
137
|
+
if (Get-Command Invoke-Section00 -ErrorAction SilentlyContinue) { Invoke-Section00 }
|
|
138
|
+
Write-Box 'Targets on disk' 'What each section can reach, and how much it currently holds'
|
|
139
|
+
$null = Show-ScanTable
|
|
140
|
+
if (Get-Command Show-PersonalScan -ErrorAction SilentlyContinue) { Show-PersonalScan }
|
|
141
|
+
Add-ReportStep -Section -1 -Title 'Read-only scan' -Status 'ran'
|
|
142
|
+
Show-SessionSummary
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function Show-SessionSummary {
|
|
146
|
+
$ws = $Script:WS
|
|
147
|
+
Write-Box 'Session summary'
|
|
148
|
+
if ($ws.DryRun) {
|
|
149
|
+
Write-Kv 'Mode:' 'DRY-RUN - nothing was deleted'
|
|
150
|
+
Write-Kv 'Would free (estimate):' (Format-Bytes $ws.TotalEstimated)
|
|
151
|
+
} else {
|
|
152
|
+
Write-Kv 'Reclaimed:' (Format-Bytes $ws.TotalFreed)
|
|
153
|
+
}
|
|
154
|
+
$secs = [int][math]::Round(((Get-Date).Ticks - $ws.Report.startedTicks) / 10000000)
|
|
155
|
+
Write-Kv 'Duration:' (Format-Duration $secs)
|
|
156
|
+
$ran = @($ws.Report.steps | Where-Object { $_.status -in 'ran', 'dry-run' }).Count
|
|
157
|
+
$skipped = @($ws.Report.steps | Where-Object { $_.status -notin 'ran', 'dry-run' }).Count
|
|
158
|
+
Write-Kv 'Sections run / skipped:' "$ran / $skipped"
|
|
159
|
+
Write-Kv 'Log:' $ws.LogFile
|
|
160
|
+
$rep = Save-Report
|
|
161
|
+
if ($rep) { Write-Kv 'Report (JSON):' $rep } elseif ($ws.NoReport) { Write-Kv 'Report:' '(disabled via --no-report)' }
|
|
162
|
+
Write-UiLine '' 'Gray'
|
|
163
|
+
Show-DriveTable
|
|
164
|
+
if ($ws.Refusals.Count -gt 0) {
|
|
165
|
+
Write-UiLine '' 'Gray'
|
|
166
|
+
Write-Warn 'refused in batch mode:'
|
|
167
|
+
foreach ($r in $ws.Refusals) { Write-Note $r }
|
|
168
|
+
}
|
|
169
|
+
if ($ws.Hints.Count -gt 0) {
|
|
170
|
+
Write-UiLine '' 'Gray'
|
|
171
|
+
Write-Info 'next steps:'
|
|
172
|
+
foreach ($h in ($ws.Hints | Sort-Object -Unique)) { Write-Note $h }
|
|
173
|
+
}
|
|
174
|
+
Write-UiLine '' 'Gray'
|
|
175
|
+
Write-Note "$Script:WS_NAME v$(Get-ToolVersion) by $Script:WS_AUTHOR - $Script:WS_REPO"
|
|
176
|
+
if ($ws.JsonMode) { Write-JsonSummary }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function Write-JsonSummary {
|
|
180
|
+
<# .SYNOPSIS --json: the only line written to stdout. #>
|
|
181
|
+
$ws = $Script:WS
|
|
182
|
+
$doc = [ordered]@{
|
|
183
|
+
tool = $Script:WS_NAME; version = (Get-ToolVersion); mode = $ws.Mode; dry_run = [bool]$ws.DryRun
|
|
184
|
+
elevated = [bool]$ws.IsAdmin; developer = $ws.Developer
|
|
185
|
+
freed_bytes = [long]$ws.TotalFreed; estimated_bytes = [long]$ws.TotalEstimated
|
|
186
|
+
sections = @($ws.Report.steps | ForEach-Object { [ordered]@{ section = $_.section; status = $_.status; freed_bytes = $_.freed_bytes } })
|
|
187
|
+
refusals = @($ws.Refusals); log_file = $ws.LogFile; report_file = $ws.ReportFile
|
|
188
|
+
}
|
|
189
|
+
[Console]::Out.WriteLine(($doc | ConvertTo-Json -Depth 5 -Compress))
|
|
190
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# system_admin.ps1 - sections 12-16: Windows Update cache, Disk Cleanup engine, DISM, hibernation file, event logs.
|
|
2
|
+
# Every section here needs Administrator rights; the runner skips them (with the --elevate hint) when not elevated.
|
|
3
|
+
|
|
4
|
+
function Get-Targets12 {
|
|
5
|
+
$SR = $Script:P.SR; $PD = $Script:P.PD
|
|
6
|
+
return @(
|
|
7
|
+
(New-Target 12 'Windows Update download cache' "$SR\SoftwareDistribution\Download" -Mode clear -Note 'wuauserv and bits are stopped for the wipe and restarted')
|
|
8
|
+
(New-Target 12 'Delivery Optimization cache' 'Delete-DeliveryOptimizationCache -Force' -Kind cmd)
|
|
9
|
+
(New-Target 12 'Windows temp' "$SR\Temp" -Mode prune -Days $Script:WS.TempDays)
|
|
10
|
+
(New-Target 12 'LocalService temp' "$SR\ServiceProfiles\LocalService\AppData\Local\Temp" -Mode prune -Days $Script:WS.TempDays)
|
|
11
|
+
(New-Target 12 'NetworkService temp' "$SR\ServiceProfiles\NetworkService\AppData\Local\Temp" -Mode prune -Days $Script:WS.TempDays)
|
|
12
|
+
(New-Target 12 'CBS servicing logs' "$SR\Logs\CBS" -Mode prune -Days 30 -Note 'the live CBS.log is open and skipped')
|
|
13
|
+
(New-Target 12 'DISM logs' "$SR\Logs\DISM" -Mode prune -Days 30)
|
|
14
|
+
(New-Target 12 'Windows Update logs' "$SR\Logs\WindowsUpdate" -Mode prune -Days 30)
|
|
15
|
+
(New-Target 12 'Live kernel reports' "$SR\LiveKernelReports" -Mode clear-old -Days 7)
|
|
16
|
+
(New-Target 12 'Windows Error Reporting queue (system)' "$PD\Microsoft\Windows\WER\ReportQueue" -Mode clear)
|
|
17
|
+
(New-Target 12 'Windows Error Reporting archive (system)' "$PD\Microsoft\Windows\WER\ReportArchive" -Mode clear)
|
|
18
|
+
(New-Target 12 'Windows Error Reporting temp (system)' "$PD\Microsoft\Windows\WER\Temp" -Mode clear)
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function Set-ServiceState {
|
|
23
|
+
<# .SYNOPSIS Stop or start a service through the dry-run-aware wrapper. Returns $true when it was running before a stop. #>
|
|
24
|
+
param([string] $Name, [string] $Action)
|
|
25
|
+
$svc = Get-Service -Name $Name -ErrorAction SilentlyContinue
|
|
26
|
+
if (-not $svc) { return $false }
|
|
27
|
+
if ($Action -eq 'stop') {
|
|
28
|
+
if ($svc.Status -ne 'Running') { return $false }
|
|
29
|
+
if ($Script:WS.DryRun) { Write-DryRun "would stop service $Name"; return $true }
|
|
30
|
+
try { Stop-Service -Name $Name -Force -ErrorAction Stop; Write-Note "stopped $Name" } catch { Write-Warn "could not stop $Name : $($_.Exception.Message)" }
|
|
31
|
+
return $true
|
|
32
|
+
}
|
|
33
|
+
if ($Script:WS.DryRun) { Write-DryRun "would start service $Name"; return $true }
|
|
34
|
+
try { Start-Service -Name $Name -ErrorAction Stop; Write-Note "started $Name" } catch { Write-Warn "could not start $Name : $($_.Exception.Message)" }
|
|
35
|
+
return $true
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function Invoke-Section12 {
|
|
39
|
+
$ws = $Script:WS
|
|
40
|
+
Write-SectionIntro @(
|
|
41
|
+
'Windows Update keeps every downloaded package after installing it; Delivery Optimization keeps peer-share',
|
|
42
|
+
'copies; C:\Windows\Temp and the servicing logs grow forever. All of it is rebuilt on demand.'
|
|
43
|
+
)
|
|
44
|
+
if (-not $ws.IsAdmin) { Write-Warn 'needs Administrator - skipped'; return }
|
|
45
|
+
$targets = Get-Targets12
|
|
46
|
+
$null = Show-TargetSizes $targets
|
|
47
|
+
if (-not (Confirm-Section 'Clear Windows Update and system temp caches now?')) { Write-Info 'skipped'; return }
|
|
48
|
+
$wuWasRunning = Set-ServiceState -Name 'wuauserv' -Action 'stop'
|
|
49
|
+
$bitsWasRunning = Set-ServiceState -Name 'bits' -Action 'stop'
|
|
50
|
+
try {
|
|
51
|
+
$null = Invoke-TargetList $targets
|
|
52
|
+
} finally {
|
|
53
|
+
if ($wuWasRunning) { $null = Set-ServiceState -Name 'wuauserv' -Action 'start' }
|
|
54
|
+
if ($bitsWasRunning) { $null = Set-ServiceState -Name 'bits' -Action 'start' }
|
|
55
|
+
}
|
|
56
|
+
if (Get-Command Delete-DeliveryOptimizationCache -ErrorAction SilentlyContinue) {
|
|
57
|
+
if ($ws.DryRun) { Write-DryRun 'would run Delete-DeliveryOptimizationCache -Force' }
|
|
58
|
+
else {
|
|
59
|
+
try { Delete-DeliveryOptimizationCache -Force -ErrorAction Stop; Write-Ok 'Delivery Optimization cache cleared' } catch { Write-Warn "Delivery Optimization: $($_.Exception.Message)" }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# ---------------------------------------------------------------------------------------------
|
|
65
|
+
# Section 13 - Disk Cleanup engine (cleanmgr)
|
|
66
|
+
# ---------------------------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
# Handlers this tool enables. Deliberately absent: DownloadsFolder (your files), Recycle Bin (section 11 asks
|
|
69
|
+
# separately), User file versions (File History), Windows ESD installation files (needed for Reset this PC),
|
|
70
|
+
# Language Pack (removes installed languages).
|
|
71
|
+
$Script:WS_CLEANMGR_HANDLERS = @(
|
|
72
|
+
'Active Setup Temp Folders', 'BranchCache', 'Content Indexer Cleaner', 'D3D Shader Cache', 'Delivery Optimization Files',
|
|
73
|
+
'Device Driver Packages', 'Diagnostic Data Viewer database files', 'Downloaded Program Files', 'Internet Cache Files',
|
|
74
|
+
'Offline Pages Files', 'Old ChkDsk Files', 'Previous Installations', 'RetailDemo Offline Content', 'Setup Log Files',
|
|
75
|
+
'System error memory dump files', 'System error minidump files', 'Temporary Files', 'Temporary Setup Files',
|
|
76
|
+
'Temporary Sync Files', 'Thumbnail Cache', 'Update Cleanup', 'Upgrade Discarded Files', 'Windows Defender',
|
|
77
|
+
'Windows Error Reporting Files', 'Windows Upgrade Log Files'
|
|
78
|
+
)
|
|
79
|
+
$Script:WS_CLEANMGR_KEY = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches'
|
|
80
|
+
$Script:WS_CLEANMGR_FLAG = 'StateFlags0077'
|
|
81
|
+
|
|
82
|
+
function Get-Targets13 {
|
|
83
|
+
return @((New-Target 13 'cleanmgr /sagerun:77 with curated handlers' ($Script:WS_CLEANMGR_HANDLERS -join ', ') -Kind cmd -Note 'never Downloads, Recycle Bin, File History, ESD files or language packs'))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function Invoke-Section13 {
|
|
87
|
+
$ws = $Script:WS
|
|
88
|
+
Write-SectionIntro @(
|
|
89
|
+
"Windows' own Disk Cleanup engine, driven with a curated handler list: temporary files, Update Cleanup,",
|
|
90
|
+
'thumbnail cache, memory dumps, setup and upgrade logs, previous Windows installations. It never touches',
|
|
91
|
+
'your Downloads folder, the Recycle Bin, File History or the files Reset-this-PC needs.'
|
|
92
|
+
)
|
|
93
|
+
if (-not $ws.IsAdmin) { Write-Warn 'needs Administrator - skipped'; return }
|
|
94
|
+
if (-not (Test-CommandPresent 'cleanmgr.exe')) { Write-Warn 'cleanmgr.exe not found on this edition of Windows - skipped'; return }
|
|
95
|
+
$present = @()
|
|
96
|
+
foreach ($h in $Script:WS_CLEANMGR_HANDLERS) { if (Test-Path -LiteralPath (Join-Path $Script:WS_CLEANMGR_KEY $h)) { $present += $h } }
|
|
97
|
+
Write-Info "handlers available on this machine: $($present.Count) of $($Script:WS_CLEANMGR_HANDLERS.Count)"
|
|
98
|
+
foreach ($h in $present) { Write-Note $h }
|
|
99
|
+
if ($ws.DryRun) { Write-DryRun 'would run cleanmgr.exe /sagerun:77 with those handlers (cleanmgr cannot preview sizes)'; return }
|
|
100
|
+
if (-not (Confirm-Section 'Run Disk Cleanup with these handlers now? (a progress window may appear; it can take several minutes)')) { Write-Info 'skipped'; return }
|
|
101
|
+
$before = Get-SystemDriveFree
|
|
102
|
+
try {
|
|
103
|
+
foreach ($h in $present) { Set-ItemProperty -LiteralPath (Join-Path $Script:WS_CLEANMGR_KEY $h) -Name $Script:WS_CLEANMGR_FLAG -Value 2 -Type DWord -ErrorAction Stop }
|
|
104
|
+
$r = Invoke-External -FilePath 'cleanmgr.exe' -ArgumentList @('/sagerun:77') -Destructive -Quiet -Label 'cleanmgr /sagerun:77'
|
|
105
|
+
if ($r.ExitCode -ne 0) { Write-Warn "cleanmgr returned $($r.ExitCode)" }
|
|
106
|
+
} catch {
|
|
107
|
+
Write-Err "could not configure Disk Cleanup: $($_.Exception.Message)"
|
|
108
|
+
} finally {
|
|
109
|
+
foreach ($h in $present) { Remove-ItemProperty -LiteralPath (Join-Path $Script:WS_CLEANMGR_KEY $h) -Name $Script:WS_CLEANMGR_FLAG -ErrorAction SilentlyContinue }
|
|
110
|
+
}
|
|
111
|
+
$gain = [math]::Max([long]0, (Get-SystemDriveFree) - $before)
|
|
112
|
+
Add-Freed $gain
|
|
113
|
+
Write-Ok ("Disk Cleanup finished - system drive gained " + (Format-Bytes $gain))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# ---------------------------------------------------------------------------------------------
|
|
117
|
+
# Section 14 - DISM component store
|
|
118
|
+
# ---------------------------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
function Get-Targets14 {
|
|
121
|
+
$note = 'superseded component versions in WinSxS'
|
|
122
|
+
if ($Script:WS.ResetBase) { $note += '; --reset-base also removes the ability to uninstall installed updates' }
|
|
123
|
+
return @((New-Target 14 'Dism /Online /Cleanup-Image /StartComponentCleanup' "$($Script:P.SR)\WinSxS" -Kind cmd -Note $note))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function Invoke-Section14 {
|
|
127
|
+
$ws = $Script:WS
|
|
128
|
+
Write-SectionIntro @(
|
|
129
|
+
'The component store (WinSxS) keeps superseded versions of Windows components. DISM removes the ones no',
|
|
130
|
+
'installed update needs. It is slow (5-20 minutes) and safe; /ResetBase (opt-in) also drops the ability to',
|
|
131
|
+
'uninstall currently installed updates.'
|
|
132
|
+
)
|
|
133
|
+
if (-not $ws.IsAdmin) { Write-Warn 'needs Administrator - skipped'; return }
|
|
134
|
+
if (-not (Test-CommandPresent 'Dism.exe')) { Write-Warn 'Dism.exe not found - skipped'; return }
|
|
135
|
+
Write-Info 'analyzing the component store (read-only, about a minute)...'
|
|
136
|
+
$a = Invoke-External -FilePath 'Dism.exe' -ArgumentList @('/Online', '/Cleanup-Image', '/AnalyzeComponentStore') -Quiet -Label 'dism analyze'
|
|
137
|
+
foreach ($l in $a.Output) { if ($l -match 'Size|Cleanup|Reclaimable|Date' ) { Write-Note ($l.Trim()) } }
|
|
138
|
+
$dismArgs = @('/Online', '/Cleanup-Image', '/StartComponentCleanup')
|
|
139
|
+
if ($ws.ResetBase) { $dismArgs += '/ResetBase'; Write-Warn '/ResetBase requested: installed updates can no longer be uninstalled afterwards' }
|
|
140
|
+
if (-not (Confirm-Section 'Run the component store cleanup now?')) { Write-Info 'skipped'; return }
|
|
141
|
+
$before = Get-SystemDriveFree
|
|
142
|
+
$r = Invoke-External -FilePath 'Dism.exe' -ArgumentList $dismArgs -Destructive -Quiet -Label 'dism StartComponentCleanup'
|
|
143
|
+
if ($r.Ran) {
|
|
144
|
+
$gain = [math]::Max([long]0, (Get-SystemDriveFree) - $before)
|
|
145
|
+
Add-Freed $gain
|
|
146
|
+
if ($r.ExitCode -eq 0) { Write-Ok ("component store cleanup finished - system drive gained " + (Format-Bytes $gain)) } else { Write-Warn "DISM returned $($r.ExitCode) - see the log" }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# ---------------------------------------------------------------------------------------------
|
|
151
|
+
# Section 15 - hibernation file
|
|
152
|
+
# ---------------------------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
function Get-Targets15 {
|
|
155
|
+
return @((New-Target 15 'hiberfil.sys (powercfg /hibernate off | /type reduced)' (Join-Path $Script:P.SD '\hiberfil.sys') -Kind cmd -Note 'off frees the whole file; reduced keeps Fast Startup at roughly 40% of RAM'))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function Invoke-Section15 {
|
|
159
|
+
$ws = $Script:WS
|
|
160
|
+
$h = Get-HiberfilInfo
|
|
161
|
+
Write-SectionIntro @(
|
|
162
|
+
'hiberfil.sys holds a copy of RAM for Hibernate and Fast Startup, so it is about 40% of your RAM - permanently.',
|
|
163
|
+
' off removes it. Sleep still works; Hibernate, Fast Startup and hibernate-on-critical-battery do not.',
|
|
164
|
+
' reduced keeps Fast Startup only, at a smaller file (Windows decides the size, usually about half).',
|
|
165
|
+
'Running speed is unaffected either way; this is purely disk space.'
|
|
166
|
+
)
|
|
167
|
+
if ($h.Bytes -gt 0) { Write-Kv 'hiberfil.sys now:' ("{0} (hibernation {1})" -f (Format-Bytes $h.Bytes), $h.State) } else { Write-Info 'hiberfil.sys is absent - hibernation is already off'; return }
|
|
168
|
+
if (-not $ws.IsAdmin) { Write-Warn 'needs Administrator - skipped'; return }
|
|
169
|
+
$choice = $ws.Hiberfil
|
|
170
|
+
if (-not $choice) {
|
|
171
|
+
if (-not $ws.Interactive) { Write-Warn 'pass --hiberfil off|reduced|keep to run this section unattended'; return }
|
|
172
|
+
$choice = Read-Choice -Prompt ' [o] turn hibernation off [r] reduced (keep Fast Startup) [k] keep as is > ' -Default 'k'
|
|
173
|
+
switch ($choice) { 'o' { $choice = 'off' } 'r' { $choice = 'reduced' } default { $choice = 'keep' } }
|
|
174
|
+
}
|
|
175
|
+
if ($choice -eq 'keep') { Write-Info 'left unchanged'; return }
|
|
176
|
+
$pcArgs = @('/hibernate', 'off')
|
|
177
|
+
if ($choice -eq 'reduced') { $pcArgs = @('/hibernate', '/type', 'reduced') }
|
|
178
|
+
if ($ws.DryRun) { Write-DryRun ("would run powercfg $($pcArgs -join ' ') (frees up to " + (Format-Bytes $h.Bytes) + ')'); Add-Freed $h.Bytes; return }
|
|
179
|
+
if (-not (Confirm-Ui -Prompt "Run powercfg $($pcArgs -join ' ') now?" -Default 'n')) { Write-Info 'skipped'; return }
|
|
180
|
+
$r = Invoke-External -FilePath 'powercfg.exe' -ArgumentList $pcArgs -Destructive -Quiet -Label "powercfg $($pcArgs -join ' ')"
|
|
181
|
+
Start-Sleep -Seconds 2
|
|
182
|
+
$after = Get-HiberfilInfo
|
|
183
|
+
$gain = [math]::Max([long]0, $h.Bytes - $after.Bytes)
|
|
184
|
+
Add-Freed $gain
|
|
185
|
+
if ($r.ExitCode -eq 0) { Write-Ok ("hibernation set to '$choice' - hiberfil.sys " + (Format-Bytes $h.Bytes) + ' -> ' + (Format-Bytes $after.Bytes) + ' (freed ' + (Format-Bytes $gain) + ')') } else { Write-Warn "powercfg returned $($r.ExitCode)" }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# ---------------------------------------------------------------------------------------------
|
|
189
|
+
# Section 16 - event logs
|
|
190
|
+
# ---------------------------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
function Get-Targets16 {
|
|
193
|
+
return @((New-Target 16 'Windows Event Logs (wevtutil cl for every log)' "$($Script:P.SR)\System32\winevt\Logs" -Kind cmd -Note 'PERMANENT - troubleshooting history is lost'))
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function Invoke-Section16 {
|
|
197
|
+
$ws = $Script:WS
|
|
198
|
+
$logDir = Join-Path $Script:P.SR 'System32\winevt\Logs'
|
|
199
|
+
Write-SectionIntro @(
|
|
200
|
+
'Clears every Windows Event Log (Application, System, Security and the hundreds of per-component logs).',
|
|
201
|
+
'This is permanent and removes the history a technician would use to diagnose a problem. Only do this on',
|
|
202
|
+
'a machine that is behaving normally.'
|
|
203
|
+
)
|
|
204
|
+
if (-not $ws.IsAdmin) { Write-Warn 'needs Administrator - skipped'; return }
|
|
205
|
+
$before = Get-DirectoryBytes $logDir
|
|
206
|
+
Write-Kv 'event log store:' (Format-Bytes $before)
|
|
207
|
+
if ($ws.DryRun) { Write-DryRun ("would clear every event log (about " + (Format-Bytes $before) + ')'); Add-Freed $before; return }
|
|
208
|
+
if (-not (Confirm-Ui -Prompt 'Permanently clear ALL Windows Event Logs?' -Default 'n')) { Write-Info 'skipped'; return }
|
|
209
|
+
$list = Invoke-External -FilePath 'wevtutil.exe' -ArgumentList @('el') -Quiet -Label 'wevtutil el'
|
|
210
|
+
$ok = 0; $failed = 0
|
|
211
|
+
foreach ($name in $list.Output) {
|
|
212
|
+
$n = ([string]$name).Trim()
|
|
213
|
+
if (-not $n) { continue }
|
|
214
|
+
$r = Invoke-External -FilePath 'wevtutil.exe' -ArgumentList @('cl', $n) -Destructive -Quiet -Label "wevtutil cl $n"
|
|
215
|
+
if ($r.ExitCode -eq 0) { $ok++ } else { $failed++ }
|
|
216
|
+
}
|
|
217
|
+
$gain = [math]::Max([long]0, $before - (Get-DirectoryBytes $logDir))
|
|
218
|
+
Add-Freed $gain
|
|
219
|
+
Write-Ok ("cleared $ok logs ($failed could not be cleared - analytic/debug channels), freed " + (Format-Bytes $gain))
|
|
220
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# test_runners.ps1 - section 3: downloaded test browsers (Cypress, Playwright, Puppeteer). Keep the newest of each.
|
|
2
|
+
|
|
3
|
+
$Script:WS_GROUP_BY_FAMILY = { param($name) ($name -replace '[-_]?v?\d[\d.]*$', '') }
|
|
4
|
+
|
|
5
|
+
function Get-Targets03 {
|
|
6
|
+
$P = $Script:P
|
|
7
|
+
return @(
|
|
8
|
+
(New-Target 3 'Cypress binary versions' "$($P.L)\Cypress\Cache" -Mode units -Dev $true -KeepNewest $true -Note 'one folder per Cypress version; newest kept')
|
|
9
|
+
(New-Target 3 'Playwright browsers' "$($P.L)\ms-playwright" -Mode units -Dev $true -KeepNewest $true -GroupBy $Script:WS_GROUP_BY_FAMILY -ExcludeNames @('.links', 'daemon') -Note 'newest build of each browser kept')
|
|
10
|
+
(New-Target 3 'Playwright-Go browsers' "$($P.L)\ms-playwright-go" -Mode units -Dev $true -KeepNewest $true -GroupBy $Script:WS_GROUP_BY_FAMILY)
|
|
11
|
+
(New-Target 3 'Puppeteer browsers' "$($P.U)\.cache\puppeteer\*" -Kind glob -Mode units -Dev $true -KeepNewest $true -Note 'per browser family: newest build kept')
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function Invoke-Section03 {
|
|
16
|
+
Write-SectionIntro @(
|
|
17
|
+
'Test runners download whole browsers per version. The newest build of each browser is always kept; older',
|
|
18
|
+
'builds go once idle for the window. A missing build is re-downloaded by the next install or "playwright install".'
|
|
19
|
+
) -Dev $true
|
|
20
|
+
$targets = Get-Targets03
|
|
21
|
+
$null = Show-TargetSizes $targets
|
|
22
|
+
if (-not (Confirm-Section 'Prune superseded test-runner browsers now?')) { Write-Info 'skipped'; return }
|
|
23
|
+
$null = Invoke-TargetList $targets
|
|
24
|
+
}
|