disk-space-saviour 0.11.30 → 0.13.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/CHANGELOG.md CHANGED
@@ -1,5 +1,92 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 74c7b55: Clean HTTP and compiled-code caches of Edge, Brave, Opera and Opera GX, Vivaldi, Arc, Naver Whale, 360 and QQ Browser, and the Firefox forks LibreWolf, Waterfox, Zen and Floorp, plus Chromium on Windows. CI: the npm audit gate fails only on high or critical advisories that a newer release fixes and reports the rest as warnings; the example app takes the http-cache-semantics fix; preview images wait for the report heading instead of a template leftover.
8
+
9
+ ## 0.12.0
10
+
11
+ ### Minor Changes
12
+
13
+ - Add `disk-space-saviour` (`dss`): a CLI and library that safely reclaims disk
14
+ space. `dss scan` reports caches and build outputs of 20+ ecosystems, version
15
+ managers, browser/IDE/AI caches, agent snapshots, system logs and forgotten
16
+ projects, and recurses into Docker: host daemons, running containers (scanned
17
+ through `docker exec`, never stopped), stopped containers (inspected with
18
+ `docker cp`, git-verified, never removed without confirmation) and nested
19
+ Docker-in-Docker daemons. `dss clean --tier safe|moderate|aggressive` deletes
20
+ with liveness and git re-checks before every item, and `dss emergency --free
21
+ 20G | --until 80%` escalates tiers until the goal is met. Every run writes a
22
+ JSON audit log.
23
+
24
+ Liveness detection matches tools by executable name as well as process name,
25
+ so a Node.js tool whose main thread is renamed (for example `MainThread`) is
26
+ still seen as running. A Docker-in-Docker CI job (`npm run test:dind`) checks
27
+ recursion two daemons deep, cleaning inside running containers, and the
28
+ approval, Git and log-backup rules for stopped containers.
29
+
30
+ Open files are matched through symlinks (a scan of `/var/...` sees files that
31
+ `lsof` reports under `/private/var/...` on macOS), and process working
32
+ directories are read with `lsof` on macOS, so a tool running elsewhere does not
33
+ block a project it is not using.
34
+
35
+ The Security workflow checks that the repository's dependency graph is enabled
36
+ before the dependency review. When it is disabled, the review is skipped with a
37
+ warning that links to the setting, and `npm audit` still fails on high-severity
38
+ advisories in every lock. Before, every pull request failed on this setting.
39
+
40
+ Fix every open CI correctness and resilience issue in the pipeline: scope the
41
+ Docker buildx cache, retry transient fetch and download failures, make the
42
+ budget wrapper escalate to SIGKILL, add a terminal status gate to every
43
+ workflow with supersede detection, verify the husky install, make the jscpd
44
+ gate analyse real files, guard the package manager declaration, turn on
45
+ command-stream errexit, gate staged formatting before release commits, add
46
+ release-preflight credential checks, and sweep zizmor/actionlint/persisted
47
+ credentials/link-recheck fixes across the workflows.
48
+
49
+ Find more regenerable data on developer Macs. New rules cover app updates
50
+ staged by Sparkle and Squirrel (ShipIt), Chrome, Yandex Browser and Firefox
51
+ HTTP caches, the Discord cache, Cursor, Windsurf and Qoder caches, the
52
+ TypeScript type acquisition cache, the rust-script cache, old playwright-go
53
+ drivers, Claude Code MCP logs and abandoned Codex runtime installs. Each
54
+ browser and updater has its own busy check, so a running Chrome does not hold
55
+ back the Firefox cache. Browser, editor and Claude Code paths are also listed
56
+ for Linux (`~/.cache`, `~/.config`) and Windows (`%LOCALAPPDATA%`, `%APPDATA%`).
57
+
58
+ Extension folders of VS Code forks (`.windsurf`, `.codeium`, `.qoder`, `.kiro`,
59
+ `.trae`) are no longer searched for projects: their bundled `node_modules`
60
+ were reported as removable, and deleting them breaks the extension.
61
+
62
+ Restore the `cargo-superseded` rule and prune Rust leaf artifacts while cargo runs, keeping library artifacts. `dss clean` watches PID 1, agents and build tools in every environment and stops cleaning it when one of them disappears. Stopped containers show their owning session, task URL, exit code, OOM flag and end time; containers kept for investigation are removed only with `--remove-container ID`. Unused tagged images show size, creation date and last user, and are removed only with `--remove-image REF` or an interactive yes per image; `--remove-unused-images` is rejected.
63
+
64
+ List the browser HTTP caches, VS Code fork caches, Discord cache and staged
65
+ macOS app updates in the README's coverage summary.
66
+
67
+ Fix the release job: the formatting check before the release commit no longer
68
+ passes the changesets that `changeset version` deleted to prettier, which
69
+ failed on the missing files and stopped every release.
70
+
71
+ The `bin` paths in `package.json` drop the leading `./`, so `npm publish` no
72
+ longer warns that the `dss` commands were "invalid and removed" (npm only
73
+ normalized them; the commands were always installed).
74
+
75
+ Report fixes from #7: `dss docker scan` lists a layer shared by several
76
+ images and tags once, with every tag on one line; the summary counts items
77
+ that need `--remove-image` or another explicit consent as `needs explicit
78
+ consent` instead of adding them to the tier totals; and a container is no
79
+ longer labelled with a session taken from a counter or flag such as `1`.
80
+
81
+ Recheck Git, liveness, Docker mounts, browser revisions, and saved scan reports before cleanup. Protect uncommitted work in stopped containers and agent snapshots, and limit emergency cleanup to the target filesystem.
82
+
83
+ Make protected-branch release fallbacks wait for real pull-request checks with
84
+ a dedicated automation token, and pin every Linux CI job to Ubuntu 24.04.
85
+
86
+ Harden CI verdicts, Docker manifest publication, timeout cleanup, broken-link
87
+ rechecks, and contributor-authored workflow logs, and refresh vulnerable
88
+ transitive development dependencies.
89
+
3
90
  ## 0.11.30
