windowsweep 1.0.0 → 1.0.1
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 +29 -0
- package/README.md +13 -11
- package/VERSION +1 -1
- package/lib/actions.ps1 +1 -1
- package/lib/config.ps1 +9 -0
- package/lib/constants.ps1 +4 -4
- package/lib/log.ps1 +1 -1
- package/lib/safety.ps1 +10 -10
- package/lib/ui.ps1 +10 -7
- package/modules/crash_trap.ps1 +2 -2
- package/modules/docker.ps1 +1 -1
- package/modules/editors.ps1 +2 -1
- package/modules/personal.ps1 +1 -1
- package/modules/projects.ps1 +5 -2
- package/modules/release_helpers.ps1 +21 -1
- package/modules/runner.ps1 +1 -1
- package/modules/self_test_extra.ps1 +86 -0
- package/package.json +1 -2
- package/windowsweep.ps1 +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,35 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [1.0.1] - 2026-09-03
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Sections 17, 18 and 19 no longer pre-select every item under `--yes`.** In the walkthrough and the menu
|
|
15
|
+
with auto-yes on, section 17 could remove every listed build artefact without a person choosing one, and
|
|
16
|
+
18/19 pre-selected everything before their final question. The selection prompt now appears even with
|
|
17
|
+
`--yes`, defaults to none, and section 17's final confirmation is never auto-answered. Batch mode already
|
|
18
|
+
refused these sections and is unchanged. (RW-002)
|
|
19
|
+
- Section 19's title no longer names Desktop; only Downloads is scanned. (RW-003)
|
|
20
|
+
- Sections 18 and 19 report the tier `recycle` instead of `permanent` in `--list`. (RW-004)
|
|
21
|
+
- `--purge-all` from a console asks you to type `purge` once per run, as documented; `--yes` remains the
|
|
22
|
+
confirmation in batch runs. (RW-005)
|
|
23
|
+
- A running editor's VSIX download cache is cleared as documented: it is its own target without the
|
|
24
|
+
running-editor guard. (RW-006)
|
|
25
|
+
- `--install-task` and `--install-alias` refuse to run under `npx` (exit 3) and print the global-install
|
|
26
|
+
steps; the npx cache is evicted and the task or alias would break later. (RW-007)
|
|
27
|
+
- The engine exits 130 when a run is interrupted before it finished; previously only the Node launcher
|
|
28
|
+
did. (RW-008)
|
|
29
|
+
- `--uninstall-data` always asks; `--yes` no longer removes your history unattended. (RW-010)
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- Internal rename `Write-Log` -> `Write-LogLine` (18 call sites) so PSScriptAnalyzer's
|
|
34
|
+
`PSAvoidOverwritingBuiltInCmdlets` passes under PowerShell 7 in CI. No behaviour change.
|
|
35
|
+
- Self-test: four new checks prove the `--yes` asymmetry at the helper and at the section 17/18/19 call sites,
|
|
36
|
+
and that every picker call in `modules/` carries `-NoAutoYes` (114 checks).
|
|
37
|
+
- Keywords trimmed to twelve (`temp-files` dropped). (RW-011)
|
|
38
|
+
|
|
10
39
|
## [1.0.0] - 2026-09-03
|
|
11
40
|
|
|
12
41
|
First release. The Windows member of the cleanup family beside
|
package/README.md
CHANGED
|
@@ -34,12 +34,12 @@ It is the Windows member of a family with [linux-cleanup](https://github.com/aon
|
|
|
34
34
|
|
|
35
35
|
| | |
|
|
36
36
|
|---|---|
|
|
37
|
-
| **Version** | `1.0.
|
|
37
|
+
| **Version** | `1.0.1` |
|
|
38
38
|
| **License** | MIT |
|
|
39
39
|
| **Node** | `>=14` (launcher only) |
|
|
40
40
|
| **Runtime** | Windows PowerShell 5.1 (built in) or PowerShell 7 |
|
|
41
41
|
| **Platforms** | Windows 10 (1809+) and Windows 11 |
|
|
42
|
-
| **Install size** | ~
|
|
42
|
+
| **Install size** | ~81 kB packed · ~273 kB unpacked · 38 files · no dependencies |
|
|
43
43
|
| **Undo** | Recycle Bin for personal files; none for caches (they regenerate) |
|
|
44
44
|
| **Status** | Stable · actively maintained |
|
|
45
45
|
|
|
@@ -119,8 +119,8 @@ security scanner or a registry cleaner. It reclaims disk space, nothing else.
|
|
|
119
119
|
- **Running-app guard** - an open browser, editor or app keeps its caches; the tool tells you which to close.
|
|
120
120
|
- **Session reports** - schema-versioned JSON, exportable to Markdown or a self-contained HTML page, plus
|
|
121
121
|
`--json` for scripts.
|
|
122
|
-
- **Self-test** -
|
|
123
|
-
fixture before you trust it.
|
|
122
|
+
- **Self-test** - 114 checks prove the guards on your machine with a real junction, a 400-character path and
|
|
123
|
+
a dry-run fixture before you trust it.
|
|
124
124
|
- **Offline by design** - zero network calls, no telemetry, no update check. Crash bundles stay on disk.
|
|
125
125
|
|
|
126
126
|
<a id="platform-support"></a>
|
|
@@ -128,9 +128,9 @@ security scanner or a registry cleaner. It reclaims disk space, nothing else.
|
|
|
128
128
|
|
|
129
129
|
| Platform | Supported | Notes |
|
|
130
130
|
|---|---|---|
|
|
131
|
-
| Windows 11 | ✅ |
|
|
132
|
-
| Windows 10 (1809 and later) | ✅ | The primary development target |
|
|
133
|
-
| Windows Server 2019+ | ⚠️ | Uses nothing newer than 1809, but
|
|
131
|
+
| Windows 11 | ✅ | Same engine and PowerShell hosts; a real run on Windows 11 is on the verification list ([roadmap](https://github.com/aoneahsan/windowsweep/blob/main/remaining-work-summary.md)) |
|
|
132
|
+
| Windows 10 (1809 and later) | ✅ | The primary development target; every real run so far |
|
|
133
|
+
| Windows Server 2019+ | ⚠️ | Uses nothing newer than 1809; CI runs the self-test and a dry-run on Windows Server (`windows-latest`) on every push, but no real cleanup has been run on Server |
|
|
134
134
|
| Linux | ❌ | `os: ["win32"]` makes npm refuse to install; use [linux-cleanup](https://github.com/aoneahsan/linux-cleanup) |
|
|
135
135
|
| macOS | ❌ | Use [macleanup](https://github.com/aoneahsan/macleanup) |
|
|
136
136
|
|
|
@@ -195,8 +195,8 @@ section with `a` run / `s` skip / `q` quit and a running total.
|
|
|
195
195
|
### The sections
|
|
196
196
|
|
|
197
197
|
**Tier** says what happens to the data: *report* deletes nothing, *rebuilds* comes back on its own, *slow*
|
|
198
|
-
comes back but costs minutes, *Recycle Bin* is recoverable until you empty it
|
|
199
|
-
changes a setting.
|
|
198
|
+
comes back but costs minutes, *Recycle Bin* is recoverable until you empty it (`--list` shows this tier as
|
|
199
|
+
`recycle`), *permanent* is not, *config* changes a setting.
|
|
200
200
|
|
|
201
201
|
| # | Section | Tier | Admin | Batch |
|
|
202
202
|
|---|---|---|:-:|---|
|
|
@@ -408,11 +408,13 @@ More: [FAQ](https://github.com/aoneahsan/windowsweep/blob/main/docs/faq.md).
|
|
|
408
408
|
| [Admin sections and elevation](https://github.com/aoneahsan/windowsweep/blob/main/docs/admin-and-elevation.md) | before running the system profile or touching the hibernation file |
|
|
409
409
|
| [Reports and logs](https://github.com/aoneahsan/windowsweep/blob/main/docs/reports-and-logs.md) | parsing the JSON or finding a log |
|
|
410
410
|
| [Troubleshooting](https://github.com/aoneahsan/windowsweep/blob/main/docs/troubleshooting.md) | something failed |
|
|
411
|
+
| [Roadmap and remaining work](https://github.com/aoneahsan/windowsweep/blob/main/remaining-work-summary.md) | you want to know what is verified, what is planned (1.1, the docs site) and what is still open |
|
|
411
412
|
|
|
412
413
|
<a id="changelog"></a>
|
|
413
414
|
## 🔄 Changelog [#](#changelog)
|
|
414
415
|
|
|
415
|
-
Latest release: **`1.0.
|
|
416
|
+
Latest release: **`1.0.1`** - `--yes` never selects personal or project items; installers refuse under npx;
|
|
417
|
+
exit 130 from the engine; documentation fixes.
|
|
416
418
|
Full history: [CHANGELOG.md](https://github.com/aoneahsan/windowsweep/blob/main/CHANGELOG.md).
|
|
417
419
|
|
|
418
420
|
<a id="contributing"></a>
|
|
@@ -478,5 +480,5 @@ this tool deletes files, so review what it proposes before confirming.
|
|
|
478
480
|
<a id="keywords"></a>
|
|
479
481
|
## 🏷️ Keywords [#](#keywords)
|
|
480
482
|
|
|
481
|
-
*windows · cleanup · disk-cleanup · cache-cleanup · disk-space ·
|
|
483
|
+
*windows · cleanup · disk-cleanup · cache-cleanup · disk-space · node-modules · npm-cache ·
|
|
482
484
|
yarn-cache · developer-tools · powershell · cli · dry-run*
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.1
|
package/lib/actions.ps1
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
$Script:WS_CHROMIUM_PROFILE_CACHES = @('Cache', 'Code Cache', 'GPUCache', 'DawnCache', 'DawnGraphiteCache', 'DawnWebGPUCache', 'GrShaderCache', 'ShaderCache', 'Service Worker\ScriptCache')
|
|
5
5
|
$Script:WS_CHROMIUM_ROOT_CACHES = @('ShaderCache', 'GrShaderCache', 'GraphiteDawnCache', 'Crashpad\completed', 'Crashpad\reports', 'SwReporter')
|
|
6
6
|
$Script:WS_ELECTRON_CACHES = @('Cache', 'Code Cache', 'GPUCache', 'DawnCache', 'DawnGraphiteCache', 'DawnWebGPUCache', 'GrShaderCache', 'ShaderCache', 'Service Worker\CacheStorage', 'Service Worker\ScriptCache', 'Crashpad\completed', 'Crashpad\reports')
|
|
7
|
-
$Script:WS_EDITOR_CACHES = @('Cache', 'CachedData', 'CachedProfilesData', '
|
|
7
|
+
$Script:WS_EDITOR_CACHES = @('Cache', 'CachedData', 'CachedProfilesData', 'Code Cache', 'GPUCache', 'DawnCache', 'DawnGraphiteCache', 'DawnWebGPUCache', 'Service Worker\CacheStorage', 'Service Worker\ScriptCache', 'Crashpad\completed', 'Crashpad\reports')
|
|
8
8
|
$Script:WS_FIREFOX_CACHES = @('cache2', 'startupCache', 'thumbnails', 'jumpListCache', 'OfflineCache', 'shader-cache')
|
|
9
9
|
|
|
10
10
|
function Get-ChromiumProfileDirs {
|
package/lib/config.ps1
CHANGED
|
@@ -88,3 +88,12 @@ function Resolve-DeveloperMode {
|
|
|
88
88
|
$ws.DeveloperSource = 'default'
|
|
89
89
|
Write-Note 'developer question not asked (non-interactive run) - defaulting to developer mode ON, the conservative choice; pass --not-developer to override'
|
|
90
90
|
}
|
|
91
|
+
|
|
92
|
+
function Confirm-PurgeAllOnce {
|
|
93
|
+
<# .SYNOPSIS --purge-all from a console asks for a typed word once per run; --yes is the confirmation in batch runs. #>
|
|
94
|
+
$ws = $Script:WS
|
|
95
|
+
if (-not $ws.PurgeAll -or $ws.DryRun -or $ws.Yes) { return }
|
|
96
|
+
if (Confirm-Typed -Prompt 'FULL PURGE: --purge-all empties every cache target completely, including files you used yesterday.' -Word 'purge') { Write-Ok 'purge-all confirmed for this run'; return }
|
|
97
|
+
$ws.PurgeAll = $false
|
|
98
|
+
Write-Info 'purge-all declined - this run prunes by the idle window instead'
|
|
99
|
+
}
|
package/lib/constants.ps1
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# Single source of truth for the version is package.json: the Node launcher exports it as
|
|
5
5
|
# WINDOWSWEEP_VERSION. A direct checkout falls back to this literal; the version:check script
|
|
6
6
|
# asserts the two never drift.
|
|
7
|
-
$Script:WS_VERSION_FALLBACK = '1.0.
|
|
7
|
+
$Script:WS_VERSION_FALLBACK = '1.0.1'
|
|
8
8
|
|
|
9
9
|
$Script:WS_NAME = 'windowsweep'
|
|
10
10
|
$Script:WS_TAGLINE = 'Safe-by-default Windows cleanup CLI - developer-aware, dry-run first, zero install via npx.'
|
|
@@ -29,7 +29,7 @@ $Script:WS_EXIT_INTERRUPT = 130
|
|
|
29
29
|
|
|
30
30
|
# Section catalogue. Numbering is a public contract frozen at 1.0.0: a section may be retired
|
|
31
31
|
# (kept as a no-op that says so) but a number is never reused for something else.
|
|
32
|
-
# Tier: report | rebuilds | slow | permanent | config
|
|
32
|
+
# Tier: report | rebuilds | slow | recycle | permanent | config
|
|
33
33
|
# Batch: safe (in --all) | optin (--only/--profile with --yes) | deep (needs --i-understand-deep)
|
|
34
34
|
# | interactive (never batch; dry-run/report allowed)
|
|
35
35
|
$Script:WS_SECTIONS = @(
|
|
@@ -51,8 +51,8 @@ $Script:WS_SECTIONS = @(
|
|
|
51
51
|
@{ Id = 15; Key = 'hiberfil'; Title = 'Hibernation file (off / reduced)'; Tier = 'config'; Admin = $true; Batch = 'deep'; Dev = $false; Fn = 'Invoke-Section15' }
|
|
52
52
|
@{ Id = 16; Key = 'eventlogs'; Title = 'Clear Windows Event Logs - PERMANENT'; Tier = 'permanent'; Admin = $true; Batch = 'deep'; Dev = $false; Fn = 'Invoke-Section16' }
|
|
53
53
|
@{ Id = 17; Key = 'projects'; Title = 'Stale project build artefacts (node_modules, dist, .next, target, ...)'; Tier = 'rebuilds'; Admin = $false; Batch = 'interactive'; Dev = $true; Fn = 'Invoke-Section17' }
|
|
54
|
-
@{ Id = 18; Key = 'partials'; Title = 'Partial / orphan downloads -> Recycle Bin'; Tier = '
|
|
55
|
-
@{ Id = 19; Key = 'large'; Title = 'Large stale personal files (Downloads
|
|
54
|
+
@{ Id = 18; Key = 'partials'; Title = 'Partial / orphan downloads -> Recycle Bin'; Tier = 'recycle'; Admin = $false; Batch = 'interactive'; Dev = $false; Fn = 'Invoke-Section18' }
|
|
55
|
+
@{ Id = 19; Key = 'large'; Title = 'Large stale personal files (Downloads) -> Recycle Bin'; Tier = 'recycle'; Admin = $false; Batch = 'interactive'; Dev = $false; Fn = 'Invoke-Section19' }
|
|
56
56
|
@{ Id = 20; Key = 'vhdx'; Title = 'Docker Desktop / WSL disk image compaction (stops Docker + WSL)'; Tier = 'config'; Admin = $true; Batch = 'deep'; Dev = $true; Fn = 'Invoke-Section20' }
|
|
57
57
|
@{ Id = 21; Key = 'diskusage'; Title = 'Disk usage report (largest entries, drives, disk images)'; Tier = 'report'; Admin = $false; Batch = 'safe'; Dev = $false; Fn = 'Invoke-Section21' }
|
|
58
58
|
)
|
package/lib/log.ps1
CHANGED
|
@@ -29,7 +29,7 @@ function Initialize-Log {
|
|
|
29
29
|
try { [IO.File]::WriteAllLines($ws.LogFile, $header) } catch { $ws.LogFile = $null }
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function Write-
|
|
32
|
+
function Write-LogLine {
|
|
33
33
|
<# .SYNOPSIS Append one timestamped line to the run log; never throws. #>
|
|
34
34
|
param([string] $Text)
|
|
35
35
|
$f = $Script:WS.LogFile
|
package/lib/safety.ps1
CHANGED
|
@@ -155,7 +155,7 @@ function Remove-TreeInternal {
|
|
|
155
155
|
try {
|
|
156
156
|
if ($isLink) {
|
|
157
157
|
if ($isDir) { [IO.Directory]::Delete($e.FullName, $false) } else { [IO.File]::Delete($e.FullName) }
|
|
158
|
-
Write-
|
|
158
|
+
Write-LogLine ("removed link (not followed): " + (Remove-LongPrefix $e.FullName))
|
|
159
159
|
} elseif ($isDir) {
|
|
160
160
|
if (-not (Remove-TreeInternal -Path $e.FullName -Result $Result)) { $allClear = $false }
|
|
161
161
|
} else {
|
|
@@ -206,24 +206,24 @@ function Remove-PathSafe {
|
|
|
206
206
|
if ($isLink) {
|
|
207
207
|
if ($isDir) { [IO.Directory]::Delete($lp, $false) } else { [IO.File]::Delete($lp) }
|
|
208
208
|
$r.Removed = $true
|
|
209
|
-
Write-
|
|
209
|
+
Write-LogLine "removed link (not followed): $p$tag"
|
|
210
210
|
} elseif ($isDir) {
|
|
211
211
|
$r.Removed = Remove-TreeInternal -Path $p -Result $r
|
|
212
212
|
$r.Bytes = [math]::Max([long]0, $bytes - $r.SkippedBytes)
|
|
213
213
|
if ($r.Skipped -gt 0) { Write-Note "$($r.Skipped) item(s) in use or inaccessible, left in place under $p" }
|
|
214
|
-
Write-
|
|
214
|
+
Write-LogLine ("removed dir: $p ($($r.Bytes) bytes, skipped $($r.Skipped))$tag")
|
|
215
215
|
} else {
|
|
216
216
|
Remove-ReadOnlyAttribute $lp
|
|
217
217
|
[IO.File]::Delete($lp)
|
|
218
218
|
$r.Removed = $true; $r.Bytes = $bytes; $r.Files = 1
|
|
219
|
-
Write-
|
|
219
|
+
Write-LogLine "removed file: $p ($bytes bytes)$tag"
|
|
220
220
|
}
|
|
221
221
|
Add-Freed $r.Bytes
|
|
222
222
|
} catch {
|
|
223
223
|
$r.Skipped++
|
|
224
224
|
$r.Reason = 'in use or access denied'
|
|
225
225
|
Write-Note "in use or inaccessible, skipped: $p"
|
|
226
|
-
Write-
|
|
226
|
+
Write-LogLine "skip (locked): $p - $($_.Exception.Message)"
|
|
227
227
|
}
|
|
228
228
|
return $r
|
|
229
229
|
}
|
|
@@ -272,7 +272,7 @@ function Remove-StaleFiles {
|
|
|
272
272
|
} catch { $null = $_ }
|
|
273
273
|
}
|
|
274
274
|
Add-Freed $out.Freed
|
|
275
|
-
Write-
|
|
275
|
+
Write-LogLine ("pruned $($out.Files) files ($($out.Freed) bytes) idle $Days+ days from $root; skipped $($out.Skipped)")
|
|
276
276
|
Write-Ok ("$Label - pruned {0} in {1} files idle {2}+ days ({3} skipped in use)" -f (Format-Bytes $out.Freed), $out.Files, $Days, $out.Skipped)
|
|
277
277
|
return $out
|
|
278
278
|
}
|
|
@@ -405,7 +405,7 @@ function Send-ToRecycleBin {
|
|
|
405
405
|
}
|
|
406
406
|
$r.Removed = $true; $r.Bytes = $bytes
|
|
407
407
|
Add-Freed $bytes
|
|
408
|
-
Write-
|
|
408
|
+
Write-LogLine "recycled: $p ($bytes bytes)"
|
|
409
409
|
Write-Ok ("moved to Recycle Bin: {0} ({1})" -f $p, (Format-Bytes $bytes))
|
|
410
410
|
} catch {
|
|
411
411
|
$r.Skipped++; $r.Reason = $_.Exception.Message
|
|
@@ -424,14 +424,14 @@ function Invoke-External {
|
|
|
424
424
|
Write-DryRun "would run: $display"
|
|
425
425
|
return [pscustomobject]@{ Ran = $false; ExitCode = 0; Output = @() }
|
|
426
426
|
}
|
|
427
|
-
Write-
|
|
427
|
+
Write-LogLine "run${tag}: $display"
|
|
428
428
|
$lines = @()
|
|
429
429
|
try {
|
|
430
430
|
$raw = & $FilePath @ArgumentList 2>&1
|
|
431
431
|
foreach ($l in $raw) {
|
|
432
432
|
$s = [string]$l
|
|
433
433
|
$lines += $s
|
|
434
|
-
if (-not $Quiet) { Write-Note $s } else { Write-
|
|
434
|
+
if (-not $Quiet) { Write-Note $s } else { Write-LogLine $s }
|
|
435
435
|
}
|
|
436
436
|
$code = $LASTEXITCODE
|
|
437
437
|
if ($null -eq $code) { $code = 0 }
|
|
@@ -439,7 +439,7 @@ function Invoke-External {
|
|
|
439
439
|
$lines += $_.Exception.Message
|
|
440
440
|
$code = 1
|
|
441
441
|
}
|
|
442
|
-
Write-
|
|
442
|
+
Write-LogLine "exit $code$tag : $display"
|
|
443
443
|
return [pscustomobject]@{ Ran = $true; ExitCode = $code; Output = $lines }
|
|
444
444
|
}
|
|
445
445
|
|
package/lib/ui.ps1
CHANGED
|
@@ -37,7 +37,7 @@ function Write-UiLine {
|
|
|
37
37
|
<# .SYNOPSIS Print one line (coloured when allowed) and append it to the log. #>
|
|
38
38
|
param([string] $Text = '', [string] $Color = 'Gray', [switch] $NoLog, [switch] $NoNewline)
|
|
39
39
|
$ws = $Script:WS
|
|
40
|
-
if (-not $NoLog) { Write-
|
|
40
|
+
if (-not $NoLog) { Write-LogLine $Text }
|
|
41
41
|
if ($ws.Mute) { return }
|
|
42
42
|
if ($ws.JsonMode) { [Console]::Error.WriteLine($Text); return }
|
|
43
43
|
if ($ws.Color) {
|
|
@@ -49,7 +49,7 @@ function Write-UiLine {
|
|
|
49
49
|
|
|
50
50
|
function Write-Info {
|
|
51
51
|
param([string] $Text)
|
|
52
|
-
if ($Script:WS.Quiet) { Write-
|
|
52
|
+
if ($Script:WS.Quiet) { Write-LogLine "INFO $Text"; return }
|
|
53
53
|
Write-UiLine ("$($Script:WS.Glyph.info) $Text") 'Cyan'
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -70,7 +70,7 @@ function Write-Err {
|
|
|
70
70
|
|
|
71
71
|
function Write-Note {
|
|
72
72
|
param([string] $Text)
|
|
73
|
-
if ($Script:WS.Quiet) { Write-
|
|
73
|
+
if ($Script:WS.Quiet) { Write-LogLine "NOTE $Text"; return }
|
|
74
74
|
Write-UiLine (" $Text") 'DarkGray'
|
|
75
75
|
}
|
|
76
76
|
|
|
@@ -154,7 +154,7 @@ function Read-Line {
|
|
|
154
154
|
if ($Prompt) { Write-UiLine $Prompt 'White' -NoNewline -NoLog }
|
|
155
155
|
try { $line = [Console]::In.ReadLine() } catch { return $null }
|
|
156
156
|
if ($null -eq $line) { return $null }
|
|
157
|
-
Write-
|
|
157
|
+
Write-LogLine ("> $line")
|
|
158
158
|
return $line
|
|
159
159
|
}
|
|
160
160
|
|
|
@@ -187,10 +187,13 @@ function Confirm-Typed {
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
function Read-MultiSelect {
|
|
190
|
-
<# .SYNOPSIS Parse "1,3,5-7" / all / none into a sorted unique list of 1-based indexes.
|
|
191
|
-
|
|
190
|
+
<# .SYNOPSIS Parse "1,3,5-7" / all / none into a sorted unique list of 1-based indexes.
|
|
191
|
+
--yes answers "all" only for callers that do not pass -NoAutoYes. The personal and project pickers pass
|
|
192
|
+
it, so under --yes they still ask when a console is present and select nothing when it is not. #>
|
|
193
|
+
param([int] $Total, [switch] $NoAutoYes)
|
|
192
194
|
if ($Total -le 0) { return @() }
|
|
193
|
-
if ($Script:WS.Yes) { return @(1..$Total) }
|
|
195
|
+
if ($Script:WS.Yes -and -not $NoAutoYes) { return @(1..$Total) }
|
|
196
|
+
if (-not $Script:WS.Interactive) { Write-Note 'non-interactive session: nothing selected (a person has to pick these)'; return @() }
|
|
194
197
|
Write-UiLine " Selection: 1,3,5-10 | all | none (Enter = none)" 'DarkGray' -NoLog
|
|
195
198
|
$reply = Read-Line (" Select items (1..$Total): ")
|
|
196
199
|
return (ConvertTo-IndexList -Text $reply -Total $Total)
|
package/modules/crash_trap.ps1
CHANGED
|
@@ -73,8 +73,8 @@ function Invoke-CrashBundle {
|
|
|
73
73
|
$ws = $Script:WS
|
|
74
74
|
$msg = $ErrorRecord.Exception.Message
|
|
75
75
|
$where = $ErrorRecord.InvocationInfo.PositionMessage
|
|
76
|
-
Write-
|
|
77
|
-
Write-
|
|
76
|
+
Write-LogLine "CRASH: $msg"
|
|
77
|
+
Write-LogLine $ErrorRecord.ScriptStackTrace
|
|
78
78
|
$extra = @("=== Error ===", $msg, $where, '', '=== Stack ===', $ErrorRecord.ScriptStackTrace) -join "`r`n"
|
|
79
79
|
$zip = New-BundleZip -Prefix 'crash' -Manifest (New-SystemManifest -Kind 'CRASH REPORT' -Extra $extra)
|
|
80
80
|
Write-UiLine '' 'Gray'
|
package/modules/docker.ps1
CHANGED
|
@@ -69,7 +69,7 @@ function Invoke-Section20 {
|
|
|
69
69
|
foreach ($img in $images) { Write-UiLine (" {0,3} {1,10} {2}" -f $i, (Format-Bytes $img.Length), $img.FullName) 'Gray'; $i++ }
|
|
70
70
|
if (-not (Test-CommandPresent 'diskpart.exe')) { Write-Warn 'diskpart.exe not found - skipped'; return }
|
|
71
71
|
$picks = @()
|
|
72
|
-
if ($ws.Yes) { $picks = @(1..$images.Count) } else { $picks = Read-MultiSelect -Total $images.Count }
|
|
72
|
+
if ($ws.Yes) { $picks = @(1..$images.Count) } else { $picks = @(Read-MultiSelect -Total $images.Count -NoAutoYes) }
|
|
73
73
|
if ($picks.Count -eq 0) { Write-Info 'nothing selected'; return }
|
|
74
74
|
if (-not (Confirm-Section 'Stop Docker Desktop and WSL now and compact the selected disks?' 'n')) { Write-Info 'skipped'; return }
|
|
75
75
|
if ($ws.DryRun) { foreach ($k in $picks) { Write-DryRun "would compact $($images[$k - 1].FullName)" }; return }
|
package/modules/editors.ps1
CHANGED
|
@@ -13,7 +13,8 @@ function Get-Targets06 {
|
|
|
13
13
|
$t = @()
|
|
14
14
|
foreach ($e in $Script:WS_EDITORS) {
|
|
15
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,
|
|
16
|
+
$t += (New-Target 6 "$($e.Name) caches" $root -Kind editor -Guard $e.Proc -Note 'Cache, CachedData, Code Cache, GPUCache, crash reports; skipped while the editor runs')
|
|
17
|
+
$t += (New-Target 6 "$($e.Name) VSIX download cache" (Join-Path $root 'CachedExtensionVSIXs') -Mode clear -Note 'extension installers already unpacked; safe while the editor runs')
|
|
17
18
|
$t += (New-Target 6 "$($e.Name) logs" (Join-Path $root 'logs') -Mode clear-old -Days 7)
|
|
18
19
|
$t += (New-Target 6 "$($e.Name) stale workspace storage" (Join-Path $root 'User\workspaceStorage') -Kind cmd -Guard $e.Proc -Note 'entries whose folder no longer exists')
|
|
19
20
|
$t += (New-Target 6 "$($e.Name) superseded extension versions" (Join-Path $P.U "$($e.Ext)\extensions") -Kind cmd -Guard $e.Proc -Note 'older versions and .obsolete entries; newest kept')
|
package/modules/personal.ps1
CHANGED
|
@@ -84,7 +84,7 @@ function Invoke-PersonalPicker {
|
|
|
84
84
|
$dest = 'the Recycle Bin'
|
|
85
85
|
if ($ws.Permanent) { $dest = 'PERMANENT deletion (--permanent)' }
|
|
86
86
|
Write-Note "selected files go to $dest"
|
|
87
|
-
$picks = Read-MultiSelect -Total $Rows.Count
|
|
87
|
+
$picks = @(Read-MultiSelect -Total $Rows.Count -NoAutoYes)
|
|
88
88
|
if ($picks.Count -eq 0) { Write-Info 'nothing selected'; return }
|
|
89
89
|
if (-not $ws.DryRun) {
|
|
90
90
|
# Personal files never auto-confirm: --yes does not apply here.
|
package/modules/projects.ps1
CHANGED
|
@@ -129,9 +129,12 @@ function Invoke-Section17 {
|
|
|
129
129
|
$out = Join-Path $ws.ReportsDir ('stale-builds-' + $ws.Stamp + '.txt')
|
|
130
130
|
try { [IO.File]::WriteAllLines($out, $reportLines); Write-Note "list saved: $out" } catch { $null = $_ }
|
|
131
131
|
}
|
|
132
|
-
$picks = Read-MultiSelect -Total ([math]::Min($found.Count, 200))
|
|
132
|
+
$picks = @(Read-MultiSelect -Total ([math]::Min($found.Count, 200)) -NoAutoYes)
|
|
133
133
|
if ($picks.Count -eq 0) { Write-Info 'nothing selected'; return }
|
|
134
|
-
if (-not
|
|
134
|
+
if (-not $ws.DryRun) {
|
|
135
|
+
# Project artefacts never auto-confirm: --yes does not apply, and the walkthrough's step answer is not a selection.
|
|
136
|
+
if (-not (Confirm-Ui -Prompt "Remove $($picks.Count) artefact folder(s)? (rebuild with the project's install/build command)" -Default 'n' -NoAutoYes)) { Write-Info 'skipped'; return }
|
|
137
|
+
}
|
|
135
138
|
foreach ($k in $picks) {
|
|
136
139
|
$f = $found[$k - 1]
|
|
137
140
|
$r = Remove-PathSafe -Path $f.Path -Within $f.Root -Label (Split-Path -Leaf $f.Path)
|
|
@@ -241,6 +241,12 @@ function Invoke-SelfTest {
|
|
|
241
241
|
try { $probe = Join-Path $d ('.write-test-' + [guid]::NewGuid().ToString('N')); Set-Content -LiteralPath $probe -Value 'x'; Remove-Item -LiteralPath $probe -Force; Write-Ok "$d writable" } catch { Write-Err "$d not writable"; $fails++ }
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
if (Get-Command Invoke-SelfTestExtra -ErrorAction SilentlyContinue) {
|
|
245
|
+
$extra = Invoke-SelfTestExtra
|
|
246
|
+
$checks += [int]$extra.Checks
|
|
247
|
+
$fails += [int]$extra.Fails
|
|
248
|
+
}
|
|
249
|
+
|
|
244
250
|
Write-Separator
|
|
245
251
|
if ($fails -eq 0) { Write-Ok "all $checks checks passed - $Script:WS_NAME is ready"; return $Script:WS_EXIT_OK }
|
|
246
252
|
Write-Err "$fails of $checks checks FAILED"
|
|
@@ -317,8 +323,21 @@ function Get-LaunchCommand {
|
|
|
317
323
|
return [pscustomobject]@{ Exe = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"; Args = @('-NoProfile', '-NoLogo', '-ExecutionPolicy', 'Bypass', '-File', "`"$($ws.ScriptPath)`"") }
|
|
318
324
|
}
|
|
319
325
|
|
|
326
|
+
function Test-NpxInstallerRefusal {
|
|
327
|
+
<# .SYNOPSIS Under npx the package lives in a cache npm evicts; a task or alias pointing there would break later. #>
|
|
328
|
+
param([string] $Flag)
|
|
329
|
+
if (-not $Script:WS.Npx) { return $false }
|
|
330
|
+
Write-Err "$Flag is refused under npx: it would point at the npx cache, which npm evicts."
|
|
331
|
+
Write-Plain ' Install once, then register from the global command:'
|
|
332
|
+
Write-Plain ' npm install -g windowsweep'
|
|
333
|
+
Write-Plain " windowsweep $Flag"
|
|
334
|
+
$Script:WS.ExitCode = $Script:WS_EXIT_REFUSED
|
|
335
|
+
return $true
|
|
336
|
+
}
|
|
337
|
+
|
|
320
338
|
function Install-WeeklyTask {
|
|
321
339
|
Write-Box 'Install weekly Scheduled Task' "$Script:WS_TASK_NAME - Sundays 03:00, safe batch, no prompts"
|
|
340
|
+
if (Test-NpxInstallerRefusal '--install-task') { return }
|
|
322
341
|
if (Get-ScheduledTask -TaskName $Script:WS_TASK_NAME -ErrorAction SilentlyContinue) { Write-Info 'task already exists'; return }
|
|
323
342
|
$lc = Get-LaunchCommand
|
|
324
343
|
$argLine = (@($lc.Args) + @('--all', '--yes', '--quiet', '--no-color')) -join ' '
|
|
@@ -347,6 +366,7 @@ $Script:WS_ALIAS_MARK = '# windowsweep alias'
|
|
|
347
366
|
|
|
348
367
|
function Install-ProfileAlias {
|
|
349
368
|
Write-Box 'Install profile alias' "Adds a 'cleanup' function to your PowerShell profile"
|
|
369
|
+
if (Test-NpxInstallerRefusal '--install-alias') { return }
|
|
350
370
|
$profilePath = $PROFILE.CurrentUserAllHosts
|
|
351
371
|
$lc = Get-LaunchCommand
|
|
352
372
|
$line = "function cleanup { & `"$($lc.Exe)`" $($lc.Args -join ' ') @args }"
|
|
@@ -388,7 +408,7 @@ function Remove-ToolData {
|
|
|
388
408
|
$bytes = Get-DirectoryBytes $ws.Home
|
|
389
409
|
Write-Info ("$($ws.Home) holds " + (Format-Bytes $bytes) + ' of logs, reports, bundles and config')
|
|
390
410
|
if ($ws.DryRun) { Write-DryRun "would remove $($ws.Home)"; return }
|
|
391
|
-
if (-not (Confirm-Ui -Prompt 'Delete it all (logs, reports, your developer answer)?' -Default 'n')) { Write-Info 'skipped'; return }
|
|
411
|
+
if (-not (Confirm-Ui -Prompt 'Delete it all (logs, reports, your developer answer)?' -Default 'n' -NoAutoYes)) { Write-Info 'skipped'; return }
|
|
392
412
|
$ws.LogFile = $null
|
|
393
413
|
try { [IO.Directory]::Delete((Get-LongPath $ws.Home), $true); Write-Ok 'removed' } catch { Write-Err "could not remove: $($_.Exception.Message)" }
|
|
394
414
|
}
|
package/modules/runner.ps1
CHANGED
|
@@ -86,7 +86,7 @@ function Invoke-SectionById {
|
|
|
86
86
|
} catch {
|
|
87
87
|
$status = 'failed'
|
|
88
88
|
Write-Err "section $Id failed: $($_.Exception.Message)"
|
|
89
|
-
Write-
|
|
89
|
+
Write-LogLine ("section $Id error: " + $_.ScriptStackTrace)
|
|
90
90
|
$ws.ExitCode = $Script:WS_EXIT_FAIL
|
|
91
91
|
}
|
|
92
92
|
$after = Get-SystemDriveFree
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# self_test_extra.ps1 - extra self-test groups: the --yes asymmetry and its call sites (RW-002), pure helpers
|
|
2
|
+
# and fixtures for logic that is invisible when wrong (RW-030). Called by Invoke-SelfTest; verdict lines only.
|
|
3
|
+
|
|
4
|
+
function Invoke-SelfTestExtra {
|
|
5
|
+
<# .SYNOPSIS Run groups [12]-[14]. Returns an object with Checks and Fails counts. #>
|
|
6
|
+
$ws = $Script:WS
|
|
7
|
+
$checks = 0; $fails = 0
|
|
8
|
+
$saved = @{ DryRun = $ws.DryRun; Yes = $ws.Yes; Interactive = $ws.Interactive; Quiet = $ws.Quiet; NoReport = $ws.NoReport
|
|
9
|
+
ScanRoots = $ws.ScanRoots; AllowOwnData = $ws.AllowOwnData; Mute = $ws.Mute; Days = $ws.Days }
|
|
10
|
+
$fx = Join-Path $ws.Home ('selftest-extra-' + [guid]::NewGuid().ToString('N'))
|
|
11
|
+
$mute = { $Script:WS.Mute = $true }
|
|
12
|
+
$unmute = { $Script:WS.Mute = $false }
|
|
13
|
+
$ancient = (Get-Date).AddDays(-400)
|
|
14
|
+
try {
|
|
15
|
+
New-Item -ItemType Directory -Force -Path $fx | Out-Null
|
|
16
|
+
$ws.AllowOwnData = $true
|
|
17
|
+
$ws.NoReport = $true
|
|
18
|
+
$ws.DryRun = $false
|
|
19
|
+
$ws.Days = 100
|
|
20
|
+
|
|
21
|
+
Write-Section '[12] Prompts: --yes never selects personal or project items'
|
|
22
|
+
# 12a - the helper asymmetry
|
|
23
|
+
$checks++
|
|
24
|
+
$ws.Yes = $true; $ws.Interactive = $false
|
|
25
|
+
& $mute
|
|
26
|
+
$none = @(Read-MultiSelect -Total 5 -NoAutoYes)
|
|
27
|
+
$all = @(Read-MultiSelect -Total 5)
|
|
28
|
+
& $unmute
|
|
29
|
+
if ($none.Count -eq 0 -and ($all -join ',') -eq '1,2,3,4,5') { Write-Ok 'Read-MultiSelect: -NoAutoYes selects nothing under --yes; without it every index' } else { Write-Err "Read-MultiSelect asymmetry broken: none=[$($none -join ',')] all=[$($all -join ',')]"; $fails++ }
|
|
30
|
+
# 12b - section 17 call site (a fixture project with an artefact idle 400 days)
|
|
31
|
+
$checks++
|
|
32
|
+
$proj = Join-Path $fx 'proj17\app'
|
|
33
|
+
New-Item -ItemType Directory -Force -Path (Join-Path $proj 'node_modules') | Out-Null
|
|
34
|
+
Set-Content -LiteralPath (Join-Path $proj 'package.json') -Value '{"name":"fixture"}'
|
|
35
|
+
Set-Content -LiteralPath (Join-Path $proj 'node_modules\x.txt') -Value 'x'
|
|
36
|
+
foreach ($ts in 'LastWriteTime', 'LastAccessTime', 'CreationTime') { Set-ItemProperty -LiteralPath (Join-Path $proj 'package.json') -Name $ts -Value $ancient }
|
|
37
|
+
$ws.ScanRoots = @((Join-Path $fx 'proj17'))
|
|
38
|
+
& $mute
|
|
39
|
+
# The fixture must be a real candidate, or the removal check below proves nothing.
|
|
40
|
+
$listed = @(Find-StaleArtefacts -Roots $ws.ScanRoots -Days 100)
|
|
41
|
+
Invoke-Section17
|
|
42
|
+
& $unmute
|
|
43
|
+
if ($listed.Count -eq 1 -and (Test-Path -LiteralPath (Join-Path $proj 'node_modules\x.txt'))) {
|
|
44
|
+
Write-Ok 'section 17 under --yes with no console lists the artefact and removes nothing'
|
|
45
|
+
} elseif ($listed.Count -ne 1) {
|
|
46
|
+
Write-Err "section 17 fixture not detected as a candidate ($($listed.Count) listed) - the check cannot prove anything"; $fails++
|
|
47
|
+
} else {
|
|
48
|
+
Write-Err 'DANGEROUS: section 17 removed an artefact under --yes with no selection'; $fails++
|
|
49
|
+
}
|
|
50
|
+
# 12c - personal picker call site (sections 18/19)
|
|
51
|
+
$checks++
|
|
52
|
+
$pfile = Join-Path $fx 'personal\old.iso'
|
|
53
|
+
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $pfile) | Out-Null
|
|
54
|
+
Set-Content -LiteralPath $pfile -Value 'x'
|
|
55
|
+
$row = [pscustomobject]@{ Path = $pfile; Bytes = [long]1; Idle = 400; Root = (Split-Path -Parent $pfile) }
|
|
56
|
+
& $mute
|
|
57
|
+
Invoke-PersonalPicker -Rows @($row) -Prompt 'fixture'
|
|
58
|
+
& $unmute
|
|
59
|
+
if (Test-Path -LiteralPath $pfile) { Write-Ok 'sections 18/19 under --yes with no console recycle nothing' } else { Write-Err 'DANGEROUS: the personal picker removed a file under --yes with no selection'; $fails++ }
|
|
60
|
+
# 12d - every Read-MultiSelect call in modules/ carries -NoAutoYes (AST, not text)
|
|
61
|
+
$checks++
|
|
62
|
+
$offenders = @()
|
|
63
|
+
foreach ($f in (Get-ChildItem -LiteralPath (Join-Path $Script:WS_ROOT 'modules') -Filter '*.ps1' -File)) {
|
|
64
|
+
if ($f.Name -like 'self_test_extra*') { continue }
|
|
65
|
+
$tokens = $null; $errors = $null
|
|
66
|
+
$ast = [System.Management.Automation.Language.Parser]::ParseFile($f.FullName, [ref]$tokens, [ref]$errors)
|
|
67
|
+
$calls = $ast.FindAll({ param($n) ($n -is [System.Management.Automation.Language.CommandAst]) -and ($n.GetCommandName() -eq 'Read-MultiSelect') }, $true)
|
|
68
|
+
foreach ($c in $calls) {
|
|
69
|
+
$has = $false
|
|
70
|
+
foreach ($el in $c.CommandElements) { if (($el -is [System.Management.Automation.Language.CommandParameterAst]) -and ($el.ParameterName -eq 'NoAutoYes')) { $has = $true } }
|
|
71
|
+
if (-not $has) { $offenders += "$($f.Name):$($c.Extent.StartLineNumber)" }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if ($offenders.Count -eq 0) { Write-Ok 'every Read-MultiSelect call in modules/ carries -NoAutoYes' } else { Write-Err "Read-MultiSelect without -NoAutoYes: $($offenders -join ', ')"; $fails++ }
|
|
75
|
+
$ws.Yes = $saved.Yes; $ws.Interactive = $saved.Interactive; $ws.ScanRoots = $saved.ScanRoots
|
|
76
|
+
|
|
77
|
+
} catch {
|
|
78
|
+
& $unmute
|
|
79
|
+
Write-Err "extra self-test crashed: $($_.Exception.Message)"; $fails++
|
|
80
|
+
} finally {
|
|
81
|
+
& $unmute
|
|
82
|
+
foreach ($k in @($saved.Keys)) { $ws[$k] = $saved[$k] }
|
|
83
|
+
try { if (Test-Path -LiteralPath $fx) { [IO.Directory]::Delete((Get-LongPath $fx), $true) } } catch { $null = $_ }
|
|
84
|
+
}
|
|
85
|
+
return [pscustomobject]@{ Checks = $checks; Fails = $fails }
|
|
86
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "windowsweep",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Safe-by-default Windows cleanup CLI - developer-aware, dry-run first, zero install via npx.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"windowsweep": "bin/windowsweep.js"
|
|
@@ -38,7 +38,6 @@
|
|
|
38
38
|
"disk-cleanup",
|
|
39
39
|
"cache-cleanup",
|
|
40
40
|
"disk-space",
|
|
41
|
-
"temp-files",
|
|
42
41
|
"node-modules",
|
|
43
42
|
"npm-cache",
|
|
44
43
|
"yarn-cache",
|
package/windowsweep.ps1
CHANGED
|
@@ -275,7 +275,7 @@ if ($Script:WS.Elevate -and -not $Script:WS.IsAdmin) {
|
|
|
275
275
|
}
|
|
276
276
|
if ($Script:WS.Elevate -and $Script:WS.IsAdmin) { Write-Note 'already elevated; --elevate is a no-op' }
|
|
277
277
|
|
|
278
|
-
if ($Script:WS.Mode -in @('walkthrough', 'menu', 'all', 'only')) { Resolve-DeveloperMode }
|
|
278
|
+
if ($Script:WS.Mode -in @('walkthrough', 'menu', 'all', 'only')) { Resolve-DeveloperMode; Confirm-PurgeAllOnce }
|
|
279
279
|
|
|
280
280
|
try {
|
|
281
281
|
Invoke-Main
|
|
@@ -288,5 +288,9 @@ try {
|
|
|
288
288
|
if ($Script:WS.CleanupLogs -and $Script:WS.LogFile -and (Test-Path -LiteralPath $Script:WS.LogFile)) {
|
|
289
289
|
try { Remove-Item -LiteralPath $Script:WS.LogFile -Force -ErrorAction SilentlyContinue } catch { $null = $_ }
|
|
290
290
|
}
|
|
291
|
+
if (-not $Script:WS.Finished -and $Script:WS.ExitCode -eq 0) {
|
|
292
|
+
Write-LogLine 'run interrupted before it finished (Ctrl-C or host stop) - exit 130'
|
|
293
|
+
exit $Script:WS_EXIT_INTERRUPT
|
|
294
|
+
}
|
|
291
295
|
}
|
|
292
296
|
exit $Script:WS.ExitCode
|