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.
- package/AI-INTEGRATION-GUIDE.md +16 -12
- package/CHANGELOG.md +82 -0
- package/LICENSE +21 -21
- package/README.md +104 -69
- package/VERSION +1 -1
- package/lib/actions.ps1 +212 -212
- package/lib/config.ps1 +108 -99
- package/lib/constants.ps1 +103 -93
- package/lib/external.ps1 +87 -0
- package/lib/fs.ps1 +189 -180
- package/lib/log.ps1 +140 -140
- package/lib/safety.ps1 +467 -493
- package/lib/scan.ps1 +109 -100
- package/lib/ui.ps1 +275 -275
- 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 -236
- package/modules/health.ps1 +101 -101
- package/modules/installed_programs.ps1 +118 -118
- package/modules/menu.ps1 +63 -63
- package/modules/notify.ps1 +60 -60
- package/modules/orphaned_appdata.ps1 +189 -189
- package/modules/personal.ps1 +126 -126
- package/modules/pkg_managers.ps1 +73 -73
- package/modules/projects.ps1 +160 -160
- package/modules/release_helpers.ps1 +438 -438
- package/modules/reports.ps1 +212 -212
- package/modules/runner.ps1 +246 -239
- package/modules/self_test_contract.ps1 +195 -0
- package/modules/self_test_extra.ps1 +366 -360
- package/modules/startup_audit.ps1 +126 -126
- 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 -118
- package/package.json +3 -3
- package/windowsweep.ps1 +322 -316
package/modules/docker.ps1
CHANGED
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
# docker.ps1 - section 5 (Docker prune) and section 20 (disk image compaction).
|
|
2
|
-
|
|
3
|
-
function Get-Targets05 {
|
|
4
|
-
return @(
|
|
5
|
-
(New-Target 5 'docker image prune (dangling layers)' 'docker image prune -f' -Kind cmd)
|
|
6
|
-
(New-Target 5 'docker builder prune (build cache idle N+ days)' 'docker builder prune -f --filter until=<N>h' -Kind cmd)
|
|
7
|
-
(New-Target 5 'docker image prune (unused images older than N days)' 'docker image prune -a -f --filter until=<N>h' -Kind cmd)
|
|
8
|
-
)
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function Test-DockerDaemon {
|
|
12
|
-
if (-not (Test-CommandPresent 'docker')) { return $false }
|
|
13
|
-
$r = Invoke-External -FilePath 'docker' -ArgumentList @('info', '--format', '{{.ServerVersion}}') -Quiet -Label 'docker info'
|
|
14
|
-
return ($r.Ran -and $r.ExitCode -eq 0)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function Invoke-DockerPrune {
|
|
18
|
-
param([string[]] $Arguments, [string] $Label)
|
|
19
|
-
$r = Invoke-External -FilePath 'docker' -ArgumentList $Arguments -Destructive -Quiet -Label $Label
|
|
20
|
-
if (-not $r.Ran) { return [long]0 }
|
|
21
|
-
$freed = [long]0
|
|
22
|
-
foreach ($l in $r.Output) { if ($l -match 'Total reclaimed space:\s*(.+)$') { $freed += ConvertFrom-SizeText $Matches[1] } }
|
|
23
|
-
if ($r.ExitCode -eq 0) { Write-Ok ("$Label - reclaimed " + (Format-Bytes $freed)) } else { Write-Warn "$Label - docker returned $($r.ExitCode)" }
|
|
24
|
-
Add-Freed $freed
|
|
25
|
-
return $freed
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function Invoke-Section05 {
|
|
29
|
-
$ws = $Script:WS
|
|
30
|
-
Write-SectionIntro @(
|
|
31
|
-
'Docker keeps every image layer and build step it ever produced. Dangling layers and the build cache are',
|
|
32
|
-
'safe to drop; images no container uses go only when older than the idle window. Volumes are never touched',
|
|
33
|
-
'(review them yourself with: docker volume ls / docker volume prune).'
|
|
34
|
-
) -Dev $true
|
|
35
|
-
if (-not (Test-CommandPresent 'docker')) { Write-Info 'docker CLI not found - skipped'; return }
|
|
36
|
-
if (-not (Test-DockerDaemon)) { Write-Warn 'Docker daemon is not running (start Docker Desktop) - skipped'; $ws.Hints += 'Start Docker Desktop and re-run: windowsweep --only 5 --yes'; return }
|
|
37
|
-
$df = Invoke-External -FilePath 'docker' -ArgumentList @('system', 'df') -Quiet -Label 'docker system df'
|
|
38
|
-
foreach ($l in $df.Output) { Write-Plain " $l" }
|
|
39
|
-
if (-not (Confirm-Section 'Prune Docker now?')) { Write-Info 'skipped'; return }
|
|
40
|
-
$hours = [int]($ws.Days * 24)
|
|
41
|
-
if ($ws.PurgeAll -or $ws.Developer -eq $false) {
|
|
42
|
-
if ($ws.Developer -ne $false -and -not (Confirm-Ui -Prompt 'FULL PURGE removes every image no container uses right now (they re-pull later). Continue?' -Default 'n')) { Write-Info 'full purge declined; doing the conservative prune instead' }
|
|
43
|
-
else { $null = Invoke-DockerPrune -Arguments @('system', 'prune', '-a', '-f') -Label 'docker system prune -a'; return }
|
|
44
|
-
}
|
|
45
|
-
$null = Invoke-DockerPrune -Arguments @('image', 'prune', '-f') -Label 'dangling image layers'
|
|
46
|
-
$null = Invoke-DockerPrune -Arguments @('builder', 'prune', '-f', '--filter', "until=${hours}h") -Label "build cache idle $($ws.Days)+ days"
|
|
47
|
-
$null = Invoke-DockerPrune -Arguments @('image', 'prune', '-a', '-f', '--filter', "until=${hours}h") -Label "unused images older than $($ws.Days) days"
|
|
48
|
-
Write-Note 'the Docker disk image (docker_data.vhdx) does not shrink by itself - section 20 compacts it (admin, stops Docker)'
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
# ---------------------------------------------------------------------------------------------
|
|
52
|
-
# Section 20 - disk image compaction
|
|
53
|
-
# ---------------------------------------------------------------------------------------------
|
|
54
|
-
|
|
55
|
-
function Get-Targets20 {
|
|
56
|
-
return @((New-Target 20 'Docker Desktop / WSL virtual disks (*.vhdx)' "$($Script:P.L)\Docker\wsl\*\*.vhdx and Packages\*\LocalState\*.vhdx" -Kind cmd -Note 'diskpart compact vdisk; Docker Desktop and WSL are stopped first'))
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function Invoke-Section20 {
|
|
60
|
-
$ws = $Script:WS
|
|
61
|
-
Write-SectionIntro @(
|
|
62
|
-
'A WSL/Docker virtual disk grows as data is written but never shrinks when it is deleted. Compaction hands the',
|
|
63
|
-
'free space back to Windows. It needs Administrator rights and STOPS Docker Desktop and every WSL distro first;',
|
|
64
|
-
'restart Docker Desktop afterwards.'
|
|
65
|
-
)
|
|
66
|
-
$images = @(Get-DiskImageFiles)
|
|
67
|
-
if ($images.Count -eq 0) { Write-Info 'no .vhdx disk images found'; return }
|
|
68
|
-
$i = 1
|
|
69
|
-
foreach ($img in $images) { Write-UiLine (" {0,3} {1,10} {2}" -f $i, (Format-Bytes $img.Length), $img.FullName) 'Gray'; $i++ }
|
|
70
|
-
if (-not (Test-CommandPresent 'diskpart.exe')) { Write-Warn 'diskpart.exe not found - skipped'; return }
|
|
71
|
-
$picks = @()
|
|
72
|
-
if ($ws.Yes) { $picks = @(1..$images.Count) } else { $picks = @(Read-MultiSelect -Total $images.Count -NoAutoYes) }
|
|
73
|
-
if ($picks.Count -eq 0) { Write-Info 'nothing selected'; return }
|
|
74
|
-
if (-not (Confirm-Section 'Stop Docker Desktop and WSL now and compact the selected disks?' 'n')) { Write-Info 'skipped'; return }
|
|
75
|
-
if ($ws.DryRun) { foreach ($k in $picks) { Write-DryRun "would compact $($images[$k - 1].FullName)" }; return }
|
|
76
|
-
Write-Info 'stopping Docker Desktop and WSL...'
|
|
77
|
-
Get-Process -Name 'Docker Desktop' -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
|
|
78
|
-
$null = Invoke-External -FilePath 'wsl.exe' -ArgumentList @('--shutdown') -Destructive -Quiet -Label 'wsl --shutdown'
|
|
79
|
-
Start-Sleep -Seconds 8
|
|
80
|
-
foreach ($k in $picks) {
|
|
81
|
-
$img = $images[$k - 1]
|
|
82
|
-
$before = (Get-Item -LiteralPath $img.FullName).Length
|
|
83
|
-
$script = Join-Path $env:TEMP ("windowsweep-diskpart-" + [guid]::NewGuid().ToString('N') + '.txt')
|
|
84
|
-
[IO.File]::WriteAllLines($script, @("select vdisk file=`"$($img.FullName)`"", 'attach vdisk readonly', 'compact vdisk', 'detach vdisk', 'exit'))
|
|
85
|
-
$r = Invoke-External -FilePath 'diskpart.exe' -ArgumentList @('/s', $script) -Destructive -Quiet -Label "diskpart compact $($img.Name)"
|
|
86
|
-
Remove-Item -LiteralPath $script -Force -ErrorAction SilentlyContinue
|
|
87
|
-
$after = (Get-Item -LiteralPath $img.FullName).Length
|
|
88
|
-
$gain = [math]::Max([long]0, $before - $after)
|
|
89
|
-
if ($r.ExitCode -eq 0) { Write-Ok ("$($img.Name): " + (Format-Bytes $before) + ' -> ' + (Format-Bytes $after) + ' (freed ' + (Format-Bytes $gain) + ')') } else { Write-Warn "$($img.Name): diskpart returned $($r.ExitCode)"; foreach ($l in $r.Output) { Write-Note $l } }
|
|
90
|
-
Add-Freed $gain
|
|
91
|
-
}
|
|
92
|
-
Write-Info 'done - start Docker Desktop again when you need it'
|
|
93
|
-
}
|
|
1
|
+
# docker.ps1 - section 5 (Docker prune) and section 20 (disk image compaction).
|
|
2
|
+
|
|
3
|
+
function Get-Targets05 {
|
|
4
|
+
return @(
|
|
5
|
+
(New-Target 5 'docker image prune (dangling layers)' 'docker image prune -f' -Kind cmd)
|
|
6
|
+
(New-Target 5 'docker builder prune (build cache idle N+ days)' 'docker builder prune -f --filter until=<N>h' -Kind cmd)
|
|
7
|
+
(New-Target 5 'docker image prune (unused images older than N days)' 'docker image prune -a -f --filter until=<N>h' -Kind cmd)
|
|
8
|
+
)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function Test-DockerDaemon {
|
|
12
|
+
if (-not (Test-CommandPresent 'docker')) { return $false }
|
|
13
|
+
$r = Invoke-External -FilePath 'docker' -ArgumentList @('info', '--format', '{{.ServerVersion}}') -Quiet -Label 'docker info'
|
|
14
|
+
return ($r.Ran -and $r.ExitCode -eq 0)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function Invoke-DockerPrune {
|
|
18
|
+
param([string[]] $Arguments, [string] $Label)
|
|
19
|
+
$r = Invoke-External -FilePath 'docker' -ArgumentList $Arguments -Destructive -Quiet -Label $Label
|
|
20
|
+
if (-not $r.Ran) { return [long]0 }
|
|
21
|
+
$freed = [long]0
|
|
22
|
+
foreach ($l in $r.Output) { if ($l -match 'Total reclaimed space:\s*(.+)$') { $freed += ConvertFrom-SizeText $Matches[1] } }
|
|
23
|
+
if ($r.ExitCode -eq 0) { Write-Ok ("$Label - reclaimed " + (Format-Bytes $freed)) } else { Write-Warn "$Label - docker returned $($r.ExitCode)" }
|
|
24
|
+
Add-Freed $freed
|
|
25
|
+
return $freed
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function Invoke-Section05 {
|
|
29
|
+
$ws = $Script:WS
|
|
30
|
+
Write-SectionIntro @(
|
|
31
|
+
'Docker keeps every image layer and build step it ever produced. Dangling layers and the build cache are',
|
|
32
|
+
'safe to drop; images no container uses go only when older than the idle window. Volumes are never touched',
|
|
33
|
+
'(review them yourself with: docker volume ls / docker volume prune).'
|
|
34
|
+
) -Dev $true
|
|
35
|
+
if (-not (Test-CommandPresent 'docker')) { Write-Info 'docker CLI not found - skipped'; return }
|
|
36
|
+
if (-not (Test-DockerDaemon)) { Write-Warn 'Docker daemon is not running (start Docker Desktop) - skipped'; $ws.Hints += 'Start Docker Desktop and re-run: windowsweep --only 5 --yes'; return }
|
|
37
|
+
$df = Invoke-External -FilePath 'docker' -ArgumentList @('system', 'df') -Quiet -Label 'docker system df'
|
|
38
|
+
foreach ($l in $df.Output) { Write-Plain " $l" }
|
|
39
|
+
if (-not (Confirm-Section 'Prune Docker now?')) { Write-Info 'skipped'; return }
|
|
40
|
+
$hours = [int]($ws.Days * 24)
|
|
41
|
+
if ($ws.PurgeAll -or $ws.Developer -eq $false) {
|
|
42
|
+
if ($ws.Developer -ne $false -and -not (Confirm-Ui -Prompt 'FULL PURGE removes every image no container uses right now (they re-pull later). Continue?' -Default 'n')) { Write-Info 'full purge declined; doing the conservative prune instead' }
|
|
43
|
+
else { $null = Invoke-DockerPrune -Arguments @('system', 'prune', '-a', '-f') -Label 'docker system prune -a'; return }
|
|
44
|
+
}
|
|
45
|
+
$null = Invoke-DockerPrune -Arguments @('image', 'prune', '-f') -Label 'dangling image layers'
|
|
46
|
+
$null = Invoke-DockerPrune -Arguments @('builder', 'prune', '-f', '--filter', "until=${hours}h") -Label "build cache idle $($ws.Days)+ days"
|
|
47
|
+
$null = Invoke-DockerPrune -Arguments @('image', 'prune', '-a', '-f', '--filter', "until=${hours}h") -Label "unused images older than $($ws.Days) days"
|
|
48
|
+
Write-Note 'the Docker disk image (docker_data.vhdx) does not shrink by itself - section 20 compacts it (admin, stops Docker)'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------------------------
|
|
52
|
+
# Section 20 - disk image compaction
|
|
53
|
+
# ---------------------------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
function Get-Targets20 {
|
|
56
|
+
return @((New-Target 20 'Docker Desktop / WSL virtual disks (*.vhdx)' "$($Script:P.L)\Docker\wsl\*\*.vhdx and Packages\*\LocalState\*.vhdx" -Kind cmd -Note 'diskpart compact vdisk; Docker Desktop and WSL are stopped first'))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function Invoke-Section20 {
|
|
60
|
+
$ws = $Script:WS
|
|
61
|
+
Write-SectionIntro @(
|
|
62
|
+
'A WSL/Docker virtual disk grows as data is written but never shrinks when it is deleted. Compaction hands the',
|
|
63
|
+
'free space back to Windows. It needs Administrator rights and STOPS Docker Desktop and every WSL distro first;',
|
|
64
|
+
'restart Docker Desktop afterwards.'
|
|
65
|
+
)
|
|
66
|
+
$images = @(Get-DiskImageFiles)
|
|
67
|
+
if ($images.Count -eq 0) { Write-Info 'no .vhdx disk images found'; return }
|
|
68
|
+
$i = 1
|
|
69
|
+
foreach ($img in $images) { Write-UiLine (" {0,3} {1,10} {2}" -f $i, (Format-Bytes $img.Length), $img.FullName) 'Gray'; $i++ }
|
|
70
|
+
if (-not (Test-CommandPresent 'diskpart.exe')) { Write-Warn 'diskpart.exe not found - skipped'; return }
|
|
71
|
+
$picks = @()
|
|
72
|
+
if ($ws.Yes) { $picks = @(1..$images.Count) } else { $picks = @(Read-MultiSelect -Total $images.Count -NoAutoYes) }
|
|
73
|
+
if ($picks.Count -eq 0) { Write-Info 'nothing selected'; return }
|
|
74
|
+
if (-not (Confirm-Section 'Stop Docker Desktop and WSL now and compact the selected disks?' 'n')) { Write-Info 'skipped'; return }
|
|
75
|
+
if ($ws.DryRun) { foreach ($k in $picks) { Write-DryRun "would compact $($images[$k - 1].FullName)" }; return }
|
|
76
|
+
Write-Info 'stopping Docker Desktop and WSL...'
|
|
77
|
+
Get-Process -Name 'Docker Desktop' -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
|
|
78
|
+
$null = Invoke-External -FilePath 'wsl.exe' -ArgumentList @('--shutdown') -Destructive -Quiet -Label 'wsl --shutdown'
|
|
79
|
+
Start-Sleep -Seconds 8
|
|
80
|
+
foreach ($k in $picks) {
|
|
81
|
+
$img = $images[$k - 1]
|
|
82
|
+
$before = (Get-Item -LiteralPath $img.FullName).Length
|
|
83
|
+
$script = Join-Path $env:TEMP ("windowsweep-diskpart-" + [guid]::NewGuid().ToString('N') + '.txt')
|
|
84
|
+
[IO.File]::WriteAllLines($script, @("select vdisk file=`"$($img.FullName)`"", 'attach vdisk readonly', 'compact vdisk', 'detach vdisk', 'exit'))
|
|
85
|
+
$r = Invoke-External -FilePath 'diskpart.exe' -ArgumentList @('/s', $script) -Destructive -Quiet -Label "diskpart compact $($img.Name)"
|
|
86
|
+
Remove-Item -LiteralPath $script -Force -ErrorAction SilentlyContinue
|
|
87
|
+
$after = (Get-Item -LiteralPath $img.FullName).Length
|
|
88
|
+
$gain = [math]::Max([long]0, $before - $after)
|
|
89
|
+
if ($r.ExitCode -eq 0) { Write-Ok ("$($img.Name): " + (Format-Bytes $before) + ' -> ' + (Format-Bytes $after) + ' (freed ' + (Format-Bytes $gain) + ')') } else { Write-Warn "$($img.Name): diskpart returned $($r.ExitCode)"; foreach ($l in $r.Output) { Write-Note $l } }
|
|
90
|
+
Add-Freed $gain
|
|
91
|
+
}
|
|
92
|
+
Write-Info 'done - start Docker Desktop again when you need it'
|
|
93
|
+
}
|
package/modules/editors.ps1
CHANGED
|
@@ -1,150 +1,150 @@
|
|
|
1
|
-
# editors.ps1 - section 6: editor caches, stale workspace storage, superseded extension versions.
|
|
2
|
-
|
|
3
|
-
$Script:WS_EDITORS = @(
|
|
4
|
-
@{ Name = 'VS Code'; Root = 'Code'; Ext = '.vscode'; Proc = @('Code') }
|
|
5
|
-
@{ Name = 'VS Code Insiders'; Root = 'Code - Insiders'; Ext = '.vscode-insiders'; Proc = @('Code - Insiders') }
|
|
6
|
-
@{ Name = 'VSCodium'; Root = 'VSCodium'; Ext = '.vscode-oss'; Proc = @('VSCodium') }
|
|
7
|
-
@{ Name = 'Cursor'; Root = 'Cursor'; Ext = '.cursor'; Proc = @('Cursor') }
|
|
8
|
-
@{ Name = 'Windsurf'; Root = 'Windsurf'; Ext = '.windsurf'; Proc = @('Windsurf') }
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
function Get-Targets06 {
|
|
12
|
-
$P = $Script:P
|
|
13
|
-
$t = @()
|
|
14
|
-
foreach ($e in $Script:WS_EDITORS) {
|
|
15
|
-
$root = Join-Path $P.A $e.Root
|
|
16
|
-
$t += (New-Target 6 "$($e.Name) caches" $root -Kind editor -Guard $e.Proc -Note 'Cache, CachedData, Code Cache, GPUCache, crash reports; skipped while the editor runs')
|
|
17
|
-
$t += (New-Target 6 "$($e.Name) VSIX download cache" (Join-Path $root 'CachedExtensionVSIXs') -Mode clear -Note 'extension installers already unpacked; safe while the editor runs')
|
|
18
|
-
$t += (New-Target 6 "$($e.Name) logs" (Join-Path $root 'logs') -Mode clear-old -Days 7)
|
|
19
|
-
$t += (New-Target 6 "$($e.Name) stale workspace storage" (Join-Path $root 'User\workspaceStorage') -Kind cmd -Guard $e.Proc -Note 'entries whose folder no longer exists')
|
|
20
|
-
$t += (New-Target 6 "$($e.Name) superseded extension versions" (Join-Path $P.U "$($e.Ext)\extensions") -Kind cmd -Guard $e.Proc -Note 'older versions and .obsolete entries; newest kept')
|
|
21
|
-
}
|
|
22
|
-
$t += (New-Target 6 'Visual Studio ComponentModelCache' "$($P.L)\Microsoft\VisualStudio\*\ComponentModelCache" -Kind glob -Mode clear -Guard @('devenv'))
|
|
23
|
-
$t += (New-Target 6 'Visual Studio designer shadow cache' "$($P.L)\Microsoft\VisualStudio\*\Designer\ShadowCache" -Kind glob -Mode clear -Guard @('devenv'))
|
|
24
|
-
$t += (New-Target 6 'Visual Studio telemetry (AppInsights)' "$($P.L)\Microsoft\VSApplicationInsights" -Mode clear)
|
|
25
|
-
$t += (New-Target 6 'Visual Studio SQM logs' "$($P.L)\Microsoft\VSCommon\*\SQM" -Kind glob -Mode clear)
|
|
26
|
-
return $t
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function Remove-StaleWorkspaceStorage {
|
|
30
|
-
<# .SYNOPSIS workspaceStorage entries pointing at a folder/workspace file that no longer exists. #>
|
|
31
|
-
param([string] $Root, [string] $Label)
|
|
32
|
-
if (-not (Test-DirPresent $Root)) { return }
|
|
33
|
-
$stale = @()
|
|
34
|
-
foreach ($e in (Get-ChildEntries $Root)) {
|
|
35
|
-
if (-not ($e -is [IO.DirectoryInfo])) { continue }
|
|
36
|
-
$json = Join-Path (Remove-LongPrefix $e.FullName) 'workspace.json'
|
|
37
|
-
if (-not (Test-PathPresent $json)) { continue }
|
|
38
|
-
try {
|
|
39
|
-
$w = Get-Content -LiteralPath $json -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
|
|
40
|
-
$uri = $w.folder
|
|
41
|
-
if (-not $uri) { $uri = $w.workspace }
|
|
42
|
-
if (-not $uri -or $uri -notmatch '^file:///') { continue }
|
|
43
|
-
$local = [Uri]::UnescapeDataString(($uri -replace '^file:///', '')) -replace '/', '\'
|
|
44
|
-
if (-not (Test-PathPresent $local)) { $stale += [pscustomobject]@{ Path = (Remove-LongPrefix $e.FullName); Target = $local } }
|
|
45
|
-
} catch { $null = $_ }
|
|
46
|
-
}
|
|
47
|
-
if ($stale.Count -eq 0) { Write-Info "$Label - none stale"; return }
|
|
48
|
-
Write-Info "$Label - $($stale.Count) entries point at folders that no longer exist:"
|
|
49
|
-
foreach ($s in $stale) { Write-Note " $($s.Target)" }
|
|
50
|
-
foreach ($s in $stale) { $null = Remove-PathSafe -Path $s.Path -Within $Root -Label $Label }
|
|
51
|
-
if (-not $Script:WS.DryRun) { Write-Ok "$Label - removed $($stale.Count) stale entries" }
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function Read-JsonFile {
|
|
55
|
-
<# .SYNOPSIS Parse a JSON file. A top-level array always comes back as an array (PowerShell 5.1's ConvertFrom-Json
|
|
56
|
-
emits an array as ONE pipeline object, which silently turns 16 entries into a single bogus one when wrapped). #>
|
|
57
|
-
param([string] $Path)
|
|
58
|
-
$parsed = Get-Content -LiteralPath $Path -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
|
|
59
|
-
if ($parsed -is [array]) { return $parsed }
|
|
60
|
-
return @($parsed)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function Get-SupersededExtensionPlan {
|
|
64
|
-
<# .SYNOPSIS Decide which extension folders are leftovers. Pure logic (exercised by the self-test).
|
|
65
|
-
A folder is a victim only when the editor's extensions.json does NOT reference it AND either the .obsolete marker
|
|
66
|
-
names it or a different version of the same extension IS referenced. Without extensions.json: .obsolete entries
|
|
67
|
-
plus every version older than the newest one present. #>
|
|
68
|
-
param([string] $Root, [object[]] $Folders)
|
|
69
|
-
$victims = @()
|
|
70
|
-
$obsolete = @{}
|
|
71
|
-
$obsFile = Join-Path $Root '.obsolete'
|
|
72
|
-
if (Test-PathPresent $obsFile) {
|
|
73
|
-
try { $o = Get-Content -LiteralPath $obsFile -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop; foreach ($p in $o.PSObject.Properties) { if ($p.Value) { $obsolete[$p.Name] = $true } } } catch { $null = $_ }
|
|
74
|
-
}
|
|
75
|
-
$installed = $null
|
|
76
|
-
$manifest = Join-Path $Root 'extensions.json'
|
|
77
|
-
if (Test-PathPresent $manifest) {
|
|
78
|
-
try {
|
|
79
|
-
$installed = @{}
|
|
80
|
-
foreach ($entry in (Read-JsonFile $manifest)) {
|
|
81
|
-
$loc = $entry.relativeLocation
|
|
82
|
-
if ($loc -is [string] -and $loc) { $installed[$loc] = $true }
|
|
83
|
-
}
|
|
84
|
-
} catch { $installed = $null }
|
|
85
|
-
}
|
|
86
|
-
if ($null -ne $installed -and $installed.Count -gt 0) {
|
|
87
|
-
$installedIds = @{}
|
|
88
|
-
foreach ($f in $Folders) { if ($installed.ContainsKey($f.Name)) { $installedIds[$f.Id] = $true } }
|
|
89
|
-
foreach ($f in $Folders) {
|
|
90
|
-
if ($installed.ContainsKey($f.Name)) { continue }
|
|
91
|
-
if ($obsolete.ContainsKey($f.Name) -or $installedIds.ContainsKey($f.Id)) { $victims += $f }
|
|
92
|
-
}
|
|
93
|
-
$note = "extensions.json lists $($installed.Count) installed extension(s); $($victims.Count) leftover folder(s)"
|
|
94
|
-
} else {
|
|
95
|
-
foreach ($group in @($Folders | Where-Object { -not $obsolete.ContainsKey($_.Name) } | Group-Object Id)) {
|
|
96
|
-
if ($group.Count -lt 2) { continue }
|
|
97
|
-
$newest = $group.Group | Sort-Object Version -Descending | Select-Object -First 1
|
|
98
|
-
foreach ($v in $group.Group) { if ($v.Path -ne $newest.Path) { $victims += $v } }
|
|
99
|
-
}
|
|
100
|
-
foreach ($f in $Folders) { if ($obsolete.ContainsKey($f.Name)) { $victims += $f } }
|
|
101
|
-
$note = "no extensions.json; using .obsolete + newest-version rule: $($victims.Count) leftover folder(s)"
|
|
102
|
-
}
|
|
103
|
-
return [pscustomobject]@{ Victims = @($victims); Note = $note }
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function Remove-SupersededExtensions {
|
|
107
|
-
<# .SYNOPSIS Extension folders the editor no longer uses.
|
|
108
|
-
The editor's own extensions.json is the source of truth: every versioned folder it does not reference is a leftover
|
|
109
|
-
(uninstalled, or superseded by the version it does reference). Without that file, fall back to the .obsolete marker
|
|
110
|
-
plus keep-the-newest-version-per-extension. #>
|
|
111
|
-
param([string] $Root, [string] $Label)
|
|
112
|
-
if (-not (Test-DirPresent $Root)) { return }
|
|
113
|
-
$folders = @()
|
|
114
|
-
foreach ($e in @(Get-ChildEntries $Root)) {
|
|
115
|
-
if (-not ($e -is [IO.DirectoryInfo]) -or (Test-ReparsePoint $e)) { continue }
|
|
116
|
-
if ($e.Name -match '^(.+?)-(\d+\.\d+\.\d+)([^\\]*)$') {
|
|
117
|
-
$ver = $null
|
|
118
|
-
try { $ver = [version]$Matches[2] } catch { continue }
|
|
119
|
-
$folders += [pscustomobject]@{ Name = $e.Name; Id = $Matches[1]; Path = (Remove-LongPrefix $e.FullName); Version = $ver }
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
if ($folders.Count -eq 0) { Write-Info "$Label - none superseded"; return }
|
|
123
|
-
$plan = Get-SupersededExtensionPlan -Root $Root -Folders $folders
|
|
124
|
-
Write-Note "$Label - $($plan.Note)"
|
|
125
|
-
$victims = @($plan.Victims)
|
|
126
|
-
if ($victims.Count -eq 0) { Write-Info "$Label - none superseded"; return }
|
|
127
|
-
foreach ($v in $victims) {
|
|
128
|
-
$r = Remove-PathSafe -Path $v.Path -Within $Root -Label $Label
|
|
129
|
-
if ($r.Removed -and -not $Script:WS.DryRun) { Write-Ok ("$Label - removed $($v.Name) (" + (Format-Bytes $r.Bytes) + ')') }
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function Invoke-Section06 {
|
|
134
|
-
Write-SectionIntro @(
|
|
135
|
-
'Editors rebuild their caches on the next start. Settings, keybindings, snippets, globalStorage and local',
|
|
136
|
-
'History are protected. A running editor is left alone except for its VSIX download cache and old logs.'
|
|
137
|
-
)
|
|
138
|
-
$targets = Get-Targets06
|
|
139
|
-
$null = Show-TargetSizes $targets
|
|
140
|
-
if (-not (Confirm-Section 'Clean editor caches now?')) { Write-Info 'skipped'; return }
|
|
141
|
-
$null = Invoke-TargetList @($targets | Where-Object { $_.Kind -ne 'cmd' })
|
|
142
|
-
foreach ($e in $Script:WS_EDITORS) {
|
|
143
|
-
$root = Join-Path $Script:P.A $e.Root
|
|
144
|
-
$extRoot = Join-Path $Script:P.U "$($e.Ext)\extensions"
|
|
145
|
-
if (-not (Test-DirPresent $root) -and -not (Test-DirPresent $extRoot)) { continue }
|
|
146
|
-
if (Test-ProcessRunning $e.Proc) { Write-Warn "$($e.Name) is running - workspace storage and extension versions left alone"; continue }
|
|
147
|
-
Remove-StaleWorkspaceStorage -Root (Join-Path $root 'User\workspaceStorage') -Label "$($e.Name) workspace storage"
|
|
148
|
-
Remove-SupersededExtensions -Root $extRoot -Label "$($e.Name) extensions"
|
|
149
|
-
}
|
|
150
|
-
}
|
|
1
|
+
# editors.ps1 - section 6: editor caches, stale workspace storage, superseded extension versions.
|
|
2
|
+
|
|
3
|
+
$Script:WS_EDITORS = @(
|
|
4
|
+
@{ Name = 'VS Code'; Root = 'Code'; Ext = '.vscode'; Proc = @('Code') }
|
|
5
|
+
@{ Name = 'VS Code Insiders'; Root = 'Code - Insiders'; Ext = '.vscode-insiders'; Proc = @('Code - Insiders') }
|
|
6
|
+
@{ Name = 'VSCodium'; Root = 'VSCodium'; Ext = '.vscode-oss'; Proc = @('VSCodium') }
|
|
7
|
+
@{ Name = 'Cursor'; Root = 'Cursor'; Ext = '.cursor'; Proc = @('Cursor') }
|
|
8
|
+
@{ Name = 'Windsurf'; Root = 'Windsurf'; Ext = '.windsurf'; Proc = @('Windsurf') }
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
function Get-Targets06 {
|
|
12
|
+
$P = $Script:P
|
|
13
|
+
$t = @()
|
|
14
|
+
foreach ($e in $Script:WS_EDITORS) {
|
|
15
|
+
$root = Join-Path $P.A $e.Root
|
|
16
|
+
$t += (New-Target 6 "$($e.Name) caches" $root -Kind editor -Guard $e.Proc -Note 'Cache, CachedData, Code Cache, GPUCache, crash reports; skipped while the editor runs')
|
|
17
|
+
$t += (New-Target 6 "$($e.Name) VSIX download cache" (Join-Path $root 'CachedExtensionVSIXs') -Mode clear -Note 'extension installers already unpacked; safe while the editor runs')
|
|
18
|
+
$t += (New-Target 6 "$($e.Name) logs" (Join-Path $root 'logs') -Mode clear-old -Days 7)
|
|
19
|
+
$t += (New-Target 6 "$($e.Name) stale workspace storage" (Join-Path $root 'User\workspaceStorage') -Kind cmd -Guard $e.Proc -Note 'entries whose folder no longer exists')
|
|
20
|
+
$t += (New-Target 6 "$($e.Name) superseded extension versions" (Join-Path $P.U "$($e.Ext)\extensions") -Kind cmd -Guard $e.Proc -Note 'older versions and .obsolete entries; newest kept')
|
|
21
|
+
}
|
|
22
|
+
$t += (New-Target 6 'Visual Studio ComponentModelCache' "$($P.L)\Microsoft\VisualStudio\*\ComponentModelCache" -Kind glob -Mode clear -Guard @('devenv'))
|
|
23
|
+
$t += (New-Target 6 'Visual Studio designer shadow cache' "$($P.L)\Microsoft\VisualStudio\*\Designer\ShadowCache" -Kind glob -Mode clear -Guard @('devenv'))
|
|
24
|
+
$t += (New-Target 6 'Visual Studio telemetry (AppInsights)' "$($P.L)\Microsoft\VSApplicationInsights" -Mode clear)
|
|
25
|
+
$t += (New-Target 6 'Visual Studio SQM logs' "$($P.L)\Microsoft\VSCommon\*\SQM" -Kind glob -Mode clear)
|
|
26
|
+
return $t
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function Remove-StaleWorkspaceStorage {
|
|
30
|
+
<# .SYNOPSIS workspaceStorage entries pointing at a folder/workspace file that no longer exists. #>
|
|
31
|
+
param([string] $Root, [string] $Label)
|
|
32
|
+
if (-not (Test-DirPresent $Root)) { return }
|
|
33
|
+
$stale = @()
|
|
34
|
+
foreach ($e in (Get-ChildEntries $Root)) {
|
|
35
|
+
if (-not ($e -is [IO.DirectoryInfo])) { continue }
|
|
36
|
+
$json = Join-Path (Remove-LongPrefix $e.FullName) 'workspace.json'
|
|
37
|
+
if (-not (Test-PathPresent $json)) { continue }
|
|
38
|
+
try {
|
|
39
|
+
$w = Get-Content -LiteralPath $json -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
|
|
40
|
+
$uri = $w.folder
|
|
41
|
+
if (-not $uri) { $uri = $w.workspace }
|
|
42
|
+
if (-not $uri -or $uri -notmatch '^file:///') { continue }
|
|
43
|
+
$local = [Uri]::UnescapeDataString(($uri -replace '^file:///', '')) -replace '/', '\'
|
|
44
|
+
if (-not (Test-PathPresent $local)) { $stale += [pscustomobject]@{ Path = (Remove-LongPrefix $e.FullName); Target = $local } }
|
|
45
|
+
} catch { $null = $_ }
|
|
46
|
+
}
|
|
47
|
+
if ($stale.Count -eq 0) { Write-Info "$Label - none stale"; return }
|
|
48
|
+
Write-Info "$Label - $($stale.Count) entries point at folders that no longer exist:"
|
|
49
|
+
foreach ($s in $stale) { Write-Note " $($s.Target)" }
|
|
50
|
+
foreach ($s in $stale) { $null = Remove-PathSafe -Path $s.Path -Within $Root -Label $Label }
|
|
51
|
+
if (-not $Script:WS.DryRun) { Write-Ok "$Label - removed $($stale.Count) stale entries" }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function Read-JsonFile {
|
|
55
|
+
<# .SYNOPSIS Parse a JSON file. A top-level array always comes back as an array (PowerShell 5.1's ConvertFrom-Json
|
|
56
|
+
emits an array as ONE pipeline object, which silently turns 16 entries into a single bogus one when wrapped). #>
|
|
57
|
+
param([string] $Path)
|
|
58
|
+
$parsed = Get-Content -LiteralPath $Path -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
|
|
59
|
+
if ($parsed -is [array]) { return $parsed }
|
|
60
|
+
return @($parsed)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function Get-SupersededExtensionPlan {
|
|
64
|
+
<# .SYNOPSIS Decide which extension folders are leftovers. Pure logic (exercised by the self-test).
|
|
65
|
+
A folder is a victim only when the editor's extensions.json does NOT reference it AND either the .obsolete marker
|
|
66
|
+
names it or a different version of the same extension IS referenced. Without extensions.json: .obsolete entries
|
|
67
|
+
plus every version older than the newest one present. #>
|
|
68
|
+
param([string] $Root, [object[]] $Folders)
|
|
69
|
+
$victims = @()
|
|
70
|
+
$obsolete = @{}
|
|
71
|
+
$obsFile = Join-Path $Root '.obsolete'
|
|
72
|
+
if (Test-PathPresent $obsFile) {
|
|
73
|
+
try { $o = Get-Content -LiteralPath $obsFile -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop; foreach ($p in $o.PSObject.Properties) { if ($p.Value) { $obsolete[$p.Name] = $true } } } catch { $null = $_ }
|
|
74
|
+
}
|
|
75
|
+
$installed = $null
|
|
76
|
+
$manifest = Join-Path $Root 'extensions.json'
|
|
77
|
+
if (Test-PathPresent $manifest) {
|
|
78
|
+
try {
|
|
79
|
+
$installed = @{}
|
|
80
|
+
foreach ($entry in (Read-JsonFile $manifest)) {
|
|
81
|
+
$loc = $entry.relativeLocation
|
|
82
|
+
if ($loc -is [string] -and $loc) { $installed[$loc] = $true }
|
|
83
|
+
}
|
|
84
|
+
} catch { $installed = $null }
|
|
85
|
+
}
|
|
86
|
+
if ($null -ne $installed -and $installed.Count -gt 0) {
|
|
87
|
+
$installedIds = @{}
|
|
88
|
+
foreach ($f in $Folders) { if ($installed.ContainsKey($f.Name)) { $installedIds[$f.Id] = $true } }
|
|
89
|
+
foreach ($f in $Folders) {
|
|
90
|
+
if ($installed.ContainsKey($f.Name)) { continue }
|
|
91
|
+
if ($obsolete.ContainsKey($f.Name) -or $installedIds.ContainsKey($f.Id)) { $victims += $f }
|
|
92
|
+
}
|
|
93
|
+
$note = "extensions.json lists $($installed.Count) installed extension(s); $($victims.Count) leftover folder(s)"
|
|
94
|
+
} else {
|
|
95
|
+
foreach ($group in @($Folders | Where-Object { -not $obsolete.ContainsKey($_.Name) } | Group-Object Id)) {
|
|
96
|
+
if ($group.Count -lt 2) { continue }
|
|
97
|
+
$newest = $group.Group | Sort-Object Version -Descending | Select-Object -First 1
|
|
98
|
+
foreach ($v in $group.Group) { if ($v.Path -ne $newest.Path) { $victims += $v } }
|
|
99
|
+
}
|
|
100
|
+
foreach ($f in $Folders) { if ($obsolete.ContainsKey($f.Name)) { $victims += $f } }
|
|
101
|
+
$note = "no extensions.json; using .obsolete + newest-version rule: $($victims.Count) leftover folder(s)"
|
|
102
|
+
}
|
|
103
|
+
return [pscustomobject]@{ Victims = @($victims); Note = $note }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function Remove-SupersededExtensions {
|
|
107
|
+
<# .SYNOPSIS Extension folders the editor no longer uses.
|
|
108
|
+
The editor's own extensions.json is the source of truth: every versioned folder it does not reference is a leftover
|
|
109
|
+
(uninstalled, or superseded by the version it does reference). Without that file, fall back to the .obsolete marker
|
|
110
|
+
plus keep-the-newest-version-per-extension. #>
|
|
111
|
+
param([string] $Root, [string] $Label)
|
|
112
|
+
if (-not (Test-DirPresent $Root)) { return }
|
|
113
|
+
$folders = @()
|
|
114
|
+
foreach ($e in @(Get-ChildEntries $Root)) {
|
|
115
|
+
if (-not ($e -is [IO.DirectoryInfo]) -or (Test-ReparsePoint $e)) { continue }
|
|
116
|
+
if ($e.Name -match '^(.+?)-(\d+\.\d+\.\d+)([^\\]*)$') {
|
|
117
|
+
$ver = $null
|
|
118
|
+
try { $ver = [version]$Matches[2] } catch { continue }
|
|
119
|
+
$folders += [pscustomobject]@{ Name = $e.Name; Id = $Matches[1]; Path = (Remove-LongPrefix $e.FullName); Version = $ver }
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if ($folders.Count -eq 0) { Write-Info "$Label - none superseded"; return }
|
|
123
|
+
$plan = Get-SupersededExtensionPlan -Root $Root -Folders $folders
|
|
124
|
+
Write-Note "$Label - $($plan.Note)"
|
|
125
|
+
$victims = @($plan.Victims)
|
|
126
|
+
if ($victims.Count -eq 0) { Write-Info "$Label - none superseded"; return }
|
|
127
|
+
foreach ($v in $victims) {
|
|
128
|
+
$r = Remove-PathSafe -Path $v.Path -Within $Root -Label $Label
|
|
129
|
+
if ($r.Removed -and -not $Script:WS.DryRun) { Write-Ok ("$Label - removed $($v.Name) (" + (Format-Bytes $r.Bytes) + ')') }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function Invoke-Section06 {
|
|
134
|
+
Write-SectionIntro @(
|
|
135
|
+
'Editors rebuild their caches on the next start. Settings, keybindings, snippets, globalStorage and local',
|
|
136
|
+
'History are protected. A running editor is left alone except for its VSIX download cache and old logs.'
|
|
137
|
+
)
|
|
138
|
+
$targets = Get-Targets06
|
|
139
|
+
$null = Show-TargetSizes $targets
|
|
140
|
+
if (-not (Confirm-Section 'Clean editor caches now?')) { Write-Info 'skipped'; return }
|
|
141
|
+
$null = Invoke-TargetList @($targets | Where-Object { $_.Kind -ne 'cmd' })
|
|
142
|
+
foreach ($e in $Script:WS_EDITORS) {
|
|
143
|
+
$root = Join-Path $Script:P.A $e.Root
|
|
144
|
+
$extRoot = Join-Path $Script:P.U "$($e.Ext)\extensions"
|
|
145
|
+
if (-not (Test-DirPresent $root) -and -not (Test-DirPresent $extRoot)) { continue }
|
|
146
|
+
if (Test-ProcessRunning $e.Proc) { Write-Warn "$($e.Name) is running - workspace storage and extension versions left alone"; continue }
|
|
147
|
+
Remove-StaleWorkspaceStorage -Root (Join-Path $root 'User\workspaceStorage') -Label "$($e.Name) workspace storage"
|
|
148
|
+
Remove-SupersededExtensions -Root $extRoot -Label "$($e.Name) extensions"
|
|
149
|
+
}
|
|
150
|
+
}
|