Skip to content

S12: CLI & Architecture (CLI 与架构) ​

核心洞察:Claude Code 不只是一个 REPL —— 它是一个多模式运行时,支持交互式、后台、远程、守护进程等多种运行方式,全部从同一个入口分发。

核心问题 ​

一个工业级 CLI 工具需要处理:

  • 多种运行模式(交互式、管道、后台、远程)
  • 启动性能(13MB 编译产物的快速启动)
  • 命令路由(101 个子命令)
  • 配置加载(7 层设置来源)
  • 进程管理(守护进程、后台会话)

源码定位 ​

文件职责
src/entrypoints/cli.tsxCLI 入口点
src/main.tsx主程序初始化
src/commands/101 个命令目录
src/bridge/REPL 桥接层
src/bridge/replBridge.tsREPL 桥接核心 (~10K 行)

入口点 —— 快速路径分发 ​

启动的第一步是快速路径检查,在加载完整程序之前处理简单请求:

typescript
// src/entrypoints/cli.tsx - 简化
async function main() {
  const args = process.argv.slice(2)

  // ===== 快速路径 (零导入,毫秒级响应) =====

  // --version: 不需要加载任何模块
  if (args.includes('--version') || args.includes('-v')) {
    console.log(VERSION)
    process.exit(0)
  }

  // --dump-system-prompt: 输出系统提示词
  if (feature('DUMP_SYSTEM_PROMPT') && args.includes('--dump-system-prompt')) {
    await dumpSystemPrompt()
    process.exit(0)
  }

  // ===== 编译时特性门控的快速路径 =====

  // 远程控制模式
  if (feature('BRIDGE_MODE') && ['remote-control', 'rc', 'remote'].includes(args[0])) {
    const { startBridge } = await import('./bridge/bridgeMain')
    await startBridge()
    return
  }

  // 守护进程模式
  if (feature('DAEMON') && args[0] === 'daemon') {
    const { startDaemon } = await import('./daemon/main')
    await startDaemon()
    return
  }

  // 后台会话管理
  if (feature('BG_SESSIONS') && ['ps', 'logs', 'attach', 'kill'].includes(args[0])) {
    const { handleBgCommand } = await import('./bg/commands')
    await handleBgCommand(args[0], args.slice(1))
    return
  }

  // ===== 主路径 =====
  const { startMain } = await import('./main')
  await startMain(args)
}

编译时特性标志 ​

typescript
// feature() 在编译时被替换为 true/false
// Bun 的死代码消除会移除不需要的代码路径
if (feature('BRIDGE_MODE')) {
  // 这段代码只在支持远程控制的构建中存在
}

效果:不需要的功能在编译时就被剔除,减小了产物体积和启动时间。

主程序初始化 ​

typescript
// src/main.tsx - 简化的初始化流程
async function startMain(args: string[]) {
  // ===== 并行初始化 (性能优化) =====
  const [
    mdmSettings,        // MDM 设备管理设置
    keychainToken,      // Keychain API Key
    bootstrapData,      // API 启动数据
  ] = await Promise.all([
    loadMDMSettings(),
    prefetchKeychainToken(),
    fetchBootstrapData(),
  ])

  // ===== 顺序初始化 =====

  // 1. 系统上下文
  const systemContext = await buildSystemContext({
    platform: process.platform,
    shell: process.env.SHELL,
    cwd: process.cwd(),
  })

  // 2. 用户上下文
  const userContext = await buildUserContext({
    settings: await loadAllSettings(),
    auth: await resolveAuth(),
  })

  // 3. 策略限制
  const policyLimits = await loadPolicyLimits()

  // 4. 工具注册
  const tools = await getTools({
    settings: userContext.settings,
    mcpConfigs: await loadMCPConfig(),
  })

  // 5. 命令注册 (Commander.js)
  const program = createCommanderProgram()
  registerAllCommands(program, tools, userContext)

  // 6. 启动 REPL
  await startRepl(program, tools, systemContext, userContext)
}

并行初始化 ​

启动                  ──┬── MDM 设置加载
                       ├── Keychain 预取
                       └── Bootstrap API
                       │
时间 ─────────────────────────────────────────→
                       │
                       ▼
                    并行完成 → 顺序初始化 → REPL 启动

三个独立的 I/O 操作并行执行,减少了启动时间。

命令系统 ​

101 个命令通过 Commander.js 注册:

