CLI 入口文件
文件概览
coding-agent 的启动由三个文件协作完成:
| 文件 | 行数 | 核心职责 |
|---|---|---|
cli.ts | 21 | 最简入口,设置环境后调用 main() |
config.ts | 567 | 配置中心,提供路径、版本、安装方式检测 |
cli/args.ts | 418 | 命令行参数解析,把字符串数组转成结构化对象 |
cli.ts:最简入口
cli.ts 只有 21 行,是整个程序的起点。它的职责非常单一:设置运行环境,然后把控制权交给 main()。
#!/usr/bin/env node
/**
* CLI entry point for the refactored coding agent.
* Uses main.ts with AgentSession and new mode modules.
*
* Test with: npx tsx src/cli-new.ts [args...]
*/
import { APP_NAME } from "./config.ts";
import { configureHttpDispatcher } from "./core/http-dispatcher.ts";
import { main } from "./main.ts";
process.title = APP_NAME;
process.env.PI_CODING_AGENT = "true";
process.env.AI_AGENT = "pi";
process.emitWarning = (() => {}) as typeof process.emitWarning;
// Configure undici's global dispatcher before provider SDKs issue requests.
// Runtime settings are applied once SettingsManager has loaded global/project settings.
configureHttpDispatcher();
main(process.argv.slice(2));
逐行解析
Shebang 行
#!/usr/bin/env node
Unix/Linux/macOS 系统的约定,告诉系统用 node 来运行这个文件。Windows 上不起作用但不会报错。
导入模块
import { APP_NAME } from "./config.ts";
import { configureHttpDispatcher } from "./core/http-dispatcher.ts";
import { main } from "./main.ts";
三个导入分别来自:
config.ts,应用名称("pi")core/http-dispatcher.ts,HTTP 连接池配置main.ts,主入口函数
设置进程标题
process.title = APP_NAME;
设置后在任务管理器里会显示为 "pi",方便用户识别。
设置环境变量
process.env.PI_CODING_AGENT = "true";
process.env.AI_AGENT = "pi";
这两个环境变量供其他代码判断当前是否运行在 coding agent 中。
禁用 Node.js 警告
process.emitWarning = (() => {}) as typeof process.emitWarning;
这是一个类型断言(as 语法)。空函数 (() => {}) 替换了原始的 emitWarning 方法,静默掉所有 Node.js 警告。
配置 HTTP 分发器
configureHttpDispatcher();
在任何 Provider SDK 发起请求之前,先配置好 undici 的全局 HTTP 连接池。
启动主程序
main(process.argv.slice(2));
process.argv 是一个字符串数组,包含命令行参数。slice(2) 跳过 node 路径和脚本路径,只保留用户输入的参数。
例如运行 pi --model gpt-4o "hello" 时:
process.argv[0]→"/usr/local/bin/node"process.argv[1]→"/path/to/cli.ts"process.argv[2]→"--model"process.argv[3]→"gpt-4o"process.argv[4]→"hello"
slice(2) 后得到 ["--model", "gpt-4o", "hello"]。
config.ts:配置中心
config.ts 是整个项目的配置基础设施,被几十个文件 import。它回答三个核心问题:
- 我是谁,应用名称、版本号
- 我在哪,程序安装位置、配置文件路径
- 我怎么被装的,安装方式检测、自我更新命令
运行方式检测
程序有两种运行方式:用 Bun 编译成独立可执行文件,或用 Node.js 通过 node/tsx 运行。
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
/**
* Detect if we're running as a Bun compiled binary.
* Bun binaries have import.meta.url containing "$bunfs", "~BUN", or "%7EBUN" (Bun's virtual filesystem path)
*/
export const isBunBinary =
import.meta.url.includes("$bunfs") || import.meta.url.includes("~BUN") || import.meta.url.includes("%7EBUN");
/** Detect if Bun is the runtime (compiled binary or bun run) */
export const isBunRuntime = !!process.versions.bun;
import.meta.url 是 ES Module 的元数据,包含当前文件的 URL 路径。Bun 编译后,路径会变成虚拟文件系统路径,包含 $bunfs、~BUN 或 %7EBUN(~ 的 URL 编码)。
!! 是双重否定运算符,把任意值转成布尔值。process.versions.bun 在 Bun 下有值,在 Node.js 下是 undefined。
安装方式检测
export type InstallMethod = "bun-binary" | "npm" | "pnpm" | "yarn" | "bun" | "unknown";
InstallMethod 是一个联合类型,只能是这几个字符串字面量之一。
检测函数
export function detectInstallMethod(): InstallMethod {
if (isBunBinary) {
return "bun-binary";
}
const resolvedPath = `${__dirname}\0${process.execPath || ""}`.toLowerCase().replace(/\\/g, "/");
if (resolvedPath.includes("/pnpm/") || resolvedPath.includes("/.pnpm/")) {
return "pnpm";
}
if (resolvedPath.includes("/yarn/") || resolvedPath.includes("/.yarn/")) {
return "yarn";
}
if (isBunRuntime || resolvedPath.includes("/install/global/node_modules/")) {
return "bun";
}
if (resolvedPath.includes("/npm/") || resolvedPath.includes("/node_modules/")) {
return "npm";
}
return "unknown";
}
逻辑很简单:把当前文件路径和可执行文件路径拼成一个字符串,转小写、统一用 / 分隔符,然后按顺序匹配关键词。
为什么需要检测安装方式? 因为程序需要告诉用户如何升级。不同安装方式的升级命令完全不同:
// pnpm 安装的
"pnpm install -g --ignore-scripts @earendil-works/pi-coding-agent"
// npm 安装的
"npm install -g --ignore-scripts @earendil-works/pi-coding-agent"
// Bun 编译的
undefined // 不支持命令行更新,需要从 GitHub 下载
接口定义
interface SelfUpdateCommandStep {
command: string;
args: string[];
display: string;
}
export interface SelfUpdateCommand extends SelfUpdateCommandStep {
steps?: SelfUpdateCommandStep[];
}
interface 定义对象的形状。SelfUpdateCommand 通过 extends 继承 SelfUpdateCommandStep 的所有属性,再加上可选的 steps(? 表示可选)。
路径函数
提供两组路径函数:
程序自身资源路径
export function getPackageDir(): string {
const envDir = process.env.PI_PACKAGE_DIR;
if (envDir) {
return normalizePath(envDir);
}
if (isBunBinary) {
return dirname(process.execPath);
}
let dir = __dirname;
while (dir !== dirname(dir)) {
if (existsSync(join(dir, "package.json"))) {
return dir;
}
dir = dirname(dir);
}
return __dirname;
}
优先使用环境变量覆盖,然后根据运行方式走不同分支。Node.js 版会从当前目录往上找 package.json。
用户配置路径
export function getAgentDir(): string {
const envDir = process.env[ENV_AGENT_DIR];
if (envDir) {
return expandTildePath(envDir);
}
return join(homedir(), CONFIG_DIR_NAME, "agent");
}
默认返回 ~/.pi/agent/,支持通过环境变量 PI_CODING_AGENT_DIR 覆盖。
读取 package.json
interface PackageJson {
name?: string;
version?: string;
piConfig?: {
name?: string;
configDir?: string;
};
}
let pkg: PackageJson = {};
try {
pkg = JSON.parse(readFileSync(getPackageJsonPath(), "utf-8")) as PackageJson;
} catch (e: unknown) {
const err = e as NodeJS.ErrnoException;
if (err.code !== "ENOENT") throw e;
}
JSON.parse() 返回的是 any 类型,用 as PackageJson 告诉 TypeScript 它实际上是一个 PackageJson 对象。catch (e: unknown) 是 TypeScript 5.0+ 推荐的写法,必须先检查类型才能使用 e。
提取的配置值:
export const PACKAGE_NAME: string = pkg.name || "@earendil-works/pi-coding-agent";
export const APP_NAME: string = piConfigName || "pi";
export const VERSION: string = pkg.version || "0.0.0";
cli/args.ts:命令行参数解析
这个文件把用户输入的命令行字符串数组解析成结构化的 Args 对象。
核心数据结构
export type Mode = "text" | "json" | "rpc";
export interface Args {
provider?: string;
model?: string;
apiKey?: string;
systemPrompt?: string;
appendSystemPrompt?: string[];
thinking?: ThinkingLevel;
continue?: boolean;
resume?: boolean;
help?: boolean;
version?: boolean;
mode?: Mode;
name?: string;
noSession?: boolean;
session?: string;
sessionId?: string;
fork?: string;
sessionDir?: string;
models?: string[];
tools?: string[];
excludeTools?: string[];
noTools?: boolean;
noBuiltinTools?: boolean;
extensions?: string[];
noExtensions?: boolean;
print?: boolean;
export?: string;
noSkills?: boolean;
skills?: string[];
promptTemplates?: string[];
noPromptTemplates?: boolean;
themes?: string[];
noThemes?: boolean;
noContextFiles?: boolean;
listModels?: string | true;
offline?: boolean;
tuiMode?: TuiMode;
verbose?: boolean;
projectTrustOverride?: boolean;
messages: string[];
fileArgs: string[];
/** Unknown flags (potentially extension flags) - map of flag name to value */
unknownFlags: Map<string, boolean | string>;
diagnostics: Array<{ type: "warning" | "error"; message: string }>;
}
? 表示可选字段,不传时为 undefined。messages、fileArgs、unknownFlags、diagnostics 没有 ?,是必填字段。
as const 与类型谓词
const VALID_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
export function isValidThinkingLevel(level: string): level is ThinkingLevel {
return VALID_THINKING_LEVELS.includes(level as ThinkingLevel);
}
as const 把数组断言为只读字面量类型,而不是宽泛的 string[]。这样 includes() 能做精确的类型收窄。
: level is ThinkingLevel 是类型谓词,告诉 TypeScript:如果函数返回 true,那么 level 的类型就是 ThinkingLevel。
parseArgs() 的解析逻辑
export function parseArgs(args: string[]): Args {
const result: Args = {
messages: [],
fileArgs: [],
unknownFlags: new Map(),
diagnostics: [],
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === "--help" || arg === "-h") {
result.help = true;
} else if (arg === "--version" || arg === "-v") {
result.version = true;
} else if (arg === "--model" && i + 1 < args.length) {
result.model = args[++i];
} else if (arg.startsWith("@")) {
result.fileArgs.push(arg.slice(1));
} else if (arg.startsWith("--")) {
// 未知的 -- 参数处理
} else if (!arg.startsWith("-")) {
result.messages.push(arg);
}
}
return result;
}
几种参数类型的处理方式:
| 类型 | 示例 | 处理方式 |
|---|---|---|
| 布尔型 | --help、--version | 直接标记为 true |
| 带值型 | --model gpt-4o | ++i 跳过值,存入字段 |
| 可多次使用 | --extension path1 --extension path2 | ?? [] 初始化 + push 追加 |
| 文件参数 | @file.txt | 去掉 @ 前缀,存入 fileArgs |
| 未知参数 | --custom-flag | 存入 unknownFlags Map |
| 纯文本 | hello world | 当作消息,存入 messages |
++i 的作用: 当 arg 是 "--model" 时,下一个 args[i+1] 就是值。args[++i] 先把 i 加 1,然后取到值,同时循环继续时 i 已经指向下一个未处理的参数。
?? [] 的作用: 空值合并运算符,如果数组还没初始化就创建空数组。
-- 未知参数处理:
} else if (arg.startsWith("--")) {
const eqIndex = arg.indexOf("=");
if (eqIndex !== -1) {
// --flag=value 格式
result.unknownFlags.set(arg.slice(2, eqIndex), arg.slice(eqIndex + 1));
} else {
// --flag value 或 --flag 格式
const flagName = arg.slice(2);
const next = args[i + 1];
if (next !== undefined && !next.startsWith("-") && !next.startsWith("@")) {
result.unknownFlags.set(flagName, next);
i++;
} else {
result.unknownFlags.set(flagName, true);
}
}
}
未知参数存入 unknownFlags Map,供扩展插件使用。支持 --flag=value 和 --flag value 两种格式。
printHelp() 函数
export function printHelp(extensionFlags?: ExtensionFlag[]): void {
// ... 打印帮助信息
}
extensionFlags?: ExtensionFlag[] 是可选参数,扩展插件可以注册额外的命令行参数。: void 表示函数不返回任何东西。
函数体使用 chalk.bold() 给终端文字加粗,用模板字符串拼接帮助文本。