246 lines
9.4 KiB
Markdown
246 lines
9.4 KiB
Markdown
# 核心模块设计 — CLI 启动与解析
|
||
|
||
> 所属项目: free-code .NET 10 重写
|
||
> 原始代码来源: `../../src/entrypoints/cli.tsx`, `../../src/screens/REPL.tsx`
|
||
> 原始设计意图: 解析命令行参数,区分交互式 REPL 模式和一次性 prompt 模式,并处理 daemon/bridge 等特殊运行模式
|
||
> 上级文档: [核心模块设计总览](核心模块设计.md)
|
||
> 交叉参考: [总体概述与技术选型](../总体概述与技术选型/总体概述与技术选型.md)
|
||
|
||
---
|
||
|
||
## 概述
|
||
|
||
CLI 启动模块是整个应用的入口,对应原始 TypeScript 项目的 `cli.tsx`。它的核心职责是在加载任何业务逻辑之前,先判断这次调用属于哪种运行模式,并将控制权交给对应的处理器。
|
||
|
||
.NET 重写将原始代码中混合在一个文件里的逻辑拆分为三个独立的类:
|
||
|
||
- `Program` — 四阶段启动协调器
|
||
- `QuickPathHandler` — 无需 DI 容器的快速路径处理
|
||
- `CliCommandBuilder` — 基于 `System.CommandLine` 的命令行定义
|
||
|
||
---
|
||
|
||
## 5.1 入口点 Program.cs
|
||
|
||
`Program.cs` 组织为四个顺序执行的阶段,每一阶段都有明确的失败边界。
|
||
|
||
**原始设计意图:** 原始 `cli.tsx` 在模块顶层直接调用 Commander.js 解析参数,然后根据 flag 条件决定走哪条路径。.NET 版本将这一隐式流程显式化为四个阶段,并统一使用 `Microsoft.Extensions.Hosting` 管理生命周期。
|
||
|
||
```csharp
|
||
public static class Program
|
||
{
|
||
public static async Task<int> Main(string[] args)
|
||
{
|
||
// Phase 1: 快速路径(不加载DI容器,零开销)
|
||
if (QuickPathHandler.TryHandle(args, out var exitCode))
|
||
return exitCode;
|
||
|
||
// Phase 2: 构建Host
|
||
var host = Host.CreateDefaultBuilder(args)
|
||
.ConfigureAppConfiguration(cfg => cfg
|
||
.AddJsonFile("appsettings.json", optional: true)
|
||
.AddJsonFile(Path.Combine(
|
||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||
".free-code", "settings.json"), optional: true, reloadOnChange: true))
|
||
.ConfigureServices((ctx, services) =>
|
||
{
|
||
services.AddCoreServices();
|
||
services.AddEngine();
|
||
services.AddTools();
|
||
services.AddCommands();
|
||
services.AddApiProviders();
|
||
services.AddMcp();
|
||
services.AddLsp();
|
||
services.AddBridge();
|
||
services.AddBusinessServices();
|
||
services.AddTasks();
|
||
services.AddSkills();
|
||
services.AddPlugins();
|
||
services.AddFeatures();
|
||
services.AddState();
|
||
services.AddTerminalUI();
|
||
})
|
||
.ConfigureLogging(logging => logging
|
||
.AddConsole()
|
||
.SetMinimumLevel(LogLevel.Information))
|
||
.Build();
|
||
|
||
// Phase 3: 初始化核心服务
|
||
await host.Services.GetRequiredService<IAppInitializer>().InitializeAsync();
|
||
|
||
// Phase 4: 启动REPL或执行一次性命令
|
||
var runner = host.Services.GetRequiredService<IAppRunner>();
|
||
return await runner.RunAsync(args);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 四阶段说明
|
||
|
||
| 阶段 | 职责 | 失败处理 |
|
||
|------|------|----------|
|
||
| Phase 1 | 快速路径检测,无 DI 开销 | 匹配则直接返回,不进入后续阶段 |
|
||
| Phase 2 | 构建 Host,注册所有服务 | `HostBuilder` 抛出则进程退出 |
|
||
| Phase 3 | 异步初始化(OAuth 刷新、MCP 连接等) | `InitializeAsync` 可抛出 `AppInitializationException` |
|
||
| Phase 4 | 启动 REPL 或执行一次性 prompt | `RunAsync` 返回退出码 |
|
||
|
||
`reloadOnChange: true` 设置在 `~/.free-code/settings.json` 上,允许用户在 REPL 运行期间修改配置后立即生效,无需重启。
|
||
|
||
---
|
||
|
||
## 5.2 快速路径处理器
|
||
|
||
`QuickPathHandler` 对应原始 `cli.tsx` 中散布在文件顶部的 fast-path 条件判断。
|
||
|
||
**原始设计意图:** 原始代码在 Commander.js 解析之前,用一系列 `process.argv.includes()` 检查来处理特殊 flag,避免触发 React/Ink 的初始化开销。.NET 版本将这些检查集中到一个静态类,并且同样在 DI 容器构建之前执行。
|
||
|
||
```csharp
|
||
public static class QuickPathHandler
|
||
{
|
||
public static bool TryHandle(string[] args, out int exitCode)
|
||
{
|
||
exitCode = 0;
|
||
|
||
// --version
|
||
if (args.Contains("--version"))
|
||
{
|
||
var ver = Assembly.GetEntryAssembly()!
|
||
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!
|
||
.InformationalVersion;
|
||
Console.WriteLine($"free-code {ver}");
|
||
return true;
|
||
}
|
||
|
||
// --dump-system-prompt
|
||
if (args.Contains("--dump-system-prompt"))
|
||
{
|
||
var builder = new SystemPromptBuilder(/* minimal config */);
|
||
Console.WriteLine(builder.BuildDefaultPrompt());
|
||
return true;
|
||
}
|
||
|
||
// MCP daemon mode
|
||
if (args.Contains("--mcp-daemon"))
|
||
{
|
||
exitCode = McpDaemon.Run(args).GetAwaiter().GetResult();
|
||
return true;
|
||
}
|
||
|
||
// Bridge mode
|
||
if (args.Contains("--bridge"))
|
||
{
|
||
exitCode = BridgeMain.Run(args).GetAwaiter().GetResult();
|
||
return true;
|
||
}
|
||
|
||
// Background session mode
|
||
if (args.Contains("--background-session"))
|
||
{
|
||
exitCode = BackgroundSessionRunner.Run(args).GetAwaiter().GetResult();
|
||
return true;
|
||
}
|
||
|
||
// Template mode
|
||
if (args.Contains("--template"))
|
||
{
|
||
exitCode = TemplateRunner.Run(args).GetAwaiter().GetResult();
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 快速路径 flag 列表
|
||
|
||
| Flag | 处理器 | 说明 |
|
||
|------|--------|------|
|
||
| `--version` | 直接打印 | 从程序集元数据读取版本号 |
|
||
| `--dump-system-prompt` | `SystemPromptBuilder` | 打印默认 System Prompt,用于调试 |
|
||
| `--mcp-daemon` | `McpDaemon.Run` | 以 MCP 服务器模式运行,供 IDE 插件调用 |
|
||
| `--bridge` | `BridgeMain.Run` | IDE 远程控制桥接模式 |
|
||
| `--background-session` | `BackgroundSessionRunner.Run` | 后台会话模式,供 Agent 工具启动子进程 |
|
||
| `--template` | `TemplateRunner.Run` | 模板执行模式 |
|
||
|
||
注意 `--mcp-daemon` 和 `--bridge` 的处理器使用 `.GetAwaiter().GetResult()` 同步等待,因为此时还没有 `async` 上下文可用(`Main` 的 `await` 路径已被 Phase 1 短路)。
|
||
|
||
---
|
||
|
||
## 5.3 CLI 命令定义 (CliCommandBuilder)
|
||
|
||
`CliCommandBuilder` 对应原始 `cli.tsx` 中的 Commander.js 配置,负责定义全局选项和根命令的处理逻辑。
|
||
|
||
**原始设计意图:** Commander.js 的 `program.option(...).action(...)` 链式 API 被映射到 `System.CommandLine` 的 `RootCommand` + `Option<T>` 模式。两者在概念上完全对应,只是 API 风格不同。
|
||
|
||
```csharp
|
||
public class CliCommandBuilder
|
||
{
|
||
public RootCommand Build()
|
||
{
|
||
var root = new RootCommand("free-code - The free build of Claude Code");
|
||
|
||
// 全局选项
|
||
var modelOption = new Option<string?>("--model", "Override default model");
|
||
var verboseOption = new Option<bool>("--verbose", "Verbose output");
|
||
var resumeOption = new Option<string?>("--resume", "Resume session ID");
|
||
root.AddGlobalOption(modelOption);
|
||
root.AddGlobalOption(verboseOption);
|
||
root.AddGlobalOption(resumeOption);
|
||
|
||
// 一次性 prompt 模式 (-p)
|
||
var promptOption = new Option<string?>("-p", "One-shot prompt (non-interactive)");
|
||
root.AddOption(promptOption);
|
||
|
||
root.SetHandler(async (string? prompt, string? model, bool verbose, string? resume) =>
|
||
{
|
||
if (prompt != null)
|
||
return await OneShotMode.ExecuteAsync(prompt, model);
|
||
return await REPLMode.StartAsync(model, verbose, resume);
|
||
}, promptOption, modelOption, verboseOption, resumeOption);
|
||
|
||
return root;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 选项说明
|
||
|
||
| 选项 | 类型 | 作用域 | 说明 |
|
||
|------|------|--------|------|
|
||
| `--model` | `string?` | 全局 | 覆盖配置文件中的默认模型 |
|
||
| `--verbose` | `bool` | 全局 | 开启详细日志输出 |
|
||
| `--resume` | `string?` | 全局 | 恢复指定 session ID 的对话历史 |
|
||
| `-p` | `string?` | 根命令 | 非交互式一次性 prompt 模式 |
|
||
|
||
`--model`、`--verbose`、`--resume` 作为全局选项注册,子命令也可以访问。`-p` 仅在根命令级别有效,因为子命令有各自的参数结构。
|
||
|
||
当 `-p` 存在时,走 `OneShotMode.ExecuteAsync` 路径(非交互式,执行完毕后退出)。否则走 `REPLMode.StartAsync` 启动交互式终端 UI。
|
||
|
||
---
|
||
|
||
## 模块间依赖
|
||
|
||
```
|
||
Program.cs
|
||
├── QuickPathHandler (静态,无依赖)
|
||
│ ├── SystemPromptBuilder (仅 --dump-system-prompt 路径)
|
||
│ ├── McpDaemon
|
||
│ ├── BridgeMain
|
||
│ ├── BackgroundSessionRunner
|
||
│ └── TemplateRunner
|
||
│
|
||
└── CliCommandBuilder (由 IAppRunner 使用)
|
||
├── OneShotMode
|
||
└── REPLMode
|
||
```
|
||
|
||
---
|
||
|
||
## 参考资料
|
||
|
||
- [核心模块设计总览](核心模块设计.md)
|
||
- [查询引擎 (QueryEngine)](核心模块设计-查询引擎-QueryEngine.md)
|
||
- [原始代码映射 — 核心模块](reference/原始代码映射-核心模块.md)
|
||
- [总体概述与技术选型](../总体概述与技术选型/总体概述与技术选型.md)
|