はじめに
CLIツールを作ろうとすると、意外と面倒なんですよね。引数のパース、ファイル操作、テンプレート生成、対話的なプロンプト...これらを一つずつライブラリを選定して組み合わせていくと、それだけで半日潰れることもある。
そんな中で見つけたのが Gluegun というツールキット。これ、CLIに必要な機能がオールインワンで揃っているんですよ。
GitHubのスター数は3,100以上。React NativeのIgnite CLIやEthereumのGraph CLIなど、実際にプロダクションで使われている実績もあります。
個人的には「CLIを作るならまずこれを検討する」というポジションのツールだと思っています。
Gluegunとは
Gluegunは、Infinite Red社が開発したTypeScript/JavaScript向けのCLIツールキットです。公式の説明では「A delightful toolkit for building TypeScript-powered command-line apps」とあります。
要するに、CLIアプリを作る際に必要な機能を一通り提供してくれるツールキットです。
内部ではyargs-parser、enquirer、fs-jetpack、axios、ejsなど、実績のあるライブラリを統合しています。これらを個別に設定する手間が省けるのは、地味にありがたい。
特徴・メリット
1. オールインワンの機能セット
Gluegunが提供する機能は幅広いです。
- パラメータ処理: コマンドライン引数とオプションの解析
- テンプレート生成: EJSベースのテンプレートからファイル生成
- ファイル操作: 読み書き、パッチング、ディレクトリ操作
- システムコマンド実行: 外部コマンドの呼び出し
- HTTP通信: APIとのやり取り
- 対話的プロンプト: ユーザー入力の取得
- カラフル出力: ターミナルへの装飾付き表示
- テーブル表示: 整形されたデータ出力
- セマンティックバージョニング: バージョン比較
- パッケージインストール: npm/yarnでの依存関係追加
これだけ揃っていれば、大抵のCLIツールは作れます。
2. 「ready-aim-fire」パターン
Gluegunは独自の設計パターンを採用しています。設定をチェーンで繋いでいき、最後に run() で実行する形式です。
const { build } = require('gluegun')
const cli = build('my-cli')
.src(__dirname)
.plugins('./node_modules', { matching: 'my-cli-*' })
.help()
.version()
.create()
cli.run()
設定が宣言的で見通しが良いんですよね。
3. プラグインシステム
機能を追加したい場合はプラグインで拡張できます。npmパッケージとして配布されているプラグインを読み込むだけ。チームで共通のコマンドを配布したい場合に便利です。
4. TypeScript完全対応
最初からTypeScriptを前提に設計されています。型定義が充実しているので、補完が効いて開発効率が上がります。
30代エンジニアとしては、型があると安心感が違う。リファクタリングもしやすいですしね。
5. 安定したプロジェクト
Gluegunは現在「安定フェーズ」にあります。新機能の追加は予定されていませんが、バグ修正やセキュリティアップデートは継続的に行われています。
「枯れた技術」とまでは言わないですが、プロダクションで安心して使えるレベルには達しています。
インストール方法
新規プロジェクトを始める場合は、公式のスキャフォールディングツールが便利です。
npx gluegun new my-cli
cd my-cli
npm link
my-cli help
これだけで、CLIの雛形ができあがります。
既存プロジェクトに追加する場合はnpmでインストール。
npm install gluegun
シンプルですね。
基本的な使い方
プロジェクト構成
Gluegunのプロジェクトは、基本的に以下の構成になります。
my-cli/
├── src/
│ ├── cli.ts # エントリーポイント
│ ├── commands/ # コマンド定義
│ │ └── generate.ts
│ ├── extensions/ # 拡張機能
│ └── templates/ # テンプレートファイル
├── package.json
└── tsconfig.json
コマンドごとにファイルを分けられるので、管理しやすいです。
コマンドの実装例
基本的なコマンドの書き方です。
// src/commands/hello.ts
import { GluegunCommand } from 'gluegun'
const command: GluegunCommand = {
name: 'hello',
description: '挨拶を表示する',
run: async (toolbox) => {
const { print, parameters } = toolbox
const name = parameters.first || 'World'
print.info(`Hello, ${name}!`)
}
}
export default command
toolbox にCLI開発に必要な機能が全部入っています。
対話的プロンプト
ユーザーに入力を求める場合はこう書きます。
// src/commands/create.ts
import { GluegunCommand } from 'gluegun'
const command: GluegunCommand = {
name: 'create',
description: 'プロジェクトを作成する',
run: async (toolbox) => {
const { prompt, print, filesystem } = toolbox
const result = await prompt.ask([
{
type: 'input',
name: 'projectName',
message: 'プロジェクト名を入力してください:'
},
{
type: 'select',
name: 'template',
message: 'テンプレートを選択してください:',
choices: ['basic', 'advanced', 'minimal']
}
])
print.info(`${result.projectName} を ${result.template} テンプレートで作成します`)
// ディレクトリ作成
filesystem.dir(result.projectName)
}
}
export default command
Enquirerベースなので、補完付きの選択UIなども簡単に作れます。
テンプレート生成
ファイルをテンプレートから生成する例です。
// src/commands/generate.ts
import { GluegunCommand } from 'gluegun'
const command: GluegunCommand = {
name: 'generate',
alias: ['g'],
description: 'コンポーネントを生成する',
run: async (toolbox) => {
const { template, print, parameters } = toolbox
const name = parameters.first
if (!name) {
print.error('コンポーネント名を指定してください')
return
}
await template.generate({
template: 'component.ts.ejs',
target: `src/components/${name}.ts`,
props: { name }
})
print.success(`${name} コンポーネントを作成しました`)
}
}
export default command
テンプレートファイル(EJS形式)はこんな感じ。
// src/templates/component.ts.ejs
export const <%= props.name %> = () => {
return {
name: '<%= props.name %>'
}
}
HTTP通信
APIとやり取りする場合。
// src/commands/fetch.ts
import { GluegunCommand } from 'gluegun'
const command: GluegunCommand = {
name: 'fetch',
description: 'APIからデータを取得する',
run: async (toolbox) => {
const { http, print } = toolbox
const api = http.create({
baseURL: 'https://api.example.com'
})
const response = await api.get('/users')
if (response.ok) {
print.table(
response.data.map((user: any) => [user.id, user.name]),
{ header: ['ID', 'Name'] }
)
} else {
print.error('データの取得に失敗しました')
}
}
}
export default command
axiosベースなので、使い慣れた感覚で書けます。
実践的なユースケース
プロジェクト生成ツール
チームで使うボイラープレート生成ツールを作る場合。
// src/commands/init.ts
import { GluegunCommand } from 'gluegun'
const command: GluegunCommand = {
name: 'init',
description: 'プロジェクトを初期化する',
run: async (toolbox) => {
const { prompt, template, filesystem, print, system } = toolbox
const config = await prompt.ask([
{
type: 'input',
name: 'name',
message: 'プロジェクト名:'
},
{
type: 'confirm',
name: 'typescript',
message: 'TypeScriptを使用しますか?'
},
{
type: 'multiselect',
name: 'features',
message: '追加する機能を選択:',
choices: ['ESLint', 'Prettier', 'Jest', 'Husky']
}
])
// ディレクトリ作成
filesystem.dir(config.name)
process.chdir(config.name)
// テンプレートからファイル生成
await template.generate({
template: 'package.json.ejs',
target: 'package.json',
props: config
})
if (config.typescript) {
await template.generate({
template: 'tsconfig.json.ejs',
target: 'tsconfig.json',
props: config
})
}
// 依存関係のインストール
print.info('依存関係をインストール中...')
await system.run('npm install')
print.success(`${config.name} プロジェクトを作成しました`)
}
}
export default command
マイグレーションツール
データベースのマイグレーション管理ツールなども作れます。
// src/commands/migrate.ts
import { GluegunCommand } from 'gluegun'
const command: GluegunCommand = {
name: 'migrate',
description: 'マイグレーションを実行する',
run: async (toolbox) => {
const { filesystem, print, parameters } = toolbox
const direction = parameters.first || 'up'
const migrationFiles = filesystem
.list('migrations')
?.filter(f => f.endsWith('.ts'))
.sort()
if (!migrationFiles?.length) {
print.warning('マイグレーションファイルがありません')
return
}
print.info(`${direction === 'up' ? '適用' : 'ロールバック'}を実行中...`)
for (const file of migrationFiles) {
print.info(` - ${file}`)
// 実際のマイグレーション処理
}
print.success('マイグレーション完了')
}
}
export default command
デプロイ自動化ツール
デプロイ作業を自動化するCLIも定番のユースケースです。
// src/commands/deploy.ts
import { GluegunCommand } from 'gluegun'
const command: GluegunCommand = {
name: 'deploy',
description: 'アプリケーションをデプロイする',
run: async (toolbox) => {
const { prompt, system, print } = toolbox
const { environment } = await prompt.ask({
type: 'select',
name: 'environment',
message: 'デプロイ先を選択:',
choices: ['staging', 'production']
})
if (environment === 'production') {
const { confirm } = await prompt.ask({
type: 'confirm',
name: 'confirm',
message: '本番環境にデプロイします。よろしいですか?'
})
if (!confirm) {
print.warning('デプロイをキャンセルしました')
return
}
}
print.info(`${environment} 環境にデプロイ中...`)
await system.run('npm run build')
await system.run(`npm run deploy:${environment}`)
print.success('デプロイ完了')
}
}
export default command
採用事例
Gluegunは実際に多くのプロジェクトで採用されています。
- Ignite CLI: React Nativeのスターターキット生成ツール
- Solidarity: 開発環境の依存関係チェックツール
- Graph CLI: The Graph(Ethereum)のインデックス管理ツール
どれも本番運用されているツールなので、信頼性は高いと言えます。
まとめ
GluegunはCLI開発の「面倒な部分」を一掃してくれるツールキットです。
向いているケース
- 中規模以上のCLIツール開発
- テンプレート生成機能が必要な場合
- チーム開発でCLIツールを作る場合
- TypeScriptで型安全に開発したい場合
向いていないケース
- 単純なワンライナーCLI
- 極限まで軽量化したい場合
正直なところ、30代になると「車輪の再発明」をする時間的余裕がなくなってきます。Gluegunはその点で、CLI開発に必要な機能が最初から揃っているので時短になる。
小さなスクリプトならシェルで十分ですが、ある程度の規模のCLIを作るなら、Gluegunを検討する価値はあると思います。