windowsweep 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,79 @@
1
+ # walkthrough.ps1 - the default interactive mode: every category, one step at a time, with a running total.
2
+
3
+ function Show-Welcome {
4
+ $ws = $Script:WS
5
+ Write-Banner
6
+ Write-Box "$Script:WS_NAME" $Script:WS_TAGLINE
7
+ Show-DriveTable
8
+ Write-UiLine '' 'Gray'
9
+ Write-Plain ' This walkthrough visits every cleanup category. At each step:'
10
+ Write-Plain ' a run it (default) s skip it q stop the walkthrough'
11
+ if ($ws.DryRun) { Write-UiLine ' DRY-RUN: every step only reports what it would remove.' 'Magenta' }
12
+ $dev = 'off'
13
+ if ($ws.Developer) { $dev = 'on' }
14
+ Write-Note "developer mode: $dev idle window: $($ws.Days) days temp window: $($ws.TempDays) days"
15
+ Write-Note "logs and reports: $($ws.Home)"
16
+ Write-Note "by $Script:WS_AUTHOR - $Script:WS_WEB"
17
+ Wait-Enter
18
+ }
19
+
20
+ function Invoke-Walkthrough {
21
+ $ws = $Script:WS
22
+ if (-not $ws.Interactive) {
23
+ Write-Err 'The walkthrough needs an interactive console. For unattended runs use: windowsweep --all --yes (or --scan / --dry-run to look first)'
24
+ $ws.ExitCode = $Script:WS_EXIT_USAGE
25
+ return
26
+ }
27
+ Show-Welcome
28
+ Write-Box 'Pre-scan' 'Read-only - what each section can reach right now'
29
+ if (Get-Command Invoke-Section00 -ErrorAction SilentlyContinue) { Invoke-Section00 }
30
+ $null = Show-ScanTable
31
+ Wait-Enter
32
+
33
+ $steps = @($Script:WS_WALKTHROUGH)
34
+ if ($ws.IsAdmin) { $steps += $Script:WS_WALKTHROUGH_ADMIN }
35
+ $total = $steps.Count + 2
36
+ $n = 0
37
+ $quit = $false
38
+ foreach ($id in $steps) {
39
+ $n++
40
+ $sec = Get-Section $id
41
+ Write-Step -N $n -Total $total -Title $sec.Title
42
+ $choice = Read-Choice -Prompt ' action: a run s skip q quit > ' -Default 'a'
43
+ if ($choice -eq 'q') { Write-Warn 'walkthrough stopped'; Add-ReportStep -Section $id -Title $sec.Title -Status 'skipped' -Note 'quit'; $quit = $true; break }
44
+ if ($choice -eq 's') { Write-Info 'skipped'; Add-ReportStep -Section $id -Title $sec.Title -Status 'skipped'; continue }
45
+ $ws.SectionPreConfirmed = $true
46
+ try { Invoke-SectionById -Id $id } finally { $ws.SectionPreConfirmed = $false }
47
+ }
48
+ if (-not $quit -and -not $ws.IsAdmin) {
49
+ $n++
50
+ Write-Step -N $n -Total $total -Title 'System-level cleanup (needs Administrator)'
51
+ Write-Plain ' Sections 12 (Windows Update cache), 13 (Disk Cleanup engine) and 14 (component store) need an elevated'
52
+ Write-Plain ' console. Run them afterwards with:'
53
+ Write-Plain ' windowsweep --profile system --yes --elevate'
54
+ Write-Plain ' and, if you want the hibernation file gone too (section 15):'
55
+ Write-Plain ' windowsweep --only 12,13,14,15 --hiberfil off --yes --i-understand-deep --elevate'
56
+ $ws.Hints += 'Admin sections: windowsweep --profile system --yes --elevate'
57
+ Wait-Enter
58
+ }
59
+ if (-not $quit) {
60
+ $n++
61
+ Write-Step -N $n -Total $total -Title 'Disk usage report'
62
+ Invoke-SectionById -Id 21
63
+ }
64
+ Show-SessionSummary
65
+ Show-NextSteps
66
+ }
67
+
68
+ function Show-NextSteps {
69
+ $ws = $Script:WS
70
+ $tips = @()
71
+ if (-not (Get-ScheduledTask -TaskName $Script:WS_TASK_NAME -ErrorAction SilentlyContinue)) { $tips += 'Schedule the safe batch weekly: windowsweep --install-task' }
72
+ $tips += 'Browse or export past reports: windowsweep --reports'
73
+ if ($ws.DryRun) { $tips += 'This was a dry-run. Run the same command without --dry-run to reclaim the space.' }
74
+ if ($tips.Count -gt 0) {
75
+ Write-UiLine '' 'Gray'
76
+ Write-Info 'next steps:'
77
+ foreach ($t in $tips) { Write-Note $t }
78
+ }
79
+ }
@@ -0,0 +1,112 @@
1
+ # windows_user.ps1 - section 9 (Windows user caches), section 10 (user temp), section 11 (Recycle Bin).
2
+
3
+ function Get-Targets09 {
4
+ $P = $Script:P
5
+ return @(
6
+ (New-Target 9 'Internet cache (INetCache)' "$($P.L)\Microsoft\Windows\INetCache" -Mode clear -Note 'IE / legacy WebView cache')
7
+ (New-Target 9 'Windows Error Reporting (user)' "$($P.L)\Microsoft\Windows\WER" -Mode clear)
8
+ (New-Target 9 'Crash dumps' "$($P.L)\CrashDumps" -Mode clear-old -Days 7)
9
+ (New-Target 9 'DirectX shader cache' "$($P.L)\D3DSCache" -Mode clear)
10
+ (New-Target 9 'NVIDIA DirectX shader cache' "$($P.L)\NVIDIA\DXCache" -Mode clear)
11
+ (New-Target 9 'NVIDIA OpenGL shader cache' "$($P.L)\NVIDIA\GLCache" -Mode clear)
12
+ (New-Target 9 'NVIDIA NV_Cache' "$($P.L)\NVIDIA Corporation\NV_Cache" -Mode clear)
13
+ (New-Target 9 'AMD DirectX shader cache' "$($P.L)\AMD\DxCache" -Mode clear)
14
+ (New-Target 9 'AMD OpenGL shader cache' "$($P.L)\AMD\GLCache" -Mode clear)
15
+ (New-Target 9 'Intel shader cache' "$($P.L)\Intel\ShaderCache" -Mode clear)
16
+ (New-Target 9 'Remote Desktop bitmap cache' "$($P.L)\Microsoft\Terminal Server Client\Cache" -Mode clear)
17
+ (New-Target 9 'OneDrive logs' "$($P.L)\Microsoft\OneDrive\logs" -Mode clear-old -Days 7)
18
+ (New-Target 9 'OneDrive setup logs' "$($P.L)\Microsoft\OneDrive\setup\logs" -Mode clear-old -Days 7)
19
+ (New-Target 9 'Store apps TempState' "$($P.L)\Packages\*\TempState" -Kind glob -Mode clear)
20
+ (New-Target 9 'Store apps AC\Temp' "$($P.L)\Packages\*\AC\Temp" -Kind glob -Mode clear)
21
+ (New-Target 9 'Store apps AC\INetCache' "$($P.L)\Packages\*\AC\INetCache" -Kind glob -Mode clear)
22
+ (New-Target 9 'Themes cached files' "$($P.L)\Microsoft\Windows\Themes\CachedFiles" -Mode clear)
23
+ (New-Target 9 'PowerShell startup profile data' "$($P.L)\Microsoft\Windows\PowerShell\StartupProfileData-*" -Kind fileglob -Mode file)
24
+ (New-Target 9 'CLR usage logs' "$($P.L)\Microsoft\CLR_v4.0\UsageLogs" -Mode clear)
25
+ (New-Target 9 'CLR usage logs (32-bit)' "$($P.L)\Microsoft\CLR_v4.0_32\UsageLogs" -Mode clear)
26
+ (New-Target 9 'Explorer thumbcache leftovers' "$($P.L)\Microsoft\Windows\Explorer\ThumbCacheToDelete" -Mode clear)
27
+ (New-Target 9 'Explorer iconcache leftovers' "$($P.L)\Microsoft\Windows\Explorer\IconCacheToDelete" -Mode clear)
28
+ (New-Target 9 'Explorer startup logs' "$($P.L)\Microsoft\Windows\Explorer\ExplorerStartupLog*.etl" -Kind fileglob -Mode file)
29
+ (New-Target 9 'Windows schema cache' "$($P.L)\Microsoft\Windows\SchCache" -Mode clear)
30
+ (New-Target 9 'Delivery Optimization (per user)' "$($P.L)\Microsoft\Windows\DeliveryOptimization\Cache" -Mode clear)
31
+ (New-Target 9 'Certificate URL cache' "$($P.LL)\Microsoft\CryptnetUrlCache" -Mode clear)
32
+ (New-Target 9 'Diagnostics results' "$($P.L)\Diagnostics" -Mode clear-old -Days 30)
33
+ (New-Target 9 'DNS resolver cache' 'ipconfig /flushdns' -Kind cmd)
34
+ )
35
+ }
36
+
37
+ function Invoke-Section09 {
38
+ Write-SectionIntro @(
39
+ 'Caches Windows and its drivers rebuild silently: shader caches, error-report queues, crash dumps, Store-app',
40
+ 'temp folders, Remote Desktop bitmaps. Files an app is holding open are skipped. The Explorer thumbnail and',
41
+ 'icon databases themselves belong to Disk Cleanup (section 13, admin), which rebuilds them cleanly.'
42
+ )
43
+ $targets = Get-Targets09
44
+ $null = Show-TargetSizes $targets
45
+ if (-not (Confirm-Section 'Clear Windows user caches now?')) { Write-Info 'skipped'; return }
46
+ $null = Invoke-TargetList $targets
47
+ $r = Invoke-External -FilePath 'ipconfig.exe' -ArgumentList @('/flushdns') -Destructive -Quiet -Label 'ipconfig /flushdns'
48
+ if ($r.Ran -and $r.ExitCode -eq 0) { Write-Ok 'DNS resolver cache flushed' }
49
+ }
50
+
51
+ # ---------------------------------------------------------------------------------------------
52
+ # Section 10 - user temp
53
+ # ---------------------------------------------------------------------------------------------
54
+
55
+ function Get-TempRoots {
56
+ $P = $Script:P
57
+ $roots = @()
58
+ foreach ($r in @($P.TEMP, $P.TMP, (Join-Path $P.L 'Temp'), (Join-Path $P.LL 'Temp'))) {
59
+ if ($r -and (Test-DirPresent $r)) { $f = Get-FullPath $r; if ($roots -notcontains $f) { $roots += $f } }
60
+ }
61
+ return $roots
62
+ }
63
+
64
+ function Get-Targets10 {
65
+ $t = @()
66
+ foreach ($r in (Get-TempRoots)) { $t += (New-Target 10 'User temp' $r -Mode prune -Days $Script:WS.TempDays -Note "files idle $($Script:WS.TempDays)+ days; open files are skipped") }
67
+ return $t
68
+ }
69
+
70
+ function Invoke-Section10 {
71
+ $ws = $Script:WS
72
+ Write-SectionIntro @(
73
+ "Temporary files idle for $($ws.TempDays)+ days. Files a running program still has open are skipped, so an",
74
+ 'installer or build that is in progress right now is safe. Lower the window with --temp-days 1 for a harder sweep.'
75
+ )
76
+ $targets = @(Get-Targets10)
77
+ if ($targets.Count -eq 0) { Write-Info 'no temp folders found'; return }
78
+ $null = Show-TargetSizes $targets
79
+ if (-not (Confirm-Section "Remove temp files idle $($ws.TempDays)+ days?")) { Write-Info 'skipped'; return }
80
+ $null = Invoke-TargetList $targets
81
+ }
82
+
83
+ # ---------------------------------------------------------------------------------------------
84
+ # Section 11 - Recycle Bin
85
+ # ---------------------------------------------------------------------------------------------
86
+
87
+ function Get-Targets11 { return @((New-Target 11 'Recycle Bin (all drives)' 'Clear-RecycleBin' -Kind cmd -Note 'PERMANENT - everything in the bin is gone')) }
88
+
89
+ function Get-RecycleBinStats {
90
+ $items = 0; $bytes = [long]0
91
+ try {
92
+ $shell = New-Object -ComObject Shell.Application
93
+ $bin = $shell.NameSpace(0xA)
94
+ foreach ($i in $bin.Items()) { $items++; try { $bytes += [long]$i.Size } catch { $null = $_ } }
95
+ } catch { $null = $_ }
96
+ return [pscustomobject]@{ Items = $items; Bytes = $bytes }
97
+ }
98
+
99
+ function Invoke-Section11 {
100
+ $ws = $Script:WS
101
+ Write-SectionIntro @('Emptying the Recycle Bin is permanent: nothing in it can be restored afterwards.')
102
+ $s = Get-RecycleBinStats
103
+ Write-Kv 'Recycle Bin:' ("{0} item(s), {1}" -f $s.Items, (Format-Bytes $s.Bytes))
104
+ if ($s.Items -eq 0) { Write-Info 'already empty'; return }
105
+ if ($ws.DryRun) { Write-DryRun ("would empty the Recycle Bin (" + (Format-Bytes $s.Bytes) + ')'); Add-Freed $s.Bytes; return }
106
+ if (-not (Confirm-Ui -Prompt "Permanently delete all $($s.Items) item(s) in the Recycle Bin?" -Default 'n')) { Write-Info 'skipped'; return }
107
+ try {
108
+ Clear-RecycleBin -Force -ErrorAction Stop
109
+ Add-Freed $s.Bytes
110
+ Write-Ok ("Recycle Bin emptied (" + (Format-Bytes $s.Bytes) + ')')
111
+ } catch { Write-Err "could not empty the Recycle Bin: $($_.Exception.Message)" }
112
+ }
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "windowsweep",
3
+ "version": "1.0.0",
4
+ "description": "Safe-by-default Windows cleanup CLI - developer-aware, dry-run first, zero install via npx.",
5
+ "bin": {
6
+ "windowsweep": "bin/windowsweep.js"
7
+ },
8
+ "scripts": {
9
+ "start": "node bin/windowsweep.js",
10
+ "test": "node bin/windowsweep.js --self-test --no-color",
11
+ "dry-run": "node bin/windowsweep.js --dry-run --all --yes --no-color --no-report",
12
+ "list": "node bin/windowsweep.js --list",
13
+ "lint:syntax": "node -c bin/windowsweep.js && node bin/windowsweep.js --self-test --no-color --no-report",
14
+ "version:check": "node -e \"const fs=require('fs');const v=require('./package.json').version;const c=fs.readFileSync('lib/constants.ps1','utf8');const f=fs.readFileSync('VERSION','utf8').trim();if(!c.includes(\\\"WS_VERSION_FALLBACK = '\\\"+v+\\\"'\\\")||f!==v){console.error('version mismatch: package.json '+v+' VERSION '+f);process.exit(1)}console.log('version parity OK: '+v)\"",
15
+ "prepublishOnly": "npm run --silent version:check && node -c bin/windowsweep.js"
16
+ },
17
+ "files": [
18
+ "bin/",
19
+ "lib/",
20
+ "modules/",
21
+ "windowsweep.ps1",
22
+ "windowsweep.cmd",
23
+ "README.md",
24
+ "LICENSE",
25
+ "CHANGELOG.md",
26
+ "SECURITY.md",
27
+ "VERSION"
28
+ ],
29
+ "engines": {
30
+ "node": ">=14"
31
+ },
32
+ "os": [
33
+ "win32"
34
+ ],
35
+ "keywords": [
36
+ "windows",
37
+ "cleanup",
38
+ "disk-cleanup",
39
+ "cache-cleanup",
40
+ "disk-space",
41
+ "temp-files",
42
+ "node-modules",
43
+ "npm-cache",
44
+ "yarn-cache",
45
+ "developer-tools",
46
+ "powershell",
47
+ "cli",
48
+ "dry-run"
49
+ ],
50
+ "author": {
51
+ "name": "Ahsan Mahmood",
52
+ "email": "aoneahsan@gmail.com",
53
+ "url": "https://aoneahsan.com"
54
+ },
55
+ "license": "MIT",
56
+ "homepage": "https://github.com/aoneahsan/windowsweep",
57
+ "repository": {
58
+ "type": "git",
59
+ "url": "git+https://github.com/aoneahsan/windowsweep.git"
60
+ },
61
+ "bugs": {
62
+ "url": "https://github.com/aoneahsan/windowsweep/issues",
63
+ "email": "aoneahsan@gmail.com"
64
+ },
65
+ "funding": {
66
+ "type": "individual",
67
+ "url": "https://aoneahsan.com/payment?project-id=windowsweep&project-identifier=windowsweep"
68
+ },
69
+ "publishConfig": {
70
+ "access": "public"
71
+ },
72
+ "preferGlobal": true
73
+ }
@@ -0,0 +1,6 @@
1
+ @echo off
2
+ rem windowsweep - launcher for users without Node. Same engine, same flags: windowsweep.cmd --help
3
+ setlocal
4
+ if not defined WINDOWSWEEP_LAUNCHER set "WINDOWSWEEP_LAUNCHER=cmd"
5
+ "%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -NoLogo -ExecutionPolicy Bypass -File "%~dp0windowsweep.ps1" %*
6
+ exit /b %ERRORLEVEL%
@@ -0,0 +1,292 @@
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
+ TotalFreed = [long]0; TotalEstimated = [long]0; SectionFreed = [long]0
25
+ Refusals = @(); Hints = @(); ExitCode = 0; Finished = $false; AllowOwnData = $false
26
+ }
27
+
28
+ foreach ($lib in @('constants', 'ui', 'log', 'fs', 'safety', 'config', 'scan', 'actions')) {
29
+ . (Join-Path $Script:WS_ROOT "lib\$lib.ps1")
30
+ }
31
+ foreach ($m in (Get-ChildItem -LiteralPath (Join-Path $Script:WS_ROOT 'modules') -Filter '*.ps1' | Sort-Object Name)) {
32
+ . $m.FullName
33
+ }
34
+
35
+ function Show-Usage {
36
+ $v = Get-ToolVersion
37
+ $text = @"
38
+ $Script:WS_NAME v$v - safe, developer-aware Windows cleanup
39
+
40
+ USAGE
41
+ windowsweep [mode] [options]
42
+
43
+ MODES (default = guided walkthrough through every category)
44
+ -w, --walkthrough Guided walkthrough, asks before every step
45
+ -m, --menu Jump-to menu - run one section at a time
46
+ -a, --all Safe batch: sections $($Script:WS_SAFE_BATCH -join ',') (+12,13 when elevated)
47
+ -s, --scan Read-only: every target with its size, nothing deleted
48
+ --only L Run exactly these sections, e.g. --only 1,3,5-7
49
+ --profile NAME dev | minimal | cache-only | system | deep | audit
50
+ --exclude L Drop sections from --all / --profile
51
+ --list Print the section catalogue and exit
52
+ --list-targets Print every path the tool can touch (read-only)
53
+ --self-test Verify syntax, safety guards, dry-run and junction handling
54
+ --reports Reports manager: list / view / export past runs
55
+ --export F [ID] Export a report: F = md | html | both, ID = N | latest | all
56
+ --stats Run history from ~\.windowsweep
57
+ --prune-history N Delete logs + reports older than N days (default 90)
58
+ --feedback How to report a bug (offline; nothing is sent)
59
+ --report-issue Open a pre-filled GitHub issue in your browser (you review it first)
60
+ --debug-bundle Zip the latest log + report for a bug report
61
+ --install-task Weekly Scheduled Task (Sunday 03:00, safe batch) --uninstall-task
62
+ --install-alias Add a 'cleanup' function to your PowerShell profile --uninstall-alias
63
+ --uninstall-data Remove ~\.windowsweep (logs, reports, config) after confirming
64
+ -V, --version Version and author -h, --help This text
65
+
66
+ OPTIONS
67
+ --dry-run Delete nothing; show what would go and how much it frees
68
+ -y, --yes Auto-confirm regenerable-cache steps (never personal files)
69
+ --i-understand-deep Allow deep sections (11 Recycle Bin, 15 hibernation, 16 event logs,
70
+ 20 disk-image compaction) to run unattended with --yes
71
+ --elevate Relaunch elevated (UAC prompt) so admin sections can run
72
+ -d, --days N Idle threshold for caches (default 100). A file goes only when its newest
73
+ timestamp (write, access, creation) is at least N days old
74
+ --temp-days N Idle threshold for %TEMP% (default 3)
75
+ --purge-all Clear cache targets completely instead of pruning idle files
76
+ --developer / --not-developer Override the saved developer answer for this run
77
+ --forget-developer Ask the developer question again
78
+ --scan-roots "P1;P2" Project roots for section 17 --exclude-path P (repeatable)
79
+ --large-file-mb N Minimum size for section 19 (default 100)
80
+ --hiberfil off|reduced|keep What section 15 does --reset-base DISM /ResetBase in 14
81
+ --permanent Sections 18/19 delete instead of using the Recycle Bin
82
+ --logs-dir P / --reports-dir P / --no-report / --cleanup-logs
83
+ --json One-line JSON summary on stdout (everything else on stderr)
84
+ -q, --quiet Less chatter --no-color --ascii (plain glyphs)
85
+
86
+ SAFETY
87
+ Every deletion passes one chokepoint that refuses drive roots, Windows, Program Files, your
88
+ profile root, Documents/Pictures/Desktop, credentials, toolchains and browser/editor state, and
89
+ that never follows a junction or symlink. Personal-file sections are interactive only and use the
90
+ Recycle Bin. --scan and --dry-run change nothing. No network calls, ever.
91
+
92
+ OUTPUT
93
+ Logs: $($Script:WS.LogsDir)
94
+ Reports: $($Script:WS.ReportsDir)
95
+
96
+ $Script:WS_NAME v$v by $Script:WS_AUTHOR <$Script:WS_EMAIL> $Script:WS_WEB
97
+ $Script:WS_REPO - $Script:WS_LICENSE
98
+ "@
99
+ Write-Host $text
100
+ }
101
+
102
+ function Get-NextArg {
103
+ param([string[]] $Argv, [ref] $Index, [string] $Name)
104
+ $Index.Value++
105
+ if ($Index.Value -ge $Argv.Count) { throw "option $Name needs a value" }
106
+ return [string]$Argv[$Index.Value]
107
+ }
108
+
109
+ function Read-Arguments {
110
+ $ws = $Script:WS
111
+ $argv = @($ws.RawArgs)
112
+ $i = 0
113
+ while ($i -lt $argv.Count) {
114
+ $a = [string]$argv[$i]
115
+ $inline = $null
116
+ if ($a -match '^(--[A-Za-z][A-Za-z0-9-]*)=(.*)$') { $a = $Matches[1]; $inline = $Matches[2] }
117
+ switch ($a) {
118
+ { $_ -in '-w', '--walkthrough' } { $ws.Mode = 'walkthrough' }
119
+ { $_ -in '-m', '--menu', '-i', '--interactive' } { $ws.Mode = 'menu' }
120
+ { $_ -in '-a', '--all', '--all-safe' } { $ws.Mode = 'all'; $ws.BatchMode = $true }
121
+ { $_ -in '-s', '--scan' } { $ws.Mode = 'scan' }
122
+ '--only' { $ws.Mode = 'only'; $ws.BatchMode = $true; if ($null -ne $inline) { $ws.OnlyList = $inline } else { $ws.OnlyList = Get-NextArg $argv ([ref]$i) $a } }
123
+ '--profile' { $ws.Mode = 'only'; $ws.BatchMode = $true; if ($null -ne $inline) { $ws.Profile = $inline } else { $ws.Profile = Get-NextArg $argv ([ref]$i) $a } }
124
+ '--exclude' { if ($null -ne $inline) { $ws.Exclude = $inline } else { $ws.Exclude = Get-NextArg $argv ([ref]$i) $a } }
125
+ '--list' { $ws.Mode = 'list' }
126
+ '--list-targets' { $ws.Mode = 'list_targets' }
127
+ '--self-test' { $ws.Mode = 'self_test' }
128
+ '--reports' { $ws.Mode = 'reports' }
129
+ '--export' {
130
+ $ws.Mode = 'export'
131
+ 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] }
132
+ if (($i + 1) -lt $argv.Count -and -not ([string]$argv[$i + 1]).StartsWith('-')) { $i++; $ws.ExportId = [string]$argv[$i] }
133
+ }
134
+ '--stats' { $ws.Mode = 'stats' }
135
+ '--prune-history' {
136
+ $ws.Mode = 'prune_history'
137
+ 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] }
138
+ }
139
+ '--feedback' { $ws.Mode = 'feedback' }
140
+ { $_ -in '--report-issue', '--report-bug' } { $ws.Mode = 'report_issue' }
141
+ '--debug-bundle' { $ws.Mode = 'debug_bundle' }
142
+ '--install-task' { $ws.Mode = 'install_task' }
143
+ '--uninstall-task' { $ws.Mode = 'uninstall_task' }
144
+ '--install-alias' { $ws.Mode = 'install_alias' }
145
+ '--uninstall-alias' { $ws.Mode = 'uninstall_alias' }
146
+ '--uninstall-data' { $ws.Mode = 'uninstall_data' }
147
+ { $_ -in '-V', '--version' } { $ws.Mode = 'version' }
148
+ { $_ -in '-h', '--help', '/?', '-?' } { $ws.Mode = 'help' }
149
+ { $_ -in '-n', '--dry-run' } { $ws.DryRun = $true }
150
+ { $_ -in '-y', '--yes' } { $ws.Yes = $true }
151
+ '--i-understand-deep' { $ws.Deep = $true }
152
+ '--elevate' { $ws.Elevate = $true }
153
+ '--elevated-child' { $ws.ElevatedChild = $true }
154
+ { $_ -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 }
155
+ '--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 }
156
+ '--purge-all' { $ws.PurgeAll = $true }
157
+ '--developer' { $ws.DeveloperFlag = $true }
158
+ { $_ -in '--not-developer', '--no-developer' } { $ws.DeveloperFlag = $false }
159
+ '--forget-developer' { $ws.ForgetDeveloper = $true }
160
+ '--scan-roots' { $v = $inline; if ($null -eq $v) { $v = Get-NextArg $argv ([ref]$i) $a }; $ws.ScanRoots = @($v -split ';' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) }
161
+ '--exclude-path' { $v = $inline; if ($null -eq $v) { $v = Get-NextArg $argv ([ref]$i) $a }; $ws.ExcludePaths += $v }
162
+ '--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 }
163
+ '--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 }
164
+ '--reset-base' { $ws.ResetBase = $true }
165
+ '--permanent' { $ws.Permanent = $true }
166
+ '--logs-dir' { $v = $inline; if ($null -eq $v) { $v = Get-NextArg $argv ([ref]$i) $a }; $ws.LogsDir = $v }
167
+ '--reports-dir' { $v = $inline; if ($null -eq $v) { $v = Get-NextArg $argv ([ref]$i) $a }; $ws.ReportsDir = $v }
168
+ '--no-report' { $ws.NoReport = $true }
169
+ '--cleanup-logs' { $ws.CleanupLogs = $true }
170
+ '--json' { $ws.JsonMode = $true; $ws.Quiet = $true }
171
+ { $_ -in '-q', '--quiet' } { $ws.Quiet = $true }
172
+ '--no-color' { $ws.NoColor = $true }
173
+ '--ascii' { $ws.Ascii = $true }
174
+ '--pwsh' { }
175
+ default { throw "unknown argument: $a" }
176
+ }
177
+ $i++
178
+ }
179
+ if ($ws.Profile -and -not $Script:WS_PROFILES.ContainsKey($ws.Profile)) {
180
+ throw "unknown profile '$($ws.Profile)' (known: $($Script:WS_PROFILES.Keys -join ', '))"
181
+ }
182
+ }
183
+
184
+ function Initialize-Paths {
185
+ $ws = $Script:WS
186
+ $ws.Stamp = (Get-Date -Format 'yyyy-MM-dd_HHmmss') + '-' + $PID
187
+ $dataHome = $env:WINDOWSWEEP_HOME
188
+ if ([string]::IsNullOrWhiteSpace($dataHome)) { $dataHome = Join-Path $env:USERPROFILE ".$Script:WS_NAME" }
189
+ $ws.Home = Get-FullPath $dataHome
190
+ if (-not $ws.LogsDir) { $ws.LogsDir = $env:WINDOWSWEEP_LOG_DIR }
191
+ if (-not $ws.LogsDir) { $ws.LogsDir = Join-Path $ws.Home 'logs' }
192
+ if (-not $ws.ReportsDir) { $ws.ReportsDir = $env:WINDOWSWEEP_REPORTS_DIR }
193
+ if (-not $ws.ReportsDir) { $ws.ReportsDir = Join-Path $ws.Home 'reports' }
194
+ $ws.LogsDir = Get-FullPath $ws.LogsDir
195
+ $ws.ReportsDir = Get-FullPath $ws.ReportsDir
196
+ $ws.FeedbackDir = Join-Path $ws.Home 'feedback'
197
+ $ws.ConfigPath = Join-Path $ws.Home 'config.json'
198
+ $ws.Launcher = $env:WINDOWSWEEP_LAUNCHER
199
+ if (-not $ws.Launcher) { $ws.Launcher = 'powershell' }
200
+ # Well-known roots used by every module. LL = LocalLow, SR = SystemRoot, PD = ProgramData, SD = system drive.
201
+ $Script:P = @{
202
+ U = (Get-FullPath $env:USERPROFILE); L = (Get-FullPath $env:LOCALAPPDATA); A = (Get-FullPath $env:APPDATA)
203
+ LL = (Get-FullPath (Join-Path (Split-Path -Parent $env:LOCALAPPDATA) 'LocalLow')); SR = (Get-FullPath $env:SystemRoot)
204
+ PD = (Get-FullPath $env:ProgramData); SD = $env:SystemDrive; TEMP = (Get-FullPath $env:TEMP); TMP = (Get-FullPath $env:TMP)
205
+ PF = (Get-FullPath $env:ProgramFiles); PF86 = (Get-FullPath ${env:ProgramFiles(x86)})
206
+ }
207
+ $ws.Npx = ($env:WINDOWSWEEP_NPX -eq '1')
208
+ $ws.IsAdmin = Test-IsAdmin
209
+ try { $ws.OsCaption = (Get-CimInstance Win32_OperatingSystem -ErrorAction Stop).Caption } catch { $ws.OsCaption = 'Windows' }
210
+ }
211
+
212
+ function Initialize-Settings {
213
+ <# .SYNOPSIS Merge config defaults under explicit flags. #>
214
+ $ws = $Script:WS
215
+ $cfg = Import-Config
216
+ if ($null -eq $ws.Days) { $ws.Days = [int]$cfg.days }
217
+ if ($null -eq $ws.TempDays) { $ws.TempDays = [int]$cfg.tempDays }
218
+ if ($null -eq $ws.LargeFileMb) { $ws.LargeFileMb = [int]$cfg.largeFileMb }
219
+ if ($ws.ScanRoots.Count -eq 0 -and $cfg.scanRoots) { $ws.ScanRoots = @($cfg.scanRoots) }
220
+ if ($cfg.excludePaths) { $ws.ExcludePaths = @($ws.ExcludePaths) + @($cfg.excludePaths) }
221
+ }
222
+
223
+ function Invoke-Main {
224
+ $ws = $Script:WS
225
+ switch ($ws.Mode) {
226
+ 'help' { Show-Usage; return }
227
+ 'version' { Show-Version; return }
228
+ 'list' { Show-SectionList; return }
229
+ 'list_targets' { Show-TargetList; return }
230
+ 'self_test' { $ws.ExitCode = Invoke-SelfTest; return }
231
+ 'scan' { Invoke-ScanMode; return }
232
+ 'all' { Invoke-BatchMode; return }
233
+ 'only' { Invoke-BatchMode; return }
234
+ 'walkthrough' { Invoke-Walkthrough; return }
235
+ 'menu' { Invoke-Menu; return }
236
+ 'reports' { Invoke-ReportsManager; return }
237
+ 'export' { $ws.ExitCode = Export-Reports -Format $ws.ExportFmt -Id $ws.ExportId; return }
238
+ 'stats' { Show-Stats; return }
239
+ 'prune_history' { Remove-OldHistory -Days $ws.PruneDays; return }
240
+ 'feedback' { Show-Feedback; return }
241
+ 'report_issue' { Invoke-ReportIssue; return }
242
+ 'debug_bundle' { New-DebugBundle; return }
243
+ 'install_task' { Install-WeeklyTask; return }
244
+ 'uninstall_task' { Uninstall-WeeklyTask; return }
245
+ 'install_alias' { Install-ProfileAlias; return }
246
+ 'uninstall_alias' { Uninstall-ProfileAlias; return }
247
+ 'uninstall_data' { Remove-ToolData; return }
248
+ default { Write-Err "unknown mode: $($ws.Mode)"; $ws.ExitCode = $Script:WS_EXIT_USAGE }
249
+ }
250
+ }
251
+
252
+ # ---------------------------------------------------------------------------------------------
253
+ # Bootstrap
254
+ # ---------------------------------------------------------------------------------------------
255
+ try {
256
+ Read-Arguments
257
+ } catch {
258
+ [Console]::Error.WriteLine("windowsweep: $($_.Exception.Message)")
259
+ [Console]::Error.WriteLine("try: windowsweep --help")
260
+ exit 2
261
+ }
262
+ Initialize-Paths
263
+ Initialize-Ui
264
+ Initialize-Safety
265
+ $quietModes = @('help', 'version', 'list')
266
+ if ($Script:WS.Mode -notin $quietModes) { Initialize-Log }
267
+ Initialize-Settings
268
+ Initialize-Report
269
+ Initialize-CrashTrap
270
+
271
+ if ($Script:WS.Elevate -and -not $Script:WS.IsAdmin) {
272
+ $code = Invoke-Elevated
273
+ Restore-Ui
274
+ exit $code
275
+ }
276
+ if ($Script:WS.Elevate -and $Script:WS.IsAdmin) { Write-Note 'already elevated; --elevate is a no-op' }
277
+
278
+ if ($Script:WS.Mode -in @('walkthrough', 'menu', 'all', 'only')) { Resolve-DeveloperMode }
279
+
280
+ try {
281
+ Invoke-Main
282
+ $Script:WS.Finished = $true
283
+ } catch {
284
+ Invoke-CrashBundle -ErrorRecord $_
285
+ $Script:WS.ExitCode = $Script:WS_EXIT_FAIL
286
+ } finally {
287
+ Restore-Ui
288
+ if ($Script:WS.CleanupLogs -and $Script:WS.LogFile -and (Test-Path -LiteralPath $Script:WS.LogFile)) {
289
+ try { Remove-Item -LiteralPath $Script:WS.LogFile -Force -ErrorAction SilentlyContinue } catch { $null = $_ }
290
+ }
291
+ }
292
+ exit $Script:WS.ExitCode