Clean Drive: list developer junk directory by directory - #26
Open
havokentity wants to merge 2 commits into
Open
Conversation
Clean Drive's system categories are a checkbox each: tick "Cache files" and everything under ~/Library/Caches goes, sight unseen. That is the wrong shape for a developer's disk, where the reclaimable space is concentrated in a handful of enormous, individually meaningful directories — one abandoned project's node_modules, last year's DerivedData, the device support for a phone sold two years ago. A developer wants to keep this one and drop that one, and a single checkbox cannot express it. So developer junk gets its own sheet, where every category expands into the individual directories that make up its total and each can be unticked on its own. Categories start with a sensible default and nothing moves until the user accepts a confirmation naming the count and the total. The safety argument is the reason the matching lives in pure helpers rather than inline in the scan. A cleaner that mistakes a source tree for a cache is not a bug you get to fix in the next release, so eligibility is decided in exactly one place — `DevJunkCategoryID.accepts` — and that gate is applied twice: when the scanner discovers a path, and again immediately before the delete, because entries survive across rescans and across the confirmation alert. Containment is component-wise, never a string prefix: `hasPrefix` would happily place ~/Code/MyApp-backup inside ~/Code/MyApp, which is exactly how this class of tool eats someone's work. The two categories that live in the user's own folders are the conservative case. A directory named node_modules only counts when a package.json sits beside it, and a .build only when there is a Package.swift; a folder someone named node_modules to hold notes is never touched, and neither is a .build belonging to some other build system. The walk is depth-capped, confined to the usual project roots, and prunes package and VCS stores so a nested copy is not offered twice. Deliberate omissions are documented where they are made: CoreSimulator/Devices holds installed simulator apps rather than caches, Docker's Data/vms holds every image and volume the user owns, ~/.gradle/wrapper holds checksum-pinned distributions, and ~/.cocoapods/repos may be a private spec repo that cannot be re-cloned. Deletion goes to the Trash rather than an outright unlink, the reverse of what the system categories do. Those sweep caches an app will regenerate in seconds; these are the user's project folders, so if the heuristics are ever wrong the mistake has to be undoable — and the sheet says plainly that the space comes back only once the Trash is emptied. Sizing runs off the main actor and streams a category at a time, so the list fills in progressively instead of waiting for the slowest tree, and the walk is cancellable: a node_modules sweep left running for a closed panel would keep a core busy for nothing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Electron applications ship their dependencies as Contents/Resources/app/node_modules with a package.json sitting right beside them, so the sibling-manifest test that keeps this feature conservative passed on them. An .app on the Desktop is exactly five components below a search root, which the depth-4 project walk reaches on its last permitted iteration, so an application the user happened to keep on the Desktop was listed as the innocuous row "app/node_modules", checked by default, and trashed on confirm. That breaks the installed application, and unlike a project's node_modules no npm install brings it back — which contradicts both the category's own detail string and the promise that the worst case here is a rebuild or a re-download. Bundles are now stepped over rather than into, and refused as an ancestor by accepts() as well, because accepts() is the gate the remover re-checks against and a fix in only one of the two would leave the last line of defence open. The extension list is deliberately wider than applications: frameworks and plug-ins carry the same layout, and a photo or Final Cut library is user data that should never be walked at all. accepts() also treated ".." as an ordinary path component. normalize() deliberately does not resolve traversal — it is a pure function and must not touch the filesystem — so containment was purely textual and "~/Documents/../.ssh/node_modules" read as living under a search root while resolving somewhere else entirely. No scanner today emits such a path, but this gate is presented as the guarantee that a stale or forged entry cannot delete the wrong thing, and against the one syntax a forged path would actually use it did not hold. Rather than teach the helpers to resolve traversal, the syntax is refused outright. Two smaller things in the panel. The Move to Trash button is live during a scan, and trashing starts a fresh one, so the cancelled scan's tail would clear isScanning and overwrite the status line with a count taken from its successor's half-filled results — the rescan ran with no progress bar, no Stop button and a wrong total. A generation counter now lets an outgoing task recognise that the state belongs to somebody else. And a wholly successful removal used to pass in silence, because presentResult returned early when there were no failures and startScan had already replaced the status line: the one path with no feedback was the one where an irreversible-looking bulk move had just succeeded. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Wave 2 Phase A.
Implementation: Clean Drive: list developer junk directory by directory
Review fixes: Clean Drive: never offer an application's own bundled dependencies
Built by an isolated agent, then adversarially reviewed by an independent agent that checked the branch out and ran
swift build+ the full suite itself. Every review finding was fixed, with a regression test added for each high and medium.🤖 Generated with Claude Code