はじめに
CLIツールの開発って、正直なところ面倒なんですよね。文字の配置、色付け、入力処理...これらを一から実装すると結構な工数がかかります。
そんな中で出会ったのが Ink というライブラリ。これ、意外と知らない人が多いんですが、ReactでCLIアプリを作れるという、なかなか画期的なツールなんですよ。
GitHubのスター数は33,000以上。月間ダウンロード数は約750万。Claude Code(Anthropic)、Gemini CLI(Google)、GitHub Copilot CLI、Cloudflare Wranglerといった有名どころのCLIツールでも採用されています。
個人的には「もっと早く知りたかった」という感想ですね。
Inkとは
Inkは、Reactをターミナル向けにレンダリングするライブラリです。公式の説明では「React for CLIs. Build and test your CLI output using components.」とあります。
要するに、普段Reactで書いているようなコンポーネントベースの設計を、そのままCLIアプリに持ち込めるという話です。
Flexboxレイアウトを実現するためにYogaエンジンを採用しており、CSSライクなプロパティでレイアウトを組めます。Webフロントエンドの経験がある人なら、学習コストはかなり低いと思います。
特徴・メリット
1. Reactの全機能が使える
useState、useEffect、カスタムフック...Reactの機能がそのまま使えます。状態管理のパターンも、Webアプリと同じ感覚で書けるんですよね。
既存のReactスキルを流用できるのは、30代エンジニアとしては時短になってありがたい。
2. コンポーネントベースの設計
UIを再利用可能なコンポーネントに分割できます。プログレスバー、テーブル、選択メニューなど、一度作れば使い回せる。保守性も上がります。
3. Flexboxレイアウト
<Box> コンポーネントが display: flex 相当の機能を提供します。width、height、padding、marginといったプロパティが使えるので、レイアウトの調整が直感的です。
4. テストしやすい
Reactのテスト手法がそのまま使えます。ink-testing-libraryを使えば、CLIの出力をユニットテストできる。これ、地味に大きいポイントだと思います。
5. TypeScript対応
公式でTypeScriptをサポートしています。型安全なCLI開発ができるのは、中規模以上のプロジェクトでは必須条件ですね。
インストール方法
基本的なインストールはシンプルです。
npm install ink react
プロジェクトを素早く始めたい場合は、公式のスキャフォールディングツールが便利です。
npx create-ink-app my-ink-cli
TypeScriptで始める場合はこちら。
npx create-ink-app --typescript my-ink-cli
個人的にはTypeScript版一択ですね。型があると補完が効いて開発効率が上がります。
基本的な使い方
シンプルな例
まずは最小構成から。
import React from 'react';
import { render, Text } from 'ink';
const App = () => {
return <Text color="green">Hello, Ink!</Text>;
};
render(<App />);
これだけで、ターミナルに緑色の「Hello, Ink!」が表示されます。
主要コンポーネント
Text
テキスト表示を担当するコンポーネントです。
import { Text } from 'ink';
// 色付きテキスト
<Text color="cyan">シアン色のテキスト</Text>
// 背景色付き
<Text backgroundColor="red">赤背景のテキスト</Text>
// 太字・イタリック
<Text bold>太字</Text>
<Text italic>イタリック</Text>
// 下線・打消し線
<Text underline>下線付き</Text>
<Text strikethrough>打消し線</Text>
Box
レイアウトを構築するコンポーネント。Flexboxベースです。
import { Box, Text } from 'ink';
<Box flexDirection="column" padding={1}>
<Box marginBottom={1}>
<Text>ヘッダー</Text>
</Box>
<Box>
<Text>コンテンツ</Text>
</Box>
</Box>
状態管理の例
Reactのフックがそのまま使えます。
import React, { useState, useEffect } from 'react';
import { render, Text } from 'ink';
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
return <Text color="yellow">経過時間: {count}秒</Text>;
};
render(<Counter />);
実践的なユースケース
プログレスバーの実装
ファイル処理やAPI呼び出しの進捗表示に使えます。
import React, { useState, useEffect } from 'react';
import { render, Box, Text } from 'ink';
const ProgressBar = ({ percent }: { percent: number }) => {
const width = 30;
const filled = Math.round(width * (percent / 100));
const empty = width - filled;
return (
<Box>
<Text color="green">{'█'.repeat(filled)}</Text>
<Text color="gray">{'░'.repeat(empty)}</Text>
<Text> {percent}%</Text>
</Box>
);
};
const App = () => {
const [progress, setProgress] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setProgress(prev => {
if (prev >= 100) {
clearInterval(timer);
return 100;
}
return prev + 5;
});
}, 200);
return () => clearInterval(timer);
}, []);
return (
<Box flexDirection="column">
<Text bold>ファイル処理中...</Text>
<ProgressBar percent={progress} />
</Box>
);
};
render(<App />);
インタラクティブな選択メニュー
ユーザー入力を受け付けるUIも構築できます。
import React, { useState } from 'react';
import { render, Box, Text, useInput } from 'ink';
const items = ['オプション1', 'オプション2', 'オプション3'];
const SelectMenu = () => {
const [selectedIndex, setSelectedIndex] = useState(0);
useInput((input, key) => {
if (key.upArrow) {
setSelectedIndex(prev => (prev > 0 ? prev - 1 : items.length - 1));
}
if (key.downArrow) {
setSelectedIndex(prev => (prev < items.length - 1 ? prev + 1 : 0));
}
if (key.return) {
console.log(`選択: ${items[selectedIndex]}`);
process.exit();
}
});
return (
<Box flexDirection="column">
<Text bold>選択してください(↑↓で移動、Enterで決定)</Text>
{items.map((item, index) => (
<Text key={item} color={index === selectedIndex ? 'cyan' : 'white'}>
{index === selectedIndex ? '❯ ' : ' '}{item}
</Text>
))}
</Box>
);
};
render(<SelectMenu />);
ローディングスピナー
API待ちなどで使えるスピナーの実装例です。
import React, { useState, useEffect } from 'react';
import { render, Text } from 'ink';
const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
const Spinner = ({ message }: { message: string }) => {
const [frame, setFrame] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setFrame(prev => (prev + 1) % spinnerFrames.length);
}, 80);
return () => clearInterval(timer);
}, []);
return (
<Text>
<Text color="cyan">{spinnerFrames[frame]}</Text>
<Text> {message}</Text>
</Text>
);
};
render(<Spinner message="データを取得中..." />);
公式コンポーネントライブラリ
Ink用のコンポーネントライブラリもいくつか公開されています。
- ink-spinner: 各種スピナー
- ink-select-input: 選択入力
- ink-text-input: テキスト入力
- ink-table: テーブル表示
- ink-link: ターミナルリンク
これらを活用すれば、開発効率がさらに上がります。
まとめ
InkはCLIツール開発の選択肢として、かなり有力だと思います。
向いているケース
- Reactの経験があるチーム
- 複雑なUI構成が必要なCLIツール
- テストカバレッジを重視するプロジェクト
- TypeScriptで型安全に開発したい場合
向いていないケース
- 単純なワンライナーCLI
- React未経験者のみのチーム
正直なところ、CLIツールの開発でここまでモダンな体験ができるとは思っていませんでした。30代になって「新しい技術への適応力が落ちたかな」と感じることもあるんですが、Reactの知識がそのまま活かせるInkは、その点でもハードルが低い。
CLIツールを作る機会があれば、一度試してみる価値はあると思います。