src/commands/
├── init/           # 初始化项目
├── config/         # 管理配置
├── doctor/         # 诊断问题
├── export/         # 导出对话
├── help/           # 帮助
├── insights/       # 使用分析
├── permissions/    # 权限管理
├── ps/             # 后台会话列表
├── update/         # 自我更新
├── install-github-app/  # GitHub App 安装
├── install-slack-app/   # Slack App 安装
└── ... (101 个目录)

每个命令是一个目录,包含 index.ts 导出命令定义:

typescript
// src/commands/doctor/index.ts - 命令定义模式
export const doctorCommand = {
  name: 'doctor',
  description: 'Check for common issues',
  action: async (options) => {
    // 检查 API Key
    const apiKeyOk = await checkApiKey()
    // 检查网络连接
    const networkOk = await checkNetwork()
    // 检查 MCP 服务器
    const mcpOk = await checkMCPServers()
    // ...
    displayResults({ apiKeyOk, networkOk, mcpOk })
  },
}

REPL 层 ​

交互式 REPL 是 Claude Code 最主要的使用方式:

typescript
// REPL 循环 (简化)
async function startRepl(tools, context) {
  const queryEngine = new QueryEngine({ tools, context })

  while (true) {
    // 1. 等待用户输入
    const input = await getUserInput()

    // 2. 检查是否是命令
    if (input.startsWith('/')) {
      await handleSlashCommand(input)
      continue
    }
    if (input.startsWith('!')) {
      await executeShellCommand(input.slice(1))
      continue
    }

    // 3. 提交给 Agent
    for await (const message of queryEngine.submitMessage(input)) {
      renderMessage(message)  // 流式渲染到终端
    }
  }
}

REPL 快捷键 ​

Enter          # 提交输入
Shift+Enter    # 多行输入
Ctrl+C         # 中断当前操作
Ctrl+D         # 退出
Escape          # 取消当前输入
↑/↓            # 历史导航
/              # 斜杠命令
!              # Shell 命令

多模式运行 ​

管道模式 ​

bash
echo "fix the bug in auth.ts" | claude
cat error.log | claude "explain this error"

非交互模式 ​

bash
claude -p "create a hello world app" --output-format json

后台模式 ​

bash
claude --bg "run all tests and fix failures"
# 返回 session ID,不阻塞终端

远程控制模式 ​

bash
claude remote-control
# 本地机器作为 bridge,接收远程指令

编译与产物 ​

源码 (TypeScript + JSX)
  │
  ▼ Bun 编译
  │
  ├── cli.js (13MB) ─── 单文件可执行
  │   ├── 所有 TypeScript 编译为 JavaScript
  │   ├── 所有依赖打包 (node_modules)
  │   ├── React/Ink 运行时
  │   └── 编译时特性标志内联
  │
  ├── cli.js.map (59MB) ─── Source Map (调试用)
  │
  └── sdk-tools.d.ts ─── SDK 类型定义

为什么单文件?

  • npm 安装后不需要 node_modules
  • 启动时不需要模块解析
  • 分发简单(一个文件)

vendor 原生绑定 ​

vendor/
├── audio-capture/    # 语音输入 (Node.js native addon)
├── ripgrep/          # 搜索引擎 (Rust 编译的二进制)
├── image-processor/  # 图片处理 (Sharp)
└── url-handler/      # URL 处理

这些是平台相关的原生二进制,不能打包到 cli.js 中。

