windowsweep 1.1.0 → 1.3.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.
Files changed (43) hide show
  1. package/AI-INTEGRATION-GUIDE.md +16 -12
  2. package/CHANGELOG.md +82 -0
  3. package/LICENSE +21 -21
  4. package/README.md +104 -69
  5. package/VERSION +1 -1
  6. package/lib/actions.ps1 +212 -212
  7. package/lib/config.ps1 +108 -99
  8. package/lib/constants.ps1 +103 -93
  9. package/lib/external.ps1 +87 -0
  10. package/lib/fs.ps1 +189 -180
  11. package/lib/log.ps1 +140 -140
  12. package/lib/safety.ps1 +467 -493
  13. package/lib/scan.ps1 +109 -100
  14. package/lib/ui.ps1 +275 -275
  15. package/modules/android_avd.ps1 +36 -36
  16. package/modules/app_caches.ps1 +58 -58
  17. package/modules/browsers.ps1 +33 -33
  18. package/modules/build_tools.ps1 +35 -35
  19. package/modules/crash_trap.ps1 +89 -89
  20. package/modules/disk_usage.ps1 +49 -49
  21. package/modules/docker.ps1 +93 -93
  22. package/modules/editors.ps1 +150 -150
  23. package/modules/globals_audit.ps1 +236 -236
  24. package/modules/health.ps1 +101 -101
  25. package/modules/installed_programs.ps1 +118 -118
  26. package/modules/menu.ps1 +63 -63
  27. package/modules/notify.ps1 +60 -60
  28. package/modules/orphaned_appdata.ps1 +189 -189
  29. package/modules/personal.ps1 +126 -126
  30. package/modules/pkg_managers.ps1 +73 -73
  31. package/modules/projects.ps1 +160 -160
  32. package/modules/release_helpers.ps1 +438 -438
  33. package/modules/reports.ps1 +212 -212
  34. package/modules/runner.ps1 +246 -239
  35. package/modules/self_test_contract.ps1 +195 -0
  36. package/modules/self_test_extra.ps1 +366 -360
  37. package/modules/startup_audit.ps1 +126 -126
  38. package/modules/system_admin.ps1 +220 -220
  39. package/modules/test_runners.ps1 +24 -24
  40. package/modules/walkthrough.ps1 +80 -79
  41. package/modules/windows_user.ps1 +118 -118
  42. package/package.json +3 -3
  43. package/windowsweep.ps1 +322 -316
