# 第 3 章:CLI 命令路由
> 一个专业的命令行工具,首先要能解析自己的参数。
---
## 3.1 核心问题
第 2 章我们有了终端 UI,但 `npx tsx src/main.tsx` 就是全部了。
真实的 Coding Agent 需要多种启动方式:
```bash
claude # 交互式 REPL
claude "修复 main.ts 的类型错误" # 带初始 prompt 启动
claude -p "总结这个文件" # 非交互,打印到 stdout(管道友好)
claude --resume abc-123 # 恢复历史会话
claude mcp list # 子命令:管理 MCP 服务器
claude --version # 版本信息
claude --help # 帮助文档
```
**这些启动模式如何统一路由到正确的处理逻辑?**
---
## 3.2 原理讲解
### Commander.js:Node.js CLI 的标准解法
[Commander.js](https://github.com/tj/commander.js) 是 Node.js 生态最广泛使用的 CLI 框架,Claude Code 使用其 TypeScript 增强版 `@commander-js/extra-typings`。
核心 API:
```typescript
const program = new Command()
// 全局参数(可选位置参数)
program.argument('[prompt]', 'Your prompt', String)
// 选项(--flag 或 -f)
program.option('-p, --print', 'Print response and exit')
program.option('--cwd <dir>', 'Working directory')
// 子命令
const mcp = program.command('mcp').description('Manage MCP servers')
mcp.command('list').action(() => { /* ... */ })
mcp.command('add <name> <url>').action((name, url) => { /* ... */ })
// 解析并执行
program.action(async (prompt, opts) => { /* 主命令处理 */ })
await program.parseAsync(process.argv)
```
**Commander 的工作原理**:`parseAsync` 遍历 `process.argv`,匹配子命令和选项,填充解析结果,然后调用匹配到的 `.action()` 回调。
### 两种解析模式
Commander 支持两种子命令模式:
| 模式 | 写法 | 适用场景 |
|------|------|---------|
| **内联 action** | `.command('list').action(() => {...})` | 逻辑简单,与主程序共享代码 |
| **独立文件** | `.command('serve', 'Start server', {executableFile: '...'})` | 逻辑复杂,需要独立进程 |
Claude Code 的 `mcp` 子命令使用**内联 action + 动态 import** 模式——子命令被懒加载,避免启动时加载不必要的模块。
### Claude Code 的三种启动模式
理解这三种模式,是理解整个系统的钥匙:
```
模式 A:交互式 REPL(默认)
claude
claude "带初始 prompt 的 REPL"
→ 启动 Ink UI,进入消息循环,持续等待用户输入
模式 B:打印模式(管道友好)
claude -p "prompt"
echo "prompt" | claude -p
→ 不启动 UI,直接输出到 stdout
→ 支持 --output-format json / stream-json
→ 可被脚本、其他程序调用
模式 C:子命令
claude mcp list / add / remove
claude config / doctor / update
→ 管理类操作,不启动 Agent 循环
```
`isInteractive`(第 1 章)和 `-p/--print` 选项共同决定走哪条路径:
```typescript
if (options.print || !process.stdin.isTTY) {
// 模式 B:打印模式
await runPrintMode(prompt, options)
} else {
// 模式 A:交互式 REPL
await launchRepl(...)
}
```
---
## 3.3 Claude Code 源码中的关键细节
### 主命令选项全景
Claude Code 的 `program` 有 40+ 个选项,分为几类:
```typescript
// src/main.tsx(精简)
program
.name('claude')
.argument('[prompt]', 'Your prompt', String)
// --- 输出控制 ---
.option('-p, --print', 'Print response and exit (pipe-friendly)')
.option('--output-format <format>', ...) // text | json | stream-json
.option('--input-format <format>', ...) // text | stream-json
// --- 会话控制 ---
.option('-c, --continue', 'Continue most recent conversation')
.option('-r, --resume [id]', 'Resume by session ID or picker')
.option('--fork-session', 'Create new session ID when resuming')
// --- 模型控制 ---
.option('--model <model>', "e.g. 'sonnet' or 'claude-sonnet-4-6'")
.option('--effort <level>', 'low | medium | high | max')
.option('--thinking <mode>', 'enabled | adaptive | disabled')
// --- 权限控制 ---
.option('--permission-mode <mode>', ...)
.option('--dangerously-skip-permissions', '...')
.option('--allowed-tools <tools...>', ...)
.option('--disallowed-tools <tools...>', ...)
// --- 上下文注入 ---
.option('--system-prompt <prompt>', ...)
.option('--append-system-prompt <prompt>', ...)
.option('--add-dir <directories...>', ...)
// --- 调试 ---
.option('-d, --debug [filter]', 'e.g. "api,hooks" or "!file"')
.option('--verbose', ...)
// --- 主命令 action ---
.action(async (prompt, options) => { ... })
```
### 子命令结构
```
claude
├── mcp ← MCP 服务器管理
│ ├── serve ← 启动 Claude Code MCP Server
│ ├── add ← 添加 MCP 服务器
│ ├── remove ← 删除 MCP 服务器
│ ├── list ← 列出所有 MCP 服务器
│ ├── get ← 查看某个 MCP 服务器详情
│ └── add-json ← 通过 JSON 添加
├── config ← 配置管理(get/set/reset)
├── doctor ← 环境检查
├── install ← IDE 集成安装
└── update ← 自动更新
```
### 启动时的性能优化:`eagerParseCliFlag`
Commander 的 `parseAsync` 是同步的——它会阻塞直到所有 action 注册完成。
但 Claude Code 有大量 `import` 语句,模块加载本身就需要 ~100ms。
解决方案:`eagerParseCliFlag` 在 import 阶段就提前读取关键 flag(不依赖 Commander):
```typescript
// src/utils/cliArgs.ts
export function eagerParseCliFlag(flag: string): boolean {
return process.argv.includes(flag)
}
// src/main.tsx 顶部,在所有 import 完成前就已经知道是否是 --print 模式
const isPrintMode = process.argv.includes('-p') || process.argv.includes('--print')
```
这让打印模式下可以跳过不必要的 Ink、Keychain 等模块的加载。
### `preAction` hook:全局前置处理
```typescript
// src/main.tsx
program.hook('preAction', async thisCommand => {
// 在任何 action 执行前运行:
// 1. 应用 --cwd 切换工作目录
// 2. 验证 --session-id 格式
// 3. 初始化遥测
// 4. 加载企业策略(MDM)
})
```
Commander 的 `preAction` hook 让全局初始化逻辑与具体命令解耦——不需要在每个 action 中都重复这些步骤。
---
## 3.4 最小化产出物
> 代码骨架位于 `../chapters/03/src/`,参考实现位于 `../chapters/03/solution/`。
### 本章要实现什么
在 `../chapters/03/src/main.ts` 中完成 CLI 路由逻辑。
**接口规范**(已提供,不要修改):
```typescript
const VERSION = '0.1.0'
// 打印模式:输出 "[Agent] (echo) <prompt>"
async function runPrintMode(prompt: string): Promise<void>
// 交互模式:启动第 2 章的 Ink App
async function runInteractiveMode(initialPrompt?: string): Promise<void>
```
**你需要实现**:
1. `runPrintMode()`:打印 `[Agent] (echo) <prompt>`
2. `runInteractiveMode()`:用 `render()` 启动第 2 章的 `App` 组件
3. Commander 程序,支持:
- `--version` → 打印 `0.1.0`
- `-p/--print` → 打印模式
- `--cwd <dir>` → 切换工作目录(更新 `state.cwd`)
- `-d/--debug` → 打印调试信息到 stderr
- `config list` 子命令 → 列出配置
- `config get <key>` 子命令 → 获取配置值
4. `preAction` hook:处理 `--cwd` 和 `--debug`
5. 主 action:根据 `isPrint` 决定走打印模式还是交互模式
**关键约束**:
- 必须 import 第 1 章的 `state`(`../../01/src/state.js`)
- 必须 import 第 2 章的 `App`(`../../02/src/app.js`)
- 管道模式(stdin 非 TTY)也走打印模式
### 验收
```bash
cd docs/chapters/03
npm install
npm test
```
卡住时查看 `../chapters/03/solution/main.ts`。
---
## 3.5 设计要点:为什么不用 `yargs` 或 `minimist`?
Claude Code 选择 Commander 而非其他 CLI 框架,原因:
1. **TypeScript 类型推导**:`@commander-js/extra-typings` 让 `opts` 的类型完全来自 `.option()` 定义,无需手写类型
2. **子命令树**:`program.command().command()` 可以无限嵌套,适合 `claude mcp add stdio` 这样的多级命令
3. **`preAction` / `postAction` hook**:跨命令的全局处理不需要中间件包装
4. **帮助文档自动生成**:`--help` 的输出格式与选项声明一一对应,不需要额外维护文档
---
## 3.6 本章小结
| 概念 | 要记住的要点 |
|------|-------------|
| Commander.js | `argument` + `option` + `command` + `action` 四件套 |
| `parseAsync` | 遍历 `process.argv`,匹配并执行对应 action |
| 三种启动模式 | 交互式 REPL / 打印模式(-p)/ 子命令 |
| `preAction` hook | 全局前置处理(cwd 切换、调试初始化等) |
| `eagerParseCliFlag` | 在 Commander 解析前提前读取关键 flag,加速启动 |
| 懒加载子命令 | `import()` 在 action 内部,避免冷启动加载不必要模块 |
| 组合前两章 | `state` 来自第 1 章,`App` 来自第 2 章;第 3 章是纯粹的"路由层",不重复定义任何逻辑 |
至此,三章产出物已经形成真正的组合关系:
```
chapters/01/src/state.ts ← 地基:进程状态 + 信号处理
↑ import
chapters/02/src/app.tsx ← UI 层:Ink 终端界面
↑ import
chapters/03/src/main.ts ← 路由层:CLI 参数解析,分发到正确模式
```
CLI 路由层是"分发枢纽":它决定这次进程应该运行哪种模式,然后把控制权交给对应的层。
接下来,我们开始构建 Agent 真正的能力——工具执行引擎。
---
## 下一章
→ [第 4 章:工具抽象与执行引擎](04-tool-engine)