npx-cli
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-packageskill) - 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 nodein published bin files. Never#!/usr/bin/env bun. The vast majority of npx users don't have Bun installed. -
Point
binat compiled JavaScript indist/. 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.jsafter 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. -
typesmust be the first condition in every exports block. -
Use
files: ["dist"]. Whitelist only. -
For dual packages (library + CLI): The
exportsfield exposes the library API. Thebinfield exposes the CLI. They are independent —binis NOT part ofexports.
Code Quality
-
anyis banned. Useunknownand narrow. -
Use
import typefor type-only imports. -
Handle errors gracefully. CLI users should never see raw stack traces. Use citty's
runMain()which handles this automatically, plusprocess.on('SIGINT', ...)for cleanup. -
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.md —
exportsmap, dual package hazard, common mistakes - reference/strict-typescript.md — tsconfig, Biome rules, ESLint type-aware rules, Vitest config
- reference/publishing-workflow.md — Changesets,
filesfield, 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.tswith the public API - Update bunup.config.ts to include both entry points
- Add
exportsfield to package.json alongside the existingbin - Add .d.ts generation:
dts: { entry: ['src/index.ts'] }
Bun-Specific Gotchas
bun builddoes not generate .d.ts files. Use Bunup ortsc --emitDeclarationOnly.bun builddoes not downlevel syntax. ES2022+ ships as-is.bun publishdoes not support--provenance. Usenpm publish.bun publishusesNPM_CONFIG_TOKEN, notNODE_AUTH_TOKEN.- Never use
#!/usr/bin/env bunin published packages. Your users don't have Bun. - Bunup
banneradds 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 todist/cli.js.
More from jwynia/agent-skills
frontend-design
Create distinctive, production-grade frontend interfaces with high design quality. Provides analysis tools for auditing existing designs and generation tools for creating color palettes, typography systems, design tokens, and component templates. Supports React, Vue, Svelte, and vanilla HTML/CSS. Use when building web components, pages, or applications. Keywords: design, UI, frontend, CSS, components, palette, typography, tokens, accessibility.
2.0Krequirements-analysis
Diagnose requirements problems and guide discovery of real needs and constraints
1.8Kgodot-best-practices
Guide AI agents through Godot 4.x GDScript coding best practices including scene organization, signals, resources, state machines, and performance optimization. This skill should be used when generating GDScript code, creating Godot scenes, designing game architecture, implementing state machines, object pooling, save/load systems, or when the user asks about Godot patterns, node structure, or GDScript standards. Keywords: godot, gdscript, game development, signals, resources, scenes, nodes, state machine, object pooling, save system, autoload, export, type hints.
1.4Kpresentation-design
Design and evaluate presentations that communicate effectively. Use when designing a presentation, creating slides, getting presentation feedback, structuring a talk, or reviewing slides. Keywords: presentation, slides, talk, PowerPoint, Keynote, reveal.js.
1.3Kweb-search-tavily
Search the web using Tavily API for high-quality, AI-optimized results with advanced filtering options. Use when you need structured search results, domain filtering, relevance scores, or AI-generated answer summaries. Requires TAVILY_API_KEY. Keywords: tavily, advanced search, filtered search, domain filtering, relevance scoring.
1.0Kstory-coach
Act as an assistive writing coach who guides but never writes for the user. Use when helping someone develop their own writing through questions, diagnosis, and frameworks. Critical constraint - never generate story prose, dialogue, or narrative content. Instead ask questions, identify issues, suggest approaches, and let the writer write.
702