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/fs.ps1 CHANGED
@@ -1,180 +1,189 @@
1
- # fs.ps1 - long-path-safe filesystem helpers. The walker never descends into reparse points
2
- # (junctions / symlinks), which .NET's own recursive enumeration does.
3
-
4
- function Get-LongPath {
5
- <# .SYNOPSIS Prefix an absolute path with \\?\ so .NET accepts paths longer than 260 chars. #>
6
- param([string] $Path)
7
- if ($Path.StartsWith('\\?\')) { return $Path }
8
- if ($Path.StartsWith('\\')) { return ('\\?\UNC\' + $Path.Substring(2)) }
9
- return ('\\?\' + $Path)
10
- }
11
-
12
- function Get-FullPath {
13
- <# .SYNOPSIS Expand env vars, resolve . and .., strip the long prefix and trailing separators. $null if unresolvable. #>
14
- param([string] $Path)
15
- if ([string]::IsNullOrWhiteSpace($Path)) { return $null }
16
- $p = [Environment]::ExpandEnvironmentVariables($Path.Trim())
17
- if ($p.StartsWith('\\?\UNC\')) { $p = '\\' + $p.Substring(8) } elseif ($p.StartsWith('\\?\')) { $p = $p.Substring(4) }
18
- try { $p = [IO.Path]::GetFullPath($p) } catch { return $null }
19
- if ($p.Length -gt 3) { $p = $p.TrimEnd('\') }
20
- return $p
21
- }
22
-
23
- function Get-ItemInfo {
24
- <# .SYNOPSIS FileInfo or DirectoryInfo for a path (long-path aware), or $null when absent. #>
25
- param([string] $Path)
26
- $lp = Get-LongPath $Path
27
- try {
28
- if ([IO.Directory]::Exists($lp)) { return (New-Object IO.DirectoryInfo($lp)) }
29
- if ([IO.File]::Exists($lp)) { return (New-Object IO.FileInfo($lp)) }
30
- } catch { $null = $_ }
31
- return $null
32
- }
33
-
34
- function Test-PathPresent {
35
- param([string] $Path)
36
- $lp = Get-LongPath $Path
37
- try { return ([IO.Directory]::Exists($lp) -or [IO.File]::Exists($lp)) } catch { return $false }
38
- }
39
-
40
- function Test-DirPresent {
41
- param([string] $Path)
42
- try { return [IO.Directory]::Exists((Get-LongPath $Path)) } catch { return $false }
43
- }
44
-
45
- function Test-ReparsePoint {
46
- <# .SYNOPSIS True when the FileSystemInfo is a junction or symbolic link. #>
47
- param([System.IO.FileSystemInfo] $Info)
48
- if ($null -eq $Info) { return $false }
49
- return (($Info.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0)
50
- }
51
-
52
- function Get-ChildEntries {
53
- <# .SYNOPSIS Non-recursive children of a directory; empty on access errors. #>
54
- param([string] $Path)
55
- $out = @()
56
- try {
57
- $di = New-Object IO.DirectoryInfo((Get-LongPath $Path))
58
- foreach ($e in $di.EnumerateFileSystemInfos()) { $out += $e }
59
- } catch { $null = $_ }
60
- return $out
61
- }
62
-
63
- function Get-NewestTimestampUtc {
64
- <# .SYNOPSIS The most recent of write / access / creation time - the safe "last touched" estimate when access tracking is off. #>
65
- param([System.IO.FileSystemInfo] $Info)
66
- $t = $Info.LastWriteTimeUtc
67
- try { if ($Info.LastAccessTimeUtc -gt $t) { $t = $Info.LastAccessTimeUtc } } catch { $null = $_ }
68
- try { if ($Info.CreationTimeUtc -gt $t) { $t = $Info.CreationTimeUtc } } catch { $null = $_ }
69
- return $t
70
- }
71
-
72
- function Get-DirectoryStats {
73
- <# .SYNOPSIS Walk a tree without following reparse points. Returns Bytes, Files, Dirs, Links, Newest (UTC), Errors. #>
74
- param([string] $Path)
75
- $bytes = [long]0; $files = 0; $dirs = 0; $links = 0; $errors = 0
76
- $newest = [datetime]::MinValue
77
- $root = Get-ItemInfo $Path
78
- if ($null -eq $root) { return [pscustomobject]@{ Bytes = 0; Files = 0; Dirs = 0; Links = 0; Newest = $newest; Errors = 0; Exists = $false } }
79
- if ($root -is [IO.FileInfo]) {
80
- return [pscustomobject]@{ Bytes = [long]$root.Length; Files = 1; Dirs = 0; Links = 0; Newest = (Get-NewestTimestampUtc $root); Errors = 0; Exists = $true }
81
- }
82
- if (Test-ReparsePoint $root) {
83
- return [pscustomobject]@{ Bytes = 0; Files = 0; Dirs = 0; Links = 1; Newest = (Get-NewestTimestampUtc $root); Errors = 0; Exists = $true }
84
- }
85
- $stack = New-Object System.Collections.Generic.Stack[System.IO.DirectoryInfo]
86
- $stack.Push($root)
87
- while ($stack.Count -gt 0) {
88
- $dir = $stack.Pop()
89
- $entries = $null
90
- try { $entries = $dir.EnumerateFileSystemInfos() } catch { $errors++; continue }
91
- try {
92
- foreach ($e in $entries) {
93
- if (($e.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { $links++; continue }
94
- if (($e.Attributes -band [IO.FileAttributes]::Directory) -ne 0) { $dirs++; $stack.Push([IO.DirectoryInfo]$e); continue }
95
- $files++
96
- $bytes += [long]$e.Length
97
- $t = $e.LastWriteTimeUtc
98
- if ($e.LastAccessTimeUtc -gt $t) { $t = $e.LastAccessTimeUtc }
99
- if ($e.CreationTimeUtc -gt $t) { $t = $e.CreationTimeUtc }
100
- if ($t -gt $newest) { $newest = $t }
101
- }
102
- } catch { $errors++ }
103
- }
104
- if ($newest -eq [datetime]::MinValue) { $newest = Get-NewestTimestampUtc $root }
105
- return [pscustomobject]@{ Bytes = $bytes; Files = $files; Dirs = $dirs; Links = $links; Newest = $newest; Errors = $errors; Exists = $true }
106
- }
107
-
108
- function Get-DirectoryBytes {
109
- <# .SYNOPSIS Total bytes under a path (0 when absent). Uses robocopy /L when available - it is native and fast. #>
110
- param([string] $Path)
111
- if (-not (Test-PathPresent $Path)) { return [long]0 }
112
- $info = Get-ItemInfo $Path
113
- if ($info -is [IO.FileInfo]) { return [long]$info.Length }
114
- if (Test-ReparsePoint $info) { return [long]0 }
115
- if (Get-Command robocopy.exe -ErrorAction SilentlyContinue) {
116
- try {
117
- $sink = Join-Path $env:TEMP ('windowsweep-null-' + [guid]::NewGuid().ToString('N'))
118
- $lines = & robocopy.exe $Path $sink /L /S /NJH /BYTES /NFL /NDL /NC /NS /XJ /R:0 /W:0 2>$null
119
- foreach ($line in $lines) {
120
- if ($line -match '^\s*Bytes\s*:\s*(\d+)') { return [long]$Matches[1] }
121
- }
122
- } catch { $null = $_ }
123
- }
124
- return [long](Get-DirectoryStats $Path).Bytes
125
- }
126
-
127
- function Get-IdleDays {
128
- <# .SYNOPSIS Days since the path (or anything inside it) was last touched. 999999 when absent. #>
129
- param([string] $Path)
130
- $info = Get-ItemInfo $Path
131
- if ($null -eq $info) { return 999999 }
132
- if ($info -is [IO.FileInfo] -or (Test-ReparsePoint $info)) {
133
- return [int][math]::Floor(([datetime]::UtcNow - (Get-NewestTimestampUtc $info)).TotalDays)
134
- }
135
- $stats = Get-DirectoryStats $Path
136
- return [int][math]::Floor(([datetime]::UtcNow - $stats.Newest).TotalDays)
137
- }
138
-
139
- function Get-StaleFiles {
140
- <# .SYNOPSIS Files under Root whose newest timestamp is at least Days old; never follows reparse points.
141
- Returns objects with Path, Bytes. Also returns the list of directories visited (for the empty sweep). #>
142
- param([string] $Root, [int] $Days)
143
- $cutoff = [datetime]::UtcNow.AddDays(-1 * $Days)
144
- $stale = New-Object System.Collections.Generic.List[object]
145
- $visited = New-Object System.Collections.Generic.List[string]
146
- $rootInfo = Get-ItemInfo $Root
147
- if ($null -eq $rootInfo -or -not ($rootInfo -is [IO.DirectoryInfo]) -or (Test-ReparsePoint $rootInfo)) {
148
- return [pscustomobject]@{ Files = $stale; Dirs = $visited }
149
- }
150
- $stack = New-Object System.Collections.Generic.Stack[System.IO.DirectoryInfo]
151
- $stack.Push($rootInfo)
152
- while ($stack.Count -gt 0) {
153
- $dir = $stack.Pop()
154
- $entries = $null
155
- try { $entries = $dir.EnumerateFileSystemInfos() } catch { continue }
156
- try {
157
- foreach ($e in $entries) {
158
- if (($e.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { continue }
159
- if (($e.Attributes -band [IO.FileAttributes]::Directory) -ne 0) {
160
- $visited.Add($e.FullName)
161
- $stack.Push([IO.DirectoryInfo]$e)
162
- continue
163
- }
164
- $t = $e.LastWriteTimeUtc
165
- if ($e.LastAccessTimeUtc -gt $t) { $t = $e.LastAccessTimeUtc }
166
- if ($e.CreationTimeUtc -gt $t) { $t = $e.CreationTimeUtc }
167
- if ($t -le $cutoff) { $stale.Add([pscustomobject]@{ Path = $e.FullName; Bytes = [long]$e.Length }) }
168
- }
169
- } catch { $null = $_ }
170
- }
171
- return [pscustomobject]@{ Files = $stale; Dirs = $visited }
172
- }
173
-
174
- function Remove-LongPrefix {
175
- <# .SYNOPSIS Strip \\?\ for display. #>
176
- param([string] $Path)
177
- if ($Path.StartsWith('\\?\UNC\')) { return ('\\' + $Path.Substring(8)) }
178
- if ($Path.StartsWith('\\?\')) { return $Path.Substring(4) }
179
- return $Path
180
- }
1
+ # fs.ps1 - long-path-safe filesystem helpers. The walker never descends into reparse points
2
+ # (junctions / symlinks), which .NET's own recursive enumeration does.
3
+
4
+ function Get-LongPath {
5
+ <# .SYNOPSIS Prefix an absolute path with \\?\ so .NET accepts paths longer than 260 chars. #>
6
+ param([string] $Path)
7
+ if ($Path.StartsWith('\\?\')) { return $Path }
8
+ if ($Path.StartsWith('\\')) { return ('\\?\UNC\' + $Path.Substring(2)) }
9
+ return ('\\?\' + $Path)
10
+ }
11
+
12
+ function Get-FullPath {
13
+ <# .SYNOPSIS Expand env vars, resolve . and .., strip the long prefix and trailing separators. $null if unresolvable. #>
14
+ param([string] $Path)
15
+ if ([string]::IsNullOrWhiteSpace($Path)) { return $null }
16
+ $p = [Environment]::ExpandEnvironmentVariables($Path.Trim())
17
+ if ($p.StartsWith('\\?\UNC\')) { $p = '\\' + $p.Substring(8) } elseif ($p.StartsWith('\\?\')) { $p = $p.Substring(4) }
18
+ try { $p = [IO.Path]::GetFullPath($p) } catch { return $null }
19
+ if ($p.Length -gt 3) { $p = $p.TrimEnd('\') }
20
+ return $p
21
+ }
22
+
23
+ function Get-ItemInfo {
24
+ <# .SYNOPSIS FileInfo or DirectoryInfo for a path (long-path aware), or $null when absent. #>
25
+ param([string] $Path)
26
+ $lp = Get-LongPath $Path
27
+ try {
28
+ if ([IO.Directory]::Exists($lp)) { return (New-Object IO.DirectoryInfo($lp)) }
29
+ if ([IO.File]::Exists($lp)) { return (New-Object IO.FileInfo($lp)) }
30
+ } catch { $null = $_ }
31
+ return $null
32
+ }
33
+
34
+ function Test-PathPresent {
35
+ param([string] $Path)
36
+ $lp = Get-LongPath $Path
37
+ try { return ([IO.Directory]::Exists($lp) -or [IO.File]::Exists($lp)) } catch { return $false }
38
+ }
39
+
40
+ function Test-DirPresent {
41
+ param([string] $Path)
42
+ try { return [IO.Directory]::Exists((Get-LongPath $Path)) } catch { return $false }
43
+ }
44
+
45
+ function Test-ReparsePoint {
46
+ <# .SYNOPSIS True when the FileSystemInfo is a junction or symbolic link. #>
47
+ param([System.IO.FileSystemInfo] $Info)
48
+ if ($null -eq $Info) { return $false }
49
+ return (($Info.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0)
50
+ }
51
+
52
+ function Get-ChildEntries {
53
+ <# .SYNOPSIS Non-recursive children of a directory; empty on access errors. #>
54
+ param([string] $Path)
55
+ $out = @()
56
+ try {
57
+ $di = New-Object IO.DirectoryInfo((Get-LongPath $Path))
58
+ foreach ($e in $di.EnumerateFileSystemInfos()) { $out += $e }
59
+ } catch { $null = $_ }
60
+ return $out
61
+ }
62
+
63
+ function Get-NewestTimestampUtc {
64
+ <# .SYNOPSIS The most recent of write / access / creation time - the safe "last touched" estimate when access tracking is off. #>
65
+ param([System.IO.FileSystemInfo] $Info)
66
+ $t = $Info.LastWriteTimeUtc
67
+ try { if ($Info.LastAccessTimeUtc -gt $t) { $t = $Info.LastAccessTimeUtc } } catch { $null = $_ }
68
+ try { if ($Info.CreationTimeUtc -gt $t) { $t = $Info.CreationTimeUtc } } catch { $null = $_ }
69
+ return $t
70
+ }
71
+
72
+ function Format-Utc8601 {
73
+ <# .SYNOPSIS ISO 8601 UTC for a timestamp, or $null when nothing was found (the MinValue sentinel).
74
+ Get-DirectoryStats returns MinValue for an empty or unreadable tree, and "0001-01-01" in a contract
75
+ field would read as a real date a caller could sort on. #>
76
+ param([datetime] $When)
77
+ if ($When -le [datetime]::MinValue) { return $null }
78
+ return $When.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
79
+ }
80
+
81
+ function Get-DirectoryStats {
82
+ <# .SYNOPSIS Walk a tree without following reparse points. Returns Bytes, Files, Dirs, Links, Newest (UTC), Errors. #>
83
+ param([string] $Path)
84
+ $bytes = [long]0; $files = 0; $dirs = 0; $links = 0; $errors = 0
85
+ $newest = [datetime]::MinValue
86
+ $root = Get-ItemInfo $Path
87
+ if ($null -eq $root) { return [pscustomobject]@{ Bytes = 0; Files = 0; Dirs = 0; Links = 0; Newest = $newest; Errors = 0; Exists = $false } }
88
+ if ($root -is [IO.FileInfo]) {
89
+ return [pscustomobject]@{ Bytes = [long]$root.Length; Files = 1; Dirs = 0; Links = 0; Newest = (Get-NewestTimestampUtc $root); Errors = 0; Exists = $true }
90
+ }
91
+ if (Test-ReparsePoint $root) {
92
+ return [pscustomobject]@{ Bytes = 0; Files = 0; Dirs = 0; Links = 1; Newest = (Get-NewestTimestampUtc $root); Errors = 0; Exists = $true }
93
+ }
94
+ $stack = New-Object System.Collections.Generic.Stack[System.IO.DirectoryInfo]
95
+ $stack.Push($root)
96
+ while ($stack.Count -gt 0) {
97
+ $dir = $stack.Pop()
98
+ $entries = $null
99
+ try { $entries = $dir.EnumerateFileSystemInfos() } catch { $errors++; continue }
100
+ try {
101
+ foreach ($e in $entries) {
102
+ if (($e.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { $links++; continue }
103
+ if (($e.Attributes -band [IO.FileAttributes]::Directory) -ne 0) { $dirs++; $stack.Push([IO.DirectoryInfo]$e); continue }
104
+ $files++
105
+ $bytes += [long]$e.Length
106
+ $t = $e.LastWriteTimeUtc
107
+ if ($e.LastAccessTimeUtc -gt $t) { $t = $e.LastAccessTimeUtc }
108
+ if ($e.CreationTimeUtc -gt $t) { $t = $e.CreationTimeUtc }
109
+ if ($t -gt $newest) { $newest = $t }
110
+ }
111
+ } catch { $errors++ }
112
+ }
113
+ if ($newest -eq [datetime]::MinValue) { $newest = Get-NewestTimestampUtc $root }
114
+ return [pscustomobject]@{ Bytes = $bytes; Files = $files; Dirs = $dirs; Links = $links; Newest = $newest; Errors = $errors; Exists = $true }
115
+ }
116
+
117
+ function Get-DirectoryBytes {
118
+ <# .SYNOPSIS Total bytes under a path (0 when absent). Uses robocopy /L when available - it is native and fast. #>
119
+ param([string] $Path)
120
+ if (-not (Test-PathPresent $Path)) { return [long]0 }
121
+ $info = Get-ItemInfo $Path
122
+ if ($info -is [IO.FileInfo]) { return [long]$info.Length }
123
+ if (Test-ReparsePoint $info) { return [long]0 }
124
+ if (Get-Command robocopy.exe -ErrorAction SilentlyContinue) {
125
+ try {
126
+ $sink = Join-Path $env:TEMP ('windowsweep-null-' + [guid]::NewGuid().ToString('N'))
127
+ $lines = & robocopy.exe $Path $sink /L /S /NJH /BYTES /NFL /NDL /NC /NS /XJ /R:0 /W:0 2>$null
128
+ foreach ($line in $lines) {
129
+ if ($line -match '^\s*Bytes\s*:\s*(\d+)') { return [long]$Matches[1] }
130
+ }
131
+ } catch { $null = $_ }
132
+ }
133
+ return [long](Get-DirectoryStats $Path).Bytes
134
+ }
135
+
136
+ function Get-IdleDays {
137
+ <# .SYNOPSIS Days since the path (or anything inside it) was last touched. 999999 when absent. #>
138
+ param([string] $Path)
139
+ $info = Get-ItemInfo $Path
140
+ if ($null -eq $info) { return 999999 }
141
+ if ($info -is [IO.FileInfo] -or (Test-ReparsePoint $info)) {
142
+ return [int][math]::Floor(([datetime]::UtcNow - (Get-NewestTimestampUtc $info)).TotalDays)
143
+ }
144
+ $stats = Get-DirectoryStats $Path
145
+ return [int][math]::Floor(([datetime]::UtcNow - $stats.Newest).TotalDays)
146
+ }
147
+
148
+ function Get-StaleFiles {
149
+ <# .SYNOPSIS Files under Root whose newest timestamp is at least Days old; never follows reparse points.
150
+ Returns objects with Path, Bytes. Also returns the list of directories visited (for the empty sweep). #>
151
+ param([string] $Root, [int] $Days)
152
+ $cutoff = [datetime]::UtcNow.AddDays(-1 * $Days)
153
+ $stale = New-Object System.Collections.Generic.List[object]
154
+ $visited = New-Object System.Collections.Generic.List[string]
155
+ $rootInfo = Get-ItemInfo $Root
156
+ if ($null -eq $rootInfo -or -not ($rootInfo -is [IO.DirectoryInfo]) -or (Test-ReparsePoint $rootInfo)) {
157
+ return [pscustomobject]@{ Files = $stale; Dirs = $visited }
158
+ }
159
+ $stack = New-Object System.Collections.Generic.Stack[System.IO.DirectoryInfo]
160
+ $stack.Push($rootInfo)
161
+ while ($stack.Count -gt 0) {
162
+ $dir = $stack.Pop()
163
+ $entries = $null
164
+ try { $entries = $dir.EnumerateFileSystemInfos() } catch { continue }
165
+ try {
166
+ foreach ($e in $entries) {
167
+ if (($e.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { continue }
168
+ if (($e.Attributes -band [IO.FileAttributes]::Directory) -ne 0) {
169
+ $visited.Add($e.FullName)
170
+ $stack.Push([IO.DirectoryInfo]$e)
171
+ continue
172
+ }
173
+ $t = $e.LastWriteTimeUtc
174
+ if ($e.LastAccessTimeUtc -gt $t) { $t = $e.LastAccessTimeUtc }
175
+ if ($e.CreationTimeUtc -gt $t) { $t = $e.CreationTimeUtc }
176
+ if ($t -le $cutoff) { $stale.Add([pscustomobject]@{ Path = $e.FullName; Bytes = [long]$e.Length }) }
177
+ }
178
+ } catch { $null = $_ }
179
+ }
180
+ return [pscustomobject]@{ Files = $stale; Dirs = $visited }
181
+ }
182
+
183
+ function Remove-LongPrefix {
184
+ <# .SYNOPSIS Strip \\?\ for display. #>
185
+ param([string] $Path)
186
+ if ($Path.StartsWith('\\?\UNC\')) { return ('\\' + $Path.Substring(8)) }
187
+ if ($Path.StartsWith('\\?\')) { return $Path.Substring(4) }
188
+ return $Path
189
+ }