windowsweep 1.1.0 → 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 +15 -11
- package/CHANGELOG.md +56 -0
- package/LICENSE +21 -21
- package/README.md +70 -42
- package/VERSION +1 -1
- package/lib/actions.ps1 +212 -212
- package/lib/config.ps1 +108 -99
- package/lib/constants.ps1 +103 -93
- package/lib/fs.ps1 +189 -180
- package/lib/log.ps1 +140 -140
- package/lib/safety.ps1 +541 -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 +242 -239
- package/modules/self_test_extra.ps1 +537 -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 +2 -2
- package/windowsweep.ps1 +322 -316
package/modules/health.ps1
CHANGED
|
@@ -1,101 +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}%) -
|
|
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 (section 25 lists them all; change them in Task Manager > Startup - this tool never does)" } 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 (
|
|
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
|
-
}
|
|
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}%) - the sections below list what can be reclaimed" -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 (section 25 lists them all; change them in Task Manager > Startup - this tool never does)" } 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 (keeps more)'
|
|
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
|
+
}
|
|
@@ -1,118 +1,118 @@
|
|
|
1
|
-
# installed_programs.ps1 - section 24: installed programs nothing has modified for N+ days. Report only.
|
|
2
|
-
#
|
|
3
|
-
# It NEVER runs an uninstaller, in any mode. Windows keeps no reliable "last launched" record, so the number
|
|
4
|
-
# reported is "not modified", derived from the files under InstallLocation - never described as "unused".
|
|
5
|
-
|
|
6
|
-
function Get-WingetIdMap {
|
|
7
|
-
<# .SYNOPSIS One winget call, parsed once: normalised program name -> package id. Empty when winget is absent. #>
|
|
8
|
-
$map = @{}
|
|
9
|
-
if (-not (Test-CommandPresent 'winget')) { return $map }
|
|
10
|
-
$lines = @()
|
|
11
|
-
try { $lines = @(& winget list --disable-interactivity 2>$null) } catch { return $map }
|
|
12
|
-
foreach ($line in $lines) {
|
|
13
|
-
$text = [string]$line
|
|
14
|
-
# "Name Id Version Source" with runs of spaces between columns; ids never contain a space.
|
|
15
|
-
if ($text -notmatch '^(.+?)\s\s+([^\s]+)\s\s+') { continue }
|
|
16
|
-
$name = ConvertTo-MatchToken $Matches[1]
|
|
17
|
-
$id = $Matches[2]
|
|
18
|
-
if ($name.Length -lt 3 -or $id -eq 'Id') { continue }
|
|
19
|
-
if (-not $map.ContainsKey($name)) { $map[$name] = $id }
|
|
20
|
-
}
|
|
21
|
-
return $map
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function Get-InstallLocationIdleDays {
|
|
25
|
-
<# .SYNOPSIS Days since the newest LAST-WRITE at depth <= 2 under an install folder, or -1 when unknowable.
|
|
26
|
-
Deliberately NOT Get-NewestTimestampUtc, which the cleanup sections use: that takes the newest of write,
|
|
27
|
-
access and creation, and last-access is live on this machine - measured 2026-09-04, every install folder
|
|
28
|
-
read as 0 days idle that way (Git, Gpg4win, RustDesk, Malwarebytes all 0d) while last-write gave 120d,
|
|
29
|
-
203d, 163d and 148d. This section asks "not modified", so last-write is the matching question. #>
|
|
30
|
-
param([string] $Location, [int] $MaxEntries = 4000)
|
|
31
|
-
if ([string]::IsNullOrWhiteSpace($Location)) { return -1 }
|
|
32
|
-
$root = Get-FullPath ($Location.Trim().Trim('"').TrimEnd('\'))
|
|
33
|
-
if (-not $root -or -not (Test-DirPresent $root)) { return -1 }
|
|
34
|
-
$newest = [datetime]::MinValue
|
|
35
|
-
$seen = 0
|
|
36
|
-
$stack = New-Object System.Collections.Generic.Stack[object]
|
|
37
|
-
$stack.Push(@($root, 0))
|
|
38
|
-
while ($stack.Count -gt 0 -and $seen -lt $MaxEntries) {
|
|
39
|
-
$item = $stack.Pop()
|
|
40
|
-
foreach ($e in (Get-ChildEntries ([string]$item[0]))) {
|
|
41
|
-
$seen++
|
|
42
|
-
if (Test-ReparsePoint $e) { continue }
|
|
43
|
-
if ($e -is [IO.DirectoryInfo]) {
|
|
44
|
-
if ([int]$item[1] -lt 2) { $stack.Push(@((Remove-LongPrefix $e.FullName), [int]$item[1] + 1)) }
|
|
45
|
-
continue
|
|
46
|
-
}
|
|
47
|
-
if ($e.LastWriteTimeUtc -gt $newest) { $newest = $e.LastWriteTimeUtc }
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
if ($newest -eq [datetime]::MinValue) { return -1 }
|
|
51
|
-
return [int][math]::Floor(([datetime]::UtcNow - $newest).TotalDays)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function Get-ProgramRemovalHint {
|
|
55
|
-
<# .SYNOPSIS The command that removes a program: winget when it knows it, otherwise its own UninstallString. #>
|
|
56
|
-
param([pscustomobject] $Row, [hashtable] $WingetMap)
|
|
57
|
-
$key = ConvertTo-MatchToken $Row.DisplayName
|
|
58
|
-
if ($WingetMap.ContainsKey($key)) { return "winget uninstall --id $($WingetMap[$key])" }
|
|
59
|
-
if ($Row.UninstallString) { return $Row.UninstallString }
|
|
60
|
-
return '(no uninstall command recorded - remove it from Settings > Apps)'
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function Get-Targets24 {
|
|
64
|
-
<# .SYNOPSIS Informational only. Section 24 declares nothing deletable: Program Files is protected. #>
|
|
65
|
-
return @((New-Target 24 'Installed programs (registry)' 'HKLM + HKCU Uninstall hives' -Kind cmd -Note 'report only; this section never runs an uninstaller'))
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function Invoke-Section24 {
|
|
69
|
-
$ws = $Script:WS
|
|
70
|
-
Write-SectionIntro @(
|
|
71
|
-
'Programs whose files nothing has modified for a long time, largest first, with the command that removes',
|
|
72
|
-
'each one. Windows keeps no reliable record of when a program last RAN, so this is "not modified", not',
|
|
73
|
-
'"not used" - check the list before acting. This section never runs an uninstaller.'
|
|
74
|
-
)
|
|
75
|
-
$rows = @(Get-InstalledProgramRows | Where-Object { $_.SystemComponent -ne 1 })
|
|
76
|
-
if ($rows.Count -eq 0) { Write-Warn 'no uninstall entries could be read'; return }
|
|
77
|
-
Write-Note "$($rows.Count) installed program(s) found; measuring the ones with an install folder"
|
|
78
|
-
$winget = Get-WingetIdMap
|
|
79
|
-
$scored = @()
|
|
80
|
-
foreach ($r in $rows) {
|
|
81
|
-
$idle = Get-InstallLocationIdleDays -Location $r.InstallLocation
|
|
82
|
-
if ($idle -lt $ws.Days) { continue }
|
|
83
|
-
$scored += [pscustomobject]@{ Row = $r; Idle = $idle; Bytes = ([long]$r.EstimatedSizeKb * 1024) }
|
|
84
|
-
}
|
|
85
|
-
$lines = @("installed programs not modified for $($ws.Days)+ days - $(Get-Date -Format 'yyyy-MM-dd HH:mm')", '')
|
|
86
|
-
if ($scored.Count -eq 0) {
|
|
87
|
-
Write-Info "no installed program has been untouched for $($ws.Days)+ days"
|
|
88
|
-
} else {
|
|
89
|
-
Write-UiLine (" {0,-44} {1,-14} {2,10} {3,6} {4}" -f 'PROGRAM', 'VERSION', 'SIZE', 'IDLE', 'PUBLISHER') 'White'
|
|
90
|
-
foreach ($s in ($scored | Sort-Object Bytes -Descending)) {
|
|
91
|
-
$size = 'unknown'
|
|
92
|
-
if ($s.Bytes -gt 0) { $size = Format-Bytes $s.Bytes }
|
|
93
|
-
$line = " {0,-44} {1,-14} {2,10} {3,5}d {4}" -f $s.Row.DisplayName, $s.Row.DisplayVersion, $size, $s.Idle, $s.Row.Publisher
|
|
94
|
-
Write-UiLine $line 'Gray'
|
|
95
|
-
$lines += $line.Trim()
|
|
96
|
-
$lines += (' remove with: ' + (Get-ProgramRemovalHint -Row $s.Row -WingetMap $winget))
|
|
97
|
-
}
|
|
98
|
-
Write-Section 'Remove one yourself with'
|
|
99
|
-
foreach ($s in ($scored | Sort-Object Bytes -Descending | Select-Object -First 10)) {
|
|
100
|
-
Write-Plain (' ' + (Get-ProgramRemovalHint -Row $s.Row -WingetMap $winget))
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
Write-Section 'Store apps (not system packages)'
|
|
104
|
-
$appx = @()
|
|
105
|
-
try { $appx = @(Get-AppxPackage -ErrorAction SilentlyContinue | Where-Object { -not $_.IsFramework -and $_.SignatureKind -ne 'System' }) } catch { $appx = @() }
|
|
106
|
-
if ($appx.Count -eq 0) { Write-Info 'none found' } else {
|
|
107
|
-
foreach ($a in ($appx | Sort-Object Name)) {
|
|
108
|
-
Write-UiLine (" {0,-52} {1}" -f $a.Name, $a.Version) 'Gray'
|
|
109
|
-
$lines += ("store {0} {1}" -f $a.Name, $a.Version)
|
|
110
|
-
$lines += (' remove with: Remove-AppxPackage -Package ' + $a.PackageFullName)
|
|
111
|
-
}
|
|
112
|
-
Write-Note 'remove one with: Remove-AppxPackage -Package <PackageFullName>'
|
|
113
|
-
}
|
|
114
|
-
if (-not $ws.NoReport) {
|
|
115
|
-
$out = Join-Path $ws.ReportsDir ('installed-programs-' + $ws.Stamp + '.txt')
|
|
116
|
-
try { [IO.File]::WriteAllLines($out, $lines); Write-Note "list saved: $out" } catch { $null = $_ }
|
|
117
|
-
}
|
|
118
|
-
}
|
|
1
|
+
# installed_programs.ps1 - section 24: installed programs nothing has modified for N+ days. Report only.
|
|
2
|
+
#
|
|
3
|
+
# It NEVER runs an uninstaller, in any mode. Windows keeps no reliable "last launched" record, so the number
|
|
4
|
+
# reported is "not modified", derived from the files under InstallLocation - never described as "unused".
|
|
5
|
+
|
|
6
|
+
function Get-WingetIdMap {
|
|
7
|
+
<# .SYNOPSIS One winget call, parsed once: normalised program name -> package id. Empty when winget is absent. #>
|
|
8
|
+
$map = @{}
|
|
9
|
+
if (-not (Test-CommandPresent 'winget')) { return $map }
|
|
10
|
+
$lines = @()
|
|
11
|
+
try { $lines = @(& winget list --disable-interactivity 2>$null) } catch { return $map }
|
|
12
|
+
foreach ($line in $lines) {
|
|
13
|
+
$text = [string]$line
|
|
14
|
+
# "Name Id Version Source" with runs of spaces between columns; ids never contain a space.
|
|
15
|
+
if ($text -notmatch '^(.+?)\s\s+([^\s]+)\s\s+') { continue }
|
|
16
|
+
$name = ConvertTo-MatchToken $Matches[1]
|
|
17
|
+
$id = $Matches[2]
|
|
18
|
+
if ($name.Length -lt 3 -or $id -eq 'Id') { continue }
|
|
19
|
+
if (-not $map.ContainsKey($name)) { $map[$name] = $id }
|
|
20
|
+
}
|
|
21
|
+
return $map
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function Get-InstallLocationIdleDays {
|
|
25
|
+
<# .SYNOPSIS Days since the newest LAST-WRITE at depth <= 2 under an install folder, or -1 when unknowable.
|
|
26
|
+
Deliberately NOT Get-NewestTimestampUtc, which the cleanup sections use: that takes the newest of write,
|
|
27
|
+
access and creation, and last-access is live on this machine - measured 2026-09-04, every install folder
|
|
28
|
+
read as 0 days idle that way (Git, Gpg4win, RustDesk, Malwarebytes all 0d) while last-write gave 120d,
|
|
29
|
+
203d, 163d and 148d. This section asks "not modified", so last-write is the matching question. #>
|
|
30
|
+
param([string] $Location, [int] $MaxEntries = 4000)
|
|
31
|
+
if ([string]::IsNullOrWhiteSpace($Location)) { return -1 }
|
|
32
|
+
$root = Get-FullPath ($Location.Trim().Trim('"').TrimEnd('\'))
|
|
33
|
+
if (-not $root -or -not (Test-DirPresent $root)) { return -1 }
|
|
34
|
+
$newest = [datetime]::MinValue
|
|
35
|
+
$seen = 0
|
|
36
|
+
$stack = New-Object System.Collections.Generic.Stack[object]
|
|
37
|
+
$stack.Push(@($root, 0))
|
|
38
|
+
while ($stack.Count -gt 0 -and $seen -lt $MaxEntries) {
|
|
39
|
+
$item = $stack.Pop()
|
|
40
|
+
foreach ($e in (Get-ChildEntries ([string]$item[0]))) {
|
|
41
|
+
$seen++
|
|
42
|
+
if (Test-ReparsePoint $e) { continue }
|
|
43
|
+
if ($e -is [IO.DirectoryInfo]) {
|
|
44
|
+
if ([int]$item[1] -lt 2) { $stack.Push(@((Remove-LongPrefix $e.FullName), [int]$item[1] + 1)) }
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
if ($e.LastWriteTimeUtc -gt $newest) { $newest = $e.LastWriteTimeUtc }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if ($newest -eq [datetime]::MinValue) { return -1 }
|
|
51
|
+
return [int][math]::Floor(([datetime]::UtcNow - $newest).TotalDays)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function Get-ProgramRemovalHint {
|
|
55
|
+
<# .SYNOPSIS The command that removes a program: winget when it knows it, otherwise its own UninstallString. #>
|
|
56
|
+
param([pscustomobject] $Row, [hashtable] $WingetMap)
|
|
57
|
+
$key = ConvertTo-MatchToken $Row.DisplayName
|
|
58
|
+
if ($WingetMap.ContainsKey($key)) { return "winget uninstall --id $($WingetMap[$key])" }
|
|
59
|
+
if ($Row.UninstallString) { return $Row.UninstallString }
|
|
60
|
+
return '(no uninstall command recorded - remove it from Settings > Apps)'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function Get-Targets24 {
|
|
64
|
+
<# .SYNOPSIS Informational only. Section 24 declares nothing deletable: Program Files is protected. #>
|
|
65
|
+
return @((New-Target 24 'Installed programs (registry)' 'HKLM + HKCU Uninstall hives' -Kind cmd -Note 'report only; this section never runs an uninstaller'))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function Invoke-Section24 {
|
|
69
|
+
$ws = $Script:WS
|
|
70
|
+
Write-SectionIntro @(
|
|
71
|
+
'Programs whose files nothing has modified for a long time, largest first, with the command that removes',
|
|
72
|
+
'each one. Windows keeps no reliable record of when a program last RAN, so this is "not modified", not',
|
|
73
|
+
'"not used" - check the list before acting. This section never runs an uninstaller.'
|
|
74
|
+
)
|
|
75
|
+
$rows = @(Get-InstalledProgramRows | Where-Object { $_.SystemComponent -ne 1 })
|
|
76
|
+
if ($rows.Count -eq 0) { Write-Warn 'no uninstall entries could be read'; return }
|
|
77
|
+
Write-Note "$($rows.Count) installed program(s) found; measuring the ones with an install folder"
|
|
78
|
+
$winget = Get-WingetIdMap
|
|
79
|
+
$scored = @()
|
|
80
|
+
foreach ($r in $rows) {
|
|
81
|
+
$idle = Get-InstallLocationIdleDays -Location $r.InstallLocation
|
|
82
|
+
if ($idle -lt $ws.Days) { continue }
|
|
83
|
+
$scored += [pscustomobject]@{ Row = $r; Idle = $idle; Bytes = ([long]$r.EstimatedSizeKb * 1024) }
|
|
84
|
+
}
|
|
85
|
+
$lines = @("installed programs not modified for $($ws.Days)+ days - $(Get-Date -Format 'yyyy-MM-dd HH:mm')", '')
|
|
86
|
+
if ($scored.Count -eq 0) {
|
|
87
|
+
Write-Info "no installed program has been untouched for $($ws.Days)+ days"
|
|
88
|
+
} else {
|
|
89
|
+
Write-UiLine (" {0,-44} {1,-14} {2,10} {3,6} {4}" -f 'PROGRAM', 'VERSION', 'SIZE', 'IDLE', 'PUBLISHER') 'White'
|
|
90
|
+
foreach ($s in ($scored | Sort-Object Bytes -Descending)) {
|
|
91
|
+
$size = 'unknown'
|
|
92
|
+
if ($s.Bytes -gt 0) { $size = Format-Bytes $s.Bytes }
|
|
93
|
+
$line = " {0,-44} {1,-14} {2,10} {3,5}d {4}" -f $s.Row.DisplayName, $s.Row.DisplayVersion, $size, $s.Idle, $s.Row.Publisher
|
|
94
|
+
Write-UiLine $line 'Gray'
|
|
95
|
+
$lines += $line.Trim()
|
|
96
|
+
$lines += (' remove with: ' + (Get-ProgramRemovalHint -Row $s.Row -WingetMap $winget))
|
|
97
|
+
}
|
|
98
|
+
Write-Section 'Remove one yourself with'
|
|
99
|
+
foreach ($s in ($scored | Sort-Object Bytes -Descending | Select-Object -First 10)) {
|
|
100
|
+
Write-Plain (' ' + (Get-ProgramRemovalHint -Row $s.Row -WingetMap $winget))
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
Write-Section 'Store apps (not system packages)'
|
|
104
|
+
$appx = @()
|
|
105
|
+
try { $appx = @(Get-AppxPackage -ErrorAction SilentlyContinue | Where-Object { -not $_.IsFramework -and $_.SignatureKind -ne 'System' }) } catch { $appx = @() }
|
|
106
|
+
if ($appx.Count -eq 0) { Write-Info 'none found' } else {
|
|
107
|
+
foreach ($a in ($appx | Sort-Object Name)) {
|
|
108
|
+
Write-UiLine (" {0,-52} {1}" -f $a.Name, $a.Version) 'Gray'
|
|
109
|
+
$lines += ("store {0} {1}" -f $a.Name, $a.Version)
|
|
110
|
+
$lines += (' remove with: Remove-AppxPackage -Package ' + $a.PackageFullName)
|
|
111
|
+
}
|
|
112
|
+
Write-Note 'remove one with: Remove-AppxPackage -Package <PackageFullName>'
|
|
113
|
+
}
|
|
114
|
+
if (-not $ws.NoReport) {
|
|
115
|
+
$out = Join-Path $ws.ReportsDir ('installed-programs-' + $ws.Stamp + '.txt')
|
|
116
|
+
try { [IO.File]::WriteAllLines($out, $lines); Write-Note "list saved: $out" } catch { $null = $_ }
|
|
117
|
+
}
|
|
118
|
+
}
|
package/modules/menu.ps1
CHANGED
|
@@ -1,63 +1,63 @@
|
|
|
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
|
-
$lo = ($Script:WS_SECTIONS | Measure-Object Id -Minimum).Minimum
|
|
41
|
-
$hi = ($Script:WS_SECTIONS | Measure-Object Id -Maximum).Maximum
|
|
42
|
-
$choice = Read-Choice -Prompt " Select [$lo-$hi / A / D / Y / S / Q]: " -Default ''
|
|
43
|
-
if ($choice -eq '') { continue }
|
|
44
|
-
switch -Regex ($choice) {
|
|
45
|
-
'^q' { Show-SessionSummary; return }
|
|
46
|
-
'^a' {
|
|
47
|
-
$ids = @($Script:WS_SAFE_BATCH)
|
|
48
|
-
if ($ws.IsAdmin) { $ids += $Script:WS_SAFE_BATCH_ADMIN }
|
|
49
|
-
$ws.BatchMode = $true
|
|
50
|
-
try { foreach ($id in $ids) { Invoke-SectionById -Id $id } } finally { $ws.BatchMode = $false }
|
|
51
|
-
Wait-Enter
|
|
52
|
-
}
|
|
53
|
-
'^d' { $ws.DryRun = -not $ws.DryRun }
|
|
54
|
-
'^y' { $ws.Yes = -not $ws.Yes }
|
|
55
|
-
'^s' { Show-SessionSummary; Wait-Enter }
|
|
56
|
-
'^\d+$' {
|
|
57
|
-
$id = [int]$choice
|
|
58
|
-
if (Get-Section $id) { Invoke-SectionById -Id $id; Wait-Enter } else { Write-Warn "no section $id" }
|
|
59
|
-
}
|
|
60
|
-
default { Write-Warn "unrecognised choice: $choice" }
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
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
|
+
$lo = ($Script:WS_SECTIONS | Measure-Object Id -Minimum).Minimum
|
|
41
|
+
$hi = ($Script:WS_SECTIONS | Measure-Object Id -Maximum).Maximum
|
|
42
|
+
$choice = Read-Choice -Prompt " Select [$lo-$hi / A / D / Y / S / Q]: " -Default ''
|
|
43
|
+
if ($choice -eq '') { continue }
|
|
44
|
+
switch -Regex ($choice) {
|
|
45
|
+
'^q' { Show-SessionSummary; return }
|
|
46
|
+
'^a' {
|
|
47
|
+
$ids = @($Script:WS_SAFE_BATCH)
|
|
48
|
+
if ($ws.IsAdmin) { $ids += $Script:WS_SAFE_BATCH_ADMIN }
|
|
49
|
+
$ws.BatchMode = $true
|
|
50
|
+
try { foreach ($id in $ids) { Invoke-SectionById -Id $id } } finally { $ws.BatchMode = $false }
|
|
51
|
+
Wait-Enter
|
|
52
|
+
}
|
|
53
|
+
'^d' { $ws.DryRun = -not $ws.DryRun }
|
|
54
|
+
'^y' { $ws.Yes = -not $ws.Yes }
|
|
55
|
+
'^s' { Show-SessionSummary; Wait-Enter }
|
|
56
|
+
'^\d+$' {
|
|
57
|
+
$id = [int]$choice
|
|
58
|
+
if (Get-Section $id) { Invoke-SectionById -Id $id; Wait-Enter } else { Write-Warn "no section $id" }
|
|
59
|
+
}
|
|
60
|
+
default { Write-Warn "unrecognised choice: $choice" }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|