mcpbeat Sign in

Npx CLI Agent Skill

Build and publish npx-executable CLI tools using Bun as the primary toolchain with npm-compatible output. Use when the user wants to create a new CLI tool, set up a command-line package for npx execution, configure argument parsing and terminal output, or publish a CLI to npm. Covers scaffolding, citty arg parsing, sub-commands, terminal UX, strict TypeScript, Biome + ESLint linting, Vitest testing, Bunup bundling, and publishing workflows. Keywords: npx, cli, command-line, binary, bin, tool, bun, citty, commander, terminal, publish, typescript, biome, vitest.

13k tokens
context cost
the whole folder, loaded on every use
18
files
ships runnable scripts
0
copies elsewhere
how many repositories repackaged it
128
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/jwynia/agent-skills --skill npx-cli

What comes with it

42 911 bytes besides the instruction
reference/cli-patterns.md
reference/esm-cjs-guide.md
reference/publishing-workflow.md
reference/strict-typescript.md
scripts/scaffold.ts
templates/biome.json
templates/bunup.config.ts
templates/eslint.config.ts
templates/gitignore
templates/package.json.hbs
templates/scripts/postbuild.ts
templates/src/cli.test.ts.hbs
templates/src/cli.ts.hbs
templates/src/index.test.ts.hbs
templates/src/index.ts.hbs
templates/tsconfig.json
templates/vitest.config.ts

The instruction itself

23 sections, as written by the author

npx CLI Tool Development (Bun-First)

Build and publish npx-executable command-line tools using Bun as the primary runtime and toolchain, producing binaries that work for all npm/npx users (Node.js runtime).

When to Use This Skill

Use when:

  • Creating a new CLI tool from scratch
  • Building an npx-executable binary
  • Setting up argument parsing, sub-commands, or terminal UX for a CLI
  • Publishing a CLI tool to npm
  • Adding a CLI to an existing library package

Do NOT use when:

  • Building a library without a CLI (use the npm-package skill)
  • Building an application (not a published package)
  • Working in a monorepo (this skill targets single-package repos)

Toolchain

| Concern | Tool | Why |

|---------|------|-----|

| Runtime / package manager | Bun | Fast install, run, transpile |

| Bundler | Bunup | Bun-native, dual entry (lib + cli), .d.ts |

| Argument parsing | citty | ~3KB, TypeScript-native, auto-help, runMain() |

| Terminal colors | picocolors | ~7KB, CJS+ESM, auto-detect |

| TypeScript | module: "nodenext", strict: true + extras | Maximum correctness |

| Formatting + basic linting | Biome v2 | Fast, single tool |

| Type-aware linting | ESLint + typescript-eslint | Deep type safety |

| Testing | Vitest | Isolation, mocking, coverage |

| Versioning | Changesets | File-based, explicit |

| Publishing | npm publish --provenance | Trusted Publishing / OIDC |

Scaffolding a New CLI

Run the scaffold script:

bun run <skill-path>/scripts/scaffold.ts ./my-cli \
  --name my-cli \
  --bin my-cli \
  --description "What this CLI does" \
  --author "Your Name" \
  --license MIT

Options:

  • --bin <name> — Binary name for npx (defaults to package name without scope)
  • --cli-only — No library exports, CLI binary only
  • --no-eslint — Skip ESLint, use Biome only

Then install dependencies:

cd my-cli
bun install
bun add -d bunup typescript vitest @vitest/coverage-v8 @biomejs/biome @changesets/cli
bun add citty picocolors
bun add -d eslint typescript-eslint  # unless --no-eslint

Project Structure

Dual (Library + CLI) — Default