4
91
 
5
92
  ### Patch Changes
package/README.md CHANGED
@@ -93,10 +93,12 @@ Go, C/C++ (CMake, ccache, vcpkg, Conan), .NET, PHP, Ruby, Swift/Xcode, Dart,
93
93
  Haskell, Scala, Elixir, OCaml, Lean, Julia, R, Zig, and scripting languages.
94
94
  Also covered: version managers (nvm, pyenv, rbenv, SDKMAN!, rustup, elan,
95
95
  ghcup, opam, swiftly, VS Code Server), Playwright, playwright-go, Puppeteer
96
- and Cypress browsers, Chrome, Yandex Browser and Firefox HTTP caches, IDE
97
- caches (JetBrains, VS Code, Cursor, Windsurf, Qoder), AI agent caches and logs,
98
- the Discord cache, macOS app updates staged by Sparkle and Squirrel, package
99
- manager archives, trash, crash reports, core dumps and journald logs.
96
+ and Cypress browsers, HTTP caches of Chrome, Chromium, Edge, Brave, Opera,
97
+ Opera GX, Vivaldi, Arc, Yandex Browser, Naver Whale, 360, QQ Browser, Firefox,
98
+ LibreWolf, Waterfox, Zen and Floorp, IDE caches (JetBrains, VS Code, Cursor,
99
+ Windsurf, Qoder), AI agent caches and logs, the Discord cache, macOS app
100
+ updates staged by Sparkle and Squirrel, package manager archives, trash, crash
101
+ reports, core dumps and journald logs.
100
102
 
101
103
  ## Safety
102
104
 
@@ -148,7 +150,9 @@ three kinds of Docker data:
148
150
  of containers dss removed), when known. No tier removes them, emergency
149
151
  mode included: approve each one with `--remove-image REF` (`repo:tag`,
150
152
  `repo` for `:latest`, or an id prefix of at least 12 characters) or an
