npx-cli

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

npx 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
    npm-package
    skill)
  • 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

工具链

ConcernToolWhy
Runtime / package managerBunFast install, run, transpile
BundlerBunupBun-native, dual entry (lib + cli), .d.ts
Argument parsingcitty~3KB, TypeScript-native, auto-help,
runMain()
Terminal colorspicocolors~7KB, CJS+ESM, auto-detect
TypeScript
module: "nodenext"
,
strict: true
+ extras
Maximum correctness
Formatting + basic lintingBiome v2Fast, single tool
Type-aware lintingESLint + typescript-eslintDeep type safety
TestingVitestIsolation, mocking, coverage
VersioningChangesetsFile-based, explicit
Publishing
npm publish --provenance
Trusted Publishing / OIDC
关注点工具原因
运行时 / 包管理器Bun安装、运行、转译速度快
打包工具BunupBun原生支持,双入口(库 + CLI),生成.d.ts类型声明
参数解析citty体积约3KB,TypeScript原生支持,自动生成帮助文档,提供
runMain()
方法
终端颜色picocolors体积约7KB,支持CJS+ESM,自动检测终端环境
TypeScript配置
module: "nodenext"
,
strict: true
+ 额外配置
确保代码最大程度的正确性
代码格式化 + 基础代码检查Biome v2速度快,单一工具完成多项任务
类型感知代码检查ESLint + typescript-eslint深度类型安全检查
测试工具Vitest支持隔离测试、模拟、覆盖率统计
版本管理Changesets基于文件的显式版本管理
发布工具
npm publish --provenance
可信发布 / 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 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:
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
可选参数:
  • --bin <name>
    — npx使用的二进制文件名(默认是不带作用域的包名)
  • --cli-only
    — 仅包含CLI二进制文件,不导出库API
  • --no-eslint
    — 跳过ESLint,仅使用Biome
然后安装依赖:
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
└── LICENSE
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

CLI-Only (No Library Exports)

仅CLI(无库导出)

Same structure minus
src/index.ts
and
src/index.test.ts
. No
exports
field in package.json, only
bin
.
结构与默认结构类似,但移除
src/index.ts
src/index.test.ts
。package.json中不包含
exports
字段,仅保留
bin
字段。

Architecture Pattern

架构模式

Separate logic from CLI wiring. The CLI entry (
cli.ts
) is a thin wrapper that:
  1. Parses arguments with citty
  2. Calls into the library/core modules
  3. 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
将业务逻辑与CLI代码解耦。CLI入口文件(
cli.ts
)是一个轻量的包装层,负责:
  1. 使用citty解析参数
  2. 调用库/核心模块的逻辑
  3. 为终端格式化输出内容
所有业务逻辑都放在可导入的模块中(
index.ts
或内部模块)。这样无需启动进程即可对逻辑进行单元测试。
cli.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

二进制文件配置

  1. 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.
  2. Point
    bin
    at compiled JavaScript in
    dist/
    .
    Never at TypeScript source. npx consumers won't have your build toolchain.
  3. Ensure the bin file is executable. The build script includes
    chmod +x dist/cli.js
    after compilation.
  4. Build with Node.js as the target. Bunup's output must run on Node.js, not require Bun runtime features.
  1. 发布的二进制文件中必须使用
    #!/usr/bin/env node
    。绝对不能使用
    #!/usr/bin/env bun
    。绝大多数npx用户没有安装Bun。
  2. bin
    字段指向
    dist/
    目录下编译后的JavaScript文件
    。绝对不能指向TypeScript源码。npx用户不会拥有你的构建工具链。
  3. 确保二进制文件具有可执行权限。构建脚本中需包含
    chmod +x dist/cli.js
    步骤,在编译完成后执行。
  4. 以Node.js为目标进行构建。Bunup的输出必须能在Node.js上运行,不能依赖Bun运行时特有的功能。

Package Configuration

包配置

  1. Always use
    "type": "module"
    in package.json.
  2. types
    must be the first condition
    in every exports block.
  3. Use
    files: ["dist"]
    .
    Whitelist only.
  4. 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
    .
  1. package.json中必须设置
    "type": "module"
  2. 在每个exports块中,
    types
    必须是第一个条件
  3. 使用
    files: ["dist"]
    。仅白名单必要文件。
  4. 对于双入口包(库 + CLI)
    exports
    字段暴露库API,
    bin
    字段暴露CLI。两者相互独立——
    bin
    不属于
    exports
    的一部分。

Code Quality

代码质量

  1. any
    is banned.
    Use
    unknown
    and narrow.
  2. Use
    import type
    for type-only imports.
  3. 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.
  4. Exit with appropriate codes. 0 for success, 1 for errors, 2 for bad arguments, 130 for SIGINT.
  1. 禁止使用
    any
    类型
    。使用
    unknown
    类型并进行类型收窄。
  2. 类型仅导入时使用
    import type
  3. 优雅处理错误。CLI用户永远不应该看到原始的堆栈跟踪。使用citty的
    runMain()
    自动处理错误,同时使用
    process.on('SIGINT', ...)
    进行清理工作。
  4. 返回合适的退出码。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
    exports
    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
修改配置前请阅读以下文档:
  • 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使用、
    files
    字段、可信发布、CI流水线

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 (
bun run build
), then spawn the compiled binary:
typescript
// 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 build
),然后启动编译后的二进制文件:
typescript
// 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
undefined
bash
undefined

Write 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
undefined
bun run release # 构建 + npm publish --provenance
undefined

Adding Sub-Commands Later

后续添加子命令

  1. Create a new file per sub-command:
    src/commands/init.ts
    ,
    src/commands/build.ts
  2. Each exports a
    defineCommand()
    result
  3. Import and wire into the main command's
    subCommands
  4. Keep logic in testable modules, commands are thin wrappers
  1. 为每个子命令创建新文件:
    src/commands/init.ts
    src/commands/build.ts
  2. 每个文件导出
    defineCommand()
    的结果
  3. 在主命令的
    subCommands
    中导入并关联这些子命令
  4. 业务逻辑放在可测试的模块中,子命令仅作为轻量包装层

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

将仅CLI包转换为双入口包(库 + CLI)

  1. Create
    src/index.ts
    with the public API
  2. Update bunup.config.ts to include both entry points
  3. Add
    exports
    field to package.json alongside the existing
    bin
  4. Add .d.ts generation:
    dts: { entry: ['src/index.ts'] }
  1. 创建
    src/index.ts
    并定义公共API
  2. 更新bunup.config.ts以包含两个入口点
  3. 在package.json中添加
    exports
    字段,与现有的
    bin
    字段并存
  4. 启用.d.ts生成:
    dts: { entry: ['src/index.ts'] }

Bun-Specific Gotchas

Bun特有的注意事项

  • 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
    .
  • 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。