my-cli/
├── src/
│   ├── index.ts            # Library exports (programmatic API)
│   ├── index.test.ts       # Unit tests for library
│   ├── cli.ts              # CLI entry point (imports from index.ts)
│   └── cli.test.ts         # CLI integration tests
├── dist/
│   ├── index.js            # Library bundle
│   ├── index.d.ts          # Type declarations
│   └── cli.js              # CLI binary (with shebang)
├── .changeset/
│   └── config.json
├── package.json
├── tsconfig.json
├── bunup.config.ts
├── biome.json
├── eslint.config.ts
├── vitest.config.ts
├── .gitignore
├── README.md
└── LICENSE

CLI-Only (No Library Exports)

Same structure minus src/index.ts and src/index.test.ts. No exports field in package.json, only bin.

Architecture Pattern

Separate logic from CLI wiring. The CLI entry (cli.ts) is a thin wrapper that:

  • Parses arguments with citty
  • Calls into the library/core modules
  • Formats output for the terminal

All business logic lives in importable modules (index.ts or internal modules). This makes logic unit-testable without spawning processes.

cli.ts → imports from → index.ts / core modules
                              ↑
                         unit tests

Key Rules (Non-Negotiable)

All rules from the npm-package skill apply here. These additional rules are specific to CLI packages:

Binary Configuration

  • Always use #!/usr/bin/env node in published bin files. Never #!/usr/bin/env bun. The vast majority of npx users don't have Bun installed.
  • Point bin at compiled JavaScript in dist/. Never at TypeScript source. npx consumers won't have your build toolchain.
  • Ensure the bin file is executable. The build script includes chmod +x dist/cli.js after compilation.
  • Build with Node.js as the target. Bunup's output must run on Node.js, not require Bun runtime features.

Package Configuration

  • Always use "type": "module" in package.json.
  • types must be the first condition in every exports block.
  • Use files: ["dist"]. Whitelist only.
  • For dual packages (library + CLI): The exports field exposes the library API. The bin field exposes the CLI. They are independent — bin is NOT part of exports.

Code Quality

  • any is banned. Use unknown and narrow.

10. Use import type for type-only imports.

11. Handle errors gracefully. CLI users should never see raw stack traces. Use citty's runMain() which handles this automatically, plus process.on('SIGINT', ...) for cleanup.

12. Exit with appropriate codes. 0 for success, 1 for errors, 2 for bad arguments, 130 for SIGINT.

Reference Documentation

Read these before modifying configuration:

  • reference/cli-patterns.md — bin setup, citty patterns, sub-commands, error handling, terminal UX, testing CLI binaries
  • reference/esm-cjs-guide.mdexports map, dual package hazard, common mistakes
  • reference/strict-typescript.md — tsconfig, Biome rules, ESLint type-aware rules, Vitest config
  • reference/publishing-workflow.md — Changesets, files field, Trusted Publishing, CI pipeline

Argument Parsing with citty

Single Command

import { defineCommand, runMain } from 'citty';

const main = defineCommand({
  meta: { name: 'my-cli', version: '1.0.0', description: '...' },
  args: {
    input: { type: 'positional', description: 'Input file', required: true },
    output: { alias: 'o', type: 'string', description: 'Output path', default: './out' },
    verbose: { alias: 'v', type: 'boolean', description: 'Verbose output', default: false },
  },
  run({ args }) {
    // args is fully typed
  },
});

void runMain(main);

Sub-Commands

import { defineCommand, runMain } from 'citty';

const init = defineCommand({ meta: { name: 'init' }, /* ... */ });
const build = defineCommand({ meta: { name: 'build' }, /* ... */ });

const main = defineCommand({
  meta: { name: 'my-cli', version: '1.0.0' },
  subCommands: { init, build },
});

void runMain(main);

See reference/cli-patterns.md for complete examples including error handling, colors, and spinners.

Testing Strategy

Unit Tests — Test the Logic

// src/index.test.ts
import { describe, it, expect } from 'vitest';
import { processInput } from './index.js';

describe('processInput', () => {
  it('handles valid input', () => {
    expect(processInput('test')).toBe('expected');
  });
});

