windowsweep 1.3.2 → 1.4.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 +8 -8
- package/CHANGELOG.md +80 -0
- package/README.md +27 -28
- package/VERSION +1 -1
- package/lib/actions.ps1 +13 -0
- package/lib/config.ps1 +4 -0
- package/lib/constants.ps1 +12 -8
- package/lib/external.ps1 +13 -0
- package/lib/fs.ps1 +116 -14
- package/lib/safety.ps1 +48 -23
- package/lib/ui.ps1 +20 -2
- package/modules/android_avd.ps1 +187 -36
- package/modules/browser_ai.ps1 +68 -0
- package/modules/browsers.ps1 +101 -33
- package/modules/data_scanner.ps1 +158 -0
- package/modules/globals_audit.ps1 +5 -3
- package/modules/installed_programs.ps1 +69 -2
- package/modules/leftovers.ps1 +388 -0
- package/modules/orphaned_appdata.ps1 +47 -3
- package/modules/personal.ps1 +7 -37
- package/modules/projects.ps1 +2 -1
- package/modules/runner.ps1 +29 -5
- package/modules/self_test_1_4.ps1 +215 -0
- package/modules/self_test_1_4b.ps1 +418 -0
- package/modules/self_test_extra.ps1 +9 -0
- package/modules/system_admin.ps1 +17 -3
- package/modules/tool_releases.ps1 +197 -0
- package/modules/uninstall.ps1 +232 -0
- package/modules/windows_user.ps1 +46 -2
- package/package.json +2 -2
- package/windowsweep.ps1 +19 -1
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# self_test_1_4.ps1 - self-test group [19]: the 1.4.0 behaviour found on a real machine (P9, 2026-11-02).
|
|
2
|
+
# Called by Invoke-SelfTestExtra with its fixture root; verdict lines only. ASCII only.
|
|
3
|
+
|
|
4
|
+
function New-AgedFiles {
|
|
5
|
+
<# .SYNOPSIS Create Count files under Dir whose write and creation times are WrittenDaysAgo old; the first
|
|
6
|
+
FreshReads of them were "read" just now, the rest were read when written. Returns the directory. #>
|
|
7
|
+
param([string] $Dir, [int] $Count, [int] $WrittenDaysAgo, [int] $FreshReads)
|
|
8
|
+
New-Item -ItemType Directory -Force -Path $Dir | Out-Null
|
|
9
|
+
$old = [datetime]::UtcNow.AddDays(-1 * $WrittenDaysAgo)
|
|
10
|
+
for ($i = 0; $i -lt $Count; $i++) {
|
|
11
|
+
$f = Join-Path $Dir ("f{0:D3}.bin" -f $i)
|
|
12
|
+
[IO.File]::WriteAllText($f, 'x')
|
|
13
|
+
[IO.File]::SetCreationTimeUtc($f, $old)
|
|
14
|
+
[IO.File]::SetLastWriteTimeUtc($f, $old)
|
|
15
|
+
if ($i -lt $FreshReads) { [IO.File]::SetLastAccessTimeUtc($f, [datetime]::UtcNow) } else { [IO.File]::SetLastAccessTimeUtc($f, $old) }
|
|
16
|
+
}
|
|
17
|
+
return $Dir
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function Invoke-SelfTest14 {
|
|
21
|
+
<# .SYNOPSIS Run group [19]. Returns an object with Checks and Fails counts. #>
|
|
22
|
+
param([string] $Fixture)
|
|
23
|
+
$checks = 0; $fails = 0
|
|
24
|
+
$root = Join-Path $Fixture 'g19'
|
|
25
|
+
Reset-AccessVerdicts
|
|
26
|
+
try {
|
|
27
|
+
Write-Section '[19] 1.4.0: idle basis when last-access is live (D82), one-line --json, orphan claims'
|
|
28
|
+
|
|
29
|
+
# 19a - the verdict rule itself
|
|
30
|
+
$checks++
|
|
31
|
+
$a = Test-AccessSampleLive -Old 30 -FreshAccess 28
|
|
32
|
+
$b = Test-AccessSampleLive -Old 30 -FreshAccess 20
|
|
33
|
+
$c = Test-AccessSampleLive -Old 10 -FreshAccess 10
|
|
34
|
+
if ($a -and -not $b -and -not $c) { Write-Ok 'live verdict: 28 of 30 old files read today is live; 20 of 30 is not; 10 files are too few to judge' } else { Write-Err "live verdict wrong: 28/30=$a 20/30=$b 10/10=$c"; $fails++ }
|
|
35
|
+
|
|
36
|
+
# 19b - a root where every old file was "read" today: the idle gate falls back to write time
|
|
37
|
+
$checks++
|
|
38
|
+
$live = New-AgedFiles -Dir (Join-Path $root 'live') -Count 30 -WrittenDaysAgo 60 -FreshReads 30
|
|
39
|
+
$isLive = Test-LastAccessLive -Root $live
|
|
40
|
+
# Assigned directly: @() around a member-accessed List[object] throws "Argument types do not match" on PS 5.1.
|
|
41
|
+
$stale = (Get-StaleFiles -Root $live -Days 30).Files
|
|
42
|
+
if ($isLive -and $stale.Count -eq 30) { Write-Ok 'live last-access: the root is detected and all 30 files written 60 days ago are idle' } else { Write-Err "live root: detected=$isLive stale=$($stale.Count) of 30"; $fails++ }
|
|
43
|
+
|
|
44
|
+
# 19c - an honest root (read when written): behaviour unchanged
|
|
45
|
+
$checks++
|
|
46
|
+
$honest = New-AgedFiles -Dir (Join-Path $root 'honest') -Count 30 -WrittenDaysAgo 60 -FreshReads 0
|
|
47
|
+
$isLive = Test-LastAccessLive -Root $honest
|
|
48
|
+
$stale = (Get-StaleFiles -Root $honest -Days 30).Files
|
|
49
|
+
if (-not $isLive -and $stale.Count -eq 30) { Write-Ok 'honest last-access: not flagged, and the 30 old files are idle as before' } else { Write-Err "honest root: detected=$isLive stale=$($stale.Count) of 30"; $fails++ }
|
|
50
|
+
|
|
51
|
+
# 19d - a mixed root: the files really read recently are still kept (the conservative rule holds)
|
|
52
|
+
$checks++
|
|
53
|
+
$mixed = New-AgedFiles -Dir (Join-Path $root 'mixed') -Count 30 -WrittenDaysAgo 60 -FreshReads 5
|
|
54
|
+
$isLive = Test-LastAccessLive -Root $mixed
|
|
55
|
+
$stale = (Get-StaleFiles -Root $mixed -Days 30).Files
|
|
56
|
+
if (-not $isLive -and $stale.Count -eq 25) { Write-Ok 'mixed root: 5 of 30 read today is a real signal - those 5 are kept, 25 are idle' } else { Write-Err "mixed root: detected=$isLive stale=$($stale.Count) (expected 25)"; $fails++ }
|
|
57
|
+
|
|
58
|
+
# 19e - a root too small to judge: conservative alone, live only when its volume was found live
|
|
59
|
+
$checks++
|
|
60
|
+
Reset-AccessVerdicts
|
|
61
|
+
$small1 = New-AgedFiles -Dir (Join-Path $root 'small1') -Count 3 -WrittenDaysAgo 60 -FreshReads 3
|
|
62
|
+
$idleAlone = Get-IdleDays $small1
|
|
63
|
+
$null = Test-LastAccessLive -Root $live
|
|
64
|
+
$small2 = New-AgedFiles -Dir (Join-Path $root 'small2') -Count 3 -WrittenDaysAgo 60 -FreshReads 3
|
|
65
|
+
$idleInherited = Get-IdleDays $small2
|
|
66
|
+
if ($idleAlone -eq 0 -and $idleInherited -ge 59) { Write-Ok 'small root: 0 days idle on its own, 60 once its volume is known to have live last-access' } else { Write-Err "small root: alone=$idleAlone inherited=$idleInherited"; $fails++ }
|
|
67
|
+
|
|
68
|
+
# 19g - the --json summary names the volume whose idle age used write/creation time
|
|
69
|
+
$checks++
|
|
70
|
+
$vol = (Get-VolumeKey $live).TrimEnd('\').ToUpperInvariant()
|
|
71
|
+
$doc = (Get-JsonSummary) | ConvertTo-Json -Depth 5 -Compress
|
|
72
|
+
$named = @(($doc | ConvertFrom-Json).access_live_volumes)
|
|
73
|
+
if ($named -contains $vol -and -not $doc.Contains("`n")) { Write-Ok "--json names the live-access volume ($vol) in access_live_volumes, still on one line" } else { Write-Err "access_live_volumes wrong: [$($named -join ',')] expected $vol"; $fails++ }
|
|
74
|
+
|
|
75
|
+
# 19h-19l - section 23 claims learned from the P52 (every case erred towards calling a live folder orphaned)
|
|
76
|
+
$set = New-Object System.Collections.Generic.HashSet[string]([StringComparer]::Ordinal)
|
|
77
|
+
foreach ($name in 'windowsweep', 'DBeaverCorp.DBeaverCE', 'Internet Download Manager') { Add-MatchTokens -Set $set -Text $name }
|
|
78
|
+
Add-CommandLineTokens -Set $set -CommandLine 'node C:/Users/x/.claude/plugins/cache/superpowers-chrome/3.0.2/mcp/dist/index.js'
|
|
79
|
+
$index = [pscustomobject]@{ Tokens = $set; RegistryCount = 3; Rows = @() }
|
|
80
|
+
$checks++
|
|
81
|
+
$dnsClaimed = -not (Test-OrphanFolderName -Name 'com.aoneahsan.windowsweep' -Index $index)
|
|
82
|
+
$dnsOrphan = Test-OrphanFolderName -Name 'com.example.zqxwvu' -Index $index
|
|
83
|
+
if ($dnsClaimed -and $dnsOrphan) { Write-Ok 'reverse-DNS data folder: com.aoneahsan.windowsweep is claimed by "windowsweep"; an unknown app id stays an orphan' } else { Write-Err "reverse-DNS claim wrong: claimed=$dnsClaimed unknownIsOrphan=$dnsOrphan"; $fails++ }
|
|
84
|
+
$checks++
|
|
85
|
+
$stem = -not (Test-OrphanFolderName -Name 'DBeaverData' -Index $index)
|
|
86
|
+
$other = Test-OrphanFolderName -Name 'ChessBase' -Index $index
|
|
87
|
+
if ($stem -and $other) { Write-Ok 'vendor stem: DBeaverData is claimed by the DBeaverCorp.DBeaverCE Store app; ChessBase is not' } else { Write-Err "vendor stem wrong: DBeaverData claimed=$stem ChessBase orphan=$other"; $fails++ }
|
|
88
|
+
$checks++
|
|
89
|
+
if (-not (Test-OrphanFolderName -Name 'IDM' -Index $index)) { Write-Ok 'acronym: IDM is claimed by Internet Download Manager' } else { Write-Err 'acronym claim missing: IDM offered as orphaned'; $fails++ }
|
|
90
|
+
$checks++
|
|
91
|
+
$bare = New-Object System.Collections.Generic.HashSet[string]([StringComparer]::Ordinal)
|
|
92
|
+
Add-MatchTokens -Set $bare -Text 'windowsweep'
|
|
93
|
+
Add-CommandLineTokens -Set $bare -CommandLine 'bash -c "grep -c vega-desktop-updater log.txt"'
|
|
94
|
+
$bareIndex = [pscustomobject]@{ Tokens = $bare; RegistryCount = 1; Rows = @() }
|
|
95
|
+
$pathClaims = -not (Test-OrphanFolderName -Name 'superpowers' -Index $index)
|
|
96
|
+
$wordIgnored = Test-OrphanFolderName -Name 'vega-desktop-updater' -Index $bareIndex
|
|
97
|
+
if ($pathClaims -and $wordIgnored) { Write-Ok 'command line: a folder in a running PATH (superpowers) is claimed; a bare word in a shell command line claims nothing' } else { Write-Err "command-line claim wrong: pathClaims=$pathClaims bareWordIgnored=$wordIgnored"; $fails++ }
|
|
98
|
+
$checks++
|
|
99
|
+
$why = Get-ProtectionReason (Join-Path $env:LOCALAPPDATA 'com.aoneahsan.windowsweep\EBWebView')
|
|
100
|
+
if ($why) { Write-Ok "the desktop app's own data folder is protected ($why)" } else { Write-Err "DANGEROUS: the desktop app's data folder is not protected"; $fails++ }
|
|
101
|
+
|
|
102
|
+
# 19f - nothing a section emits can reach --json stdout
|
|
103
|
+
$checks++
|
|
104
|
+
$runner = Get-Content -LiteralPath (Join-Path $Script:WS_ROOT 'modules\runner.ps1') -Raw
|
|
105
|
+
$admin = Get-Content -LiteralPath (Join-Path $Script:WS_ROOT 'modules\system_admin.ps1') -Raw
|
|
106
|
+
$runnerOk = $runner -match '\$null = & \$sec\.Fn'
|
|
107
|
+
$doLines = @($admin -split "`n" | Where-Object { $_ -match 'Delete-DeliveryOptimizationCache -Force -ErrorAction' })
|
|
108
|
+
$doOk = ($doLines.Count -ge 1) -and (@($doLines | Where-Object { $_ -notmatch '\*>&1' }).Count -eq 0)
|
|
109
|
+
if ($runnerOk -and $doOk) { Write-Ok 'section output is discarded by the runner, and the Delivery Optimization cmdlet is captured on every stream' } else { Write-Err "stdout guards missing: runner=$runnerOk deliveryOptimization=$doOk"; $fails++ }
|
|
110
|
+
} catch {
|
|
111
|
+
Write-Err "group [19] crashed: $($_.Exception.Message) | at $(($_.ScriptStackTrace -split "`n" | Select-Object -First 3) -join ' <- ')"; $fails++
|
|
112
|
+
} finally {
|
|
113
|
+
Reset-AccessVerdicts
|
|
114
|
+
try { if (Test-Path -LiteralPath $root) { [IO.Directory]::Delete((Get-LongPath $root), $true) } } catch { $null = $_ }
|
|
115
|
+
}
|
|
116
|
+
return [pscustomobject]@{ Checks = $checks; Fails = $fails }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function Invoke-SelfTest21 {
|
|
120
|
+
<# .SYNOPSIS Group [21]: section 29 (the Chrome on-device AI policy) - never written by a dry-run, never batched. #>
|
|
121
|
+
$checks = 0; $fails = 0
|
|
122
|
+
$ws = $Script:WS
|
|
123
|
+
$savedDry = $ws.DryRun
|
|
124
|
+
$key = 'HKCU:\Software\windowsweep-selftest-' + [guid]::NewGuid().ToString('N')
|
|
125
|
+
try {
|
|
126
|
+
Write-Section '[21] 1.4.0: the Chrome on-device AI policy (section 29)'
|
|
127
|
+
# 21a - a dry-run prints the change and writes nothing
|
|
128
|
+
$checks++
|
|
129
|
+
$ws.DryRun = $true
|
|
130
|
+
$dry = Set-BrowserAiPolicy -KeyPath $key
|
|
131
|
+
if ($dry -eq 'would-set' -and -not (Test-Path -LiteralPath $key)) { Write-Ok 'dry-run: the policy is described, no registry key is written' } else { Write-Err "dry-run wrote the policy or answered '$dry'"; $fails++ }
|
|
132
|
+
# 21b - a real run writes exactly the value Chrome reads (on a throwaway key, never the machine policy)
|
|
133
|
+
$checks++
|
|
134
|
+
$ws.DryRun = $false
|
|
135
|
+
$real = Set-BrowserAiPolicy -KeyPath $key
|
|
136
|
+
$state = Get-BrowserAiPolicyState -KeyPath $key
|
|
137
|
+
if ($real -eq 'set' -and $state -eq 1) { Write-Ok "a real run sets $($Script:WS_BROWSERAI_POLICY_VALUE) = 1" } else { Write-Err "policy write wrong: result='$real' value='$state'"; $fails++ }
|
|
138
|
+
# 21c - it changes a browser's configuration: deep, admin, config tier, in no batch or profile
|
|
139
|
+
$checks++
|
|
140
|
+
$s = Get-Section 29
|
|
141
|
+
$batched = @($Script:WS_SAFE_BATCH + $Script:WS_SAFE_BATCH_ADMIN + @($Script:WS_PROFILES.Values | ForEach-Object { $_ }) + $Script:WS_WALKTHROUGH + $Script:WS_WALKTHROUGH_ADMIN) -contains 29
|
|
142
|
+
if ($s -and $s.Batch -eq 'deep' -and $s.Admin -and $s.Tier -eq 'config' -and -not $batched) { Write-Ok 'section 29 is deep, admin and config, and no batch, profile or walkthrough runs it' } else { Write-Err "section 29 contract wrong: batch=$($s.Batch) admin=$($s.Admin) tier=$($s.Tier) batched=$batched"; $fails++ }
|
|
143
|
+
} catch {
|
|
144
|
+
Write-Err "group [21] crashed: $($_.Exception.Message) | at $(($_.ScriptStackTrace -split "`n" | Select-Object -First 3) -join ' <- ')"; $fails++
|
|
145
|
+
} finally {
|
|
146
|
+
$ws.DryRun = $savedDry
|
|
147
|
+
try { if (Test-Path -LiteralPath $key) { Remove-Item -LiteralPath $key -Recurse -Force } } catch { $null = $_ }
|
|
148
|
+
}
|
|
149
|
+
return [pscustomobject]@{ Checks = $checks; Fails = $fails }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function Invoke-SelfTest20 {
|
|
153
|
+
<# .SYNOPSIS Group [20]: section 28 (superseded releases, Node pins) and the D79 exception rules. #>
|
|
154
|
+
param([string] $Fixture)
|
|
155
|
+
$checks = 0; $fails = 0
|
|
156
|
+
$root = Join-Path $Fixture 'g20'
|
|
157
|
+
$savedExclude = $Script:WS_EXCLUDE
|
|
158
|
+
try {
|
|
159
|
+
Write-Section '[20] 1.4.0: superseded releases (section 28) and narrow exceptions (D79)'
|
|
160
|
+
$rel = Join-Path $root 'codex\releases'
|
|
161
|
+
foreach ($v in '0.1.0-x86_64-pc-windows-msvc', '0.2.0-x86_64-pc-windows-msvc', '0.3.0-x86_64-pc-windows-msvc', '0.4.0-x86_64-pc-windows-msvc', 'notes') { New-Item -ItemType Directory -Force -Path (Join-Path $rel $v) | Out-Null }
|
|
162
|
+
$link = Join-Path $root 'codex\current'
|
|
163
|
+
|
|
164
|
+
# 20b first, while the link is absent - no readable current release means nothing is dropped
|
|
165
|
+
$checks++
|
|
166
|
+
$none = Get-CodexReleasePlan -Root $rel -CurrentLink $link
|
|
167
|
+
if ($none.Reason -and @($none.Drop).Count -eq 0) { Write-Ok 'no current-release link: nothing is offered (fail closed)' } else { Write-Err "missing link not fail-closed: drop=[$($none.Drop -join ',')]"; $fails++ }
|
|
168
|
+
|
|
169
|
+
# 20a - current 0.3.0 keeps itself, the newer 0.4.0, the rollback 0.2.0 and the odd folder; only 0.1.0 goes
|
|
170
|
+
$checks++
|
|
171
|
+
New-Item -ItemType Junction -Path $link -Target (Join-Path $rel '0.3.0-x86_64-pc-windows-msvc') | Out-Null
|
|
172
|
+
$plan = Get-CodexReleasePlan -Root $rel -CurrentLink $link
|
|
173
|
+
$dropNames = @($plan.Drop | ForEach-Object { Split-Path -Leaf $_ })
|
|
174
|
+
$keepNames = @($plan.Keep | ForEach-Object { Split-Path -Leaf $_ } | Sort-Object)
|
|
175
|
+
if (-not $plan.Reason -and ($dropNames -join ',') -eq '0.1.0-x86_64-pc-windows-msvc' -and $keepNames.Count -eq 4) { Write-Ok 'Codex plan: current, newer, rollback and an odd folder kept; only the oldest release goes' } else { Write-Err "Codex plan wrong: drop=[$($dropNames -join ',')] keep=[$($keepNames -join ',')] reason='$($plan.Reason)'"; $fails++ }
|
|
176
|
+
|
|
177
|
+
# 20c - the live exceptions are exactly the live plan's Drop folders, and the kept ones stay protected
|
|
178
|
+
$checks++
|
|
179
|
+
$live = Get-CodexReleasePlan
|
|
180
|
+
$codexPrefix = (Join-Path $Script:P.U '.codex') + '\'
|
|
181
|
+
$ex = @($Script:WS_PROTECT.Exceptions | Where-Object { $_.StartsWith($codexPrefix, [StringComparison]::OrdinalIgnoreCase) } | Sort-Object)
|
|
182
|
+
$want = @($live.Drop | Sort-Object)
|
|
183
|
+
$keptOpen = @($live.Keep | Where-Object { -not (Get-ProtectionReason $_) })
|
|
184
|
+
if ((($ex -join '|') -eq ($want -join '|')) -and $keptOpen.Count -eq 0) { Write-Ok "D79: $($ex.Count) Codex exception(s), exactly the superseded releases; every kept release still refused" } else { Write-Err "D79 exceptions wrong: exceptions=[$($ex -join ',')] plan=[$($want -join ',')] keptButAllowed=[$($keptOpen -join ',')]"; $fails++ }
|
|
185
|
+
|
|
186
|
+
# 20d - an exception never lifts the user's own --exclude-path
|
|
187
|
+
$checks++
|
|
188
|
+
$tempEx = $Script:WS_PROTECT.Exceptions | Select-Object -First 1
|
|
189
|
+
$inside = Join-Path $tempEx 'probe.bin'
|
|
190
|
+
$open = Get-ProtectionReason $inside
|
|
191
|
+
$Script:WS_EXCLUDE = @{ Prefixes = @($tempEx.TrimEnd('\') + '\'); Roots = @($tempEx) }
|
|
192
|
+
$closed = Get-ProtectionReason $inside
|
|
193
|
+
if (-not $open -and $closed -like 'excluded: *') { Write-Ok 'inside an exception, the path is allowed - until the user excludes it' } else { Write-Err "exception vs exclusion wrong: open='$open' closed='$closed'"; $fails++ }
|
|
194
|
+
$Script:WS_EXCLUDE = $savedExclude
|
|
195
|
+
|
|
196
|
+
# 20e - Node: the active version, exact pins and the newest of a pinned major stay; nothing else is needed
|
|
197
|
+
$checks++
|
|
198
|
+
$pins = @('24.13.0', '24', '22', '20', 'v24.16.0' | ForEach-Object { ConvertTo-NodePin $_ })
|
|
199
|
+
$np = Get-NodeVersionPlan -Installed @('v22.22.0', 'v24.10.0', 'v24.13.0') -Active 'v24.13.0' -Pins $pins
|
|
200
|
+
$odd = Get-NodeVersionPlan -Installed @('v22.22.0', 'v24.10.0') -Active 'v22.22.0' -Pins @((ConvertTo-NodePin 'banana'))
|
|
201
|
+
if (($np.Drop -join ',') -eq 'v24.10.0' -and @($np.Keep).Count -eq 2 -and @($np.Unmet).Count -eq 2 -and $odd.Reason -and @($odd.Drop).Count -eq 0) { Write-Ok 'Node plan: only v24.10.0 is unrequested; 20 and 24.16.0 are reported as missing; an unreadable request keeps everything' } else { Write-Err "Node plan wrong: drop=[$($np.Drop -join ',')] keep=[$($np.Keep -join ',')] unmet=[$($np.Unmet -join ',')] oddDrop=[$($odd.Drop -join ',')]"; $fails++ }
|
|
202
|
+
|
|
203
|
+
# 20f - version requests as projects write them
|
|
204
|
+
$checks++
|
|
205
|
+
$kinds = @('v24.16.0', '24', '>=22.12.0', '^24', 'lts/*', 'banana' | ForEach-Object { $n = ConvertTo-NodePin $_; "$($n.Kind):$($n.Major)" })
|
|
206
|
+
if (($kinds -join ' ') -eq 'exact:24 major:24 major:22 major:24 newest:0 unknown:0') { Write-Ok 'version requests: exact, major, ranges, lts and nonsense each read correctly' } else { Write-Err "version request parsing wrong: $($kinds -join ' ')"; $fails++ }
|
|
207
|
+
} catch {
|
|
208
|
+
Write-Err "group [20] crashed: $($_.Exception.Message) | at $(($_.ScriptStackTrace -split "`n" | Select-Object -First 3) -join ' <- ')"; $fails++
|
|
209
|
+
} finally {
|
|
210
|
+
$Script:WS_EXCLUDE = $savedExclude
|
|
211
|
+
try { $j = Join-Path $root 'codex\current'; if (Test-Path -LiteralPath $j) { [IO.Directory]::Delete($j, $false) } } catch { $null = $_ }
|
|
212
|
+
try { if (Test-Path -LiteralPath $root) { [IO.Directory]::Delete((Get-LongPath $root), $true) } } catch { $null = $_ }
|
|
213
|
+
}
|
|
214
|
+
return [pscustomobject]@{ Checks = $checks; Fails = $fails }
|
|
215
|
+
}
|