The Developer Experience Checklist for CLI Tools
You shipped a CLI tool. It works. But does it feel good to use? Developer experience (DX) is what separates tools people install once and forget from tools they recommend to their team.
This checklist covers everything that makes a CLI tool professional — from first install to daily use. Each item is a concrete, implementable pattern. No vague advice.
Installation Experience
1. Zero-config first run
The tool should do something useful immediately after install, without creating config files or reading documentation:
npm install -g mytool
mytool scan . # Just works
2. Helpful --help output
Structure help like a conversation, not a man page:
mytool - Scan your project for common issues
Usage:
mytool scan [path] Scan a directory for issues
mytool fix [path] Auto-fix what can be fixed
mytool init Create a config file
Options:
-f, --format <fmt> Output format: text, json, table (default: text)
-t, --threshold <n> Fail if score below threshold
--no-color Disable colored output
-v, --verbose Show detailed information
-q, --quiet Only show errors
Examples:
mytool scan Scan current directory
mytool scan src/ --json Scan src/ and output JSON
mytool fix --threshold 80 Fix issues, fail if score < 80
Key details: examples section, grouped options, default values shown inline.
3. Version output follows convention
mytool --version
# mytool/1.2.3 node/20.11.0 darwin-arm64
Include the Node.js version and platform — it makes bug reports useful.
program.version(
`${pkg.name}/${pkg.version} node/${process.version} ${process.platform}-${process.arch}`
);
Output Experience
4. Color that communicates, not decorates
// Good: color conveys meaning
console.log(chalk.green('✓ 42 files passed'));
console.log(chalk.yellow('⚠ 3 warnings'));
console.log(chalk.red('✗ 1 error'));
// Bad: color as decoration
console.log(chalk.blue.bold.underline('=== Results ==='));
console.log(chalk.magenta('Files:') + chalk.cyan(' 42'));
5. Respect NO_COLOR
import chalk from 'chalk';
// chalk v5+ respects NO_COLOR automatically
// But also handle --no-color flag
if (options.noColor || process.env.NO_COLOR) {
chalk.level = 0;
}
6. Progress for anything over 2 seconds
import ora from 'ora';
const spinner = ora('Scanning files...').start();
// ... work ...
spinner.succeed(`Scanned ${count} files in ${elapsed}ms`);
7. Summary at the end
Always end with a clear summary:
✓ 142 files scanned
⚠ 3 warnings
✗ 1 error (see above)
Score: 94/100
Run `mytool fix` to auto-fix 2 of 3 warnings
The last line should tell the user what to do next.
Error Experience
8. Every error suggests a fix
// Bad
throw new Error('ENOENT: no such file');
// Good
throw new CliError('Config file not found: .mytoolrc.json', {
suggestion: 'Run `mytool init` to create one, or specify a path with --config',
});
9. Distinguish user errors from bugs
try {
await runTool(args);
} catch (error) {
if (error instanceof CliError) {
// User error: clean message, no stack trace
console.error(chalk.red(`Error: ${error.message}`));
if (error.suggestion) {
console.error(chalk.yellow(` ${error.suggestion}`));
}
process.exit(error.code);
}
// Bug: show stack trace and ask for report
console.error(chalk.red('Unexpected error — this is a bug'));
console.error(error.stack);
console.error(chalk.gray('\nPlease report: https://github.com/you/tool/issues'));
process.exit(99);
}
Automation Experience
10. --json for everything
Every command that produces output should support --json:
if (options.json) {
console.log(JSON.stringify(results, null, 2));
} else {
printFormattedReport(results);
}
11. Non-zero exit codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Tool found issues |
| 2 | Invalid input/config |
| 3 | Network error |
| 99 | Unexpected error (bug) |
12. Stderr for diagnostics, stdout for data
process.stderr.write('Scanning...\n'); // Progress
process.stdout.write(JSON.stringify(results)); // Data
13. Config file support
// .mytoolrc.json
{
"threshold": 85,
"ignore": ["vendor/", "dist/"],
"format": "table"
}
14. Stdin support
cat files.txt | mytool scan --stdin
docker logs app | mytool filter --level error
Performance Experience
15. Fast startup
Users notice if your tool takes over 500ms just to show --help. Minimize top-level imports:
// Bad: imports everything on startup
import lighthouse from 'lighthouse';
import * as chromeLauncher from 'chrome-launcher';
// Good: lazy import only when needed
program.command('audit').action(async () => {
const { default: lighthouse } = await import('lighthouse');
const chromeLauncher = await import('chrome-launcher');
// ...
});
16. Show timing
const start = performance.now();
const results = await runScan(path);
const elapsed = Math.round(performance.now() - start);
console.log(chalk.gray(` Completed in ${elapsed}ms`));
Update Experience
17. Update notifications
import updateNotifier from 'update-notifier';
const notifier = updateNotifier({ pkg });
notifier.notify(); // Shows message if update available
This prints a non-intrusive notice: "Update available: 1.2.3 → 1.3.0. Run npm i -g mytool to update."
The Complete Checklist
- [ ] Works without config on first run
- [ ]
--helphas examples section - [ ]
--versionshows platform info - [ ] Colors convey meaning (green=good, red=bad, yellow=warning)
- [ ] Respects
NO_COLORenvironment variable - [ ] Progress spinner for operations > 2s
- [ ] Summary with "what to do next" at the end
- [ ] Errors include fix suggestions
- [ ] User errors vs bugs have different output
- [ ]
--jsonflag for machine-readable output - [ ] Non-zero exit codes for failures
- [ ] Stderr for diagnostics, stdout for data
- [ ] Config file support (
.toolrc.json) - [ ] Stdin piping support
- [ ] Fast startup (< 500ms for
--help) - [ ] Timing shown for operations
- [ ] Update notifications
Score yourself: 13+ is excellent, 10-12 is good, under 10 means your users are probably frustrated.
Wilson Xu has published 10+ npm CLI tools, each designed with these DX principles. Find them at npm and follow at dev.to/chengyixu.
Top comments (0)