Skip to content

Reference ​

Architecture ​

A single CLI runner on commander with one subcommand per command — each subcommand is self-contained: its own flag parsing, its own core-logic module, its own report renderer. What's shared across all fourteen is three things: an output module (banner, color, aligned columns for file/finding lists, consistent unified-diff rendering), the --dry-run/-y/--diff safety model for the commands that write to disk, and — for every command where it applies — the "real AST, not regex" principle: code parsing goes through the TypeScript Compiler API's ts.createSourceFile/scanner, exports-doctor resolves paths segment by segment via readdir (never existsSync, which lies about case on Windows/Mac), readme-check compiles a single virtual code block through a full ts.Program with a custom CompilerHost, and stale-ts-ignore takes that same technique to a whole real project, twice, the most compute a command here ever does. scripts-check is the deliberate exception — README prose and CI YAML aren't code to parse, so it scans text instead and says so, rather than implying a precision it doesn't have; orphan-tests checks file existence on disk, not source at all.

full-check is the one command that isn't its own diagnostic at all — it calls the other thirteen's own programmatic run*() functions directly (each already exported from the package's own root, for exactly this kind of composition) and never passes whichever option would let a command actually write to disk. Each call is wrapped individually, so one command throwing doesn't take the rest of the sweep down with it. Its interactive prompt (only for stale-ts-ignore, only in a real terminal) is deliberately a single question() call with no retry loop — real testing found that a SECOND question() on the same readline/promises interface can silently lose the answer and hang once more than one line is already buffered on stdin, a genuine quirk of piped/non-interactive input, not a bug specific to this code; asking once, with an explicit default on anything unrecognized, sidesteps the whole class of failure instead of risking it.

The tsconfig.json loader (ts.findConfigFile auto-detection, ts.parseJsonConfigFileContent for real compiler options) lives in src/utils/load-tsconfig.ts, not inside either command that needs it — it started out private to readme-check, and was extracted the moment stale-ts-ignore needed the same resolution logic too (plus the project's real file list, which readme-check never needed for its own single-block checks).

The import graph (parsing import/export, resolving relative specifiers, detecting a pnpm/npm/yarn workspace) lives in src/utils/, not inside any single command — it started out as part of dead-exports, and was extracted into shared infrastructure the moment a second consumer needed it (unused-deps/circular-imports), not preemptively. The same real-comment-detection pass (masking every string/template/regex/JSX text span before scanning for comment tokens, so a ////* inside one is never mistaken for a real comment) started out inside strip-comments and moved to src/utils/ the moment todo-report needed it too — same principle, applied again.

The humor and color in the output aren't decoration bolted on separately — they're part of the same output module: the banner and the celebration on a clean run are randomized from a shared phrase pool, the findings list itself stays dry and easy to scan regardless of the banner's mood, and color auto-disables outside a real interactive terminal (CI, NO_COLOR, TERM=dumb, a pipe) on top of the explicit --quiet/--plain flags; --color and FORCE_COLOR turn it back on. Every report builds its styling through one small style module (src/format/style.ts) rather than writing escape codes itself, so the palette is changed in one place and a report's text is identical with and without color. See Output & colors.

Comparison ​

Doesn't duplicate LintSync (syncing ESLint/Prettier/Stylelint configs across projects) or Polyrepo (versions, releases, publishing, dependency drift across local repos) — both already cover their own niche, and nothing from either scope made it into DevToolz on purpose. It's not a replacement for a real linter either: ESLint/Biome are configured once for a whole project and hold dozens of rules in mind at the same time, while each DevToolz command is a narrow, self-contained check of exactly one thing, with no configuration, meant to run once right before a commit or as its own separate CI step, without installing or maintaining a shared config.

Development ​

bash
git clone https://github.com/macrulezru/devtoolz.git
cd devtoolz
npm install
npm test

Tests are end-to-end, against real temporary directories (mkdtempSync plus real files on disk) rather than mocked AST — that discipline has repeatedly caught genuine bugs on a real filesystem, including Windows-specific ones (case-insensitivity, backslashes in paths that TypeScript always normalizes to forward slashes internally).

bash
npm run build       # tsc -> dist/
npm run typecheck   # tsc --noEmit
npm run lint        # eslint .
npm run format      # prettier --check .

License ​

MIT — see LICENSE.