The CLI Toolkit for Developers Who Sweat the Invisible Mess in Their Projects
theme: minimalism
🎙️ Foreword
In today's highly mature frontend engineering standardization, whether you have a code cleanliness obsession or not, you definitely know these tools that enforce explicit code standards:
- Various Linters: eslint, oxlint, stylelint, markdownlint, commitlint, and even npmpackagejsonlint for
package.jsonand zhlint for Chinese/English writing standards. - Code Formatters: prettier, biome, editorconfig
We agonize over a blank line, a space, a semicolon, unused variables, or non-standard naming. We can't tolerate any yellow squiggly lines or background highlights in the IDE.
However, the truly dirty engineering problems are not in the code writing style, but hidden in invisible corners like project dependencies, version redundancy, junk files, and code volume. These problems are often ignored or overlooked by most developers.
Writing elegant code is only surface-level tidiness; a clean project structure, healthy dependencies, controllable versions, and transparent volume are the ultimate pursuits of a high-level frontend cleanliness obsession.
Many projects appear to run, but actually harbor massive technical debt:
- Severely outdated dependencies: Vulnerabilities, old syntax, and deprecated APIs silently accumulate.
- Increasing redundant dependencies: Packages that will never be used reside permanently in
node_modules, wasting bandwidth and disk space. - Phantom dependency time bombs: Due to package hoisting, indirect dependencies are introduced, allowing your code to reference uninstalled third-party packages. The project runs fine, but a time bomb is planted, especially if your project publishes an NPM package, spreading the problem to your users and losing trust.
- Vague project code volume: Dead code, redundant files, duplicated logic, and massive code files.
None of these problems are easy to solve manually. This article discusses several minimalist, efficient CLI tools that the author (a severe code-cleanliness obsessive) heavily relies on to cure project messiness.
TL;DR
In the era of prevalent AI development tools, sticking to the old ways of coding is no longer realistic. People are increasingly turning to AI for every problem, like "update all project dependencies to the latest," "check if all packages in the workspace have redundant or missing dependencies," or "calculate the code volume for me." You might first think of AI, and indeed AI seems suitable for such drudgery, but...
"There is nothing new under the sun." The problems you think of have likely already been explored by others, and mature solutions exist.
Yes, there are already very mature solutions. They are not only free but also far more efficient than AI tools (when solving problems with a clear direction, traditional solutions are still much more efficient than AI). Why waste tokens? Ultimately, AI might just end up calling traditional tools to do the drudgery you wanted it to do.
This article will introduce the following CLI tools:
| Tool | Function | Notes |
|---|---|---|
| taze | Update dependencies | Once you use it, you'll forget the old flame ncu |
| ncu | Update dependencies | A veteran tool, used for many years |
| knip | Check for redundant or missing dependencies (and much more) | depcheck recommended it before going dormant; powerful enough to have its own website |
| depcheck | Check for redundant or missing dependencies | Went dormant in 2023 |
| syncpack | Unify dependencies | A pleasant surprise discovery while writing this article; also powerful enough to have its own website |
| tokei | Code line counting | Faster and smarter |
| cloc | Code line counting | A veteran tool, used for many years |
🥦 Keeping Dependencies Fresh - taze / ncu
Whether dependency versions are kept fresh is an important basis for judging the health of a frontend or Node project.
Every time I take over an old project, the first thing I do is check how stale and decayed the project dependencies are, then formulate a stable upgrade plan. A long-standing habit of mine is to check and update project dependency versions first thing every day when I open my workspace.
This habit has brought me many benefits, allowing me to perceive updates from well-known frameworks, libraries, and toolchains in the community firsthand, and learn new APIs. However, it also means being the first to be affected by issues from second and third-party packages. Unstable, you say? Upgrading is a double-edged sword; we need to upgrade to enjoy the community's dividends (bug fixes, feature enhancements, performance improvements), but we must also bear the potential introduction of new bugs.
In fact, by perceiving the iteration of second and third-party package versions, I can confine problematic versions to a smaller scope and provide more precise and timely feedback to colleagues on second-party packages or authors of third-party packages, preventing problems from expanding. Long-term stability is true stability.
Maintaining inert stability and refusing to upgrade leads to outdated dependency versions. Besides not being able to enjoy the community's dividends, more seriously, it can render the project unmaintainable. I have seen many such projects where development dependencies are completely out of sync with the current Node environment, even throwing errors during the installation phase.
You can use the following two tools:
- ncu A veteran tool over ten years old, still actively maintained. I used it for many years.
- taze Pronounced
/ta:zei/, Persian for "fresh". Developed by antfu. I have now abandoned my old flamencuand switched to it.
Global Installation
Not required, but recommended.
Install globally to get the global commands taze and ncu:
npm install -g taze
npm install -g npm-check-updates # Note: not ncu
A Quick Trial
Let's do a simple comparison.
Below, we want to check and update all dependencies across the entire Workspace (including the root directory) into packages.json. The commands used are taze -r -w --force and ncu -w -u respectively:
ncu is slightly slower, but the difference is not obvious. However, if you use pnpm -r exec, the difference is very noticeable (taze 28s, ncu about 4min):
Postscript: This is the biggest reason I switched from
ncutotaze. Previously, I was usingpnpm -r exec ncuto update the entire Workspace and didn't knowncuhad a-wparameter. This highlights the importance of writing articles; to be more rigorous, I carefully reviewedncu's parameters and found I had misunderstood it all along.
Common Parameters
Both taze and ncu offer a very rich set of command-line parameters. Below is a comparison of commonly used ones:
| Function | taze | ncu | Notes |
|---|---|---|---|
| Recursive Scan | -r / --recursive |
w / --workspaces --deep |
taze -r and ncu --deep are equivalent, scanning even directories not part of the Workspace |
| Write Mode | -w / --write |
‑u / --upgrade |
Updates all package version numbers in package.json to the latest within the specified range |
| Update Global | ‑g / --global |
‑g / --global |
ncu explicitly states it won't update globally but will give the update command; taze can update with one click using taze -g --install |
| Ignore Cache | --force / -f |
--cacheClear |
taze caches by default, needs --force to bypass and force fetch new data; ncu needs --cache to actively enable caching. Unlike taze --force which bypasses without clearing, ncu --cacheClear clears the cache |
| Interactive Mode | ‑I / --interactive |
‑i / --interactive |
Interactive mode, letting you decide which upgraded packages' latest versions to write to package.json, i.e., default write mode; note taze uses uppercase I |
| Ignore Packages | --exclude pkg1,pkg2 |
‑x / --reject pkg1,pkg2 |
Exclude packages from upgrade; taze supports pkg@versionRange for fine-grained exclusion of specific version ranges, e.g., taze --exclude typescript@7 only blocks v7, allowing v6 minor upgrades |
| Specify Packages | --include pkg1,pkg2 |
‑f / --filter pkg1,pkg2 |
Only update specific packages; both support regex |
| Ignore Paths | --ignore-paths <paths> |
None | |
| Group Display | --group |
--format group |
More intuitive and aesthetically pleasing; default for taze, requires parameter for ncu |
| Timeout Setting | --request-timeout 120000 |
--timeout 120000 |
Set request timeout (ms), default is 30 seconds |
| Update Peer | --peer |
--dep peer |
ncu allows finer control over which dependency types to update via --dep; taze only has the --peer switch |
| Execute Install | --install |
--install <always|never|prompt> |
Whether to execute install after updating versions; ncu offers finer control |
| Concurrency Control | --concurrency N |
--concurrency N |
|
| Log Control | --loglevel <level> |
-l, --loglevel <level> |
Set log output level to control the amount of terminal output information |
| Output JSON | --json |
--jsonUpgraded |
Output upgrade content in JSON format (machine-readable); requires some waiting time after command execution |
Some other differences:
- Regarding updating to target version ranges,
tazeandncuhave completely different designs; taze uses a subcommand mode. ncucan control exit codes via--errorLevel N, whiletazehas no equivalent capability, makingncumore suitable for CI scenarios.ncuprovides a-r, --registryparameter;tazedoes not, but you can use.npmrc.
Configuring taze
Step 1, install as a dev dependency:
pnpm -w add -D taze
Step 2, add a command to scripts in package.json:
{
...
"scripts": {
"taze": "taze -r"
}
}
Step 3, add a configuration file taze.config.ts (or .tazerc, .tazerc.json, taze.config.{js,mjs,cjs}) in the root directory:
import {
defineConfig
} from 'taze';
// https://github.com/antfu-collective/taze?tab=readme-ov-file#config-file
export default defineConfig({
mode: 'major', // Update version range
write: true, // Write to package.json
exclude: [ // Packages to ignore; recommend adding comments
'typescript', // Don't upgrade to 7 yet, @typescript-eslint doesn't support it, causing Eslint errors
'eslint', // TODO up 10
'@babel/*' // Don't upgrade to 8 yet, indirect dependency @babel/runtime causes build failures
]
});
Configuring ncu
Step 1, install as a dev dependency:
pnpm -w add -D npm-check-updates
Step 2, add a command to scripts in package.json:
{
...
"scripts": {
"ncu": "ncu -w"
}
}
Step 3, add a configuration file .ncurc.yaml (or .ncurc.yml, .ncurc, .ncurc.json, .ncurc.{js,mjs,cjs}) in the root directory:
# Write to package.json
upgrade: true
# Update dependencies and devDependencies (ignore peerDependencies, etc.)
dep: dev,prod
# Group display (more aesthetically pleasing)
format: group
# Timeout setting
timeout: 120000
# Don't prompt for install to save time
install: never
# Packages to temporarily not upgrade (can add comments explaining why)
reject:
- 'typescript', # Don't upgrade to 7 yet, @typescript-eslint doesn't support it, causing Eslint errors
- 'eslint' # TODO up 10
- '@babel/*' # Don't upgrade to 8 yet, indirect dependency @babel/runtime causes build failures
Summary
Both taze and ncu are very powerful and easy to use for updating dependencies. I have personally fully switched to taze, but I still keep both installed globally.
After each upgrade, carefully review the output, paying special attention to major version updates. If a major version is incompatible, it's recommended to first add the corresponding package to the ignore list.
Keeping Dependencies Clean - knip / depcheck
Another important but easily overlooked issue with project dependencies is dependency chaos:
- Version number chaos: the same package having different versions across different Workspace Packages, easily leading to larger build sizes and even runtime issues.
- Redundant dependencies, wasting bandwidth.
- Missing dependencies, which are hard to detect promptly in the presence of phantom dependencies, but are a hidden time bomb.
The problem of version number chaos can be solved with taze and ncu. The remaining issues of "too many or too few" can be addressed with the following two tools:
- knip Not only checks dependencies but also finds unreferenced files,
exports, etc. (Among the small tools recommended in this article, it's the only one with its own website, which shows how powerful it is.) - depcheck Checks for redundant and missing dependencies. Went dormant in 2025/07 and recommended
knip.
Global Installation
Not required, but recommended.
Install globally to get the global commands knip and depcheck:
npm install -g knip
npm install -g depcheck
A Quick Trial
Use both tools for a similar task: checking the entire Workspace (including the root directory).
You can see knip wins outright—simple, efficient, and with more features—while depcheck needs to leverage pnpm's capabilities (and requires the --no-bail parameter to prevent premature exit).
Common Parameters
depcheck's functionality is relatively simple with fewer parameters. Below is a comparison of commonly used ones:
| Function | knip | depcheck | Notes |
|---|---|---|---|
| Specify Directory | Pass path directly knip ./packages/a |
depcheck ./packages/a |
Both support directly passing the project directory path as the first positional argument |
| Recursive Scan | Automatically recognizes Workspaces | ❌ Not supported, requires Shell loop or pnpm -r |
|
| Output JSON | --reporter json |
--json |
knip supports multiple output formats; depcheck only has simple JSON |
| Ignore Packages | Configure ignoreDependencies (no direct CLI flag, config file recommended) |
--ignores="pkg1,@scope/*" |
|
| Ignore Paths | Configure ignore |
--ignore-patterns=dist,covera |
|
| Ignore Bin Packages | Configure ignoreBinaries |
--ignore-bin-package knip automatically ignores common ones like cross-env, rimraf, while depcheck can only configure in ignores (this config item seems ineffective) Can only configure in |
|
Read .xxignore |
No CLI parameter, set in config | --ignore-path=.gitignore |
|
| Skip Dependencies | --include dependencies to see only unused deps |
--skip-missing=trueGitHub |
|
| Only Check Certain Issues | --include dependencies/files/exports |
None | |
| Write Mode | --fix |
❌ Not supported | Unique to knip, removes unused dependencies from package.json; not recommended for use |
Configuring knip
knip provides a command to initialize in a project: pnpm create @knip/config, but the generated config file is knip.json. Since the configuration is relatively simple, you can easily do it manually.
Step 1, install as a dev dependency:
pnpm -w add -D knip
Step 2, add a command to scripts in package.json:
{
...
"scripts": {
"knip": "knip"
}
}
It's necessary to explain the --strict parameter, which is a command-line-only parameter with no place in the configuration file.
Sometimes using --strict can align with pnpm's strict phantom dependency detection, but the implicit meaning of --strict is to only scan production code. What does that mean? It means config files, unit tests, Storybook, etc., will not be checked.
Because of this, it's not recommended to add --strict to your NPM scripts. Instead, you can occasionally run it with this parameter to check for phantom dependencies.
Step 3, add a configuration file knip.config.ts (or .knip.{json,jsonc}, knip.{json,jsonc}, knip.{ts,js}, knip.config.{ts,js}, or add a knip field in package.json) in the root directory:
import {
KnipConfig
} from 'knip';
export default {
workspaces: {
'.': {
entry: [ // This is a Taro mini-program, so it has multiple entries
'src/app.tsx',
'src/app.config.ts',
'src/{pages,subpackages}/**/index.{ts,tsx,config.ts}'
],
project: [
'src/**/*.{ts,tsx,less,css}'
],
paths: {
'@/*': ['./src/*']
}
}
},
tags: ['-lintignore'],
ignoreDependencies: [/^@tarojs\//]
} satisfies KnipConfig;
For those wishing to use knip in VSCode or WebStorm, the official team also provides plugins, and even an MCP Server for AI, etc. See Knip - Integrations for details.
Configuring depcheck
Step 1, install as a dev dependency:
pnpm -w add -D depcheck
Step 2, add a command to scripts in package.json:
{
...
"scripts": {
"depcheck": "pnpm -r --include-workspace-root --no-bail exec depcheck"
}
}
Step 3, add a configuration file .depcheck.yaml (or .depcheck.yml, .depcheck.json) in the root directory:
ignorePatterns:
- dist
- __*
ignores:
- '@/*'
- '@babel/*'
- '@commitlint/*'
- '@kcuf/*config'
- '@tarojs/*'
- '@vitest/coverage-v8'
- eslint-import-resolver-custom-alias
- cross-env
- lint-staged
- rimraf
- markdownlint-cli2
- npm-package-json-lint
- typescript
- autoprefixer
- depcheck
- husky
- lerna
- less
- postcss
- react-refresh
- stylelint
You can see that to avoid interference, many ignore items need to be written. depcheck only scans import/require in TS/JS code by default and cannot recognize implicit dependencies in package.json#scripts and other config files, thus generating many false positives. Although it also provides --ignore-bin-package and --specials parameters, the latter seems ineffective, meaning you still need to write a long list of ignores.
Summary
Whether in terms of powerful functionality or configuration simplicity, knip surpasses depcheck by several levels.
Although I recommend enabling the configuration items that update package.json (write: true for taze, update: true for ncu), be sure to carefully review which packages have been upgraded before committing changes to Git, and pay close attention to major versions. If you find issues running locally, roll back the version changes promptly.
If you find a problem with a minor version upgrade of a package, you can set an override or package.json#resolutions (pnpm <11) to temporarily bypass it.
🪢 Version Chaos - syncpack
While writing this article, I learned about syncpack from knip's documentation, a CLI tool that can sort out and resolve dependency version chaos. It's also a tool worthy of its own website.
syncpack's functionality overlaps somewhat with knip; it can also check and upgrade updates syncpack update, but its core capability is checking whether dependencies are consistent across large Monorepo projects, which is exactly where its name comes from.
Global Installation
Not required, but recommended.
Install globally to get the global command syncpack:
npm install -g syncpack
A Quick Trial
Unlike the other small tools introduced above, syncpack primarily operates through subcommands:
| Command | Function | Notes |
|---|---|---|
list |
List all dependencies and their occurrence count | |
lint |
List version inconsistency issues | |
fix |
Fix issues found by lint |
|
format |
Modify the format of package.json |
It modifies field order, not version numbers. I usually leave field order management to npmpackagejsonlint |
update |
Update dependency version numbers | Leave this to taze |
Below are the effects of syncpack list and syncpack lint:
Configuring syncpack
Since I just got acquainted with
syncpack, and it has some functional overlap withtaze, I haven't added it to my NPM scripts yet. But you can still try installing it as a project dependency.
Step 1, install as a dev dependency:
pnpm -w add -D syncpack
Step 2, add a command to scripts in package.json:
{
...
"scripts": {
"syncpack": "syncpack lint"
}
}
Step 3, add a configuration file syncpack.config.ts (or .syncpackrc, .syncpackrc.config.{json,yaml,yml,js,ts,mjs,cjs}, syncpack.config.{js,mjs,cjs}, or add a syncpack or config.syncpack field in package.json) in the root directory:
export default {
indent: " ",
} satisfies import("syncpack").RcFile;
Summary
syncpack can help you check how many total dependencies your project has, how many times each dependency is declared, and which dependency declarations might be inconsistent or problematic, thus making your project dependencies cleaner. It also has the ability to update dependency versions (similar to taze).
🧮 Project Volume - tokei / cloc
I quite like counting code:
- When a new code solution replaces an old, poor implementation, I want to see if the new solution reduces the amount of code.
- After finishing a new feature, I estimate the amount of new code added.
- When the boss asks about the project's volume.
- When I want to check for massive files in the project (a maintainability killer).
Available tools:
- tokei A code line counting tool written in Rust. Automatically ignores non-project code, faster and smarter.
- cloc A veteran tool, used for many years.
I used cloc for a long time, but cloc isn't smart enough. Firstly, it must have a path parameter; entering cloc alone yields no result. And cloc . will include the huge node_modules, which is time-consuming and not what we want. I needed a smarter, faster tool, which is tokei. You just need to execute tokei in the project directory to get the answer, with no waiting and no need for complex parameters.
Besides being written in Rust, tokei is fast because it actively ignores node_modules and the contents of Ignore files like .gitignore, so build logic, etc., are also ignored.
Another important reason, actually the primary reason, I switched to tokei is that cloc distinguishes between JS and JSX but not between TS and TSX, whereas tokei does.
Installation
cloc is written in Perl, while tokei is in Rust. Both can be installed via brew:
brew install tokei
brew install cloc
A Quick Trial
Let's check all source code parts of a project, including engineering configs and documentation, but excluding node_modules and build output directories:
tokei
cloc . --exclude-dir=node_modules,dist
It can be seen that tokei slightly outperforms the veteran cloc in terms of performance, ease of use, and aesthetics. There are slight differences in the data between the two. Also, it's best to exclude pnpm-lock.yaml, as this file can have tens of thousands of lines.
Common Parameters
Full disclosure: the table below was compiled by AI (but has been reviewed).
| Function Description | tokei | cloc | Notes |
|---|---|---|---|
| Directory / File | tokei src |
cloc src |
tokei defaults to the current directory, can be omitted; but cloc must have a path |
| Exclude | -e, --exclude <glob> |
--exclude-dir=dir1,dir2--exclude-ext=js,min.js |
tokei supports glob; cloc distinguishes directories and extensions, does not support glob |
| Detail | -f, --files |
--by-file |
Output statistics for each file |
| Specify Language | -t, --type TypeScript,TSX |
--include-lang=TypeScript |
tokei language names are case-sensitive |
| Exclude Specified Language | No native parameter | --exclude-lang=Markdown |
tokei can filter by extension using --exclude |
| Output Format | -o,--output json/yaml/cbor |
--json / --yaml / --xml / --csv / --sql |
cloc has more output formats, including SQL, XML |
| Output File | Redirect > out.json |
--report-file=out.txt |
cloc natively supports an output file parameter |
| Sort Output | -s,--sort code/files/lines/comments/blanks |
--sort=code |
tokei can sort by blank lines, comments; cloc only supports files, code, comment |
Read .gitignore |
Reads by default, can use --no-ignore to turn off default behavior |
Does not manage | tokei respects .gitignore etc., won't touch files and directories not considered source code, hence fast; cloc needs manual exclusion of node_modules, dist, etc. |
| Count Hidden | --hidden |
--include-hidden |
Both skip hidden files by default |
| Non-recursive | None, always recursive | --no-recurse |
tokei cannot disable recursive subdirectory scanning, can only -e exclude subdirectories |
| View Supported Language List | -l, --languages |
--show-ext |
Print all recognized language and extension mappings |
| Diff Comparison | Does not support diff | --diff dirA dirB |
cloc's killer feature, compares line count changes between two codebases, supports git commit |
| Process Archives | Not supported | Natively supports zip/tar.gz etc. archives | cloc can directly count code inside archives; tokei needs them extracted first |
Summary
Neither of these two tools needs to be installed or configured within the project (as they are not npm packages). Although there are npm packages with the same names, don't confuse them. Although I am a long-time user of cloc, tokei is smarter, simpler, and faster, making it hard not to "switch affections."
🏓 Summary of Tips
taze
taze -r # Recursive scan, including root and all Workspace packages
taze --force # Bypass local npm meta cache (not force upgrade version)
taze --maturity‑period‑exclude # Ignore package publish cooldown period, directly check latest version
ncu
ncu --registry https://registry.npmmirror.com # Specify Registry to speed up
ncu -w # Scan entire Workspace, including root and all Workspace Packages
ncu -u # Scan current directory and update
ncu -wu # Scan entire Workspace and update
ncu -ui # Interactive update
ncu -u --prod # Only upgrade production dependencies (dependencies)
ncu -u --dev # Only upgrade development dependencies (devDependencies)
ncu -u vite,vitest # Only update specified packages
knip
knip --include unlisted # Phantom dependencies
knip --include dependencies # Unused dependencies
knip --include files # Unused files
knip --include exports # Unused exports
tokei
tokei -f -s lines # Project file size details, sorted by lines of code to find massive files
tokei -t TypeScript,TSX -f -s lines # More precise, find massive TS or TSX files
cloc
cloc src packages-*/*/src # Count code in all src directories
cloc src --by-file # Project file size details, automatically sorted by lines of code
🙋 FAQ
❓ Why didn't taze find a package I just published?
It might be cached, use taze --force.
❓ How to ignore a specific Workspace when using depcheck with pnpm -r?
Use pnpm --filter for reverse filtering:
pnpm -r --no-bail --filter '!documentation' exec depcheck
❓ How to upgrade to pre-release versions like alpha, beta, rc?
Use taze newest.
ncu provides a --target parameter, with values latest, newest, greatest, minor, patch, semver, @[tag], and also a --pre parameter specifically for upgrading to pre-release versions.
📌 Links
🪭 Final Words
Don't let project dependencies become a black box. As a qualified developer, 0999
If ESLint and Prettier are code-level cleanliness obsessions, then CLI tools like taze, ncu, knip, depcheck, tokei are the engineering-level cleanliness cures.
They don't format your code or constrain syntax styles, but they can help you clean up project redundancy, update dependency versions, and count code structure, transforming the entire project from "just runnable" to "clean, transparent, healthy, and maintainable."