disk-space-saviour 0.0.0-stage → 0.11.30
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 +506 -0
- package/LICENSE +24 -0
- package/README.md +536 -2
- package/bin/dss.js +5 -0
- package/package.json +96 -4
- package/src/audit.js +84 -0
- package/src/clean.js +716 -0
- package/src/cli.js +448 -0
- package/src/docker/cli.js +289 -0
- package/src/docker/containers.js +327 -0
- package/src/docker/images.js +122 -0
- package/src/docker/scan.js +495 -0
- package/src/emergency.js +169 -0
- package/src/env/local.js +715 -0
- package/src/env/resolve.js +36 -0
- package/src/env/shell.js +433 -0
- package/src/exec.js +185 -0
- package/src/git.js +338 -0
- package/src/health.js +174 -0
- package/src/index.d.ts +254 -0
- package/src/index.js +52 -0
- package/src/items.js +157 -0
- package/src/liveness.js +190 -0
- package/src/options.js +112 -0
- package/src/paths.js +182 -0
- package/src/report.js +373 -0
- package/src/rules/ecosystems.js +916 -0
- package/src/rules/other.js +375 -0
- package/src/rules/versions.js +256 -0
- package/src/scan.js +384 -0
- package/src/scanners/agents.js +166 -0
- package/src/scanners/common.js +38 -0
- package/src/scanners/global.js +217 -0
- package/src/scanners/projects.js +335 -0
- package/src/scanners/rust.js +404 -0
- package/src/scanners/system.js +171 -0
- package/src/scanners/versions.js +230 -0
- package/src/units.js +130 -0
package/README.md
CHANGED
|
@@ -1,3 +1,537 @@
|
|
|
1
|
-
#
|
|
1
|
+
# disk-space-saviour
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`dss` finds and safely deletes regenerable disk usage: package manager
|
|
4
|
+
caches, build outputs, forgotten dependency folders, old toolchain versions,
|
|
5
|
+
agent and browser caches, system logs, and Docker data. That includes caches
|
|
6
|
+
inside running containers and in nested Docker-in-Docker daemons. It is a
|
|
7
|
+
command line tool and a JavaScript library (`scan()` → report,
|
|
8
|
+
`clean(report)` → audit log).
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
Build machines and AI agent hosts fill their disks with data that can be
|
|
13
|
+
rebuilt: `node_modules`, `target/`, `~/.cache`, Docker layers and caches left
|
|
14
|
+
inside long-lived containers. Deleting this data by hand is risky, because
|
|
15
|
+
the same folder may belong to a build that is running right now or to a
|
|
16
|
+
container holding unpushed work. `dss` only touches regenerable data. It
|
|
17
|
+
checks liveness and Git state again right before each deletion, and it never
|
|
18
|
+
deletes anything unless you ask it to.
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g disk-space-saviour # or: npx disk-space-saviour scan
|
|
24
|
+
|
|
25
|
+
dss scan # report only, never deletes
|
|
26
|
+
dss clean # plan for the safe tier (dry run)
|
|
27
|
+
dss clean --yes # delete the safe tier
|
|
28
|
+
dss clean --tier moderate --yes
|
|
29
|
+
dss emergency --free 20G --yes # escalate tiers until 20 GiB is free
|
|
30
|
+
dss docker scan --recursive # containers and nested daemons
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Example (`dss scan ~/work` on two projects untouched for 40 days):
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
disk-space-saviour scan of host (build-01) in 3.7s (report only, nothing was deleted)
|
|
37
|
+
/: 75.8 GiB free of 193 GiB (61% used)
|
|
38
|
+
|
|
39
|
+
MODERATE 2 items, 7.6 MiB
|
|
40
|
+
4.8 MiB cargo-target /home/me/work/api/target
|
|
41
|
+
2.9 MiB node-modules /home/me/work/web/node_modules
|
|
42
|
+
|
|
43
|
+
Reclaimable (cumulative): safe 0 B · moderate 7.6 MiB · aggressive 7.6 MiB; blocked 0 B
|
|
44
|
+
Audit log: /home/me/.local/state/disk-space-saviour/audit/dss-scan-2026-09-26T00-31-05-432Z-232874.json
|
|
45
|
+
Run `dss clean --tier safe` to see the plan, add --yes to delete.
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`dss clean` without `--yes` asks on a terminal. When nobody can answer
|
|
49
|
+
(CI, agents, pipes), it prints the plan, deletes nothing, and says
|
|
50
|
+
`Dry run: pass --yes to delete non-interactively.` Add `--json` to any
|
|
51
|
+
command to get the report or audit log as JSON (`dss scan --json`).
|
|
52
|
+
|
|
53
|
+
## Commands
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
dss scan [paths...] [--docker] [--depth N] [--json] # report only
|
|
57
|
+
dss clean [paths...] --tier safe|moderate [--yes] [--older-than 1h]
|
|
58
|
+
dss emergency --free 20G | --until 80% [--path /] [--yes]
|
|
59
|
+
dss docker scan|clean [--container ID] [--recursive]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
| Exit code | Meaning |
|
|
63
|
+
| --------- | ---------------------------------------- |
|
|
64
|
+
| 0 | success (a dry run counts as success) |
|
|
65
|
+
| 1 | error, or at least one deletion failed |
|
|
66
|
+
| 2 | usage error |
|
|
67
|
+
| 3 | `dss emergency` could not reach its goal |
|
|
68
|
+
|
|
69
|
+
Run `dss --help` for every option. The most useful ones:
|
|
70
|
+
|
|
71
|
+
- `--only NAME` limits a run to an ecosystem, rule or kind (`--only rust`,
|
|
72
|
+
`--only npm-cache`, `--only docker-stopped-container`).
|
|
73
|
+
- `--exclude PATH|GLOB` makes paths untouchable.
|
|
74
|
+
- `--scanner NAME` restricts scanning to `projects`, `global`, `versions`,
|
|
75
|
+
`agents` or `system`.
|
|
76
|
+
- `--report FILE` limits cleaning to items in a saved `dss scan --json`
|
|
77
|
+
report. The current rules rescan those items before deletion.
|
|
78
|
+
|
|
79
|
+
## Tiers
|
|
80
|
+
|
|
81
|
+
Tiers are cumulative: `moderate` includes `safe`, and `aggressive` includes
|
|
82
|
+
both.
|
|
83
|
+
|
|
84
|
+
| Tier | What it removes |
|
|
85
|
+
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
86
|
+
| `safe` | Download caches (npm, pnpm, yarn, pip, uv, Cargo registry, Gradle, Go, NuGet, and many more), dangling Docker images and build cache. Includes these caches inside running containers. |
|
|
87
|
+
| `moderate` | Whole `node_modules`, `target/`, `.venv`, `build/` and similar folders of projects inactive for `--inactive` (30 days); toolchain versions nothing uses; unused tagged images, each one only with `--remove-image REF` or an interactive yes; stopped containers (`--remove-stopped-containers` or an interactive yes). |
|
|
88
|
+
| `aggressive` | For emergencies: everything regenerable, including build outputs of projects in active use. Only `dss emergency` reaches it. |
|
|
89
|
+
|
|
90
|
+
Covered ecosystems: JavaScript/TypeScript (npm, yarn, pnpm, bun, deno,
|
|
91
|
+
framework build caches), Python, Rust, JVM (Gradle, Maven, Kotlin, Android),
|
|
92
|
+
Go, C/C++ (CMake, ccache, vcpkg, Conan), .NET, PHP, Ruby, Swift/Xcode, Dart,
|
|
93
|
+
Haskell, Scala, Elixir, OCaml, Lean, Julia, R, Zig, and scripting languages.
|
|
94
|
+
Also covered: version managers (nvm, pyenv, rbenv, SDKMAN!, rustup, elan,
|
|
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.
|
|
100
|
+
|
|
101
|
+
## Safety
|
|
102
|
+
|
|
103
|
+
- **Dry run by default.** `scan` never deletes. `clean` and `emergency`
|
|
104
|
+
delete only with `--yes` or an interactive yes, and never with
|
|
105
|
+
`--dry-run`.
|
|
106
|
+
- **Regenerable data only.** Every rule describes data that a tool
|
|
107
|
+
recreates: a cache, a build output or a dependency install.
|
|
108
|
+
- **Liveness checks, repeated right before each path is deleted.** Paths
|
|
109
|
+
that a process holds open (`/proc/*/fd`, `cwd`, `exe`, or `lsof` on
|
|
110
|
+
macOS) are skipped. So are projects with a running `cargo`, `gradle`,
|
|
111
|
+
`npm`, `node` or similar tool, and anything modified within
|
|
112
|
+
`--older-than` (1h).
|
|
113
|
+
- **Rust builds are pruned in two parts.** `cargo-superseded` removes
|
|
114
|
+
older hashes of a unit (`.rlib`, `.rmeta`, `.so`/`.dylib`/`.a`,
|
|
115
|
+
`build/*`, `.fingerprint`) that a newer build replaced, but never while
|
|
116
|
+
`cargo` or `rustc` runs in the project: a running build may have judged
|
|
117
|
+
them fresh. `cargo-superseded-leaf` removes incremental session
|
|
118
|
+
directories idle for 3 hours and test/example binaries older than 3
|
|
119
|
+
hours that a newer build replaced. Nothing links against them, so this
|
|
120
|
+
part stays eligible while cargo runs; each file a process runs or holds
|
|
121
|
+
open is skipped.
|
|
122
|
+
- **Running tasks must survive.** Before its first deletion in an
|
|
123
|
+
environment (the host or a container), `clean` records PID 1, agents
|
|
124
|
+
(`claude`, `codex`, `solve`, ...) and build tools (`cargo`, `gradle`,
|
|
125
|
+
...) with their start times. It checks them again after every removed
|
|
126
|
+
path and item. If one is gone or its PID was reused, cleaning in that
|
|
127
|
+
environment stops at once, and the audit log and summary say which
|
|
128
|
+
process was lost. A build that simply finishes during the cleanup also
|
|
129
|
+
stops it: an exit cannot be told apart from a crash.
|
|
130
|
+
- **Git awareness.** A project or container is not touched while it has
|
|
131
|
+
uncommitted changes, unpushed commits or stashes. The blocker is shown in
|
|
132
|
+
the report. Stopped containers with Git work require an exact
|
|
133
|
+
`--allow-dirty-container ID` override.
|
|
134
|
+
- **Audit log on every run.** Each run, dry runs included, writes
|
|
135
|
+
`dss-<command>-<time>-<pid>.json` with every decision, the bytes freed
|
|
136
|
+
and left per environment, and disk usage before and after.
|
|
137
|
+
|
|
138
|
+
## Docker
|
|
139
|
+
|
|
140
|
+
With `--docker`, or automatically when a daemon is reachable, `dss` handles
|
|
141
|
+
three kinds of Docker data:
|
|
142
|
+
|
|
143
|
+
- **Daemon data:** `docker system df`, dangling images and build cache.
|
|
144
|
+
`--include-volumes` lists unattached volumes for manual inspection; it
|
|
145
|
+
does not delete them.
|
|
146
|
+
- **Unused tagged images** are listed with their size, creation date and
|
|
147
|
+
the container that used them last (from the `docker inspect` backups
|
|
148
|
+
of containers dss removed), when known. No tier removes them, emergency
|
|
149
|
+
mode included: approve each one with `--remove-image REF` (`repo:tag`,
|
|
150
|
+
`repo` for `:latest`, or an id prefix of at least 12 characters) or an
|
|
151
|
+
interactive yes for that image.
|
|
152
|
+
- **Running containers** are never stopped, restarted or removed. They are
|
|
153
|
+
scanned and cleaned from the inside through `docker exec`, with the same
|
|
154
|
+
rules and liveness checks as the host.
|
|
155
|
+
- **Stopped containers** are listed with their size, image, command, owner
|
|
156
|
+
session (`HIVE_MIND_PARENT_SESSION_ID` and other session labels or
|
|
157
|
+
environment variables, else a session id in the container name), task
|
|
158
|
+
URL, exit code and reason, OOM flag and end time, and the Git state of
|
|
159
|
+
repositories in their writable layer. The layer is copied out with
|
|
160
|
+
`docker cp` and checked with Git. A container is removed only with
|
|
161
|
+
`--remove-stopped-containers` or after an interactive yes per container,
|
|
162
|
+
and only when its Git state is clean. Before `docker rm`, its logs and
|
|
163
|
+
`docker inspect` output are saved to `--backup-dir`.
|
|
164
|
+
- **Containers kept for investigation** (a non-zero exit code or OOM
|
|
165
|
+
killed) are never removed by a tier or by `--remove-stopped-containers`,
|
|
166
|
+
in emergency mode too. Name each one with `--remove-container ID|NAME`
|
|
167
|
+
(a full name, or an id prefix of at least 12 characters) to remove it.
|
|
168
|
+
|
|
169
|
+
`--recursive` follows Docker-in-Docker: a container that runs its own daemon
|
|
170
|
+
is scanned as a new environment, down to `--depth` levels (default 3). The
|
|
171
|
+
CI job `Docker-in-Docker Integration` builds a host → l1 → l2 chain and runs
|
|
172
|
+
`tests/integration/dind.mjs` against it (`npm run test:dind`).
|
|
173
|
+
|
|
174
|
+
## Emergency mode
|
|
175
|
+
|
|
176
|
+
`dss emergency --free 20G` (at least 20 GiB available) or
|
|
177
|
+
`dss emergency --until 80%` (at most 80% used) works through the tiers in
|
|
178
|
+
order. Within a tier it removes caches first, then Docker objects, and
|
|
179
|
+
stopped containers last, only when approved. It re-reads the disk after
|
|
180
|
+
every deletion and stops as soon as the goal is met. It exits with 3 when
|
|
181
|
+
even the aggressive tier cannot reach the goal. `--path` chooses the volume
|
|
182
|
+
(default `/`); items on other host volumes are skipped.
|
|
183
|
+
|
|
184
|
+
## Library
|
|
185
|
+
|
|
186
|
+
```js
|
|
187
|
+
import { scan, clean, emergency, formatReport } from 'disk-space-saviour';
|
|
188
|
+
|
|
189
|
+
const report = await scan({ roots: ['/work'], docker: false });
|
|
190
|
+
console.log(formatReport(report));
|
|
191
|
+
|
|
192
|
+
// Library calls delete unless dryRun is set.
|
|
193
|
+
const audit = await clean(report, { tier: 'safe', dryRun: true });
|
|
194
|
+
console.log(audit.plannedBytes, audit.file);
|
|
195
|
+
|
|
196
|
+
await emergency({ free: '20G', removeStoppedContainers: false });
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Types ship in `src/index.d.ts`. See `examples/basic-usage.js` for a runnable
|
|
200
|
+
example and `examples/universal-app` for a report viewer.
|
|
201
|
+
|
|
202
|
+
## Configuration
|
|
203
|
+
|
|
204
|
+
| Option / variable | Default | Purpose |
|
|
205
|
+
| -------------------------------- | ------------------------------------------ | ------------------------------------------- |
|
|
206
|
+
| `--audit-dir`, `DSS_AUDIT_DIR` | `$XDG_STATE_HOME/disk-space-saviour/audit` | Where audit logs go |
|
|
207
|
+
| `--backup-dir`, `DSS_BACKUP_DIR` | `…/disk-space-saviour/backups` | Container log backups before `docker rm` |
|
|
208
|
+
| `--older-than` | `1h` | Activity window; newer files are kept |
|
|
209
|
+
| `--inactive` | `30d` | Project inactivity before `moderate` |
|
|
210
|
+
| `--min-size` | `1M` | Smaller items are not reported |
|
|
211
|
+
| `--depth` | `3` | Docker nesting depth |
|
|
212
|
+
| `--journal-keep` | `512M` | journald size to keep |
|
|
213
|
+
| `--verbose`, `DSS_DEBUG=1` | off | List everything, trace commands and timings |
|
|
214
|
+
|
|
215
|
+
`$XDG_STATE_HOME` falls back to `~/.local/state`. Linux and macOS are
|
|
216
|
+
supported (liveness uses `/proc` on Linux and `lsof` on macOS).
|
|
217
|
+
|
|
218
|
+
### Repository settings
|
|
219
|
+
|
|
220
|
+
#### Protected-Branch Release Pull Requests
|
|
221
|
+
|
|
222
|
+
If `main` requires pull requests and the `Pipeline Status` check, configure a
|
|
223
|
+
repository secret named `RELEASE_PR_TOKEN`. It must be a fine-grained PAT for
|
|
224
|
+
an automation actor other than the workflow's built-in `GITHUB_TOKEN`, scoped
|
|
225
|
+
to this repository with Contents and Pull requests write access and Checks read
|
|
226
|
+
access. The release and generated-preview fallbacks use it to open the PR, wait
|
|
227
|
+
for the PR's own checks, and merge only after they pass. The manual
|
|
228
|
+
changeset-PR mode uses it for the same reason. Teams that generate short-lived
|
|
229
|
+
GitHub App installation tokens can wire that action output to the same workflow
|
|
230
|
+
inputs instead of storing a PAT.
|
|
231
|
+
|
|
232
|
+
Repositories that allow the release workflow to push directly to `main` do not
|
|
233
|
+
exercise the fallback, but manual changeset PR creation still requires this
|
|
234
|
+
secret.
|
|
235
|
+
|
|
236
|
+
#### Optional Docker Hub Publishing
|
|
237
|
+
|
|
238
|
+
Docker publishing is disabled by default. To enable it for a project that ships
|
|
239
|
+
a Docker image, add a `Dockerfile` and configure these GitHub Actions settings:
|
|
240
|
+
|
|
241
|
+
| Setting | Type | Description |
|
|
242
|
+
| -------------------- | ------------------ | ------------------------------------------------------------------------------------- |
|
|
243
|
+
| `DOCKERHUB_IMAGE` | Variable | Docker Hub image name, for example `namespace/image`. This enables Docker publishing. |
|
|
244
|
+
| `DOCKERHUB_USERNAME` | Variable | Docker Hub username used by `docker/login-action`. |
|
|
245
|
+
| `DOCKERHUB_TOKEN` | Secret | Docker Hub access token used for registry authentication. |
|
|
246
|
+
| `DOCKER_CONTEXT` | Variable, optional | Docker build context. Defaults to `.`. |
|
|
247
|
+
| `DOCKERFILE` | Variable, optional | Dockerfile path. Defaults to `./Dockerfile`. |
|
|
248
|
+
|
|
249
|
+
When enabled, the release workflow waits until the exact published npm version
|
|
250
|
+
is visible in the npm registry, then publishes Docker Hub tags for `latest` and
|
|
251
|
+
that same version. The Docker build also receives `NPM_PACKAGE_VERSION` as a
|
|
252
|
+
build argument so Dockerfiles can install the matching published package.
|
|
253
|
+
|
|
254
|
+
#### ESLint Rules
|
|
255
|
+
|
|
256
|
+
Customize ESLint in `eslint.config.js`. Current configuration:
|
|
257
|
+
|
|
258
|
+
- ES Modules support
|
|
259
|
+
- Prettier integration
|
|
260
|
+
- No console restrictions (common in CLI tools)
|
|
261
|
+
- Strict equality enforcement
|
|
262
|
+
- Async/await best practices
|
|
263
|
+
- **Strict unused variables rule**: No exceptions - all unused variables, arguments, and caught errors must be removed (no `_` prefix exceptions)
|
|
264
|
+
|
|
265
|
+
#### Prettier Options
|
|
266
|
+
|
|
267
|
+
Configured in `.prettierrc`:
|
|
268
|
+
|
|
269
|
+
- Single quotes
|
|
270
|
+
- Semicolons
|
|
271
|
+
- 2-space indentation
|
|
272
|
+
- 80-character line width
|
|
273
|
+
- ES5 trailing commas
|
|
274
|
+
- LF line endings
|
|
275
|
+
|
|
276
|
+
## Development
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
npm install
|
|
280
|
+
npm test # Node.js, 30s per test
|
|
281
|
+
bun test --timeout 30000 # Bun
|
|
282
|
+
deno test --allow-read # Deno (read-only tests)
|
|
283
|
+
npm run check # lint + format + duplication
|
|
284
|
+
npm run test:dind # Docker-in-Docker integration (needs --privileged)
|
|
285
|
+
node bin/dss.js scan --verbose
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Project Structure
|
|
289
|
+
|
|
290
|
+
```
|
|
291
|
+
.
|
|
292
|
+
├── .changeset/ # Changeset configuration
|
|
293
|
+
├── .github/workflows/ # GitHub Actions CI/CD
|
|
294
|
+
├── .husky/ # Git hooks (pre-commit)
|
|
295
|
+
├── examples/ # Usage examples
|
|
296
|
+
│ └── universal-app/ # React + GitHub Pages + Electron + Capacitor app
|
|
297
|
+
├── scripts/ # Build and release scripts
|
|
298
|
+
├── src/ # Source code
|
|
299
|
+
│ ├── index.js # Main entry point
|
|
300
|
+
│ └── index.d.ts # TypeScript definitions
|
|
301
|
+
├── tests/ # Test files
|
|
302
|
+
├── .eslintrc.js # ESLint configuration
|
|
303
|
+
├── .prettierrc # Prettier configuration
|
|
304
|
+
├── bunfig.toml # Bun configuration
|
|
305
|
+
├── deno.json # Deno configuration
|
|
306
|
+
└── package.json # Node.js package manifest
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Multi-Runtime Support
|
|
310
|
+
|
|
311
|
+
This template is designed to work seamlessly with all major JavaScript runtimes:
|
|
312
|
+
|
|
313
|
+
- **Bun**: Primary runtime with highest performance, uses native test support (`bun test`)
|
|
314
|
+
- **Node.js**: Alternative runtime, uses built-in test runner (`node --test`)
|
|
315
|
+
- **Deno**: Secure runtime with built-in TypeScript support (`deno test`)
|
|
316
|
+
|
|
317
|
+
The [test-anywhere](https://github.com/link-foundation/test-anywhere) framework provides a unified testing API that works identically across all runtimes.
|
|
318
|
+
|
|
319
|
+
### Package Manager Agnostic
|
|
320
|
+
|
|
321
|
+
While `package.json` is the source of truth for dependencies, the template supports:
|
|
322
|
+
|
|
323
|
+
- **bun**: Primary choice, uses `bun.lockb`
|
|
324
|
+
- **npm**: Uses `package-lock.json`
|
|
325
|
+
- **yarn**: Uses `yarn.lock`
|
|
326
|
+
- **pnpm**: Uses `pnpm-lock.yaml`
|
|
327
|
+
- **deno**: Uses `deno.json` for configuration
|
|
328
|
+
|
|
329
|
+
Note: `package-lock.json` is not committed by default to allow any package manager.
|
|
330
|
+
|
|
331
|
+
### Universal App Example
|
|
332
|
+
|
|
333
|
+
The template includes `examples/universal-app`, a Vite React app that imports
|
|
334
|
+
`add` and `multiply` from `src/index.js` and renders a visual calculator UI.
|
|
335
|
+
The same static build is used by:
|
|
336
|
+
|
|
337
|
+
- GitHub Pages (`npm run example:web:build`)
|
|
338
|
+
- Electron desktop packaging (`npm run example:desktop:package`)
|
|
339
|
+
- Capacitor Android/iOS sync (`npm run example:mobile:sync`)
|
|
340
|
+
|
|
341
|
+
The example app has its own `package.json` and lockfile so template users can
|
|
342
|
+
opt into the frontend stack without adding React, Electron, or Capacitor to the
|
|
343
|
+
library package itself.
|
|
344
|
+
|
|
345
|
+
See [examples/universal-app/README.md](examples/universal-app/README.md) for
|
|
346
|
+
local web, desktop, Android, and iOS testing instructions.
|
|
347
|
+
|
|
348
|
+
### Code Quality
|
|
349
|
+
|
|
350
|
+
- **ESLint**: Configured with recommended rules + Prettier integration
|
|
351
|
+
- **Prettier**: Consistent code formatting
|
|
352
|
+
- **Husky + lint-staged**: Pre-commit hooks ensure code quality
|
|
353
|
+
- **File size limit**: Files must stay under 1500 lines for maintainability (enforced via ESLint and CI)
|
|
354
|
+
|
|
355
|
+
### Release Workflow
|
|
356
|
+
|
|
357
|
+
The release workflow uses [Changesets](https://github.com/changesets/changesets) for version management:
|
|
358
|
+
|
|
359
|
+
1. **Creating a changeset**: Run `bun run changeset` to document changes
|
|
360
|
+
2. **PR validation**: CI checks for valid changeset in each PR
|
|
361
|
+
3. **Automated versioning**: Merging to `main` triggers version bump
|
|
362
|
+
4. **npm publishing**: Automated via OIDC trusted publishing (no tokens needed)
|
|
363
|
+
5. **Optional Docker Hub publishing**: When configured, waits for the exact npm version and tags the Docker image with that version
|
|
364
|
+
6. **GitHub releases**: Auto-created with formatted release notes
|
|
365
|
+
|
|
366
|
+
> **First release of a brand-new package**: OIDC trusted publishing cannot
|
|
367
|
+
> create a package that does not exist yet (the first publish fails with
|
|
368
|
+
> `E404`, because a trusted publisher can only be configured for an existing
|
|
369
|
+
> package). To bootstrap, add a repository secret named `NPM_TOKEN` (a
|
|
370
|
+
> granular/automation token with publish access). The release workflow passes
|
|
371
|
+
> it as `NODE_AUTH_TOKEN` automatically. Once the package exists and OIDC
|
|
372
|
+
> trusted publishing is configured on npmjs.com, the token becomes optional and
|
|
373
|
+
> can be removed.
|
|
374
|
+
|
|
375
|
+
#### Manual Releases
|
|
376
|
+
|
|
377
|
+
Two manual release modes are available via GitHub Actions:
|
|
378
|
+
|
|
379
|
+
- **Instant release**: Immediately bump version and publish
|
|
380
|
+
- **Changeset PR**: Create a PR with changeset for review
|
|
381
|
+
|
|
382
|
+
### CI/CD Pipeline
|
|
383
|
+
|
|
384
|
+
The GitHub Actions workflow (`.github/workflows/release.yml`) implements a fast-fail pipeline:
|
|
385
|
+
|
|
386
|
+
**Fast checks** (~7-30s each, run first for fastest feedback):
|
|
387
|
+
|
|
388
|
+
1. **Test compilation**: Syntax-checks all `.mjs` files with `node --check`
|
|
389
|
+
2. **Lint, format & secrets scan**: ESLint, Prettier, jscpd, and [secretlint](https://github.com/secretlint/secretlint) for credential leak detection
|
|
390
|
+
3. **File line limits**: Enforces the 1500-line limit on JavaScript (`.js`, `.mjs`, `.cjs`) and Markdown (`.md`) files plus `release.yml`
|
|
391
|
+
4. **Changeset check**: Validates PR has exactly one changeset (added by that PR)
|
|
392
|
+
5. **Version check**: Blocks manual version changes in `package.json`
|
|
393
|
+
6. **Documentation validation**: Checks required doc files (doc line limits are enforced by the file line limits check)
|
|
394
|
+
|
|
395
|
+
**Slow checks** (only run after all fast checks pass):
|
|
396
|
+
|
|
397
|
+
7. **Test matrix**: 3 runtimes × 3 OS = 9 test combinations
|
|
398
|
+
8. **Broken link checks**: Validates all links in Markdown/HTML files (separate workflow)
|
|
399
|
+
|
|
400
|
+
**Release** (on merge to main):
|
|
401
|
+
|
|
402
|
+
9. **Changeset merge**: Combines multiple pending changesets at release time
|
|
403
|
+
10. **Release**: Automated versioning and npm publishing
|
|
404
|
+
11. **Optional Docker publish**: Publishes Docker Hub `latest` and npm-version tags after the npm package is visible
|
|
405
|
+
|
|
406
|
+
#### Reasonable Timeouts
|
|
407
|
+
|
|
408
|
+
Every CI job declares an explicit `timeout-minutes` so hung steps fail
|
|
409
|
+
in minutes instead of reaching the GitHub Actions default of six hours.
|
|
410
|
+
Fast checks use 5-10 minute caps, release jobs use 30 minutes, and the
|
|
411
|
+
link checker uses 10 minutes for external network variance.
|
|
412
|
+
|
|
413
|
+
That cap is a backstop, never the deadline: GitHub reports a job it
|
|
414
|
+
kills as **cancelled**, not **failed**. Long steps therefore own an
|
|
415
|
+
explicit budget via `scripts/run-with-budget-warning.sh`, which warns at
|
|
416
|
+
70% of the budget and fails the step with exit code 124 when it expires.
|
|
417
|
+
See [CI-TIMEOUT-BUDGETS.md](docs/CI-TIMEOUT-BUDGETS.md).
|
|
418
|
+
|
|
419
|
+
Individual tests are also capped inside supported runners:
|
|
420
|
+
`npm test` runs `node --test --test-timeout=30000`, and the CI Bun
|
|
421
|
+
runner uses `bun test --timeout 30000`. Both bound a _single test_, not
|
|
422
|
+
the suite, which is why the suite budget above exists. Deno does not
|
|
423
|
+
provide a single global per-test timeout flag, so Deno tests are
|
|
424
|
+
protected by their step budget and the matrix job backstop.
|
|
425
|
+
|
|
426
|
+
See [BEST-PRACTICES.md](docs/BEST-PRACTICES.md) for detailed explanations of each practice.
|
|
427
|
+
|
|
428
|
+
#### Robust Changeset Handling
|
|
429
|
+
|
|
430
|
+
The CI/CD pipeline is designed to handle concurrent PRs gracefully:
|
|
431
|
+
|
|
432
|
+
- **PR Validation**: Only validates changesets **added by the current PR**, not pre-existing ones from other merged PRs. This prevents false failures when multiple PRs merge before a release cycle completes.
|
|
433
|
+
|
|
434
|
+
- **Release-time Merging**: If multiple changesets exist when releasing, they are automatically merged into a single changeset with:
|
|
435
|
+
- The highest version bump type (major > minor > patch)
|
|
436
|
+
- All descriptions preserved in chronological order
|
|
437
|
+
|
|
438
|
+
This design decouples PR validation from the need to pull changes from the default branch, reducing conflicts and ensuring that even if CI/CD fails, all unpublished changesets will still get published when the error is resolved.
|
|
439
|
+
|
|
440
|
+
### Deploying the example app
|
|
441
|
+
|
|
442
|
+
The `example-app.yml` workflow deploys the universal example app to GitHub
|
|
443
|
+
Pages on every push to `main`. Before the first run on `main` in a new
|
|
444
|
+
repository created from this template, open **Settings → Pages** and set
|
|
445
|
+
**Source = GitHub Actions**. This is a one-time manual step and cannot be
|
|
446
|
+
configured from a workflow because the Pages source defaults to
|
|
447
|
+
_Deploy from a branch_. Without it, the `pages-deploy` job fails on
|
|
448
|
+
`actions/deploy-pages` with `Get Pages site failed` /
|
|
449
|
+
`Failed to create deployment`. After flipping the source, the workflow
|
|
450
|
+
provisions the Pages site on its first run.
|
|
451
|
+
|
|
452
|
+
### Auto-regenerated preview screenshots
|
|
453
|
+
|
|
454
|
+
The same `example-app.yml` workflow contains a `preview-regen` job that boots
|
|
455
|
+
the built example app in a headless Chromium via
|
|
456
|
+
[`browser-commander`](https://www.npmjs.com/package/browser-commander) +
|
|
457
|
+
Playwright and writes fresh screenshots to
|
|
458
|
+
`docs/screenshots/example-app/example-app-{locale}-{theme}.png` on every
|
|
459
|
+
push to `main` (and on `workflow_dispatch`). Any drift is committed back to
|
|
460
|
+
`main` so README/site images never go stale between releases. Screenshot-only
|
|
461
|
+
pushes do not match this workflow's path filter, while a protected-branch
|
|
462
|
+
fallback PR remains eligible for its required checks. The job runs in the
|
|
463
|
+
official Playwright container with the browser already installed, avoiding CI
|
|
464
|
+
stalls from live Chromium downloads.
|
|
465
|
+
|
|
466
|
+
The same script is available locally:
|
|
467
|
+
|
|
468
|
+
```bash
|
|
469
|
+
npm install --prefix examples/universal-app
|
|
470
|
+
npm run example:web:preview-images
|
|
471
|
+
# Verbose probe of <html data-theme>, <html lang>, and PNG signatures:
|
|
472
|
+
PREVIEW_VERBOSE=1 npm run example:web:preview-images
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
The matrix defaults to `{en, ru} × {light, dark}`. The shipped example app
|
|
476
|
+
has no localization or theme toggle yet, so every cell currently renders
|
|
477
|
+
the same UI — when a fork adds either, the matrix produces real per-cell
|
|
478
|
+
variants without script edits.
|
|
479
|
+
|
|
480
|
+
### Broken Link Checker
|
|
481
|
+
|
|
482
|
+
The link checker workflow (`.github/workflows/links.yml`) validates all links in Markdown and HTML files:
|
|
483
|
+
|
|
484
|
+
1. **Detection**: Uses [lychee](https://github.com/lycheeverse/lychee-action) to scan all `*.md` and `*.html` files
|
|
485
|
+
2. **Web Archive fallback**: For any broken links found, automatically checks the [Wayback Machine](https://web.archive.org) for archived versions
|
|
486
|
+
3. **Actionable suggestions**: Reports one of three outcomes for each broken link:
|
|
487
|
+
- **Archived**: Suggests the Web Archive URL as a replacement
|
|
488
|
+
- **Not archived**: Clearly reports the link is unrecoverable
|
|
489
|
+
4. **Scheduled checks**: Runs weekly to catch links that break over time (even if no files changed)
|
|
490
|
+
5. **Issue creation**: On scheduled runs, creates a GitHub Issue with the full broken links report
|
|
491
|
+
|
|
492
|
+
Add regex patterns to `.lycheeignore` to exclude URLs from checks (e.g., local dev URLs, example.com, known rate-limited sites).
|
|
493
|
+
|
|
494
|
+
### Scripts Reference
|
|
495
|
+
|
|
496
|
+
| Script | Description |
|
|
497
|
+
| ------------------------------------ | ----------------------------------------------------- |
|
|
498
|
+
| `bun test --timeout 30000` | Run tests with Bun and a 30s per-test cap |
|
|
499
|
+
| `npm test` | Run tests with Node.js and a 30s per-test cap |
|
|
500
|
+
| `bun run lint` | Check code with ESLint |
|
|
501
|
+
| `bun run lint:fix` | Fix ESLint issues automatically |
|
|
502
|
+
| `bun run format` | Format code with Prettier |
|
|
503
|
+
| `bun run format:check` | Check formatting without changing files |
|
|
504
|
+
| `bun run check` | Run all checks (lint + format) |
|
|
505
|
+
| `npm run example:web:dev` | Start the universal app Vite dev server |
|
|
506
|
+
| `npm run example:web:build` | Build the universal app static web bundle |
|
|
507
|
+
| `npm run example:web:preview-images` | Regenerate preview screenshots via browser-commander |
|
|
508
|
+
| `npm run example:desktop:package` | Package the Electron desktop app locally |
|
|
509
|
+
| `npm run example:mobile:sync` | Build and sync the app bundle into Capacitor projects |
|
|
510
|
+
| `bun run changeset` | Create a new changeset |
|
|
511
|
+
|
|
512
|
+
### Best Practices
|
|
513
|
+
|
|
514
|
+
The repository implements CI/CD best practices for AI-driven development. See [BEST-PRACTICES.md](docs/BEST-PRACTICES.md) for details on:
|
|
515
|
+
|
|
516
|
+
- File size limits for AI readability
|
|
517
|
+
- Automated formatting and linting
|
|
518
|
+
- Multi-runtime and cross-platform testing
|
|
519
|
+
- Changeset-based versioning
|
|
520
|
+
- Concurrency control for CI/CD pipelines
|
|
521
|
+
|
|
522
|
+
## Contributing
|
|
523
|
+
|
|
524
|
+
See [CONTRIBUTING.md](docs/CONTRIBUTING.md) for detailed contribution guidelines.
|
|
525
|
+
|
|
526
|
+
Quick steps:
|
|
527
|
+
|
|
528
|
+
1. Fork the repository
|
|
529
|
+
2. Create a feature branch: `git checkout -b feature/my-feature`
|
|
530
|
+
3. Make your changes
|
|
531
|
+
4. Create a changeset: `bun run changeset`
|
|
532
|
+
5. Commit your changes (pre-commit hooks will run automatically)
|
|
533
|
+
6. Push and create a Pull Request
|
|
534
|
+
|
|
535
|
+
## License
|
|
536
|
+
|
|
537
|
+
[Unlicense](LICENSE) - Public Domain
|
package/bin/dss.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,98 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "disk-space-saviour",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.11.30",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"types": "src/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"dss": "./bin/dss.js",
|
|
10
|
+
"disk-space-saviour": "./bin/dss.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin/",
|
|
14
|
+
"src/",
|
|
15
|
+
"CHANGELOG.md",
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./src/index.d.ts",
|
|
22
|
+
"import": "./src/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"test": "node --test --test-timeout=30000 tests/*.test.js",
|
|
27
|
+
"test:dind": "node tests/integration/dind.mjs",
|
|
28
|
+
"lint": "node scripts/lint.mjs",
|
|
29
|
+
"lint:all": "eslint .",
|
|
30
|
+
"lint:fix": "eslint . --fix",
|
|
31
|
+
"format": "prettier --write .",
|
|
32
|
+
"format:check": "prettier --check .",
|
|
33
|
+
"check:duplication": "jscpd .",
|
|
34
|
+
"check": "npm run lint && npm run format:check && npm run check:duplication",
|
|
35
|
+
"example:web:dev": "npm --prefix examples/universal-app run dev",
|
|
36
|
+
"example:web:build": "npm --prefix examples/universal-app run build",
|
|
37
|
+
"example:web:preview": "npm --prefix examples/universal-app run preview",
|
|
38
|
+
"example:web:preview-images": "node scripts/update-preview-images.mjs",
|
|
39
|
+
"example:desktop:package": "npm --prefix examples/universal-app run desktop:package",
|
|
40
|
+
"example:desktop:make": "npm --prefix examples/universal-app run desktop:make",
|
|
41
|
+
"example:mobile:sync": "npm --prefix examples/universal-app run mobile:sync",
|
|
42
|
+
"prepare": "node scripts/install-git-hooks.mjs",
|
|
43
|
+
"changeset": "changeset",
|
|
44
|
+
"changeset:version": "node scripts/changeset-version.mjs",
|
|
45
|
+
"changeset:publish": "changeset publish",
|
|
46
|
+
"changeset:status": "changeset status --since=origin/main"
|
|
47
|
+
},
|
|
48
|
+
"keywords": [
|
|
49
|
+
"disk-space",
|
|
50
|
+
"cleanup",
|
|
51
|
+
"cache",
|
|
52
|
+
"docker",
|
|
53
|
+
"docker-in-docker",
|
|
54
|
+
"node_modules",
|
|
55
|
+
"cargo",
|
|
56
|
+
"emergency",
|
|
57
|
+
"cli"
|
|
58
|
+
],
|
|
59
|
+
"author": "",
|
|
60
|
+
"license": "Unlicense",
|
|
61
|
+
"devEngines": {
|
|
62
|
+
"packageManager": {
|
|
63
|
+
"name": "npm"
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"publishConfig": {
|
|
67
|
+
"access": "public"
|
|
68
|
+
},
|
|
69
|
+
"repository": {
|
|
70
|
+
"type": "git",
|
|
71
|
+
"url": "git+https://github.com/konard/disk-space-saviour.git"
|
|
72
|
+
},
|
|
73
|
+
"engines": {
|
|
74
|
+
"node": ">=20.0.0"
|
|
75
|
+
},
|
|
76
|
+
"devDependencies": {
|
|
77
|
+
"@changesets/cli": "^2.29.7",
|
|
78
|
+
"eslint": "^9.38.0",
|
|
79
|
+
"eslint-config-prettier": "^10.1.8",
|
|
80
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
81
|
+
"husky": "^9.1.7",
|
|
82
|
+
"jscpd": "^4.0.5",
|
|
83
|
+
"lint-staged": "^16.2.6",
|
|
84
|
+
"prettier": "^3.6.2",
|
|
85
|
+
"test-anywhere": "^0.8.48"
|
|
86
|
+
},
|
|
87
|
+
"lint-staged": {
|
|
88
|
+
"*.{js,mjs,cjs}": [
|
|
89
|
+
"eslint --fix --max-warnings 0 --no-warn-ignored",
|
|
90
|
+
"prettier --write",
|
|
91
|
+
"prettier --check"
|
|
92
|
+
],
|
|
93
|
+
"*.md": [
|
|
94
|
+
"prettier --write",
|
|
95
|
+
"prettier --check"
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
}
|