DEV Community

Wilson Xu
Wilson Xu

Posted on

I Built 30 Developer CLI Tools in a Week — Here's What I Learned

I Built 30 Developer CLI Tools in a Week — Here's What I Learned

Last week, I set myself an absurd challenge: build and publish 30 command-line tools to npm in seven days. Not toy scripts. Real, useful developer tools — the kind you'd actually install globally and reach for every day.

I made it. Thirty packages. All written in TypeScript, all published to npm, all solving real problems I'd personally run into. Along the way I broke my terminal, mass-published half-broken packages at 2 AM, rewrote the same commander.js boilerplate so many times I could type it with my eyes closed, and learned more about the CLI ecosystem in one week than I had in the previous year.

This is the story of what I built, what went wrong, and what patterns emerged that might save you a lot of time if you ever decide to build your own CLI tools.


Why 30 Tools?

I didn't start with the number 30. I started with a single annoyance.

I wanted to capture a webpage as clean text from my terminal — no browser, no copy-paste, just pipe a URL and get readable content. I looked for an existing tool. Nothing quite worked the way I wanted. So I built one. That took about three hours. Publishing it to npm took another thirty minutes. And then I thought: what else am I annoyed by?

Turns out, a lot. I kept a running list of friction points in my daily workflow — things that required switching context, opening a browser, or writing throwaway scripts. Each one became a tool. After the first five shipped on day one, I realized I could systematize the process and just keep going.

The constraint of "ship it today" forced me to make decisions fast. No framework analysis paralysis. No bike-shedding over config formats. Build the thing, write help text, publish, move on. Some tools took ninety minutes. Some took six hours. The average was about three.

The Categories That Emerged

Looking back at all 30 tools, they fell naturally into five buckets:

Git & Repository Tools — Anything that surfaces information about your codebase. Activity dashboards, changelog generators, README scorers, repo cards.

API & Network Tools — HTTP clients, port scanners, URL metadata extractors. The things you reach for when you're debugging a service or poking at an endpoint.

Environment & Config Management — Syncing .env files, scanning for missing variables, generating .gitignore files, managing licenses.

Code Quality & Analysis — Dependency auditors, JSON differ, package.json linters, AST-based code review helpers.

Monitoring & DevOps — Performance watchers, log streamers, npm download trackers, uptime monitors.

Here's a closer look at the ten I'm most proud of.


1. websnap-reader — Capture Webpages from Your Terminal

This was the tool that started everything. Give it a URL, get back clean, readable text — no JavaScript rendering noise, no cookie banners, no sidebar ads. Just the content.

npx websnap-reader https://example.com/blog/some-article
Enter fullscreen mode Exit fullscreen mode

Under the hood, it fetches the page, runs it through Mozilla's Readability algorithm (the same one Firefox uses for Reader View), and outputs Markdown to stdout. You can pipe it into other tools, save it to a file, or just read it right there in the terminal.

import { Readability } from "@mozilla/readability";
import { JSDOM } from "jsdom";

async function extractContent(url: string): Promise<string> {
  const response = await fetch(url);
  const html = await response.text();
  const dom = new JSDOM(html, { url });
  const reader = new Readability(dom.window.document);
  const article = reader.parse();

  if (!article) {
    throw new Error("Could not extract readable content");
  }

  return convertToMarkdown(article.content, article.title);
}
Enter fullscreen mode Exit fullscreen mode

The key insight: by converting to Markdown instead of plain text, the output stays useful for downstream processing. You can pipe it into an LLM, append it to notes, or diff two versions of the same page.


2. @chengyixu/gitpulse — Git Activity Dashboard

I wanted a quick way to see "what happened in this repo recently" without opening GitHub or scrolling through git log. GitPulse gives you a visual dashboard right in the terminal: commit frequency heatmap, top contributors, file churn analysis, and branch activity.

npx @chengyixu/gitpulse --days 30
Enter fullscreen mode Exit fullscreen mode
📊 Repository Activity — last 30 days
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Commits: 147    Contributors: 5    Files changed: 312

Weekly Heatmap:
Mon ██████████████░░░░░░  68%
Tue ████████████████████  100%
Wed ███████████░░░░░░░░░  55%
Thu █████████████████░░░  85%
Fri ██████████████░░░░░░  70%
Sat ████░░░░░░░░░░░░░░░░  20%
Sun ██░░░░░░░░░░░░░░░░░░  10%

Most Changed Files:
  src/index.ts          (+340, -128)  24 commits
  src/utils/parser.ts   (+210, -89)   18 commits
  package.json          (+45, -12)    31 commits
Enter fullscreen mode Exit fullscreen mode

The interesting challenge here was performance. Running git log with custom formatting and then parsing the output in Node.js is surprisingly fast — faster than using a git library like isomorphic-git. For read-only operations on local repos, shelling out to the native git binary wins every time.

