disk-space-saviour 0.11.30 → 0.12.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 +81 -0
- package/README.md +3 -1
- package/package.json +3 -3
- package/src/docker/containers.js +10 -3
- package/src/docker/scan.js +46 -10
- package/src/index.d.ts +2 -1
- package/src/items.js +14 -5
- package/src/report.js +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,86 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Add `disk-space-saviour` (`dss`): a CLI and library that safely reclaims disk
|
|
8
|
+
space. `dss scan` reports caches and build outputs of 20+ ecosystems, version
|
|
9
|
+
managers, browser/IDE/AI caches, agent snapshots, system logs and forgotten
|
|
10
|
+
projects, and recurses into Docker: host daemons, running containers (scanned
|
|
11
|
+
through `docker exec`, never stopped), stopped containers (inspected with
|
|
12
|
+
`docker cp`, git-verified, never removed without confirmation) and nested
|
|
13
|
+
Docker-in-Docker daemons. `dss clean --tier safe|moderate|aggressive` deletes
|
|
14
|
+
with liveness and git re-checks before every item, and `dss emergency --free
|
|
15
|
+
20G | --until 80%` escalates tiers until the goal is met. Every run writes a
|
|
16
|
+
JSON audit log.
|
|
17
|
+
|
|
18
|
+
Liveness detection matches tools by executable name as well as process name,
|
|
19
|
+
so a Node.js tool whose main thread is renamed (for example `MainThread`) is
|
|
20
|
+
still seen as running. A Docker-in-Docker CI job (`npm run test:dind`) checks
|
|
21
|
+
recursion two daemons deep, cleaning inside running containers, and the
|
|
22
|
+
approval, Git and log-backup rules for stopped containers.
|
|
23
|
+
|
|
24
|
+
Open files are matched through symlinks (a scan of `/var/...` sees files that
|
|
25
|
+
`lsof` reports under `/private/var/...` on macOS), and process working
|
|
26
|
+
directories are read with `lsof` on macOS, so a tool running elsewhere does not
|
|
27
|
+
block a project it is not using.
|
|
28
|
+
|
|
29
|
+
The Security workflow checks that the repository's dependency graph is enabled
|
|
30
|
+
before the dependency review. When it is disabled, the review is skipped with a
|
|
31
|
+
warning that links to the setting, and `npm audit` still fails on high-severity
|
|
32
|
+
advisories in every lock. Before, every pull request failed on this setting.
|
|
33
|
+
|
|
34
|
+
Fix every open CI correctness and resilience issue in the pipeline: scope the
|
|
35
|
+
Docker buildx cache, retry transient fetch and download failures, make the
|
|
36
|
+
budget wrapper escalate to SIGKILL, add a terminal status gate to every
|
|
37
|
+
workflow with supersede detection, verify the husky install, make the jscpd
|
|
38
|
+
gate analyse real files, guard the package manager declaration, turn on
|
|
39
|
+
command-stream errexit, gate staged formatting before release commits, add
|
|
40
|
+
release-preflight credential checks, and sweep zizmor/actionlint/persisted
|
|
41
|
+
credentials/link-recheck fixes across the workflows.
|
|
42
|
+
|
|
43
|
+
Find more regenerable data on developer Macs. New rules cover app updates
|
|
44
|
+
staged by Sparkle and Squirrel (ShipIt), Chrome, Yandex Browser and Firefox
|
|
45
|
+
HTTP caches, the Discord cache, Cursor, Windsurf and Qoder caches, the
|
|
46
|
+
TypeScript type acquisition cache, the rust-script cache, old playwright-go
|
|
47
|
+
drivers, Claude Code MCP logs and abandoned Codex runtime installs. Each
|
|
48
|
+
browser and updater has its own busy check, so a running Chrome does not hold
|
|
49
|
+
back the Firefox cache. Browser, editor and Claude Code paths are also listed
|
|
50
|
+
for Linux (`~/.cache`, `~/.config`) and Windows (`%LOCALAPPDATA%`, `%APPDATA%`).
|
|
51
|
+
|
|
52
|
+
Extension folders of VS Code forks (`.windsurf`, `.codeium`, `.qoder`, `.kiro`,
|
|
53
|
+
`.trae`) are no longer searched for projects: their bundled `node_modules`
|
|
54
|
+
were reported as removable, and deleting them breaks the extension.
|
|
55
|
+
|
|
56
|
+
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.
|
|
57
|
+
|
|
58
|
+
List the browser HTTP caches, VS Code fork caches, Discord cache and staged
|
|
59
|
+
macOS app updates in the README's coverage summary.
|
|
60
|
+
|
|
61
|
+
Fix the release job: the formatting check before the release commit no longer
|
|
62
|
+
passes the changesets that `changeset version` deleted to prettier, which
|
|
63
|
+
failed on the missing files and stopped every release.
|
|
64
|
+
|
|
65
|
+
The `bin` paths in `package.json` drop the leading `./`, so `npm publish` no
|
|
66
|
+
longer warns that the `dss` commands were "invalid and removed" (npm only
|
|
67
|
+
normalized them; the commands were always installed).
|
|
68
|
+
|
|
69
|
+
Report fixes from #7: `dss docker scan` lists a layer shared by several
|
|
70
|
+
images and tags once, with every tag on one line; the summary counts items
|
|
71
|
+
that need `--remove-image` or another explicit consent as `needs explicit
|
|
72
|
+
consent` instead of adding them to the tier totals; and a container is no
|
|
73
|
+
longer labelled with a session taken from a counter or flag such as `1`.
|
|
74
|
+
|
|
75
|
+
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.
|
|
76
|
+
|
|
77
|
+
Make protected-branch release fallbacks wait for real pull-request checks with
|
|
78
|
+
a dedicated automation token, and pin every Linux CI job to Ubuntu 24.04.
|
|
79
|
+
|
|
80
|
+
Harden CI verdicts, Docker manifest publication, timeout cleanup, broken-link
|
|
81
|
+
rechecks, and contributor-authored workflow logs, and refresh vulnerable
|
|
82
|
+
transitive development dependencies.
|
|
83
|
+
|
|
3
84
|
## 0.11.30
|
|
4
85
|
|
|
5
86
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -148,7 +148,9 @@ three kinds of Docker data:
|
|
|
148
148
|
of containers dss removed), when known. No tier removes them, emergency
|
|
149
149
|
mode included: approve each one with `--remove-image REF` (`repo:tag`,
|
|
150
150
|
`repo` for `:latest`, or an id prefix of at least 12 characters) or an
|
|
151
|
-
interactive yes for that image.
|
|
151
|
+
interactive yes for that image. The summary line counts them, and
|
|
152
|
+
stopped containers that need consent, as `needs explicit consent`, so
|
|
153
|
+
the tier totals match what `clean --tier … --yes` removes.
|
|
152
154
|
- **Running containers** are never stopped, restarted or removed. They are
|
|
153
155
|
scanned and cleaned from the inside through `docker exec`, with the same
|
|
154
156
|
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.
|
|
3
|
+
"version": "0.12.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": "
|
|
10
|
-
"disk-space-saviour": "
|
|
9
|
+
"dss": "bin/dss.js",
|
|
10
|
+
"disk-space-saviour": "bin/dss.js"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"bin/",
|
package/src/docker/containers.js
CHANGED
|
@@ -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
|
}
|
package/src/docker/scan.js
CHANGED
|
@@ -201,22 +201,58 @@ function layerAdvice(command, bytes) {
|
|
|
201
201
|
: null;
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
236
|
+
history = await docker.history(id);
|
|
215
237
|
} catch (error) {
|
|
216
|
-
trace('history failed',
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|