9.4 KiB
核心模块设计 — CLI 启动与解析
所属项目: free-code .NET 10 重写 原始代码来源:
../../src/entrypoints/cli.tsx,../../src/screens/REPL.tsx原始设计意图: 解析命令行参数,区分交互式 REPL 模式和一次性 prompt 模式,并处理 daemon/bridge 等特殊运行模式 上级文档: 核心模块设计总览 交叉参考: 总体概述与技术选型
概述
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 管理生命周期。
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 容器构建之前执行。
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 风格不同。
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