🔭 如果让你接手一个 29 个 packages 的 monorepo,你会怎么开始读它的源码?
一个直觉是直接看文档——但文档只能告诉你"做了什么",不能告诉你"怎么跑的"。
另一个直觉是从入口文件开始一行行看——但你很快会发现,TypeScript 带 Effect-ts 的代码比普通 Node.js 项目复杂得多,没有调试器根本跟不动。
opencode 的作者留了一把钥匙:.vscode/launch.example.json。一个文件,三行配置,就能让你从源码跑起来,在任何文件设断点,跟着执行流走完整个 Agent 循环。
这篇文章就是这把钥匙的使用说明书。
【问题】为什么要从源码跑起来?
直接跑 binary 不行吗?
opencode 发布到 npm 的二进制版本是编译后的——你用 npx opencode 启动的是一个打包好的可执行文件。它工作得很好,但当你想要理解它的运行时行为时:
❌ console.log 加不了 — 二进制无法修改❌ 断点打不了 — 没有源码映射❌ 变量看不了 — 不知道中间状态❌ 调用栈跟不了 — 不知道谁调了谁源码调试解决什么?
源码调试给你三样二进制版本给不了的东西:
断点可控 — 在任何 .ts 文件任意一行按 F9,执行到那里自动停 变量可见 — 悬停看值,Watch 窗口跟踪表达式,不需要猜 调用链可跟 — 看 Call Stack 知道从哪来到哪去,理解代码路径
本文目标
读完本文,你将能:
从 GitHub clone opencode 并 bun install跑起来配置 VSCode 调试器,F5 启动带断点 找到项目入口 index.ts,理解 CLI 的 23 个子命令注册选择适合你的调试方式:F5 / bun --inspect/console.log
【设计】调试环境的四要素
搭建 opencode 的调试环境,需要理解四个要素的选择:
| Bun 1.3.x | ||
| Bun | --conditions=browser flag | |
| VSCode | ||
packages/opencode/src/index.ts |
依赖安装
先把代码拉下来装依赖:

关键数据确认:
709 个依赖包安装完成,耗时约 11 秒 29 个 workspace packages 全部用 workspace:*协议链接Bun 1.3.14 是项目要求的版本( package.json中packageManager: "bun@1.3.14")
启动命令
装完依赖就能跑:
bun run dev这条命令实际执行的是(来自 packages/opencode/package.json):
"dev":"bun run --conditions=browser ./src/index.ts"
--conditions=browser 是 Bun 的 exports conditions 特性——它让 TypeScript 在解析 package.json 的 exports 字段时走 browser 条件分支。这在 opencode 的很多跨平台模块中用到,比如 #db 导入:
"imports":{"#db":{"bun":"./src/storage/db.bun.ts","node":"./src/storage/db.node.ts","default":"./src/storage/db.bun.ts"}}调试架构
VSCode 调试 opencode 的架构是这样的:

流程很清晰:
VSCode 通过 Bun Debug Extension 连接 Bun Runtime 的 WebSocket Inspector Bun Runtime 监听在 ws://localhost:6499/opencode 进程跑在 Bun Runtime 之上,任何 .ts 文件都可断点
【源码】从 bun run dev 到 index.ts 到断点
第一步:配 VSCode launch.json
opencode 源码里已经给了示例配置:
cp .vscode/launch.example.json .vscode/launch.json这是最终的 launch.json:

两个配置:
opencode (attach) | bun --inspect 启动,再 F5 attach | |
opencode (launch) |
推荐用 launch 方式——F5 一键启动,不需要手动敲命令。
第二步:找到入口
CLI 的真正入口是 packages/opencode/src/index.ts:

