citty の使い方

Elegant CLI Builder

v0.2.2/週MITCLI
AI生成コンテンツ

この記事はAIによって生成されました。内容の正確性は保証されません。最新の情報は公式ドキュメントをご確認ください。

citty の使い方 — Node.js向けエレガントなCLIビルダー

一言でいうと

citty は、ゼロ依存・軽量でありながら、サブコマンド・プラグイン・自動ヘルプ生成などを備えた、モダンなCLIアプリケーションビルダーです。Node.js 標準の util.parseArgs をベースにしており、TypeScript との親和性も高く設計されています。

どんな時に使う?

  • 自作のCLIツールを素早く構築したい時defineCommandrunMain だけで、引数パース・ヘルプ表示・エラーハンドリングが揃います
  • サブコマンドを持つ複雑なCLIを作りたい時 — ネストされたサブコマンドを遅延読み込み(lazy import)で効率的に構成できます
  • Nuxt / UnJS エコシステムの中で統一的にCLIを作りたい時 — UnJS プロジェクトの一部として開発されており、同エコシステムのツールと相性が良いです

インストール

# npm
npm install citty

# yarn
yarn add citty

# pnpm
pnpm add citty

開発用CLIツールとして使う場合は -D フラグを付けてください。

基本的な使い方

最もシンプルなCLIアプリケーションの例です。

// cli.ts
import { defineCommand, runMain } from "citty";

const main = defineCommand({
  meta: {
    name: "greet",
    version: "1.0.0",
    description: "A simple greeting CLI",
  },
  args: {
    name: {
      type: "positional",
      description: "Your name",
      required: true,
    },
    friendly: {
      type: "boolean",
      description: "Use friendly greeting",
      alias: ["f"],
    },
    greeting: {
      type: "string",
      description: "Custom greeting message",
      default: "Hello",
    },
  },
  run({ args }) {
    const prefix = args.friendly ? "Hey" : args.greeting;
    console.log(`${prefix}, ${args.name}!`);
  },
});

runMain(main);
# 基本実行
npx tsx cli.ts John
# Hello, John!

# フラグ付き
npx tsx cli.ts John --friendly
# Hey, John!

# カスタム挨拶
npx tsx cli.ts John --greeting "Good morning"
# Good morning, John!

# 自動生成されるヘルプ
npx tsx cli.ts --help

よく使うAPI

1. defineCommand — コマンド定義

CLIコマンドを型安全に定義するための関数です。args で定義した引数は run の中で型推論が効きます。

import { defineCommand } from "citty";

const cmd = defineCommand({
  meta: {
    name: "build",
    version: "2.0.0",
    description: "Build the project",
  },
  args: {
    outDir: {
      type: "string",
      description: "Output directory",
      default: "./dist",
      valueHint: "dir",
    },
    minify: {
      type: "boolean",
      description: "Enable minification",
      default: true,
    },
    target: {
      type: "enum",
      description: "Build target",
      options: ["es2020", "es2022", "esnext"],
      default: "es2022",
    },
  },
  run({ args }) {
    // args.outDir: string, args.minify: boolean, args.target: "es2020" | "es2022" | "esnext"
    console.log(`Building to ${args.outDir} (target: ${args.target}, minify: ${args.minify})`);
  },
});

2. runMain — メインコマンドの実行

--help / -h--version / -v の自動処理、エラー時のグレースフルな終了を含むエントリーポイントです。

import { defineCommand, runMain } from "citty";

const main = defineCommand({
  meta: { name: "mycli", version: "1.0.0", description: "My CLI tool" },
  args: {
    verbose: { type: "boolean", description: "Enable verbose output" },
  },
  run({ args }) {
    if (args.verbose) {
      console.log("Verbose mode enabled");
    }
    console.log("Running...");
  },
});

// process.argv を自動的にパースして実行
runMain(main);

3. サブコマンド(subCommands)と遅延読み込み

ネストされたサブコマンドを定義できます。大規模CLIでは遅延読み込みにより、実行されるコマンドだけをインポートできます。

import { defineCommand, runMain } from "citty";

// サブコマンドを個別に定義
const installCmd = defineCommand({
  meta: {
    name: "install",
    description: "Install dependencies",
    alias: ["i", "add"], // エイリアスでもアクセス可能
  },
  args: {
    pkg: { type: "positional", description: "Package name", required: true },
    dev: { type: "boolean", description: "Install as devDependency", alias: ["D"] },
  },
  run({ args }) {
    const flag = args.dev ? "(dev) " : "";
    console.log(`${flag}Installing ${args.pkg}...`);
  },
});

const main = defineCommand({
  meta: { name: "pkg-manager", version: "1.0.0", description: "Package manager CLI" },
  subCommands: {
    install: installCmd,
    // 遅延読み込み: 実行時にのみインポートされる
    remove: () => import("./commands/remove").then((m) => m.default),
    // 非表示のサブコマンド(ヘルプに表示されない)
    // meta.hidden: true を設定
  },
});

runMain(main);
npx tsx cli.ts install lodash --dev
# (dev) Installing lodash...

npx tsx cli.ts i lodash -D
# エイリアスでも同じ動作

4. defineCittyPlugin — プラグイン定義

再利用可能な setup / cleanup フックをプラグインとして切り出せます。

