Appearance
02 — 入口与启动流程
概述
Claude Code 有三种运行模式,对应三个入口点。理解启动流程是阅读后续代码的基础——因为启动阶段决定了哪些模块被初始化、哪些被跳过,以及运行时状态的初始值。
| 模式 | 入口 | 场景 |
|---|---|---|
| 交互式 REPL | main.tsx → replLauncher.tsx | 终端中直接运行 claude |
| 非交互式 (pipe/print) | main.tsx → cli/print.ts | claude --print "..." 或 管道输入 |
| MCP Server | entrypoints/mcp.ts | 其他工具通过 MCP 协议调用 Claude Code |
| SDK 嵌入 | entrypoints/sdk/ | 第三方代码通过 SDK 直接调用 |
三种模式最终都收敛到同一个 query 循环(query.ts),只是 UI 层和 IO 层不同。
启动时序
阶段 1: 并行预取 — 为什么在 import 之前?
main.tsx 的前 20 行是精心设计的启动优化。在 Bun 解析后续 ~135ms 的 import 时,已经有三个异步操作在并行执行:
typescript
// 这些副作用必须在所有其他 import 之前执行:
profileCheckpoint('main_tsx_entry'); // 标记入口时间
startMdmRawRead(); // 启动 plutil 子进程读取 MDM 受管配置
startKeychainPrefetch(); // 启动 macOS Keychain 读取 (OAuth + 旧版 API Key)这种"在 import 期间做 IO"的模式看起来不寻常,但在 CLI 工具中是有效的——用户感知到的启动时间 = max(import 时间, IO 时间),而不是两者之和。
阶段 3: setup.ts — 做了什么?
setup() 是一次性的会话环境准备。它和 init() 的区别是:init() 是全局的(不依赖具体目录),setup() 是会话级的(依赖 cwd)。
| 步骤 | 具体操作 | 为什么重要 |
|---|---|---|
| Node 版本检查 | 要求 >= 18 | 兼容性保证 |
| Git Root 查找 | findCanonicalGitRoot() | 确定项目边界,CLAUDE.md 搜索范围 |
| CWD 设置 | setCwd() + setProjectRoot() | 所有 Tool 的路径解析基准 |
| Session 初始化 | switchSession() | 创建唯一 Session ID,关联日志/遥测 |
| Worktree (可选) | createWorktreeForSession() | Git worktree 隔离工作区 |
| Hooks 快照 | captureHooksConfigSnapshot() | 运行时检测 hooks 配置变化 |
| File Watcher | initializeFileChangedWatcher() | 监听配置文件变更 |
| Session Memory | initSessionMemory() | 加载上一次会话的记忆上下文 |
| Release Notes | checkForReleaseNotes() | 升级后首次运行提示新功能 |
阶段 4: 运行时组装 — Tool、Command、Prompt 是如何装配的?
这是启动中最关键的步骤。在 REPL 启动前,main.tsx 会把所有 "零件" 装配到一起:
- Tool 加载 (
getTools()): 从tools/目录加载所有内置 Tool 实例,加上 MCP 提供的远程工具 - Command 加载 (
getCommands()): 从commands/目录加载所有斜杠命令定义 - System Prompt 构建 (
getSystemPrompt()): 组装完整的系统提示(见 07-服务层) - MCP 连接: 发现并连接所有配置的 MCP 服务器
- Agent 定义: 加载内置 Agent + 用户自定义 Agent(从
~/.claude/agents/)
所有这些最终汇聚成 AppState 的初始值,传给 REPL 组件。
replLauncher.tsx — 为什么单独一个文件?
tsx
export async function launchRepl(root, appProps, replProps, renderAndRun) {
const { App } = await import('./components/App.js')
const { REPL } = await import('./screens/REPL.js')
await renderAndRun(root, <App {...appProps}><REPL {...replProps} /></App>)
}这个文件虽然只有几行,但它存在的原因是 代码分割:通过 dynamic import,App 和 REPL 的代码(及其依赖的 100+ 组件)只在交互模式下才加载。非交互模式(--print)不会执行这段代码,从而加快冷启动。
entrypoints/ — 其他入口
| 文件 | 运行模式 | 说明 |
|---|---|---|
cli.tsx | 非交互 | --print 模式:只输出文本,不启动 TUI |
init.ts | 所有模式 | 全局初始化(配置、认证、遥测),所有入口都会调用 |
mcp.ts | MCP Server | Claude Code 自身作为 MCP 服务端暴露工具能力 |
sdk/coreSchemas.ts | SDK | SDK 的输入/输出 JSON Schema |
sdk/coreTypes.ts | SDK | SDK 的 TypeScript 类型定义 |
sdk/controlSchemas.ts | SDK | SDK 控制命令的 Schema |
agentSdkTypes.ts | SDK | Agent SDK 类型定义 |
sandboxTypes.ts | 所有模式 | 沙箱类型定义 |
入口收敛图
无论哪种入口,最终都通过 query.ts 的 query() 生成器与 Claude API 通信。这个生成器是整个系统的核心,详见 04-REPL 与 Query 循环。
Feature Flags — 编译期代码分割
项目使用 bun:bundle 的 feature() 在 构建时 决定代码分支。这不是运行时 if/else,而是在 bundle 时直接删除不需要的代码路径:
typescript
import { feature } from 'bun:bundle'
// 构建时如果 COORDINATOR_MODE=false,整个 require 和 coordinatorModule 会被删除
const coordinatorModule = feature('COORDINATOR_MODE')
? require('./coordinator/coordinatorMode.js') : null| Feature Flag | 功能 | 说明 |
|---|---|---|
COORDINATOR_MODE | 协调者模式 | 多 Agent 编排的高级模式 |
KAIROS | Assistant 模式 | Kairos 助手(含 Dream、Brief 等) |
BRIDGE_MODE | IDE Bridge | VS Code / JetBrains 集成 |
DAEMON | 后台守护进程 | 远程控制服务端 |
VOICE_MODE | 语音输入 | 语音转文字 |
HISTORY_SNIP | 历史截断 | 对话历史管理优化 |
WORKFLOW_SCRIPTS | 工作流脚本 | 可编排的自动化脚本 |
PROACTIVE | 主动建议 | 模型主动提出操作建议 |
CCR_REMOTE_SETUP | 远程设置 | claude.ai Web 端远程控制 |
EXPERIMENTAL_SKILL_SEARCH | 实验性 Skill 搜索 | 基于语义的 Skill 查找 |
这意味着不同的构建配置会产生功能不同的二进制文件。公开发行版通常只启用稳定的 feature flags。