npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

disk-space-saviour

v0.25.0

Published

Safely reclaims disk space: caches and build outputs of 20+ ecosystems, forgotten projects, and Docker (running, stopped and nested containers), with an emergency mode

Readme

disk-space-saviour

dss finds and safely deletes regenerable disk usage: package manager caches, build outputs, forgotten dependency folders, old toolchain versions, agent and browser caches, system logs, and Docker data. That includes caches inside running containers and in nested Docker-in-Docker daemons. It is a command line tool and a JavaScript library (scan() → report, clean(report) → audit log).

Why

Build machines and AI agent hosts fill their disks with data that can be rebuilt: node_modules, target/, ~/.cache, Docker layers and caches left inside long-lived containers. Deleting this data by hand is risky, because the same folder may belong to a build that is running right now or to a container holding unpushed work. dss only touches regenerable data. It checks liveness and Git state again right before each deletion, and it never deletes anything unless you ask it to.

Quick Start

npm install -g disk-space-saviour   # or: npx disk-space-saviour scan

dss scan                 # report only, never deletes
dss clean                # plan for the safe tier (dry run)
dss clean --yes          # delete the safe tier
dss clean --tier moderate --yes
dss emergency --free 20G --yes      # escalate tiers until 20 GiB is free
dss docker scan --recursive         # containers and nested daemons

Example (dss scan ~/work on two projects):

disk-space-saviour scan of host (build-01) in 3.7s (report only, nothing was deleted)
/: 75.8 GiB free of 193 GiB (61% used)

SAFE  2 items, 7.6 MiB
     4.8 MiB  cargo-target  /home/me/work/api/target
     2.9 MiB  node-modules  /home/me/work/web/node_modules

Reclaimable (cumulative): safe 7.6 MiB · moderate 7.6 MiB · aggressive 7.6 MiB; blocked 0 B
Audit log: /home/me/.local/state/disk-space-saviour/audit/dss-scan-2026-09-26T00-31-05-432Z-232874.json
Run `dss clean --tier safe` to see the plan, add --yes to delete.

dss clean without --yes asks on a terminal. When nobody can answer (CI, agents, pipes), it prints the plan, deletes nothing, and says Dry run: pass --yes to delete non-interactively. Add --json to any command to get the report or audit log as JSON (dss scan --json).

Commands

dss scan [paths...] [--docker] [--depth N] [--json]      # report only
dss clean [paths...] --tier safe|moderate [--yes] [--older-than 1h]
dss emergency --free 20G | --until 80% [--path /] [--yes]
dss docker scan|clean [--container ID] [--recursive]
dss usage [paths...] [--min-size 1G] [--json]            # where space goes

| Exit code | Meaning | | --------- | ---------------------------------------- | | 0 | success (a dry run counts as success) | | 1 | error, or at least one deletion failed | | 2 | usage error | | 3 | dss emergency could not reach its goal |

Run dss --help for every option. The most useful ones:

  • --only NAME|ID|PATH limits a run to an ecosystem, rule or kind (--only rust, --only npm-cache, --only docker-stopped-container), a single item by its JSON id, or the items inside a path or matching a glob (--only ~/Library/Caches/Codex, --only '**/Crashpad/*'). A path never widens to the item holding it. When every entry names a cache or toolchain version rule or one of its items, only those rules are scanned, so dss clean --only openai-app-caches --yes takes seconds.
  • --exclude PATH|GLOB prunes matching paths and descendants before reading them. Patterns containing separators match full paths and their ancestors; other patterns also match ancestor basenames.
  • --scanner NAME restricts scanning to projects, global, versions, agents or system.
  • --report FILE limits cleaning to items in a saved dss scan --json report. --from FILE also accepts an audit JSON containing the full report. Recent reports authenticated by this installation are reused; older, unsigned or edited reports require discovery again. Changing discovery or action options also rebuilds the plan. Every deletion still gets fresh checks.
  • dss usage shows where the space of a whole volume goes, including data no rule covers. One read-only du -axk pass walks the macOS Data volume (/Users, /Applications, /private, /opt, ...) or / on Linux, or the given paths, staying on their filesystem. It prints a tree of the largest directories (1% of the walk and up, or --min-size), the largest files, and for each directory what the latest dss scan or clean found inside it ([dss: 1.2 GiB freeable, 3.4 GiB blocked]). On macOS it adds the APFS volumes a walk cannot see (system, VM swap, Preboot, Recovery), local snapshots and the part of the Data volume left unread. A directory filled by many small entries names what fills it by pattern (T/ms-* (68523 entries, each under 64 MiB)). It needs du, so it runs on macOS and Linux; a full walk takes minutes.
  • APFS clones that share every block with an installed app, such as Chrome's code-sign clones, free nothing when deleted, so they are never offered. The scan lists them under SHARED WITH INSTALLED APPS and dss usage marks them, because du and Finder count them at full size.
  • node_modules counts as regenerable only while it holds what package managers install: packages, @scope folders, .bin, lockfile copies and tool caches such as .cache or .vite. Any other data kept there (verification runs, captured evidence) blocks the whole directory, and the check runs again right before deleting.
  • Git worktree folders (.worktrees, worktrees) restart the project search depth, so dependencies of nested worktrees are found below --max-depth.
  • clean automatically reuses a matching scan from the last 30 minutes. --fresh bypasses this plan and the discovery/size index; --cache-dir DIR chooses the state location (default $XDG_STATE_HOME/disk-space-saviour, or ~/.local/state/disk-space-saviour).

