第 29 章:搭建你的工坊

源码验证日期:2026-05-15,基于 commit
0d81bb6
卷一,你跟着一条消息走完了它的全程旅程。
卷二,你打开了引擎室的门,一台一台地拆开那些引擎看它们的内部构造。
现在,卷三。
这一卷,你要从一个读者变成一个工匠。不再是看别人造好的房子,而是走进工坊,拿起锤子和锯子,自己动手。
但在你拿起锤子之前,工坊得先搭好。
路线图
1 | graph LR |
这一章结束时,你将拥有什么
- 源码完整地躺在你的硬盘上,你可以用任何编辑器打开它
- 你可以用 Bun 从源码直接运行 Claude Code,看到它的输出
- 你知道怎么加日志、怎么追踪执行流、怎么验证你的修改
- 你理解这个源码仓库的”特殊之处”——它不是 Anthropic 的内部开发仓库,而是从 npm 包的 source map 里提取出来的
最后一点很重要。它会影响到你能做什么、不能做什么。我们先把这件事说清楚。
技术栈概览
Claude Code 的技术栈——你在卷二已经从架构层面认识了它们,现在从开发者的角度再看一遍:
| 类别 | 技术 |
|---|---|
| 语言 | TypeScript(.ts / .tsx) |
| 运行时 | Node.js >= 18 + Bun API |
| 打包器 | Bun 内置打包器 |
| 模块系统 | ESM("type": "module") |
| CLI 框架 | Commander.js(@commander-js/extra-typings) |
| 终端 UI | React + 自定义 Ink fork(src/ink/) |
| 验证 | Zod v4 |
| MCP 协议 | @modelcontextprotocol/sdk |
| AI SDK | @anthropic-ai/sdk |
| 测试 | Vitest |
| 可观测性 | OpenTelemetry |
记住这张表。接下来整个卷三,你都会和这些技术打交道。
第一步:获取源码
Claude Code 的源码仓库本质上是从 npm 包里提取出来的。有两种方式获取:
方式一:直接克隆仓库1
2git clone https://github.com/niteshjaitwar/claudeai.git
cd claudeai
这是最简单的方式。源码就在 src/ 目录下,以 TypeScript 文件的形式完整呈现。
方式二:自己从 npm 包提取
如果你想亲手体验这个过程(推荐),可以这样做:1
2
3
4
5
6
7
8
9# 创建工作目录
mkdir claude-code-source && cd claude-code-source
# 下载 npm 包
npm pack @anthropic-ai/[email protected]
# 解压
tar -xzf anthropic-ai-claude-code-2.1.88.tgz
cd package
npm 包里有一个 cli.js.map 文件。这个 source map 的 sourcesContent 字段包含了所有原始 TypeScript 源码。创建一个 unpack.mjs 文件来提取:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34import { readFileSync, writeFileSync, mkdirSync } from "fs";
import { dirname, join } from "path";
const mapFile = join(import.meta.dirname, "cli.js.map");
const outDir = join(import.meta.dirname, "unpacked");
console.log("Reading source map...");
const map = JSON.parse(readFileSync(mapFile, "utf-8"));
const sources = map.sources || [];
const contents = map.sourcesContent || [];
console.log(`Found ${sources.length} source files.`);
let written = 0;
let skipped = 0;
for (let i = 0; i < sources.length; i++) {
const src = sources[i];
const content = contents[i];
if (content == null) {
skipped++;
continue;
}
const outPath = join(outDir, src.replace(/^\.\.\//g, ""));
mkdirSync(dirname(outPath), { recursive: true });
writeFileSync(outPath, content);
written++;
}
console.log(`Done! Wrote ${written} files to ${outDir}`);
if (skipped > 0) console.log(`Skipped ${skipped} files with no content.`);
运行它:1
node unpack.mjs
你会看到类似这样的输出:1
2
3
4Reading source map...
Found 847 source files.
Done! Wrote 847 files to unpacked
Skipped 0 files with no content.
八百多个 TypeScript 文件,全部展开在你面前。现在,unpacked/src/ 目录下的结构和你在卷一、卷二里看到的完全一样。
第二步:理解仓库结构
不管你用的是哪种方式,最终你面对的是同一个结构。打开源码根目录:1
2
3
4
5
6
7
8claude-code-source/
package.json # npm 包配置
bun.lock # Bun 锁文件
cli.js # 打包后的入口(一个巨大的 JS 文件)
cli.js.map # source map 文件
README.md # 说明文档
src/ # 源码目录——你的游乐场
vendor/ # 原生模块的 C 源码
src/ 目录下的结构,你在卷二第一章已经见过了。快速回忆一下关键位置:1
2
3
4
5
6
7
8
9
10
11
12
13
14src/
entrypoints/
cli.tsx # 命令行入口,所有启动路径的起点
init.ts # 初始化逻辑(配置、网络、遥测)
main.tsx # 主应用逻辑,Commander 命令定义
ink.tsx # 终端 UI 渲染
components/ # React/Ink UI 组件
tools/ # 工具实现(Bash、Read、Write、Edit...)
services/ # 核心服务(API、MCP、分析)
hooks/ # React hooks
utils/ # 工具函数
constants/ # 常量和系统提示词
cli/ # CLI 子命令处理器
context/ # 上下文管理
你需要记住的关键入口点:
| 文件 | 作用 | 你在卷几认识的 |
|---|---|---|
src/entrypoints/cli.tsx | 最先执行的代码,处理 --version、--daemon-worker 等快速路径 | 卷二第 2 章 |
src/entrypoints/init.ts | 配置、网络、代理、遥测的初始化 | 卷二第 2 章 |
src/main.tsx | Commander 命令定义、REPL 启动 | 卷一第 2 章 |
src/tools.ts | 工具注册表,getAllBaseTools() | 卷二第 5 章 |
第三步:安装运行时——Bun
Claude Code 是用 Bun 构建和打包的。你可以在源码里看到 import { feature } from 'bun:bundle' 这样的 Bun 特有导入。运行源码需要 Bun 环境。
检查你的系统是否已经安装了 Bun:1
bun --version
如果没有安装,用官方安装脚本:1
curl -fsSL https://bun.sh/install | bash
安装完成后重启终端,确认版本:1
2bun --version
# 应该输出 1.x.x 或更高版本
Claude Code 的 package.json 里标注了 Node.js 引擎要求:1
2
3"engines": {
"node": ">=18.0.0"
}
但注意,这个仓库不是用 Node.js 运行的——它是用 Bun 打包的。
第四步:从源码运行 Claude Code
这里要说实话:因为这个源码是从 npm 包里提取出来的,而不是 Anthropic 的内部 monorepo,所以你不能简单地 npm install && npm run dev。没有构建脚本,没有 TypeScript 编译配置,没有开发服务器。
但这并不意味着你什么都不能做。你面对的是一个已经构建好的发布包加上完整的原始 TypeScript 源码的组合。这意味着:
方式 A:直接运行打包后的 cli.js1
2# 在 npm 包解压后的 package/ 目录
node cli.js --version
输出类似:1
2.1.88 (Claude Code)
你可以直接用 node cli.js 替代全局安装的 claude 命令。所有参数都一样:1
2
3
4
5
6
7
8# 启动交互式会话
node cli.js
# 打印版本
node cli.js --version
# 非交互式查询
node cli.js -p "你好"
方式 B:安装依赖后用 Bun 运行1
2# 在源码根目录
bun install
package.json 的 dependencies 字段是空的(所有依赖都被打包进了 cli.js),但 bun.lock 文件里记录了原始的依赖树。bun install 会根据锁文件尝试恢复依赖。
如果这一步成功了,你就可以:1
bun run src/entrypoints/cli.tsx
如果安装依赖遇到了问题,别担心。方式 A——直接用 node cli.js——永远可用,而且对于学习源码来说已经足够了。
第五步:设置调试环境
你不需要一个完美的”dev server”来调试代码。以下是几种实用的调试方法,从简单到复杂排列。
方法 1:console.log 调试法
最简单也最有效。你有源码,你可以修改它。
但等一下——你改的是 src/entrypoints/cli.tsx,而 cli.js 是打包后的文件。修改 TypeScript 源码不会影响打包后的输出。
这就是提取源码仓库的局限性。src/ 目录是参考用的,实际运行的是 cli.js。
方法 2:修改打包后的文件
既然实际运行的是 cli.js,你可以直接在 cli.js 里加日志。虽然它是一个巨大的单文件(可能有几十万行),但你可以用搜索定位到你想修改的地方。
打开 cli.js,搜索 async function main(),在函数开头加上:1
console.log('[DEBUG] main() called with:', process.argv.slice(2));
然后运行:1
node cli.js --version
你会看到你的调试输出。这个方法粗暴,但管用。
方法 3:Node.js 调试器
用 Node.js 内置的调试器:1
node --inspect-brk cli.js
然后在 Chrome 浏览器里打开 chrome://inspect,点击 “inspect” 链接,就可以用 DevTools 设置断点、单步执行了。
方法 4:VS Code 调试配置
如果你用 VS Code,可以在项目根目录创建 .vscode/launch.json:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Claude Code",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/cli.js",
"args": ["--version"],
"console": "integratedTerminal"
},
{
"name": "Debug Claude Code (interactive)",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/cli.js",
"args": [],
"console": "integratedTerminal"
}
]
}
按 F5 就能启动调试。Source map 文件(cli.js.map)的存在意味着 VS Code 有可能把断点映射回 TypeScript 源码——试试看在 src/ 里的 .tsx 文件上设断点。
第六步:探索——用你的新眼睛再看一遍
环境搭好之后,打开 src/ 目录,用你在卷一、卷二学到的知识重新审视它。
试试这些练习:
练习 1:追踪 --version 的路径1
node cli.js --version
在 src/entrypoints/cli.tsx 里找到这段代码:1
2
3
4if (args.length === 1 && (args[0] === '--version' || args[0] === '-v' || args[0] === '-V')) {
console.log(`${MACRO.VERSION} (Claude Code)`);
return;
}
注意它的位置——在所有动态导入之前。这意味着 --version 是零依赖的快速路径,连 startupProfiler 都不加载。为什么?因为版本查询是脚本里最常调用的操作之一,每次调用不应该付出启动整个应用的代价。
练习 2:数一数快速路径
在 cli.tsx 的 main() 函数里,有多少个 “fast-path”?搜索 profileCheckpoint 调用——每一个都标记了一条不同的启动路径。你会发现除了默认的完整 CLI 路径之外,还有 daemon、bridge、bg(后台任务)、templates、environment-runner、self-hosted-runner、tmux worktree 等等。
练习 3:看 init() 的初始化顺序
打开 src/entrypoints/init.ts,看 init() 函数的执行顺序。profileCheckpoint 调用穿插其间:1
2
3
4
5
6
7
8
9init_function_start
→ init_configs_enabled
→ init_safe_env_vars_applied
→ init_after_graceful_shutdown
→ init_after_1p_event_logging
→ init_after_oauth_populate
→ init_network_configured
→ init_scratchpad_created
→ init_completed
这个顺序不是随意的。配置必须先于环境变量应用,环境变量必须先于网络配置,网络配置必须先于 API 预连接。每一步都依赖于前一步的结果。
常见错误
| 常见错误 | 检查方法 |
|---|---|
node cli.js 报 Cannot find module | 确认在正确的目录下,ls -la cli.js 应该显示一个几十 MB 的文件 |
bun install 失败 | 不影响学习,直接用 node cli.js 即可 |
| Source map 断点不生效 | 确认 cli.js.map 和 cli.js 在同一目录 |
bun:bundle 导入报错 | 这是 Bun 打包器特有的,只在打包后的文件中可用 |
| TypeScript 编译报错 | src/ 是参考用的,不需要编译也能阅读理解 |
遇到问题?
Node 版本不对
1 | node --version |
推荐使用 nvm(Node Version Manager)管理 Node 版本:1
2nvm install 20
nvm use 20
依赖安装相关问题
这个仓库的 package.json 里 dependencies 是空对象——所有运行时依赖都已经打包进了 cli.js。如果你尝试 bun install 或 npm install 看到什么都没装,这是正常的。
试试看
- 运行
node cli.js --help,浏览完整的命令行帮助信息。看看你认识多少命令——它们对应源码里的哪些文件? - 在
cli.js中搜索profileCheckpoint,数一数有多少条启动路径。对比你在卷二第 2 章学到的启动流程图。 - 打开
src/tools/目录,浏览工具列表。你在卷二第 5 章学过哪些工具?看看它们的目录结构是否如你所料。
验证——你的工坊搭好了吗?
依次运行以下命令,确认每一步的输出符合预期:
检查 1:源码完整性1
2ls src/entrypoints/cli.tsx
# 应该显示文件存在
检查 2:运行版本命令1
2node cli.js --version
# 应该输出:2.1.88 (Claude Code) 或类似版本号
检查 3:运行帮助命令1
2node cli.js --help
# 应该输出完整的命令行帮助信息
检查 4:启动交互模式(可以 Ctrl+C 退出)1
2
3
4node cli.js
# 应该看到 Claude Code 的交互式界面启动
# 如果你有有效的 API key,可以输入一条消息测试
# 如果没有,看到启动画面就够了,按 Ctrl+C 退出
四个检查都通过了?恭喜,你的工坊搭好了。
你面前是一台完整的引擎,拆解开躺在工作台上。每一颗螺丝、每一根线路你都叫得出名字。从下一章开始,你要拿起工具,开始动手修改它了。
检查点
- 技术栈:TypeScript + Bun + React/Ink + Zod v4 + MCP SDK
- 源码获取:克隆仓库或从 npm 包 source map 提取
- 运行方式:
node cli.js直接运行打包后的文件 - 调试方法:console.log、Node.js Inspector、VS Code + source map
- 关键入口:
cli.tsx→init.ts→main.tsx→tools.ts