关于 npx-cli
面向单包仓库的技能指南,用于构建并发布可通过 npx 执行的命令行工具。它以 Bun 为主要开发工具链,同时要求产物可在 Node.js 环境运行。
主要功能
- 01Bun 优先的工具链配置
- 02生成 CLI 项目脚手架
- 03解析参数与子命令
- 04分离 CLI 与业务逻辑
- 05配置测试、检查与发布流程
适用场景
- 01从头创建 npx 命令行工具
- 02为现有库添加 CLI
- 03配置参数解析和终端交互
- 04将 CLI 包发布到 npm
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jwynia/ agent-skills --skill npx-cli安装后在 Claude Code 中输入/npx-cli确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
reference/cli-patterns.mdesm-cjs-guide.mdpublishing-workflow.mdstrict-typescript.mdscripts/scaffold.tstemplates/biome.jsonbunup.config.tseslint.config.tsgitignorepackage.json.hbstsconfig.jsonvitest.config.tstemplates/scripts/postbuild.tstemplates/src/cli.test.ts.hbscli.ts.hbsindex.test.ts.hbsindex.ts.hbs详细介绍
npx CLI 工具开发(Bun 优先)
使用 Bun 作为主要运行时和工具链,构建并发布可通过 npx 执行的命令行工具,生成适用于所有 npm/npx 用户(Node.js 运行时)的二进制文件。
何时使用此技能
适用于:
- 从头开始创建新的 CLI 工具
- 构建可通过 npx 执行的二进制文件
- 为 CLI 设置参数解析、子命令或终端 UX
- 将 CLI 工具发布到 npm
- 将 CLI 添加到现有库包中
不适用于:
- 构建不带 CLI 的库(请使用
npm-package技能) - 构建应用程序(而非已发布的包)
- 在 monorepo 中工作(此技能面向单包仓库)
工具链
| 关注事项 | 工具 | 原因 |
|---|---|---|
| 运行时 / 包管理器 | Bun | 安装、运行和转译速度快 |
| 打包工具 | Bunup | 原生支持 Bun、双入口(库 + CLI)、生成 .d.ts |
| 参数解析 | citty | 约 3KB,原生支持 TypeScript,自动生成帮助信息,提供 runMain() |
| 终端颜色 | picocolors | 约 7KB,支持 CJS+ESM,自动检测 |
| TypeScript | module: "nodenext"、strict: true + 其他选项 | 最大程度确保正确性 |
| 格式化 + 基础 lint | Biome v2 | 快速、单一工具 |
| 类型感知 lint | ESLint + typescript-eslint | 深入的类型安全检查 |
| 测试 | Vitest | 隔离、模拟、覆盖率 |
| 版本管理 | Changesets | 基于文件、明确 |
| 发布 | npm publish --provenance | 可信发布 / OIDC |
搭建新的 CLI
运行脚手架脚本:
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
选项:
--bin <name>— npx 的二进制名称(默认为不带作用域的包名)--cli-only— 不导出库,仅包含 CLI 二进制文件--no-eslint— 跳过 ESLint,仅使用 Biome
然后安装依赖:
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
项目结构
双入口(库 + 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
└── LICENSE
仅 CLI(不导出库)
结构相同,但不包含 src/index.ts 和 src/index.test.ts。package.json 中不设置 exports 字段,仅设置 bin。
架构模式
将逻辑与 CLI 接线分离。 CLI 入口(cli.ts)是一个轻量包装器,负责:
- 使用 citty 解析参数
- 调用库/核心模块
- 格式化终端输出
所有业务逻辑都放在可导入的模块(index.ts 或内部模块)中。这样无需启动进程即可对逻辑进行单元测试。
cli.ts → imports from → index.ts / core modules
↑
unit tests
关键规则(必须遵守)
此处同样适用 npm-package 技能中的所有规则。以下附加规则专用于 CLI 包:
二进制文件配置
-
发布的 bin 文件始终使用
#!/usr/bin/env node。 绝不使用#!/usr/bin/env bun。绝大多数 npx 用户都没有安装 Bun。 -
将
bin指向dist/中编译后的 JavaScript。 绝不指向 TypeScript 源码。npx 用户不会拥有你的构建工具链。 -
确保 bin 文件具有可执行权限。 构建脚本会在编译后执行
chmod +x dist/cli.js。 -
以 Node.js 为目标进行构建。 Bunup 的输出必须能在 Node.js 上运行,不能依赖 Bun 运行时功能。
包配置
-
始终在 package.json 中使用
"type": "module"。 -
每个 exports 块中,
types都必须是第一个条件。 -
使用
files: ["dist"]。 仅使用白名单。 -
对于双用途包(库 + CLI):
exports字段公开库 API,bin字段公开 CLI。二者相互独立——bin不属于exports。
代码质量
-
禁止使用
any。 使用unknown并进行类型收窄。 -
仅导入类型时使用
import type。 -
妥善处理错误。 CLI 用户不应看到原始堆栈跟踪。使用 citty 的
runMain()自动处理错误,并通过process.on('SIGINT', ...)执行清理。 -
使用恰当的退出码。 成功时为 0,错误时为 1,参数错误时为 2,收到 SIGINT 时为 130。
参考文档
修改配置前请先阅读:
- reference/cli-patterns.md — bin 设置、citty 模式、子命令、错误处理、终端 UX、CLI 二进制文件测试
- reference/esm-cjs-guide.md —
exports映射、双包隐患、常见错误 - reference/strict-typescript.md — tsconfig、Biome 规则、ESLint 类型感知规则、Vitest 配置
- reference/publishing-workflow.md — Changesets、
files字段、Trusted Publishing、CI 流水线
使用 citty 解析参数
单个命令
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);
子命令
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。
测试策略
单元测试 — 测试逻辑
// 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');
});
});
集成测试 — 测试二进制文件
先构建(bun run build),然后启动编译后的二进制文件:
// 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');
});
});
开发工作流
# 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
之后添加子命令
- 为每个子命令创建一个新文件:
src/commands/init.ts、src/commands/build.ts - 每个文件都导出一个
defineCommand()结果 - 导入并将其接入主命令的
subCommands - 将逻辑放在可测试的模块中,命令只作为薄封装
将仅含 CLI 的包转换为双用途包(库 + CLI)
- 创建包含公共 API 的
src/index.ts - 更新 bunup.config.ts,包含两个入口点
- 在 package.json 中现有的
bin旁添加exports字段 - 添加 .d.ts 生成配置:
dts: { entry: ['src/index.ts'] }
Bun 特有的注意事项
bun build不会生成 .d.ts 文件。 使用 Bunup 或tsc --emitDeclarationOnly。bun build不会转换较新语法。 ES2022+ 将原样发布。bun publish不支持--provenance。 请使用npm publish。bun publish使用NPM_CONFIG_TOKEN,而不是NODE_AUTH_TOKEN。- 切勿在已发布的包中使用
#!/usr/bin/env bun。 用户并没有安装 Bun。 - Bunup
banner会为所有输出文件添加 shebang,包括库入口。如果这有问题,请使用构建后脚本,仅为dist/cli.js添加 shebang。