学习工作台专题学习
工作台/Column Docs/深入 Claude Code 源码
返回专题目录
2026年7月11日14 分钟

第 1 章项目全景与四种入口形态一个 AI CLI 产品的技术蓝图

墨圆
墨圆团队发布于 2026年7月11日

来源与授权

本文来自 Claude-Code-Source-Study 原文,固定导入版本为 31b6e07a82d4。Copyright (c) 2026 Yao,依据 MIT License 提供。

本站仅按“github-public-noncommercial-2026-07-11”用于匿名、免费、非商业学习,并调整章节编排、站内链接、重复标题与阅读标记;不代表原作者对本站的合作、认证或背书。如权利方要求删除,我们会立即下架;授权或删除请求请通过本站首页底部公开联系方式联系。

本章是《深入 Claude Code 源码》系列的开篇。我们将从技术栈选型、启动链路、模块依赖三个维度,建立对整个项目的全局认知。

为什么要从全景开始?

当你面对一个约 1835 个 TypeScript 源码文件(含 .tsx)、最大单文件 main.tsx 4683 行的大型项目时,最大的挑战不是读懂某个函数,而是不知道从哪里开始读

Claude Code 是 Anthropic 公司开发的 AI 驱动命令行编程助手。它不是一个简单的 API wrapper —— 它是一个完整的 AI Agent 运行时,包含了从终端 UI 渲染、多 Agent 编排、工具系统、权限安全到 Prompt 工程的全技术栈。理解它的全貌,等于理解了一个生产级 AI 产品的完整架构。

本章将回答四个核心问题:

  1. 同一份源码是怎么对外露出的?entrypoints/ 下四种入口形态(CLI / SDK / MCP server / Sandbox runner)共用一份代码
  2. 为什么选择这些技术? — Bun + TypeScript + Ink + Commander.js 的技术栈选型逻辑
  3. 程序是怎么启动的? — 从 cli.tsx 到 REPL 的启动链路
  4. 代码是怎么组织的? — 约 1835 个 TypeScript 文件的模块依赖全景

入口形态全景:同一份源码,四张脸

打开 entrypoints/ 目录,你会看到的不是一个 main.ts,而是六个并列的文件 / 目录:

代码示例
entrypoints/
├── cli.tsx              # 入口形态一:交互式 / 非交互式 CLI
├── sdk/                 # 入口形态二:Agent SDK 的内部实现
│   ├── controlSchemas.ts
│   ├── coreSchemas.ts
│   └── coreTypes.ts
├── agentSdkTypes.ts     # 入口形态二的公共表面(@npm 包导出)
├── mcp.ts               # 入口形态三:把自己作为 MCP server 启动
├── sandboxTypes.ts      # 入口形态四:Sandbox runner 的配置 schema
└── init.ts              # 共享初始化模块(仅 main.tsx 的 preAction 真正调用)

这种"一份源码,多个入口"的结构是 Claude Code 区别于普通 CLI 的关键。它意味着同一份工具实现、同一套权限系统、同一套 prompt 工程,可以被四种完全不同的外部使用者调用。

