0. 本章学习目标

理解 opencode 进程如何启动,yargs 如何注册子命令,run 如何处理参数、stdin、文件附件、session 和事件订阅。

1. 一句话讲明白

OpenCode 的 CLI 层不是 agent 本体,而是入口适配层:把命令行输入整理成 session API 请求,再交给 runtime。来源:packages/opencode/src/index.ts:70-180packages/opencode/src/cli/cmd/run.ts:768-879

2. 它在 OpenCode agent 中的位置

index.ts 是进程 main;RunCommand 是 agent 学习第一入口;effectCmd 是命令运行时外壳。来源:packages/opencode/src/index.ts:158-180packages/opencode/src/cli/effect-cmd.ts:70-93

3. 生活类比

CLI 像服务台:确认目录、附件、旧会话、交互模式,然后把工单交给 agent 服务系统。

4. Java 开发者类比

  • index.ts 类似 main() + Picocli 根命令。
  • RunCommand 类似 @Command handler。
  • effectCmd 类似拦截器,负责加载项目上下文和 finally 清理。
  • 本地 Server.Default().app.fetch 类似 in-process controller 调用。

5. 最小源码路径

  1. packages/opencode/src/index.ts:58-91
  2. packages/opencode/src/index.ts:158-180
  3. packages/opencode/src/cli/effect-cmd.ts:70-93
  4. packages/opencode/src/cli/cmd/run.ts:127-245
  5. packages/opencode/src/cli/cmd/run.ts:246-360
  6. packages/opencode/src/cli/cmd/run.ts:396-516
  7. packages/opencode/src/cli/cmd/run.ts:768-879

6. 用户输入到 agent 行动的整体链路

process.argv
  -> hideBin
  -> yargs(args)
  -> RunCommand handler
  -> resolve message/stdin/files/session
  -> createOpencodeClient
  -> client.session.prompt
  -> SessionPrompt.prompt

7. 核心源码逐段讲解

7.1 yargs 入口

const args = hideBin(process.argv)
const cli = yargs(args)
  .scriptName("opencode")
  .option("pure", { describe: "run without external plugins", type: "boolean" })

来源:packages/opencode/src/index.ts:58-91

7.2 effectCmd

const useInstance = typeof opts.instance === "function" ? opts.instance(args) : opts.instance !== false
if (!useInstance) {
  await AppRuntime.runPromise(opts.handler(args))
  return
}
const { store, ctx } = await AppRuntime.runPromise(
  InstanceStore.Service.use((store) => store.load({ directory }).pipe(Effect.map((ctx) => ({ store, ctx })))),
)
try {
  await AppRuntime.runPromise(opts.handler(args).pipe(Effect.provideService(InstanceRef, ctx)))
} finally {
  await AppRuntime.runPromise(store.dispose(ctx))
}

来源:packages/opencode/src/cli/effect-cmd.ts:70-93

7.3 run 命令发送 prompt

const result = await client.session.prompt({
  sessionID,
  agent,
  model,
  variant: args.variant,
  parts: [...files, { type: "text", text: message }],
})

来源:packages/opencode/src/cli/cmd/run.ts:791-798

7.4 事件渲染

CLI 监听 message.part.updatedsession.errorpermission.asked,把 runtime 状态渲染到 stdout/UI。来源:packages/opencode/src/cli/cmd/run.ts:637-759

8. 关键 TypeScript 语法复习

  • default import:import yargs from "yargs"
  • namespace import:import * as Log
  • 泛型函数:effectCmd = <Args, A>(...)
  • object spread/array spread:parts: [...files, {...}]
  • dynamic import:await import("@/server/server")

9. 设计模式和架构思想

  • Command Pattern
  • Adapter
  • Interceptor
  • Event-driven UI
  • Single runtime path

10. 模块协作

CLI 负责 cwd、附件、session 和输出;Provider/Tool/Session runtime 在后续服务中处理。关键边界:CLI 只解析 provider/model,不直接调用模型。来源:packages/opencode/src/cli/cmd/run.ts:31-41

11. mini agent 对应代码

async function main(argv: string[]) {
  const args = parseArgs(argv)
  const files = await resolveFiles(args.file, args.cwd)
  const client = createMiniAgentClient({ cwd: args.cwd })
  const sessionID = await getOrCreateSession(client, args)
  renderEvents(client.subscribe(sessionID))
  await client.prompt(sessionID, { parts: [...files, { type: "text", text: args.message }] })
}

12. 费曼复述区

  1. 为什么 CLI 不是 agent 核心?
  2. effectCmd 解决了什么重复问题?
  3. 为什么本地模式也走 createOpencodeClient

13. 练习题

  • 找到 RunCommand--model--agent 参数。
  • 解释 run --attach 为什么不需要本地 instance。
  • 实现一个支持 run "hello"--file 的 mini CLI。

14. 源码追踪任务

  1. index.ts:158-180 -> RunCommand
  2. effect-cmd.ts:70-93 -> InstanceStore.Service.load
  3. run.ts:791-798 -> session API handler
  4. run.ts:637-759 -> 事件渲染

15. 面试式自测

  1. CLI 层该不该直接调用模型?
  2. 为什么要支持 session resume/fork?
  3. --file 在 CLI 层和 runtime 层分别做什么?

16. 下一步阅读建议

下一章读 “用户输入与会话”,理解 client.session.prompt 进入 runtime 后如何转成 MessageV2.User 和 parts。