package/lib/scan.ps1 CHANGED
@@ -1,100 +1,109 @@
1
- # scan.ps1 - read-only scanners: the target table, --list-targets and the pre-scan.
2
- # Every module contributes its targets through a Get-TargetsNN function; nothing here deletes.
3
-
4
- function New-Target {
5
- <# .SYNOPSIS Describe one path the tool can touch and how it is treated.
6
- Kind: dir | file | glob (expands to dirs) | fileglob (expands to files) | cmd (informational)
7
- Mode: prune (files idle >= Days) | clear (everything inside) | clear-old (entries idle >= Days) |
8
- units (each child is a version/unit; idle gate, -KeepNewest per group) | file (remove the file)
9
- Dev: $true = a developer cache: pruned by the idle gate in developer mode, cleared otherwise.
10
- Guard: process names; the target is skipped while any of them runs. #>
11
- param([int] $Section, [string] $Label, [string] $Path, [string] $Kind = 'dir', [string] $Mode = 'prune',
12
- [int] $Days = 0, [bool] $Dev = $false, [string[]] $Guard = @(), [bool] $KeepNewest = $false,
13
- [scriptblock] $GroupBy = $null, [string[]] $ExcludeNames = @(), [string] $Note = '')
14
- return [pscustomobject]@{ Section = $Section; Label = $Label; Path = $Path; Kind = $Kind; Mode = $Mode; Days = $Days
15
- Dev = $Dev; Guard = $Guard; KeepNewest = $KeepNewest; GroupBy = $GroupBy; ExcludeNames = $ExcludeNames; Note = $Note }
16
- }
17
-
18
- function Get-AllTargets {
19
- <# .SYNOPSIS Collect every declared target from the loaded modules: one Get-TargetsNN per catalogue row.
20
- Driven by WS_SECTIONS, never a literal range, so a new section is reachable the moment it is declared. #>
21
- $all = @()
22
- foreach ($s in $Script:WS_SECTIONS) {
23
- $fn = 'Get-Targets{0:00}' -f $s.Id
24
- if (Get-Command $fn -ErrorAction SilentlyContinue) { $all += @(& $fn) }
25
- }
26
- return $all
27
- }
28
-
29
- function Resolve-TargetPaths {
30
- <# .SYNOPSIS Expand a glob target into concrete existing paths; a plain path returns itself when present. #>
31
- param([pscustomobject] $Target)
32
- if ($Target.Kind -eq 'cmd') { return @() }
33
- if ($Target.Kind -in 'chromium', 'firefox', 'electron', 'editor') { return @(Get-TargetCachePaths $Target) }
34
- if ($Target.Kind -eq 'glob') {
35
- try { return @(Get-Item -Path $Target.Path -Force -ErrorAction SilentlyContinue | Where-Object { $_ -is [IO.DirectoryInfo] } | ForEach-Object { $_.FullName }) } catch { return @() }
36
- }
37
- if ($Target.Kind -eq 'fileglob') {
38
- try { return @(Get-Item -Path $Target.Path -Force -ErrorAction SilentlyContinue | Where-Object { $_ -is [IO.FileInfo] } | ForEach-Object { $_.FullName }) } catch { return @() }
39
- }
40
- if (Test-PathPresent $Target.Path) { return @((Get-FullPath $Target.Path)) }
41
- return @()
42
- }
43
-
44
- function Show-ScanTable {
45
- <# .SYNOPSIS Read-only table of every target with its current size. Returns total bytes found. #>
46
- param([int[]] $Sections = @())
47
- $total = [long]0
48
- $targets = @(Get-AllTargets)
49
- if ($Sections.Count -gt 0) { $targets = @($targets | Where-Object { $Sections -contains $_.Section }) }
50
- $bySection = $targets | Group-Object Section | Sort-Object { [int]$_.Name }
51
- foreach ($g in $bySection) {
52
- $sec = Get-Section ([int]$g.Name)
53
- Write-Section ("[{0:00}] {1}" -f $sec.Id, $sec.Title)
54
- Write-UiLine (" {0,-46} {1,10} {2}" -f 'ITEM', 'SIZE', 'PATH') 'White'
55
- $secTotal = [long]0
56
- foreach ($t in $g.Group) {
57
- if ($t.Kind -eq 'cmd') { Write-UiLine (" {0,-46} {1,10} {2}" -f $t.Label, '(cmd)', $t.Path) 'DarkGray'; continue }
58
- $paths = @(Resolve-TargetPaths $t)
59
- if ($paths.Count -eq 0) { Write-UiLine (" {0,-46} {1,10} {2}" -f $t.Label, 'absent', $t.Path) 'DarkGray'; continue }
60
- $bytes = [long]0
61
- foreach ($p in $paths) {
62
- # Sized once per path: a second Get-DirectoryBytes pass for --json would walk every target twice.
63
- $b = [long](Get-DirectoryBytes $p)
64
- $bytes += $b
65
- if ($Script:WS.JsonMode) { $Script:WS.ScanTargets += [ordered]@{ section = $t.Section; label = $t.Label; path = $p; bytes = $b } }
66
- }
67
- $secTotal += $bytes
68
- $shown = $t.Path
69
- if ($paths.Count -gt 1) { $shown = "$($t.Path) ($($paths.Count) matches)" }
70
- Write-UiLine (" {0,-46} {1,10} {2}" -f $t.Label, (Format-Bytes $bytes), $shown) 'Gray'
71
- }
72
- Write-UiLine (" {0,-46} {1,10}" -f 'section total on disk', (Format-Bytes $secTotal)) 'Cyan'
73
- $total += $secTotal
74
- }
75
- Write-UiLine '' 'Gray'
76
- Write-Info ("Currently on disk across all listed targets: " + (Format-Bytes $total))
77
- Write-Note 'These are sizes on disk, not what a run would delete: the idle gate keeps recently used files, and running apps are skipped.'
78
- return $total
79
- }
80
-
81
- function Show-TargetList {
82
- <# .SYNOPSIS --list-targets: every path the tool can touch, plus the protected lists. #>
83
- Write-Box 'Cleanup targets' 'Every path this tool can touch, grouped by section (read-only)'
84
- $targets = @(Get-AllTargets)
85
- foreach ($g in ($targets | Group-Object Section | Sort-Object { [int]$_.Name })) {
86
- $sec = Get-Section ([int]$g.Name)
87
- Write-Section ("[{0:00}] {1}" -f $sec.Id, $sec.Title)
88
- foreach ($t in $g.Group) {
89
- $note = ''
90
- if ($t.Note) { $note = " - $($t.Note)" }
91
- Write-UiLine (" {0,-44} {1}{2}" -f $t.Label, $t.Path, $note) 'Gray'
92
- }
93
- }
94
- Write-Section 'PROTECTED - never deleted, no flag bypasses this'
95
- foreach ($s in $Script:WS_PROTECT.Subtrees) { Write-UiLine " $($Script:WS.Glyph.bullet) $s" 'DarkGray' }
96
- Write-UiLine " $($Script:WS.Glyph.bullet) every drive root, Windows, System32, Program Files, ProgramData, the user profile root and AppData roots" 'DarkGray'
97
- Write-UiLine " $($Script:WS.Glyph.bullet) browser profile data (Local Storage, IndexedDB, cookies, history, bookmarks, extensions, PWA CacheStorage)" 'DarkGray'
98
- Write-UiLine " $($Script:WS.Glyph.bullet) editor user data (settings, globalStorage, local History); UWP LocalState; toolchains (nvm, npm globals, corepack, ...)" 'DarkGray'
99
- Write-UiLine " $($Script:WS.Glyph.bullet) NTUSER.DAT, UsrClass.dat, hiberfil/pagefile/swapfile (only powercfg touches hiberfil), Prefetch, Windows\Installer, WinSxS (DISM only)" 'DarkGray'
100
- }
1
+ # scan.ps1 - read-only scanners: the target table, --list-targets and the pre-scan.
2
+ # Every module contributes its targets through a Get-TargetsNN function; nothing here deletes.
3
+
4
+ function New-Target {
5
+ <# .SYNOPSIS Describe one path the tool can touch and how it is treated.
6
+ Kind: dir | file | glob (expands to dirs) | fileglob (expands to files) | cmd (informational)
7
+ Mode: prune (files idle >= Days) | clear (everything inside) | clear-old (entries idle >= Days) |
8
+ units (each child is a version/unit; idle gate, -KeepNewest per group) | file (remove the file)
9
+ Dev: $true = a developer cache: pruned by the idle gate in developer mode, cleared otherwise.
10
+ Guard: process names; the target is skipped while any of them runs. #>
11
+ param([int] $Section, [string] $Label, [string] $Path, [string] $Kind = 'dir', [string] $Mode = 'prune',
12
+ [int] $Days = 0, [bool] $Dev = $false, [string[]] $Guard = @(), [bool] $KeepNewest = $false,
13
+ [scriptblock] $GroupBy = $null, [string[]] $ExcludeNames = @(), [string] $Note = '')
14
+ return [pscustomobject]@{ Section = $Section; Label = $Label; Path = $Path; Kind = $Kind; Mode = $Mode; Days = $Days
15
+ Dev = $Dev; Guard = $Guard; KeepNewest = $KeepNewest; GroupBy = $GroupBy; ExcludeNames = $ExcludeNames; Note = $Note }
16
+ }
17
+
18
+ function Get-AllTargets {
19
+ <# .SYNOPSIS Collect every declared target from the loaded modules: one Get-TargetsNN per catalogue row.
20
+ Driven by WS_SECTIONS, never a literal range, so a new section is reachable the moment it is declared. #>
21
+ $all = @()
22
+ foreach ($s in $Script:WS_SECTIONS) {
23
+ $fn = 'Get-Targets{0:00}' -f $s.Id
24
+ if (Get-Command $fn -ErrorAction SilentlyContinue) { $all += @(& $fn) }
25
+ }
26
+ return $all
27
+ }
28
+
29
+ function Resolve-TargetPaths {
30
+ <# .SYNOPSIS Expand a glob target into concrete existing paths; a plain path returns itself when present. #>
31
+ param([pscustomobject] $Target)
32
+ if ($Target.Kind -eq 'cmd') { return @() }
33
+ if ($Target.Kind -in 'chromium', 'firefox', 'electron', 'editor') { return @(Get-TargetCachePaths $Target) }
34
+ if ($Target.Kind -eq 'glob') {
35
+ try { return @(Get-Item -Path $Target.Path -Force -ErrorAction SilentlyContinue | Where-Object { $_ -is [IO.DirectoryInfo] } | ForEach-Object { $_.FullName }) } catch { return @() }
36
+ }
37
+ if ($Target.Kind -eq 'fileglob') {
38
+ try { return @(Get-Item -Path $Target.Path -Force -ErrorAction SilentlyContinue | Where-Object { $_ -is [IO.FileInfo] } | ForEach-Object { $_.FullName }) } catch { return @() }
39
+ }
40
+ if (Test-PathPresent $Target.Path) { return @((Get-FullPath $Target.Path)) }
41
+ return @()
42
+ }
43
+
44
+ function Show-ScanTable {
45
+ <# .SYNOPSIS Read-only table of every target with its current size. Returns total bytes found. #>
46
+ param([int[]] $Sections = @())
47
+ $total = [long]0
48
+ $targets = @(Get-AllTargets)
49
+ if ($Sections.Count -gt 0) { $targets = @($targets | Where-Object { $Sections -contains $_.Section }) }
50
+ $bySection = $targets | Group-Object Section | Sort-Object { [int]$_.Name }
51
+ foreach ($g in $bySection) {
52
+ $sec = Get-Section ([int]$g.Name)
53
+ Write-Section ("[{0:00}] {1}" -f $sec.Id, $sec.Title)
54
+ Write-UiLine (" {0,-46} {1,10} {2}" -f 'ITEM', 'SIZE', 'PATH') 'White'
55
+ $secTotal = [long]0
56
+ foreach ($t in $g.Group) {
57
+ if ($t.Kind -eq 'cmd') { Write-UiLine (" {0,-46} {1,10} {2}" -f $t.Label, '(cmd)', $t.Path) 'DarkGray'; continue }
58
+ $paths = @(Resolve-TargetPaths $t)
59
+ if ($paths.Count -eq 0) { Write-UiLine (" {0,-46} {1,10} {2}" -f $t.Label, 'absent', $t.Path) 'DarkGray'; continue }
60
+ $bytes = [long]0
61
+ foreach ($p in $paths) {
62
+ # Sized once per path: a second pass for --json would walk every target twice. Under --json the walk
63
+ # is Get-DirectoryStats, which returns the newest timestamp out of the SAME enumeration that produces
64
+ # the byte count - so newest_write_utc costs no extra walk. Outside --json the faster robocopy path in
65
+ # Get-DirectoryBytes is kept, because a human --scan has no use for the timestamp.
66
+ $newest = $null
67
+ if ($Script:WS.JsonMode) {
68
+ $st = Get-DirectoryStats $p
69
+ $b = [long]$st.Bytes
70
+ $newest = Format-Utc8601 $st.Newest
71
+ } else {
72
+ $b = [long](Get-DirectoryBytes $p)
73
+ }
74
+ $bytes += $b
75
+ if ($Script:WS.JsonMode) { $Script:WS.ScanTargets += [ordered]@{ section = $t.Section; label = $t.Label; path = $p; bytes = $b; newest_write_utc = $newest } }
76
+ }
77
+ $secTotal += $bytes
78
+ $shown = $t.Path
79
+ if ($paths.Count -gt 1) { $shown = "$($t.Path) ($($paths.Count) matches)" }
80
+ Write-UiLine (" {0,-46} {1,10} {2}" -f $t.Label, (Format-Bytes $bytes), $shown) 'Gray'
81
+ }
82
+ Write-UiLine (" {0,-46} {1,10}" -f 'section total on disk', (Format-Bytes $secTotal)) 'Cyan'
83
+ $total += $secTotal
84
+ }
85
+ Write-UiLine '' 'Gray'
86
+ Write-Info ("Currently on disk across all listed targets: " + (Format-Bytes $total))
87
+ Write-Note 'These are sizes on disk, not what a run would delete: the idle gate keeps recently used files, and running apps are skipped.'
88
+ return $total
89
+ }
90
+
91
+ function Show-TargetList {
92
+ <# .SYNOPSIS --list-targets: every path the tool can touch, plus the protected lists. #>
93
+ Write-Box 'Cleanup targets' 'Every path this tool can touch, grouped by section (read-only)'
94
+ $targets = @(Get-AllTargets)
95
+ foreach ($g in ($targets | Group-Object Section | Sort-Object { [int]$_.Name })) {
96
+ $sec = Get-Section ([int]$g.Name)
97
+ Write-Section ("[{0:00}] {1}" -f $sec.Id, $sec.Title)
98
+ foreach ($t in $g.Group) {
99
+ $note = ''
100
+ if ($t.Note) { $note = " - $($t.Note)" }
101
+ Write-UiLine (" {0,-44} {1}{2}" -f $t.Label, $t.Path, $note) 'Gray'
102
+ }
103
+ }
104
+ Write-Section 'PROTECTED - never deleted, no flag bypasses this'
105
+ foreach ($s in $Script:WS_PROTECT.Subtrees) { Write-UiLine " $($Script:WS.Glyph.bullet) $s" 'DarkGray' }
106
+ # ONE list, read here and by --list --json (Get-CatalogueJson). Printing one wording and publishing another
107
+ # is exactly the drift a machine-readable copy of a human list invites, so there is only ever one copy.
108
+ foreach ($c in $Script:WS_PROTECT_CATEGORIES) { Write-UiLine " $($Script:WS.Glyph.bullet) $c" 'DarkGray' }
109
+ }