1.1 入口形态一:CLI(cli.tsx

cli.tsx 是用户在终端键入 claude 后真正执行的那个文件。但它本身并不是主程序 —— 它的角色更像门房:在加载重型模块之前,先看清来人是谁,能就地打发的就当场打发,省下后续几百毫秒的启动开销。

最极端的例子是 --version

TYPESCRIPT
// entrypoints/cli.tsx:36-42
if (args.length === 1 && (args[0] === '--version' || args[0] === '-v' || args[0] === '-V')) {
  // MACRO.VERSION is inlined at build time
  console.log(`${MACRO.VERSION} (Claude Code)`)
  return
}

这段代码只用了一个被编译期内联的常量,连字符串拼接都被 Bun bundler 折叠掉了 —— 也就是说,claude --version 的执行链路里除了 cli.tsx 本身,不加载任何其他模块

沿着同样的思路,cli.tsx 一口气定义了十几条快速路径,覆盖 --dump-system-promptdaemonremote-control / bridge / sync、后台任务管理(ps / logs / attach / kill)、模板任务(new / list / reply)、environment-runner 等等。每一条都遵循同一个套路:参数命中 → 动态 import 只跟这条路径相关的少数模块 → 干完事就 return

只有当所有快速路径都不匹配时,才会走到文件末尾那行真正昂贵的 await import('../main.js'),把全量 CLI 拉起来。

1.2 入口形态二:Agent SDK(agentSdkTypes.ts + sdk/

如果你不是在终端里用 claude,而是在自己的 Node 或 Bun 程序里这样写:

TYPESCRIPT
import { query, createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk'

那么真正映射到的入口就是 entrypoints/agentSdkTypes.ts

这个文件有意思的地方在于:它导出的所有运行时函数,比如 querytoolcreateSdkMcpServerunstable_v2_createSession实现体全部是 throw new Error('not implemented')。看起来像废代码,其实是故意的 —— 真正的实现在 SDK 打包发布时由 bundle 注入,源码仓库里只留下一份"声明骨架"。

它的角色其实是 公共 API 表面:把可以暴露给外部用户的类型、可序列化的消息结构、settings schema、tool 类型,统一从 sdk/ 子目录 re-export 出来。同文件内的 type re-export 在编译后不存在运行时实体,自然也不会 throw。

值得一提的是,agentSdkTypes.ts 里的 import 路径写的是 ./sdk/runtimeTypes.js./sdk/controlTypes.js./sdk/settingsTypes.generated.js./sdk/toolTypes.js 这一组 .js 文件,但当前源码树里 sdk/ 目录下真正能看到的 .ts 源文件只有三个:controlSchemas.tscoreSchemas.tscoreTypes.ts。其余几个 .js 路径在受审源码树里查不到 .ts 原型 —— 这是"公共 API 门面"分工下没有纳入本仓库的部分,由发布时的 SDK bundle 补齐。

把"对外接口"和"内部实现"通过单文件门面隔离开,是 Claude Code 发布 SDK 时反复用到的一个手法。

1.3 入口形态三:MCP server(mcp.ts

entrypoints/mcp.ts 解决的是另一个方向的问题 —— 不是"别人怎么调用 Claude Code",而是"Claude Code 怎么把自己暴露成一个标准 MCP server,让别的程序调用"。

入口函数 startMCPServer(cwd, debug, verbose) 拿到工作目录之后,就注册两个标准 MCP handler:

TYPESCRIPT
// entrypoints/mcp.ts:47-57
const server = new Server(
  { name: 'claude/tengu', version: MACRO.VERSION },
  { capabilities: { tools: {} } },
)
  • ListToolsRequestSchema handler 把 Claude Code 内部那一整套工具(Bash、FileRead、FileWrite、Grep……)翻译成 MCP 协议里的 Tool 描述返回。
  • CallToolRequestSchema handler 把外部 MCP 客户端的调用路由到内部 tool.call(),并复用同一套 hasPermissionsToUseTool 权限校验。

需要留意的是,MCP 入口构造出来的 ToolUseContext 是一个简化版isNonInteractiveSession: truemcpClients: []agentDefinitions 为空。也就是说,它不参与多 Agent 编排,也不挂任何 MCP 子客户端 —— 它只做一件事:把自己的工具暴露给外部 MCP 客户端用。

1.4 入口形态四:Sandbox runner(sandboxTypes.ts

第四种入口,乍看最不像入口 —— sandboxTypes.ts 整个文件只做一件事:导出几份 Zod schema,包括 SandboxNetworkConfigSchemaSandboxFilesystemConfigSchemaSandboxSettingsSchema,以及它们 infer 出来的 TypeScript 类型。

但它的确是一种入口形态。当 claude 以 sandbox 模式拉起子命令时,使用的就是这份 schema 校验过的配置 —— enabledfailIfUnavailableautoAllowBashIfSandboxednetworkfilesystem 这些字段会一路传到子进程,去限定它的网络和文件系统访问范围。换句话说,schema 即接口:宿主进程读这份 schema 就知道沙箱能开哪些口子,企业部署方写一份 settings JSON 就能把策略焊进 CLI。

文件内的注释还留下了一些有意思的演化痕迹。比如 enableWeakerNetworkIsolation 这个开关,注释明写 "macOS only: Allow access to com.apple.trustd.agent",目的是让 Go 工具链(gh / gcloud / terraform)在 MITM 代理 + 自签 CA 的场景下能完成 TLS 验证。又比如 autoAllowBashIfSandboxedenabledPlatforms,是为了让 NVIDIA 这样的大客户能在 macOS 上启用沙箱、在 Linux / WSL 上先关掉。这些字段不是凭空设计出来的,而是企业部署一线打回来的需求被原样焊进了源码。

1.5 init.ts:不是第五种入口,是 CLI 的初始化模块

容易让人误会的是 entrypoints/init.ts 也躺在同一个目录下,但它不是第五种入口形态。它是一个普通的初始化模块,对外导出一个 init() 函数。

在当前源码树里,唯一真正 import { init } from './entrypoints/init.js' 并把它接入运行时的,是 main.tsx:在文件顶部 import,在 program.hook('preAction', ...)await init()。也就是说,init() 只在 cli.tsx → main.tsx → Commander preAction 这一条链路上被显式触发。

其余三种入口 —— agentSdkTypes.tsmcp.tssandboxTypes.ts —— 在本仓库源码里都没有 import init

  • agentSdkTypes.ts 的运行时实现由发布时的 SDK bundle 替换,初始化职责由 bundle 自己接管。
  • mcp.ts 由外部 MCP 客户端拉起 startMCPServer,宿主自负初始化。
  • sandboxTypes.ts 只导出 schema,由读 schema 的宿主进程负责自己的初始化。

init() 函数本身用 lodash-es/memoize 包装,无论被 preAction 触发多少次都只执行一次。它做的事情大致按这样的顺序:

  • 启用配置系统(enableConfigs),并把"安全的"环境变量先注入进来(applySafeConfigEnvironmentVariables),给后续的 trust dialog 留出安全缓冲。
  • 提前注入 NODE_EXTRA_CA_CERTS,这一步必须早于第一次 TLS 握手,否则 Bun / BoringSSL 会缓存掉空的证书库。
  • 注册优雅退出、惰性初始化 1P 事件日志、回填 OAuth 账户信息、探测 JetBrains IDE 和 GitHub 仓库环境。
  • 异步加载远端 managed settings 与 policy limits,配置 mTLS 与代理。
  • 对 Anthropic API 提前做一次 TCP + TLS 预连接,与 action handler 的工作并行,省下 100–200ms 的首请求延迟。
  • 仅当 CLAUDE_CODE_REMOTE 为 truthy 时,才懒加载 upstreamproxy/,把 CCR 上游代理挂进去。

因为只有 CLI 主入口走 init(),所以本书后面把"配置即代码"单独拎出来讲的那一章,讨论范围也限定在这条链路上 —— migrations 的演化、企业 MDM 的合并顺序、applyConfigEnvironmentVariables 的两阶段(trust 前的安全子集 / trust 后的完整集),都在这一层发生,不向其余三种入口推广。


一、技术栈选型:为什么是 Bun + TypeScript + Ink?

1.1 运行时:深度依赖 Bun

Claude Code 的构建与部分运行时特性深度依赖 Bun。但需要注意,这并不意味着它是一个 Bun-only 项目 —— 源码中明确检测 Node 版本(setup.ts:69-79),并提供了 isRunningWithBun() 函数来区分运行态(utils/bundledMode.ts:1-22),说明项目同时考虑了非 Bun 环境的兼容。

核心上,选择 Bun 有明确的工程理由:

  • 启动速度:Bun 的冷启动时间远低于 Node.js,对 CLI 工具至关重要。用户输入 claude 到看到界面的时间直接影响体验。
  • 内置 bundler:Bun 的 bun:bundle 模块提供了编译期 feature() 函数,可以实现 Dead Code Elimination(DCE)。这让同一份代码可以构建出内部版和外部版两个不同的产品。
  • TypeScript 原生支持:无需 ts-node 或额外的转译步骤。

更准确地说,项目的构建/打包能力深度依赖 Bun,且部分运行路径针对 Bun 做了优化或假设,但运行态是被区分对待的。

一个关键的 Bun 特性在整个代码库中被大量使用:

TYPESCRIPT
// entrypoints/cli.tsx:1
import { feature } from 'bun:bundle';

// tools.ts:26-28
const SleepTool =
  feature('PROACTIVE') || feature('KAIROS')
    ? require('./tools/SleepTool/SleepTool.js').SleepTool
    : null

feature() 在编译时被替换为 truefalse,配合 require()(而非 import),Bun 的 bundler 可以在编译期直接删除不需要的代码分支,实现真正的零成本抽象。

1.2 语言:TypeScript + Zod

TypeScript 提供静态类型安全。值得注意的是,项目大量使用 Zod 做运行时类型验证 —— 这是因为 AI 模型返回的工具调用参数是动态的,TypeScript 的编译期类型检查无法覆盖。

1.3 终端 UI:Ink(React for CLI)

Claude Code 的终端界面不是用 console.log 拼的,而是用 Ink —— 一个在终端中运行 React 的框架。项目甚至 fork 了 Ink 框架并做了大量深度定制(ink/ 目录约有 96 个文件):

代码示例
ink/
├── reconciler.ts          # 自定义 React reconciler
├── layout/                # 基于 Yoga 的布局引擎
├── render-node-to-output.ts  # 渲染到终端
├── optimizer.ts           # 渲染性能优化
├── line-width-cache.ts    # 行宽缓存
└── ...(共约 96 个文件)

为什么用 React 做终端 UI?因为 Claude Code 的界面是高度动态的:流式输出、多工具并行进度条、权限确认对话框、主题切换……这些用命令式代码管理会变成噩梦,但用 React 的声明式模型就很自然。

1.4 CLI 参数解析:Commander.js

参数解析使用 @commander-js/extra-typings —— Commander.js 的类型增强版。在 main.tsx 中可以看到它被用来定义所有 CLI 选项和子命令。

技术栈总结

层级技术选择选择理由
运行时Bun(主要)快速启动 + 编译期 DCE(兼容 Node.js 运行态)
语言TypeScript类型安全 + 大型项目可维护性
运行时验证ZodAI 返回值的动态验证
终端 UIInk (forked)React 声明式 UI 模型
CLI 解析Commander.js成熟的参数解析方案
布局引擎Yoga终端中的 Flexbox

二、启动链路:从 claude 到 REPL 的调用流程

当用户在终端输入 claude 并回车时,在交互式主路径上,程序大体经过以下几个阶段的初始化,最终启动交互式 REPL。每个阶段都有明确的职责分工:

流程图
图表进入视野后渲染

注意init() 并不是独立的启动层级。它定义在 entrypoints/init.ts 中,但实际是在 main.tsx 的 Commander preAction hook 内被调用的(main.tsx:907-916)。这意味着 init() 的执行时机由 Commander.js 的命令解析流程控制——只有当用户执行一个真正的命令时才会触发,显示帮助信息(--help)时不会执行。

同样需要注意的是,setup() 并非所有命令都会经过的统一启动层。源码明确写到,有些子命令从不调用 setup(),所以 preAction 中需要额外补上 initSinks() 来保证日志不丢失(main.tsx:926-930)。setup() 真正发生在进入交互式 REPL 的默认 action 流程里(main.tsx:1903-1935)。因此,更准确地说,setup() 是"进入交互式会话前的 session setup",而不是所有子命令共用的统一启动阶段。

2.1 第一层:cli.tsx — Bootstrap 入口

文件entrypoints/cli.tsx(约 300 行)

这是程序的真正入口。它的核心设计原则是:尽可能少加载模块,尽可能快返回

TYPESCRIPT
// entrypoints/cli.tsx:33-42
async function main(): Promise<void> {
  const args = process.argv.slice(2);

  // Fast-path for --version/-v: zero module loading needed
  if (args.length === 1 && (args[0] === '--version' || args[0] === '-v' || args[0] === '-V')) {
    console.log(`${MACRO.VERSION} (Claude Code)`);
    return;
  }
  // ...
}

--version 路径实现了零 import 返回 —— 除了 cli.tsx 本身,不加载任何其他模块。MACRO.VERSION 是编译时内联的常量,连字符串拼接的成本都省了。

cli.tsx 定义了大量「快速路径」(fast-path),每个路径只动态 import 必需的模块。以下是主要的快速路径(完整列表还包括 --daemon-worker--claude-in-chrome-mcpenvironment-runnerself-hosted-runner、template jobs、tmux worktree 等,详见源码):

快速路径触发条件加载的模块
--version单参数 -v/-V/--version
--dump-system-prompt首参数匹配config, model, prompts
daemon首参数 daemonconfig, sinks, daemon/main
bridge/remote首参数 remote-controlconfig, auth, bridge
ps/logs/attach/kill首参数匹配config, cli/bg
正常启动无匹配main.tsx(全量)

只有当没有任何快速路径匹配时,才加载最重的 main.tsx

TYPESCRIPT
// entrypoints/cli.tsx:291-298
const { startCapturingEarlyInput } = await import('../utils/earlyInput.js');
startCapturingEarlyInput();
profileCheckpoint('cli_before_main_import');
const { main: cliMain } = await import('../main.js');
profileCheckpoint('cli_after_main_import');
await cliMain();

注意这里用了 profileCheckpoint() 打点 —— 团队在持续监控 import main.js 的耗时,因为这一步会触发大量模块的求值。

2.2 第二层:main.tsx — 主应用编排器

文件main.tsx(约 4683 行)

这是整个项目最大的单文件。它是整个 CLI 应用的编排中心,负责:

  • 用 Commander.js 定义所有 CLI 参数和子命令(configdoctormcp 等)
  • 通过 preAction hook 编排初始化流程:init() → 配置迁移 → 远程设置加载
  • 认证流程(API Key / OAuth / AWS / GCP)
  • 模型选择与验证
  • 权限模式初始化
  • MCP 服务器配置与连接
  • 插件加载与 Agent 定义发现
  • 创建 AppState 并启动 REPL(交互模式)或执行 print 模式(非交互模式)

main.tsx 的前 20 行展示了一个精妙的启动优化技巧 —— 侧效果前置

TYPESCRIPT
// main.tsx:1-20
import { profileCheckpoint, profileReport } from './utils/startupProfiler.js';
profileCheckpoint('main_tsx_entry');  // 立即打点

import { startMdmRawRead } from './utils/settings/mdm/rawRead.js';
startMdmRawRead();  // 立即启动 MDM 子进程

import { ensureKeychainPrefetchCompleted, startKeychainPrefetch }
  from './utils/secureStorage/keychainPrefetch.js';
startKeychainPrefetch();  // 立即启动 Keychain 预取

这些 import 语句之间穿插着函数调用 —— 在后续 135ms 的 import 求值期间,MDM 子进程和 Keychain 读取已经在并行执行了。这是一个巧妙的性能优化:利用 JavaScript 模块求值的阻塞时间来并行执行 I/O。

2.3 init() — 核心初始化(在 main.tsx 的 preAction 中调用)

文件entrypoints/init.ts

init() 定义在单独的文件中,但通过 main.tsx 的 Commander preAction hook 调用(main.tsx:916)。它用 lodash 的 memoize 包装,确保无论被调用多少次都只执行一次:

TYPESCRIPT
// entrypoints/init.ts:57
export const init = memoize(async (): Promise<void> => {
  // 配置验证
  enableConfigs();
  // 环境变量应用(安全的部分)
  applySafeConfigEnvironmentVariables();
  // CA 证书配置
  applyExtraCACertsFromConfig();
  // 优雅退出注册
  setupGracefulShutdown();
  // 遥测初始化
  // 代理配置
  // MTLS 配置
  // Policy limits 加载
  // ...
});

关键设计:init() 区分了「安全环境变量」和「完整环境变量」。在用户接受信任对话框(trust dialog)之前,只应用安全的环境变量。这是安全性考虑 —— 未确认信任的项目不应该能通过 .claude/settings.json 修改关键环境变量。

2.4 setup.ts — 交互式会话的 Session Setup

文件setup.ts(约 477 行)

setup() 处理进入交互式 REPL 前的 session 级初始化。需要强调的是,它只在交互式会话路径上被调用,并非所有子命令(如 configdoctor)都会执行此步骤:

  • 设置工作目录(CWD)
  • Git Worktree 创建(如果启用 --worktree
  • Hooks 配置快照
  • 文件变更监听器初始化
  • Session Memory 初始化
  • Analytics 事件 tengu_started 发送
TYPESCRIPT
// setup.ts:56-66
export async function setup(
  cwd: string,
  permissionMode: PermissionMode,
  allowDangerouslySkipPermissions: boolean,
  worktreeEnabled: boolean,
  // ...
): Promise<void> {
  setCwd(cwd);
  captureHooksConfigSnapshot();
  initializeFileChangedWatcher(cwd);
  // ...
}

2.5 replLauncher.tsx — 启动 REPL

文件replLauncher.tsx(约 22 行)

最终的 REPL 启动异常简洁 —— 它只做一件事:将 <App><REPL> 组件渲染到 Ink 的 React 树中:

TYPESCRIPT
// replLauncher.tsx:12-22
export async function launchRepl(
  root: Root, appProps: AppWrapperProps,
  replProps: REPLProps,
  renderAndRun: (root: Root, element: React.ReactNode) => Promise<void>
): Promise<void> {
  const { App } = await import('./components/App.js');
  const { REPL } = await import('./screens/REPL.js');
  await renderAndRun(root,
    <App {...appProps}>
      <REPL {...replProps} />
    </App>
  );
}

注意 AppREPL 都是动态 import 的 —— 延迟到最后一刻才加载这些重量级 UI 模块。


三、模块依赖全景:约 1835 个文件的组织方式

Claude Code 的源码按职责划分为 10+ 个顶层模块:

流程图
图表进入视野后渲染

3.1 核心文件规模

文件行数职责
main.tsx4683主编排器,CLI 参数定义,启动流程
query.ts1729对话循环,API 调用,工具执行
Tool.ts792Tool 接口定义,buildTool()
commands.ts754命令聚合入口:内建命令 + 动态技能 + 插件 + workflow 命令
tools.ts389工具注册表,getAllBaseTools()
setup.ts477会话初始化
cli.tsx303Bootstrap 入口

3.2 目录结构与职责

代码示例
claude-code-cli/
├── entrypoints/          # 入口点 (cli.tsx, init.ts)
├── state/                # 状态管理 (store, AppState)
├── tools/                # 40+ 工具实现,每个一个目录
│   ├── AgentTool/        # Agent 子系统
│   ├── BashTool/         # Shell 命令执行
│   ├── FileEditTool/     # 文件编辑
│   ├── GlobTool/         # 文件搜索
│   └── ...
├── commands/             # 内建斜杠命令(通过 commands.ts 与技能/插件命令动态聚合)
├── services/             # 核心服务
│   ├── api/              # Anthropic API 客户端
│   ├── compact/          # 上下文压缩
│   ├── mcp/              # MCP 协议实现
│   └── analytics/        # 分析与 Feature Flags
├── components/           # 380+ UI 组件文件
│   └── design-system/    # 设计系统基础组件
├── ink/                  # Fork 的 Ink 框架 (约 96 个文件)
├── utils/                # 工具函数
│   ├── permissions/      # 权限系统
│   ├── settings/         # 多层配置系统
│   └── hooks/            # Hook 系统
├── constants/            # 常量定义(System Prompt 的核心组装逻辑在 prompts.ts 及相关 section 系统中)
├── tasks/                # 任务系统 (并发 Agent)
├── coordinator/          # 多 Agent 协调
├── memdir/               # 自动记忆系统
├── skills/               # 技能框架
├── bridge/               # IDE 远程桥接
└── migrations/           # 配置迁移脚本

3.3 关键数据流

整个应用的核心数据流可以概括为一条主线:

代码示例
用户输入 → query.ts 组装消息 → Anthropic API → 模型返回 tool_use
→ 查找并执行 Tool → 结果回传 API → 模型继续/结束

这条主线由 query.ts 驱动,它是 agent turn orchestration 的核心入口。每一次用户提问,都会触发这个循环,直到模型决定停止使用工具并给出最终回复。需要注意的是,随着项目演进,很多关键逻辑已经分散到 services/tools/toolOrchestration.jsservices/api/*services/compact/* 等模块中,query.ts 更准确地说是对话循环的编排入口,而非唯一的核心。


四、几个值得注意的架构决策

4.1 单文件 vs 模块化的取舍

main.tsx 有 4683 行,query.ts 有 1729 行 —— 这看起来违背了「小文件」原则。但这是有意为之的:

  • main.tsx 是编排器,它不仅需要 import 几乎所有模块来完成初始化,更重要的是它承担了大量 CLI option parsing、feature gate 分支、auth/provider 策略选择、interactive/non-interactive 分流、resume/remote/assistant 等模式编排。它是一个"策略汇聚层",拆分只会把紧密耦合的编排逻辑分散到多个文件,并不会减少复杂度。
  • query.ts 是对话循环,它的逻辑是高度内聚的。拆分会增加理解成本。

4.2 动态 import 的战略性使用

项目中有两种 import 模式:

TYPESCRIPT
// 静态 import:模块求值时立即加载
import { BashTool } from './tools/BashTool/BashTool.js'

// 动态 import:运行到此处时才加载
const { App } = await import('./components/App.js')

规律:核心逻辑用静态 import(确保类型检查),UI 和可选功能用动态 import(延迟加载)。

4.3 懒 require() 的两种用途

项目中大量使用懒 require() 模式,但它服务于两个不同的目的

用途一:打破循环依赖

TYPESCRIPT
// tools.ts:63-66
const getTeamCreateTool = () =>
  require('./tools/TeamCreateTool/TeamCreateTool.js')
    .TeamCreateTool as typeof import('./tools/TeamCreateTool/TeamCreateTool.js').TeamCreateTool

用途二:配合 feature() 实现编译期 DCE

TYPESCRIPT
// main.tsx:74-81, tools.ts:25-53
const SleepTool =
  feature('PROACTIVE') || feature('KAIROS')
    ? require('./tools/SleepTool/SleepTool.js').SleepTool
    : null

这里用 require() 而非 import 是刻意为之:静态 import 会被模块系统无条件加载,即使在 if (false) 分支里也会被打包。而 require() 是运行时调用,配合编译期 feature() 常量折叠,Bun 的 bundler 可以在构建时直接删除整个 require() 调用及其依赖。

共同的模式特征:

  1. 用函数包装 require() —— 只在调用时才执行
  2. as typeof import(...) 保留类型信息 —— 不丢失类型安全
  3. 注释标注原因 —— // Lazy require to break circular dependency 或 feature gate 条件

五、可迁移的设计模式

模式 1:分层启动 + 快速路径

将 CLI 启动分为多层,每层只加载必需的模块。对于简单命令(如 --version),在最早的层级就返回,避免加载整个应用。

适用场景:任何 CLI 工具,特别是启动速度敏感的场景。

模式 2:侧效果前置并行化

利用 JavaScript 模块求值的阻塞时间,在 import 语句之间插入 I/O 操作的启动调用。这样在后续模块加载期间,I/O 已经在并行执行。

适用场景:任何需要在启动时执行 I/O(网络请求、文件读取、子进程)的应用。

模式 3:编译期 Feature Flag + 条件 require

用编译期常量(如 Bun 的 feature())配合条件 require() 实现零成本的功能门控。编译器可以在构建时完全删除不需要的代码分支。

适用场景:需要从同一份代码构建多个版本(如内部版/外部版、免费版/付费版)的项目。



下一章预告

第 2 章:启动链路与冷启动优化 — 毫秒级 CLI 启动的工程艺术

我们将深入 cli.tsx 和 main.tsx 的启动路径,揭示 Claude Code 团队如何将 CLI 启动时间优化到毫秒级别。


全部内容请关注 https://github.com/luyao618/Claude-Code-Source-Study (求一颗免费的小星星)