Integration Tests — Test the Binary

Build first (bun run build), then spawn the compiled binary:

// src/cli.test.ts
import { describe, it, expect } from 'vitest';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';

const exec = promisify(execFile);

describe('CLI', () => {
  it('prints help', async () => {
    const { stdout } = await exec('node', ['./dist/cli.js', '--help']);
    expect(stdout).toContain('my-cli');
  });
});

Development Workflow

# Write code and tests
bun run test:watch    # Vitest watch mode

# Check everything
bun run lint          # Biome + ESLint
bun run typecheck     # tsc --noEmit
bun run test          # Vitest

# Build and try the CLI locally
bun run build
node ./dist/cli.js --help
node ./dist/cli.js some-input

# Prepare release
bunx changeset
bunx changeset version

# Publish
bun run release       # Build + npm publish --provenance

Adding Sub-Commands Later

  • Create a new file per sub-command: src/commands/init.ts, src/commands/build.ts
  • Each exports a defineCommand() result
  • Import and wire into the main command's subCommands
  • Keep logic in testable modules, commands are thin wrappers

Converting a CLI-Only Package to Dual (Library + CLI)

  • Create src/index.ts with the public API
  • Update bunup.config.ts to include both entry points
  • Add exports field to package.json alongside the existing bin
  • Add .d.ts generation: dts: { entry: ['src/index.ts'] }

Bun-Specific Gotchas

  • bun build does not generate .d.ts files. Use Bunup or tsc --emitDeclarationOnly.
  • bun build does not downlevel syntax. ES2022+ ships as-is.
  • bun publish does not support --provenance. Use npm publish.
  • bun publish uses NPM_CONFIG_TOKEN, not NODE_AUTH_TOKEN.
  • Never use #!/usr/bin/env bun in published packages. Your users don't have Bun.
  • Bunup banner adds the shebang to ALL output files, including the library entry. If this is a problem, use a post-build script to add the shebang only to dist/cli.js.

Other skills for the same job

different authors, same section of the catalogue
Finishing A Development Branch
by ZhanlinCui
×7

Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup

1k tokens
Github Release Management
by ComeOnOliver
×3

Comprehensive GitHub release orchestration with AI swarm coordination for automated versioning, testing, deployment, and rollback management

10k tokens
Migrate To Shoehorn
by mxyhi
×2

Migrate test files from `as` type assertions to @total-typescript/shoehorn. Use when user mentions shoehorn, wants to replace `as` in tests, or needs partial test data.

965 tokens
Bun Development
by ComeOnOliver
×2

Modern JavaScript/TypeScript development with Bun runtime. Covers package management, bundling, testing, and migration from Node.js. Use when working with Bun, optimizing JS/TS development speed, or migrating from Node.js to Bun.

8k tokens
Framework Migration Deps Upgrade
by ComeOnOliver
×2

You are a dependency management expert specializing in safe, incremental upgrades of project dependencies. Plan and execute dependency updates with minimal risk, proper testing, and clear migration pa

8k tokens
Debugging Strategies
by ComeOnOliver
×2

Master systematic debugging techniques, profiling tools, and root cause analysis to efficiently track down bugs across any codebase or technology stack. Use when investigating bugs, performance issues, or unexpected behavior.

6k tokens
Backend Dev Guidelines
by ComeOnOliver
×2

Opinionated backend development standards for Node.js + Express + TypeScript microservices. Covers layered architecture, BaseController pattern, dependency injection, Prisma repositories, Zod validation, unifiedConfig, Sentry error tracking, async safety, and testing discipline.

36k tokens
Javascript Typescript Jest
by github
vendor ×1

Best practices for writing JavaScript/TypeScript tests using Jest, including mocking strategies, test structure, and common patterns.

558 tokens

How to use it

Copy the folder

Take jwynia/npx-cli from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.

Install what it needs

The instructions reference npx. Without those the skill loads but fails at the first command.