CLI ソースコード分析
モジュール概要
CLI は OpenCode のコマンドラインエントリレイヤーであり、packages/opencode/src/cli/ に位置しています。TypeScript 版は Yargs(Go 版の Cobra ではなく)を使用してコマンドライン引数とサブコマンドを解析し、2つの実行モードをサポートしています:
- TUI モード(デフォルト):Ink(React ベースのターミナル UI レンダリングエンジン)を起動し、バックグラウンドの Worker スレッドが SSE イベントストリーム経由で Server と通信し、Agent のインタラクションとツール実行を処理します
- 非インタラクティブモード(
runサブコマンド):単一のプロンプトを実行して終了し、@opencode-ai/sdkv2 の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 | ~6 | cmd() ヘルパー関数:Yargs CommandModule の型安全なラッパー |
src/cli/cmd/run.ts | ~690 | run サブコマンド:非インタラクティブモードの中核実装、SSE イベントストリーム消費、ツールレンダリング、ファイル添付、パーミッション処理 |
src/cli/cmd/tui/worker.ts | ~175 | TUI Worker:SSE 接続管理、Agent セッションプロキシ、Ink UI へのイベント転送 |
src/cli/cmd/tui/event.ts | — | TUI BusEvent 定義:Worker と Ink UI 間のイベントプロトコル |
src/cli/cmd/session.ts | — | session サブコマンド:セッションの CRUD、共有、アーカイブ操作 |
src/cli/cmd/agent.ts | — | agent サブコマンド:Agent リストの取得と管理 |
src/cli/cmd/mcp.ts | — | mcp サブコマンド:MCP サーバ管理 |
src/cli/cmd/models.ts | — | models サブコマンド:利用可能なモデルの一覧 |
src/cli/cmd/providers.ts | — | providers サブコマンド:利用可能な Provider の一覧 |
src/cli/cmd/serve.ts | — | serve サブコマンド:ローカル HTTP サーバの起動 |
src/cli/cmd/debug/ | — | debug サブコマンドグループ:デバッグツールコレクション |
src/cli/ui.ts | — | ターミナル UI ヘルパー:Style 定数、inline()/block() フォーマット関数 |
src/cli/logo.ts | — | Logo 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 レベルサブコマンドを定義しています:list、create、delete、share、archive。
プロジェクト初期化: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
})
}
さらに、webfetch、codesearch、websearch、skill、todo などのツール用のレンダリング関数もあります。認識されないツールタイプの場合は、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 / カスタムソリューションの代わりに Yargs | Yargs は成熟したサブコマンドネスト、引数バリデーション、自動生成ヘルプ、型安全なビルダーパターンを提供します。ネストされたサブコマンドシステム(session -> list/create/delete/share/archive)は Yargs で自然に表現できます |
| インプロセス呼び出しの代わりに SSE | TS 版の 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 検出)を共有し、重複コードと不整合な状態を避けます |
他のモジュールとの関係
- Instance(
src/project/):bootstrap()はプロジェクトインスタンスを初期化するInstance.provide()を呼び出します;Worker はクリーンアップのためにInstance.disposeAll()を呼び出します。Instance は CLI とビジネスロジックのブリッジです - Session(
src/session/):runコマンドはOpencodeClient経由で Session に対して演算を実行します(作成、継続、フォーク);sessionサブコマンドは Session モジュールの CRUD インターフェースを直接呼び出します - Agent(
src/agent/):runコマンドの--agentパラメータは実行 Agent を指定します;agentサブコマンドは利用可能な Agent を表示するためにAgent.list()を呼び出します。CLI 自体は Agent ロジックを実行しません — パラメータを渡すだけです - Provider(
src/provider/):runコマンドの--modelパラメータはProvider.getModel()経由でモデルの可用性をバリデーションします;modelsとprovidersサブコマンドは Provider クエリインターフェースを直接呼び出します - Bus(
src/bus/):Worker はBus.subscribeAll()経由ですべてのイベントをサブスクライブし、Ink UI にブリッジします;runコマンドは Bus 経由でSession.errorとPermission.askedイベントをリッスンします - MCP(
src/mcp/):mcpサブコマンドは MCP サーバの開始/停止を管理します;Instance.provide()は初期化中に MCP サーバを自動的に検出して接続します - Config(
src/config/):bootstrap()は初期化中に Config をロードします;configサブコマンドは設定の表示と変更のインターフェースを提供します。すべてのサブコマンドのデフォルトパラメータ(model、Agent など)は Config から取得されます - Storage(
src/storage/):Session メッセージと状態は Storage 経由で永続化され、CLI はOpencodeClient経由で間接的に Storage を操作します - Server(
src/server/):serveサブコマンドはローカル HTTP サーバを起動します;TUI Worker とrunコマンドは Server への SSE 接続経由で通信します - UI Helpers(
cli/ui.ts、cli/logo.ts):すべてのサブコマンドが統一されたターミナル出力フォーマットを共有し、brand の一貫性と可読性を確保します