第二节:cli bootstrap 机制
命令行启动与参数解析

作者:小学子 📚 | 日期:2026年4月2日 | 第一阶段 · 模块一


# 第一阶段 · 模块一 · 第二节:cli.tsx bootstrap 机制深度解析

核心问题

cli.tsx 的 bootstrap 机制是什么?动态导入如何实现零模块加载?feature() Bun DCE 的作用是什么?快速路径的设计有什么讲究?


◇ 本节位置


        Claude Code 全局架构
        
        ┌─────────────────────────────────────────────────────────────────────┐
        │  入口层(entrypoints/)                                             │
        │                                                                      │
        │  cli.tsx ──> main.tsx ──> REPL.tsx (交互模式)                      │
        │                     └──> QueryEngine.ts (SDK/headless)              │
        │                                                                      │
        │  ← 本节内容                                                         │
        └──────────────────────────────┬──────────────────────────────────────┘
                                       ▼
        ┌─────────────────────────────────────────────────────────────────────┐
        │  查询引擎层(query.ts / QueryEngine.ts)                            │
        └─────────────────────────────────────────────────────────────────────┘
        


一、cli.tsx 概览

1.1 源码规模

源码位置:`src/entrypoints/cli.tsx` 第 1-302 行

指标数值
总行数302 行
快速路径数10+ 个
动态导入数20+ 个

1.2 核心职责

cli.tsx 的核心职责是在加载任何业务逻辑之前,先检查特殊标志


        // cli.tsx 的结构
        async function main(): Promise<void> {
          const args = process.argv.slice(2);
        
          // 快速路径 1:--version
          if (...) { return; }
        
          // 快速路径 2:--dump-system-prompt
          if (...) { return; }
        
          // ... 更多快速路径
        
          // 最后:加载完整 CLI
          const { main: cliMain } = await import('../main.js');
          await cliMain();
        }
        


二、--version 零模块加载

2.1 源码实现

源码位置:`src/entrypoints/cli.tsx` 第 36-41 行


        // Fast-path for --version/-v: zero module loading needed
        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;  // ⚠️ 关键:直接返回,不加载任何模块
        }
        

2.2 五问分析

问 1:为什么 --version 需要零模块加载?

用户期望 `--version` 立即响应(毫秒级):


        用户心理模型:
        $ claude --version
        claude-code 2.1.88    ← 期望立即看到输出
        
        实际情况(如果没有零模块加载):
        $ claude --version
        [等待数百毫秒加载所有模块]
        [然后才打印版本号]
        

问 2:MACRO.VERSION 是什么?


        // MACRO 是 Bun 的编译时内建变量
        // 在构建时从 package.json + git describe 注入
        console.log(`${MACRO.VERSION} (Claude Code)`);
        // 输出:claude-code 2.1.88
        

MACRO 是在编译时确定的值,运行时直接使用,不需要任何模块加载。

问 3:动态导入的本质是什么?


        // 静态导入
        import { main } from '../main.js';
        // 编译时就确定,运行时立即加载所有依赖
        
        // 动态导入
        const { main } = await import('../main.js');
        // 运行时才加载,执行到这里才触发
        

导入方式加载时机适用场景
静态导入编译时始终需要的模块
动态导入运行时按需加载的模块

问 4:cli.tsx 顶部有静态导入吗?


        // cli.tsx 第 1 行
        import { feature } from 'bun:bundle';  // ⚠️ 唯一的静态导入
        

唯一的静态导入是 `feature` from `bun:bundle`,因为:

- `feature()` 需要在模块加载时就可用

- Bun bundle 的 feature flag 是编译时常量

问 5:--version 之后的其他标志为什么用动态导入?

因为 `--version` 是最高频的命令,其他命令(如 `--help`、`claude "hello"`)可以接受稍长的加载时间。


三、快速路径详解

3.1 完整快速路径地图

源码位置:`src/entrypoints/cli.tsx` 第 28-302 行


        cli.tsx main()
            │
            ├── --version / -v / -V
            │     └── console.log(MACRO.VERSION)  ← 零模块加载
            │         源码:第 36-41 行
            │
            ├── --dump-system-prompt
            │     └── 加载 config.js + prompts.js
            │         源码:第 54-75 行
            │
            ├── --claude-in-chrome-mcp
            │     └── 加载 mcpServer.js
            │         源码:第 79-85 行
            │
            ├── --chrome-native-host
            │     └── 加载 chromeNativeHost.js
            │         源码:第 86-91 行
            │
            ├── --computer-use-mcp
            │     └── 加载 computerUse/mcpServer.js
            │         源码:第 92-99 行
            │
            ├── --daemon-worker=<kind>
            │     └── 加载 workerRegistry.js
            │         源码:第 110-115 行
            │
            ├── remote-control / rc / remote / sync / bridge
            │     └── 加载 bridgeMain.js
            │         源码:第 117-158 行
            │
            ├── daemon
            │     └── 加载 daemon/main.js
            │         源码:第 169-179 行
            │
            ├── ps / logs / attach / kill / --bg / --background
            │     └── 加载 cli/bg.js
            │         源码:第 190-206 行
            │
            ├── new / list / reply
            │     └── 加载 templateJobs.js
            │         源码:第 217-224 行
            │
            ├── environment-runner
            │     └── 加载 environment-runner/main.js
            │         源码:第 235-241 行
            │
            ├── self-hosted-runner
            │     └── 加载 self-hosted-runner/main.js
            │         源码:第 252-258 行
            │
            └── [其他命令]
                  └── 加载 main.js  ← 完整 CLI
                      源码:第 292-298 行
        

