main.ts 第五阶段:最终检查与模式执行
概述
运行时创建好之后,在进入实际模式之前,还有几个安全检查。检查通过后,程序根据 appMode 进入三种模式之一。
if (parsed.help) {
const extensionFlags = resourceLoader
.getExtensions()
.extensions.flatMap((extension) => Array.from(extension.flags.values()));
printHelp(extensionFlags);
process.exit(0);
}
if (parsed.listModels !== undefined) {
const searchPattern = typeof parsed.listModels === "string" ? parsed.listModels : undefined;
await listModels(modelRuntime, searchPattern, AbortSignal.timeout(15_000));
process.exit(0);
}
// Read piped stdin content (if any) - skip for RPC mode which uses stdin for JSON-RPC
let stdinContent: string | undefined;
if (appMode !== "rpc") {
stdinContent = await readPipedStdin();
if (stdinContent !== undefined && appMode === "interactive") {
appMode = "print";
}
}
time("readPipedStdin");
const { initialMessage, initialImages } = await prepareInitialMessage(
parsed,
settingsManager.getImageAutoResize(),
stdinContent,
);
time("prepareInitialMessage");
initTheme(settingsManager.getTheme(), appMode === "interactive");
time("initTheme");
// Show deprecation warnings in interactive mode
if (appMode === "interactive" && deprecationWarnings.length > 0) {
await showDeprecationWarnings(deprecationWarnings);
}
time("resolveModelScope");
reportDiagnostics(runtime.diagnostics);
if (runtime.diagnostics.some((diagnostic) => diagnostic.type === "error")) {
if (runtime.diagnostics.some((diagnostic) => diagnostic.message.includes("Failed to load extension"))) {
console.error(chalk.yellow(EXTENSION_LOAD_FAILURE_HINT));
}
process.exit(1);
}
time("createAgentSession");
if (appMode !== "interactive" && !session.model) {
console.error(chalk.red(formatNoModelsAvailableMessage()));
process.exit(1);
}
const startupBenchmark = isTruthyEnvFlag(process.env.PI_STARTUP_BENCHMARK);
if (startupBenchmark && appMode !== "interactive") {
console.error(chalk.red("Error: PI_STARTUP_BENCHMARK only supports interactive mode"));
process.exit(1);
}
// RPC refreshes catalogs here in the background; interactive mode starts its refresh after TUI initialization.
if (!offlineMode && appMode === "rpc") {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 15_000);
void modelRuntime
.refresh({ signal: controller.signal })
.catch(() => {})
.finally(() => clearTimeout(timeout));
}
if (appMode === "rpc") {
printTimings();
await runRpcMode(runtime);
} else if (appMode === "interactive") {
const interactiveMode = new InteractiveMode(runtime, {
migratedProviders,
modelFallbackMessage,
autoTrustOnReloadCwd,
initialMessage,
initialImages,
initialMessages: parsed.messages,
verbose: parsed.verbose,
tuiMode: parsed.tuiMode,
});
if (startupBenchmark) {
await interactiveMode.init();
time("interactiveMode.init");
// Give the TUI's stdin handler a brief chance to consume terminal query replies
// (Kitty keyboard protocol, device attributes, cell size) before restoring the terminal.
await new Promise((resolve) => setTimeout(resolve, 150));
interactiveMode.stop();
stopThemeWatcher();
printTimings();
if (process.stdout.writableLength > 0) {
await new Promise<void>((resolve) => process.stdout.once("drain", resolve));
}
if (process.stderr.writableLength > 0) {
await new Promise<void>((resolve) => process.stderr.once("drain", resolve));
}
return;
}
printTimings();
await interactiveMode.run();
} else {
printTimings();
const exitCode = await runPrintMode(runtime, {
mode: toPrintOutputMode(appMode),
messages: parsed.messages,
initialMessage,
initialImages,
});
stopThemeWatcher();
restoreStdout();
if (exitCode !== 0) {
process.exitCode = exitCode;
}
return;
}
最终检查
检查 1:--help
if (parsed.help) {
const extensionFlags = resourceLoader
.getExtensions()
.extensions.flatMap((extension) => Array.from(extension.flags.values()));
printHelp(extensionFlags);
process.exit(0);
}
虽然 --help 在第二阶段解析时就发现了,但那时候扩展还没加载。这里等扩展加载完后,把扩展注册的额外参数也加进帮助信息里,然后打印并退出。
检查 2:--list-models
if (parsed.listModels !== undefined) {
const searchPattern = typeof parsed.listModels === "string" ? parsed.listModels : undefined;
await listModels(modelRuntime, searchPattern, AbortSignal.timeout(15_000));
process.exit(0);
}
pi --list-models 或 pi --list-models sonnet。等模型运行时准备好后,列出可用模型(支持模糊搜索),然后退出。
检查 3:读取管道输入
let stdinContent: string | undefined;
if (appMode !== "rpc") {
stdinContent = await readPipedStdin();
if (stdinContent !== undefined && appMode === "interactive") {
appMode = "print";
}
}
检测有没有管道输入(比如 echo "hello" | pi)。如果有,读取内容作为初始消息。
RPC 模式跳过这步,因为 RPC 用 stdin 传输 JSON-RPC 消息,不是管道文本。
检查 4:准备初始消息
const { initialMessage, initialImages } = await prepareInitialMessage(
parsed,
settingsManager.getImageAutoResize(),
stdinContent,
);
把用户输入的各种来源合并成一条初始消息:
用户输入的消息(parsed.messages)
+ 文件内容(@file.txt)
+ 图片(@image.png)
+ 管道输入(stdin)
→ initialMessage + initialImages
检查 5:初始化主题
initTheme(settingsManager.getTheme(), appMode === "interactive");
加载主题文件,设置终端的配色方案。只有 interactive 模式才完整初始化主题。
检查 6:报告诊断信息
reportDiagnostics(runtime.diagnostics);
if (runtime.diagnostics.some((diagnostic) => diagnostic.type === "error")) {
if (runtime.diagnostics.some((diagnostic) => diagnostic.message.includes("Failed to load extension"))) {
console.error(chalk.yellow(EXTENSION_LOAD_FAILURE_HINT));
}
process.exit(1);
}
把运行时创建过程中收集的所有警告/错误打印出来。如果有 error(比如扩展加载失败),退出并给出提示。
检查 7:模型是否可用
if (appMode !== "interactive" && !session.model) {
console.error(chalk.red(formatNoModelsAvailableMessage()));
process.exit(1);
}
非交互模式下,必须有可用模型。交互模式下可以没有模型(用户可以稍后选择),但 print/rpc 模式必须现在就有。
检查 8:刷新模型目录
if (!offlineMode && appMode === "rpc") {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 15_000);
void modelRuntime
.refresh({ signal: controller.signal })
.catch(() => {})
.finally(() => clearTimeout(timeout));
}
RPC 模式下,在后台刷新模型目录(获取最新的可用模型列表)。
模式执行
所有检查通过后,程序根据 appMode 进入三种模式之一:
模式不是在这里选择的
第五阶段不判断模式,只做两件事:①处理管道输入导致的模式切换(有管道输入时把 interactive 降级为 print),②根据已有的 appMode 值做 if-else 分发。真正的模式决策发生在第二阶段的 resolveAppMode(),根据命令行参数(--rpc → rpc,-p → print,其他 → interactive)确定初始值。
模式 1:RPC 模式
if (appMode === "rpc") {
printTimings();
await runRpcMode(runtime);
}
用途: 让其他程序通过 JSON-RPC 协议调用 Agent 的能力。比如 IDE 插件可以这样和 Agent 通信。
特点:
- stdin 接收 JSON-RPC 请求
- stdout 返回 JSON-RPC 响应
- 无 UI,无交互
- 永远运行,直到被杀死
典型使用场景:
- VS Code 插件调用 Agent 的代码编辑能力
- 其他 CLI 工具通过管道发送 JSON-RPC 请求
- 自动化脚本调用 Agent 的文件操作能力
模式 2:Interactive 模式
} else if (appMode === "interactive") {
const interactiveMode = new InteractiveMode(runtime, {
migratedProviders,
modelFallbackMessage,
autoTrustOnReloadCwd,
initialMessage,
initialImages,
initialMessages: parsed.messages,
verbose: parsed.verbose,
tuiMode: parsed.tuiMode,
});
printTimings();
await interactiveMode.run();
}
用途: 用户在终端里直接和 Agent 对话,有完整的 UI 界面。
InteractiveMode 的参数:
| 参数 | 来源 | 作用 |
|---|---|---|
runtime | 第四阶段创建 | 提供核心能力 |
migratedProviders | 第三阶段迁移 | 被迁移的 Provider 列表 |
modelFallbackMessage | 运行时 | 模型不可用时的提示 |
autoTrustOnReloadCwd | 信任计算 | 自动信任的目录 |
initialMessage | 步骤 4 | 初始消息 |
initialImages | 步骤 4 | 初始图片 |
initialMessages | 参数解析 | 用户输入的消息列表 |
verbose | 参数解析 | 详细模式 |
tuiMode | 参数解析 | TUI 模式(regular/fullscreen) |
InteractiveMode 做什么:
- 初始化 TUI(终端用户界面)
- 显示启动画面、模型选择器
- 进入对话循环:用户输入 → AI 思考 → 工具执行 → 输出结果
- 处理快捷键(Ctrl+P 切换模型、Ctrl+C 中断等)
模式 3:Print 模式
} else {
printTimings();
const exitCode = await runPrintMode(runtime, {
mode: toPrintOutputMode(appMode),
messages: parsed.messages,
initialMessage,
initialImages,
});
stopThemeWatcher();
restoreStdout();
if (exitCode !== 0) {
process.exitCode = exitCode;
}
return;
}
用途: 非交互式执行,输出结果后退出。适合脚本调用。
典型用法:
# 直接提问
pi -p "列出 src/ 下所有 .ts 文件"
# 管道输入
echo "分析这段代码" | pi
# 使用文件作为输入
pi -p @prompt.txt
runPrintMode() 做什么:
- 发送初始消息给 AI
- 等待 AI 响应(可能调用工具)
- 把结果输出到 stdout
- 返回退出码(0=成功,非0=失败)
最后两行:
stopThemeWatcher(); // 停止主题文件监听
restoreStdout(); // 恢复被 takeOverStdout() 拦截的 stdout
执行流总结
用户输入: pi [args]
│
├─ 第一阶段:特殊命令拦截
│ └─ auth / install / config → 处理并退出
│
├─ 第二阶段:解析参数
│ └─ parseArgs() → --version / --export → 处理并退出
│
├─ 第三阶段:基础设施初始化
│ ├─ runMigrations()
│ ├─ SettingsManager.create()
│ ├─ showFirstTimeSetup()
│ └─ createSessionManager()
│
├─ 第四阶段:创建运行时环境
│ ├─ resolveCliPaths()
│ ├─ createRuntime() 工厂函数
│ └─ createAgentSessionRuntime()
│
└─ 第五阶段:最终检查与模式执行
├─ --help / --list-models → 处理并退出
├─ rpc → JSON-RPC 服务
├─ interactive → TUI 交互界面
└─ print → 输出结果,退出
整个 main.ts 的生命周期
从用户输入命令到程序退出,完整流程:
cli.ts
│
├─ 设置环境(进程名、环境变量、禁用警告)
├─ 配置 HTTP 分发器
└─ main(process.argv.slice(2))
│
├─ 第一阶段:特殊命令拦截
│ ├─ runAuthCommand()
│ ├─ handlePackageCommand()
│ └─ handleConfigCommand()
│
├─ 第二阶段:解析参数
│ ├─ parseArgs()
│ ├─ --version / --export
│ └─ resolveAppMode()
│
├─ 第三阶段:基础设施初始化
│ ├─ runMigrations()
│ ├─ SettingsManager.create()
│ ├─ showFirstTimeSetup()
│ ├─ createSessionManager()
│ └─ missingSessionCwdIssue
│
├─ 第四阶段:创建运行时环境
│ ├─ resolveCliPaths()
│ ├─ createRuntime() 工厂函数
│ │ ├─ 确定项目信任状态
│ │ ├─ createAgentSessionServices()
│ │ ├─ 解析模型
│ │ └─ createAgentSessionFromServices()
│ └─ createAgentSessionRuntime()
│
└─ 第五阶段:最终检查与模式执行
├─ --help / --list-models
├─ readPipedStdin()
├─ prepareInitialMessage()
├─ initTheme()
├─ reportDiagnostics()
└─ 进入对应模式
├─ runRpcMode()
├─ InteractiveMode.run()
└─ runPrintMode()