コンテンツにスキップ

CLI ソースコード分析

モジュール概要

CLI は OpenCode のコマンドラインエントリレイヤーであり、packages/opencode/src/cli/ に位置しています。TypeScript 版は Yargs(Go 版の Cobra ではなく)を使用してコマンドライン引数とサブコマンドを解析し、2つの実行モードをサポートしています:

  • TUI モード(デフォルト):Ink(React ベースのターミナル UI レンダリングエンジン)を起動し、バックグラウンドの Worker スレッドが SSE イベントストリーム経由で Server と通信し、Agent のインタラクションとツール実行を処理します
  • 非インタラクティブモードrun サブコマンド):単一のプロンプトを実行して終了し、@opencode-ai/sdk v2 の OpencodeClient をベースにして、20 以上のツールタイプのインラインレンダリングを実装した完全な Agent セッションを実現します

Go 版との根本的な違い:Go 版はインプロセスの App サービスインスタンスを直接保持していますが、TS 版は SSE 経由で独立した Server プロセスと通信します — CLI は純粋なクライアントであり、すべてのビジネスロジックは Server が担います。このアーキテクチャにより、CLI は opencode serve で起動した Server にリモート接続できるようになり、「ローカル編集、リモート推論」のワークフローを実現しています。

コア責務:

  • コマンドライン引数の解析とサブコマンドルーティング(Yargs フレームワーク)
  • プロジェクトインスタンスの初期化(bootstrap -> Instance.provide
  • SSE イベントストリームの確立と双方向通信
  • 非インタラクティブモードでのツール状態レンダリング(20 以上のツールタイプ)
  • サブコマンドシステム管理(session、agent、mcp、models、providers、serve など)

主要ファイル

ファイルパス行数責任範囲
src/cli/bootstrap.ts~17プロジェクト初期化ラッパー:Instance.provide を呼び出してプロジェクトディレクトリを設定し、設定を読み込みます
src/cli/cmd/cmd.ts~6cmd() ヘルパー関数:Yargs CommandModule の型安全なラッパー
src/cli/cmd/run.ts~690run サブコマンド:非インタラクティブモードの中核実装、SSE イベントストリーム消費、ツールレンダリング、ファイル添付、パーミッション処理
src/cli/cmd/tui/worker.ts~175TUI Worker:SSE 接続管理、Agent セッションプロキシ、Ink UI へのイベント転送
src/cli/cmd/tui/event.tsTUI BusEvent 定義:Worker と Ink UI 間のイベントプロトコル
src/cli/cmd/session.tssession サブコマンド:セッションの CRUD、共有、アーカイブ操作
src/cli/cmd/agent.tsagent サブコマンド:Agent リストの取得と管理
src/cli/cmd/mcp.tsmcp サブコマンド:MCP サーバ管理
src/cli/cmd/models.tsmodels サブコマンド:利用可能なモデルの一覧
src/cli/cmd/providers.tsproviders サブコマンド:利用可能な Provider の一覧
src/cli/cmd/serve.tsserve サブコマンド:ローカル HTTP サーバの起動
src/cli/cmd/debug/debug サブコマンドグループ:デバッグツールコレクション
src/cli/ui.tsターミナル UI ヘルパー:Style 定数、inline()/block() フォーマット関数
src/cli/logo.tsLogo ASCII アート:起動時にブランドアイデンティティをレンダリング
src/cli/heap.tsヒープスナップショット:writeHeapSnapshot() でメモリ分析
src/cli/upgrade.ts自動アップグレード:新バージョンの検出とインストール
src/cli/network.tsネットワーク検出:接続確認とタイムアウト処理
src/cli/error.tsエラー処理:グローバル例外キャプチャとフォーマット出力

型システム

UI.Style — ターミナルスタイル定数

ui.ts は統合ターミナル出力スタイル定数を定義しており、すべてのサブコマンドで一貫した出力フォーマットを保証します:

export const Style = {
  TEXT_NORMAL,       // Default text
  TEXT_DIM,          // Gray dimmed text (supplementary info)
  TEXT_INFO_BOLD,    // Blue bold (titles, status)
  TEXT_WARNING_BOLD, // Yellow bold (warnings)
  TEXT_DANGER_BOLD,  // Red bold (errors, rejections)
} as const

UI 出力関数

// Single-line info display: icon + title + description
export function inline(info: {
  icon: string
  title: string
  description?: string
}): void

// Info block with separator: title + optional output content
export function block(info: {
  icon: string
  title: string
  description?: string
}, output?: string): void

inline() はコンパクトなステータス表示(例:「Agent: build」)に使用され、block() はツール実行結果など詳細な出力コンテンツを表示する必要があるシナリオに使用されます。

cmd() ヘルパー関数

// cmd.ts — type-safe Yargs CommandModule wrapper
export function cmd(mod: CommandModule): CommandModule {
  return mod
}

わずか 6 行のコードですが、cmd() の価値はすべてのサブコマンドに統一された型シグネチャ制約を提供することにあります — 各サブモジュールは cmd() を通じてエクスポートされ、Yargs の CommandModule インターフェースが正しく実装されていることを保証します。

コアフロー

エントリポイントとコマンドルーティング

CLI のエントリポイントは packages/opencode/src/node.ts にあり、Yargs を使用して完全なサブコマンドシステムを定義しています:

opencode                    # Main entry (no args -> launches TUI mode)
  |-- run [message]          # Non-interactive mode
  |   |-- --continue, -c     # Continue previous session
  |   |-- --session, -s      # Specify session ID
  |   |-- --fork             # Fork from existing session
  |   |-- --model, -m        # Specify model (provider/model format)
  |   |-- --agent            # Specify execution Agent
  |   |-- --format           # Output format (default / json)
  |   |-- --file, -f         # Attach files to context
  |   |-- --attach           # Connect to remote server
  |   |-- --thinking         # Show reasoning process
  |   `-- --dangerously-skip-permissions  # Skip permission confirmations
  |-- session                # Session management
  |   |-- list               # List all sessions
  |   |-- create             # Create new session
  |   |-- delete             # Delete session
  |   |-- share              # Share session
  |   `-- archive            # Archive session
  |-- agent                  # Agent management (list built-in and custom Agents)
  |-- mcp                    # MCP server management
  |-- models                 # Model list (grouped by Provider)
  |-- providers               # Provider list
  |-- serve                  # Start local HTTP server
  |-- config                 # Configuration management
  `-- debug                  # Debug tool group

各サブコマンドは cmd() でラップされ、Yargs に登録されます。例えば、session サブコマンドは内部でネストされた Yargs ビルダーを使用して、5 つの第 2 レベルサブコマンドを定義しています:listcreatedeletesharearchive

プロジェクト初期化:bootstrap()

bootstrap.ts は、プロジェクトコンテキストを必要とするすべてのサブコマンドの共通前提条件です:

// Simplified bootstrap implementation
export async function bootstrap(
  dir: string,       // Project working directory
  cb: () => Promise<void>  // Callback after initialization completes
) {
  await Instance.provide({ directory: dir })
  await cb()
}

Instance.provide は、指定されたディレクトリに基づいて完全なプロジェクトインスタンスを初期化します — 設定のロード、Storage への接続、Provider の登録、MCP サーバの検出、など。bootstrap() はこの初期化プロセスを統一されたエントリポイントとしてラップするため、サブコマンドは自分のビジネスロジックだけを気にすれば済みます。

非インタラクティブモード:run コマンドの詳細

run.ts(約 690 行)は CLI モジュールで最も複雑なファイルであり、完全な非インタラクティブ Agent セッションを実装しています。コアフローは 4 つのフェーズに分かれています:

フェーズ 1:初期化とセッション準備

// Simplified initialization sequence
await bootstrap(dir, async () => {
  // 1. Parse command-line arguments
  const message = argv._[0] as string           // User message
  const sessionID = argv.session                 // Specified session ID
  const continue_ = argv.continue                // Continue previous session
  const fork = argv.fork                         // Fork session
  const model = argv.model                       // Model selection
  const agent = argv.agent                       // Agent selection
  const files = argv.file as string[] ?? []      // Attached files
  const format = argv.format ?? "default"        // Output format

  // 2. Create SSE client connection
  const client = new OpencodeClient(/* ... */)
})

フェーズ 2:ファイル添付とセッションフォーク

run コマンドは --file パラメータを通じてファイルの内容をユーザーメッセージにインジェクションできます:

// File attachment logic: includes file contents as message context
const fileParts: Part[] = []
for (const filePath of files) {
  const content = await readFile(filePath, "utf-8")
  fileParts.push({
    type: "text",
    text: `--- ${filePath} ---\n${content}`,
  })
}
// File contents are prepended to the user message

セッションフォーク機構により、ユーザーは既存のセッションから新しいブランチを作成できます:

  • --fork <sessionID>: 指定されたセッションのメッセージ履歴に基づいて新しいセッションを作成
  • --fork + --continue: 新しいセッションで会話をフォークして継続
  • --continue--fork なし):最新のセッションを直接継続

フェーズ 3:SSE イベントストリーム消費とツールレンダリング

これは run コマンドの中核です — SSE 接続経由で Agent のストリーミングレスポンスをリアルタイムで受信し、イベントタイプに基づいて対応するレンダリング関数を呼び出します:

// Main event stream consumption loop (simplified)
for await (const event of client.events()) {
  switch (event.type) {
    case "message":
      // Text output -> print directly to terminal
      process.stdout.write(event.content)
      break

    case "tool":
      // Tool event -> call corresponding rendering function
      renderTool(event.tool, event.state)
      break

    case "error":
      // Error -> formatted output
      UI.inline({
        icon: "✗",
        title: event.name,
        description: event.message,
      })
      break

    case "permission":
      // Permission request -> auto-deny in non-interactive mode
      if (!dangerouslySkipPermissions) {
        UI.inline({
          icon: "🔒",
          title: "Permission denied",
          description: event.tool,
        })
      }
      break
  }
}

フェーズ 4:出力フォーマットの選択

run コマンドは 2 つの出力形式をサポートしています:

  • --format default:人間が読みやすいターミナル出力で、UI.inline()UI.block() を使用してツールステータスをフォーマット
  • --format json:構造化された JSON イベントストリームで、1 行に 1 つの JSON オブジェクトを出力し、スクリプト統合や CI/CD シナリオに適しています

ツールレンダリングシステム

run.ts には 20 以上のツールタイプそれぞれに専用のレンダリング関数が含まれており、セマンティクスに応じて異なる情報を表示します:

// glob tool: display search pattern, match count, root directory
function renderGlob(tool: ToolEvent) {
  UI.inline({
    icon: "📁",
    title: `glob: ${tool.input.pattern}`,
    description: `${tool.matchCount} matches in ${tool.input.root}`,
  })
}

// grep tool: display search pattern, match count
function renderGrep(tool: ToolEvent) {
  UI.inline({
    icon: "🔍",
    title: `grep: ${tool.input.pattern}`,
    description: `${tool.matchCount} matches`,
  })
}

// read tool: display file path and parameters
function renderRead(tool: ToolEvent) {
  UI.inline({
    icon: "📄",
    title: `read: ${tool.input.filePath}`,
    description: tool.input.offset ? `lines ${tool.input.offset}-${tool.input.offset + tool.input.limit}` : "",
  })
}

// edit/write tool: diff display
function renderEdit(tool: ToolEvent) {
  UI.block({
    icon: "✏️",
    title: `edit: ${tool.input.filePath}`,
  }, tool.diff)  // Display specific diff content
}

// bash tool: command + output
function renderBash(tool: ToolEvent) {
  UI.block({
    icon: "⚡",
    title: tool.input.command,
  }, tool.output)
}

// task tool: sub-Agent task status
function renderTask(tool: ToolEvent) {
  UI.inline({
    icon: "🤖",
    title: `task: ${tool.input.description}`,
    description: tool.state,  // pending / running / completed / error
  })
}

さらに、webfetchcodesearchwebsearchskilltodo などのツール用のレンダリング関数もあります。認識されないツールタイプの場合は、fallback レンダラーが使用されます — ツール名と JSON シリアライズされた入力パラメータを出力します。

TUI モード:Worker アーキテクチャの詳細

ユーザーが引数なしで opencode を実行すると、TUI モードに入ります。TS 版の TUI アーキテクチャは Go 版と根本的に異なります:

  • Go 版:インプロセスで *app.App を直接保持し、Bubble Tea の program.Send() を通じてイベントを注入
  • TS 版:UI プロセス(Ink/React)と Server プロセスが分離しており、Worker が SSE 経由で両者をブリッジ

Worker コアロジックcmd/tui/worker.ts、約 175 行):

// Worker startup sequence (simplified)
async function startWorker() {
  // 1. Initialize project instance
  await Instance.provide({ directory: workdir })

  // 2. Establish SSE event stream connection
  const eventStream = await startEventStream(serverURL)

  // 3. Subscribe to all Bus events and forward to UI
  Bus.subscribeAll((event) => {
    Rpc.emit(event.type, event.data)  // Forward to Ink rendering thread via RPC
  })

  // 4. Start Agent session
  // ... session management logic
}

イベントストリームブリッジ機構

Server process                    Worker thread                   Ink UI (React)
    |                              |                              |
    |  Bus.publish(event)          |                              |
    | --------------------------> |                              |
    |                              |  Rpc.emit(eventType, data)   |
    |                              | --------------------------> |
    |                              |                              |  React component update
    |                              |                              |  Re-render terminal

Worker は Bus.subscribeAll() を通じてすべてのイベント(メッセージ更新、ツールステータス、セッション変更など)をサブスクライブし、Rpc.emit() を通じて Ink レンダリングスレッドに転送します。Ink コンポーネントは Rpc.on() を通じてイベントをリッスンし、React ステート更新をトリガーします。

自動再接続機構

// Auto-reconnect when SSE connection drops
eventStream.on("close", () => {
  setTimeout(() => {
    startEventStream(serverURL)  // Retry after 250ms delay
  }, 250)
})

Worker は SSE 接続が切断されると 250ms の遅延後に自動的に再接続します。この遅延はネットワークジッター時の再接続嵐を避けながら、ユーザー体験を損なわない十分な応答性を維持しています。

Instance クリーンアップ

// Listen for instance disposal events, trigger resource cleanup
Bus.subscribe(Bus.InstanceDisposed, () => {
  settle()  // Complete all in-progress operations
  Instance.disposeAll()  // Clean up all resources
})

プロジェクトインスタンスが破棄された場合(例:作業ディレクトリの切り替え)、Worker は進行中のすべての操作をクリーンアップし、SSE 接続を閉じ、Storage と Provider のリソースを解放します。

エラー処理

CLI レベルのエラー処理は 3 つのレイヤーをカバーします:

1. グローバル例外キャプチャ

// error.ts — process-level exception fallback
process.on("unhandledRejection", (error) => {
  UI.inline({
    icon: "✗",
    title: "Unexpected error",
    description: String(error),
  })
  process.exit(1)
})

process.on("uncaughtException", (error) => {
  UI.inline({
    icon: "✗",
    title: "Uncaught exception",
    description: String(error),
  })
  process.exit(1)
})

2. セッションエラーイベント

// run.ts — Session-level error handling
Bus.subscribe(Session.error, (event) => {
  const { name, message } = extractError(event.error)
  UI.inline({
    icon: "✗",
    title: name,
    description: message,
  })
})

3. パーミッション処理

非インタラクティブモードでは、ユーザーがパーミッション確認を行うことができないため、デフォルトの戦略はすべてのパーミッションリクエストを自動的に拒否することです:

// run.ts — permission event handling
Bus.subscribe(Permission.asked, (event) => {
  if (dangerouslySkipPermissions) {
    // Auto-approve under --dangerously-skip-permissions flag
    event.respond(true)
  } else {
    // Default: auto-deny
    event.respond(false)
  }
})

--dangerously-skip-permissions フラグは CI/CD などの完全に自動化されたシナリオに適しており、すべてのパーミッション確認をスキップします。フラグ名の「dangerously」という接頭辞は、この操作のリスクを明確に示しています。

ヒープスナップショット

heap.ts はメモリ分析のサポートを提供します:

// heap.ts — auto-write heap snapshot on startup
import { writeHeapSnapshot } from "node:v8"

// Write heap snapshot to project directory
writeHeapSnapshot(path.join(workdir, "server.heapsnapshot"))

ヒープスナップショットはメモリリークの問題のトラブルシューティングに使用されます。生成された server.heapsnapshot ファイルは、Chrome DevTools の Memory パネルで読み込んで分析できます。

呼び出しチェーンの例

チェーン 1:非インタラクティブモード — ユーザーが opencode run "read main.ts" を実行

User executes: opencode run "read main.ts"
    |
    v
Yargs parses arguments -> matches "run" subcommand
    |
    v
run.ts handler executes:
    |-- bootstrap(cwd, callback)
    |   `-- Instance.provide({ directory: cwd })
    |       |-- Load Config (opencode.json)
    |       |-- Initialize Storage
    |       |-- Register Providers (load API Keys)
    |       `-- Discover MCP servers
    |
    |-- Create or restore Session
    |   |-- --continue? -> Session.get(recent)
    |   |-- --fork?     -> Session.create({ parentID: fork })
    |   `-- Default      -> Session.create()
    |
    |-- Construct user message
    |   |-- text: "read main.ts"
    |   |-- --file attached? -> append file contents
    |   `-- --model? -> override default model
    |
    |-- Create OpencodeClient -> SSE connection to Server
    |
    `-- Event stream consumption loop:
        |-- message.text   -> process.stdout.write()
        |-- tool.read      -> renderRead({ filePath, offset, limit })
        |-- tool.read.done -> block("edit", diff)
        |-- tool.bash      -> block("⚡", command + output)
        |-- error          -> inline("✗", error.name, message)
        `-- session.end    -> break (exit loop)

Process exits, output sent to stdout

チェーン 2:TUI モード — Worker イベント転送

User runs: opencode (no arguments)
    |
    v
Yargs -> no matching subcommand -> launch TUI mode
    |
    v
Ink rendering engine starts (React component tree)
    |
    v
Worker thread starts:
    |-- Instance.provide({ directory: cwd })
    |-- startEventStream(serverURL)
    |   `-- SSE connection established
    |
    `-- Bus.subscribeAll() registers global event listeners
        |
        v
User types message in Ink UI:
    |-- Rpc.call("session.prompt", { message })
    |   `-- Worker receives -> calls Session.prompt()
    |
    |-- Server processes message:
    |   |-- SessionPrompt.loop()
    |   |-- LLM.stream() -> Vercel AI SDK streamText()
    |   |-- Bus.publish(MessageV2.Event.PartUpdated)
    |   `-- Bus.publish(MessageV2.Event.Updated)
    |
    `-- Worker event forwarding:
        |-- Bus -> Rpc.emit("message.updated", data)
        |   `-- Ink component setState() -> re-render message list
        |
        `-- Bus -> Rpc.emit("tool.state", data)
            `-- Ink component setState() -> update tool status indicator

チェーン 3:セッションフォークと継続

User executes: opencode run "continue editing" --fork abc123 --model anthropic/claude-sonnet-4
    |
    v
run.ts parses arguments:
    |-- message = "continue editing"
    |-- fork = "abc123"
    |-- model = { providerID: "anthropic", modelID: "claude-sonnet-4" }
    |
    v
Session.create({ parentID: "abc123" })
    |-- Copy message history from parent session abc123
    |-- New session gets independent ID, does not affect parent session
    |
    v
Model override:
    `-- Provider.getModel("anthropic", "claude-sonnet-4")
       `-- Subsequent LLM calls use the specified model
    |
    v
Normal event stream consumption (same as Chain 1)

設計上のトレードオフ

決定事項根拠
Commander / カスタムソリューションの代わりに YargsYargs は成熟したサブコマンドネスト、引数バリデーション、自動生成ヘルプ、型安全なビルダーパターンを提供します。ネストされたサブコマンドシステム(session -> list/create/delete/share/archive)は Yargs で自然に表現できます
インプロセス呼び出しの代わりに SSETS 版の CLI-Server 分離アーキテクチャにより、CLI は opencode serve で起動した Server にリモート接続できます。SSE は一方向イベントストリーミングのトランスポートプロトコルとして機能し、双方向通信には OpencodeClient の RPC 呼び出しを組み合わせます
Bubble Tea の代わりに Ink(React)Go の Bubble Tea は Elm Architecture(Model-Update-View)を使用し、TS 版は Ink + React を使用します — React のコンポーネントモデルは複雑なターミナル UI の構築により適しており、より大きな開発者エコシステムを持っています
非インタラクティブモードではパーミッションを拒否するデフォルトユーザーがパーミッション確認を行うことができないため、自動拒否が最も安全なデフォルト戦略です。--dangerously-skip-permissions は明示的な Danger フラグ付きの上書きオプションを提供します
20 以上のツールタイプそれぞれに専用のレンダリング各ツールには異なるセマンティクスがあります(glob はマッチ数を表示、edit は diff を表示、bash はコマンド出力を表示)。汎用レンダラーよりも専用のレンダリング関数が可読性に優れています。フォールバック関数は認識されていないツールも適切に表示されることを保証します
メインスレッドの代わりに Worker スレッドSSE イベントストリーム消費と Bus イベントサブスクリプションは Worker スレッドで実行され、Ink の React レンダリングループのブロックを避けます。Ink は Rpc.emit/Rpc.on 経由で Worker と通信し、UI の応答性を維持します
250ms の再接続遅延ネットワークジッター時の再接続嵐を避けながら(短すぎるとサーバに過負荷がかかります)、十分な応答性を維持します(ユーザーは 250ms の中断はほとんど気づきません)
bootstrap() を統一初期化エントリポイントとしてプロジェクトコンテキストを必要とするすべてのサブコマンドが同じ初期化ロジック(Config ロード、Provider 登録、MCP 検出)を共有し、重複コードと不整合な状態を避けます

他のモジュールとの関係

  • Instancesrc/project/):bootstrap() はプロジェクトインスタンスを初期化する Instance.provide() を呼び出します;Worker はクリーンアップのために Instance.disposeAll() を呼び出します。Instance は CLI とビジネスロジックのブリッジです
  • Sessionsrc/session/):run コマンドは OpencodeClient 経由で Session に対して演算を実行します(作成、継続、フォーク);session サブコマンドは Session モジュールの CRUD インターフェースを直接呼び出します
  • Agentsrc/agent/):run コマンドの --agent パラメータは実行 Agent を指定します;agent サブコマンドは利用可能な Agent を表示するために Agent.list() を呼び出します。CLI 自体は Agent ロジックを実行しません — パラメータを渡すだけです
  • Providersrc/provider/):run コマンドの --model パラメータは Provider.getModel() 経由でモデルの可用性をバリデーションします;modelsproviders サブコマンドは Provider クエリインターフェースを直接呼び出します
  • Bussrc/bus/):Worker は Bus.subscribeAll() 経由ですべてのイベントをサブスクライブし、Ink UI にブリッジします;run コマンドは Bus 経由で Session.errorPermission.asked イベントをリッスンします
  • MCPsrc/mcp/):mcp サブコマンドは MCP サーバの開始/停止を管理します;Instance.provide() は初期化中に MCP サーバを自動的に検出して接続します
  • Configsrc/config/):bootstrap() は初期化中に Config をロードします;config サブコマンドは設定の表示と変更のインターフェースを提供します。すべてのサブコマンドのデフォルトパラメータ(model、Agent など)は Config から取得されます
  • Storagesrc/storage/):Session メッセージと状態は Storage 経由で永続化され、CLI は OpencodeClient 経由で間接的に Storage を操作します
  • Serversrc/server/):serve サブコマンドはローカル HTTP サーバを起動します;TUI Worker と run コマンドは Server への SSE 接続経由で通信します
  • UI Helperscli/ui.tscli/logo.ts):すべてのサブコマンドが統一されたターミナル出力フォーマットを共有し、brand の一貫性と可読性を確保します