完整架构回顾 ​

                    ┌─────────────────────┐
                    │    用户输入          │
                    └──────────┬──────────┘
                               │
          ┌────────────────────┼───────────────────┐
          │                    │                    │
    ┌─────▼─────┐      ┌──────▼──────┐     ┌──────▼──────┐
    │ 交互式 REPL│      │ 管道/非交互  │     │ 后台/远程    │
    └─────┬─────┘      └──────┬──────┘     └──────┬──────┘
          │                    │                    │
          └────────────────────┼───────────────────┘
                               │
                    ┌──────────▼──────────┐
                    │    QueryEngine      │  ← 消息生命周期
                    └──────────┬──────────┘
                               │
                    ┌──────────▼──────────┐
                    │   Agent Loop        │  ← while(true)
                    │   (query.ts)        │
                    └──────────┬──────────┘
                               │
                 ┌─────────────┼─────────────┐
                 │             │             │
          ┌──────▼──────┐     │      ┌──────▼──────┐
          │ System      │     │      │ Compact     │
          │ Prompt      │     │      │ (压缩)      │
          └─────────────┘     │      └─────────────┘
                              │
                    ┌─────────▼─────────┐
                    │   API Client      │  ← 流式调用
                    │   (claude.ts)     │
                    └─────────┬─────────┘
                              │
              ┌───────────────┼───────────────┐
              │               │               │
       ┌──────▼──────┐ ┌─────▼──────┐ ┌──────▼──────┐
       │   Hooks     │ │ Permissions│ │  Tools      │
       │  (Pre/Post) │ │ (6 stages) │ │ (45+ 内置)  │
       └─────────────┘ └────────────┘ └──────┬──────┘
                                             │
                              ┌──────────────┼──────────────┐
                              │              │              │
                       ┌──────▼──┐    ┌──────▼──┐    ┌─────▼──────┐
                       │ Bash    │    │ File    │    │ Agent      │
                       │ Tool    │    │ Tools   │    │ (子代理)    │
                       └─────────┘    └─────────┘    └────────────┘
                                                          │
                                                     ┌────▼────┐
                                                     │ MCP     │
                                                     │ Servers │
                                                     └─────────┘

从 30 行到 30,000+ 行 ​

回顾整个教程的演进:

S01: Agent Loop      ── while(true) + stop_reason
  │
  ├── S02: Tools     ── 统一 Tool 接口 + 分发表
  │     │
  │     └── S03: Permissions ── 多层权限检查管线
  │
  ├── S04: System Prompt ── 动态构建 + CLAUDE.md
  │     │
  │     └── S05: Compact ── 三层上下文压缩
  │
  ├── S06: Subagents ── 独立上下文 + 工具集隔离
  │     │
  │     ├── S07: MCP  ── 标准协议扩展
  │     │
  │     └── S08: Hooks ── 事件驱动中间件
  │
  └── S09: Streaming ── 全链路异步生成器
        │
        ├── S10: Skills ── 两层按需注入
        │
        ├── S11: State  ── 会话持久化与恢复
        │
        └── S12: CLI    ── 多模式运行时

每一层都是在 Agent Loop 的基础上叠加的。去掉所有层之后,最核心的依然是那 30 行代码:

while (stop_reason === "tool_use") {
    response = callLLM(messages)
    results = executeTools(response)
    messages.push(response, results)
}

关键设计决策 ​

为什么用 Bun 而不是 Node.js? ​

  1. 编译速度 —— Bun 的打包速度远快于 webpack/esbuild
  2. 单文件输出 —— 内置的打包能力,不需要额外工具
  3. TypeScript 原生 —— 不需要 tsc 编译步骤
  4. 启动速度 —— Bun 运行时启动更快

为什么用 Ink (React for Terminal)? ​

  1. 组件化 —— 复杂的终端 UI 用组件拆分更清晰
  2. 状态管理 —— React 的 hooks 和 context 在终端也好用
  3. 声明式 —— 描述"UI 应该是什么样",而不是"如何更新 UI"

为什么 101 个命令各自独立目录? ​

  1. 按需加载 —— 每个命令单独导入,不执行的命令不加载
  2. 关注分离 —— 每个命令独立开发和测试
  3. 可发现 —— 目录结构即命令清单

本章小结 ​

本教程从 30 行的 Agent Loop 出发,逐层拆解了 Claude Code 的完整架构:

章节核心概念源码位置
S01Agent Loopquery.ts, QueryEngine.ts
S02Tool 系统Tool.ts, tools/
S03权限系统utils/permissions/
S04系统提示词utils/systemPrompt.ts, utils/claudemd.ts
S05上下文压缩services/compact/
S06子代理tools/AgentTool/
S07MCP 协议services/mcp/
S08钩子系统utils/hooks.ts
S09流式处理services/api/claude.ts
S10技能系统tools/SkillTool/, services/plugins/
S11状态管理state/, utils/sessionStorage.ts
S12CLI 架构entrypoints/cli.tsx, main.tsx

最终理解:Claude Code 是一个以 Agent Loop 为核心,以统一 Tool 接口为扩展点,以权限系统为安全保障,以流式处理为用户体验基础的工业级 AI 编程代理。它的每一层设计都服务于一个目标 —— 让 AI 安全、高效、可靠地帮助人类编写代码。

基于 Claude Code v2.1.88 源码分析