文件位置
src/entrypoints/cli.tsx
作用
Claude Code 的引导入口点(bootstrap entrypoint)。在加载完整的 CLI 之前检查特殊标志,实现快速路径(fast-path)以最小化模块加载。
整体架构
void main() ← 顶层调用,不 await
│
├─ 快速路径(fast-path):不加载 main.tsx,直接处理特定命令
│ ├─ --version / -v
│ ├─ --dump-system-prompt
│ ├─ --claude-in-chrome-mcp
│ ├─ --chrome-native-host
│ ├─ --computer-use-mcp
│ ├─ --daemon-worker
│ ├─ remote-control / rc / remote / sync / bridge
│ ├─ daemon
│ ├─ ps / logs / attach / kill / --bg / --background
│ ├─ new / list / reply(模板任务)
│ ├─ environment-runner
│ ├─ self-hosted-runner
│ └─ --tmux + --worktree
│
└─ 正常路径:import { main } from '../main.js' → 加载完整 CLI
详细流程
1. 前置环境设置(模块顶层)
// 防止 corepack 自动锁定 yarn 版本
process.env.COREPACK_ENABLE_AUTO_PIN = '0';
// 远程环境限制 Node 堆内存为 8GB
if (process.env.CLAUDE_CODE_REMOTE === 'true') {
process.env.NODE_OPTIONS = '--max-old-space-size=8192';
}
2. main() 函数
快速路径检测
| 命令/标志 | 处理方式 | 说明 |
|---|---|---|
--version / -v | console.log(MACRO.VERSION) | 零模块加载 |
--dump-system-prompt | 加载 prompt 模块,输出后退出 | Ant-only,feature flag 控制 |
--claude-in-chrome-mcp | 启动 Chrome MCP 服务 | - |
--chrome-native-host | 启动 Chrome Native Host | - |
--computer-use-mcp | 启动 Computer Use MCP 服务 | feature flag 控制 |
--daemon-worker=<kind> | 启动 daemon worker 进程 | 由 supervisor 孵化 |
remote-control/rc/remote/sync/bridge | 桥接模式,远程控制 | feature flag + GrowthBook gate |
daemon <subcommand> | 长期运行的后台守护进程 | feature flag 控制 |
ps/logs/attach/kill + --bg/--background | 后台会话管理 | feature flag 控制 |
new/list/reply | 模板任务命令 | feature flag 控制 |
environment-runner | BYOC 环境运行器 | feature flag 控制 |
self-hosted-runner | 自托管运行器 | feature flag 控制 |
--tmux + --worktree | tmux worktree 快速路径 | 在加载完整 CLI 前 exec 进 tmux |
--update / --upgrade | 重写 argv 为 update 子命令 | 用户习惯兼容 |
设计特点
1. 极致的延迟加载
模块加载全部使用动态 await import(),确保快速路径只需加载必要的模块。例如 --version 是唯一真正的零加载路径。
2. 构建时死代码消除
所有 feature('XXX') 守卫在构建时通过 Bun bundler 的 feature 机制消除外部(external)构建中不需要的代码分支。例如 bridge、daemon、bg sessions 等在企业版/开源版中被编译时移除。
3. 快速路径优先级
成功匹配的快速路径都会 return,不会 fallthrough 到完整 CLI 的加载。但对某些"有条件的快速路径"(如 bridge mode 需要检查 auth + GrowthBook gate),失败后会 fallthrough。
4. 两种进程生命周期
| 路径 | 生命周期 |
|---|---|
| 快速路径 | 执行完后进程自然退出或 process.exit() |
| 正常路径 | 由 launchRepl() 启动 Ink TUI,进程保持运行直到用户退出 |
3. 正常路径
当没有匹配任何快速路径时:
const { startCapturingEarlyInput } = await import('../utils/earlyInput.js');
startCapturingEarlyInput(); // 开始捕获用户提前输入(在 REPL 启动前)
profileCheckpoint('cli_before_main_import');
const { main: cliMain } = await import('../main.js');
profileCheckpoint('cli_after_main_import');
await cliMain(); // 进入 main.tsx 的主逻辑
profileCheckpoint('cli_after_main_complete');
main.tsx 内部:
- 构建 Commander.js
program(注册所有选项和子命令) - 调用
program.parseAsync(process.argv) - Commander 解析 argv → 触发
.action()handler - Handler 内部根据选项走不同分支:
--continue→launchRepl()--resume→launchRepl()claude connect <url>→launchRepl()claude ssh <host>→launchRepl()- 默认(新会话) →
launchRepl()
launchRepl()通过 Ink(React 终端渲染器)渲染 TUI,进入事件循环
4. void main() 之后
void main() 不 await 返回的 Promise,这意味着:
- 快速路径:Promise resolve 后进程自然退出(或主动
process.exit()) - 正常路径:Promise 虽然 resolve 了,但 Ink TUI 持有事件循环,进程保持运行。用户在 REPL 中交互直到退出
快速路径汇总流程图
process.argv[2..]
│
├── --version/-v ──────────────────→ console.log → return
├── --dump-system-prompt ───────────→ 构建 system prompt → console.log → return
├── --claude-in-chrome-mcp ─────────→ runClaudeInChromeMcpServer() → return
├── --chrome-native-host ───────────→ runChromeNativeHost() → return
├── --computer-use-mcp ─────────────→ runComputerUseMcpServer() → return (feature gated)
├── --daemon-worker ────────────────→ runDaemonWorker() → return (feature gated)
├── remote-control/rc/remote/sync/bridge ──→ bridgeMain() → return (feature gated)
├── daemon ─────────────────────────→ daemonMain() → return (feature gated)
├── ps/logs/attach/kill/--bg ───────→ 后台会话管理 → return (feature gated)
├── new/list/reply ─────────────────→ templatesMain() → process.exit(0) (feature gated)
├── environment-runner ─────────────→ environmentRunnerMain() → return (feature gated)
├── self-hosted-runner ─────────────→ selfHostedRunnerMain() → return (feature gated)
├── --tmux + --worktree ────────────→ execIntoTmuxWorktree() → return (替换进程)
├── --update/--upgrade ─────────────→ 重写 argv 为 'update' → fallthrough
└── 其他 ───────────────────────────→ import { main } → await cliMain() → REPL
关键设计决策
为什么用 void main() 而不是 await main()?
cli.tsx 是模块顶层代码,不能使用 await(除非在异步函数内)。把所有代码都包在 main() 异步函数中,最后 void main() 调用,这是 Node.js/TypeScript 中处理顶层异步的标准模式。
为什么快速路径和正常路径都 return 但不一定退出进程?
- 快速路径:任务完成,事件循环没有 pending handles,进程自然退出
- 正常路径:
launchRepl()中的 Ink 渲染器持有事件循环(通过 React reconciler + terminal I/O),所以进程不会退出
fast-path 和 main.tsx 的 --worktree 选项的关系
cli.tsx 中有一个更早的 --tmux + --worktree 快速路径(行 248-274),它在 import { main } 之前就尝试 execIntoTmuxWorktree()。如果成功(result.handled = true),当前进程会被 tmux 替换,根本不会进入 main.tsx。
而 main.tsx 中注册的 --worktree / --tmux 选项(行 3810-3812)是给非 tmux 场景的 worktree 创建用的(直接创建 worktree 不启动 tmux session)。
各快速路径的 feature flag 依赖
| 路径 | Build-time flag | Runtime gate |
|---|---|---|
| remote-control/bridge | BRIDGE_MODE | isBridgeEnabled() (GrowthBook) |
| daemon | DAEMON | 无 |
| bg sessions | BG_SESSIONS | 无 |
| templates | TEMPLATES | 无 |
| environment-runner | BYOC_ENVIRONMENT_RUNNER | 无 |
| self-hosted-runner | SELF_HOSTED_RUNNER | 无 |
| computer-use-mcp | CHICAGO_MCP | 无 |
| dump-system-prompt | DUMP_SYSTEM_PROMPT | 无 |