mcpField
C
cli-framework-oclif-ink

cli-framework-oclif-ink

github.com/agents-inc開発2646 回インストール2 日前に更新
リポジトリ

cli-framework-oclif-ink について

cli-framework-oclif-inkは、oclifでTypeScript製CLIを、Inkで対話型ターミナルUIを構築するためのスキルです。両者の連携、プラグイン、ライフサイクル管理、テストを扱います。

主な機能

  1. 01oclifのコマンド、フラグ、引数
  2. 02ReactベースのInkターミナルUI
  3. 03対話型ウィザードと進捗表示
  4. 04プラグイン構成とライフサイクルフック
  5. 05コマンドとコンポーネントのテスト

活用シーン

  1. 01複数コマンドのCLI構築
  2. 02ターミナルのウィザードやダッシュボード追加
  3. 03oclifコマンドとInk UIの連携
  4. 04CLI出力とキー操作のテスト

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/agents-inc/skills --skill cli-framework-oclif-ink

インストール後、Claude Code で/cli-framework-oclif-inkを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

metadata.yaml
examples/
advanced.md
core.md
testing.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>

コメント