Architecture
purgeit is a TypeScript CLI and library. The codebase is split into a framework-agnostic core and an Ink/React TUI layer.
Module overview
Section titled “Module overview”src/├── cli/ # argument parsing, headless orchestration, TUI dispatch├── config/ # cosmiconfig integration and config validation├── delete/ # deletion engine with safety guards├── rules/ # default rules, gates, merge logic, validators├── scan/ # filesystem walker, size computation, async queue├── ui/ # Ink-based interactive terminal UI├── format.ts # byte formatting and error helpers├── types.ts # shared TypeScript interfaces└── index.ts # public library APICore design principles
Section titled “Core design principles”src/ui/is the only place that imports React or Ink. The scanner, rule engine, config loader, and CLI core are framework-agnostic. This keeps the library API testable without rendering anything and is enforced by a CI grep check.- Async streaming. The scanner emits
found,size,warning, anddoneevents progressively so the UI can render matches as they are discovered and update sizes as they resolve. - Bounded concurrency. Both discovery and sizing use a shared
p-limitinstance (default 8) to avoid exhausting file descriptors or child processes. - Real filesystem tests. Unit tests use temporary directories rather than mocked
fscalls, so the tests exercise the same code paths used in production.
Scan flow
Section titled “Scan flow”scan()starts an async discovery pass.- In
projectsmode,listProjects()enumerates immediate children, identifies which are projects, and runs manifest validators. walk()descends each project tree, matching directory names against the ruleset.- Always-safe matches stop the walk; gated matches are reported only when the gate predicate passes.
- Each reported match is sized by
computeSize(), which batchesducalls on Unix and falls back to a pure-Node walk on Windows. - Events are streamed to the consumer via an
AsyncQueue.
Rule engine
Section titled “Rule engine”A ResolvedRuleSet contains:
alwaysSafe: directories that are always deletable (e.g.node_modules).gated: directories that are only deletable when a sibling condition is met (e.g.buildnext to apackage.json).skipDirs: directories that are never descended into.pruneMeta: directories treated like VCS metadata (e.g..git).targets: named groups of rule names.
User configs are merged with the built-in ruleset via mergeRuleSets(), then CLI flags (--no-gated, --targets) are applied via applyCliFilters().
Deletion
Section titled “Deletion”deleteEntries() is the last line of defense. It refuses to delete the filesystem root or the user’s home directory, supports dry-run mode, and continues past individual failures while aggregating counts in the final done event.