3.2 profileCheckpoint 的作用

源码位置:`src/entrypoints/cli.tsx` 第 47 行


        // 每次进入一个快速路径时调用
        profileCheckpoint('cli_version_path');      // --version 路径
        profileCheckpoint('cli_daemon_path');        // --daemon 路径
        profileCheckpoint('cli_bg_path');            // --bg 路径
        

作用:记录启动性能,用于分析用户使用模式和性能瓶颈。

3.3 五问分析

问 1:为什么 --dump-system-prompt 需要加载 config.js?


        // --dump-system-prompt 需要渲染完整的系统提示词
        // 但系统提示词依赖配置(如模型选择、工具列表)
        const { enableConfigs } = await import('../utils/config.js');
        enableConfigs();  // 加载配置
        const { getSystemPrompt } = await import('../constants/prompts.js');
        const prompt = await getSystemPrompt([], model);
        

问 2:--bg 和 daemon 有什么区别?

特性--bgdaemon
用途后台会话管理长运行守护进程
生命周期用户控制系统级服务
加载模块cli/bg.jsdaemon/main.js

问 3:为什么 remote-control 需要那么多检查?


        // 1. 认证检查
        const { getClaudeAIOAuthTokens } = await import('../utils/auth.js');
        if (!getClaudeAIOAuthTokens()?.accessToken) {
          exitWithError(BRIDGE_LOGIN_ERROR);  // 必须登录
        }
        
        // 2. 版本检查
        const versionError = checkBridgeMinVersion();
        if (versionError) { exitWithError(versionError); }
        
        // 3. 策略检查
        if (!isPolicyAllowed('allow_remote_control')) {
          exitWithError("Remote Control is disabled by your organization's policy.");
        }
        

remote-control 涉及安全敏感操作,所以需要多层检查。


四、feature() Bun DCE

4.1 feature() 是什么?

源码位置:`src/entrypoints/cli.tsx` 第 1 行


        import { feature } from 'bun:bundle';
        

`feature()` 是 Bun 的编译时内建函数,用于条件编译。

4.2 源码示例

源码位置:`src/entrypoints/cli.tsx` 第 92-99 行


        // feature() 必须在 if 条件内 inline
        // 否则 DCE(Dead Code Elimination)可能失效
        if (feature('CHICAGO_MCP') && process.argv[2] === '--computer-use-mcp') {
          const { runComputerUseMcpServer } = await import('../utils/computerUse/mcpServer.js');
          await runComputerUseMcpServer();
          return;
        }
        

4.3 五问分析

问 1:Dead Code Elimination (DCE) 是什么?


        构建前(feature='CHICAGO_MCP'=false):
        if (feature('CHICAGO_MCP') && process.argv[2] === '--computer-use-mcp') {
          // 100行 computerUse 相关代码
        }
        
        构建后(消除后):
        // 整个 if 块被删除,0行代码
        

问 2:为什么需要 DCE?

问题解决方案
CLI 功能太多用 feature flag 选择性包含
某些用户不需要 daemon构建时消除
某些功能仅限内部使用构建时消除
减少最终产物大小DCE

问 3:为什么注释说 "must stay inline"?


        // ✓ 正确:feature() 在 if 条件内 inline
        if (feature('DAEMON') && args[0] === 'daemon') { }
        
        // ✗ 错误:提取到变量可能破坏 DCE
        const isDaemon = feature('DAEMON');  // 可能被优化掉
        if (isDaemon && args[0] === 'daemon') { }
        

问 4:feature() 和动态导入的区别?

机制作用时机效果
动态导入运行时延迟加载,但代码仍存在于产物中
feature()构建时代码被完全消除,不存在于产物中

问 5:外部构建和内部构建的区别?


        // --dump-system-prompt 是 Ant-only(内部使用)
        // 注释说明:Ant-only: eliminated from external builds via feature flag
        if (feature('DUMP_SYSTEM_PROMPT') && args[0] === '--dump-system-prompt') {
          // ...
        }
        

外部用户构建时,`DUMP_SYSTEM_PROMPT` 为 false,整个 block 被消除。


五、--bare 与 CLAUDE_CODE_SIMPLE

5.1 源码实现

