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-hashandimage-batchmoved 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-jsdocleaves a/** */directly above an exported declaration alone, so IDE tooltips survive; everything else goes. Understands.vuefiles —<script>through the same parser,<!-- -->inside<template>through a line-aware scan.console-strip— removesconsole.log/console.debug/debuggerstatements left in by mistake.console.warn/console.errorare deliberately not touched by default — often legitimate production logging, not debug leftovers (--methodsoverrides 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-lessif/while/forbody, 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 frompackage.json'sexports/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.--strictchecks 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, soimport './foo'against a realFoo.tsworks fine right up until it hits Linux CI. Checks every path segment, not just the file name; resolvestsconfig.jsonpath aliases (@/...) too.--fixrewrites the mismatched specifier to its real case (alias-resolved ones excluded — see the command's own page).unused-deps— findspackage.jsondependencies 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 fromscripts/lint-stagedby its bin name, mentioned in a config file (vitest.config.ts'senvironment: '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 produceundefinedat runtime in ESM. Shows every cycle as its full chain, not just the first one found. A cycle made entirely ofimport typeisn't a runtime bug and is hidden by default —--include-typesshows those too, clearly marked.exports-doctor— resolves every pathpackage.jsondeclares (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, abinentry missing its#!/usr/bin/env nodeshebang, and — the sneaky one — a subpath whose runtime conditions resolve fine but that never declarestypesat all anywhere under it, so a TypeScript consumer silently gets no type hints with no error anywhere.readme-check— extracts fencedtscode blocks from README/docs and actually typechecks them with the TypeScript Compiler API against the package's owntsconfig.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— findscatchblocks that do nothing with the error, or do so little it's effectively swallowed: fully empty, or a body that's nothing butconsole.*calls with nothrow, no write to an outer-scope variable, no meaningfulreturn. A comment inside the block exempts the finding, same principle as ESLint'sno-emptyfor documented empty blocks.todo-report— summarizesTODO/FIXME/HACKcomments 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.--tagsconfigures 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-checkspackage.json'sscriptsagainst 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 ownworking-directory:is understood to belong to a differentpackage.jsonentirely.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__/testsdirectory one level down), or an explicit--source-dir/--test-dirmirror.stale-ts-ignore— finds a// @ts-ignorethat 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-ignoreanywhere..vuefiles get an isolated per-file check instead, the same techniquereadme-checkuses 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. Sincestale-ts-ignoreis the one command that can make a bare run noticeably slower, a plaindevtoolz full-checkin a real terminal asks about it first;--skip stale-ts-ignoreskips 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(orFORCE_COLOR=1) turns it on anyway — fordevtoolz … --color | less -R, or a CI log that renders ANSI.--plainturns off color, the banner and the celebration copy together.--quietonly drops the banner and the output of a clean run; color stays.--jsonprints 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.