第 29 章:搭建你的工坊

个人公众号

源码验证日期:2026-05-15,基于 commit 0d81bb6

卷一,你跟着一条消息走完了它的全程旅程。
卷二,你打开了引擎室的门,一台一台地拆开那些引擎看它们的内部构造。

现在,卷三。

这一卷,你要从一个读者变成一个工匠。不再是看别人造好的房子,而是走进工坊,拿起锤子和锯子,自己动手。

但在你拿起锤子之前,工坊得先搭好。


路线图

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
graph LR
CH29["🔧 第 29 章<br/>搭建你的工坊"] --> CH30["第 30 章<br/>第一次修改"]
CH30 --> CH31["第 31 章<br/>创建你的第一个工具"]
CH31 --> CH32["第 32 章<br/>处理用户输入"]
CH32 --> CH33["第 33 章<br/>添加权限规则"]
CH33 --> CH34["第 34 章<br/>接入 MCP Server"]
CH34 --> CH35["第 35 章<br/>构建多 Agent 协作"]
CH35 --> CH36["第 36 章<br/>开发完整插件"]
CH36 --> CH37["第 37 章<br/>编写测试"]
CH37 --> CH38["第 38 章<br/>调试的艺术"]
CH38 --> CH39["第 39 章<br/>从代码到贡献"]
CH39 --> CH40["第 40 章<br/>第一个真实贡献"]

style CH29 fill:#4CAF50,color:#fff,stroke:#333
style CH30 fill:#e1f5fe,stroke:#333
style CH31 fill:#e1f5fe,stroke:#333
style CH32 fill:#e1f5fe,stroke:#333
style CH33 fill:#e1f5fe,stroke:#333
style CH34 fill:#e1f5fe,stroke:#333
style CH35 fill:#e1f5fe,stroke:#333
style CH36 fill:#e1f5fe,stroke:#333
style CH37 fill:#e1f5fe,stroke:#333
style CH38 fill:#e1f5fe,stroke:#333
style CH39 fill:#e1f5fe,stroke:#333
style CH40 fill:#e1f5fe,stroke:#333

这一章结束时,你将拥有什么

  1. 源码完整地躺在你的硬盘上,你可以用任何编辑器打开它
  2. 你可以用 Bun 从源码直接运行 Claude Code,看到它的输出
  3. 你知道怎么加日志、怎么追踪执行流、怎么验证你的修改
  4. 你理解这个源码仓库的”特殊之处”——它不是 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
终端 UIReact + 自定义 Ink fork(src/ink/
验证Zod v4
MCP 协议@modelcontextprotocol/sdk
AI SDK@anthropic-ai/sdk
测试Vitest
可观测性OpenTelemetry

记住这张表。接下来整个卷三,你都会和这些技术打交道。


第一步:获取源码

Claude Code 的源码仓库本质上是从 npm 包里提取出来的。有两种方式获取:

方式一:直接克隆仓库

1
2
git 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
34
import { 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
4
Reading 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
8
claude-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
14
src/
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.tsxCommander 命令定义、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
2
bun --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.js

1
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.jsondependencies 字段是空的(所有依赖都被打包进了 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
4
if (args.length === 1 && (args[0] === '--version' || args[0] === '-v' || args[0] === '-V')) {
console.log(`${MACRO.VERSION} (Claude Code)`);
return;
}

注意它的位置——在所有动态导入之前。这意味着 --version 是零依赖的快速路径,连 startupProfiler 都不加载。为什么?因为版本查询是脚本里最常调用的操作之一,每次调用不应该付出启动整个应用的代价。

练习 2:数一数快速路径

cli.tsxmain() 函数里,有多少个 “fast-path”?搜索 profileCheckpoint 调用——每一个都标记了一条不同的启动路径。你会发现除了默认的完整 CLI 路径之外,还有 daemonbridgebg(后台任务)、templatesenvironment-runnerself-hosted-runnertmux worktree 等等。

练习 3:看 init() 的初始化顺序

打开 src/entrypoints/init.ts,看 init() 函数的执行顺序。profileCheckpoint 调用穿插其间:

1
2
3
4
5
6
7
8
9
init_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.jsCannot find module确认在正确的目录下,ls -la cli.js 应该显示一个几十 MB 的文件
bun install 失败不影响学习,直接用 node cli.js 即可
Source map 断点不生效确认 cli.js.mapcli.js 在同一目录
bun:bundle 导入报错这是 Bun 打包器特有的,只在打包后的文件中可用
TypeScript 编译报错src/ 是参考用的,不需要编译也能阅读理解

遇到问题?

Node 版本不对

1
2
node --version
# 如果低于 v18.0.0,需要升级

推荐使用 nvm(Node Version Manager)管理 Node 版本:

1
2
nvm install 20
nvm use 20

依赖安装相关问题

这个仓库的 package.jsondependencies 是空对象——所有运行时依赖都已经打包进了 cli.js。如果你尝试 bun installnpm install 看到什么都没装,这是正常的。


试试看

  1. 运行 node cli.js --help,浏览完整的命令行帮助信息。看看你认识多少命令——它们对应源码里的哪些文件?
  2. cli.js 中搜索 profileCheckpoint,数一数有多少条启动路径。对比你在卷二第 2 章学到的启动流程图。
  3. 打开 src/tools/ 目录,浏览工具列表。你在卷二第 5 章学过哪些工具?看看它们的目录结构是否如你所料。

验证——你的工坊搭好了吗?

依次运行以下命令,确认每一步的输出符合预期:

检查 1:源码完整性

1
2
ls src/entrypoints/cli.tsx
# 应该显示文件存在

检查 2:运行版本命令

1
2
node cli.js --version
# 应该输出:2.1.88 (Claude Code) 或类似版本号

检查 3:运行帮助命令

1
2
node cli.js --help
# 应该输出完整的命令行帮助信息

检查 4:启动交互模式(可以 Ctrl+C 退出)

1
2
3
4
node 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.tsxinit.tsmain.tsxtools.ts

上一章:你已经是引擎师了 | 下一章:第一次修改