cli-framework-oclif-ink について
cli-framework-oclif-inkは、oclifでTypeScript製CLIを、Inkで対話型ターミナルUIを構築するためのスキルです。両者の連携、プラグイン、ライフサイクル管理、テストを扱います。
主な機能
- 01oclifのコマンド、フラグ、引数
- 02ReactベースのInkターミナルUI
- 03対話型ウィザードと進捗表示
- 04プラグイン構成とライフサイクルフック
- 05コマンドとコンポーネントのテスト
活用シーン
- 01複数コマンドのCLI構築
- 02ターミナルのウィザードやダッシュボード追加
- 03oclifコマンドとInk UIの連携
- 04CLI出力とキー操作のテスト
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ agents-inc/ skills --skill cli-framework-oclif-inkインストール後、Claude Code で/cli-framework-oclif-inkを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
metadata.yamlexamples/advanced.mdcore.mdtesting.md詳細
oclif + Ink CLI のパターン
クイックガイド: コマンドのルーティング、フラグ/引数の解析、プラグインアーキテクチャには oclif を使用します。Flexbox レイアウトを備えた React ベースの対話型ターミナル UI には Ink を使用します。コマンドに豊富な状態管理機能を持つインターフェースが必要な場合は、両方を組み合わせます。oclif コマンドから Ink を描画する場合は、必ず
await waitUntilExit()を使用してください。JSON 出力モードを維持するには、console.logではなくthis.log()を使用します。
<critical_requirements>
重要:このスキルを使用する前に
すべてのコードは、プロジェクトの CLAUDE.md に記載された規約(kebab-case、名前付きエクスポート、インポート順序、
import type、名前付き定数)に従う必要があります。
(oclif コマンドでは、render() の後に必ず await waitUntilExit() を使用してください。使用しないと、UI の処理が完了する前にプロセスが終了します。)
(コマンドでは、必ず this.log()/this.warn()/this.error() を使用してください。console.log を使うと、--json モードとテストの出力キャプチャが機能しなくなります。)
(Ink では、すべてのテキストを <Text> コンポーネントで囲む必要があります。文字列をそのまま配置すると、レンダリングエラーが発生します。)
(非同期処理をキャンセルするには、必ず useEffect のクリーンアップを使用してください。ユーザーが Ctrl+C を押すと Ink コンポーネントがアンマウントされます。)
</critical_requirements>
自動検出: oclif、@oclif/core、@oclif/test、Ink、ink、@inkjs/ui、Command クラス、Flags、Args、useInput、useApp、useFocus、render()、waitUntilExit、ターミナル UI、CLI コマンド、ink-testing-library
使用する場面:
- フラグ/引数の解析機能を備えた複数コマンドの CLI を構築する場合
- 対話型ターミナル UI(ウィザード、ダッシュボード、進捗表示)を作成する場合
- コマンドルーティングと、React ベースの高機能なインターフェースを組み合わせる場合
- プラグインで拡張可能な CLI アーキテクチャを構築する場合
使用しない場面:
- 単発のシンプルなスクリプト(プレーンな Node.js で十分な場合)
- 基本的なプロンプトだけが必要な場合(軽量なプロンプトライブラリで十分な場合)
- 起動時間が 100ms 未満であることが重要な場合(oclif により約 200ms のオーバーヘッドが発生します)
対象となる主なパターン:
- 型付きフラグ、引数、出力メソッドを備えた oclif コマンドの構成
- Ink コンポーネント、Flexbox レイアウト、キーボード入力、フォーカス管理
- 統合:ライフサイクルを管理しながら、oclif コマンドから Ink を描画する方法
- @inkjs/ui の組み込みコンポーネント(Select、TextInput、Spinner など)
- プラグインアーキテクチャとライフサイクルフック
- 複数ステップのウィザード、進捗インジケーター、キャンセル可能な処理
@oclif/testを使ったコマンドのテストと、ink-testing-libraryを使ったコンポーネントのテスト
基本方針
oclif と Ink は、それぞれ異なる問題を解決します。oclif は、コマンドルーティング、フラグ解析、ヘルプ生成、プラグイン検出、自動更新といった、面倒ですが重要な処理を担います。Ink は、React のコンポーネントモデルと Flexbox レイアウトを使った、状態を持つ対話型ターミナル UI を担います。
コマンドが処理を実行して出力するだけなら、oclif のみを使用します。ウィザード、ダッシュボード、進捗表示など、リアルタイムのユーザー操作が必要なコマンドには、Ink を追加します。統合方法はシンプルです。oclif コマンドの run() から render() を呼び出し、waitUntilExit() を待機します。
主なアーキテクチャ上の決定事項:
- コマンドは
.tsファイル(.tsxではありません)とし、Ink コンポーネントは別の.tsxファイルからインポートします - プロセスのライフサイクルは oclif が管理し、その中で UI のライフサイクルを Ink が管理します
- キーボード入力の処理は oclif コマンドではなく、Ink コンポーネント内の
useInputで行います - 複雑な Ink UI の状態管理には、外部ストアを使用します(props のバケツリレーは避けます)
基本パターン
パターン 1:型付きフラグと引数を備えた oclif コマンド
コマンドでは、メタデータとフラグ/引数の定義に静的プロパティを使用します。run() メソッドは非同期で、JSON 出力に対応するために型付きデータを返します。
import { Command, Flags, Args } from "@oclif/core";
const DEFAULT_RETRIES = 3;
export class Deploy extends Command {
static summary = "Deploy to target environment";
static enableJsonFlag = true; // Adds --json flag
static flags = {
env: Flags.string({
char: "e",
required: true,
options: ["staging", "production"] as const,
}),
retries: Flags.integer({
char: "r",
default: DEFAULT_RETRIES,
min: 0,
max: 10,
}),
verbose: Flags.boolean({ char: "v", default: false, allowNo: true }),
apiKey: Flags.string({ env: "MY_CLI_API_KEY" }), // From env var
};
static args = {
target: Args.string({ description: "Deploy target", required: true }),
};
async run(): Promise<{ status: string }> {
const { args, flags } = await this.parse(Deploy);
// Use this.log, this.warn, this.error -- never console.*
this.log(`Deploying ${args.target} to ${flags.env}`);
return { status: "deployed" };
}
}
フラグの型、引数、出力メソッド、エラー処理の詳細は、examples/core.md のパターン 1~5 を参照してください。
パターン 2:キーボード入力を処理する Ink コンポーネント
Ink コンポーネントは、入力、アプリのライフサイクル、フォーカスのためにフックを使用する React の関数コンポーネントです。
import React, { useState } from "react";
import { Box, Text, useInput, useApp } from "ink";
interface SelectorProps {
items: string[];
onSelect: (item: string) => void;
}
export const Selector: React.FC<SelectorProps> = ({ items, onSelect }) => {
const [index, setIndex] = useState(0);
const { exit } = useApp();
useInput((input, key) => {
if (key.upArrow) setIndex((i) => Math.max(0, i - 1));
if (key.downArrow) setIndex((i) => Math.min(items.length - 1, i + 1));
if (key.return) onSelect(items[index]);
if (input === "q") exit();
});
return (
<Box flexDirection="column">
{items.map((item, i) => (
<Text key={item} bold={i === index}>
{i === index ? "> " : " "}
{item}
</Text>
))}
</Box>
);
};
スタイル設定、レイアウト、@inkjs/ui コンポーネントの詳細は、examples/core.md のパターン 6~8 を参照してください。
パターン 3:oclif コマンドから Ink をレンダリングする
統合パターン:oclif コマンドで Ink コンポーネントをレンダリングし、完了を待機します。
import { Command, Flags } from "@oclif/core";
import { render } from "ink";
import React from "react";
import { SetupWizard } from "../components/setup-wizard.js";
export class Init extends Command {
static summary = "Initialize a new project";
static flags = {
yes: Flags.boolean({ char: "y", description: "Use defaults", default: false }),
};
async run(): Promise<void> {
const { flags } = await this.parse(Init);
if (flags.yes) {
this.log("Initialized with defaults.");
return;
}
// CRITICAL: Destructure waitUntilExit and await it
const { waitUntilExit } = render(<SetupWizard />);
await waitUntilExit();
}
}
非対話型のフォールバックを含む統合パターン全体については、examples/core.md のパターン 9 を参照してください。
パターン 4:複数ステップのウィザード
ウィザードでは、戻る/進むナビゲーションとデータの蓄積を、ステップベースの状態で管理します。
const MultiStepWizard: React.FC<WizardProps> = ({ steps, onComplete }) => {
const [currentIndex, setCurrentIndex] = useState(0);
const [data, setData] = useState<Record<string, unknown>>({});
const handleNext = (stepData: Record<string, unknown>) => {
const merged = { ...data, ...stepData };
setData(merged);
if (currentIndex === steps.length - 1) onComplete(merged);
else setCurrentIndex((i) => i + 1);
};
const handleBack = () => setCurrentIndex((i) => Math.max(0, i - 1));
// Render steps[currentIndex].component with {onNext, onBack, data} props
};
ナビゲーションを含むウィザードの実装全体については、examples/advanced.md のパターン 1~2 を参照してください。
パターン 5:プラグインアーキテクチャ
oclif プラグインは、独自のコマンドとフックを持つ npm パッケージです。ホスト CLI は package.json にプラグインを登録します。
{
"oclif": {
"plugins": [
"@oclif/plugin-help",
"@oclif/plugin-autocomplete",
"@myorg/cli-plugin-analytics"
]
}
}
プラグインの作成とユーザーがインストール可能なプラグインのサポートについては、examples/advanced.md のパターン 4 を参照してください。
パターン 6:コマンドとコンポーネントのテスト
コマンドのテスト(フラグ、引数、出力、エラー)には @oclif/test を、Ink コンポーネントのテスト(レンダリング、キーボードシミュレーション)には ink-testing-library を使用します。
// Command test
import { runCommand } from "@oclif/test";
const { stdout, error } = await runCommand(["deploy", "--env", "staging", "app"]);
expect(stdout).toContain("Deploying");
// Ink component test
import { render } from "ink-testing-library";
const { lastFrame, stdin } = render(<Selector items={["a", "b"]} onSelect={fn} />);
stdin.write("\u001B[B"); // Down arrow
stdin.write("\r"); // Enter
expect(fn).toHaveBeenCalledWith("b");
非同期処理、モック、スナップショットテストを含むテストパターン全体については、examples/testing.md を参照してください。
<decision_framework>
判断フレームワーク
Building a CLI?
|
+-> Need multiple commands / subcommands?
| +-> YES -> oclif (multi-command mode)
| +-> NO -> oclif (single-command mode) or plain Node.js
|
+-> Need interactive terminal UI?
| +-> Simple prompts (name, confirm)? -> Lightweight prompt library
| +-> Complex stateful UI (wizard, dashboard)? -> Ink
|
+-> Need both routing AND complex UI?
+-> YES -> oclif commands + Ink components
+-> NO -> Use whichever fits the primary need
コマンドファイルの構成
src/
commands/ # oclif command classes (.ts files)
init.ts
config/
get.ts # mycli config get <key>
set.ts # mycli config set <key> <value>
components/ # Ink React components (.tsx files)
wizard.tsx
progress.tsx
hooks/ # oclif lifecycle hooks
init.ts # Runs before every command
postrun.ts # Runs after every command
lib/ # Shared utilities
</decision_framework>
詳細なリソース:
- examples/core.md -- コマンド、フラグ、引数、Ink コンポーネント、統合
- examples/advanced.md -- ウィザード、進捗状況、プラグイン、フック、エラーバウンダリ
- examples/testing.md -- コマンドテスト、コンポーネントテスト、非同期テスト
<red_flags>
要注意事項
優先度:高
await waitUntilExit()の不足 -- Ink UI が完了する前にコマンドが終了し、ユーザーには何も表示されません- コマンドで
console.logを使用する ----json出力モードが壊れ、@oclif/testでも出力を取得できません - Ink 内で文字列をそのまま使用する -- すべてのテキストを
<Text>で囲む必要があります。囲まないとレンダリングに失敗します - レンダリングループをブロックする -- コンポーネント内の同期処理によって、ターミナル UI がフリーズします
優先度:中
.tsxファイルをコマンドとして使用する -- oclif は.tsxファイルを自動検出しません。.tsxコンポーネントをインポートする.tsコマンドファイルを使用してください- Ctrl+C の処理がない --
useInputまたはuseApp().exit()を使って、必ず終了手段を用意してください - useEffect にクリーンアップがない -- 終了後の状態更新を避けるため、アンマウント時に非同期処理をキャンセルする必要があります
useInputフックが競合する -- 複数の有効なuseInputフックが同時に実行されます。適用範囲を限定するにはisActiveオプションを使用してください
注意点とエッジケース:
- oclif のフックは順番ではなく並列で実行されます。フック間の実行順序に依存しないでください
useInputは貼り付けたテキストに対して一度だけ実行され、文字ごとには実行されません。複数文字の入力文字列を明示的に処理してください- Ink v5 には React 18 以降、Ink v6 には React 19 以降が必要です。使用している Ink バージョンのピア依存関係を確認してください
enableJsonFlagを設定すると、run()の戻り値が JSON 出力になります。戻り値の型が利用側の想定と一致するようにしてください- oclif の
this.error()は例外をスローし、プロセスを終了します。値を返すことはありません
</red_flags>
<critical_reminders>
重要な注意事項
すべてのコードは CLAUDE.md のプロジェクト規約に従う必要があります(kebab-case、名前付きエクスポート、インポート順序、
import type、名前付き定数)
(oclif コマンドでは render() の後に必ず await waitUntilExit() を呼び出してください。呼び出さないと、UI が完了する前にプロセスが終了します)
(コマンドでは必ず this.log()/this.warn()/this.error() を使用してください。console.log は --json モードとテストでの出力取得を壊します)
(Ink ではすべてのテキストを <Text> コンポーネントで囲んでください。文字列をそのまま使用するとレンダリングエラーが発生します)
(非同期処理をキャンセルするため、必ず useEffect のクリーンアップを使用してください。ユーザーが Ctrl+C を押すと Ink コンポーネントはアンマウントされます)
これらのルールに従わないと、プロセスが予告なく終了したり、JSON 出力が壊れたり、ターミナルのレンダリングがクラッシュしたりします。
</critical_reminders>