源码位置:`src/entrypoints/cli.tsx` 第 288-290 行


        // --bare: set SIMPLE early so gates fire during module eval / commander
        // option building (not just inside the action handler).
        if (args.includes('--bare')) {
          process.env.CLAUDE_CODE_SIMPLE = '1';
        }
        

5.2 五问分析

问 1:为什么 --bare 需要提前设置环境变量?


        // 问题:某些模块在 import 时就检查环境变量
        // 如果在 main.js 里才设置,模块级别的检查已经错过了
        
        // cli.tsx 在加载任何模块之前就设置
        if (args.includes('--bare')) {
          process.env.CLAUDE_CODE_SIMPLE = '1';  // 提前设置
        }
        
        // main.tsx 里才能正确检测到这个变量
        

问 2:CLAUDE_CODE_SIMPLE 控制什么?

根据注释,它影响:

- 模块级别的 gate 检查

- Commander option building

问 3:为什么注释说 "not just inside the action handler"?


        // Commander 的 option building 在 action 执行之前
        // 如果在 action handler 里才设置,某些检查已经错过了
        program
          .option('--bare')
          .action(async (options) => {
            // 如果在这里设置,Commander 已经构建完选项了
            process.env.CLAUDE_CODE_SIMPLE = '1';  // 太晚了
          });
        


六、设计模式

6.1 责任链模式


        process.argv
            │
            ├── --version → 直接返回
            ├── --daemon → daemonMain
            ├── --bg → bgHandler
            └── [其他] → main.js
        

每个特殊标志对应一个处理器,形成责任链

6.2 延迟加载模式


        const { daemonMain } = await import('../daemon/main.js');
        // 按需加载,不是所有用户都需要 daemon
        

6.3 条件编译模式


        if (feature('DAEMON') && args[0] === 'daemon') {
          // 构建时被消除
        }
        

6.4 早早退出模式


        // 如果匹配快速路径,立即 return
        // 不执行后续代码
        if (args[0] === '--version') {
          console.log(MACRO.VERSION);
          return;  // 早早退出
        }
        


七、思考题

思考题 1:动态导入的错误处理

问题:如果 `main.js` 加载失败,用户的体验是什么?如何改进?

答案

用户会看到难以理解的错误:


        Error: Cannot find module '../main.js'
        SyntaxError: /path/to/main.js:123
        

改进方案:


        try {
          const { main: cliMain } = await import('../main.js');
          await cliMain();
        } catch (error) {
          exitWithError(
            `Failed to load Claude Code. This may be caused by a corrupted installation.\n` +
            `Try reinstalling: npm install -g @anthropic-ai/claude-code\n` +
            `Error: ${error.message}`
          );
        }
        


思考题 2:feature() 的边界

问题:如果 `--version` 也用 `feature()` 包裹,构建时消除后会发生什么?

答案


        // 假设这样写:
        if (feature('VERSION') && args[0] === '--version') {
          console.log(`${MACRO.VERSION} (Claude Code)`);
          return;
        }
        

如果 `feature('VERSION')` 在构建时为 `false`,整个 if 块被消除:


        // 构建后:
        if (false && args[0] === '--version') { ... }  // 永远不执行
        
        // 用户执行:
        claude --version
        // → 程序跳过检查
        // → 尝试加载 main.js(而不是打印版本)
        // → 用户看到完整 CLI 启动,而不是版本号
        

结论:`--version` 不能用 `feature()` 包裹,因为它是一个必须始终存在的功能。


思考题 3:快速路径的扩展

问题:假设要添加一个新命令 `--backup`,它应该用快速路径还是普通路径?判断标准是什么?

答案

判断标准

标准快速路径普通路径
执行频率高(如 --version, --help)
启动耗时必须 <50ms可以接受 200ms+
依赖模块少或无
是否需要完整初始化

分析 `--backup`


        场景 A:用户备份配置
        claude --backup
        // 需要:读取配置 → 写入备份文件
        // 依赖:config.js, fs 模块
        // 耗时:~100ms
        // 建议:普通路径
        
        场景 B:用户查看备份工具帮助
        claude --backup --help
        // 需要:显示帮助信息
        // 依赖:help 模块
        // 耗时:~50ms
        // 建议:快速路径
        

通用决策树


        新命令需要加载多少模块?
            │
            ├── ≤ 1 个模块 → 快速路径
            │
            └── ≥ 2 个模块 → 执行频率高吗?
                              ├── 高 → 快速路径(值得优化)
                              └── 低 → 普通路径
        


八、延伸阅读

文件行数核心内容
`src/entrypoints/cli.tsx`302Bootstrap 入口
`src/main.tsx`4683主程序
`src/daemon/main.js`?守护进程
`src/cli/bg.js`?后台会话管理


九、下节预告

下一节我们将深入 main.tsx 的 preAction 钩子

- 为什么 `claude --help` 不触发初始化?

- preAction 如何实现惰性初始化?

- init() 函数的职责


*- 第一轮:□ 事实准确性*

*- 第二轮:□ 深度与洞见*

*- 第三轮:□ 可读性与价值*