npx skills add https://github.com/jwynia/agent-skills --skill npx-cli使用 Bun 作为主要运行时和工具链,构建并发布可通过 npx 执行命令行工具,生成适用于所有 npm/npx 用户(Node.js 运行时)的二进制文件。
在以下情况下使用:
在以下情况下请勿使用:
npm-package 技能)| 关注点 | 工具 | 原因 |
|---|---|---|
| 运行时 / 包管理器 | Bun | 安装、运行、转译速度快 |
| 打包工具 | Bunup | Bun 原生,双入口(库 + cli),支持 .d.ts |
| 参数解析 | citty | ~3KB,TypeScript 原生,自动帮助,runMain() |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 终端颜色 | picocolors | ~7KB,支持 CJS+ESM,自动检测 |
| TypeScript | module: "nodenext", strict: true + 额外配置 | 最大程度保证正确性 |
| 格式化 + 基础代码检查 | Biome v2 | 快速,单一工具 |
| 类型感知代码检查 | ESLint + typescript-eslint | 深度类型安全 |
| 测试 | Vitest | 隔离、模拟、覆盖率 |
| 版本管理 | Changesets | 基于文件,显式声明 |
| 发布 | npm publish --provenance | 可信发布 / OIDC |
运行脚手架脚本:
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 # 除非使用了 --no-eslint
my-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
└── LICENSE
结构相同,减去 src/index.ts 和 src/index.test.ts。package.json 中没有 exports 字段,只有 bin。
将业务逻辑与 CLI 连接代码分离。 CLI 入口(cli.ts)是一个薄包装层,它:
所有业务逻辑都位于可导入的模块中(index.ts 或内部模块)。这使得逻辑可以在不产生进程的情况下进行单元测试。
cli.ts → 导入自 → index.ts / 核心模块
↑
单元测试
npm-package 技能中的所有规则在此均适用。以下附加规则特定于 CLI 包:
#!/usr/bin/env node。 切勿使用 #!/usr/bin/env bun。绝大多数 npx 用户没有安装 Bun。bin 指向 dist/ 中编译后的 JavaScript 文件。 切勿指向 TypeScript 源代码。npx 使用者不会有你的构建工具链。chmod +x dist/cli.js。"type": "module"。types 必须是每个 exports 块中的第一个条件。files: ["dist"]。 仅白名单。exports 字段公开库 API。bin 字段公开 CLI。它们是独立的 — bin 不是 exports 的一部分。any。 使用 unknown 并进行类型收窄。import type。runMain() 可以自动处理此问题,再加上 process.on('SIGINT', ...) 进行清理。修改配置前请阅读:
exports 映射、双包风险、常见错误files 字段、可信发布、CI 流水线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);
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');
});
});
# 编写代码和测试
bun run test:watch # Vitest 监视模式
# 检查所有内容
bun run lint # Biome + ESLint
bun run typecheck # tsc --noEmit
bun run test # Vitest
# 构建并本地尝试 CLI
bun run build
node ./dist/cli.js --help
node ./dist/cli.js some-input
# 准备发布
bunx changeset
bunx changeset version
# 发布
bun run release # 构建 + npm publish --provenance
src/commands/init.ts、src/commands/build.tsdefineCommand() 的结果subCommandssrc/index.tsexports 字段,与现有的 bin 并列dts: { entry: ['src/index.ts'] }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。banner 会将 shebang 添加到所有输出文件,包括库入口点。如果这是个问题,请使用构建后脚本仅将 shebang 添加到 dist/cli.js。每周安装量
75
代码仓库
GitHub 星标数
38
首次出现
2026年2月16日
安全审计
已安装于
opencode69
codex69
gemini-cli68
github-copilot67
kimi-cli66
amp66
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).
Use when:
Do NOT use when:
npm-package skill)| 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 |
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 onlyThen 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
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
Same structure minus src/index.ts and src/index.test.ts. No exports field in package.json, only bin.
Separate logic from CLI wiring. The CLI entry (cli.ts) is a thin wrapper that:
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
All rules from the npm-package skill apply here. These additional rules are specific to CLI packages:
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.
Pointbin 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.
Always use"type": "module" in package.json.
types must be the first condition in every exports block.
Usefiles: ["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.
any is banned. Use unknown and narrow.
Useimport type for type-only imports.
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.
Exit with appropriate codes. 0 for success, 1 for errors, 2 for bad arguments, 130 for SIGINT.
Read these before modifying configuration:
exports map, dual package hazard, common mistakesfiles field, Trusted Publishing, CI pipelineimport { 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);
See reference/cli-patterns.md for complete examples including error handling, colors, and spinners.
// 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');
});
});
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');
});
});
# 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.tsdefineCommand() resultsubCommandssrc/index.ts with the public APIexports field to package.json alongside the existing bindts: { entry: ['src/index.ts'] }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.Weekly Installs
75
Repository
GitHub Stars
38
First Seen
Feb 16, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode69
codex69
gemini-cli68
github-copilot67
kimi-cli66
amp66
Lark CLI Wiki API 使用指南:获取知识空间节点信息与权限管理
39,100 周安装
Web无障碍开发指南:WCAG 2.1标准、ARIA模式与键盘导航最佳实践
168 周安装
WordPress插件开发指南:架构、安全、生命周期与Settings API详解
158 周安装
产品分析指南:北极星指标、漏斗分析、事件追踪与关键指标框架
155 周安装
设计思维完整指南:IDEO/斯坦福五阶段方法论、工具模板与实践案例
157 周安装
error-detective 错误排查专家技能:日志分析、堆栈跟踪、分布式系统错误关联与模式识别
160 周安装
Google Workspace API 自动化脚本 - 集成 Drive、Gmail、Calendar 和 Docs
156 周安装
#!/usr/bin/env bunbanner 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.