从 claude 命令敲下到 REPL 界面出现,中间发生了什么?
一、入口链路总览
用户执行: claude "修复 bug" │ ▼ cli.js (npm bin 入口, 单文件打包) │ — 检测 Node ≥ 18 │ — 加载 vendor/ 中的 bundled 依赖 ▼ main.tsx (真正的主入口, 4684 行) │ ├─ [1] 副作用 import: 性能计时、MDM 预读、Keychain 预取 ├─ [2] Commander 解析 CLI 参数 ├─ [3] init() — 核心初始化 ├─ [4] 认证 & 配置 ├─ [5] 迁移脚本执行 ├─ [6] 工具/命令/MCP 服务端加载 └─ [7] launchRepl() — 启动 React Ink 应用
|
二、Phase 1: 副作用 Import(性能优化关键)
main.tsx 的前 20 行是精心设计的 并行预加载:
import { profileCheckpoint } from './utils/startupProfiler.js'; profileCheckpoint('main_tsx_entry');
import { startMdmRawRead } from './utils/settings/mdm/rawRead.js'; startMdmRawRead();
import { startKeychainPrefetch } from './utils/secureStorage/keychainPrefetch.js'; startKeychainPrefetch();
|
设计原理
- 问题: Node.js 模块加载是同步的,import 链会导致 ~135ms 的阻塞
- 解决: 在 import 链的最早期就 spawn 异步子进程,让 I/O 与模块加载并行
- 效果: macOS 上节省约 65ms 的启动时间
三、Phase 2: Commander CLI 解析
main.tsx 使用 @commander-js/extra-typings(类型安全版本的 Commander.js)解析命令行参数:
const program = new CommanderCommand() .name('claude') .argument('[prompt...]', '提供初始 prompt') .option('-p, --print', '非交互模式,输出后退出') .option('--model <model>', '指定模型') .option('--permission-mode <mode>', '权限模式') .option('--resume <sessionId>', '恢复会话') .option('--continue', '继续上一次会话') .option('--max-turns <n>', '最大 turn 数')
|
核心 CLI 模式
| 模式 |
触发方式 |
说明 |
| 交互式 REPL |
claude (无参数) |
启动终端 UI |
| 单次查询 |
claude "问题" |
带 -p 则非交互 |
| 恢复会话 |
claude --resume <id> |
从历史恢复 |
| 继续上次 |
claude --continue |
自动恢复最近会话 |
| 管道模式 |
cat file | claude -p |
从 stdin 读取 |
| SDK/Headless |
通过 SDK 编程调用 |
无 UI |
四、Phase 3: init() — 核心初始化
定义在 src/entrypoints/init.ts,使用 memoize 确保全局只执行一次:
export const init = memoize(async (): Promise<void> => { enableConfigs();
applySafeConfigEnvironmentVariables();
applyExtraCACertsFromConfig();
setupGracefulShutdown();
});
|
初始化顺序的关键约束
enableConfigs() │ ← 必须先加载配置 ▼ applySafeConfigEnvironmentVariables() │ ← 安全的环境变量(不含敏感配置) ▼ applyExtraCACertsFromConfig() │ ← CA 证书必须在任何 TLS 连接之前 ▼ configureGlobalAgents() / configureGlobalMTLS() │ ← 代理配置在 API 调用之前 ▼ preconnectAnthropicApi() │ ← TCP 连接预热 ▼ [可选] initializeTelemetry()
|
五、Phase 4: bootstrap/state.ts — 全局状态单例
这是整个应用的 全局状态中心(1759 行),采用模块级闭包模式:
type State = { originalCwd: string projectRoot: string totalCostUSD: number totalAPIDuration: number cwd: string modelUsage: { [model: string]: ModelUsage } mainLoopModelOverride: ModelSetting | undefined isInteractive: boolean sessionId: SessionId meter: Meter | null }
|
为什么不用 React Context?
因为 state.ts 需要在 非 React 环境 中使用(如 SDK 模式、Headless 模式、子 Agent 进程)。它是进程级的全局单例,通过导出的 getter/setter 函数访问:
export function getSessionId(): SessionId { return state.sessionId } export function setSessionId(id: SessionId) { state.sessionId = id }
|
六、Phase 5: 迁移脚本
main.tsx 会依次执行多个数据迁移脚本(src/migrations/),处理版本升级的配置变更:
migrateFennecToOpus() migrateLegacyOpusToCurrent() migrateOpusToOpus1m() migrateSonnet1mToSonnet45() migrateSonnet45ToSonnet46()
migrateAutoUpdatesToSettings() migrateBypassPermissionsAcceptedToSettings() migrateEnableAllProjectMcpServersToSettings()
|
设计模式
- 每个迁移是幂等的(可重复执行)
- 命名反映了 Anthropic 模型的演进历史:Fennec → Opus → Opus 1M → Sonnet 4.5 → Sonnet 4.6
七、Phase 6: 工具、命令、MCP 加载
const tools = getTools(toolPermissionContext);
const commands = getCommands();
const { tools: mcpTools, commands: mcpCommands } = await getMcpToolsCommandsAndResources(mcpClients);
initBuiltinPlugins();
initBundledSkills();
|
八、Phase 7: 启动 REPL
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> ); }
|
延迟 import 的设计
App.js 和 REPL.js 通过 动态 import 加载
- 目的:避免在非交互模式下加载 React/Ink(节省 ~400ms)
- SDK/Headless 模式完全跳过此步骤
九、启动时序图
时间线 ──────────────────────────────────────────────────────►
[0ms] main.tsx 入口 ├── profileCheckpoint('main_tsx_entry') ├── startMdmRawRead() ──→ [子进程并行] └── startKeychainPrefetch() ──→ [子进程并行]
[10ms] 模块 import 链加载中...
[135ms] Commander 解析 CLI 参数
[140ms] init() 开始 ├── enableConfigs() ├── applySafeConfigEnvironmentVariables() ├── applyExtraCACertsFromConfig() ├── setupGracefulShutdown() └── preconnectAnthropicApi() ──→ [TCP 预热并行]
[200ms] 认证检查 (OAuth / API Key) ├── ensureKeychainPrefetchCompleted() ← 等待 [50ms] 预取 └── 验证 token 有效性
[250ms] 迁移脚本执行
[260ms] 加载工具 + 命令 + MCP
[350ms] GrowthBook 特性标志初始化
[400ms] launchRepl() → 动态 import React/Ink
[550ms] REPL 界面渲染完成 → 用户可输入
|
十、关键设计决策解析
1. 为什么用 Bun 打包但 Node.js 运行?
- Bun 的
feature() 宏在打包时求值,实现 编译期条件分支
- 内部版本(
USER_TYPE=ant)包含 REPL Tool、调试工具等
- 外部版本去除了所有 ant-only 代码,减少包体积
2. 为什么有这么多 eslint-disable 注释?
- 顶层副作用 import 是故意的(性能优化需要在 import 阶段执行)
require() 用于条件加载(dynamic import 返回 Promise,不适合同步场景)
- 这些 lint 规则抑制表明开发团队对代码规范有严格要求,每个例外都是有意为之
3. 为什么 init() 用 memoize?
- 防止在多入口场景(如 SDK + CLI 同时调用)重复初始化
- 保证 OpenTelemetry、优雅退出等全局设施只初始化一次