windowsweep 1.0.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/CHANGELOG.md +44 -0
- package/LICENSE +21 -0
- package/README.md +482 -0
- package/SECURITY.md +46 -0
- package/VERSION +1 -0
- package/bin/windowsweep.js +83 -0
- package/lib/actions.ps1 +209 -0
- package/lib/config.ps1 +90 -0
- package/lib/constants.ps1 +89 -0
- package/lib/fs.ps1 +180 -0
- package/lib/log.ps1 +140 -0
- package/lib/safety.ps1 +493 -0
- package/lib/scan.ps1 +94 -0
- package/lib/ui.ps1 +232 -0
- package/modules/android_avd.ps1 +36 -0
- package/modules/app_caches.ps1 +58 -0
- package/modules/browsers.ps1 +33 -0
- package/modules/build_tools.ps1 +35 -0
- package/modules/crash_trap.ps1 +89 -0
- package/modules/disk_usage.ps1 +49 -0
- package/modules/docker.ps1 +93 -0
- package/modules/editors.ps1 +149 -0
- package/modules/health.ps1 +101 -0
- package/modules/menu.ps1 +61 -0
- package/modules/personal.ps1 +124 -0
- package/modules/pkg_managers.ps1 +69 -0
- package/modules/projects.ps1 +140 -0
- package/modules/release_helpers.ps1 +414 -0
- package/modules/reports.ps1 +212 -0
- package/modules/runner.ps1 +190 -0
- package/modules/system_admin.ps1 +220 -0
- package/modules/test_runners.ps1 +24 -0
- package/modules/walkthrough.ps1 +79 -0
- package/modules/windows_user.ps1 +112 -0
- package/package.json +73 -0
- package/windowsweep.cmd +6 -0
- package/windowsweep.ps1 +292 -0
|
@@ -0,0 +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 }
|
|
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
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
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, VSIX cache, crash reports')
|
|
17
|
+
$t += (New-Target 6 "$($e.Name) logs" (Join-Path $root 'logs') -Mode clear-old -Days 7)
|
|
18
|
+
$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')
|
|
19
|
+
$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')
|
|
20
|
+
}
|
|
21
|
+
$t += (New-Target 6 'Visual Studio ComponentModelCache' "$($P.L)\Microsoft\VisualStudio\*\ComponentModelCache" -Kind glob -Mode clear -Guard @('devenv'))
|
|
22
|
+
$t += (New-Target 6 'Visual Studio designer shadow cache' "$($P.L)\Microsoft\VisualStudio\*\Designer\ShadowCache" -Kind glob -Mode clear -Guard @('devenv'))
|
|
23
|
+
$t += (New-Target 6 'Visual Studio telemetry (AppInsights)' "$($P.L)\Microsoft\VSApplicationInsights" -Mode clear)
|
|
24
|
+
$t += (New-Target 6 'Visual Studio SQM logs' "$($P.L)\Microsoft\VSCommon\*\SQM" -Kind glob -Mode clear)
|
|
25
|
+
return $t
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function Remove-StaleWorkspaceStorage {
|
|
29
|
+
<# .SYNOPSIS workspaceStorage entries pointing at a folder/workspace file that no longer exists. #>
|
|
30
|
+
param([string] $Root, [string] $Label)
|
|
31
|
+
if (-not (Test-DirPresent $Root)) { return }
|
|
32
|
+
$stale = @()
|
|
33
|
+
foreach ($e in (Get-ChildEntries $Root)) {
|
|
34
|
+
if (-not ($e -is [IO.DirectoryInfo])) { continue }
|
|
35
|
+
$json = Join-Path (Remove-LongPrefix $e.FullName) 'workspace.json'
|
|
36
|
+
if (-not (Test-PathPresent $json)) { continue }
|
|
37
|
+
try {
|
|
38
|
+
$w = Get-Content -LiteralPath $json -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
|
|
39
|
+
$uri = $w.folder
|
|
40
|
+
if (-not $uri) { $uri = $w.workspace }
|
|
41
|
+
if (-not $uri -or $uri -notmatch '^file:///') { continue }
|
|
42
|
+
$local = [Uri]::UnescapeDataString(($uri -replace '^file:///', '')) -replace '/', '\'
|
|
43
|
+
if (-not (Test-PathPresent $local)) { $stale += [pscustomobject]@{ Path = (Remove-LongPrefix $e.FullName); Target = $local } }
|
|
44
|
+
} catch { $null = $_ }
|
|
45
|
+
}
|
|
46
|
+
if ($stale.Count -eq 0) { Write-Info "$Label - none stale"; return }
|
|
47
|
+
Write-Info "$Label - $($stale.Count) entries point at folders that no longer exist:"
|
|
48
|
+
foreach ($s in $stale) { Write-Note " $($s.Target)" }
|
|
49
|
+
foreach ($s in $stale) { $null = Remove-PathSafe -Path $s.Path -Within $Root -Label $Label }
|
|
50
|
+
if (-not $Script:WS.DryRun) { Write-Ok "$Label - removed $($stale.Count) stale entries" }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function Read-JsonFile {
|
|
54
|
+
<# .SYNOPSIS Parse a JSON file. A top-level array always comes back as an array (PowerShell 5.1's ConvertFrom-Json
|
|
55
|
+
emits an array as ONE pipeline object, which silently turns 16 entries into a single bogus one when wrapped). #>
|
|
56
|
+
param([string] $Path)
|
|
57
|
+
$parsed = Get-Content -LiteralPath $Path -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
|
|
58
|
+
if ($parsed -is [array]) { return $parsed }
|
|
59
|
+
return @($parsed)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function Get-SupersededExtensionPlan {
|
|
63
|
+
<# .SYNOPSIS Decide which extension folders are leftovers. Pure logic (exercised by the self-test).
|
|
64
|
+
A folder is a victim only when the editor's extensions.json does NOT reference it AND either the .obsolete marker
|
|
65
|
+
names it or a different version of the same extension IS referenced. Without extensions.json: .obsolete entries
|
|
66
|
+
plus every version older than the newest one present. #>
|
|
67
|
+
param([string] $Root, [object[]] $Folders)
|
|
68
|
+
$victims = @()
|
|
69
|
+
$obsolete = @{}
|
|
70
|
+
$obsFile = Join-Path $Root '.obsolete'
|
|
71
|
+
if (Test-PathPresent $obsFile) {
|
|
72
|
+
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 = $_ }
|
|
73
|
+
}
|
|
74
|
+
$installed = $null
|
|
75
|
+
$manifest = Join-Path $Root 'extensions.json'
|
|
76
|
+
if (Test-PathPresent $manifest) {
|
|
77
|
+
try {
|
|
78
|
+
$installed = @{}
|
|
79
|
+
foreach ($entry in (Read-JsonFile $manifest)) {
|
|
80
|
+
$loc = $entry.relativeLocation
|
|
81
|
+
if ($loc -is [string] -and $loc) { $installed[$loc] = $true }
|
|
82
|
+
}
|
|
83
|
+
} catch { $installed = $null }
|
|
84
|
+
}
|
|
85
|
+
if ($null -ne $installed -and $installed.Count -gt 0) {
|
|
86
|
+
$installedIds = @{}
|
|
87
|
+
foreach ($f in $Folders) { if ($installed.ContainsKey($f.Name)) { $installedIds[$f.Id] = $true } }
|
|
88
|
+
foreach ($f in $Folders) {
|
|
89
|
+
if ($installed.ContainsKey($f.Name)) { continue }
|
|
90
|
+
if ($obsolete.ContainsKey($f.Name) -or $installedIds.ContainsKey($f.Id)) { $victims += $f }
|
|
91
|
+
}
|
|
92
|
+
$note = "extensions.json lists $($installed.Count) installed extension(s); $($victims.Count) leftover folder(s)"
|
|
93
|
+
} else {
|
|
94
|
+
foreach ($group in @($Folders | Where-Object { -not $obsolete.ContainsKey($_.Name) } | Group-Object Id)) {
|
|
95
|
+
if ($group.Count -lt 2) { continue }
|
|
96
|
+
$newest = $group.Group | Sort-Object Version -Descending | Select-Object -First 1
|
|
97
|
+
foreach ($v in $group.Group) { if ($v.Path -ne $newest.Path) { $victims += $v } }
|
|
98
|
+
}
|
|
99
|
+
foreach ($f in $Folders) { if ($obsolete.ContainsKey($f.Name)) { $victims += $f } }
|
|
100
|
+
$note = "no extensions.json; using .obsolete + newest-version rule: $($victims.Count) leftover folder(s)"
|
|
101
|
+
}
|
|
102
|
+
return [pscustomobject]@{ Victims = @($victims); Note = $note }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function Remove-SupersededExtensions {
|
|
106
|
+
<# .SYNOPSIS Extension folders the editor no longer uses.
|
|
107
|
+
The editor's own extensions.json is the source of truth: every versioned folder it does not reference is a leftover
|
|
108
|
+
(uninstalled, or superseded by the version it does reference). Without that file, fall back to the .obsolete marker
|
|
109
|
+
plus keep-the-newest-version-per-extension. #>
|
|
110
|
+
param([string] $Root, [string] $Label)
|
|
111
|
+
if (-not (Test-DirPresent $Root)) { return }
|
|
112
|
+
$folders = @()
|
|
113
|
+
foreach ($e in @(Get-ChildEntries $Root)) {
|
|
114
|
+
if (-not ($e -is [IO.DirectoryInfo]) -or (Test-ReparsePoint $e)) { continue }
|
|
115
|
+
if ($e.Name -match '^(.+?)-(\d+\.\d+\.\d+)([^\\]*)$') {
|
|
116
|
+
$ver = $null
|
|
117
|
+
try { $ver = [version]$Matches[2] } catch { continue }
|
|
118
|
+
$folders += [pscustomobject]@{ Name = $e.Name; Id = $Matches[1]; Path = (Remove-LongPrefix $e.FullName); Version = $ver }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if ($folders.Count -eq 0) { Write-Info "$Label - none superseded"; return }
|
|
122
|
+
$plan = Get-SupersededExtensionPlan -Root $Root -Folders $folders
|
|
123
|
+
Write-Note "$Label - $($plan.Note)"
|
|
124
|
+
$victims = @($plan.Victims)
|
|
125
|
+
if ($victims.Count -eq 0) { Write-Info "$Label - none superseded"; return }
|
|
126
|
+
foreach ($v in $victims) {
|
|
127
|
+
$r = Remove-PathSafe -Path $v.Path -Within $Root -Label $Label
|
|
128
|
+
if ($r.Removed -and -not $Script:WS.DryRun) { Write-Ok ("$Label - removed $($v.Name) (" + (Format-Bytes $r.Bytes) + ')') }
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function Invoke-Section06 {
|
|
133
|
+
Write-SectionIntro @(
|
|
134
|
+
'Editors rebuild their caches on the next start. Settings, keybindings, snippets, globalStorage and local',
|
|
135
|
+
'History are protected. A running editor is left alone except for its VSIX download cache and old logs.'
|
|
136
|
+
)
|
|
137
|
+
$targets = Get-Targets06
|
|
138
|
+
$null = Show-TargetSizes $targets
|
|
139
|
+
if (-not (Confirm-Section 'Clean editor caches now?')) { Write-Info 'skipped'; return }
|
|
140
|
+
$null = Invoke-TargetList @($targets | Where-Object { $_.Kind -ne 'cmd' })
|
|
141
|
+
foreach ($e in $Script:WS_EDITORS) {
|
|
142
|
+
$root = Join-Path $Script:P.A $e.Root
|
|
143
|
+
$extRoot = Join-Path $Script:P.U "$($e.Ext)\extensions"
|
|
144
|
+
if (-not (Test-DirPresent $root) -and -not (Test-DirPresent $extRoot)) { continue }
|
|
145
|
+
if (Test-ProcessRunning $e.Proc) { Write-Warn "$($e.Name) is running - workspace storage and extension versions left alone"; continue }
|
|
146
|
+
Remove-StaleWorkspaceStorage -Root (Join-Path $root 'User\workspaceStorage') -Label "$($e.Name) workspace storage"
|
|
147
|
+
Remove-SupersededExtensions -Root $extRoot -Label "$($e.Name) extensions"
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# health.ps1 - section 0: read-only system health report.
|
|
2
|
+
|
|
3
|
+
function Get-Targets00 { return @() }
|
|
4
|
+
|
|
5
|
+
function Get-HiberfilInfo {
|
|
6
|
+
$p = Join-Path $Script:P.SD '\hiberfil.sys'
|
|
7
|
+
$size = [long]0
|
|
8
|
+
# Get-Item cannot open the hibernation file without elevation, but FileInfo reads its size from the directory entry.
|
|
9
|
+
try { $f = New-Object IO.FileInfo($p); if ($f.Exists) { $size = [long]$f.Length } } catch { $size = 0 }
|
|
10
|
+
$state = 'unknown'
|
|
11
|
+
try {
|
|
12
|
+
$a = (& powercfg.exe /a 2>$null) -join ' '
|
|
13
|
+
if ($a -match 'Hibernate' -and $a -notmatch 'Hibernation has not been enabled') { $state = 'available' } else { $state = 'off' }
|
|
14
|
+
} catch { $null = $_ }
|
|
15
|
+
return [pscustomobject]@{ Path = $p; Bytes = $size; State = $state }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function Get-DiskImageFiles {
|
|
19
|
+
<# .SYNOPSIS Docker Desktop / WSL virtual disks (they grow but never shrink on their own). #>
|
|
20
|
+
$L = $Script:P.L
|
|
21
|
+
$out = @()
|
|
22
|
+
foreach ($root in @((Join-Path $L 'Docker\wsl'), (Join-Path $L 'Packages'))) {
|
|
23
|
+
if (-not (Test-DirPresent $root)) { continue }
|
|
24
|
+
try { $out += Get-ChildItem -LiteralPath $root -Recurse -Filter '*.vhdx' -File -Depth 4 -ErrorAction SilentlyContinue } catch { $null = $_ }
|
|
25
|
+
}
|
|
26
|
+
return $out
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function Get-BlockingApps {
|
|
30
|
+
<# .SYNOPSIS Running apps whose caches this tool skips while they run. #>
|
|
31
|
+
$map = [ordered]@{ chrome = 'Chrome'; msedge = 'Edge'; brave = 'Brave'; firefox = 'Firefox'; vivaldi = 'Vivaldi'; opera = 'Opera'; Code = 'VS Code'; Cursor = 'Cursor'; Windsurf = 'Windsurf'; Discord = 'Discord'; slack = 'Slack'; 'ms-teams' = 'Teams'; Zoom = 'Zoom'; Spotify = 'Spotify'; Postman = 'Postman'; Figma = 'Figma'; Notion = 'Notion' }
|
|
32
|
+
$out = @()
|
|
33
|
+
foreach ($k in $map.Keys) { if (Test-ProcessRunning @($k)) { $out += $map[$k] } }
|
|
34
|
+
return $out
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function Invoke-Section00 {
|
|
38
|
+
$ws = $Script:WS
|
|
39
|
+
Write-SectionIntro @('Read-only snapshot of the machine: nothing is changed here.')
|
|
40
|
+
try {
|
|
41
|
+
$os = Get-CimInstance Win32_OperatingSystem -ErrorAction Stop
|
|
42
|
+
Write-Kv 'Windows:' "$($os.Caption) $($os.Version) (build $($os.BuildNumber))"
|
|
43
|
+
$up = (Get-Date) - $os.LastBootUpTime
|
|
44
|
+
Write-Kv 'Uptime:' ("{0}d {1}h {2}m" -f $up.Days, $up.Hours, $up.Minutes)
|
|
45
|
+
Write-Kv 'RAM:' ("{0} total, {1} free" -f (Format-Bytes ($os.TotalVisibleMemorySize * 1KB)), (Format-Bytes ($os.FreePhysicalMemory * 1KB)))
|
|
46
|
+
} catch { Write-Kv 'Windows:' $ws.OsCaption }
|
|
47
|
+
Write-Kv 'PowerShell:' "$($PSVersionTable.PSVersion) ($($PSVersionTable.PSEdition))"
|
|
48
|
+
$adm = 'no'
|
|
49
|
+
if ($ws.IsAdmin) { $adm = 'yes' } elseif (Test-CanElevate) { $adm = 'no (your account can elevate: --elevate)' } else { $adm = 'no (standard user; admin sections need an administrator)' }
|
|
50
|
+
Write-Kv 'Elevated:' $adm
|
|
51
|
+
$dev = 'not decided yet'
|
|
52
|
+
if ($null -ne $ws.Developer) { if ($ws.Developer) { $dev = "on ($($ws.DeveloperSource))" } else { $dev = "off ($($ws.DeveloperSource))" } }
|
|
53
|
+
Write-Kv 'Developer mode:' $dev
|
|
54
|
+
$hints = @(Get-ToolchainHints)
|
|
55
|
+
if ($hints.Count -gt 0) { Write-Kv 'Dev tooling found:' ($hints -join ', ') }
|
|
56
|
+
Write-UiLine '' 'Gray'
|
|
57
|
+
Show-DriveTable
|
|
58
|
+
try {
|
|
59
|
+
$sys = New-Object IO.DriveInfo($Script:P.SD)
|
|
60
|
+
$pct = 100.0 * $sys.AvailableFreeSpace / $sys.TotalSize
|
|
61
|
+
if ($sys.AvailableFreeSpace -lt 10GB -or $pct -lt 10) { Write-Warn ("system drive has only {0} free ({1:N1}%) - Windows slows down badly below ~10%; this run should help" -f (Format-Bytes $sys.AvailableFreeSpace), $pct) }
|
|
62
|
+
} catch { $null = $_ }
|
|
63
|
+
Write-UiLine '' 'Gray'
|
|
64
|
+
$h = Get-HiberfilInfo
|
|
65
|
+
if ($h.Bytes -gt 0) { Write-Kv 'hiberfil.sys:' ("{0} (hibernation {1}) - section 15 can shrink or remove it (admin)" -f (Format-Bytes $h.Bytes), $h.State) } else { Write-Kv 'hiberfil.sys:' 'absent (hibernation off)' }
|
|
66
|
+
try {
|
|
67
|
+
foreach ($pf in (Get-CimInstance Win32_PageFileUsage -ErrorAction Stop)) { Write-Kv 'pagefile:' ("{0} {1} allocated" -f $pf.Name, (Format-Bytes ($pf.AllocatedBaseSize * 1MB))) }
|
|
68
|
+
} catch { $null = $_ }
|
|
69
|
+
foreach ($img in (Get-DiskImageFiles)) { Write-Kv 'disk image:' ("{0} {1} (section 20 compacts it, admin)" -f $img.FullName, (Format-Bytes $img.Length)) }
|
|
70
|
+
if (Test-CommandPresent 'wsl.exe') {
|
|
71
|
+
try {
|
|
72
|
+
$raw = & wsl.exe --list --verbose 2>$null
|
|
73
|
+
$txt = (($raw | ForEach-Object { [string]$_ }) -join "`n") -replace "`0", ''
|
|
74
|
+
$lines = @($txt -split "`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -and $_ -notmatch '^NAME' })
|
|
75
|
+
if ($lines.Count -gt 0) { Write-Kv 'WSL distros:' (($lines | ForEach-Object { ($_ -replace '\s+', ' ') }) -join ' | ') }
|
|
76
|
+
} catch { $null = $_ }
|
|
77
|
+
}
|
|
78
|
+
Write-UiLine '' 'Gray'
|
|
79
|
+
try { $n = @(Get-CimInstance Win32_StartupCommand -ErrorAction Stop).Count; Write-Kv 'Startup items:' "$n (review in Task Manager > Startup; this tool does not change them)" } catch { $null = $_ }
|
|
80
|
+
try {
|
|
81
|
+
$ss = (Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\StorageSense\Parameters\StoragePolicy' -ErrorAction Stop).'01'
|
|
82
|
+
$ssText = 'off'
|
|
83
|
+
if ($ss -eq 1) { $ssText = 'on' }
|
|
84
|
+
Write-Kv 'Storage Sense:' $ssText
|
|
85
|
+
} catch { $null = $_ }
|
|
86
|
+
try {
|
|
87
|
+
$la = (& fsutil.exe behavior query disablelastaccess 2>$null) -join ' '
|
|
88
|
+
if ($la -match '=\s*(\d)') {
|
|
89
|
+
$v = [int]$Matches[1]
|
|
90
|
+
$laText = 'disabled - idle age uses write/creation times (the safe reading)'
|
|
91
|
+
if ($v -eq 1 -or $v -eq 3) { $laText = 'enabled - last-access times are reliable' }
|
|
92
|
+
Write-Kv 'Last-access tracking:' $laText
|
|
93
|
+
}
|
|
94
|
+
} catch { $null = $_ }
|
|
95
|
+
$blocking = @(Get-BlockingApps)
|
|
96
|
+
if ($blocking.Count -gt 0) {
|
|
97
|
+
Write-UiLine '' 'Gray'
|
|
98
|
+
Write-Warn ("running now, their caches will be skipped until closed: " + ($blocking -join ', '))
|
|
99
|
+
}
|
|
100
|
+
Add-Freed 0
|
|
101
|
+
}
|
package/modules/menu.ps1
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# menu.ps1 - jump-to menu: run one section at a time; toggle dry-run and auto-yes.
|
|
2
|
+
|
|
3
|
+
function Show-Menu {
|
|
4
|
+
$ws = $Script:WS
|
|
5
|
+
Write-UiLine '' 'Gray'
|
|
6
|
+
Write-Banner
|
|
7
|
+
$sys = New-Object IO.DriveInfo($Script:P.SD)
|
|
8
|
+
$dry = 'OFF'
|
|
9
|
+
if ($ws.DryRun) { $dry = 'ON' }
|
|
10
|
+
$yes = 'OFF'
|
|
11
|
+
if ($ws.Yes) { $yes = 'ON' }
|
|
12
|
+
$adm = 'no'
|
|
13
|
+
if ($ws.IsAdmin) { $adm = 'yes' }
|
|
14
|
+
Write-UiLine (" system drive free: {0} dry-run: {1} auto-yes: {2} elevated: {3}" -f (Format-Bytes $sys.AvailableFreeSpace), $dry, $yes, $adm) 'Cyan'
|
|
15
|
+
Write-UiLine '' 'Gray'
|
|
16
|
+
foreach ($s in $Script:WS_SECTIONS) {
|
|
17
|
+
$mark = ' '
|
|
18
|
+
if ($ws.Report.steps | Where-Object { $_.section -eq $s.Id -and $_.status -in 'ran', 'dry-run' }) { $mark = $ws.Glyph.ok }
|
|
19
|
+
$flags = ''
|
|
20
|
+
if ($s.Admin) { $flags += ' [admin]' }
|
|
21
|
+
if ($s.Batch -eq 'deep') { $flags += ' [deep]' }
|
|
22
|
+
if ($s.Batch -eq 'interactive') { $flags += ' [interactive]' }
|
|
23
|
+
$color = 'Gray'
|
|
24
|
+
if ($s.Admin -and -not $ws.IsAdmin) { $color = 'DarkGray' }
|
|
25
|
+
Write-UiLine (" {0} [{1,2}] {2}{3}" -f $mark, $s.Id, $s.Title, $flags) $color
|
|
26
|
+
}
|
|
27
|
+
Write-UiLine '' 'Gray'
|
|
28
|
+
Write-UiLine ' [A] run the safe batch [D] toggle dry-run [Y] toggle auto-yes [S] summary [Q] quit' 'White'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function Invoke-Menu {
|
|
32
|
+
$ws = $Script:WS
|
|
33
|
+
if (-not $ws.Interactive) {
|
|
34
|
+
Write-Err 'The menu needs an interactive console. For unattended runs use: windowsweep --all --yes'
|
|
35
|
+
$ws.ExitCode = $Script:WS_EXIT_USAGE
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
while ($true) {
|
|
39
|
+
Show-Menu
|
|
40
|
+
$choice = Read-Choice -Prompt ' Select [0-21 / A / D / Y / S / Q]: ' -Default ''
|
|
41
|
+
if ($choice -eq '') { continue }
|
|
42
|
+
switch -Regex ($choice) {
|
|
43
|
+
'^q' { Show-SessionSummary; return }
|
|
44
|
+
'^a' {
|
|
45
|
+
$ids = @($Script:WS_SAFE_BATCH)
|
|
46
|
+
if ($ws.IsAdmin) { $ids += $Script:WS_SAFE_BATCH_ADMIN }
|
|
47
|
+
$ws.BatchMode = $true
|
|
48
|
+
try { foreach ($id in $ids) { Invoke-SectionById -Id $id } } finally { $ws.BatchMode = $false }
|
|
49
|
+
Wait-Enter
|
|
50
|
+
}
|
|
51
|
+
'^d' { $ws.DryRun = -not $ws.DryRun }
|
|
52
|
+
'^y' { $ws.Yes = -not $ws.Yes }
|
|
53
|
+
'^s' { Show-SessionSummary; Wait-Enter }
|
|
54
|
+
'^\d+$' {
|
|
55
|
+
$id = [int]$choice
|
|
56
|
+
if (Get-Section $id) { Invoke-SectionById -Id $id; Wait-Enter } else { Write-Warn "no section $id" }
|
|
57
|
+
}
|
|
58
|
+
default { Write-Warn "unrecognised choice: $choice" }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# personal.ps1 - sections 18 and 19: personal files in Downloads. Interactive only; the Recycle Bin is the destination.
|
|
2
|
+
|
|
3
|
+
$Script:WS_PARTIAL_PATTERNS = @('*.crdownload', '*.part', '*.partial', '*.fdmdownload', '*.opdownload', '*.!ut', '*.aria2', '*.download', '*.bc!', '*.tmp')
|
|
4
|
+
|
|
5
|
+
function Get-PersonalRoots {
|
|
6
|
+
$roots = @()
|
|
7
|
+
$d = Join-Path $Script:P.U 'Downloads'
|
|
8
|
+
if (Test-DirPresent $d) { $roots += (Get-FullPath $d) }
|
|
9
|
+
return $roots
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function Get-Targets18 {
|
|
13
|
+
$t = @()
|
|
14
|
+
foreach ($r in (Get-PersonalRoots)) { $t += (New-Target 18 'Partial / orphan downloads' $r -Kind cmd -Note ($Script:WS_PARTIAL_PATTERNS -join ' ')) }
|
|
15
|
+
return $t
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function Get-Targets19 {
|
|
19
|
+
$t = @()
|
|
20
|
+
foreach ($r in (Get-PersonalRoots)) { $t += (New-Target 19 "Files >= $($Script:WS.LargeFileMb) MB idle $($Script:WS.Days)+ days" $r -Kind cmd -Note 'moved to the Recycle Bin after you pick them') }
|
|
21
|
+
return $t
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function Get-FilesUnder {
|
|
25
|
+
<# .SYNOPSIS Files under Root up to MaxDepth, never following links, never entering protected folders. #>
|
|
26
|
+
param([string] $Root, [int] $MaxDepth = 4)
|
|
27
|
+
$out = New-Object System.Collections.Generic.List[object]
|
|
28
|
+
$stack = New-Object System.Collections.Generic.Stack[object]
|
|
29
|
+
$stack.Push(@($Root, 0))
|
|
30
|
+
while ($stack.Count -gt 0) {
|
|
31
|
+
$item = $stack.Pop()
|
|
32
|
+
foreach ($e in (Get-ChildEntries ([string]$item[0]))) {
|
|
33
|
+
if (Test-ReparsePoint $e) { continue }
|
|
34
|
+
if ($e -is [IO.DirectoryInfo]) { if ([int]$item[1] -lt $MaxDepth) { $stack.Push(@((Remove-LongPrefix $e.FullName), [int]$item[1] + 1)) }; continue }
|
|
35
|
+
$out.Add($e)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return $out
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function Get-PartialDownloads {
|
|
42
|
+
$list = @()
|
|
43
|
+
foreach ($root in (Get-PersonalRoots)) {
|
|
44
|
+
foreach ($f in (Get-FilesUnder -Root $root -MaxDepth 4)) {
|
|
45
|
+
foreach ($pat in $Script:WS_PARTIAL_PATTERNS) {
|
|
46
|
+
if ($f.Name -like $pat) { $list += [pscustomobject]@{ Path = (Remove-LongPrefix $f.FullName); Bytes = [long]$f.Length; Idle = [int][math]::Floor(([datetime]::UtcNow - (Get-NewestTimestampUtc $f)).TotalDays); Root = $root }; break }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return @($list | Sort-Object Bytes -Descending)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function Get-LargeStaleFiles {
|
|
54
|
+
$ws = $Script:WS
|
|
55
|
+
$min = [long]$ws.LargeFileMb * 1MB
|
|
56
|
+
$list = @()
|
|
57
|
+
foreach ($root in (Get-PersonalRoots)) {
|
|
58
|
+
foreach ($f in (Get-FilesUnder -Root $root -MaxDepth 4)) {
|
|
59
|
+
if ([long]$f.Length -lt $min) { continue }
|
|
60
|
+
$idle = [int][math]::Floor(([datetime]::UtcNow - (Get-NewestTimestampUtc $f)).TotalDays)
|
|
61
|
+
if ($idle -lt $ws.Days) { continue }
|
|
62
|
+
$list += [pscustomobject]@{ Path = (Remove-LongPrefix $f.FullName); Bytes = [long]$f.Length; Idle = $idle; Root = $root }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return @($list | Sort-Object Bytes -Descending | Select-Object -First 100)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function Show-FileTable {
|
|
69
|
+
param([object[]] $Rows)
|
|
70
|
+
Write-UiLine (" {0,3} {1,10} {2,6} {3}" -f '#', 'SIZE', 'IDLE', 'FILE') 'White'
|
|
71
|
+
$i = 1
|
|
72
|
+
foreach ($r in $Rows) { Write-UiLine (" {0,3} {1,10} {2,5}d {3}" -f $i, (Format-Bytes $r.Bytes), $r.Idle, $r.Path) 'Gray'; $i++ }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function Invoke-PersonalPicker {
|
|
76
|
+
param([object[]] $Rows, [string] $Prompt)
|
|
77
|
+
$ws = $Script:WS
|
|
78
|
+
$Rows = @($Rows)
|
|
79
|
+
if ($Rows.Count -eq 0) { Write-Info 'none found'; return }
|
|
80
|
+
Show-FileTable $Rows
|
|
81
|
+
$total = [long]0
|
|
82
|
+
foreach ($r in $Rows) { $total += $r.Bytes }
|
|
83
|
+
Write-Info ("total: " + (Format-Bytes $total))
|
|
84
|
+
$dest = 'the Recycle Bin'
|
|
85
|
+
if ($ws.Permanent) { $dest = 'PERMANENT deletion (--permanent)' }
|
|
86
|
+
Write-Note "selected files go to $dest"
|
|
87
|
+
$picks = Read-MultiSelect -Total $Rows.Count
|
|
88
|
+
if ($picks.Count -eq 0) { Write-Info 'nothing selected'; return }
|
|
89
|
+
if (-not $ws.DryRun) {
|
|
90
|
+
# Personal files never auto-confirm: --yes does not apply here.
|
|
91
|
+
if (-not (Confirm-Ui -Prompt "$Prompt ($($picks.Count) file(s))?" -Default 'n' -NoAutoYes)) { Write-Info 'skipped'; return }
|
|
92
|
+
}
|
|
93
|
+
foreach ($k in $picks) { $null = Send-ToRecycleBin -Path $Rows[$k - 1].Path -Within $Rows[$k - 1].Root }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function Invoke-Section18 {
|
|
97
|
+
Write-SectionIntro @(
|
|
98
|
+
'Half-finished downloads left behind by browsers and download managers (.crdownload, .part, .tmp, ...).',
|
|
99
|
+
'You pick what goes; it lands in the Recycle Bin. Desktop and cloud-synced folders are never scanned.'
|
|
100
|
+
)
|
|
101
|
+
if (@(Get-PersonalRoots).Count -eq 0) { Write-Info 'no Downloads folder found'; return }
|
|
102
|
+
Invoke-PersonalPicker -Rows @(Get-PartialDownloads) -Prompt 'Move the selected partial downloads to the Recycle Bin'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function Invoke-Section19 {
|
|
106
|
+
$ws = $Script:WS
|
|
107
|
+
Write-SectionIntro @(
|
|
108
|
+
"Files of $($ws.LargeFileMb) MB or more in Downloads that nothing has touched for $($ws.Days)+ days: old installers, ISOs,",
|
|
109
|
+
'archives you already extracted. You pick what goes; it lands in the Recycle Bin.'
|
|
110
|
+
)
|
|
111
|
+
if (@(Get-PersonalRoots).Count -eq 0) { Write-Info 'no Downloads folder found'; return }
|
|
112
|
+
Invoke-PersonalPicker -Rows @(Get-LargeStaleFiles) -Prompt 'Move the selected files to the Recycle Bin'
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function Show-PersonalScan {
|
|
116
|
+
<# .SYNOPSIS Read-only listing used by --scan. #>
|
|
117
|
+
Write-Box 'Personal files (read-only)' 'Sections 18 and 19 are interactive only - nothing here is deleted'
|
|
118
|
+
Write-Section 'Partial / orphan downloads'
|
|
119
|
+
$p = @(Get-PartialDownloads)
|
|
120
|
+
if ($p.Count -eq 0) { Write-Info 'none found' } else { Show-FileTable @($p | Select-Object -First 30) }
|
|
121
|
+
Write-Section "Files >= $($Script:WS.LargeFileMb) MB idle $($Script:WS.Days)+ days in Downloads"
|
|
122
|
+
$l = @(Get-LargeStaleFiles)
|
|
123
|
+
if ($l.Count -eq 0) { Write-Info 'none found' } else { Show-FileTable @($l | Select-Object -First 30) }
|
|
124
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# pkg_managers.ps1 - section 1: package-manager caches. All regenerable; the idle gate keeps recent work fast.
|
|
2
|
+
|
|
3
|
+
function Get-Targets01 {
|
|
4
|
+
$P = $Script:P
|
|
5
|
+
$npm = Get-NpmCacheDir
|
|
6
|
+
return @(
|
|
7
|
+
(New-Target 1 'npm cache (_cacache)' (Join-Path $npm '_cacache') -Mode prune -Dev $true -Note 'regenerated on the next npm install')
|
|
8
|
+
(New-Target 1 'npx package cache (_npx)' (Join-Path $npm '_npx') -Mode prune -Dev $true)
|
|
9
|
+
(New-Target 1 'npm logs' (Join-Path $npm '_logs') -Mode clear-old -Days 7)
|
|
10
|
+
(New-Target 1 'Yarn v1 cache' "$($P.L)\Yarn\Cache" -Mode prune -Dev $true -Note 'yarn classic offline mirror')
|
|
11
|
+
(New-Target 1 'Yarn Berry global cache' "$($P.L)\Yarn\Berry\cache" -Mode prune -Dev $true)
|
|
12
|
+
(New-Target 1 'pnpm content store' "$($P.L)\pnpm\store" -Mode prune -Dev $true -Note 'pnpm store prune is used instead when pnpm is installed')
|
|
13
|
+
(New-Target 1 'pnpm metadata cache' "$($P.L)\pnpm-cache" -Mode prune -Dev $true)
|
|
14
|
+
(New-Target 1 'bun install cache' "$($P.U)\.bun\install\cache" -Mode prune -Dev $true -Note 'global bun packages stay')
|
|
15
|
+
(New-Target 1 'deno deps cache' "$($P.L)\deno\deps" -Mode prune -Dev $true -Note 'installed deno scripts (~\.deno\bin) stay')
|
|
16
|
+
(New-Target 1 'deno gen cache' "$($P.L)\deno\gen" -Mode prune -Dev $true)
|
|
17
|
+
(New-Target 1 'deno npm cache' "$($P.L)\deno\npm" -Mode prune -Dev $true)
|
|
18
|
+
(New-Target 1 'deno remote cache' "$($P.L)\deno\remote" -Mode prune -Dev $true)
|
|
19
|
+
(New-Target 1 'pip cache' "$($P.L)\pip\cache" -Mode prune -Dev $true)
|
|
20
|
+
(New-Target 1 'uv cache' "$($P.L)\uv\cache" -Mode prune -Dev $true -Note 'uv cache prune runs first when uv is installed')
|
|
21
|
+
(New-Target 1 'poetry cache' "$($P.L)\pypoetry\Cache" -Mode prune -Dev $true)
|
|
22
|
+
(New-Target 1 'Composer cache (files)' "$($P.L)\Composer\files" -Mode prune -Dev $true)
|
|
23
|
+
(New-Target 1 'Composer cache (roaming)' "$($P.A)\Composer\cache" -Mode prune -Dev $true)
|
|
24
|
+
(New-Target 1 'NuGet http cache' "$($P.L)\NuGet\v3-cache" -Mode clear)
|
|
25
|
+
(New-Target 1 'NuGet plugins cache' "$($P.L)\NuGet\plugins-cache" -Mode clear)
|
|
26
|
+
(New-Target 1 'NuGet scratch' "$($P.L)\Temp\NuGetScratch" -Mode clear)
|
|
27
|
+
(New-Target 1 'NuGet global packages' "$($P.U)\.nuget\packages" -Mode prune -Dev $true -Note 'restored on the next dotnet restore')
|
|
28
|
+
(New-Target 1 'Cargo registry cache' "$($P.U)\.cargo\registry\cache" -Mode prune -Dev $true -Note 'installed cargo binaries (~\.cargo\bin) stay')
|
|
29
|
+
(New-Target 1 'Cargo registry sources' "$($P.U)\.cargo\registry\src" -Mode prune -Dev $true)
|
|
30
|
+
(New-Target 1 'Cargo git checkouts' "$($P.U)\.cargo\git\checkouts" -Mode prune -Dev $true)
|
|
31
|
+
(New-Target 1 'Go build cache' "$($P.L)\go-build" -Mode prune -Dev $true)
|
|
32
|
+
(New-Target 1 'Go module download cache' "$($P.U)\go\pkg\mod\cache\download" -Mode prune -Dev $true)
|
|
33
|
+
(New-Target 1 'Dart/Flutter pub cache' "$($P.L)\Pub\Cache" -Mode prune -Dev $true -Note 'redownloaded by flutter pub get')
|
|
34
|
+
(New-Target 1 'Dart/Flutter pub cache (legacy)' "$($P.A)\Pub\Cache" -Mode prune -Dev $true)
|
|
35
|
+
(New-Target 1 'Dart analysis server cache' "$($P.U)\.dartServer" -Mode prune -Dev $true)
|
|
36
|
+
(New-Target 1 'Electron download cache' "$($P.L)\electron\Cache" -Mode prune -Dev $true)
|
|
37
|
+
(New-Target 1 'electron-builder cache' "$($P.L)\electron-builder\Cache" -Mode prune -Dev $true)
|
|
38
|
+
(New-Target 1 'node-gyp headers cache' "$($P.L)\node-gyp\Cache" -Mode prune -Dev $true)
|
|
39
|
+
(New-Target 1 'Scoop installer cache' "$($P.U)\scoop\cache" -Mode prune -Dev $true)
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function Invoke-Section01 {
|
|
44
|
+
$ws = $Script:WS
|
|
45
|
+
Write-SectionIntro @(
|
|
46
|
+
'Package-manager caches only re-download what a future install needs. Globally installed packages,',
|
|
47
|
+
'toolchain managers (nvm, Volta, corepack, pnpm global, cargo bin, go bin) are protected and never touched.'
|
|
48
|
+
) -Dev $true
|
|
49
|
+
$targets = Get-Targets01
|
|
50
|
+
$null = Show-TargetSizes $targets
|
|
51
|
+
if (-not (Confirm-Section 'Prune package-manager caches now?')) { Write-Info 'skipped'; return }
|
|
52
|
+
# Tool-native smart prunes first: they know which entries are still referenced.
|
|
53
|
+
if ((Test-CommandPresent 'pnpm') -and $ws.Developer -ne $false -and -not $ws.PurgeAll) {
|
|
54
|
+
Write-Info 'pnpm store prune - removes packages no project references'
|
|
55
|
+
$r = Invoke-External -FilePath 'pnpm' -ArgumentList @('store', 'prune') -Destructive -Quiet -Label 'pnpm store prune'
|
|
56
|
+
if ($r.Ran -and $r.ExitCode -eq 0) { Write-Ok 'pnpm store pruned' }
|
|
57
|
+
$targets = @($targets | Where-Object { $_.Label -ne 'pnpm content store' })
|
|
58
|
+
}
|
|
59
|
+
if ((Test-CommandPresent 'uv') -and $ws.Developer -ne $false -and -not $ws.PurgeAll) {
|
|
60
|
+
Write-Info 'uv cache prune - removes unreachable wheels and sources'
|
|
61
|
+
$r = Invoke-External -FilePath 'uv' -ArgumentList @('cache', 'prune') -Destructive -Quiet -Label 'uv cache prune'
|
|
62
|
+
if ($r.Ran -and $r.ExitCode -eq 0) { Write-Ok 'uv cache pruned' }
|
|
63
|
+
}
|
|
64
|
+
if ((Test-CommandPresent 'npm') -and $ws.PurgeAll) {
|
|
65
|
+
Write-Info 'npm cache clean --force'
|
|
66
|
+
$null = Invoke-External -FilePath 'npm' -ArgumentList @('cache', 'clean', '--force') -Destructive -Quiet -Label 'npm cache clean'
|
|
67
|
+
}
|
|
68
|
+
$null = Invoke-TargetList $targets
|
|
69
|
+
}
|