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/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
|
|
73
|
-
<# .SYNOPSIS
|
|
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
|
-
if ($
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if ($
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
+
}
|