CLI 源码解读
模块概述
CLI 是 OpenCode 的命令行入口层,位于 packages/opencode/src/cli/。TypeScript 版本使用 Yargs(而非 Go 版本的 Cobra)解析命令行参数和子命令,支持两种运行模式:
- TUI 模式(默认):启动 Ink(基于 React 的终端 UI 渲染引擎),后台 Worker 线程通过 SSE 事件流与 Server 通信,处理 Agent 交互和工具执行
- 非交互模式(
run子命令):单次 Prompt 执行后退出,基于@opencode-ai/sdkv2 的OpencodeClient实现完整的 Agent 会话,包含 20+ 种工具的内联渲染
与 Go 版本的根本差异在于:Go 版本在进程内直接持有 App 服务实例,而 TS 版本通过 SSE 与独立的 Server 进程通信——CLI 是纯粹的客户端,Server 承载所有业务逻辑。这种架构使得 CLI 可以远程连接到 opencode serve 启动的服务器,实现”本地编辑、远端推理”的工作模式。
核心职责:
- 命令行参数解析与子命令路由(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 art:启动时渲染品牌标识 |
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, // 默认文本
TEXT_DIM, // 灰色弱化文本(辅助信息)
TEXT_INFO_BOLD, // 蓝色加粗(标题、状态)
TEXT_WARNING_BOLD, // 黄色加粗(警告)
TEXT_DANGER_BOLD, // 红色加粗(错误、拒绝)
} as const
UI 输出函数
// 单行信息展示:icon + 标题 + 描述
export function inline(info: {
icon: string
title: string
description?: string
}): void
// 带分隔线的信息块:标题 + 可选输出内容
export function block(info: {
icon: string
title: string
description?: string
}, output?: string): void
inline() 用于紧凑的状态提示(如 “Agent: build”),block() 用于需要展示详细输出内容的场景(如工具执行结果)。
cmd() 辅助函数
// cmd.ts — 类型安全的 Yargs CommandModule 包装
export function cmd(mod: CommandModule): CommandModule {
return mod
}
虽然只有 6 行代码,cmd() 函数的价值在于为所有子命令提供统一的类型签名约束——每个子命令模块通过 cmd() 导出,确保 Yargs 的 CommandModule 接口被正确实现。
核心流程
入口与命令路由
CLI 入口位于 packages/opencode/src/node.ts,它使用 Yargs 定义完整的子命令体系:
opencode # 主入口(无参数 → 启动 TUI 模式)
├── run [message] # 非交互模式
│ ├── --continue, -c # 继续上次会话
│ ├── --session, -s # 指定会话 ID
│ ├── --fork # 从已有会话分叉
│ ├── --model, -m # 指定模型(provider/model 格式)
│ ├── --agent # 指定执行 Agent
│ ├── --format # 输出格式(default / json)
│ ├── --file, -f # 附加文件到上下文
│ ├── --attach # 连接到远程服务器
│ ├── --thinking # 显示推理过程
│ └── --dangerously-skip-permissions # 跳过权限确认
├── session # 会话管理
│ ├── list # 列出所有会话
│ ├── create # 创建新会话
│ ├── delete # 删除会话
│ ├── share # 分享会话
│ └── archive # 归档会话
├── agent # Agent 管理(列出内置和自定义 Agent)
├── mcp # MCP 服务器管理
├── models # 模型列表(按 Provider 分组展示)
├── providers # Provider 列表
├── serve # 启动本地 HTTP 服务器
├── config # 配置管理
└── debug # 调试工具组
每个子命令通过 cmd() 包装后注册到 Yargs,例如 session 子命令内部使用嵌套的 Yargs builder 定义 list、create、delete、share、archive 五个二级子命令。
项目初始化:bootstrap()
bootstrap.ts 是所有需要项目上下文的子命令的公共前置步骤:
// 简化的 bootstrap 实现
export async function bootstrap(
dir: string, // 项目工作目录
cb: () => Promise<void> // 初始化完成后的回调
) {
await Instance.provide({ directory: dir })
await cb()
}
Instance.provide 根据给定目录初始化完整的项目实例——加载配置、连接 Storage、注册 Provider、发现 MCP 服务器等。bootstrap() 将这个初始化过程封装为统一入口,子命令只需关心自己的业务逻辑。
非交互模式:run 命令详解
run.ts(~690 行)是 CLI 模块中最复杂的文件,实现了完整的非交互式 Agent 会话。核心流程分为四个阶段:
阶段一:初始化与会话准备
// 简化的初始化序列
await bootstrap(dir, async () => {
// 1. 解析命令行参数
const message = argv._[0] as string // 用户消息
const sessionID = argv.session // 指定会话 ID
const continue_ = argv.continue // 继续上次会话
const fork = argv.fork // 分叉会话
const model = argv.model // 模型选择
const agent = argv.agent // Agent 选择
const files = argv.file as string[] ?? [] // 附加文件
const format = argv.format ?? "default" // 输出格式
// 2. 创建 SSE 客户端连接
const client = new OpencodeClient(/* ... */)
})
阶段二:文件附加与会话分叉
run 命令支持通过 --file 参数将文件内容注入到用户消息中:
// 文件附加逻辑:将文件内容作为消息上下文
const fileParts: Part[] = []
for (const filePath of files) {
const content = await readFile(filePath, "utf-8")
fileParts.push({
type: "text",
text: `--- ${filePath} ---\n${content}`,
})
}
// 文件内容拼接到用户消息之前
会话分叉机制允许用户从已有会话创建新分支:
--fork <sessionID>:基于指定会话的消息历史创建新会话--fork+--continue:分叉后继续在新会话中对话--continue(无--fork):直接继续最近一次会话
阶段三:SSE 事件流消费与工具渲染
这是 run 命令的核心——通过 SSE 连接实时接收 Agent 的流式响应,并根据事件类型调用对应的渲染函数:
// 事件流消费的主循环(简化)
for await (const event of client.events()) {
switch (event.type) {
case "message":
// 文本输出 → 直接打印到终端
process.stdout.write(event.content)
break
case "tool":
// 工具事件 → 调用对应的渲染函数
renderTool(event.tool, event.state)
break
case "error":
// 错误 → 格式化输出
UI.inline({
icon: "✗",
title: event.name,
description: event.message,
})
break
case "permission":
// 权限请求 → 非交互模式默认拒绝
if (!dangerouslySkipPermissions) {
UI.inline({
icon: "🔒",
title: "Permission denied",
description: event.tool,
})
}
break
}
}
阶段四:输出格式化
run 命令支持两种输出格式:
--format default:人类可读的终端输出,使用UI.inline()和UI.block()格式化工具状态--format json:结构化的 JSON 事件流,每行一个 JSON 对象,适用于脚本集成和 CI/CD 场景
工具渲染系统
run.ts 包含 20+ 种工具类型的专用渲染函数,每种工具根据其语义展示不同的信息:
// glob 工具:显示搜索模式、匹配数、根目录
function renderGlob(tool: ToolEvent) {
UI.inline({
icon: "📁",
title: `glob: ${tool.input.pattern}`,
description: `${tool.matchCount} matches in ${tool.input.root}`,
})
}
// grep 工具:显示搜索模式、匹配数
function renderGrep(tool: ToolEvent) {
UI.inline({
icon: "🔍",
title: `grep: ${tool.input.pattern}`,
description: `${tool.matchCount} matches`,
})
}
// read 工具:显示文件路径和参数
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 工具:diff 展示
function renderEdit(tool: ToolEvent) {
UI.block({
icon: "✏️",
title: `edit: ${tool.input.filePath}`,
}, tool.diff) // 展示具体的 diff 内容
}
// bash 工具:命令 + 输出
function renderBash(tool: ToolEvent) {
UI.block({
icon: "⚡",
title: tool.input.command,
}, tool.output)
}
// task 工具:子 Agent 任务状态
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 启动序列(简化)
async function startWorker() {
// 1. 初始化项目实例
await Instance.provide({ directory: workdir })
// 2. 建立 SSE 事件流连接
const eventStream = await startEventStream(serverURL)
// 3. 订阅所有 Bus 事件并转发到 UI
Bus.subscribeAll((event) => {
Rpc.emit(event.type, event.data) // 通过 RPC 转发到 Ink 渲染线程
})
// 4. 启动 Agent 会话
// ... 会话管理逻辑
}
事件流桥接机制:
Server 进程 Worker 线程 Ink UI(React)
│ │ │
│ Bus.publish(event) │ │
│ ──────────────────────────> │ │
│ │ Rpc.emit(eventType, data) │
│ │ ──────────────────────────> │
│ │ │ React 组件更新
│ │ │ 重新渲染终端
Worker 通过 Bus.subscribeAll() 订阅所有事件(消息更新、工具状态、Session 变更等),然后通过 Rpc.emit() 转发到 Ink 渲染线程。Ink 组件通过 Rpc.on() 监听事件并触发 React 状态更新。
自动重连机制:
// SSE 连接断开时的自动重连
eventStream.on("close", () => {
setTimeout(() => {
startEventStream(serverURL) // 250ms 延迟后重试
}, 250)
})
Worker 在 SSE 连接断开后等待 250ms 自动重连。这个延迟避免了在网络抖动时的重连风暴,同时足够短以保持用户体验的流畅性。
实例清理:
// 监听实例销毁事件,触发资源清理
Bus.subscribe(Bus.InstanceDisposed, () => {
settle() // 完成所有进行中的操作
Instance.disposeAll() // 清理所有资源
})
当项目实例被销毁时(如切换工作目录),Worker 清理所有进行中的操作、关闭 SSE 连接、释放 Storage 和 Provider 资源。
错误处理
CLI 层面的错误处理覆盖三个层级:
1. 全局异常捕获
// error.ts — 进程级异常兜底
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. Session 错误事件
// run.ts — Session 级错误处理
Bus.subscribe(Session.error, (event) => {
const { name, message } = extractError(event.error)
UI.inline({
icon: "✗",
title: name,
description: message,
})
})
3. 权限处理
在非交互模式下,因为没有用户在场确认权限请求,默认策略是自动拒绝所有权限请求:
// run.ts — 权限事件处理
Bus.subscribe(Permission.asked, (event) => {
if (dangerouslySkipPermissions) {
// --dangerously-skip-permissions 标志下自动批准
event.respond(true)
} else {
// 默认:自动拒绝
event.respond(false)
}
})
--dangerously-skip-permissions 标志适用于 CI/CD 等完全自动化的场景,跳过所有权限确认。标志名称中的 “dangerously” 前缀明确提示此操作的风险。
Heap 快照
heap.ts 提供内存分析支持:
// heap.ts — 启动时自动写入堆快照
import { writeHeapSnapshot } from "node:v8"
// 写入堆快照到项目目录
writeHeapSnapshot(path.join(workdir, "server.heapsnapshot"))
堆快照用于排查内存泄漏问题。生成的 server.heapsnapshot 文件可以用 Chrome DevTools 的 Memory 面板加载分析。
调用链示例
链路 1:非交互模式 — 用户执行 opencode run "读取 main.ts"
用户执行: opencode run "读取 main.ts"
│
▼
Yargs 解析参数 → 匹配 "run" 子命令
│
▼
run.ts handler 执行:
├─ bootstrap(cwd, callback)
│ └─ Instance.provide({ directory: cwd })
│ ├─ 加载 Config(opencode.json)
│ ├─ 初始化 Storage
│ ├─ 注册 Provider(加载 API Key)
│ └─ 发现 MCP 服务器
│
├─ 创建或恢复 Session
│ ├─ --continue? → Session.get(recent)
│ ├─ --fork? → Session.create({ parentID: fork })
│ └─ 默认 → Session.create()
│
├─ 构造用户消息
│ ├─ text: "读取 main.ts"
│ ├─ --file 附加? → 追加文件内容
│ └ --model? → 覆盖默认模型
│
├─ 创建 OpencodeClient → SSE 连接到 Server
│
└─ 事件流消费循环:
├─ 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 (退出循环)
进程退出,输出结果到 stdout
链路 2:TUI 模式 — Worker 事件转发
用户运行: opencode(无参数)
│
▼
Yargs → 无匹配子命令 → 启动 TUI 模式
│
▼
Ink 渲染引擎启动(React 组件树)
│
▼
Worker 线程启动:
├─ Instance.provide({ directory: cwd })
├─ startEventStream(serverURL)
│ └─ SSE 连接建立
│
└─ Bus.subscribeAll() 注册全局事件监听
│
▼
用户在 Ink UI 中输入消息:
├─ Rpc.call("session.prompt", { message })
│ └─ Worker 接收 → 调用 Session.prompt()
│
├─ Server 处理消息:
│ ├─ SessionPrompt.loop()
│ ├─ LLM.stream() → Vercel AI SDK streamText()
│ ├─ Bus.publish(MessageV2.Event.PartUpdated)
│ └─ Bus.publish(MessageV2.Event.Updated)
│
└─ Worker 事件转发:
├─ Bus → Rpc.emit("message.updated", data)
│ └─ Ink 组件 setState() → 重新渲染消息列表
│
└─ Bus → Rpc.emit("tool.state", data)
└─ Ink 组件 setState() → 更新工具状态指示器
链路 3:会话分叉与继续
用户执行: opencode run "继续修改" --fork abc123 --model anthropic/claude-sonnet-4
│
▼
run.ts 解析参数:
├─ message = "继续修改"
├─ fork = "abc123"
├─ model = { providerID: "anthropic", modelID: "claude-sonnet-4" }
│
▼
Session.create({ parentID: "abc123" })
├─ 从父会话 abc123 复制消息历史
├─ 新会话获得独立 ID,不影响父会话
│
▼
模型覆盖:
└─ Provider.getModel("anthropic", "claude-sonnet-4")
└─ 后续 LLM 调用使用指定模型
│
▼
正常事件流消费(同链路 1)
设计取舍
| 决策 | 理由 |
|---|---|
| Yargs 而非 Commander / 自研 | Yargs 提供成熟的子命令嵌套、参数校验、自动帮助生成和类型安全的 builder 模式。子命令体系(session → list/create/delete/share/archive)的嵌套定义在 Yargs 中自然表达 |
| SSE 而非进程内调用 | TS 版本的 CLI 与 Server 分离架构使得 CLI 可以远程连接到 opencode serve 启动的服务器。SSE 作为单向事件流的传输协议,配合 OpencodeClient 的 RPC 调用实现双向通信 |
| Ink(React)而非 Bubble Tea | Go 版本的 Bubble Tea 使用 Elm Architecture(Model-Update-View),TS 版本使用 Ink + React——React 的组件化模型更适合构建复杂的终端 UI,且开发者生态更大 |
| 非交互模式默认拒绝权限 | 没有用户在场确认权限请求,自动拒绝是最安全的默认策略。--dangerously-skip-permissions 以显式的危险标志提供覆盖选项 |
| 20+ 工具类型各自渲染 | 每种工具的语义不同(glob 展示匹配数、edit 展示 diff、bash 展示命令输出),专用渲染函数比通用渲染器提供更好的可读性。fallback 函数保证未识别工具也能正常展示 |
| 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.list()展示可用 Agent。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子命令提供配置查看和修改接口。所有子命令的默认参数(模型、Agent 等)都来自 Config - Storage(
src/storage/):Session 的消息和状态通过 Storage 持久化,CLI 通过OpencodeClient间接操作 - Server(
src/server/):serve子命令启动本地 HTTP 服务器;TUI Worker 和run命令通过 SSE 连接到 Server 进行通信 - UI 辅助(
cli/ui.ts、cli/logo.ts):所有子命令共享统一的终端输出格式,确保品牌一致性和可读性