oclif

Heroku/Salesforceが開発したNode.js/TypeScript向けのオープンCLIフレームワーク。プラグインシステムが特徴です。

javascriptclitypescriptframeworkplugin

フレームワーク

oclif

概要

oclifは、Heroku/Salesforceが開発したNode.js/TypeScript向けのオープンCLIフレームワークです。プラグインシステムが特徴で、拡張可能で本格的なCLIアプリケーションの構築を可能にします。エンタープライズグレードのCLIツール開発で人気が高く、Heroku CLIやSalesforce CLIなどの大規模プロジェクトで採用されています。

詳細

oclifは「Open CLI Framework」の略称で、Salesforce(旧Heroku)によって開発されました。単なるコマンドライン引数パーサーではなく、本格的なCLIアプリケーション開発のための包括的なフレームワークです。プラグインアーキテクチャ、自動テスト生成、配布システムなど、エンタープライズレベルのCLIツールに必要な機能を統合的に提供します。TypeScriptファーストの設計により、型安全性と開発者体験の両方を重視しています。

主な特徴

  • プラグインアーキテクチャ: 拡張可能なプラグインシステム
  • TypeScriptサポート: 型安全性とIntelliSenseの恩恵
  • 自動ヘルプ生成: 美しく構造化されたヘルプメッセージ
  • テスト支援: CLIコマンドの自動テスト機能
  • 配布システム: npm、tarballs、インストーラーの生成
  • フック: ライフサイクルフックによる拡張性
  • 設定管理: 設定ファイルとフラグの統合管理

メリット・デメリット

メリット

  • エンタープライズ向け: 大規模なCLIアプリケーションに適した設計
  • プラグイン対応: 柔軟な拡張システム
  • 型安全性: TypeScriptによる堅牢な開発体験
  • 豊富な機能: テスト、配布、設定管理まで包括的にサポート
  • 実績: Salesforce、Herokuでの実績とコミュニティサポート

デメリット

  • 複雑性: シンプルなCLIには機能が過剰な場合がある
  • 学習コスト: フレームワーク独自の概念の理解が必要
  • 依存関係: 多くの依存関係によるバンドルサイズの増加
  • オーバーヘッド: 小規模なツールには重すぎる場合がある

主要リンク

書き方の例

// package.json
{
  "name": "mycli",
  "version": "1.0.0",
  "oclif": {
    "bin": "mycli",
    "dirname": "mycli",
    "commands": "./lib/commands",
    "plugins": [
      "@oclif/plugin-help",
      "@oclif/plugin-plugins"
    ],
    "topicSeparator": " "
  }
}

// src/commands/hello.ts
import { Args, Command, Flags } from '@oclif/core'

export default class Hello extends Command {
  static override args = {
    person: Args.string({ description: '挨拶する相手の名前' }),
  }

  static override description = 'シンプルな挨拶コマンド'

  static override examples = [
    '<%= config.bin %> <%= command.id %> 太郎',
    '<%= config.bin %> <%= command.id %> --name=花子',
  ]

  static override flags = {
    name: Flags.string({ char: 'n', description: '挨拶する相手の名前' }),
    from: Flags.string({ char: 'f', description: '挨拶者の名前', required: true }),
    uppercase: Flags.boolean({ char: 'u', description: '大文字で出力' }),
  }

  public async run(): Promise<void> {
    const { args, flags } = await this.parse(Hello)

    const name = flags.name ?? args.person ?? 'World'
    let message = `こんにちは、${name}さん! ${flags.from}より`

    if (flags.uppercase) {
      message = message.toUpperCase()
    }

    this.log(message)
  }
}

// src/commands/config/set.ts
import { Args, Command, Flags } from '@oclif/core'

export default class ConfigSet extends Command {
  static override args = {
    key: Args.string({ description: '設定キー', required: true }),
    value: Args.string({ description: '設定値', required: true }),
  }

  static override description = '設定値を設定する'

  static override flags = {
    global: Flags.boolean({ char: 'g', description: 'グローバル設定に保存' }),
  }

  public async run(): Promise<void> {
    const { args, flags } = await this.parse(ConfigSet)
    
    // 設定保存のロジック
    this.log(`設定保存: ${args.key} = ${args.value}`)
    if (flags.global) {
      this.log('グローバル設定に保存されました')
    }
  }
}

// src/commands/deploy.ts
import { Command, Flags } from '@oclif/core'
import { ux } from '@oclif/core'

export default class Deploy extends Command {
  static override description = 'アプリケーションをデプロイする'

  static override flags = {
    environment: Flags.string({
      char: 'e',
      description: 'デプロイ環境',
      options: ['development', 'staging', 'production'],
      default: 'staging',
    }),
    'dry-run': Flags.boolean({ description: 'デプロイをシミュレート' }),
    verbose: Flags.boolean({ char: 'v', description: '詳細ログを出力' }),
  }

  public async run(): Promise<void> {
    const { flags } = await this.parse(Deploy)

    if (flags['dry-run']) {
      this.log('🔍 ドライランモード')
    }

    this.log(`🚀 ${flags.environment}環境にデプロイ開始`)

    // プログレスバーの表示
    const bar = ux.progress({
      format: 'デプロイ中... [{bar}] {percentage}% | {value}/{total}',
    })
    bar.start(100, 0)

    // デプロイ処理のシミュレーション
    for (let i = 0; i <= 100; i += 10) {
      bar.update(i)
      await new Promise(resolve => setTimeout(resolve, 200))
    }

    bar.stop()
    this.log('✅ デプロイが完了しました')
  }
}

// src/hooks/init.ts
import { Hook } from '@oclif/core'

const hook: Hook<'init'> = async function (opts) {
  // 初期化処理
  this.log('CLIが初期化されました')
}

export default hook

// プラグインの例 - src/commands/plugins/install.ts
import { Args, Command } from '@oclif/core'

export default class PluginsInstall extends Command {
  static override args = {
    plugin: Args.string({ description: 'インストールするプラグイン名', required: true }),
  }

  static override description = 'プラグインをインストール'

  public async run(): Promise<void> {
    const { args } = await this.parse(PluginsInstall)
    
    this.log(`プラグインをインストール中: ${args.plugin}`)
    // プラグインインストールのロジック
    this.log('インストール完了!')
  }
}