windowsweep 1.0.1 → 1.1.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.
@@ -0,0 +1,126 @@
1
+ # startup_audit.ps1 - section 25: everything that starts with the user or the machine. Report only.
2
+ #
3
+ # The tool shows; the user decides in Task Manager. That non-goal is deliberate: disabling a startup item is
4
+ # a system configuration change, not cleanup, and a wrong one is invisible until the next boot.
5
+
6
+ function Get-StartupApprovedState {
7
+ <# .SYNOPSIS enabled | disabled | unknown from an Explorer StartupApproved value.
8
+ The value is 12 bytes: a DWORD of flags then a FILETIME. Explorer stamps that FILETIME when the item is
9
+ DISABLED and zeroes it when the item is enabled, so a non-zero tail is the reliable signal.
10
+ Measured on Windows 10 19045 (2026-09-04): five items carried a real FILETIME under a leading 0x01, and
11
+ two enabled ones (0x01 and 0x04) carried an all-zero tail - the widely quoted "byte 0 is 02/03" table
12
+ does not hold there, so the tail is read first and 03 / 07 are kept only as a fallback marker.
13
+ A value shorter than 12 bytes is reported as unknown; it is never guessed. #>
14
+ param([byte[]] $Value)
15
+ if ($null -eq $Value -or $Value.Length -lt 12) { return 'unknown' }
16
+ for ($i = 4; $i -lt 12; $i++) { if ($Value[$i] -ne 0) { return 'disabled' } }
17
+ if ($Value[0] -eq 3 -or $Value[0] -eq 7) { return 'disabled' }
18
+ return 'enabled'
19
+ }
20
+
21
+ function Get-StartupApprovedMap {
22
+ <# .SYNOPSIS name -> enabled|disabled|unknown, from the StartupApproved keys Explorer maintains. #>
23
+ $map = @{}
24
+ $keys = @(
25
+ 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run',
26
+ 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run32',
27
+ 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\StartupFolder',
28
+ 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run',
29
+ 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\StartupFolder'
30
+ )
31
+ foreach ($k in $keys) {
32
+ $props = $null
33
+ try { $props = Get-ItemProperty -LiteralPath $k -ErrorAction Stop } catch { continue }
34
+ foreach ($prop in $props.PSObject.Properties) {
35
+ if ($prop.Name.StartsWith('PS')) { continue }
36
+ if (-not ($prop.Value -is [byte[]])) { continue }
37
+ $map[$prop.Name] = Get-StartupApprovedState -Value $prop.Value
38
+ }
39
+ }
40
+ return $map
41
+ }
42
+
43
+ function Get-StartupItems {
44
+ <# .SYNOPSIS Every startup entry the user can see, from all five places Windows keeps them. #>
45
+ $P = $Script:P
46
+ $approved = Get-StartupApprovedMap
47
+ $items = @()
48
+ $runKeys = @(
49
+ @('HKCU Run', 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'),
50
+ @('HKCU RunOnce', 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce'),
51
+ @('HKLM Run', 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'),
52
+ @('HKLM RunOnce', 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce'),
53
+ @('HKLM Run (32)', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run'),
54
+ @('HKLM RunOnce (32)', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\RunOnce')
55
+ )
56
+ foreach ($rk in $runKeys) {
57
+ # NOT $p: PowerShell variable names are case-insensitive, so $p would silently null the $P roots table.
58
+ $props = $null
59
+ try { $props = Get-ItemProperty -LiteralPath $rk[1] -ErrorAction Stop } catch { continue }
60
+ foreach ($prop in $props.PSObject.Properties) {
61
+ if ($prop.Name.StartsWith('PS')) { continue }
62
+ $state = 'enabled'
63
+ if ($approved.ContainsKey($prop.Name)) { $state = $approved[$prop.Name] }
64
+ $items += [pscustomobject]@{ Source = $rk[0]; Name = $prop.Name; State = $state; Command = [string]$prop.Value }
65
+ }
66
+ }
67
+ foreach ($folder in @(@('Startup (user)', (Join-Path $P.A 'Microsoft\Windows\Start Menu\Programs\Startup')), @('Startup (all users)', (Join-Path $P.PD 'Microsoft\Windows\Start Menu\Programs\Startup')))) {
68
+ if (-not (Test-DirPresent $folder[1])) { continue }
69
+ foreach ($e in (Get-ChildEntries $folder[1])) {
70
+ if ($e -is [IO.DirectoryInfo] -or $e.Name -eq 'desktop.ini') { continue }
71
+ $state = 'enabled'
72
+ if ($approved.ContainsKey($e.Name)) { $state = $approved[$e.Name] }
73
+ $items += [pscustomobject]@{ Source = $folder[0]; Name = $e.Name; State = $state; Command = (Remove-LongPrefix $e.FullName) }
74
+ }
75
+ }
76
+ try {
77
+ foreach ($t in (Get-ScheduledTask -ErrorAction Stop)) {
78
+ if (-not (@($t.Triggers) | Where-Object { $_.CimClass.CimClassName -eq 'MSFT_TaskLogonTrigger' })) { continue }
79
+ $state = 'enabled'
80
+ if ($t.State -eq 'Disabled') { $state = 'disabled' }
81
+ $cmd = ''
82
+ foreach ($a in @($t.Actions)) { if ($a.Execute) { $cmd = [string]$a.Execute } }
83
+ $items += [pscustomobject]@{ Source = 'Scheduled task (logon)'; Name = $t.TaskName; State = $state; Command = $cmd }
84
+ }
85
+ } catch { $null = $_ }
86
+ try {
87
+ foreach ($c in (Get-CimInstance Win32_StartupCommand -ErrorAction Stop)) {
88
+ if (@($items | Where-Object { $_.Name -eq $c.Name }).Count -gt 0) { continue }
89
+ $state = 'enabled'
90
+ if ($approved.ContainsKey([string]$c.Name)) { $state = $approved[[string]$c.Name] }
91
+ $items += [pscustomobject]@{ Source = "WMI ($($c.Location))"; Name = [string]$c.Name; State = $state; Command = [string]$c.Command }
92
+ }
93
+ } catch { $null = $_ }
94
+ return @($items | Sort-Object Source, Name)
95
+ }
96
+
97
+ function Get-Targets25 {
98
+ <# .SYNOPSIS Informational only. Section 25 changes nothing: it never disables or removes a startup item. #>
99
+ return @((New-Target 25 'Startup items (registry, folders, tasks)' 'Run keys + Startup folders + logon tasks' -Kind cmd -Note 'report only; change them yourself in Task Manager > Startup'))
100
+ }
101
+
102
+ function Invoke-Section25 {
103
+ $ws = $Script:WS
104
+ Write-SectionIntro @(
105
+ 'Everything that launches when you sign in or the machine boots: Run and RunOnce keys, both Startup',
106
+ 'folders, scheduled tasks with a logon trigger, and what Explorer records as enabled or disabled.',
107
+ 'This section only shows them - change them in Task Manager > Startup, where you can undo it.'
108
+ )
109
+ $items = @(Get-StartupItems)
110
+ if ($items.Count -eq 0) { Write-Info 'no startup items found'; return }
111
+ Write-UiLine (" {0,-22} {1,-30} {2,-9} {3}" -f 'SOURCE', 'NAME', 'STATE', 'COMMAND') 'White'
112
+ $lines = @("startup items - $(Get-Date -Format 'yyyy-MM-dd HH:mm')", '')
113
+ foreach ($it in $items) {
114
+ $color = 'Gray'
115
+ if ($it.State -eq 'disabled') { $color = 'DarkGray' }
116
+ $line = " {0,-22} {1,-30} {2,-9} {3}" -f $it.Source, $it.Name, $it.State, $it.Command
117
+ Write-UiLine $line $color
118
+ $lines += $line.Trim()
119
+ }
120
+ $on = @($items | Where-Object { $_.State -eq 'enabled' }).Count
121
+ Write-Info "$($items.Count) startup item(s), $on enabled"
122
+ if (-not $ws.NoReport) {
123
+ $out = Join-Path $ws.ReportsDir ('startup-items-' + $ws.Stamp + '.txt')
124
+ try { [IO.File]::WriteAllLines($out, $lines); Write-Note "list saved: $out" } catch { $null = $_ }
125
+ }
126
+ }
@@ -31,6 +31,7 @@ function Get-Targets09 {
31
31
  (New-Target 9 'Certificate URL cache' "$($P.LL)\Microsoft\CryptnetUrlCache" -Mode clear)
32
32
  (New-Target 9 'Diagnostics results' "$($P.L)\Diagnostics" -Mode clear-old -Days 30)
33
33
  (New-Target 9 'DNS resolver cache' 'ipconfig /flushdns' -Kind cmd)
34
+ (New-Target 9 'Microsoft Store download cache' 'wsreset.exe' -Kind cmd -Note 'printed as a next step, never run for you: wsreset has no silent mode and always opens the Store')
34
35
  )
35
36
  }
36
37
 
@@ -46,6 +47,11 @@ function Invoke-Section09 {
46
47
  $null = Invoke-TargetList $targets
47
48
  $r = Invoke-External -FilePath 'ipconfig.exe' -ArgumentList @('/flushdns') -Destructive -Quiet -Label 'ipconfig /flushdns'
48
49
  if ($r.Ran -and $r.ExitCode -eq 0) { Write-Ok 'DNS resolver cache flushed' }
50
+ # wsreset is the only lever for the Store download cache, and it has no silent switch: every build opens
51
+ # the Store window when it finishes. A cleanup run must not pop a window, so it is offered, never executed.
52
+ if (Test-PathPresent (Join-Path $Script:P.SR 'System32\wsreset.exe')) {
53
+ $Script:WS.Hints += 'Microsoft Store cache: run wsreset.exe yourself when you want it cleared (it opens the Store when it finishes)'
54
+ }
49
55
  }
50
56
 
51
57
  # ---------------------------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "windowsweep",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
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"
@@ -24,7 +24,8 @@
24
24
  "LICENSE",
25
25
  "CHANGELOG.md",
26
26
  "SECURITY.md",
27
- "VERSION"
27
+ "VERSION",
28
+ "AI-INTEGRATION-GUIDE.md"
28
29
  ],
29
30
  "engines": {
30
31
  "node": ">=14"
package/windowsweep.ps1 CHANGED
@@ -20,7 +20,11 @@ $Script:WS = @{
20
20
  ScanRoots = @(); ExcludePaths = @(); Hiberfil = ''; ResetBase = $false; Permanent = $false
21
21
  OnlyList = ''; Profile = ''; Exclude = ''; ExportFmt = 'both'; ExportId = 'latest'; PruneDays = 90
22
22
  LogsDir = ''; ReportsDir = ''; NoReport = $false; CleanupLogs = $false
23
- JsonMode = $false; Quiet = $false; NoColor = $false; Ascii = $false
23
+ JsonMode = $false; Quiet = $false; NoColor = $false; Ascii = $false; Notify = $false
24
+ # Scripted selection (--select / --select-file): a person's choice made in advance. SelectActive is LATCHED
25
+ # once at parse time because SelectQueue empties as prompts consume it.
26
+ SelectQueue = @(); SelectPaths = @(); SelectActive = $false; LastSelectionScripted = $false
27
+ Candidates = @(); ScanTargets = @()
24
28
  TotalFreed = [long]0; TotalEstimated = [long]0; SectionFreed = [long]0
25
29
  Refusals = @(); Hints = @(); ExitCode = 0; Finished = $false; AllowOwnData = $false
26
30
  }
@@ -79,6 +83,11 @@ OPTIONS
79
83
  --large-file-mb N Minimum size for section 19 (default 100)
80
84
  --hiberfil off|reduced|keep What section 15 does --reset-base DISM /ResetBase in 14
81
85
  --permanent Sections 18/19 delete instead of using the Recycle Bin
86
+ --select L Pre-answer the next interactive selection, e.g. --select 1,3-5. Repeatable: the
87
+ lists are consumed in the order the prompts appear
88
+ --select-file P UTF-8 file of one full path per line, matched against each prompt's candidates.
89
+ Either flag lets sections 17/18/19/23 run unattended - a person did choose
90
+ --notify Show a Windows notification when the run ends
82
91
  --logs-dir P / --reports-dir P / --no-report / --cleanup-logs
83
92
  --json One-line JSON summary on stdout (everything else on stderr)
84
93
  -q, --quiet Less chatter --no-color --ascii (plain glyphs)
@@ -167,6 +176,15 @@ function Read-Arguments {
167
176
  '--reports-dir' { $v = $inline; if ($null -eq $v) { $v = Get-NextArg $argv ([ref]$i) $a }; $ws.ReportsDir = $v }
168
177
  '--no-report' { $ws.NoReport = $true }
169
178
  '--cleanup-logs' { $ws.CleanupLogs = $true }
179
+ '--notify' { $ws.Notify = $true }
180
+ '--select' { $v = $inline; if ($null -eq $v) { $v = Get-NextArg $argv ([ref]$i) $a }; $ws.SelectQueue += $v }
181
+ '--select-file' {
182
+ $v = $inline; if ($null -eq $v) { $v = Get-NextArg $argv ([ref]$i) $a }
183
+ $f = Get-FullPath $v
184
+ if (-not $f -or -not (Test-Path -LiteralPath $f)) { throw "--select-file not found: $v" }
185
+ $ws.SelectPaths = @([IO.File]::ReadAllLines($f) | ForEach-Object { $_.Trim() } | Where-Object { $_ -and -not $_.StartsWith('#') })
186
+ if ($ws.SelectPaths.Count -eq 0) { throw "--select-file has no paths in it: $v" }
187
+ }
170
188
  '--json' { $ws.JsonMode = $true; $ws.Quiet = $true }
171
189
  { $_ -in '-q', '--quiet' } { $ws.Quiet = $true }
172
190
  '--no-color' { $ws.NoColor = $true }
@@ -179,6 +197,8 @@ function Read-Arguments {
179
197
  if ($ws.Profile -and -not $Script:WS_PROFILES.ContainsKey($ws.Profile)) {
180
198
  throw "unknown profile '$($ws.Profile)' (known: $($Script:WS_PROFILES.Keys -join ', '))"
181
199
  }
200
+ # Latched here, once: the queue is drained by the prompts, so a recomputed flag would flip to false mid-run.
201
+ $ws.SelectActive = ($ws.SelectQueue.Count -gt 0 -or $ws.SelectPaths.Count -gt 0)
182
202
  }
183
203
 
184
204
  function Initialize-Paths {