文件路径:sources/opencode/packages/opencode/src/index.ts — 第 1–46 行
关键结构(摘录自 packages/opencode/src/index.ts 第 33–135 行):
const args = hideBin(process.argv) // 33const cli = yargs(args) // 45 .parserConfiguration({ "populate--": true }) .scriptName("opencode") .wrap(100) .help("help", "show help") .version("version", "show version number", InstallationVersion) .option("print-logs", { // 53describe: "print logs to stderr", type: "boolean", }) .option("pure", { // 62describe: "run without external plugins", type: "boolean", }) .middleware(async (opts) => { // 66if (opts.printLogs) process.env.OPENCODE_PRINT_LOGS = "1"if (opts.pure) process.env.OPENCODE_PURE = "1"Heap.start() process.env.AGENT = "1" process.env.OPENCODE = "1" process.env.OPENCODE_PID = String(process.pid) }) .usage("") // 79 .completion("completion", "generate shell completion script") // 80 .command(AcpCommand) // 81 .command(McpCommand) .command(TuiThreadCommand) .command(AttachCommand) .command(RunCommand)// ... (23 个子命令,见完整源码) .command(DbCommand) // 103 .fail((msg, err) => { // 104if (msg?.startsWith("Unknown argument") /* ... */) { cli.showHelp(show) }if (err) throw err process.exit(1) }) .strict() // 116try { // 118if (args.includes("-h") || args.includes("--help")) {await cli.parse(args, (err, _argv, out) => {if (!out) return; show(out) }) } else {await cli.parse() }} catch (e) { // 128const formatted = FormatError(e)if (formatted) UI.error(formatted) process.exitCode = 1} finally { process.exit() } // 136入口做的事很清楚:
hideBin(process.argv)— 去掉bun run自身参数,拿到[ "run", "--model", "..." ].middleware()— 在命令执行前设环境变量:AGENT=1、OPENCODE=1、启动 Heap 监控.command(XxxCommand)— 注册 23个子命令,每个 XxxCommand 是一个 yargs.CommandModuleawait cli.parse()— yargs 自动匹配命令并执行
第三步:设断点
这是最关键的一步——调试器不是为了"看代码高亮",而是为了在特定执行点停下来观察状态。
以 run 命令为例——这是 opencode 最核心的命令,启动 Agent 循环。断点设在哪?
推荐的第一个断点:packages/opencode/src/cli/cmd/run/runtime.ts
这个文件包含 runInteractiveRuntime() 函数——整个 Agent 的主循环就在这里。
断点设置步骤:
打开 packages/opencode/src/index.ts在 await cli.parse()这一行按 F9(设断点)按 F5(选择 opencode (launch)配置)CLI 启动后会停在 await cli.parse(),然后按 F8(Step Into)跟进 yargs 内部或者更直接:直接在 runtime.ts的runInteractiveRuntime()函数第一行设断点
查看 package.json 的 scripts
确认一下 dev 脚本的完整配置:

文件路径:sources/opencode/packages/opencode/package.json — 第 4–17 行
总共 8 个 scripts,开发最常用的是 dev 和 build:
dev | bun run --conditions=browser ./src/index.ts | |
build | bun run script/build.ts | |
test | bun test --timeout 30000 --only-failures | |
typecheck | tsgo --noEmit |
【权衡】VSCode debug vs bun --inspect vs console.log
三种调试方式各有适用场景:

方式一:VSCode F5 Launch(推荐用于源码阅读)
# 一键启动调试 —— 在 VSCode 里按 F5,选择 "opencode (launch)"VSCode 自动启动 Bun Inspector 模式,所有 .ts 文件可断点。
优点:
断点可控,想停哪停哪 Variable 窗口查看运行时状态 Call Stack 跟踪调用链 Debug Console 执行表达式
缺点:
需要一次 launch.json 配置 启动略慢(Bun 要编译 TypeScript)
方式二:bun --inspect(适合快速排查)
bun --inspect run --conditions=browser ./src/index.ts run然后在 Chrome 打开 chrome://inspect,找到 opencode 进程,点击 inspect。
优点:
不需要 VSCode,任何编辑器可用 Chrome DevTools 的调试体验很成熟 适合 Server 端排查
缺点:
每次要手动打开浏览器连 WebSocket 不如 VSCode 的变量监视方便
方式三:console.log(不推荐用于源码学习)
console.log(">>> 到这儿了", variable)优点: 零配置,任何环境都能用。
缺点:
改源码有 PR 残留风险 看不到调用栈 看不到变量当前状态 每次改了要重新启动
选型建议
bun --inspect | |
console.log | |
bun --inspect |
【锚点】
调试环境搭好,源码就变成你的游乐场了。
本篇总结
bun installbun run dev | |
cp launch.example.json launch.json | |
packages/opencode/src/index.ts | |
runtime.tsrunInteractiveRuntime() | |
--inspect 查 bug,console.log 速验 |
实操清单
# 1. 前置检查bash demo/setup-check.sh# 2. 装依赖cd sources/opencode && bun install# 3. 配调试cp .vscode/launch.example.json .vscode/launch.json# 4. VSCode 打开项目code sources/opencode# 5. 在 index.ts 第 118 行设断点(await cli.parse())# 6. F5 → 选择 "opencode (launch)" → 开始调试下篇预告
下篇文章讲 Effect-ts 预备课——不学 Effect-ts 语法,而是理解为什么 opencode 选择它作为编程基石,以及你看源码时最需要知道的 3 个 Effect-ts 模式。
| 1.3 | Effect-ts 预备课 |
夜雨聆风