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 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.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** | ~78 kB packed · ~263 kB unpacked · 37 files · no dependencies |
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** - proves the guards on your machine with a real junction, a 400-character path and a dry-run
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 | ✅ | Windows PowerShell 5.1 or PowerShell 7 |
132
- | Windows 10 (1809 and later) | ✅ | The primary development target |
133
- | Windows Server 2019+ | ⚠️ | Uses nothing newer than 1809, but Server is not in the test matrix |
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, *permanent* is not, *config*
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&nbsp;[#](#changelog)
414
415
 
415
- Latest release: **`1.0.0`** - first release: 22 sections, developer mode, the deletion chokepoint, dry-run, reports.
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&nbsp;[#](#keywords)
480
482
 
481
- *windows · cleanup · disk-cleanup · cache-cleanup · disk-space · temp-files · node-modules · npm-cache ·
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.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', 'CachedExtensionVSIXs', 'Code Cache', 'GPUCache', 'DawnCache', 'DawnGraphiteCache', 'DawnWebGPUCache', 'Service Worker\CacheStorage', 'Service Worker\ScriptCache', 'Crashpad\completed', 'Crashpad\reports')
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.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 = 'permanent'; Admin = $false; Batch = 'interactive'; Dev = $false; Fn = 'Invoke-Section18' }
55
- @{ Id = 19; Key = 'large'; Title = 'Large stale personal files (Downloads, Desktop) -> Recycle Bin'; Tier = 'permanent'; Admin = $false; Batch = 'interactive'; Dev = $false; Fn = 'Invoke-Section19' }
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-Log {
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-Log ("removed link (not followed): " + (Remove-LongPrefix $e.FullName))
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-Log "removed link (not followed): $p$tag"
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-Log ("removed dir: $p ($($r.Bytes) bytes, skipped $($r.Skipped))$tag")
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-Log "removed file: $p ($bytes bytes)$tag"
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-Log "skip (locked): $p - $($_.Exception.Message)"
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-Log ("pruned $($out.Files) files ($($out.Freed) bytes) idle $Days+ days from $root; skipped $($out.Skipped)")
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-Log "recycled: $p ($bytes bytes)"
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-Log "run${tag}: $display"
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-Log $s }
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-Log "exit $code$tag : $display"
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-Log $Text }
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-Log "INFO $Text"; return }
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-Log "NOTE $Text"; return }
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-Log ("> $line")
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
- param([int] $Total)
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)
@@ -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-Log "CRASH: $msg"
77
- Write-Log $ErrorRecord.ScriptStackTrace
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'
@@ -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 }
@@ -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, VSIX cache, crash reports')
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')
@@ -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.
@@ -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 (Confirm-Section "Remove $($picks.Count) artefact folder(s)? (rebuild with the project's install/build command)" 'n')) { Write-Info 'skipped'; return }
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
  }
@@ -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-Log ("section $Id error: " + $_.ScriptStackTrace)
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.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