Skip to content

DevToolz ​

A toolbox of independent CLI commands for routine dev chores — the kind of thing you'd otherwise do with a half-remembered regex or by hand, right before a commit. One binary, one subcommand per chore, each safe by default: preview first, -y to actually write anything.

The image commands image-hash and image-batch moved to their own package, MediaToolz, in DevToolz 0.5.0. Here both only print where they went and exit with code 2.

Features ​

  • strip-comments — removes //, /* */, /** */, and <!-- --> from source files in place. --keep-jsdoc leaves a /** */ directly above an exported declaration alone, so IDE tooltips survive; everything else goes. Understands .vue files — <script> through the same parser, <!-- --> inside <template> through a line-aware scan.
  • console-strip — removes console.log/console.debug/debugger statements left in by mistake. console.warn/console.error are deliberately not touched by default — often legitimate production logging, not debug leftovers (--methods overrides the list). Only ever deletes a call that's a whole statement on its own — one that's part of a larger expression, or sits inside a brace-less if/while/for body, is left alone and reported separately, never guessed at.
  • dead-exports — finds named exports nothing in the project imports. Aware of what "dead" actually means for a published library: a package's own public entry points (auto-detected from package.json's exports/main/module/bin) are exempt by default — being unused inside the repo isn't the same as being unused, that's the whole point of exporting it. Traces re-export chains (export { x } from './y', export * from './y') back to where a symbol is really declared, and auto-detects a pnpm/npm/yarn workspace so a sibling package importing your export doesn't read as dead either. --strict checks the entry points too, for when that's actually wanted.
  • case-check — finds imports whose case doesn't match the real file on disk. Windows and macOS are case-insensitive by default, so import './foo' against a real Foo.ts works fine right up until it hits Linux CI. Checks every path segment, not just the file name; resolves tsconfig.json path aliases (@/...) too. --fix rewrites the mismatched specifier to its real case (alias-resolved ones excluded — see the command's own page).
  • unused-deps — finds package.json dependencies nothing imports, and the reverse — a "phantom" dependency, genuinely imported but never declared. Aware of the usual ways a package gets used without a direct import — invoked from scripts/lint-staged by its bin name, mentioned in a config file (vitest.config.ts's environment: 'happy-dom') — plus a separate category of packages that could never appear as text anywhere by design (@types/*, typescript, postcss/sass).
  • circular-imports — finds import cycles (A → B → … → A) in your own code — the kind that can silently produce undefined at runtime in ESM. Shows every cycle as its full chain, not just the first one found. A cycle made entirely of import type isn't a runtime bug and is hidden by default — --include-types shows those too, clearly marked.
  • exports-doctor — resolves every path package.json declares (main/module/types/typings/bin/exports, including nested condition objects, subpath maps, and fallback arrays) against what's actually on disk. Catches a file that doesn't exist, one that exists under a different case, a bin entry missing its #!/usr/bin/env node shebang, and — the sneaky one — a subpath whose runtime conditions resolve fine but that never declares types at all anywhere under it, so a TypeScript consumer silently gets no type hints with no error anywhere.
  • readme-check — extracts fenced ts code blocks from README/docs and actually typechecks them with the TypeScript Compiler API against the package's own tsconfig.json — nothing ever runs, only compiles. The virtual file lives at the package root, so both relative imports and a self-referencing import of the package's own published name (import { x } from 'my-package') resolve exactly as they would for a real consumer.
  • empty-catch — finds catch blocks that do nothing with the error, or do so little it's effectively swallowed: fully empty, or a body that's nothing but console.* calls with no throw, no write to an outer-scope variable, no meaningful return. A comment inside the block exempts the finding, same principle as ESLint's no-empty for documented empty blocks.
  • todo-report — summarizes TODO/FIXME/HACK comments across the project — file:line plus the note's actual text, including one wrapped across several // lines or a multi-line /* */ block, joined back into one readable line. --tags configures the list; --max <n> turns the default "fail on any finding" into a ratchet for a project knowingly living with existing debt.
  • scripts-check — cross-checks package.json's scripts against README/docs and .github/workflows/*.yml: a script mentioned but not declared (a real broken link), and, as a weaker signal, a declared script nothing documents. npm's own reserved lifecycle names are exempt from the second direction; a CI step under its own working-directory: is understood to belong to a different package.json entirely.
  • orphan-tests — finds test files whose source disappeared, the test still green and testing nothing real. Reliable only under a simple naming convention: co-located (including a sibling __tests__/tests directory one level down), or an explicit --source-dir/--test-dir mirror.
  • stale-ts-ignore — finds a // @ts-ignore that no longer suppresses anything. Runs a real project-wide typecheck twice — once as-is, once with every directive masked out — and compares the delta at each one's own line; the most expensive command here, so it says so before the work starts, and skips it entirely when there's no @ts-ignore anywhere. .vue files get an isolated per-file check instead, the same technique readme-check uses for its own virtual files.
  • ui — starts a local web interface and opens it in the browser, so the commands above can be run with forms, tables and graphs instead of flags. The checks of this package in one place, with an overview of all of them, every finding opened next to its code with syntax highlighting, and import cycles drawn as a graph; code cleanup with a diff you approve file by file and an undo. The server listens on this machine only, behind a one-time token, and nothing is written without an explicit choice.
  • full-check — runs all thirteen commands above in one sweep, each in its own already-safe mode, and shows one summary table instead of thirteen reports. Since stale-ts-ignore is the one command that can make a bare run noticeably slower, a plain devtoolz full-check in a real terminal asks about it first; --skip stale-ts-ignore skips both the question and the command.

strip-comments, console-strip, and case-check share the same safety model: --dry-run (or just running with neither --dry-run nor -y) only previews, -y/--yes is required to actually write anything, --diff shows a real unified diff per file. dead-exports, unused-deps, circular-imports, exports-doctor, readme-check, empty-catch, todo-report, scripts-check, orphan-tests, stale-ts-ignore, and full-check are all read-only — none of them ever write anything, there's nothing to preview. Every command supports --json for machine-readable output.

Output & colors ​

Every command colors its output with one shared palette: file paths in cyan with their :line:column dimmed, names and kinds in yellow, the "N problems found" heading in bold red, what a command would do in bold yellow and what it did in bold green, hints dimmed with the flags they mention (-y, --dry-run) picked out in cyan, and ✔/✖ in green/red in the full-check table. Diffs are red and green as before.

Color is on in a real interactive terminal and turns itself off when output is piped, when CI or NO_COLOR is set, or when TERM=dumb. Four things override that:

  • --color (or FORCE_COLOR=1) turns it on anyway — for devtoolz … --color | less -R, or a CI log that renders ANSI.
  • --plain turns off color, the banner and the celebration copy together.
  • --quiet only drops the banner and the output of a clean run; color stays.
  • --json prints the report as data, with no styling at all.

The text is the same with and without color — only escape codes are added, so a colored log still reads fine once they are stripped.

How It Works ​

Real AST parsing via the TypeScript Compiler API where the check calls for it, not regex guessing — a string, template literal, or regex-literal content that merely contains the text being looked for is never touched. exports-doctor and readme-check both look at a package the way a real consumer would: the first checks package.json's declared paths against the real disk, the second typechecks doc examples against the package's own build — both catch exactly the class of drift between what's written and what actually works that otherwise only ever surfaces for someone outside the project. scripts-check is the deliberate exception — it scans README prose and YAML text rather than parsing either, and says so plainly rather than implying AST-level precision it doesn't have. stale-ts-ignore takes real compilation the furthest of any command here — a genuine ts.Program over the whole project, run twice, rather than a single file or an isolated block.