ターミナルをスクリプトの「ゴミ捨て場」にしないために
DevOpsエンジニアやバックエンドエンジニアであれば、データベースのクリーンアップやシステムのヘルスチェックなど、繰り返しの作業には慣れているはずです。通常、数行のシェルスクリプトを書いたり、長いコマンドをコピー&ペーストしたりすることが多いでしょう。また、NeovimをプロフェッショナルなIDEに構築して作業効率を高めていても、バラバラなスクリプト管理は生産性を下げてしまいます。この方法は最初は手軽ですが、長期的には管理が非常に困難になります。
以前、私はよく node script.js というコマンドでスクリプトを実行していました。しかし、引数が増えるにつれて、process.argv を手動で処理するのはまさに災難でした。特にNode.jsのスループットを最大化する必要があるツールを開発する場合、コマンド構造を管理し、ヘルプドキュメントを自動化するためには、本格的なフレームワークが必要です。
Node.jsエコシステムでCLIを構築する3つの道
以下は、私がこれまでのプロジェクトで試してきた3つの一般的な手法です:
1. process.argv を使用する (Vanilla Node.js)
これは、Node.jsのデフォルトの引数配列をパースする最も原始的なアプローチです。
- メリット: ライブラリをインストールせずにすぐに実行可能。20行以下の個人用スクリプトに適しています。
- デメリット: エラーハンドリングやヘルプ画面のロジックを自作する必要があります。引数が3つ以上になると、コードが非常に煩雑になります。
2. Commander.js または Yargs
これら2つのライブラリは、Node.jsコミュニティにおける定番の選択肢です。
- メリット: フラグ(
-f,--force)の処理が非常にスムーズ。見栄えの良いヘルプページを自動生成します。 - デメリット: CLIが数十のサブコマンドを持つようになると、メインファイルが肥大化し、メンテナンスが極めて困難になります。
3. Oclif (Open CLI Framework)
これはSalesforce社製の「重装備」なフレームワークで、Heroku CLIの構築にも使われています。単なるライブラリとは異なり、Oclifは厳格なディレクトリ構造を提供します。
- メリット: デフォルトでTypeScriptをサポート。ファイルベースでコマンドを自動ロードし、非常に強力なテストシステムを備えています。
- デメリット: 構造に慣れるまでに、最初に30分ほど時間がかかります。
なぜ実務プロジェクトでOclifが最良の選択なのか?
最近のあるプロジェクトで、私たちのチームは5人の開発者をサポートするために itfz-cli というツールを構築しました。複雑なDockerやAWSのコマンドをいくつも覚える代わりに、開発者は itfz-cli deploy --stage=staging と入力するだけで済むようになりました。その結果、新しい環境の構築時間は15分から30秒未満に短縮されました。
Oclifはスケーラビリティ(拡張性)の問題を解決してくれます。各コマンドは src/commands ディレクトリ内の独立したファイルです。機能を追加したいときは、既存の安定したロジックを壊す心配をすることなく、新しいファイルを作成するだけです。
最初のCLIの実装に取り掛かる
ステップ1:プロジェクトの初期化
グローバルなジェネレーターをインストールする手間を省くため、npx を使用して素早く初期化します:
npx oclif generate my-cli
私は TypeScript を選択することをお勧めします。TypeScriptプロジェクトのバンドルサイズを削減する手法を意識しつつ、型定義のヒントを活用することで、ユーザーからの引数を処理する際の初歩的なミスを防ぐことができます。
ステップ2:標準的なディレクトリ構造
インストール後、プロジェクト構造が非常に整理されていることがわかります:
my-cli/
├── src/
│ └── commands/
│ └── check/index.ts (コマンド: my-cli check)
├── package.json
└── tsconfig.json
src/commands/ 内の各ファイルが1つのコマンドに対応します。例えば、src/commands/db/migrate.ts というファイルは、自動的に my-cli db:migrate というコマンドを作成します。
ステップ3:コマンドのロジックを書く
src/commands/check.ts にウェブサイトのステータスを確認するツールを作成してみましょう。より本格的なHurlによるAPIテストと監視のような機能を自作のCLIに統合することも可能です。
import {Command, Flags} from '@oclif/core'
import axios from 'axios'
export default class CheckStatus extends Command {
static description = 'ウェブサイトが稼働中かどうかを確認する'
static flags = {
timeout: Flags.integer({char: 't', description: 'タイムアウト時間 (ms)', default: 3000}),
}
static args = [{name: 'url', description: '確認するウェブサイトのURL', required: true}]
public async run(): Promise<void> {
const {args, flags} = await this.parse(CheckStatus)
this.log(`🚀 接続中: ${args.url}...`)
try {
const start = Date.now()
await axios.get(args.url, {timeout: flags.timeout})
this.log(`✅ OK! レスポンス時間: ${Date.now() - start}ms`)
} catch (error) {
this.error(`❌ エラー: ${error.message}`)
}
}
}
クイック解説:
- static flags:
--timeoutのようなハイフン付きのオプションを定義します。 - static args: URLのように直接渡される引数です。
- this.log / this.error: システムのデータフローを壊さずにターミナルに情報を出力するための標準的な方法です。
ステップ4:試運転とインストール
開発中は、次のコマンドで素早くテストできます:
./bin/run.js check https://google.com
このツールを(ls や cd のように)システムコマンドとして使用するには、npm link を使用します。その後、PC上のどのディレクトリからでも my-cli check ... と入力できるようになります。
複雑なロジックを処理するコツ
CLIが大きくなるにつれて、複数のコマンドでコードの重複が発生します。解決策は Base Class を使用することです。データベース接続やログ出力を一元管理する BaseCommand クラスを作成し、他のコマンドにそれを継承させます。
// src/base.ts
import {Command} from '@oclif/core'
export abstract class BaseCommand extends Command {
async init() {
this.log('--- システム接続を設定中 ---')
}
async finally(err: Error | undefined) {
this.log('--- 接続を安全に閉じました ---')
return super.finally(err)
}
}
おわりに
CLIの構築は単にコードを書くことではなく、自分自身や同僚のための作業エクスペリエンスを設計することです。これは、VS Code拡張を自作して開発環境を改善するプロセスにも通じる、非常にやりがいのあるエンジニアリングです。
もしあなたのチームが、いまだにバラバラに実行される大量の .sh や .js ファイルに苦労しているなら、ある日の午後を使ってOclifでそれらを再整理してみてください.作業効率の向上に、きっと驚くはずです。