Tiers

Tiers are cumulative: moderate includes safe. Regenerable data is never ranked by how recently it was used: a cache, build output or dependency directory is safe and is held back only while something uses it (see Safety).

| Tier | What it removes | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | safe | Everything regenerable: download, runtime and application caches, idle package-runner entries, old browser revisions, node_modules, vendor, target/, .venv, build/ and similar folders of any project, dangling Docker images and build cache. | | moderate | Data that is disposable but not regenerated by a tool: old agent scratch and Telegram downloaded media (secret-chat media may be irreplaceable), unused toolchain versions no process runs from (the item names the global packages it removes; never one holding virtual environments), tagged images and stopped containers (both require explicit consent). | | aggressive | Accepted by dss emergency for compatibility; currently the same as moderate. |

Covered ecosystems: JavaScript/TypeScript (npm, yarn, pnpm, bun, deno, framework build caches), Python, Rust, JVM (Gradle, Maven, Kotlin, Android), Go, C/C++ (CMake, ccache, vcpkg, Conan), .NET, PHP, Ruby, Swift/Xcode, Dart, Haskell, Scala, Elixir, OCaml, Lean, Julia, R, Zig, and scripting languages. Also covered: version managers (nvm, pyenv, rbenv, SDKMAN!, rustup, elan, ghcup, opam, swiftly, VS Code Server, Claude Code, the Codex daemon and Copilot CLI), Playwright, playwright-go, Puppeteer and Cypress browsers, HTTP caches of Chrome, Chromium, Edge, Brave, Opera, Opera GX, Vivaldi, Arc, Yandex Browser, Naver Whale, 360, QQ Browser, Firefox, LibreWolf, Waterfox, Zen and Floorp, IDE caches (JetBrains, VS Code, Cursor, Windsurf, Qoder), AI agent caches and logs, the Discord cache, macOS app updates staged by Sparkle and Squirrel, package manager archives, trash, crash reports, core dumps and journald logs. Application downloads are covered too: Chrome bundle clones left by exited browsers (kept while any browser runs the clone's hard-linked executable, and counted as free when they still share their blocks with the installed app), Chromium on-device AI models (downloaded again when a page uses built-in AI), Google Updater installers, ChatGPT Atlas and Codex app caches, Electron and electron-builder archives, the Bun transpiler cache, Docker Desktop and Warp staged updates (kept while the app runs), Zed language servers, tree-sitter toolchains, Codex plugin staging, the Clang module cache Crashpad reports (processed ones, and unsent ones after 30 days), rotated app logs in ~/Library/Logs (warp.log.old.2) and the GitHub CLI run-log cache. Telegram pure caches, Safari/WebKit, Chromium code/GPU/Dawn/service-worker caches and marker-validated Electron caches are included. Old Playwright browser revisions are kept while a Playwright package recorded in the registry's .links still launches them or a browser runs from them; Playwright installs and downloads left in temporary directories are removed once untouched. Telegram caches are removed only while Telegram is not running. Telegram databases, browser profiles and Playwright MCP profiles are preserved. macOS access failures explain when Full Disk Access is needed. Agent scratch cleanup requires inactivity and Git safety checks. Anything inside a Claude Code session scratchpad ({TMP}/claude-<uid>/<project>/<session>) is kept until the session and its transcript in ~/.claude/projects stay untouched for the inactive window (30 days), checked again right before deletion.

Safety

  • Dry run by default. scan never deletes. clean and emergency delete only with --yes or an interactive yes, and never with --dry-run.
  • Regenerable data only. Every rule describes data that a tool recreates: a cache, a build output or a dependency install.
  • Liveness checks, repeated right before each path is deleted. Paths that a process holds open (/proc/*/fd, cwd, exe, or lsof on macOS) are skipped. So are projects with a running cargo, gradle, npm, node or similar tool, and anything modified within --older-than (1h). How long ago a project was last used does not matter. A tool running elsewhere does not block a runtime cache it is not using: a script of an installed project keeps no lock on the Bun install cache, while bun install, bunx or an auto-installing script does. A directory on the PATH of a running process is kept too: the process may still start programs from it.
  • Per-app macOS folders stay while their app runs. Caches, containers and other folders under ~/Library/<kind>/<bundle-id> belong to the app with that bundle identifier, read from the Info.plist of every bundle a running process starts from. Such a folder is blocked while that app, a helper or an updater of it runs (for example the WebKit cache of Music, Telegram's group container), even when no rule names the app's process. A path held open names its holder: in use: <path> (pid N, App).
  • Blocked space is grouped by what frees it. A WARNING: line and the FREEABLE ON CONDITIONS section total blocked space by the condition that clears every blocker of an item: quit an app, a process or Claude Code session ending, files closing, the activity window passing, a container stopping or a later scan finishing what this one ran out of time for. Items kept for Git state, protected installs or unverifiable records are counted apart. clean repeats the summary after the plan. Rerun dss once a condition is met; it rechecks everything before deleting. clean scans again instead of reusing a saved scan when a process named by its blockers has exited, and warns when the scan behind its plan stopped early (for example at --scan-budget), since items it never reached are not planned.
  • Toolchain versions are kept while anything may need them. The manager's default and aliases, versions pinned by a project (.nvmrc, rust-toolchain.toml, ...) and the newest install stay. A partial nvm pin such as 20 keeps only the newest matching 20.x, as nvm resolves it. A version holding globally installed npm packages or pyenv virtual environments is blocked; bundled npm/corepack and use-m caches (<name>-v-<version>) are reinstalled on demand and do not count. A Claude Code version stays while a running session uses it: its executable is open, or its ~/.local/state/claude/locks/<version>.lock names a live process or cannot be read. All of this is checked again right before each version is deleted. Container paths are checked individually before each bounded deletion batch.
  • Rust builds are pruned in two parts. cargo-superseded removes older hashes of a unit (.rlib, .rmeta, .so/.dylib/.a, build/*, .fingerprint) that a newer build replaced, but never while cargo or rustc runs in the project: a running build may have judged them fresh. cargo-superseded-leaf removes incremental session directories idle for 3 hours and test/example binaries older than 3 hours that a newer build replaced. Nothing links against them, so this part stays eligible while cargo runs; each file a process runs or holds open is skipped.
  • Running tasks must survive. Before its first deletion in an environment (the host or a container), clean records PID 1, agents (claude, codex, solve, ...) and build tools (cargo, gradle, ...) with their start times. It checks them again after every removed path and item. If one is gone or its PID was reused, cleaning in that environment stops at once, and the audit log and summary say which process was lost. A build that simply finishes during the cleanup also stops it: an exit cannot be told apart from a crash. Host cleanup watches processes in its own mount namespace; each container watches its own tasks. Processes with unknown namespace membership remain watched.
  • Git awareness. Whole projects and containers are protected while they have unsaved changes, unpushed commits or stashes. Ignored dependency and build outputs can be cleaned despite unrelated source changes; tracked or non-ignored files inside an artifact remain protected. Stopped-container scans show Git state before cleanup. Unstaged and untracked files can be recognized as preserved when their bytes and modes all match one cached remote ref, such as origin/recovery/<branch>. No remote fetch is performed; missing refs, staged work and unreadable files remain blockers. Detached upstream checkouts are accepted only when their commit is reachable from a remote ref. Nested Docker layer copies and temporary Codex plugin checkouts are excluded from this inspection. Unsaved container work requires an exact --allow-dirty-container ID override.
  • Audit log on every run. Each run, dry runs included, writes dss-<command>-<time>-<pid>.json with every decision, the bytes freed and left per environment, and disk usage before and after. Scan audits include every candidate and its blockers; blocked candidates do not contribute to reclaimable tier totals. If a read-only scan cannot save its audit, it warns and still prints the report. When the disk is full, clean and emergency delete anyway and write their audit as soon as deletions free room; if it never fits, the summary says Audit log: not written (ENOSPC ...). Other audit failures, such as an unusable --audit-dir, still stop them before deleting. Root scans with another user's HOME use a private temporary audit directory. Set --audit-dir to choose a persistent writable location.

Long-lived npm exec/npx servers allow idle npm download and node-gyp header caches to be cleaned. Active npx hashes remain protected through their child command paths, working directories and open files; other hashes are separate candidates subject to the age window. Install commands still block relevant caches. Bunx, pnpm dlx and Yarn dlx entries use the same path protection, and dss always protects its own installation. Interrupts finish the partial audit and report the abort. Unreadable processes retain their readable identity; uncertain activity blocks relevant paths and owners and produces one environment hint. Old isolation logs and sanitized upload staging are moderate-tier candidates. Opam download cleanup removes its cache directory directly and does not invoke opam clean or migrate the root.

Scans have a ten-minute budget per filesystem environment (two minutes for dss emergency, which must act quickly on a full disk), configurable with --scan-budget 30m (library: scanBudget). Progress goes to stderr, leaving JSON stdout clean. SIGINT and SIGTERM stop new work, cancel subprocesses and print completed findings marked aborted; incomplete findings remain blocked. The CLI exits 130 or 143 respectively. A second signal exits immediately. Library callers can pass an AbortSignal and an onScanProgress callback.

Global caches run first, prioritizing rules that can use native sizing without recursive activity timestamps, and emit findings as each rule finishes. Non-interactive clean plans or deletes completed safe host caches while the remaining discovery continues. emergency does the same, checks actual free space after each deletion, and skips the remaining scan once the goal is met. Interactive runs show a plan before asking for deletion consent. A later phase hitting its budget blocks its partial findings while completed independent findings stay usable.

Project discovery uses eight directory workers, prunes matched generated trees, Git objects, home media/Photos libraries, macOS Library and mounted volumes. Source activity is checked separately without sizing entire project trees again. Spotlight (mdfind) or plocate/locate supplies verified early hints when available; the walk also runs because an index can miss files. Version-pin discovery also prunes dependency/build trees.

The persistent index/ stores directory listings and candidate measurements. Each descendant directory's own mtime/ctime and identity is checked before reusing its listing, so a nested addition is visible even when an ancestor did not change. Unchanged directories avoid another listing; discovery still checks their metadata. Sizing uses bounded workers and du -skx when no recursive activity timestamp is needed. Sizes cached for up to an hour are labeled estimated, because changing an existing descendant need not update the candidate's directory mtime. Cleanup always bypasses that cache and rechecks processes, open files, Git, path boundaries and sizes. Safe cleanup also rechecks source activity. Explicit moderate cleanup preserves the ability to remove ignored dependencies when only source changed. Library callers opt into state with cacheDir.

On macOS, reports include sysctl vm.swapusage and the largest memory consumers from top (memory includes compressed and swapped pages). These are memory diagnostics, not verified per-process swap ownership. On Linux, reports include /proc/meminfo swap and the processes with the most swapped memory (VmSwap). Each scan records swap usage and its largest holders in the state directory, so the next scan shows how much both grew during the last week. A WARNING: line appears when swap grew by 1 GiB or more, is at least 75% full, or exceeds the free disk space it can grow into, naming the processes holding the memory and their growth. The dss run itself is listed as (this dss run) and never named as a process to quit. Long-running AI agent sessions keep their history in memory: quitting and resuming them (codex resume <session-id>, claude --resume) releases it. When Docker Desktop consumes memory, saving work and restarting it or lowering its VM memory limit can help. dss never deletes swap files.

Git clones in temporary directories ({TMP}/* and {TMP}/*/*) are safe to remove once untouched for the activity window when every worktree sharing the repository is clean, none lives outside the clone, nothing is stashed, and every commit reachable from HEAD, a branch or a worktree is advertised by an https or ssh remote. That last check runs git ls-remote against the remote rather than trusting remote-tracking refs, outside any repository, with only the https and ssh transports and no prompts. Clones of local paths and clones whose .git is a symlink are left alone, and cleanup repeats every check right before deleting.

Cache discovery honors tool configuration before default paths, including GOMODCACHE, GOPATH, GOCACHE, CARGO_HOME, RUSTUP_HOME, GRADLE_USER_HOME, DENO_DIR, BUN_INSTALL_CACHE_DIR and XDG_CACHE_HOME. Go, npm, pip, Yarn and pnpm cache queries are bounded and cached per environment. Safe rules also cover perlbrew build/download trees, SDKMAN tmp/archive contents, nvm downloads, idle target/semver-checks, and downloaded Ruby .gem files. SDKMAN parent directories and installed gems are preserved.

Docker

With --docker, or automatically when a daemon is reachable, dss handles three kinds of Docker data:

  • Daemon data: docker system df, dangling images and build cache. --include-volumes lists unattached volumes for manual inspection; it does not delete them.
  • Unused tagged images are listed with their size, creation date and the container that used them last (from the docker inspect backups of containers dss removed), when known. No tier removes them, emergency mode included: approve each one with --remove-image REF (repo:tag, repo for :latest, or an id prefix of at least 12 characters) or an interactive yes for that image. The summary line counts them, and stopped containers that need consent, as needs explicit consent, so the tier totals match what clean --tier … --yes removes.
  • Running containers are never stopped, restarted or removed. They are scanned and cleaned from the inside through docker exec, with the same rules and liveness checks as the host. An unreadable /proc probe is retried with a read-only root docker exec --privileged inspection. If this is denied or incomplete, cleanup stays blocked and names the unreadable processes. This also applies when dss runs inside a container with access to its Docker daemon. Root probes also retry unreadable processes as their owning UID/GID with setpriv when available. Readable evidence is retained after failed retries; unresolved inspection produces one environment hint. Blocked totals show measured bytes and separately identify visible bytes whose freeable size is unknown.
  • Stopped containers are listed with their size, image, command, owner session (HIVE_MIND_PARENT_SESSION_ID and other session labels or environment variables, else a session id in the container name), task URL, exit code and reason, OOM flag and end time, and the Git state of repositories in their writable layer. The layer is copied out with docker cp and checked with Git. A container is removed only with --remove-stopped-containers or after an interactive yes per container, and only when its Git state is clean. Before docker rm, its logs and docker inspect output are saved to --backup-dir.
  • Containers kept for investigation (a non-zero exit code or OOM killed) are never removed by a tier or by --remove-stopped-containers, in emergency mode too. Name each one with --remove-container ID|NAME (a full name, or an id prefix of at least 12 characters) to remove it. Alternatively, --investigation-max-age 7d lifts holds for containers that ended at least seven days ago; removal still requires consent and Git checks. The default keeps investigation holds indefinitely.

Container cache sizes count changed writable-layer file blocks and mounted data. Immutable image data is reported separately as imageBytes and is excluded from reclaimable totals, including the host scan when dss runs inside an identifiable container. Image-only caches stay visible and blocked; rebuild the image to remove that data. These estimates exclude directory metadata overhead. A small stopped container pinning a large image is shown even below --min-size; its potential image bytes require a separate image removal after the container is removed.

--no-docker makes no Docker calls, including for storage accounting and saved-report cleanup. An overlay root without authoritative upperdir evidence has unknown freeable bytes and contributes zero to reclaimable totals. Measurements prefer accessible upperdirs; the diff fallback shares one serialized snapshot and cached stats per container/run, checks host and cgroup memory headroom, and skips excessive candidate counts. Fresh Git checks before stopped-container removal still inspect new work. Verbose mode reports storage measurement progress. Host scans prune runtime, nested overlay, read-only and unselected mount points; explicitly selected ordinary mount roots are allowed. For containerd-backed running containers, the daemon-side init process's /proc/<pid>/mountinfo supplies the overlay upperdir when GraphDriver is absent. JSON environments and the text summary show the storage source. Paths are measured on the daemon host and removed inside the container so overlay whiteouts are respected; cleanup reports the measured change in allocated upperdir blocks when available. Remote filesystem operations use bounded POSIX-shell batches and do not require Node inside the target container.

Docker discovery uses a size-free container listing and measures each container independently. Failed size queries are retried once; objects remain visible with sizeUnknown when measurement fails. Scans continue through other objects and nested daemons, report partial errors, and return a nonzero CLI exit status when discovery is incomplete.

--recursive follows Docker-in-Docker: a container that runs its own daemon is scanned as a new environment, down to --depth levels (default 3). The CI job Docker-in-Docker Integration builds a host → l1 → l2 chain and runs tests/integration/dind.mjs against it (npm run test:dind).

Scan roots

Project discovery walks a list of roots per environment; the traversal policy (which mount points are crossed) uses the same requested roots, so both always agree:

  • host: the positional paths (roots in the API), otherwise the homes and temporary directories;
  • every scanned container, at every Docker depth: --container-root PATH (containerRoots, repeatable) replaces the defaults; without it, the container's homes, temporary directories, existing common workspace paths (/workspace, /workspaces, /app, /src, /code, /project, /usr/src/app) and every host root that also exists inside the container. Host and container paths are different namespaces, so no path is remapped.

Each JSON environment carries scope: its roots with source (roots, container-roots, home, tmp, container-default) and status (searched, partial, absent, inaccessible, pruned, excluded, not-searched), plus maxDepth, exclude and prunedMounts. The text report names requested roots that were not searched. scanStatus: complete means complete within that scope; exclusions, mount boundaries and liveness checks still apply.

Hard links

Sizes are physical reclaimable bytes. A file's blocks count toward an item only when every hard link to it lies inside the deletion set: a package cache or node_modules hard-linked to a shared store (pnpm, Nix, uv) reports those blocks as sharedBytes, next to allocatedBytes (what du prints). Each dev:inode is counted once across the whole selection, so files linked between two selected paths are reclaimable once. In the audit log, sharedBytes lists blocks that stayed allocated; freedBytes is a measured before/after value for Docker writable layers (counting only blocks whose every hard link is inside the deletion set, marked freedAccounting: 'hardlink-aware-measurement') and otherwise the link-aware estimate, marked with freedEstimated and freedAccounting.

Emergency mode

dss emergency --free 20G (at least 20 GiB available) or dss emergency --until 80% (at most 80% used) works through the tiers in order. Within a tier it removes caches first, then Docker objects, and stopped containers last, only when approved. It re-reads the disk after every deletion and stops as soon as the goal is met. It exits with 3 when even the aggressive tier cannot reach the goal. --path chooses the volume (default /); items on other host volumes are skipped.

Library

import { scan, clean, emergency, formatReport } from 'disk-space-saviour';

const report = await scan({ roots: ['/work'], docker: false });
console.log(formatReport(report));

// Library calls delete unless dryRun is set.
const audit = await clean(report, { tier: 'safe', dryRun: true });
console.log(audit.plannedBytes, audit.file);

await emergency({ free: '20G', removeStoppedContainers: false });

Types ship in src/index.d.ts. See examples/basic-usage.js for a runnable example and examples/universal-app for a report viewer.

Configuration

| Option / variable | Default | Purpose | | -------------------------------- | ------------------------------------------ | ------------------------------------------- | | --audit-dir, DSS_AUDIT_DIR | $XDG_STATE_HOME/disk-space-saviour/audit | Where audit logs go | | --backup-dir, DSS_BACKUP_DIR | …/disk-space-saviour/backups | Container log backups before docker rm | | --older-than | 1h | Activity window; newer files are kept | | --inactive | 30d | Idle time before agent scratch is abandoned | | --min-size | 1M | Smaller items are not reported | | --depth | 3 | Docker nesting depth | | --journal-keep | 512M | journald size to keep | | --verbose, DSS_DEBUG=1 | off | List everything, trace commands and timings |

$XDG_STATE_HOME falls back to ~/.local/state. Linux and macOS are supported (liveness uses /proc on Linux and lsof on macOS).

Repository settings

Protected-Branch Release Pull Requests

If main requires pull requests and the Pipeline Status check, configure a repository secret named RELEASE_PR_TOKEN. It must be a fine-grained PAT for an automation actor other than the workflow's built-in GITHUB_TOKEN, scoped to this repository with Contents and Pull requests write access and Checks read access. The release and generated-preview fallbacks use it to open the PR, wait for the PR's own checks, and merge only after they pass. The manual changeset-PR mode uses it for the same reason. Teams that generate short-lived GitHub App installation tokens can wire that action output to the same workflow inputs instead of storing a PAT.

Repositories that allow the release workflow to push directly to main do not exercise the fallback, but manual changeset PR creation still requires this secret.

Optional Docker Hub Publishing

Docker publishing is disabled by default. To enable it for a project that ships a Docker image, add a Dockerfile and configure these GitHub Actions settings:

| Setting | Type | Description | | -------------------- | ------------------ | ------------------------------------------------------------------------------------- | | DOCKERHUB_IMAGE | Variable | Docker Hub image name, for example namespace/image. This enables Docker publishing. | | DOCKERHUB_USERNAME | Variable | Docker Hub username used by docker/login-action. | | DOCKERHUB_TOKEN | Secret | Docker Hub access token used for registry authentication. | | DOCKER_CONTEXT | Variable, optional | Docker build context. Defaults to .. | | DOCKERFILE | Variable, optional | Dockerfile path. Defaults to ./Dockerfile. |

When enabled, the release workflow waits until the exact published npm version is visible in the npm registry, then publishes Docker Hub tags for latest and that same version. The Docker build also receives NPM_PACKAGE_VERSION as a build argument so Dockerfiles can install the matching published package.

ESLint Rules

Customize ESLint in eslint.config.js. Current configuration:

  • ES Modules support
  • Prettier integration
  • No console restrictions (common in CLI tools)
  • Strict equality enforcement
  • Async/await best practices
  • Strict unused variables rule: No exceptions - all unused variables, arguments, and caught errors must be removed (no _ prefix exceptions)

Prettier Options

Configured in .prettierrc:

  • Single quotes
  • Semicolons
  • 2-space indentation
  • 80-character line width
  • ES5 trailing commas
  • LF line endings

Development

npm install
npm test                  # Node.js, 30s per test
bun test --timeout 30000  # Bun
deno test -A --parallel   # Deno (fixtures need -A)
npm run check             # lint + format + duplication
npm run test:dind         # Docker-in-Docker integration (needs --privileged)
node bin/dss.js scan --verbose

Project Structure

.
├── .changeset/           # Changeset configuration
├── .github/workflows/    # GitHub Actions CI/CD
├── .husky/               # Git hooks (pre-commit)
├── examples/             # Usage examples
│   └── universal-app/    # React + GitHub Pages + Electron + Capacitor app
├── scripts/              # Build and release scripts
├── src/                  # Source code
│   ├── index.js          # Main entry point
│   └── index.d.ts        # TypeScript definitions
├── tests/                # Test files
├── .eslintrc.js          # ESLint configuration
├── .prettierrc           # Prettier configuration
├── bunfig.toml           # Bun configuration
├── deno.json             # Deno configuration
└── package.json          # Node.js package manifest

Multi-Runtime Support

This template is designed to work seamlessly with all major JavaScript runtimes:

  • Bun: Primary runtime with highest performance, uses native test support (bun test)
  • Node.js: Alternative runtime, uses built-in test runner (node --test)
  • Deno: Secure runtime with built-in TypeScript support (deno test)

The test-anywhere framework provides a unified testing API that works identically across all runtimes.

Package Manager Agnostic

While package.json is the source of truth for dependencies, the template supports:

  • bun: Primary choice, uses bun.lockb
  • npm: Uses package-lock.json
  • yarn: Uses yarn.lock
  • pnpm: Uses pnpm-lock.yaml
  • deno: Uses deno.json for configuration

Note: package-lock.json is not committed by default to allow any package manager.

Universal App Example

The template includes examples/universal-app, a Vite React app that imports add and multiply from src/index.js and renders a visual calculator UI. The same static build is used by:

  • GitHub Pages (npm run example:web:build)
  • Electron desktop packaging (npm run example:desktop:package)
  • Capacitor Android/iOS sync (npm run example:mobile:sync)

The example app has its own package.json and lockfile so template users can opt into the frontend stack without adding React, Electron, or Capacitor to the library package itself.

See examples/universal-app/README.md for local web, desktop, Android, and iOS testing instructions.

Code Quality

  • ESLint: Configured with recommended rules + Prettier integration
  • Prettier: Consistent code formatting
  • Husky + lint-staged: Pre-commit hooks ensure code quality
  • File size limit: Files must stay under 1500 lines for maintainability (enforced via ESLint and CI)

Release Workflow

The release workflow uses Changesets for version management:

  1. Creating a changeset: Run bun run changeset to document changes
  2. PR validation: CI checks for valid changeset in each PR
  3. Automated versioning: Merging to main triggers version bump
  4. npm publishing: Automated via OIDC trusted publishing (no tokens needed)
  5. Optional Docker Hub publishing: When configured, waits for the exact npm version and tags the Docker image with that version
  6. GitHub releases: Auto-created with formatted release notes

First release of a brand-new package: npm publishes only through trusted publishing, and a trusted publisher can only be attached to a package that already exists (the first publish from CI fails with E404). Publish the first version once with package-registry-manager: npx package-registry-manager setup --registry npm --execute. It signs in through the browser, publishes, attaches this workflow as the trusted publisher and signs out again. No npm token is ever stored.

Manual Releases

Two manual release modes are available via GitHub Actions:

  • Instant release: Immediately bump version and publish
  • Changeset PR: Create a PR with changeset for review

CI/CD Pipeline

The GitHub Actions workflow (.github/workflows/release.yml) implements a fast-fail pipeline:

Fast checks (~7-30s each, run first for fastest feedback):

  1. Test compilation: Syntax-checks all .mjs files with node --check
  2. Lint, format & secrets scan: ESLint, Prettier, jscpd, and secretlint for credential leak detection
  3. File line limits: Enforces the 1500-line limit on JavaScript (.js, .mjs, .cjs) and Markdown (.md) files plus release.yml
  4. Changeset check: Validates PR has exactly one changeset (added by that PR)
  5. Version check: Blocks manual version changes in package.json
  6. Documentation validation: Checks required doc files (doc line limits are enforced by the file line limits check)

Slow checks (only run after all fast checks pass):

  1. Test matrix: 3 runtimes × 3 OS = 9 test combinations
  2. Broken link checks: Validates all links in Markdown/HTML files (separate workflow)

Release (on merge to main):

  1. Changeset merge: Combines multiple pending changesets at release time
  2. Release: Automated versioning and npm publishing
  3. Optional Docker publish: Publishes Docker Hub latest and npm-version tags after the npm package is visible

Reasonable Timeouts

Every CI job declares an explicit timeout-minutes so hung steps fail in minutes instead of reaching the GitHub Actions default of six hours. Fast checks use 5-10 minute caps, release jobs use 50 minutes, and the link checker uses 10 minutes for external network variance.

That cap is a backstop, never the deadline: GitHub reports a job it kills as cancelled, not failed. Long steps therefore own an explicit budget via scripts/run-with-budget-warning.sh, which warns at 70% of the budget and fails the step with exit code 124 when it expires. See CI-TIMEOUT-BUDGETS.md.

Individual tests are also capped inside supported runners: npm test runs node --test --test-timeout=30000, and the CI Bun runner uses bun test --timeout 30000. Both bound a single test, not the suite, which is why the suite budget above exists. Deno does not provide a single global per-test timeout flag, so Deno tests are protected by their step budget and the matrix job backstop.

See BEST-PRACTICES.md for detailed explanations of each practice.

Robust Changeset Handling

The CI/CD pipeline is designed to handle concurrent PRs gracefully:

  • 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.

  • Release-time Merging: If multiple changesets exist when releasing, they are automatically merged into a single changeset with:

    • The highest version bump type (major > minor > patch)
    • All descriptions preserved in chronological order

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.

Deploying the example app

The example-app.yml workflow deploys the universal example app to GitHub Pages on every push to main. Before the first run on main in a new repository created from this template, open Settings → Pages and set Source = GitHub Actions. This is a one-time manual step and cannot be configured from a workflow because the Pages source defaults to Deploy from a branch. Without it, the pages-deploy job fails on actions/deploy-pages with Get Pages site failed / Failed to create deployment. After flipping the source, the workflow provisions the Pages site on its first run.

Auto-regenerated preview screenshots

The same example-app.yml workflow contains a preview-regen job that boots the built example app in a headless Chromium via browser-commander + Playwright and writes fresh screenshots to docs/screenshots/example-app/example-app-{locale}-{theme}.png on every push to main (and on workflow_dispatch). Any drift is committed back to main so README/site images never go stale between releases. Screenshot-only pushes do not match this workflow's path filter, while a protected-branch fallback PR remains eligible for its required checks. The job runs in the official Playwright container with the browser already installed, avoiding CI stalls from live Chromium downloads.

The same script is available locally:

npm install --prefix examples/universal-app
npm run example:web:preview-images
# Verbose probe of <html data-theme>, <html lang>, and PNG signatures:
PREVIEW_VERBOSE=1 npm run example:web:preview-images

The matrix defaults to {en, ru} × {light, dark}. The shipped example app has no localization or theme toggle yet, so every cell currently renders the same UI — when a fork adds either, the matrix produces real per-cell variants without script edits.

Broken Link Checker

The link checker workflow (.github/workflows/links.yml) validates all links in Markdown and HTML files:

  1. Detection: Uses lychee to walk the checkout for .md and .html files (respecting .gitignore, including dot directories such as .changeset)
  2. Transient re-check: Links that failed with a transport error or an HTTP 429/5xx answer are retried with a capped wait before they count as broken; GitHub file links fall back to the contents API. Set RECHECK_VERBOSE=true to log every probe
  3. Web Archive fallback: For any broken links found, automatically checks the Wayback Machine for archived versions
  4. Actionable suggestions: Reports one of three outcomes for each broken link:
    • Archived: Suggests the Web Archive URL as a replacement
    • Not archived: Clearly reports the link is unrecoverable
  5. Scheduled checks: Runs weekly to catch links that break over time (even if no files changed)
  6. Issue creation: On scheduled runs, creates a GitHub Issue with the full broken links report

Add regex patterns to .lycheeignore to exclude URLs from checks (e.g., local dev URLs, example.com, known rate-limited sites).

Scripts Reference

| Script | Description | | ------------------------------------ | ----------------------------------------------------- | | bun test --timeout 30000 | Run tests with Bun and a 30s per-test cap | | npm test | Run tests with Node.js and a 30s per-test cap | | bun run lint | Check code with ESLint | | bun run lint:fix | Fix ESLint issues automatically | | bun run format | Format code with Prettier | | bun run format:check | Check formatting without changing files | | bun run check | Run all checks (lint + format) | | npm run example:web:dev | Start the universal app Vite dev server | | npm run example:web:build | Build the universal app static web bundle | | npm run example:web:preview-images | Regenerate preview screenshots via browser-commander | | npm run example:desktop:package | Package the Electron desktop app locally | | npm run example:mobile:sync | Build and sync the app bundle into Capacitor projects | | bun run changeset | Create a new changeset |

Best Practices

The repository implements CI/CD best practices for AI-driven development. See BEST-PRACTICES.md for details on:

  • File size limits for AI readability
  • Automated formatting and linting
  • Multi-runtime and cross-platform testing
  • Changeset-based versioning
  • Concurrency control for CI/CD pipelines

Contributing

See CONTRIBUTING.md for detailed contribution guidelines.

Quick steps:

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/my-feature
  3. Make your changes
  4. Create a changeset: bun run changeset
  5. Commit your changes (pre-commit hooks will run automatically)
  6. Push and create a Pull Request

License

Unlicense - Public Domain

Explicit macOS app removal and leftovers

dss apps remove "/Users/me/Applications/Example.app" previews removal; --yes moves only that bundle to the macOS Trash. It never empties Trash or removes support data. App identity and process activity are checked before removal; symlinked paths, nested app components and App Store receipts are refused. Use a vendor uninstaller first when one exists.

Chrome web apps should first be uninstalled using chrome://apps. --launcher-only explicitly allows moving their launcher bundles to Trash; it preserves Chrome's app registration, cookies, profiles and website data, so Chrome may recreate the launcher. The command is separate from automatic cache cleanup. Removed URL handlers may be recreated by their owning tool.

dss apps leftovers reports possible leftover Library files, including logs. An absent bundle in standard application folders is only a clue: apps can live elsewhere, and helpers or other apps can share data. Caches and logs are marked disposable-after-activity-check; settings, application support, containers, and saved state require a user decision. Every candidate remains read-only, including disposable candidates. No leftover is safe to delete unconditionally; logs may be needed for investigation. Inventory errors appear in the report.

Platform-specific competitor comparison

See the feature comparison tables for dss and 30 competitors across Linux, macOS, FreeBSD, OpenBSD, NetBSD, illumos and AIX. Capability, interface and safety-backend tables distinguish implemented features, vendor documentation, partial coverage and unknowns. The comparison is a sourced inventory, not a performance or superiority claim.