function getCommitData(days: number): CommitData[] {
  const since = new Date();
  since.setDate(since.getDate() - days);

  const result = execSync(
    `git log --since="${since.toISOString()}" --format="%H|%an|%ae|%aI|%s" --numstat`,
    { encoding: "utf-8" }
  );

  return parseGitLogOutput(result);
}
Enter fullscreen mode Exit fullscreen mode

3. @chengyixu/depcheck-ai — AI-Powered Dependency Auditing

This one goes beyond npm audit. It doesn't just check for known vulnerabilities — it analyzes your dependency tree for abandoned packages, license conflicts, unnecessary bloat, and potential supply-chain risks.

npx @chengyixu/depcheck-ai --scan
Enter fullscreen mode Exit fullscreen mode

It flags packages that haven't been updated in over a year, identifies dependencies you're importing but only using one function from (suggesting you inline it), and cross-references licenses to catch GPL contamination in MIT projects.

interface DependencyReport {
  name: string;
  version: string;
  lastPublished: Date;
  weeklyDownloads: number;
  licenseCompatible: boolean;
  usedExports: string[];
  totalExports: number;
  riskScore: number; // 0-100
  recommendation: "keep" | "replace" | "inline" | "remove";
}
Enter fullscreen mode Exit fullscreen mode

The "AI" part comes from using heuristics that mimic what a senior developer would look for during a dependency review — not from calling an LLM API. I debated adding an actual AI integration, but decided that fast, deterministic analysis was more valuable for a CLI tool than waiting for an API call.


4. dev-portscan — Find What's Running on Your Ports

Every developer has had the "port 3000 is already in use" moment. dev-portscan shows you exactly what's running on your common development ports, with process names, PIDs, and the option to kill them.

npx dev-portscan
Enter fullscreen mode Exit fullscreen mode
PORT    PID     PROCESS         STATUS
3000    12847   node            ● LISTENING
3001    12848   node            ● LISTENING
5173    —       —               ○ FREE
5432    1203    postgres        ● LISTENING
6379    1198    redis-server    ● LISTENING
8080    —       —               ○ FREE
8443    15234   node            ● LISTENING
Enter fullscreen mode Exit fullscreen mode

The implementation differs significantly between macOS and Linux. On macOS, you use lsof. On Linux, you read from /proc/net/tcp. I ended up writing platform-specific modules and switching at runtime:

const scanner =
  process.platform === "darwin"
    ? new DarwinPortScanner()
    : new LinuxPortScanner();

const results = await scanner.scan(commonDevPorts);
Enter fullscreen mode Exit fullscreen mode

This was the tool that taught me the most about cross-platform CLI development. What works perfectly on your MacBook might explode on Ubuntu, and you won't know until someone files an issue.


5. jsondiff-cli — Structural JSON Comparison

diff works great for text. It's terrible for JSON. Reordered keys, different formatting, nested arrays — diff treats all of these as changes even when the data is semantically identical.

npx jsondiff-cli config.old.json config.new.json
Enter fullscreen mode Exit fullscreen mode

jsondiff-cli compares JSON structurally. It understands that {"a":1,"b":2} and {"b":2,"a":1} are the same object. It can diff deeply nested structures and show you exactly which values changed, which keys were added, and which were removed — with a clean, colorized output.

function deepDiff(a: unknown, b: unknown, path = ""): Difference[] {
  if (typeof a !== typeof b) {
    return [{ path, type: "type_change", from: a, to: b }];
  }

  if (Array.isArray(a) && Array.isArray(b)) {
    return diffArrays(a, b, path);
  }

  if (isObject(a) && isObject(b)) {
    const allKeys = new Set([...Object.keys(a), ...Object.keys(b)]);
    return [...allKeys].flatMap((key) =>
      deepDiff(a[key], b[key], `${path}.${key}`)
    );
  }

  if (a !== b) {
    return [{ path, type: "value_change", from: a, to: b }];
  }

  return [];
}
Enter fullscreen mode Exit fullscreen mode

6. @chengyixu/env-sync — Sync .env Files Across Projects

If you work on multiple services that share environment variables, you know the pain of keeping .env files in sync. Change the database URL in one project, forget to update it in three others.

npx @chengyixu/env-sync --source ./api/.env --targets ./web/.env ./worker/.env
Enter fullscreen mode Exit fullscreen mode

env-sync compares .env files, shows you what's different, and lets you selectively sync variables across projects. It understands comments, preserves formatting, and never overwrites without confirmation.

The hardest part wasn't the syncing logic — it was parsing .env files correctly. There are at least four different ways people write them: with quotes, without quotes, with export prefixes, with inline comments. I ended up writing a parser that handles all the variants:

function parseEnvLine(line: string): EnvEntry | null {
  const trimmed = line.trim();
  if (!trimmed || trimmed.startsWith("#")) {
    return { type: "comment", raw: line };
  }

  const match = trimmed.match(
    /^(?:export\s+)?([A-Za-z_]\w*)=(?:"([^"]*?)"|'([^']*?)'|(.*))/
  );

  if (!match) return null;

  const key = match[1];
  const value = match[2] ?? match[3] ?? match[4]?.split("#")[0].trim() ?? "";

  return { type: "variable", key, value, raw: line };
}
Enter fullscreen mode Exit fullscreen mode

7. httpc-cli — Terminal HTTP Client

Yes, curl exists. Yes, httpie exists. I built httpc-cli anyway because I wanted something that hit the sweet spot between curl's power and httpie's readability, with built-in response formatting and request history.

npx httpc-cli GET https://api.github.com/users/chengyixu
Enter fullscreen mode Exit fullscreen mode
HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Remaining: 58

{
  "login": "chengyixu",
  "name": "Wilson Xu",
  "public_repos": 42,
  "followers": 128
}

 234ms  📦 1.2KB
Enter fullscreen mode Exit fullscreen mode

The killer feature is request chaining — you can reference values from previous responses in subsequent requests:

httpc-cli POST /api/login --body '{"user":"admin"}' --save token
httpc-cli GET /api/data --header "Auth: Bearer {{token.body.jwt}}"
Enter fullscreen mode Exit fullscreen mode

8. changelog-gen-cli — Auto-Generate Changelogs

This tool reads your git history and produces a structured changelog grouped by type (features, fixes, refactors) based on conventional commit messages.

npx changelog-gen-cli --from v1.0.0 --to v1.1.0
Enter fullscreen mode Exit fullscreen mode
## [1.1.0] - 2026-03-18