import { defineCommand, defineCittyPlugin, runMain } from "citty";

// タイマープラグイン: コマンドの実行時間を計測
const timerPlugin = defineCittyPlugin({
  name: "timer",
  setup() {
    console.time("execution");
  },
  cleanup() {
    console.timeEnd("execution");
  },
});

// ロガープラグイン
const loggerPlugin = defineCittyPlugin({
  name: "logger",
  setup({ args }) {
    console.log(`[LOG] Command started with args:`, args);
  },
  cleanup() {
    console.log("[LOG] Command finished");
  },
});

const main = defineCommand({
  meta: { name: "app", description: "App with plugins" },
  plugins: [timerPlugin, loggerPlugin],
  run() {
    console.log("Doing work...");
  },
});

runMain(main);
// [LOG] Command started with args: { ... }
// Doing work...
// [LOG] Command finished
// execution: 1.234ms

プラグインの setup はコマンドの setup より前に(定義順で)実行され、cleanup はコマンドの cleanup より後に(逆順で)実行されます。

5. parseArgs / renderUsage — ユーティリティ関数

引数のパースやヘルプ文字列の生成を個別に利用できます。

import { parseArgs, renderUsage, defineCommand } from "citty";

// 引数を手動でパース
const argsDef = {
  name: { type: "string" as const, description: "User name", required: true },
  verbose: { type: "boolean" as const, description: "Verbose output" },
};

const parsed = parseArgs(["--name", "Alice", "--verbose"], argsDef);
console.log(parsed);
// { name: "Alice", verbose: true }

// コマンドのヘルプ文字列を取得(テストやカスタム表示に便利)
const cmd = defineCommand({
  meta: { name: "demo", version: "1.0.0", description: "Demo CLI" },
  args: {
    file: { type: "positional", description: "Input file", required: true },
    output: { type: "string", description: "Output path", alias: ["o"], valueHint: "path" },
  },
  run() {},
});

const usage = await renderUsage(cmd);
console.log(usage);

類似パッケージとの比較

特徴cittycommanderyargscacclipanion
依存関係ゼロゼロ多数ゼロ少数
バンドルサイズ (minzip)~3KB~10KB~70KB~4KB~15KB
TypeScript サポート◎(型推論あり)◎(デコレータ)
サブコマンド◎(遅延読み込み対応)
プラグインシステム××××
自動ヘルプ生成
ベースutil.parseArgs独自実装独自実装独自実装独自実装
エコシステムUnJS最大シェア広く普及Vite系Yarn

選定の指針:

  • 軽量さ・モダンさ重視 → citty
  • 実績・ドキュメント・コミュニティの大きさ重視 → commander / yargs
  • Vite プラグインなど小規模CLI → cac
  • 厳密な型安全性をデコレータで → clipanion

注意点・Tips

ケバブケースとキャメルケースの両方でアクセス可能

引数名をケバブケース(output-dir)で定義しても、キャメルケース(outputDir)でアクセスできます。

args: {
  "output-dir": { type: "string", description: "Output directory" },
},
run({ args }) {
  // どちらでもOK
  console.log(args["output-dir"]);
  console.log(args.outputDir);
},

Boolean の --no- プレフィックス

boolean 型の引数は自動的に --no- プレフィックスによる否定をサポートします。default: true に設定している場合や negativeDescription を指定した場合、ヘルプにも否定形が表示されます。

args: {
  color: {
    type: "boolean",
    description: "Enable colored output",
    default: true,
  },
},
mycli --no-color  # args.color === false

組み込みフラグとの競合に注意

--help / -h--version / -v は自動的に処理されますが、同名の引数やエイリアスを自分で定義すると、組み込みの動作は無効化されます。意図しない場合は名前の衝突を避けてください。

cleanup は必ず実行される

run() が例外をスローしても cleanup は必ず呼ばれます。データベース接続のクローズや一時ファイルの削除など、確実に後処理が必要な場面で活用してください。

defineCommand({
  setup() {
    // DB接続の確立など
  },
  cleanup() {
    // run() が失敗しても必ず実行される
    // DB接続のクローズなど
  },
  run() {
    throw new Error("Something went wrong");
  },
});

Resolvable<T> による動的解決

metaargssubCommands はすべて Resolvable<T> 型を受け付けます。つまり、値そのもの・Promise・関数・async 関数のいずれでも渡せます。設定ファイルの読み込み結果に応じて引数を動的に変えるといった高度なパターンが可能です。

defineCommand({
  meta: async () => {
    const pkg = await import("./package.json");
    return { name: pkg.name, version: pkg.version, description: pkg.description };
  },
  args: () => ({
    port: { type: "string", description: "Port number", default: process.env.PORT || "3000" },
  }),
  run({ args }) {
    console.log(`Starting on port ${args.port}`);
  },
});

まとめ

citty は、ゼロ依存・軽量でありながら、サブコマンドの遅延読み込み・プラグインシステム・TypeScript 型推論といったモダンな機能を備えたCLIビルダーです。UnJS エコシステムの一部として活発にメンテナンスされており、小〜中規模のCLIツール構築に最適な選択肢と言えます。commander や yargs からの移行先としても、そのシンプルなAPIと軽量さは大きな魅力です。