cli-framework-oclif-ink
oclifのコマンドフレームワークとReactベースのInkによるターミナル描画を組み合わせ、モダンなCLI(コマンドラインインターフェース)開発を効率的に行うSkill。
📜 元の英語説明(参考)
Modern CLI development combining oclif's command framework with Ink's React-based terminal rendering
🇯🇵 日本人クリエイター向け解説
oclifのコマンドフレームワークとReactベースのInkによるターミナル描画を組み合わせ、モダンなCLI(コマンドラインインターフェース)開発を効率的に行うSkill。
※ jpskill.com 編集部が日本のビジネス現場向けに補足した解説です。Skill本体の挙動とは独立した参考情報です。
下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。
mkdir -p ~/.claude/skills && cd ~/.claude/skills && curl -L -o cli-framework-oclif-ink.zip https://jpskill.com/download/10250.zip && unzip -o cli-framework-oclif-ink.zip && rm cli-framework-oclif-ink.zip
$d = "$env:USERPROFILE\.claude\skills"; ni -Force -ItemType Directory $d | Out-Null; iwr https://jpskill.com/download/10250.zip -OutFile "$d\cli-framework-oclif-ink.zip"; Expand-Archive "$d\cli-framework-oclif-ink.zip" -DestinationPath $d -Force; ri "$d\cli-framework-oclif-ink.zip"
完了後、Claude Code を再起動 → 普通に「動画プロンプト作って」のように話しかけるだけで自動発動します。
💾 手動でダウンロードしたい(コマンドが難しい人向け)
- 1. 下の青いボタンを押して
cli-framework-oclif-ink.zipをダウンロード - 2. ZIPファイルをダブルクリックで解凍 →
cli-framework-oclif-inkフォルダができる - 3. そのフォルダを
C:\Users\あなたの名前\.claude\skills\(Win)または~/.claude/skills/(Mac)へ移動 - 4. Claude Code を再起動
⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。
🎯 このSkillでできること
下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。
📦 インストール方法 (3ステップ)
- 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
- 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
- 3. 展開してできたフォルダを、ホームフォルダの
.claude/skills/に置く- · macOS / Linux:
~/.claude/skills/ - · Windows:
%USERPROFILE%\.claude\skills\
- · macOS / Linux:
Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。
詳しい使い方ガイドを見る →- 最終更新
- 2026-05-18
- 取得日時
- 2026-05-18
- 同梱ファイル
- 1
📖 Skill本文(日本語訳)
※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。
oclif + Ink CLI パターンの紹介
クイックガイド: コマンドのルーティング、フラグ/引数の解析、およびプラグインアーキテクチャには oclif を使用します。Flexbox レイアウトによる React ベースのインタラクティブなターミナル UI には Ink を使用します。コマンドが豊富なステートフルインターフェースを必要とする場合は、両方を組み合わせます。oclif コマンドから Ink をレンダリングするときは、常に
await waitUntilExit()を使用してください。JSON 出力モードを維持するには、console.logの代わりにthis.log()を使用してください。
<critical_requirements>
重要な注意点: この Skill を使用する前に
すべてのコードは、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, terminal UI, CLI command, 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を使用したコンポーネントのテスト
<philosophy>
哲学
oclif と Ink は、直交する問題を解決します。oclif は、退屈だが重要な部分を処理します。コマンドルーティング、フラグ解析、ヘルプ生成、プラグイン検出、自動更新です。Ink は、インタラクティブな部分を処理します。Flexbox レイアウトによる React のコンポーネントモデルを使用したステートフルなターミナル UI です。
コマンドがその処理を行い、出力を印刷する場合は、oclif のみを使用します。コマンドがリアルタイムのユーザーインタラクション (ウィザード、ダッシュボード、進捗) を必要とする場合は、Ink を追加します。統合ポイントは簡単です。oclif コマンドの run() は render() を呼び出し、waitUntilExit() を待ちます。
主要なアーキテクチャ上の決定事項:
- コマンドは
.tsファイル (.tsxではない) です -- 別の.tsxファイルから Ink コンポーネントをインポートします - oclif はプロセスライフサイクルを処理します。Ink はその中の UI ライフサイクルを処理します
- キーボード処理は、oclif コマンドではなく、
useInputを介して Ink コンポーネントに存在します - 複雑な Ink UI の状態管理には、(プロップドリリングではなく) 外部ストアを使用する必要があります
</philosophy>
<patterns>
コアパターン
パターン 1: 型付きフラグと引数を持つ oclif コマンド
コマンドは、メタデータとフラグ/引数の定義に静的プロパティを使用します。run() メソッドは非同期であり、JSON 出力サポートのために型付きデータを返します。
import { Command, Flags, Args } from "@oclif/core";
const DEFAULT_RETRIES = 3;
export class Deploy extends Command {
static summary = "ターゲット環境へのデプロイ";
static enableJsonFlag = true; // --json フラグを追加
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" }), // 環境変数から
};
static args = {
target: Args.string({ description: "デプロイターゲット", required: true }),
};
async run(): Promise<{ status: string }> {
const { args, flags } = await this.parse(Deploy);
// this.log, this.warn, this.error を使用してください -- 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";
(原文はここで切り詰められています)
📜 原文 SKILL.md(Claudeが読む英語/中国語)を展開
oclif + Ink CLI Patterns
Quick Guide: Use oclif for command routing, flag/arg parsing, and plugin architecture. Use Ink for React-based interactive terminal UIs with Flexbox layout. Combine both when commands need rich stateful interfaces. Always
await waitUntilExit()when rendering Ink from oclif commands. Usethis.log()instead ofconsole.logto preserve JSON output mode.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST await waitUntilExit() after render() in oclif commands -- without it the process exits before the UI completes)
(You MUST use this.log() / this.warn() / this.error() in commands -- console.log breaks --json mode and test capture)
(You MUST wrap all text in <Text> components in Ink -- bare strings cause rendering errors)
(You MUST use useEffect cleanup to cancel async operations -- Ink components unmount when the user presses Ctrl+C)
</critical_requirements>
Auto-detection: oclif, @oclif/core, @oclif/test, Ink, ink, @inkjs/ui, Command class, Flags, Args, useInput, useApp, useFocus, render(), waitUntilExit, terminal UI, CLI command, ink-testing-library
When to use:
- Building multi-command CLIs with flag/arg parsing
- Creating interactive terminal UIs (wizards, dashboards, progress displays)
- Combining command routing with rich React-based interfaces
- Building plugin-extensible CLI architectures
When NOT to use:
- Simple one-off scripts (plain Node.js suffices)
- Basic prompts only (a lightweight prompt library suffices)
- Performance-critical startup under 100ms (oclif adds ~200ms overhead)
Key patterns covered:
- oclif command structure with typed flags, args, and output methods
- Ink components, Flexbox layout, keyboard input, and focus management
- Integration: rendering Ink from oclif commands with lifecycle management
- @inkjs/ui pre-built components (Select, TextInput, Spinner, etc.)
- Plugin architecture and lifecycle hooks
- Multi-step wizards, progress indicators, and cancelable operations
- Testing commands with
@oclif/testand components withink-testing-library
<philosophy>
Philosophy
oclif and Ink solve orthogonal problems. oclif handles the boring-but-critical parts: command routing, flag parsing, help generation, plugin discovery, auto-updates. Ink handles the interactive parts: stateful terminal UIs using React's component model with Flexbox layout.
Use oclif alone when commands do their work and print output. Add Ink when a command needs real-time user interaction (wizards, dashboards, progress). The integration point is simple: the oclif command's run() calls render() and awaits waitUntilExit().
Key architectural decisions:
- Commands are
.tsfiles (not.tsx) -- they import Ink components from separate.tsxfiles - oclif handles process lifecycle; Ink handles UI lifecycle within it
- Keyboard handling lives in Ink components via
useInput, not in oclif commands - State management for complex Ink UIs should use an external store (not prop drilling)
</philosophy>
<patterns>
Core Patterns
Pattern 1: oclif Command with Typed Flags and Args
Commands use static properties for metadata and flag/arg definitions. The run() method is async and returns typed data for JSON output support.
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" };
}
}
See examples/core.md Pattern 1-5 for complete flag types, args, output methods, and error handling.
Pattern 2: Ink Component with Keyboard Handling
Ink components are React functional components using hooks for input, app lifecycle, and focus.
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>
);
};
See examples/core.md Pattern 6-8 for styling, layout, and @inkjs/ui components.
Pattern 3: Rendering Ink from oclif Command
The integration pattern: oclif command renders an Ink component and awaits its completion.
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();
}
}
See examples/core.md Pattern 9 for the full integration pattern with non-interactive fallback.
Pattern 4: Multi-Step Wizard
Wizards use step-based state with back/forward navigation and data accumulation.
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
};
See examples/advanced.md Pattern 1-2 for complete wizard implementation with navigation.
Pattern 5: Plugin Architecture
oclif plugins are npm packages with their own commands and hooks. The host CLI registers plugins in package.json.
{
"oclif": {
"plugins": [
"@oclif/plugin-help",
"@oclif/plugin-autocomplete",
"@myorg/cli-plugin-analytics"
]
}
}
See examples/advanced.md Pattern 4 for creating plugins and user-installable plugin support.
Pattern 6: Testing Commands and Components
Use @oclif/test for command tests (flags, args, output, errors) and ink-testing-library for Ink component tests (rendering, keyboard simulation).
// 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");
See examples/testing.md for full testing patterns including async operations, mocking, and snapshot tests.
</patterns>
<decision_framework>
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
Command File Organization
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>
Detailed Resources:
- examples/core.md -- Commands, flags, args, Ink components, integration
- examples/advanced.md -- Wizards, progress, plugins, hooks, error boundaries
- examples/testing.md -- Command tests, component tests, async testing
<red_flags>
RED FLAGS
High Priority:
- Missing
await waitUntilExit()-- Command exits before Ink UI completes, user sees nothing - Using
console.login commands -- Breaks--jsonoutput mode and is not captured by@oclif/test - Bare strings in Ink -- All text must be wrapped in
<Text>or rendering fails - Blocking the render loop -- Synchronous work in components freezes the terminal UI
Medium Priority:
.tsxfiles as commands -- oclif does not auto-discover.tsxfiles; use.tscommand files that import.tsxcomponents- Missing Ctrl+C handling -- Always provide an exit mechanism via
useInputoruseApp().exit() - No cleanup in useEffect -- Async operations must be canceled on unmount to avoid state updates after exit
- Conflicting
useInputhooks -- Multiple activeuseInputhooks fire simultaneously; use theisActiveoption to scope them
Gotchas & Edge Cases:
- oclif hooks run in parallel, not sequence -- don't depend on execution order between hooks
useInputfires once for pasted text, not per-character -- handle multi-character input strings explicitly- Ink v5 requires React 18+, Ink v6 requires React 19+ -- check your Ink version's peer dependencies
enableJsonFlagmakesrun()return value the JSON output -- ensure the return type matches what consumers expect- oclif's
this.error()throws (exits the process) -- it does not return
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST await waitUntilExit() after render() in oclif commands -- without it the process exits before the UI completes)
(You MUST use this.log() / this.warn() / this.error() in commands -- console.log breaks --json mode and test capture)
(You MUST wrap all text in <Text> components in Ink -- bare strings cause rendering errors)
(You MUST use useEffect cleanup to cancel async operations -- Ink components unmount when the user presses Ctrl+C)
Failure to follow these rules will cause silent process exits, broken JSON output, and terminal rendering crashes.
</critical_reminders>