Repository navigation
devtrim 0.11.0
·
8 commits
to main
since this release
Immutable
release. Only release title and notes can be modified.
A full review of the source against this Mac's real state. The read-only scan
offered 21.6 GiB while node-modules and artifacts failed outright on a
busy machine, and the toolchain the developer was using was offered for
removal. This release fixes what the review found, adds two read-only
reports, keeps the interface responsive, and cuts the test suite to the tests
that guard something (522 → under 300). It also ships the unpublished 0.10.10
fixes below.
Added
devtrim worktreeslists each repository's linked worktrees under the scan roots with their sizes: onegit worktree list --porcelain -zper repository through the hardened Git invocation, the branch or detached HEAD, and the lock and prune reasons Git reports. A listed path is measured only when it is a real directory inside a scan root whose.gitfile leads back to that repository; anything else is listed unsized with the reason, and the main worktree is listed unsized. Sizes follow no link, stay on one device, never count a nested worktree twice and are lower bounds when entries cannot be read. The report never says safe, stale or removable, prints no total (cloned worktrees share blocks), adds no deletion authority, rejects every mutation flag and writes no journalclean leftoversalso lists the folders inside a repository's top-level.scratchwhen Git ignores that folder (git check-ignore) and tracks nothing in it, labelling a linked worktree there as one. Repositories are found anywhere under the roots, not only one level downclean cachesoffers Gradle'scachesandwrapper/distsunderGRADLE_USER_HOME(default~/.gradle; never the folder itself, which holdsgradle.propertiesandinit.d), offered only when each folder holds what Gradle writes there (caches/modules-2, agradle-<version>-bin/-alldistribution) and the Gradle home is not inside a Git repository, soGRADLE_USER_HOMEalone never makes a folder a cache, and refused at scan and at apply while a Gradle daemon, build or Wrapper runs; the Go module cache at the pathgo env GOMODCACHEreports (queried withGOTOOLCHAIN=local, inside the home folder and holding Go'scache/download, asked again at apply; a permanent removal is refused while Go has left a folder read-only, andgo clean -modcacheis named); and the CocoaPods download cache~/Library/Caches/CocoaPods, whose finding namespod installas what brings it backclean xcodealso offerswatchOS DeviceSupport,tvOS DeviceSupportandvisionOS DeviceSupportbesideiOS DeviceSupport, with the same rulescodex_homesin~/.config/devtrim.tomlnames more Codex homes (for examplecodex_homes = ["~/.codex-berlin"]) thatagentsjudges exactly as~/.codex, at scan and at apply. Nothing is inferred from folder names. An entry that is not an absolute folder inside the home folder, is reached through a link, overlaps~/.codexor another entry, or does not exist refuses every command with an error naming the entry- The manual's "Keeping worktrees small" recipe: sparse checkouts, per-worktree installs from the package manager's store, APFS clones, and why cloned trees look larger than the space they hold
Fixed
toolchainsoffered the toolchain swiftly had in use: swiftly selects it through~/.swiftly/config.json, not through a symlink, so the 4.6 GiB in-useswift-6.3.3-RELEASE.xctoolchainon the development machine looked unreferenced. Every toolchain swiftly lists as installed is now kept, at scan and at apply, and shown as report-only withswiftly uninstall <version>as its removal path; a swiftly configuration that cannot be read, parsed or mapped to a toolchain name withholds the whole category (swiftly 1.1.4Config.swift,ToolchainVersion.swift;SWIFTLY_HOME_DIRhonoured)simulatorsmade everyscanexit nonzero on a Mac with the Command Line Tools but no Xcode. Exactly twoxcrunanswers now mean "nothing to manage", named in a diagnostic:xcrun simctlexiting 72 because there is nosimctl, andxcrun --versionexiting 1 with no developer directory. Any other failure is still an errorxcodefailed the whole category when one DeviceSupport or idle-Xcode DerivedData folder could not be read or measured. That folder now offers nothing and is named in one error entry; the rest is offered- Every permanent removal (
--shred, andtrash-empty) of a tree holding a folder without write and search permission was refused part-way, after the files before that folder were gone, leaving the rest under a quarantine name; a Go module cache moved to the Trash and then purged is the ordinary case. The permanent preflight now checks every folder first and restores the target whole, naming the folder. In the Trash, where the operator confirmed the purge by its size,trash-emptyinstead gives a read-only folder the current user owns its owner's write permission just before emptying it, so the Go module cacheclean cachesmoves there is purged; a folder owned by another user still refuses caches: the.cache/nodeentry is narrowed to Corepack's own cache,.cache/node/corepack(COREPACK_HOME's default;corepack cache clean), and labelled as Corepack's. The npm finding says it is npm's whole cache folder (_cacache,_npx,_logs)node-modules,artifactsandpurgeno longer fail on a busy Mac with "build processes changed again while their working directories were read". A build process that started during the liveness probe had to be read completely on one morelsof; a short-lived one that exited first refused the whole probe (9 of 30 read-only previews on the development machine, about 200 matching processes at load average 60–135). Each later lookup is now judged by the first round's rule — a processlsofdid not report passes only when a freshpgrepno longer lists it — up to fivelsofruns in all; a list still changing after that refuses. The same loop then passed 30 of 30- The build-process probe trusted an
lsofexit 0 even when it left a requested process out, so a buildlsofdid not report could lose its repository's dependencies; such a process is now judged like any unreported one (it passes only when a freshpgrepno longer lists it). Pre-existing; found by the release's security review trash-emptyno longer fails, preview and apply, when one Trash item cannot be measured. That item offers nothing and is named in one error entry; the other items are listed and purged, and the run exits nonzero- A category whose scan failed no longer vanishes from the
scantable, where it read as having nothing to offer. It keeps a row marked failed with the command that retries it uninstallno longer drops its whole report when one folder inside a container is unreadable; that size is a disclosed lower bound.icloudnames an unreadable folder as an error and still lists the files it could read- Names stored decomposed (NFD), such as
caféwritten ascafeplus a combining accent, print as written instead ofcafe\u{301}. Leading marks, the invisible combining grapheme joiner U+034F, variation selectors, joiners, controls and bidirectional characters stay escaped - The TUI no longer freezes while a scan or apply runs. Both run on a worker thread; the working screen counts elapsed seconds. During a scan,
qand Ctrl+C stop it: the terminal is restored, every command the scan started is killed, and devtrim exits 130. During an apply,qand Ctrl+C are refused until it returns, so no target is left half moved, and the screen says so. SIGINT and SIGTERM sent to the TUI process now restore the terminal, kill every started command and exit 130 or 143 instead of leaving the terminal in raw mode on the alternate screen; a second signal exits at once. Keys typed while work runs are still dropped, so type-ahead cannot approve a plan - In the TUI, a category whose scan failed is counted in the results title and named in the status with "r retries";
ralso works on the Error screen. The title drops words before numbers so the mode, danger and failure count fit at 64 columns.salways turns permanent mode off (turning it on still needs a selected Trash item). A row without a path no longer reads "command action" unless it is a typed command. The Outcome and Error screens stop scrolling at their last line. The help overlay, README and manual say whatEsc,q,rand signals do on each screen;Escon the menu still quits - The non-interactive refusal of a danger-9 plan named
-y, which cannot answer a typed size; it now names--yolo - Report-only commands (
largest,icloud,uninstall) ended with "0 B actionable"; they now total what they report and say nothing is removed. Underscan --shredthe table's hint names--shred. The CLI danger tag for 6–8 uses named ANSI red instead of a fixed RGB orange. Thelargestnote claims a lower bound only when entries were skipped.statuslabels disk sizes as used and available instead of calling their sum the disk's size. A flag refusal's JSON envelope names the refusing operation (devtrim --jsonsaidunknown).--helpsays which commands honour the global mutation flags, andcompletions --helpdescribes its argument - Documentation: the manual's quick-start and
trash-emptysamples now show the real output, the release binary is 2.6 MB (not 1.5 MB), the README says which Python cache names need no corroborating file, the site no longer claims "bundler outputs", and the docs say what--jsondoes with a prompt on a terminal (stdout stays one document; an apply without-yor--yolostill asks on stderr) - Removed a stale doc comment that described a deleted
count-objectshelper
Changed
- The test suite keeps only tests that fail when the code breaks in a way that matters to a user, to data or to security: 522
#[test]functions became 279 before this release's additions. Every test the mutation gate names was kept unchanged; 18 spot checks broke the production code a removed test covered and showed a kept test failing (one removal was overturned and kept) - New planted mutation cases:
worktrees/common-dir,leftovers/scratch-ignored,trash/unmeasurable-item,tui/shred-off,toolchains/swiftly-{scan,apply,unparsable,unmapped},simulators/no-simctl,xcode/unreadable-folder,caches/gradle-running-{scan,apply},caches/gradle-repository,caches/gradle-layout,caches/go-namespace,sink/read-only-go-cache,sink/trash-permission-repair,liveness/lsof-silent-omissionandagents/codex-homes-{scan,missing,identity}. The gate now refuses a marker that is a prefix of another before building anything (PV xcode/active-identitywas one; it is now-spelling); the build-process successor case now removes the lookup bound
Fixed (unpublished 0.10.10)
- Interrupting a CLI run with Ctrl-C (SIGINT) or SIGTERM now stops every command it started. Each command runs in a process group of its own, which the terminal's Ctrl-C never reaches, so an interrupted run used to leave a
dockerprune or a stuck query running. Every live group is now registered insrc/process.rs; on either signal they are all killed before devtrim exits with 130 (SIGINT) or 143 (SIGTERM), and an apply attempt in flight stays in the journal without a result, whichhistoryreports as interrupted. A group is never signalled after its leader was reaped. The handler writes no message and a second signal exits at once, so a full or unread stderr can never stall the exit. A run started with SIGINT ignored (a background job of a non-interactive shell) now stops on SIGINT too, since there is no safe way to read the earlier disposition. The TUI handles the same signals itself (see Fixed above): it restores the terminal first, then kills every command and exits. The handler usessignal-hook, a crate already in the lockfile through crossterm - A running coding agent now protects the repository containing its working directory from
node_modulesandartifactscleanup, as any build process does; a repository it edits by absolute path from elsewhere is not protected. The build-process namespgrep -xmatches gaincodex,claude,rust-analyzerand Claude Code's real process name, its version number (2.1.294for~/.local/share/claude/versions/2.1.294), matched only as a whole dotted triple of digits with an optional SemVer pre-release suffix such as2.2.0-beta.1(a process named like that qualifies, which can only protect or refuse more;2.1,1.2.3.4andcodex-code-modedo not match) - One cache that cannot be measured no longer fails the whole
cachescategory. It is named in one error finding, as the project categories name folders their walk cannot read, so the run still exits nonzero and nothing in it is offered, while the other caches are listed as usual - Moves to the Trash use
NSFileManagerinstead of Finder's AppleScript, which needed Automation permission for the calling process (it failed with that error on 2026-10-02) and was slow on a loaded machine. Every check around the move is unchanged. Finder's "Put Back" may not remember where an item came from; drag it out of the Trash to restore it. The deletion ast-grep rules now also reject a.delete(…)or.delete_all(…)Trash call outside the sink - External commands run under a wall-clock limit, so one stuck program no longer hangs a scan. Git,
simctl,docker,npmandbrewqueries get two minutes, thepgrepandlsofliveness probes three (a system-widelsoftook 16 s at load average 300), and the typed Docker, simulator and maintenance commands an apply runs fifteen. Past its limit the command's whole process group is killed and the child reaped, so a helper such asdocker's buildx plugin does not keep working after the failure is reported, and the repository, category or safety check that asked fails with an error naming the command and the limit; a timed-out liveness probe refuses like any failed one and never reads as "nothing running". Other categories still complete. Each command runs in a process group of its own. Therustixdependency gains itsprocessfeature for the group kill (no new crate). Theno-unbounded-subprocessast-grep rule rejects an unboundedoutput,statusorspawninsrc/