npx-cli
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesenpx CLI Tool Development (Bun-First)
npx CLI工具开发(优先使用Bun)
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).
使用Bun作为主要运行时和工具链构建并发布npx可执行的命令行工具,生成的二进制文件可在所有npm/npx用户环境中运行(基于Node.js运行时)。
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 skill)
npm-package - Building an application (not a published package)
- Working in a monorepo (this skill targets single-package repos)
适用于以下场景:
- 从零开始创建新的CLI工具
- 构建支持npx执行的二进制文件
- 为CLI配置参数解析、子命令或终端用户体验
- 将CLI工具发布到npm
- 为现有库包添加CLI功能
不适用于以下场景:
- 构建不带CLI的库(请使用技能)
npm-package - 构建应用程序(而非可发布的包)
- 工作在单体仓库中(本技能针对单包仓库)
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, |
| Terminal colors | picocolors | ~7KB, CJS+ESM, auto-detect |
| TypeScript | | 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 | | Trusted Publishing / OIDC |
| 关注点 | 工具 | 原因 |
|---|---|---|
| 运行时 / 包管理器 | Bun | 安装、运行、转译速度快 |
| 打包工具 | Bunup | Bun原生支持,双入口(库 + CLI),生成.d.ts类型声明 |
| 参数解析 | citty | 体积约3KB,TypeScript原生支持,自动生成帮助文档,提供 |
| 终端颜色 | picocolors | 体积约7KB,支持CJS+ESM,自动检测终端环境 |
| TypeScript配置 | | 确保代码最大程度的正确性 |
| 代码格式化 + 基础代码检查 | Biome v2 | 速度快,单一工具完成多项任务 |
| 类型感知代码检查 | ESLint + typescript-eslint | 深度类型安全检查 |
| 测试工具 | Vitest | 支持隔离测试、模拟、覆盖率统计 |
| 版本管理 | Changesets | 基于文件的显式版本管理 |
| 发布工具 | | 可信发布 / OIDC认证 |
Scaffolding a New CLI
搭建新CLI项目脚手架
Run the scaffold script:
bash
bun run <skill-path>/scripts/scaffold.ts ./my-cli \
--name my-cli \
--bin my-cli \
--description "What this CLI does" \
--author "Your Name" \
--license MITOptions:
- — Binary name for npx (defaults to package name without scope)
--bin <name> - — No library exports, CLI binary only
--cli-only - — Skip ESLint, use Biome only
--no-eslint
Then install dependencies:
bash
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运行脚手架脚本:
bash
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可选参数:
- — npx使用的二进制文件名(默认是不带作用域的包名)
--bin <name> - — 仅包含CLI二进制文件,不导出库API
--cli-only - — 跳过ESLint,仅使用Biome
--no-eslint
然后安装依赖:
bash
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 # 若使用了--no-eslint则无需执行Project Structure
项目结构
Dual (Library + CLI) — Default
双入口(库 + CLI)—— 默认结构
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
└── LICENSEmy-cli/
├── src/
│ ├── index.ts # 库导出的公共API
│ ├── index.test.ts # 库的单元测试
│ ├── cli.ts # CLI入口文件(从index.ts导入逻辑)
│ └── cli.test.ts # CLI集成测试
├── dist/
│ ├── index.js # 库打包产物
│ ├── index.d.ts # 类型声明文件
│ └── cli.js # CLI二进制文件(包含shebang)
├── .changeset/
│ └── config.json
├── package.json
├── tsconfig.json
├── bunup.config.ts
├── biome.json
├── eslint.config.ts
├── vitest.config.ts
├── .gitignore
├── README.md
└── LICENSECLI-Only (No Library Exports)
仅CLI(无库导出)
Same structure minus and . No field in package.json, only .
src/index.tssrc/index.test.tsexportsbin结构与默认结构类似,但移除和。package.json中不包含字段,仅保留字段。
src/index.tssrc/index.test.tsexportsbinArchitecture Pattern
架构模式
Separate logic from CLI wiring. The CLI entry () is a thin wrapper that:
cli.ts- Parses arguments with citty
- Calls into the library/core modules
- Formats output for the terminal
All business logic lives in importable modules ( or internal modules). This makes logic unit-testable without spawning processes.
index.tscli.ts → imports from → index.ts / core modules
↑
unit tests将业务逻辑与CLI代码解耦。CLI入口文件()是一个轻量的包装层,负责:
cli.ts- 使用citty解析参数
- 调用库/核心模块的逻辑
- 为终端格式化输出内容
所有业务逻辑都放在可导入的模块中(或内部模块)。这样无需启动进程即可对逻辑进行单元测试。
index.tscli.ts → 导入自 → index.ts / 核心模块
↑
单元测试Key Rules (Non-Negotiable)
核心规则(必须遵守)
All rules from the npm-package skill apply here. These additional rules are specific to CLI packages:
npm-package技能中的所有规则均适用。以下是CLI包特有的额外规则:
Binary Configuration
二进制文件配置
-
Always usein published bin files. Never
#!/usr/bin/env node. The vast majority of npx users don't have Bun installed.#!/usr/bin/env bun -
Pointat compiled JavaScript in
bin. Never at TypeScript source. npx consumers won't have your build toolchain.dist/ -
Ensure the bin file is executable. The build script includesafter compilation.
chmod +x dist/cli.js -
Build with Node.js as the target. Bunup's output must run on Node.js, not require Bun runtime features.
-
发布的二进制文件中必须使用。绝对不能使用
#!/usr/bin/env node。绝大多数npx用户没有安装Bun。#!/usr/bin/env bun -
字段指向
bin目录下编译后的JavaScript文件。绝对不能指向TypeScript源码。npx用户不会拥有你的构建工具链。dist/ -
确保二进制文件具有可执行权限。构建脚本中需包含步骤,在编译完成后执行。
chmod +x dist/cli.js -
以Node.js为目标进行构建。Bunup的输出必须能在Node.js上运行,不能依赖Bun运行时特有的功能。
Package Configuration
包配置
-
Always usein package.json.
"type": "module" -
must be the first condition in every exports block.
types -
Use. Whitelist only.
files: ["dist"] -
For dual packages (library + CLI): Thefield exposes the library API. The
exportsfield exposes the CLI. They are independent —binis NOT part ofbin.exports
-
package.json中必须设置。
"type": "module" -
在每个exports块中,必须是第一个条件。
types -
使用。仅白名单必要文件。
files: ["dist"] -
对于双入口包(库 + CLI):字段暴露库API,
exports字段暴露CLI。两者相互独立——bin不属于bin的一部分。exports
Code Quality
代码质量
-
is banned. Use
anyand narrow.unknown -
Usefor type-only imports.
import type -
Handle errors gracefully. CLI users should never see raw stack traces. Use citty'swhich handles this automatically, plus
runMain()for cleanup.process.on('SIGINT', ...) -
Exit with appropriate codes. 0 for success, 1 for errors, 2 for bad arguments, 130 for SIGINT.
-
禁止使用类型。使用
any类型并进行类型收窄。unknown -
类型仅导入时使用。
import type -
优雅处理错误。CLI用户永远不应该看到原始的堆栈跟踪。使用citty的自动处理错误,同时使用
runMain()进行清理工作。process.on('SIGINT', ...) -
返回合适的退出码。0表示成功,1表示错误,2表示参数错误,130表示收到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 — map, dual package hazard, common mistakes
exports - reference/strict-typescript.md — tsconfig, Biome rules, ESLint type-aware rules, Vitest config
- reference/publishing-workflow.md — Changesets, field, Trusted Publishing, CI pipeline
files
修改配置前请阅读以下文档:
- reference/cli-patterns.md — 二进制文件配置、citty使用模式、子命令、错误处理、终端用户体验、CLI二进制文件测试
- reference/esm-cjs-guide.md — 映射、双入口包注意事项、常见错误
exports - reference/strict-typescript.md — tsconfig配置、Biome规则、ESLint类型感知规则、Vitest配置
- reference/publishing-workflow.md — Changesets使用、字段、可信发布、CI流水线
files
Argument Parsing with citty
使用citty进行参数解析
Single Command
单命令模式
typescript
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);typescript
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是完全类型化的
},
});
void runMain(main);Sub-Commands
子命令模式
typescript
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.
typescript
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);查看reference/cli-patterns.md获取完整示例,包括错误处理、颜色输出和加载动画。
Testing Strategy
测试策略
Unit Tests — Test the Logic
单元测试 — 测试业务逻辑
typescript
// 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');
});
});typescript
// 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 (), then spawn the compiled binary:
bun run buildtypescript
// 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');
});
});先构建项目(),然后启动编译后的二进制文件:
bun run buildtypescript
// 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
开发工作流
bash
undefinedbash
undefinedWrite code and tests
编写代码和测试
bun run test:watch # Vitest watch mode
bun run test:watch # Vitest监听模式
Check everything
检查所有内容
bun run lint # Biome + ESLint
bun run typecheck # tsc --noEmit
bun run test # Vitest
bun run lint # Biome + ESLint代码检查
bun run typecheck # tsc --noEmit类型检查
bun run test # Vitest测试
Build and try the CLI locally
构建并本地测试CLI
bun run build
node ./dist/cli.js --help
node ./dist/cli.js some-input
bun run build
node ./dist/cli.js --help
node ./dist/cli.js some-input
Prepare release
准备发布版本
bunx changeset
bunx changeset version
bunx changeset
bunx changeset version
Publish
发布
bun run release # Build + npm publish --provenance
undefinedbun run release # 构建 + npm publish --provenance
undefinedAdding Sub-Commands Later
后续添加子命令
- Create a new file per sub-command: ,
src/commands/init.tssrc/commands/build.ts - Each exports a result
defineCommand() - Import and wire into the main command's
subCommands - Keep logic in testable modules, commands are thin wrappers
- 为每个子命令创建新文件:、
src/commands/init.tssrc/commands/build.ts - 每个文件导出的结果
defineCommand() - 在主命令的中导入并关联这些子命令
subCommands - 业务逻辑放在可测试的模块中,子命令仅作为轻量包装层
Converting a CLI-Only Package to Dual (Library + CLI)
将仅CLI包转换为双入口包(库 + CLI)
- Create with the public API
src/index.ts - Update bunup.config.ts to include both entry points
- Add field to package.json alongside the existing
exportsbin - Add .d.ts generation:
dts: { entry: ['src/index.ts'] }
- 创建并定义公共API
src/index.ts - 更新bunup.config.ts以包含两个入口点
- 在package.json中添加字段,与现有的
exports字段并存bin - 启用.d.ts生成:
dts: { entry: ['src/index.ts'] }
Bun-Specific Gotchas
Bun特有的注意事项
- does not generate .d.ts files. Use Bunup or
bun build.tsc --emitDeclarationOnly - does not downlevel syntax. ES2022+ ships as-is.
bun build - does not support
bun publish. Use--provenance.npm publish - uses
bun publish, notNPM_CONFIG_TOKEN.NODE_AUTH_TOKEN - Never use in published packages. Your users don't have Bun.
#!/usr/bin/env bun - Bunup 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
banner.dist/cli.js
- 不会生成.d.ts文件。请使用Bunup或
bun build。tsc --emitDeclarationOnly - 不会降级语法。ES2022+语法会直接输出。
bun build - 不支持
bun publish参数。请使用--provenance。npm publish - 使用
bun publish环境变量,而非NPM_CONFIG_TOKEN。NODE_AUTH_TOKEN - 绝对不要在发布的包中使用。你的用户没有安装Bun。
#!/usr/bin/env bun - Bunup的会为所有输出文件添加shebang,包括库入口文件。如果这会造成问题,请使用构建后脚本仅为
banner添加shebang。dist/cli.js