151
- interactive yes for that image.
153
+ interactive yes for that image. The summary line counts them, and
154
+ stopped containers that need consent, as `needs explicit consent`, so
155
+ the tier totals match what `clean --tier … --yes` removes.
152
156
  - **Running containers** are never stopped, restarted or removed. They are
153
157
  scanned and cleaned from the inside through `docker exec`, with the same
154
158
  rules and liveness checks as the host.
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "disk-space-saviour",
3
- "version": "0.11.30",
3
+ "version": "0.13.0",
4
4
  "description": "Safely reclaims disk space: caches and build outputs of 20+ ecosystems, forgotten projects, and Docker (running, stopped and nested containers), with an emergency mode",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "types": "src/index.d.ts",
8
8
  "bin": {
9
- "dss": "./bin/dss.js",
10
- "disk-space-saviour": "./bin/dss.js"
9
+ "dss": "bin/dss.js",
10
+ "disk-space-saviour": "bin/dss.js"
11
11
  },
12
12
  "files": [
13
13
  "bin/",
@@ -108,12 +108,19 @@ export function ownerOf(ps, inspected) {
108
108
  };
109
109
  }
110
110
 
111
+ /** Counters and flags such as `1` or `true` are not session ids. */
112
+ function sessionLike(value) {
113
+ const text = String(value).trim();
114
+ return text !== '' && !/^(\d+|true|false|yes|no)$/i.test(text);
115
+ }
116
+
111
117
  function sessionOf(hints, name) {
112
- const entries = Object.entries(hints);
118
+ const entries = Object.entries(hints).filter(([, value]) =>
119
+ sessionLike(value)
120
+ );
113
121
  const found =
114
122
  entries.find(([key]) => /HIVE_MIND_PARENT_SESSION_ID$/.test(key)) ??
115
- entries.find(([key]) => /session/i.test(key)) ??
116
- entries[0];
123
+ entries.find(([key]) => /session/i.test(key));
117
124
  if (found) {
118
125
  return found;
119
126
  }
@@ -201,22 +201,58 @@ function layerAdvice(command, bytes) {
201
201
  : null;
202
202
  }
203
203
 
204
- async function imageHints(docker, verbose) {
205
- const largest = [...(verbose.Images ?? [])]
206
- .filter((image) => image.Containers !== '0')
207
- .sort((a, b) => parseDockerSize(b.Size) - parseDockerSize(a.Size))
208
- .slice(0, LAYER_HINT_IMAGES);
209
- const hints = [];
210
- for (const image of largest) {
204
+ /** Every tag of each image id, largest images first. */
205
+ function imagesById(rows) {
206
+ const byId = new Map();
207
+ for (const image of rows) {
211
208
  const name =
212
209
  image.Tag === '<none>' ? image.ID : `${image.Repository}:${image.Tag}`;
210
+ const entry = byId.get(image.ID) ?? { id: image.ID, names: [], image };
211
+ entry.names.push(name);
212
+ byId.set(image.ID, entry);
213
+ }
214
+ return [...byId.values()];
215
+ }
216
+
217
+ /** The same layer, whichever image's history it was read from. */
218
+ function layerKey(layer) {
219
+ return layer.ID && layer.ID !== '<missing>'
220
+ ? layer.ID
221
+ : `${layer.CreatedBy ?? ''}\0${layer.Size ?? ''}`;
222
+ }
223
+
224
+ export async function imageHints(docker, verbose) {
225
+ const largest = imagesById(
226
+ [...(verbose.Images ?? [])].filter((image) => image.Containers !== '0')
227
+ )
228
+ .sort(
229
+ (a, b) => parseDockerSize(b.image.Size) - parseDockerSize(a.image.Size)
230
+ )
231
+ .slice(0, LAYER_HINT_IMAGES);
232
+ const byLayer = new Map();
233
+ for (const { id, names } of largest) {
234
+ let history;
213
235
  try {
214
- hints.push(...layerHints(name, await docker.history(image.ID)));
236
+ history = await docker.history(id);
215
237
  } catch (error) {
216
- trace('history failed', name, error.message);
238
+ trace('history failed', names[0], error.message);
239
+ continue;
240
+ }
241
+ for (const layer of history) {
242
+ const [hint] = layerHints(names[0], [layer]);
243
+ if (!hint) {
244
+ continue;
245
+ }
246
+ const key = layerKey(layer);
247
+ const shared = byLayer.get(key);
248
+ if (shared) {
249
+ shared.images.push(...names.filter((n) => !shared.images.includes(n)));
250
+ } else {
251
+ byLayer.set(key, { ...hint, images: [...names] });
252
+ }
217
253
  }
218
254
  }
219
- return hints;
255
+ return [...byLayer.values()];
220
256
  }
221
257
 
222
258
  async function stoppedItem(daemon, ps, inspected, options) {
package/src/index.d.ts CHANGED
@@ -151,7 +151,8 @@ export interface Report {
151
151
  options: Record<string, unknown>;
152
152
  environments: EnvironmentDescriptor[];
153
153
  items: Item[];
154
- totals: Record<Tier | 'blocked', TierTotal>;
154
+ /** Tier totals exclude items that need explicit consent (`consent`). */
155
+ totals: Record<Tier | 'blocked' | 'consent', TierTotal>;
155
156
  docker: {
156
157
  daemons: Array<Record<string, unknown>>;
157
158
  containers: Array<Record<string, unknown>>;
package/src/items.js CHANGED
@@ -84,13 +84,22 @@ export function block(item, reason) {
84
84
  */
85
85
  export function tierTotals(items) {
86
86
  const totals = {};
87
+ // A tier total is what `clean --tier <tier> --yes` removes: items that
88
+ // need a per-object flag or an interactive yes are counted apart.
89
+ const sum = (selected) => ({
90
+ items: selected.length,
91
+ bytes: selected.reduce((total, item) => total + item.bytes, 0),
92
+ });
87
93
  for (const tier of TIERS) {
88
- const selected = selectByTier(items, tier);
89
- totals[tier] = {
90
- items: selected.length,
91
- bytes: selected.reduce((sum, item) => sum + item.bytes, 0),
92
- };
94
+ totals[tier] = sum(
95
+ selectByTier(items, tier, (item) => !item.requiresConfirmation)
96
+ );
93
97
  }
98
+ totals.consent = sum(
99
+ selectByTier(items, 'aggressive', (item) =>
100
+ Boolean(item.requiresConfirmation)
101
+ )
102
+ );
94
103
  totals.blocked = {
95
104
  items: items.filter((item) => item.blockers.length > 0).length,
96
105
  bytes: items
package/src/report.js CHANGED
@@ -216,7 +216,7 @@ function dockerSection(report) {
216
216
  }
217
217
  for (const hint of docker.hints ?? []) {
218
218
  lines.push(
219
- ` hint ${hint.image}: ${formatBytes(hint.bytes)} layer, ${hint.hint}`
219
+ ` hint ${(hint.images ?? [hint.image]).join(', ')}: ${formatBytes(hint.bytes)} layer, ${hint.hint}`
220
220
  );
221
221
  }
222
222
  for (const volume of docker.volumes ?? []) {
@@ -231,9 +231,12 @@ function totalsSection(report) {
231
231
  const parts = TIERS.map(
232
232
  (tier) => `${tier} ${formatBytes(report.totals[tier].bytes)}`
233
233
  );
234
+ const consent = report.totals.consent?.bytes
235
+ ? `; needs explicit consent ${formatBytes(report.totals.consent.bytes)}`
236
+ : '';
234
237
  return [
235
238
  '',
236
- `Reclaimable (cumulative): ${parts.join(' · ')}; blocked ${formatBytes(report.totals.blocked.bytes)}`,
239
+ `Reclaimable (cumulative): ${parts.join(' · ')}${consent}; blocked ${formatBytes(report.totals.blocked.bytes)}`,
237
240
  ];
238
241
  }
239
242
 
@@ -171,9 +171,110 @@ export const OTHER_RULES = [
171
171
  '~/Library/Caches/Chromium/*/Code Cache',
172
172
  '{LOCALAPPDATA}/Google/Chrome/User Data/*/Cache',
173
173
  '{LOCALAPPDATA}/Google/Chrome/User Data/*/Code Cache',
174
+ '{LOCALAPPDATA}/Chromium/User Data/*/Cache',
175
+ '{LOCALAPPDATA}/Chromium/User Data/*/Code Cache',
174
176
  ],
175
177
  busy: ['Google Chrome', 'chrome', 'Chromium', 'chromium'],
176
178
  },
179
+ {
180
+ id: 'edge-http-caches',
181
+ ecosystem: 'browsers',
182
+ description: 'Microsoft Edge HTTP and compiled-code caches',
183
+ paths: [
184
+ '~/.cache/microsoft-edge/*/Cache',
185
+ '~/.config/microsoft-edge/*/Code Cache',
186
+ '~/Library/Caches/Microsoft Edge/*/Cache',
187
+ '~/Library/Caches/Microsoft Edge/*/Code Cache',
188
+ '{LOCALAPPDATA}/Microsoft/Edge/User Data/*/Cache',
189
+ '{LOCALAPPDATA}/Microsoft/Edge/User Data/*/Code Cache',
190
+ ],
191
+ busy: ['Microsoft Edge', 'msedge'],
192
+ },
193
+ {
194
+ id: 'brave-http-caches',
195
+ ecosystem: 'browsers',
196
+ description: 'Brave HTTP and compiled-code caches',
197
+ paths: [
198
+ '~/.cache/BraveSoftware/Brave-Browser/*/Cache',
199
+ '~/.config/BraveSoftware/Brave-Browser/*/Code Cache',
200
+ '~/Library/Caches/BraveSoftware/Brave-Browser/*/Cache',
201
+ '~/Library/Caches/BraveSoftware/Brave-Browser/*/Code Cache',
202
+ '{LOCALAPPDATA}/BraveSoftware/Brave-Browser/User Data/*/Cache',
203
+ '{LOCALAPPDATA}/BraveSoftware/Brave-Browser/User Data/*/Code Cache',
204
+ ],
205
+ busy: ['Brave Browser', 'brave'],
206
+ },
207
+ {
208
+ // Opera keeps a single profile: its caches sit at the top of its folders.
209
+ id: 'opera-http-caches',
210
+ ecosystem: 'browsers',
211
+ description: 'Opera and Opera GX HTTP and compiled-code caches',
212
+ paths: [
213
+ '~/.cache/opera/Cache',
214
+ '~/.config/opera/Code Cache',
215
+ '~/Library/Caches/com.operasoftware.Opera/Cache',
216
+ '~/Library/Caches/com.operasoftware.Opera/Code Cache',
217
+ '~/Library/Caches/com.operasoftware.OperaGX/Cache',
218
+ '~/Library/Caches/com.operasoftware.OperaGX/Code Cache',
219
+ '{LOCALAPPDATA}/Opera Software/Opera Stable/Cache',
220
+ '{LOCALAPPDATA}/Opera Software/Opera Stable/Code Cache',
221
+ '{LOCALAPPDATA}/Opera Software/Opera GX Stable/Cache',
222
+ '{LOCALAPPDATA}/Opera Software/Opera GX Stable/Code Cache',
223
+ ],
224
+ busy: ['Opera', 'opera', 'Opera GX'],
225
+ },
226
+ {
227
+ id: 'vivaldi-http-caches',
228
+ ecosystem: 'browsers',
229
+ description: 'Vivaldi HTTP and compiled-code caches',
230
+ paths: [
231
+ '~/.cache/vivaldi/*/Cache',
232
+ '~/.config/vivaldi/*/Code Cache',
233
+ '~/Library/Caches/Vivaldi/*/Cache',
234
+ '~/Library/Caches/Vivaldi/*/Code Cache',
235
+ '{LOCALAPPDATA}/Vivaldi/User Data/*/Cache',
236
+ '{LOCALAPPDATA}/Vivaldi/User Data/*/Code Cache',
237
+ ],
238
+ busy: ['Vivaldi', 'vivaldi', 'vivaldi-bin'],
239
+ },
240
+ {
241
+ id: 'arc-http-caches',
242
+ ecosystem: 'browsers',
243
+ description: 'Arc HTTP and compiled-code caches',
244
+ paths: [
245
+ '~/Library/Caches/Arc/User Data/*/Cache',
246
+ '~/Library/Caches/Arc/User Data/*/Code Cache',
247
+ ],
248
+ busy: ['Arc'],
249
+ },
250
+ {
251
+ id: 'whale-http-caches',
252
+ ecosystem: 'browsers',
253
+ description: 'Naver Whale HTTP and compiled-code caches',
254
+ paths: [
255
+ '~/.cache/naver-whale/*/Cache',
256
+ '~/.config/naver-whale/*/Code Cache',
257
+ '~/Library/Caches/Naver/Whale/*/Cache',
258
+ '~/Library/Caches/Naver/Whale/*/Code Cache',
259
+ '{LOCALAPPDATA}/Naver/Naver Whale/User Data/*/Cache',
260
+ '{LOCALAPPDATA}/Naver/Naver Whale/User Data/*/Code Cache',
261
+ ],
262
+ busy: ['Whale', 'whale', 'Naver Whale'],
263
+ },
264
+ {
265
+ id: 'chinese-chromium-http-caches',
266
+ ecosystem: 'browsers',
267
+ description: '360 and QQ Browser HTTP and compiled-code caches',
268
+ paths: [
269
+ '{LOCALAPPDATA}/360Chrome/Chrome/User Data/*/Cache',
270
+ '{LOCALAPPDATA}/360Chrome/Chrome/User Data/*/Code Cache',
271
+ '{APPDATA}/360se6/User Data/*/Cache',
272
+ '{APPDATA}/360se6/User Data/*/Code Cache',
273
+ '{LOCALAPPDATA}/Tencent/QQBrowser/User Data/*/Cache',
274
+ '{LOCALAPPDATA}/Tencent/QQBrowser/User Data/*/Code Cache',
275
+ ],
276
+ busy: ['360chrome', '360se', 'QQBrowser'],
277
+ },
177
278
  {
178
279
  id: 'yandex-browser-http-caches',
179
280
  ecosystem: 'browsers',
@@ -199,6 +300,35 @@ export const OTHER_RULES = [
199
300
  ],
200
301
  busy: ['firefox', 'Firefox'],
201
302
  },
303
+ {
304
+ id: 'firefox-fork-http-caches',
305
+ ecosystem: 'browsers',
306
+ description: 'LibreWolf, Waterfox, Zen and Floorp HTTP caches',
307
+ paths: [
308
+ '~/.cache/librewolf/*/cache2',
309
+ '~/.cache/waterfox/*/cache2',
310
+ '~/.cache/zen/*/cache2',
311
+ '~/.cache/floorp/*/cache2',
312
+ '~/Library/Caches/librewolf/Profiles/*/cache2',
313
+ '~/Library/Caches/Waterfox/Profiles/*/cache2',
314
+ '~/Library/Caches/zen/Profiles/*/cache2',
315
+ '~/Library/Caches/Floorp/Profiles/*/cache2',
316
+ '{LOCALAPPDATA}/librewolf/Profiles/*/cache2',
317
+ '{LOCALAPPDATA}/Waterfox/Profiles/*/cache2',
318
+ '{LOCALAPPDATA}/zen/Profiles/*/cache2',
319
+ '{LOCALAPPDATA}/Floorp/Profiles/*/cache2',
320
+ ],
321
+ busy: [
322
+ 'librewolf',
323
+ 'LibreWolf',
324
+ 'waterfox',
325
+ 'Waterfox',
326
+ 'zen',
327
+ 'Zen',
328
+ 'floorp',
329
+ 'Floorp',
330
+ ],
331
+ },
202
332
  {
203
333
  id: 'copilot-caches',
204
334
  ecosystem: 'ide',