windowsweep 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI-INTEGRATION-GUIDE.md +203 -0
- package/CHANGELOG.md +124 -0
- package/LICENSE +21 -21
- package/README.md +88 -49
- package/VERSION +1 -1
- package/lib/actions.ps1 +212 -209
- package/lib/config.ps1 +108 -99
- package/lib/constants.ps1 +103 -89
- package/lib/fs.ps1 +189 -180
- package/lib/log.ps1 +140 -140
- package/lib/safety.ps1 +541 -493
- package/lib/scan.ps1 +109 -94
- package/lib/ui.ps1 +275 -235
- package/modules/android_avd.ps1 +36 -36
- package/modules/app_caches.ps1 +58 -58
- package/modules/browsers.ps1 +33 -33
- package/modules/build_tools.ps1 +35 -35
- package/modules/crash_trap.ps1 +89 -89
- package/modules/disk_usage.ps1 +49 -49
- package/modules/docker.ps1 +93 -93
- package/modules/editors.ps1 +150 -150
- package/modules/globals_audit.ps1 +236 -0
- package/modules/health.ps1 +101 -101
- package/modules/installed_programs.ps1 +118 -0
- package/modules/menu.ps1 +63 -61
- package/modules/notify.ps1 +60 -0
- package/modules/orphaned_appdata.ps1 +189 -0
- package/modules/personal.ps1 +126 -124
- package/modules/pkg_managers.ps1 +73 -69
- package/modules/projects.ps1 +160 -143
- package/modules/release_helpers.ps1 +438 -434
- package/modules/reports.ps1 +212 -212
- package/modules/runner.ps1 +242 -190
- package/modules/self_test_extra.ps1 +537 -86
- package/modules/startup_audit.ps1 +126 -0
- package/modules/system_admin.ps1 +220 -220
- package/modules/test_runners.ps1 +24 -24
- package/modules/walkthrough.ps1 +80 -79
- package/modules/windows_user.ps1 +118 -112
- package/package.json +4 -3
- package/windowsweep.ps1 +322 -296
package/windowsweep.ps1
CHANGED
|
@@ -1,296 +1,322 @@
|
|
|
1
|
-
# windowsweep - safe, developer-aware Windows cleanup CLI. Entry point.
|
|
2
|
-
#
|
|
3
|
-
# windowsweep.ps1 (guided walkthrough)
|
|
4
|
-
# windowsweep.ps1 --help (every mode and option)
|
|
5
|
-
#
|
|
6
|
-
# Runs on Windows PowerShell 5.1 and PowerShell 7. Flags are parsed from $args (no param block) so
|
|
7
|
-
# GNU-style options survive `powershell -File`. Source is ASCII-only by design.
|
|
8
|
-
#
|
|
9
|
-
# Author: Ahsan Mahmood <aoneahsan@gmail.com> https://aoneahsan.com
|
|
10
|
-
# License: MIT (see LICENSE)
|
|
11
|
-
|
|
12
|
-
$ErrorActionPreference = 'Continue'
|
|
13
|
-
$Script:WS_ROOT = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
14
|
-
|
|
15
|
-
$Script:WS = @{
|
|
16
|
-
Mode = 'walkthrough'; RawArgs = @($args); ScriptPath = $MyInvocation.MyCommand.Path
|
|
17
|
-
DryRun = $false; Yes = $false; Deep = $false; Elevate = $false; ElevatedChild = $false; BatchMode = $false
|
|
18
|
-
Days = $null; TempDays = $null; LargeFileMb = $null; PurgeAll = $false
|
|
19
|
-
DeveloperFlag = $null; ForgetDeveloper = $false; Developer = $null; DeveloperSource = ''
|
|
20
|
-
ScanRoots = @(); ExcludePaths = @(); Hiberfil = ''; ResetBase = $false; Permanent = $false
|
|
21
|
-
OnlyList = ''; Profile = ''; Exclude = ''; ExportFmt = 'both'; ExportId = 'latest'; PruneDays = 90
|
|
22
|
-
LogsDir = ''; ReportsDir = ''; NoReport = $false; CleanupLogs = $false
|
|
23
|
-
JsonMode = $false; Quiet = $false; NoColor = $false; Ascii = $false
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
$
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
--
|
|
53
|
-
--
|
|
54
|
-
--
|
|
55
|
-
--
|
|
56
|
-
--
|
|
57
|
-
--
|
|
58
|
-
--
|
|
59
|
-
--
|
|
60
|
-
--
|
|
61
|
-
--
|
|
62
|
-
--
|
|
63
|
-
--
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
--
|
|
68
|
-
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
--
|
|
72
|
-
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
--
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
--
|
|
79
|
-
--
|
|
80
|
-
--
|
|
81
|
-
--
|
|
82
|
-
--
|
|
83
|
-
--
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
$
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
'--
|
|
129
|
-
'--
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
'--
|
|
135
|
-
'--
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
'--
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
'--
|
|
145
|
-
'--
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
{ $_ -in '-
|
|
151
|
-
'--
|
|
152
|
-
'--
|
|
153
|
-
'--
|
|
154
|
-
|
|
155
|
-
'--
|
|
156
|
-
'--
|
|
157
|
-
'--
|
|
158
|
-
{ $_ -in '
|
|
159
|
-
'--
|
|
160
|
-
|
|
161
|
-
'--
|
|
162
|
-
'--
|
|
163
|
-
'--
|
|
164
|
-
'--
|
|
165
|
-
'--
|
|
166
|
-
'--
|
|
167
|
-
'--
|
|
168
|
-
'--no-
|
|
169
|
-
'--
|
|
170
|
-
'--
|
|
171
|
-
{ $
|
|
172
|
-
'--
|
|
173
|
-
'--
|
|
174
|
-
'--
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
$ws.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
$ws.
|
|
208
|
-
$
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
$ws = $
|
|
215
|
-
$
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if ($
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
1
|
+
# windowsweep - safe, developer-aware Windows cleanup CLI. Entry point.
|
|
2
|
+
#
|
|
3
|
+
# windowsweep.ps1 (guided walkthrough)
|
|
4
|
+
# windowsweep.ps1 --help (every mode and option)
|
|
5
|
+
#
|
|
6
|
+
# Runs on Windows PowerShell 5.1 and PowerShell 7. Flags are parsed from $args (no param block) so
|
|
7
|
+
# GNU-style options survive `powershell -File`. Source is ASCII-only by design.
|
|
8
|
+
#
|
|
9
|
+
# Author: Ahsan Mahmood <aoneahsan@gmail.com> https://aoneahsan.com
|
|
10
|
+
# License: MIT (see LICENSE)
|
|
11
|
+
|
|
12
|
+
$ErrorActionPreference = 'Continue'
|
|
13
|
+
$Script:WS_ROOT = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
14
|
+
|
|
15
|
+
$Script:WS = @{
|
|
16
|
+
Mode = 'walkthrough'; RawArgs = @($args); ScriptPath = $MyInvocation.MyCommand.Path
|
|
17
|
+
DryRun = $false; Yes = $false; Deep = $false; Elevate = $false; ElevatedChild = $false; BatchMode = $false
|
|
18
|
+
Days = $null; TempDays = $null; LargeFileMb = $null; PurgeAll = $false
|
|
19
|
+
DeveloperFlag = $null; ForgetDeveloper = $false; Developer = $null; DeveloperSource = ''
|
|
20
|
+
ScanRoots = @(); ExcludePaths = @(); Excluded = @(); Hiberfil = ''; ResetBase = $false; Permanent = $false
|
|
21
|
+
OnlyList = ''; Profile = ''; Exclude = ''; ExportFmt = 'both'; ExportId = 'latest'; PruneDays = 90
|
|
22
|
+
LogsDir = ''; ReportsDir = ''; NoReport = $false; CleanupLogs = $false
|
|
23
|
+
JsonMode = $false; Quiet = $false; NoColor = $false; Ascii = $false; Notify = $false
|
|
24
|
+
# Scripted selection (--select / --select-file): a person's choice made in advance. SelectActive is LATCHED
|
|
25
|
+
# once at parse time because SelectQueue empties as prompts consume it.
|
|
26
|
+
SelectQueue = @(); SelectPaths = @(); SelectActive = $false; LastSelectionScripted = $false
|
|
27
|
+
Candidates = @(); ScanTargets = @()
|
|
28
|
+
TotalFreed = [long]0; TotalEstimated = [long]0; SectionFreed = [long]0
|
|
29
|
+
Refusals = @(); Hints = @(); ExitCode = 0; Finished = $false; AllowOwnData = $false
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
foreach ($lib in @('constants', 'ui', 'log', 'fs', 'safety', 'config', 'scan', 'actions')) {
|
|
33
|
+
. (Join-Path $Script:WS_ROOT "lib\$lib.ps1")
|
|
34
|
+
}
|
|
35
|
+
foreach ($m in (Get-ChildItem -LiteralPath (Join-Path $Script:WS_ROOT 'modules') -Filter '*.ps1' | Sort-Object Name)) {
|
|
36
|
+
. $m.FullName
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function Show-Usage {
|
|
40
|
+
$v = Get-ToolVersion
|
|
41
|
+
$text = @"
|
|
42
|
+
$Script:WS_NAME v$v - names every path before it touches one
|
|
43
|
+
|
|
44
|
+
USAGE
|
|
45
|
+
windowsweep [mode] [options]
|
|
46
|
+
|
|
47
|
+
MODES (default = the guided walkthrough)
|
|
48
|
+
-w, --walkthrough Guided walkthrough, asks before every step
|
|
49
|
+
-m, --menu Jump-to menu - run one section at a time
|
|
50
|
+
-a, --all Safe batch: sections $($Script:WS_SAFE_BATCH -join ',') (+$($Script:WS_SAFE_BATCH_ADMIN -join ',') when elevated)
|
|
51
|
+
-s, --scan Every target with its size; nothing is deleted
|
|
52
|
+
--only L Run exactly these sections, e.g. --only 1,3,5-7
|
|
53
|
+
--profile NAME dev | minimal | cache-only | system | deep | audit
|
|
54
|
+
--exclude L Drop sections from --all / --profile
|
|
55
|
+
--list Print the section catalogue and exit
|
|
56
|
+
--list-targets Print every path the tool can touch (read-only)
|
|
57
|
+
--self-test Verify syntax, safety guards, dry-run and junction handling
|
|
58
|
+
--reports Reports manager: list / view / export past runs
|
|
59
|
+
--export F [ID] Export a report: F = md | html | both, ID = N | latest | all
|
|
60
|
+
--stats Run history from ~\.windowsweep
|
|
61
|
+
--prune-history N Delete logs + reports older than N days (default 90)
|
|
62
|
+
--feedback How to report a bug (offline; nothing is sent)
|
|
63
|
+
--report-issue Open a pre-filled GitHub issue in your browser (you review it first)
|
|
64
|
+
--debug-bundle Zip the latest log + report for a bug report
|
|
65
|
+
--install-task Weekly Scheduled Task (Sunday 03:00, safe batch) --uninstall-task
|
|
66
|
+
--install-alias Add a 'cleanup' function to your PowerShell profile --uninstall-alias
|
|
67
|
+
--uninstall-data Remove ~\.windowsweep (logs, reports, config) after confirming
|
|
68
|
+
-V, --version Version and author -h, --help This text
|
|
69
|
+
|
|
70
|
+
OPTIONS
|
|
71
|
+
--dry-run Delete nothing; show what would go and how much it frees
|
|
72
|
+
-y, --yes Auto-confirm regenerable-cache steps (never personal files)
|
|
73
|
+
--i-understand-deep Allow deep sections (11 Recycle Bin, 15 hibernation, 16 event logs,
|
|
74
|
+
20 disk-image compaction) to run unattended with --yes
|
|
75
|
+
--elevate Relaunch elevated (UAC prompt) so admin sections can run
|
|
76
|
+
-d, --days N Idle threshold for caches (default 100). A file goes only when its newest
|
|
77
|
+
timestamp (write, access, creation) is at least N days old
|
|
78
|
+
--temp-days N Idle threshold for %TEMP% (default 3)
|
|
79
|
+
--purge-all Clear cache targets completely instead of pruning idle files
|
|
80
|
+
--developer / --not-developer Override the saved developer answer for this run
|
|
81
|
+
--forget-developer Ask the developer question again
|
|
82
|
+
--scan-roots "P1;P2" Project roots for section 17 --exclude-path P (repeatable, all sections)
|
|
83
|
+
--large-file-mb N Minimum size for section 19 (default 100)
|
|
84
|
+
--hiberfil off|reduced|keep What section 15 does --reset-base DISM /ResetBase in 14
|
|
85
|
+
--permanent Sections 18, 19 and 23 delete instead of using the Recycle Bin
|
|
86
|
+
--select L Pre-answer the next interactive selection, e.g. --select 1,3-5. Repeatable: the
|
|
87
|
+
lists are consumed in the order the prompts appear
|
|
88
|
+
--select-file P UTF-8 file of one full path per line, matched against each prompt's candidates.
|
|
89
|
+
Either flag lets sections 17/18/19/23 run unattended - a person did choose
|
|
90
|
+
--notify Show a Windows notification when the run ends
|
|
91
|
+
--logs-dir P / --reports-dir P / --no-report / --cleanup-logs
|
|
92
|
+
--json One-line JSON summary on stdout (everything else on stderr)
|
|
93
|
+
-q, --quiet Less chatter --no-color --ascii (plain glyphs)
|
|
94
|
+
|
|
95
|
+
SAFETY
|
|
96
|
+
Every deletion passes one chokepoint that refuses drive roots, Windows, Program Files, your
|
|
97
|
+
profile root, Documents/Pictures/Desktop, credentials, toolchains and browser/editor state, and
|
|
98
|
+
that never follows a junction or symlink. Sections 17, 18, 19 and 23 ask you to pick item by
|
|
99
|
+
item; 18, 19 and 23 send what you pick to the Recycle Bin. --scan and --dry-run delete nothing,
|
|
100
|
+
though both still write a log and a report. No network calls, ever.
|
|
101
|
+
|
|
102
|
+
OUTPUT
|
|
103
|
+
Logs: $($Script:WS.LogsDir)
|
|
104
|
+
Reports: $($Script:WS.ReportsDir)
|
|
105
|
+
|
|
106
|
+
$Script:WS_NAME v$v by $Script:WS_AUTHOR <$Script:WS_EMAIL> $Script:WS_WEB
|
|
107
|
+
$Script:WS_REPO - $Script:WS_LICENSE
|
|
108
|
+
"@
|
|
109
|
+
Write-Host $text
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function Get-NextArg {
|
|
113
|
+
param([string[]] $Argv, [ref] $Index, [string] $Name)
|
|
114
|
+
$Index.Value++
|
|
115
|
+
if ($Index.Value -ge $Argv.Count) { throw "option $Name needs a value" }
|
|
116
|
+
return [string]$Argv[$Index.Value]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function Read-Arguments {
|
|
120
|
+
$ws = $Script:WS
|
|
121
|
+
$argv = @($ws.RawArgs)
|
|
122
|
+
$i = 0
|
|
123
|
+
while ($i -lt $argv.Count) {
|
|
124
|
+
$a = [string]$argv[$i]
|
|
125
|
+
$inline = $null
|
|
126
|
+
if ($a -match '^(--[A-Za-z][A-Za-z0-9-]*)=(.*)$') { $a = $Matches[1]; $inline = $Matches[2] }
|
|
127
|
+
switch ($a) {
|
|
128
|
+
{ $_ -in '-w', '--walkthrough' } { $ws.Mode = 'walkthrough' }
|
|
129
|
+
{ $_ -in '-m', '--menu', '-i', '--interactive' } { $ws.Mode = 'menu' }
|
|
130
|
+
{ $_ -in '-a', '--all', '--all-safe' } { $ws.Mode = 'all'; $ws.BatchMode = $true }
|
|
131
|
+
{ $_ -in '-s', '--scan' } { $ws.Mode = 'scan' }
|
|
132
|
+
'--only' { $ws.Mode = 'only'; $ws.BatchMode = $true; if ($null -ne $inline) { $ws.OnlyList = $inline } else { $ws.OnlyList = Get-NextArg $argv ([ref]$i) $a } }
|
|
133
|
+
'--profile' { $ws.Mode = 'only'; $ws.BatchMode = $true; if ($null -ne $inline) { $ws.Profile = $inline } else { $ws.Profile = Get-NextArg $argv ([ref]$i) $a } }
|
|
134
|
+
'--exclude' { if ($null -ne $inline) { $ws.Exclude = $inline } else { $ws.Exclude = Get-NextArg $argv ([ref]$i) $a } }
|
|
135
|
+
'--list' { $ws.Mode = 'list' }
|
|
136
|
+
'--list-targets' { $ws.Mode = 'list_targets' }
|
|
137
|
+
'--self-test' { $ws.Mode = 'self_test' }
|
|
138
|
+
'--reports' { $ws.Mode = 'reports' }
|
|
139
|
+
'--export' {
|
|
140
|
+
$ws.Mode = 'export'
|
|
141
|
+
if ($null -ne $inline) { $ws.ExportFmt = $inline } elseif (($i + 1) -lt $argv.Count -and -not ([string]$argv[$i + 1]).StartsWith('-')) { $i++; $ws.ExportFmt = [string]$argv[$i] }
|
|
142
|
+
if (($i + 1) -lt $argv.Count -and -not ([string]$argv[$i + 1]).StartsWith('-')) { $i++; $ws.ExportId = [string]$argv[$i] }
|
|
143
|
+
}
|
|
144
|
+
'--stats' { $ws.Mode = 'stats' }
|
|
145
|
+
'--prune-history' {
|
|
146
|
+
$ws.Mode = 'prune_history'
|
|
147
|
+
if ($null -ne $inline) { $ws.PruneDays = [int]$inline } elseif (($i + 1) -lt $argv.Count -and ([string]$argv[$i + 1]) -match '^\d+$') { $i++; $ws.PruneDays = [int]$argv[$i] }
|
|
148
|
+
}
|
|
149
|
+
'--feedback' { $ws.Mode = 'feedback' }
|
|
150
|
+
{ $_ -in '--report-issue', '--report-bug' } { $ws.Mode = 'report_issue' }
|
|
151
|
+
'--debug-bundle' { $ws.Mode = 'debug_bundle' }
|
|
152
|
+
'--install-task' { $ws.Mode = 'install_task' }
|
|
153
|
+
'--uninstall-task' { $ws.Mode = 'uninstall_task' }
|
|
154
|
+
'--install-alias' { $ws.Mode = 'install_alias' }
|
|
155
|
+
'--uninstall-alias' { $ws.Mode = 'uninstall_alias' }
|
|
156
|
+
'--uninstall-data' { $ws.Mode = 'uninstall_data' }
|
|
157
|
+
{ $_ -in '-V', '--version' } { $ws.Mode = 'version' }
|
|
158
|
+
{ $_ -in '-h', '--help', '/?', '-?' } { $ws.Mode = 'help' }
|
|
159
|
+
{ $_ -in '-n', '--dry-run' } { $ws.DryRun = $true }
|
|
160
|
+
{ $_ -in '-y', '--yes' } { $ws.Yes = $true }
|
|
161
|
+
'--i-understand-deep' { $ws.Deep = $true }
|
|
162
|
+
'--elevate' { $ws.Elevate = $true }
|
|
163
|
+
'--elevated-child' { $ws.ElevatedChild = $true }
|
|
164
|
+
{ $_ -in '-d', '--days' } { $v = $inline; if ($null -eq $v) { $v = Get-NextArg $argv ([ref]$i) $a }; if ($v -notmatch '^\d+$') { throw "--days needs a whole number, got '$v'" }; $ws.Days = [int]$v }
|
|
165
|
+
'--temp-days' { $v = $inline; if ($null -eq $v) { $v = Get-NextArg $argv ([ref]$i) $a }; if ($v -notmatch '^\d+$') { throw "--temp-days needs a whole number, got '$v'" }; $ws.TempDays = [int]$v }
|
|
166
|
+
'--purge-all' { $ws.PurgeAll = $true }
|
|
167
|
+
'--developer' { $ws.DeveloperFlag = $true }
|
|
168
|
+
{ $_ -in '--not-developer', '--no-developer' } { $ws.DeveloperFlag = $false }
|
|
169
|
+
'--forget-developer' { $ws.ForgetDeveloper = $true }
|
|
170
|
+
'--scan-roots' { $v = $inline; if ($null -eq $v) { $v = Get-NextArg $argv ([ref]$i) $a }; $ws.ScanRoots = @($v -split ';' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) }
|
|
171
|
+
'--exclude-path' { $v = $inline; if ($null -eq $v) { $v = Get-NextArg $argv ([ref]$i) $a }; $ws.ExcludePaths += $v }
|
|
172
|
+
'--large-file-mb' { $v = $inline; if ($null -eq $v) { $v = Get-NextArg $argv ([ref]$i) $a }; if ($v -notmatch '^\d+$') { throw "--large-file-mb needs a whole number, got '$v'" }; $ws.LargeFileMb = [int]$v }
|
|
173
|
+
'--hiberfil' { $v = $inline; if ($null -eq $v) { $v = Get-NextArg $argv ([ref]$i) $a }; if ($v -notin 'off', 'reduced', 'keep') { throw "--hiberfil must be off, reduced or keep" }; $ws.Hiberfil = $v }
|
|
174
|
+
'--reset-base' { $ws.ResetBase = $true }
|
|
175
|
+
'--permanent' { $ws.Permanent = $true }
|
|
176
|
+
'--logs-dir' { $v = $inline; if ($null -eq $v) { $v = Get-NextArg $argv ([ref]$i) $a }; $ws.LogsDir = $v }
|
|
177
|
+
'--reports-dir' { $v = $inline; if ($null -eq $v) { $v = Get-NextArg $argv ([ref]$i) $a }; $ws.ReportsDir = $v }
|
|
178
|
+
'--no-report' { $ws.NoReport = $true }
|
|
179
|
+
'--cleanup-logs' { $ws.CleanupLogs = $true }
|
|
180
|
+
'--notify' { $ws.Notify = $true }
|
|
181
|
+
'--select' { $v = $inline; if ($null -eq $v) { $v = Get-NextArg $argv ([ref]$i) $a }; $ws.SelectQueue += $v }
|
|
182
|
+
'--select-file' {
|
|
183
|
+
$v = $inline; if ($null -eq $v) { $v = Get-NextArg $argv ([ref]$i) $a }
|
|
184
|
+
$f = Get-FullPath $v
|
|
185
|
+
if (-not $f -or -not (Test-Path -LiteralPath $f)) { throw "--select-file not found: $v" }
|
|
186
|
+
$ws.SelectPaths = @([IO.File]::ReadAllLines($f) | ForEach-Object { $_.Trim() } | Where-Object { $_ -and -not $_.StartsWith('#') })
|
|
187
|
+
if ($ws.SelectPaths.Count -eq 0) { throw "--select-file has no paths in it: $v" }
|
|
188
|
+
}
|
|
189
|
+
'--json' { $ws.JsonMode = $true; $ws.Quiet = $true }
|
|
190
|
+
{ $_ -in '-q', '--quiet' } { $ws.Quiet = $true }
|
|
191
|
+
'--no-color' { $ws.NoColor = $true }
|
|
192
|
+
'--ascii' { $ws.Ascii = $true }
|
|
193
|
+
'--pwsh' { }
|
|
194
|
+
default { throw "unknown argument: $a" }
|
|
195
|
+
}
|
|
196
|
+
$i++
|
|
197
|
+
}
|
|
198
|
+
if ($ws.Profile -and -not $Script:WS_PROFILES.ContainsKey($ws.Profile)) {
|
|
199
|
+
throw "unknown profile '$($ws.Profile)' (known: $($Script:WS_PROFILES.Keys -join ', '))"
|
|
200
|
+
}
|
|
201
|
+
# Latched here, once: the queue is drained by the prompts, so a recomputed flag would flip to false mid-run.
|
|
202
|
+
$ws.SelectActive = ($ws.SelectQueue.Count -gt 0 -or $ws.SelectPaths.Count -gt 0)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function Initialize-Paths {
|
|
206
|
+
$ws = $Script:WS
|
|
207
|
+
$ws.Stamp = (Get-Date -Format 'yyyy-MM-dd_HHmmss') + '-' + $PID
|
|
208
|
+
$dataHome = $env:WINDOWSWEEP_HOME
|
|
209
|
+
if ([string]::IsNullOrWhiteSpace($dataHome)) { $dataHome = Join-Path $env:USERPROFILE ".$Script:WS_NAME" }
|
|
210
|
+
$ws.Home = Get-FullPath $dataHome
|
|
211
|
+
if (-not $ws.LogsDir) { $ws.LogsDir = $env:WINDOWSWEEP_LOG_DIR }
|
|
212
|
+
if (-not $ws.LogsDir) { $ws.LogsDir = Join-Path $ws.Home 'logs' }
|
|
213
|
+
if (-not $ws.ReportsDir) { $ws.ReportsDir = $env:WINDOWSWEEP_REPORTS_DIR }
|
|
214
|
+
if (-not $ws.ReportsDir) { $ws.ReportsDir = Join-Path $ws.Home 'reports' }
|
|
215
|
+
$ws.LogsDir = Get-FullPath $ws.LogsDir
|
|
216
|
+
$ws.ReportsDir = Get-FullPath $ws.ReportsDir
|
|
217
|
+
$ws.FeedbackDir = Join-Path $ws.Home 'feedback'
|
|
218
|
+
$ws.ConfigPath = Join-Path $ws.Home 'config.json'
|
|
219
|
+
$ws.Launcher = $env:WINDOWSWEEP_LAUNCHER
|
|
220
|
+
if (-not $ws.Launcher) { $ws.Launcher = 'powershell' }
|
|
221
|
+
# Well-known roots used by every module. LL = LocalLow, SR = SystemRoot, PD = ProgramData, SD = system drive.
|
|
222
|
+
$Script:P = @{
|
|
223
|
+
U = (Get-FullPath $env:USERPROFILE); L = (Get-FullPath $env:LOCALAPPDATA); A = (Get-FullPath $env:APPDATA)
|
|
224
|
+
LL = (Get-FullPath (Join-Path (Split-Path -Parent $env:LOCALAPPDATA) 'LocalLow')); SR = (Get-FullPath $env:SystemRoot)
|
|
225
|
+
PD = (Get-FullPath $env:ProgramData); SD = $env:SystemDrive; TEMP = (Get-FullPath $env:TEMP); TMP = (Get-FullPath $env:TMP)
|
|
226
|
+
PF = (Get-FullPath $env:ProgramFiles); PF86 = (Get-FullPath ${env:ProgramFiles(x86)})
|
|
227
|
+
}
|
|
228
|
+
$ws.Npx = ($env:WINDOWSWEEP_NPX -eq '1')
|
|
229
|
+
$ws.IsAdmin = Test-IsAdmin
|
|
230
|
+
try { $ws.OsCaption = (Get-CimInstance Win32_OperatingSystem -ErrorAction Stop).Caption } catch { $ws.OsCaption = 'Windows' }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function Initialize-Settings {
|
|
234
|
+
<# .SYNOPSIS Merge config defaults under explicit flags. #>
|
|
235
|
+
$ws = $Script:WS
|
|
236
|
+
$cfg = Import-Config
|
|
237
|
+
if ($null -eq $ws.Days) { $ws.Days = [int]$cfg.days }
|
|
238
|
+
if ($null -eq $ws.TempDays) { $ws.TempDays = [int]$cfg.tempDays }
|
|
239
|
+
if ($null -eq $ws.LargeFileMb) { $ws.LargeFileMb = [int]$cfg.largeFileMb }
|
|
240
|
+
if ($ws.ScanRoots.Count -eq 0 -and $cfg.scanRoots) { $ws.ScanRoots = @($cfg.scanRoots) }
|
|
241
|
+
if ($cfg.excludePaths) { $ws.ExcludePaths = @($ws.ExcludePaths) + @($cfg.excludePaths) }
|
|
242
|
+
# Must come after the merge above, or every config-file exclusion is silently dropped.
|
|
243
|
+
Initialize-Exclusions
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function Invoke-Main {
|
|
247
|
+
$ws = $Script:WS
|
|
248
|
+
switch ($ws.Mode) {
|
|
249
|
+
'help' { Show-Usage; return }
|
|
250
|
+
'version' { Show-Version; return }
|
|
251
|
+
'list' { Show-SectionList; return }
|
|
252
|
+
'list_targets' { Show-TargetList; return }
|
|
253
|
+
'self_test' { $ws.ExitCode = Invoke-SelfTest; return }
|
|
254
|
+
'scan' { Invoke-ScanMode; return }
|
|
255
|
+
'all' { Invoke-BatchMode; return }
|
|
256
|
+
'only' { Invoke-BatchMode; return }
|
|
257
|
+
'walkthrough' { Invoke-Walkthrough; return }
|
|
258
|
+
'menu' { Invoke-Menu; return }
|
|
259
|
+
'reports' { Invoke-ReportsManager; return }
|
|
260
|
+
'export' { $ws.ExitCode = Export-Reports -Format $ws.ExportFmt -Id $ws.ExportId; return }
|
|
261
|
+
'stats' { Show-Stats; return }
|
|
262
|
+
'prune_history' { Remove-OldHistory -Days $ws.PruneDays; return }
|
|
263
|
+
'feedback' { Show-Feedback; return }
|
|
264
|
+
'report_issue' { Invoke-ReportIssue; return }
|
|
265
|
+
'debug_bundle' { New-DebugBundle; return }
|
|
266
|
+
'install_task' { Install-WeeklyTask; return }
|
|
267
|
+
'uninstall_task' { Uninstall-WeeklyTask; return }
|
|
268
|
+
'install_alias' { Install-ProfileAlias; return }
|
|
269
|
+
'uninstall_alias' { Uninstall-ProfileAlias; return }
|
|
270
|
+
'uninstall_data' { Remove-ToolData; return }
|
|
271
|
+
default { Write-Err "unknown mode: $($ws.Mode)"; $ws.ExitCode = $Script:WS_EXIT_USAGE }
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
# ---------------------------------------------------------------------------------------------
|
|
276
|
+
# Bootstrap
|
|
277
|
+
# ---------------------------------------------------------------------------------------------
|
|
278
|
+
try {
|
|
279
|
+
Read-Arguments
|
|
280
|
+
} catch {
|
|
281
|
+
[Console]::Error.WriteLine("windowsweep: $($_.Exception.Message)")
|
|
282
|
+
[Console]::Error.WriteLine("try: windowsweep --help")
|
|
283
|
+
exit 2
|
|
284
|
+
}
|
|
285
|
+
Initialize-Paths
|
|
286
|
+
Initialize-Ui
|
|
287
|
+
Initialize-Safety
|
|
288
|
+
$quietModes = @('help', 'version', 'list')
|
|
289
|
+
if ($Script:WS.Mode -notin $quietModes) { Initialize-Log }
|
|
290
|
+
Initialize-Settings
|
|
291
|
+
Initialize-Report
|
|
292
|
+
Initialize-CrashTrap
|
|
293
|
+
|
|
294
|
+
if ($Script:WS.Elevate -and -not $Script:WS.IsAdmin) {
|
|
295
|
+
$code = Invoke-Elevated
|
|
296
|
+
Restore-Ui
|
|
297
|
+
exit $code
|
|
298
|
+
}
|
|
299
|
+
if ($Script:WS.Elevate -and $Script:WS.IsAdmin) { Write-Note 'already elevated; --elevate is a no-op' }
|
|
300
|
+
|
|
301
|
+
if ($Script:WS.Mode -in @('walkthrough', 'menu', 'all', 'only')) { Resolve-DeveloperMode; Confirm-PurgeAllOnce }
|
|
302
|
+
# A scan deletes nothing, so it neither asks the developer question nor confirms a purge - but it must still
|
|
303
|
+
# REPORT the answer it was given. -ReadOnly takes the flag or the saved answer and stops there.
|
|
304
|
+
elseif ($Script:WS.Mode -eq 'scan') { Resolve-DeveloperMode -ReadOnly }
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
Invoke-Main
|
|
308
|
+
$Script:WS.Finished = $true
|
|
309
|
+
} catch {
|
|
310
|
+
Invoke-CrashBundle -ErrorRecord $_
|
|
311
|
+
$Script:WS.ExitCode = $Script:WS_EXIT_FAIL
|
|
312
|
+
} finally {
|
|
313
|
+
Restore-Ui
|
|
314
|
+
if ($Script:WS.CleanupLogs -and $Script:WS.LogFile -and (Test-Path -LiteralPath $Script:WS.LogFile)) {
|
|
315
|
+
try { Remove-Item -LiteralPath $Script:WS.LogFile -Force -ErrorAction SilentlyContinue } catch { $null = $_ }
|
|
316
|
+
}
|
|
317
|
+
if (-not $Script:WS.Finished -and $Script:WS.ExitCode -eq 0) {
|
|
318
|
+
Write-LogLine 'run interrupted before it finished (Ctrl-C or host stop) - exit 130'
|
|
319
|
+
exit $Script:WS_EXIT_INTERRUPT
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
exit $Script:WS.ExitCode
|