windowsweep 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI-INTEGRATION-GUIDE.md +203 -0
- package/CHANGELOG.md +124 -0
- package/LICENSE +21 -21
- package/README.md +88 -49
- package/VERSION +1 -1
- package/lib/actions.ps1 +212 -209
- package/lib/config.ps1 +108 -99
- package/lib/constants.ps1 +103 -89
- package/lib/fs.ps1 +189 -180
- package/lib/log.ps1 +140 -140
- package/lib/safety.ps1 +541 -493
- package/lib/scan.ps1 +109 -94
- package/lib/ui.ps1 +275 -235
- package/modules/android_avd.ps1 +36 -36
- package/modules/app_caches.ps1 +58 -58
- package/modules/browsers.ps1 +33 -33
- package/modules/build_tools.ps1 +35 -35
- package/modules/crash_trap.ps1 +89 -89
- package/modules/disk_usage.ps1 +49 -49
- package/modules/docker.ps1 +93 -93
- package/modules/editors.ps1 +150 -150
- package/modules/globals_audit.ps1 +236 -0
- package/modules/health.ps1 +101 -101
- package/modules/installed_programs.ps1 +118 -0
- package/modules/menu.ps1 +63 -61
- package/modules/notify.ps1 +60 -0
- package/modules/orphaned_appdata.ps1 +189 -0
- package/modules/personal.ps1 +126 -124
- package/modules/pkg_managers.ps1 +73 -69
- package/modules/projects.ps1 +160 -143
- package/modules/release_helpers.ps1 +438 -434
- package/modules/reports.ps1 +212 -212
- package/modules/runner.ps1 +242 -190
- package/modules/self_test_extra.ps1 +537 -86
- package/modules/startup_audit.ps1 +126 -0
- package/modules/system_admin.ps1 +220 -220
- package/modules/test_runners.ps1 +24 -24
- package/modules/walkthrough.ps1 +80 -79
- package/modules/windows_user.ps1 +118 -112
- package/package.json +4 -3
- package/windowsweep.ps1 +322 -296
package/modules/pkg_managers.ps1
CHANGED
|
@@ -1,69 +1,73 @@
|
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
$
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
$
|
|
67
|
-
}
|
|
68
|
-
|
|
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
|
+
# Hugging Face keeps revisions as snapshots\<rev> linking into blobs, so removing a whole snapshot orphans
|
|
41
|
+
# blobs: this prunes by the idle gate instead of keeping "the newest per model". AI agent state under
|
|
42
|
+
# ~\.cache (claude, codex, gemini, copilot) stays protected by lib/safety.ps1.
|
|
43
|
+
(New-Target 1 'Hugging Face model cache' "$($P.U)\.cache\huggingface\hub" -Mode prune -Dev $true -Note 'models are re-downloaded on next use; the idle gate keeps recent ones')
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function Invoke-Section01 {
|
|
48
|
+
$ws = $Script:WS
|
|
49
|
+
Write-SectionIntro @(
|
|
50
|
+
'Package-manager caches only re-download what a future install needs. Globally installed packages,',
|
|
51
|
+
'toolchain managers (nvm, Volta, corepack, pnpm global, cargo bin, go bin) are protected and never touched.'
|
|
52
|
+
) -Dev $true
|
|
53
|
+
$targets = Get-Targets01
|
|
54
|
+
$null = Show-TargetSizes $targets
|
|
55
|
+
if (-not (Confirm-Section 'Prune package-manager caches now?')) { Write-Info 'skipped'; return }
|
|
56
|
+
# Tool-native smart prunes first: they know which entries are still referenced.
|
|
57
|
+
if ((Test-CommandPresent 'pnpm') -and $ws.Developer -ne $false -and -not $ws.PurgeAll) {
|
|
58
|
+
Write-Info 'pnpm store prune - removes packages no project references'
|
|
59
|
+
$r = Invoke-External -FilePath 'pnpm' -ArgumentList @('store', 'prune') -Destructive -Quiet -Label 'pnpm store prune'
|
|
60
|
+
if ($r.Ran -and $r.ExitCode -eq 0) { Write-Ok 'pnpm store pruned' }
|
|
61
|
+
$targets = @($targets | Where-Object { $_.Label -ne 'pnpm content store' })
|
|
62
|
+
}
|
|
63
|
+
if ((Test-CommandPresent 'uv') -and $ws.Developer -ne $false -and -not $ws.PurgeAll) {
|
|
64
|
+
Write-Info 'uv cache prune - removes unreachable wheels and sources'
|
|
65
|
+
$r = Invoke-External -FilePath 'uv' -ArgumentList @('cache', 'prune') -Destructive -Quiet -Label 'uv cache prune'
|
|
66
|
+
if ($r.Ran -and $r.ExitCode -eq 0) { Write-Ok 'uv cache pruned' }
|
|
67
|
+
}
|
|
68
|
+
if ((Test-CommandPresent 'npm') -and $ws.PurgeAll) {
|
|
69
|
+
Write-Info 'npm cache clean --force'
|
|
70
|
+
$null = Invoke-External -FilePath 'npm' -ArgumentList @('cache', 'clean', '--force') -Destructive -Quiet -Label 'npm cache clean'
|
|
71
|
+
}
|
|
72
|
+
$null = Invoke-TargetList $targets
|
|
73
|
+
}
|
package/modules/projects.ps1
CHANGED
|
@@ -1,143 +1,160 @@
|
|
|
1
|
-
# projects.ps1 - section 17: stale build artefacts in projects nobody has touched for N+ days. Interactive only.
|
|
2
|
-
|
|
3
|
-
$Script:WS_ARTEFACT_DIRS = @('node_modules', 'dist', 'build', 'out', '.next', '.nuxt', '.turbo', '.vite', '.svelte-kit', '.astro', '.parcel-cache', 'target', 'vendor', 'coverage', '.nyc_output', '__pycache__', '.pytest_cache', '.dart_tool', '.angular')
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
$roots
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
$
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if ($
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
$found
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
$
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
Write-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
$f
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
1
|
+
# projects.ps1 - section 17: stale build artefacts in projects nobody has touched for N+ days. Interactive only.
|
|
2
|
+
|
|
3
|
+
$Script:WS_ARTEFACT_DIRS = @('node_modules', 'dist', 'build', 'out', '.next', '.nuxt', '.turbo', '.vite', '.svelte-kit', '.astro', '.parcel-cache', 'target', 'vendor', 'coverage', '.nyc_output', '__pycache__', '.pytest_cache', '.dart_tool', '.angular', '.nx', '.mypy_cache', '.ruff_cache', '.tox', '.eggs', '.output', '.serverless')
|
|
4
|
+
# Never here: .venv / venv (nobody expects an environment to vanish) and .terraform (provider binaries + lock state).
|
|
5
|
+
# A name in this table is an artefact ONLY when the same folder carries one of the listed markers: a bare
|
|
6
|
+
# .cache belongs to whoever made it, but beside a Gatsby or Parcel config it is a build cache.
|
|
7
|
+
$Script:WS_ARTEFACT_DIRS_MARKED = @{ '.cache' = @('gatsby-config.js', 'gatsby-config.ts', 'gatsby-config.mjs', '.parcelrc', 'parcel.config.js') }
|
|
8
|
+
$Script:WS_PROJECT_MARKERS = @('package.json', 'pubspec.yaml', 'composer.json', 'Cargo.toml', 'go.mod', 'build.gradle', 'build.gradle.kts', 'pom.xml', 'pyproject.toml', 'requirements.txt', 'angular.json', 'next.config.js', 'next.config.mjs', 'nuxt.config.ts', 'vite.config.ts', 'vite.config.js')
|
|
9
|
+
$Script:WS_NEVER_ENTER = @('.git', 'AppData', 'node_modules', '$RECYCLE.BIN', 'System Volume Information', '.gradle', '.m2', '.nuget', '.cargo', '.vscode', '.idea')
|
|
10
|
+
|
|
11
|
+
function Get-ProjectScanRoots {
|
|
12
|
+
$ws = $Script:WS
|
|
13
|
+
$U = $Script:P.U
|
|
14
|
+
$roots = @()
|
|
15
|
+
if ($ws.ScanRoots.Count -gt 0) {
|
|
16
|
+
foreach ($r in $ws.ScanRoots) {
|
|
17
|
+
$f = Get-FullPath $r
|
|
18
|
+
if (-not $f -or -not (Test-DirPresent $f)) { Write-Warn "scan root not found: $r"; continue }
|
|
19
|
+
if ($f -match '^[A-Za-z]:\\?$' -or $f.Equals($U, [StringComparison]::OrdinalIgnoreCase)) { Write-Warn "refusing to scan a whole drive or your profile root: $f (name a projects folder)"; continue }
|
|
20
|
+
$roots += $f
|
|
21
|
+
}
|
|
22
|
+
return $roots
|
|
23
|
+
}
|
|
24
|
+
$names = @('source\repos', 'source', 'repos', 'Projects', 'projects', 'code', 'Code', 'dev', 'Dev', 'Development', 'work', 'Work', 'git', 'GitHub', 'Documents\GitHub', 'Documents\Projects', 'Documents\code', 'Documents\dev', 'Documents\repos')
|
|
25
|
+
foreach ($n in $names) { $p = Join-Path $U $n; if (Test-DirPresent $p) { $roots += (Get-FullPath $p) } }
|
|
26
|
+
foreach ($d in [IO.DriveInfo]::GetDrives()) {
|
|
27
|
+
if (-not $d.IsReady -or $d.DriveType -ne [IO.DriveType]::Fixed) { continue }
|
|
28
|
+
foreach ($n in @('work', 'code', 'projects', 'repos', 'dev', 'src', 'git', 'Projects', 'Code', 'Work')) {
|
|
29
|
+
$p = Join-Path $d.Name $n
|
|
30
|
+
if ((Test-DirPresent $p) -and ($roots -notcontains (Get-FullPath $p))) { $roots += (Get-FullPath $p) }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return @($roots | Select-Object -Unique)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function Get-Targets17 {
|
|
37
|
+
$roots = @(Get-ProjectScanRoots)
|
|
38
|
+
$t = @()
|
|
39
|
+
foreach ($r in $roots) { $t += (New-Target 17 'Project scan root' $r -Kind cmd -Note ($Script:WS_ARTEFACT_DIRS -join ', ')) }
|
|
40
|
+
if ($t.Count -eq 0) { $t += (New-Target 17 'Project scan root' '(none detected - pass --scan-roots "D:\work;E:\code")' -Kind cmd) }
|
|
41
|
+
return $t
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function Test-MarkedArtefactDir {
|
|
45
|
+
<# .SYNOPSIS True when Name is a marker-gated artefact folder AND Dir carries one of its markers. #>
|
|
46
|
+
param([string] $Dir, [string] $Name)
|
|
47
|
+
if (-not $Script:WS_ARTEFACT_DIRS_MARKED.ContainsKey($Name)) { return $false }
|
|
48
|
+
$names = @(Get-ChildEntries $Dir | ForEach-Object { $_.Name })
|
|
49
|
+
foreach ($m in $Script:WS_ARTEFACT_DIRS_MARKED[$Name]) { if ($names -contains $m) { return $true } }
|
|
50
|
+
return $false
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function Test-ProjectMarker {
|
|
54
|
+
param([string] $Dir, [string] $ArtefactName)
|
|
55
|
+
$entries = Get-ChildEntries $Dir
|
|
56
|
+
$names = @($entries | ForEach-Object { $_.Name })
|
|
57
|
+
if ($ArtefactName -in 'bin', 'obj') { return [bool]($names | Where-Object { $_ -like '*.csproj' -or $_ -like '*.sln' -or $_ -like '*.fsproj' }) }
|
|
58
|
+
foreach ($m in $Script:WS_PROJECT_MARKERS) { if ($names -contains $m) { return $true } }
|
|
59
|
+
return ($names -contains '.git')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function Get-ProjectActivityDays {
|
|
63
|
+
<# .SYNOPSIS Days since any SOURCE file in the project changed (artefact dirs and .git excluded). #>
|
|
64
|
+
param([string] $ProjectDir)
|
|
65
|
+
$newest = [datetime]::MinValue
|
|
66
|
+
$stack = New-Object System.Collections.Generic.Stack[string]
|
|
67
|
+
$stack.Push($ProjectDir)
|
|
68
|
+
$seen = 0
|
|
69
|
+
while ($stack.Count -gt 0 -and $seen -lt 200000) {
|
|
70
|
+
$dir = $stack.Pop()
|
|
71
|
+
foreach ($e in (Get-ChildEntries $dir)) {
|
|
72
|
+
$seen++
|
|
73
|
+
if (Test-ReparsePoint $e) { continue }
|
|
74
|
+
if ($e -is [IO.DirectoryInfo]) {
|
|
75
|
+
if ($Script:WS_ARTEFACT_DIRS -contains $e.Name -or $Script:WS_NEVER_ENTER -contains $e.Name -or (Test-MarkedArtefactDir -Dir $dir -Name $e.Name)) { continue }
|
|
76
|
+
$stack.Push((Remove-LongPrefix $e.FullName)); continue
|
|
77
|
+
}
|
|
78
|
+
$t = Get-NewestTimestampUtc $e
|
|
79
|
+
if ($t -gt $newest) { $newest = $t }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if ($newest -eq [datetime]::MinValue) { return (Get-IdleDays $ProjectDir) }
|
|
83
|
+
return [int][math]::Floor(([datetime]::UtcNow - $newest).TotalDays)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function Find-StaleArtefacts {
|
|
87
|
+
<# .SYNOPSIS Artefact folders (node_modules, dist, ...) whose project has been idle for N+ days. #>
|
|
88
|
+
param([string[]] $Roots, [int] $Days, [int] $MaxDepth = 6)
|
|
89
|
+
$ws = $Script:WS
|
|
90
|
+
$found = New-Object System.Collections.Generic.List[object]
|
|
91
|
+
$projectAge = @{}
|
|
92
|
+
foreach ($root in $Roots) {
|
|
93
|
+
$stack = New-Object System.Collections.Generic.Stack[object]
|
|
94
|
+
$stack.Push(@($root, 0))
|
|
95
|
+
while ($stack.Count -gt 0) {
|
|
96
|
+
$item = $stack.Pop()
|
|
97
|
+
$dir = [string]$item[0]; $depth = [int]$item[1]
|
|
98
|
+
foreach ($e in (Get-ChildEntries $dir)) {
|
|
99
|
+
if (-not ($e -is [IO.DirectoryInfo]) -or (Test-ReparsePoint $e)) { continue }
|
|
100
|
+
$full = Remove-LongPrefix $e.FullName
|
|
101
|
+
$skip = $false
|
|
102
|
+
foreach ($x in $ws.ExcludePaths) { if ($x -and (Test-PathWithin -Path $full -Within $x)) { $skip = $true; break } }
|
|
103
|
+
if ($skip) { continue }
|
|
104
|
+
if ($Script:WS_ARTEFACT_DIRS -contains $e.Name -or ($e.Name -in 'bin', 'obj') -or (Test-MarkedArtefactDir -Dir $dir -Name $e.Name)) {
|
|
105
|
+
if (-not (Test-ProjectMarker -Dir $dir -ArtefactName $e.Name)) { continue }
|
|
106
|
+
if (-not $projectAge.ContainsKey($dir)) { $projectAge[$dir] = Get-ProjectActivityDays $dir }
|
|
107
|
+
$age = $projectAge[$dir]
|
|
108
|
+
if ($age -ge $Days) { $found.Add([pscustomobject]@{ Path = $full; Project = $dir; Age = $age; Bytes = [long](Get-DirectoryBytes $full); Root = $root }) }
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
if ($Script:WS_NEVER_ENTER -contains $e.Name -or $e.Name.StartsWith('.')) { continue }
|
|
112
|
+
if ($depth -lt $MaxDepth) { $stack.Push(@($full, $depth + 1)) }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return @($found | Sort-Object Bytes -Descending)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function Invoke-Section17 {
|
|
120
|
+
$ws = $Script:WS
|
|
121
|
+
Write-SectionIntro @(
|
|
122
|
+
'node_modules, dist, .next, target, vendor and friends are rebuilt by the next install or build. Only projects',
|
|
123
|
+
"whose SOURCE files nobody touched for $($ws.Days)+ days are listed, and nothing goes without your selection.",
|
|
124
|
+
'Toolchain folders (.gradle, .nuget, .cargo, AppData) and .git are never entered.'
|
|
125
|
+
)
|
|
126
|
+
$roots = @(Get-ProjectScanRoots)
|
|
127
|
+
if ($roots.Count -eq 0) { Write-Warn 'no project roots found. Pass --scan-roots "D:\work;E:\code" (semicolon-separated) or save scanRoots in config.json.'; return }
|
|
128
|
+
Write-Info ("scanning: " + ($roots -join ' '))
|
|
129
|
+
$found = @(Find-StaleArtefacts -Roots $roots -Days $ws.Days)
|
|
130
|
+
if ($found.Count -eq 0) { Write-Info "no build artefacts in projects idle $($ws.Days)+ days"; return }
|
|
131
|
+
$reportLines = @("stale build artefacts - projects idle $($ws.Days)+ days - $(Get-Date -Format 'yyyy-MM-dd HH:mm')", '')
|
|
132
|
+
# One offered list drives the table, the JSON candidates and the selection indexes: they cannot disagree.
|
|
133
|
+
$offered = @($found | Select-Object -First 200)
|
|
134
|
+
if ($found.Count -gt $offered.Count) { Write-Warn 'list truncated to 200 entries' }
|
|
135
|
+
Write-UiLine (" {0,3} {1,10} {2,6} {3}" -f '#', 'SIZE', 'IDLE', 'ARTEFACT') 'White'
|
|
136
|
+
$i = 1; $total = [long]0
|
|
137
|
+
foreach ($f in $offered) {
|
|
138
|
+
Write-UiLine (" {0,3} {1,10} {2,5}d {3}" -f $i, (Format-Bytes $f.Bytes), $f.Age, $f.Path) 'Gray'
|
|
139
|
+
$reportLines += ("{0,10} {1,5}d {2}" -f (Format-Bytes $f.Bytes), $f.Age, $f.Path)
|
|
140
|
+
Add-JsonCandidate -Section 17 -Index $i -Path $f.Path -Bytes $f.Bytes -IdleDays $f.Age -Project $f.Project
|
|
141
|
+
$total += $f.Bytes; $i++
|
|
142
|
+
}
|
|
143
|
+
Write-Info ("total: " + (Format-Bytes $total))
|
|
144
|
+
if (-not $ws.NoReport) {
|
|
145
|
+
$out = Join-Path $ws.ReportsDir ('stale-builds-' + $ws.Stamp + '.txt')
|
|
146
|
+
try { [IO.File]::WriteAllLines($out, $reportLines); Write-Note "list saved: $out" } catch { $null = $_ }
|
|
147
|
+
}
|
|
148
|
+
$picks = @(Read-MultiSelect -Total $offered.Count -NoAutoYes -Candidates @($offered | ForEach-Object { $_.Path }))
|
|
149
|
+
if ($picks.Count -eq 0) { Write-Info 'nothing selected'; return }
|
|
150
|
+
if (-not $ws.DryRun) {
|
|
151
|
+
# Project artefacts never auto-confirm: --yes does not apply, and the walkthrough's step answer is not a selection.
|
|
152
|
+
# An explicit --select / --select-file choice does answer it - that is a person choosing, in advance.
|
|
153
|
+
if (-not (Confirm-Ui -Prompt "Remove $($picks.Count) artefact folder(s)? (rebuild with the project's install/build command)" -Default 'n' -NoAutoYes -ScriptedOk)) { Write-Info 'skipped'; return }
|
|
154
|
+
}
|
|
155
|
+
foreach ($k in $picks) {
|
|
156
|
+
$f = $offered[$k - 1]
|
|
157
|
+
$r = Remove-PathSafe -Path $f.Path -Within $f.Root -Label (Split-Path -Leaf $f.Path)
|
|
158
|
+
if ($r.Removed -and -not $ws.DryRun) { Write-Ok ("removed " + (Format-Bytes $r.Bytes) + " $($f.Path)") }
|
|
159
|
+
}
|
|
160
|
+
}
|