### Features
- Add dark mode support (#142)
- Implement WebSocket reconnection (#138)

### Bug Fixes
- Fix memory leak in event handler (#145)
- Correct timezone offset calculation (#140)

### Performance
- Optimize image lazy loading (#143)
Enter fullscreen mode Exit fullscreen mode

If your commits don't follow conventional commit format, it uses keyword detection as a fallback — commits starting with "fix", "add", "update", "remove" get categorized automatically. Not perfect, but surprisingly effective for 90% of real-world repos.


9. readme-score-cli — Score Your README Quality

Inspired by the idea that a good README is the most important documentation a project can have, this tool analyzes your README.md and gives it a score out of 100 based on completeness criteria.

npx readme-score-cli
Enter fullscreen mode Exit fullscreen mode
README Score: 72/100

✅ Title and description     (+10)
✅ Installation instructions  (+15)
✅ Usage examples             (+15)
✅ License section            (+5)
⚠️  No API documentation      (-10)
❌ No contributing guide      (-8)
❌ No badges                  (-5)
❌ No screenshots/demo        (-10)
💡 Tip: Add a GIF demo to boost engagement
Enter fullscreen mode Exit fullscreen mode

It checks for the presence and quality of sections that research shows correlate with project adoption: clear installation steps, runnable code examples, visual demos, contribution guidelines, and badges showing build status or coverage.


10. repocard-cli — Beautiful Repo Cards in Terminal

The last tool on my highlight list generates a visual "card" for any GitHub repository, right in the terminal. Think of it as the social preview card you see when sharing a GitHub link — but rendered with Unicode box-drawing characters and colors.

npx repocard-cli facebook/react
Enter fullscreen mode Exit fullscreen mode
╭──────────────────────────────────────────────╮
│  ⭐ facebook/react                           │
│  The library for web and native user         │
│  interfaces.                                 │
│                                              │
│  ★ 228k    🍴 46.5k    📦 TypeScript         │
│  📋 MIT    🔄 Updated 2 hours ago            │
│                                              │
│  Topics: javascript, frontend, ui, react     │
╰──────────────────────────────────────────────╯
Enter fullscreen mode Exit fullscreen mode

This was a lesson in terminal rendering. Getting box-drawing characters to align correctly when mixed with emoji (which are double-width in most terminals) required writing a string-width calculation function that accounts for Unicode character widths:

import stringWidth from "string-width";

function padToWidth(str: string, targetWidth: number): string {
  const currentWidth = stringWidth(str);
  const padding = Math.max(0, targetWidth - currentWidth);
  return str + " ".repeat(padding);
}
Enter fullscreen mode Exit fullscreen mode

Without string-width, the boxes would be jagged and misaligned in every terminal that handles emoji differently (which is all of them).


The Tech Stack

Every tool used the same foundation:

  • TypeScript — Non-negotiable. The type safety catches so many bugs during development that it pays for the compilation step ten times over.
  • commander.js — For argument parsing. I tried yargs and meow too, but Commander hits the best balance of features and simplicity for CLI tools.
  • chalk — Terminal colors. Yes, there are newer alternatives. Chalk just works.
  • ora — Spinner animations for long-running operations. Users need to know something is happening.
  • tsup — For bundling TypeScript into a single executable JS file. Way faster than tsc alone.

The project structure I converged on after the first few tools:

my-cli-tool/
├── src/
│   ├── index.ts          # CLI entry point (commander setup)
│   ├── commands/          # One file per subcommand
│   └── utils/             # Shared helpers
├── package.json
├── tsconfig.json
└── README.md
Enter fullscreen mode Exit fullscreen mode

In package.json, the critical fields:

{
  "name": "my-cli-tool",
  "bin": {
    "my-cli-tool": "./dist/index.js"
  },
  "files": ["dist"],
  "type": "module",
  "scripts": {
    "build": "tsup src/index.ts --format esm --dts",
    "prepublishOnly": "npm run build"
  }
}
Enter fullscreen mode Exit fullscreen mode

The bin field is what makes npx my-cli-tool work. The files field ensures you only publish the built output, not your source. And prepublishOnly guarantees the build runs before every publish — I forgot this on my second tool and published an empty package. Don't be me.


Lessons Learned

1. Ship the simplest version first. My best tools started as 50-line scripts. The temptation to add features before publishing is strong. Resist it. A tool that exists and does one thing is infinitely more useful than a tool that does ten things and lives on your hard drive.

2. process.exit() is your friend — and your enemy. Call it too early and you'll cut off output streams. Call it too late (or never) and your CLI will hang. The pattern I settled on: wrap your main function in a try/catch, print the error, then process.exit(1). Never let an unhandled rejection silently kill your tool.

async function main() {
  try {
    const program = createCLI();
    await program.parseAsync(process.argv);
  } catch (error) {
    console.error(chalk.red(`Error: ${error.message}`));
    process.exit(1);
  }
}

main();
Enter fullscreen mode Exit fullscreen mode

3. Test with npx before you publish. Run npm link in your project, then try using your tool from a different directory. The number of path-resolution bugs this catches is humbling.

4. Write help text like a human. The default --help output from Commander is fine, but adding examples makes the difference between a tool people use and a tool people uninstall. Add .addHelpText('after', ...) with real examples.

5. Respect the terminal. Not everyone uses a modern terminal with 256-color support. Always provide a --no-color flag. Use chalk.level detection. Don't assume the terminal is wider than 80 columns.

6. Stdin is a superpower. If your tool processes data, accept it from stdin too. This single decision makes your tool composable with every other Unix tool. cat data.json | jsondiff-cli - other.json is more powerful than any flag you could add.

7. npm publishing is deceptively tricky. Scoped packages (@username/tool) are private by default — you need --access public on first publish. Package names must be globally unique. The .npmignore and files field in package.json interact in surprising ways. I published node_modules once. Once.


How to Get Started Building Your Own CLI Tools

If this inspired you to build something, here's the fastest path:

mkdir my-tool && cd my-tool
npm init -y
npm install typescript commander chalk ora
npm install -D tsup @types/node
npx tsc --init
Enter fullscreen mode Exit fullscreen mode

Set up your src/index.ts:

#!/usr/bin/env node
import { Command } from "commander";
import chalk from "chalk";

const program = new Command();

program
  .name("my-tool")
  .description("What it does, in one sentence")
  .version("1.0.0")
  .argument("<input>", "what the user provides")
  .action(async (input) => {
    console.log(chalk.green(`Processing: ${input}`));
    // Your logic here
  });

program.parse();
Enter fullscreen mode Exit fullscreen mode

Build and test:

npx tsup src/index.ts --format esm
node dist/index.js test-input
Enter fullscreen mode Exit fullscreen mode

Publish:

npm login
npm publish --access public
Enter fullscreen mode Exit fullscreen mode

That's it. You can go from idea to published npm package in under an hour. The ecosystem is mature, the tooling is solid, and the feedback loop of building something you personally need is incredibly motivating.


The Numbers

After one week:

  • 30 packages published to npm
  • ~15,000 lines of TypeScript
  • 5 categories of tools
  • 1 broken publish (the node_modules incident)
  • 3 packages that needed immediate hotfixes post-publish
  • 0 regrets

The tools I built for myself ended up being the tools I reach for every day. dev-portscan saves me two minutes every morning. websnap-reader replaced a whole browser workflow. gitpulse is the first thing I run when I come back to a repo I haven't touched in a while.

Building CLI tools is the most underrated form of developer productivity investment. Each one takes a few hours to build and saves minutes every day — forever.

If you're looking for your next side project, open your terminal and ask yourself: what annoys me right now? Then build the tool that fixes it.


All 30 tools are available on npm under @chengyixu. The source code is on GitHub. PRs welcome — especially cross-platform fixes, because I definitely didn't test everything on Windows.

Top comments (0)