npx-cli について
単一パッケージのリポジトリで、npx から実行できるコマンドラインツールを構築・公開するためのスキルです。開発には Bun を使い、Node.js で動作するバイナリを作成します。
主な機能
- 01Bun を中心としたツールチェーン
- 02CLI プロジェクトの雛形作成
- 03引数とサブコマンドの解析
- 04CLI とロジックの分離
- 05テストと公開手順の設定
活用シーン
- 01npx CLI の新規作成
- 02既存ライブラリへの CLI 追加
- 03引数解析とターミナル操作の設定
- 04CLI パッケージの 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 を主なランタイムおよびツールチェーンとして使用し、すべての npm/npx ユーザー(Node.js ランタイム)で動作するバイナリを生成して、npx で実行可能なコマンドラインツールをビルドして公開します。
このスキルを使用する場合
以下の場合に使用します:
- CLI ツールをゼロから作成する
- npx で実行可能なバイナリをビルドする
- CLI の引数解析、サブコマンド、またはターミナル UX を設定する
- CLI ツールを npm に公開する
- 既存のライブラリパッケージに CLI を追加する
以下の場合は使用しないでください:
- CLI のないライブラリをビルドする(
npm-packageスキルを使用してください) - アプリケーションをビルドする(公開するパッケージではないもの)
- モノレポで作業する(このスキルは単一パッケージのリポジトリを対象としています)
ツールチェーン
| 項目 | ツール | 理由 |
|---|---|---|
| ランタイム/パッケージマネージャー | Bun | インストール、実行、トランスパイルが高速 |
| バンドラー | Bunup | Bun ネイティブ、デュアルエントリー(ライブラリ+CLI)、.d.ts |
| 引数解析 | citty | 約 3KB、TypeScript ネイティブ、自動ヘルプ、runMain() |
| ターミナルの色付け | picocolors | 約 7KB、CJS+ESM、自動検出 |
| TypeScript | module: "nodenext"、strict: true +追加設定 | 最大限の正確性 |
| フォーマット+基本的なリンティング | Biome v2 | 高速で、ツールが 1 つで済む |
| 型を考慮したリンティング | ESLint + typescript-eslint | 高度な型安全性 |
| テスト | Vitest | 分離、モック、カバレッジ |
| バージョン管理 | Changesets | ファイルベース、明示的 |
| 公開 | npm publish --provenance | Trusted Publishing/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 を更新して、両方のエントリポイントを含めます
- 既存の
binと並べて、package.json に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が使用するのはNODE_AUTH_TOKENではなくNPM_CONFIG_TOKENです。- 公開パッケージでは
#!/usr/bin/env bunを使用しないでください。 ユーザーは Bun をインストールしていません。 - Bunup の
bannerはライブラリのエントリを含む、すべての出力ファイルに shebang を追加します。 問題になる場合は、ビルド後スクリプトを使ってdist/cli.jsにだけ shebang を追加してください。