← back to stream

A CLI template for publishing npm packages

I publish enough small npm packages that the setup tax became the bottleneck. Every new package meant the same hour of plumbing: TypeScript config, ESLint, Prettier, Jest, Husky hooks, commitlint, semantic-release, a CI file. By the third package I was copy-pasting from the previous one and introducing drift — a missing script here, an outdated config there. So I packaged my baseline into a CLI: @sargonpiraev/create-npm-package.

What it does

npx @sargonpiraev/create-npm-package my-pkg
cd my-pkg && npm install

You get a directory that's ready to publish today — not "ready after you add testing and CI", actually ready. The scaffold contains:

  • TypeScript with ESM output and a sensible tsconfig.json
  • ESLint + Prettier wired together, no bikeshed about formatting
  • Jest with coverage and an XML reporter for CI
  • Husky pre-commit hook running lint + format
  • commitlint enforcing conventional commits
  • semantic-release with a .releaserc.json that actually works on CI
  • GitLab CI pipeline — build, test, release — ready to push

The CLI itself is small on purpose: read a project name from argv, copy the template directory, rewrite package.json with the new name, done. No interactive prompts, no "which framework", no template variants. One opinionated shape.

The point isn't the tools, it's the defaults

Half the value of a template like this isn't "you get Jest" — you could run npm init and add Jest in five minutes. The value is that the configs all agree with each other. ESLint doesn't fight Prettier. The commit hook and commitlint agree on which subjects are valid. semantic-release reads the same commit format that commitlint enforces. CI reads the same npm scripts you use locally.

Getting that whole matrix right once and freezing it is what saves the hour per package — not any individual config.

How I maintain it

The template lives as a regular directory inside the package. When I hit something that annoys me across multiple packages, I fix it in the template and bump. New packages pick up the fix automatically. Existing packages I patch by hand — I don't try to "update them from the template" because that's scope creep disguised as tooling.

The CLI itself is small enough (~100 lines) that I never touch it. All the real churn is in the template files, where it should be.

What I'd do differently

I'd re-do this with a .gitignore.npmignore rename step baked in from day one instead of bolted on — npm's handling of those files bit me twice before I cached the fix. And I'd skip GitLab CI and ship a GitHub Actions variant, because most of my personal packages are on GitHub now. Neither of these would justify a rewrite, but they're the friction points that remain.

The CLI has created about a dozen packages for me so far, including the Habitify API client and MCP server referenced in the OpenAPI → MCP pipeline. The setup time per package went from an hour of fiddly yak-shaving to about thirty seconds plus npm install.