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.
Files changed (41) hide show
  1. package/AI-INTEGRATION-GUIDE.md +203 -0
  2. package/CHANGELOG.md +124 -0
  3. package/LICENSE +21 -21
  4. package/README.md +88 -49
  5. package/VERSION +1 -1
  6. package/lib/actions.ps1 +212 -209
  7. package/lib/config.ps1 +108 -99
  8. package/lib/constants.ps1 +103 -89
  9. package/lib/fs.ps1 +189 -180
  10. package/lib/log.ps1 +140 -140
  11. package/lib/safety.ps1 +541 -493
  12. package/lib/scan.ps1 +109 -94
  13. package/lib/ui.ps1 +275 -235
  14. package/modules/android_avd.ps1 +36 -36
  15. package/modules/app_caches.ps1 +58 -58
  16. package/modules/browsers.ps1 +33 -33
  17. package/modules/build_tools.ps1 +35 -35
  18. package/modules/crash_trap.ps1 +89 -89
  19. package/modules/disk_usage.ps1 +49 -49
  20. package/modules/docker.ps1 +93 -93
  21. package/modules/editors.ps1 +150 -150
  22. package/modules/globals_audit.ps1 +236 -0
  23. package/modules/health.ps1 +101 -101
  24. package/modules/installed_programs.ps1 +118 -0
  25. package/modules/menu.ps1 +63 -61
  26. package/modules/notify.ps1 +60 -0
  27. package/modules/orphaned_appdata.ps1 +189 -0
  28. package/modules/personal.ps1 +126 -124
  29. package/modules/pkg_managers.ps1 +73 -69
  30. package/modules/projects.ps1 +160 -143
  31. package/modules/release_helpers.ps1 +438 -434
  32. package/modules/reports.ps1 +212 -212
  33. package/modules/runner.ps1 +242 -190
  34. package/modules/self_test_extra.ps1 +537 -86
  35. package/modules/startup_audit.ps1 +126 -0
  36. package/modules/system_admin.ps1 +220 -220
  37. package/modules/test_runners.ps1 +24 -24
  38. package/modules/walkthrough.ps1 +80 -79
  39. package/modules/windows_user.ps1 +118 -112
  40. package/package.json +4 -3
  41. package/windowsweep.ps1 +322 -296
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
+ }