命令行入口、无头模式与打包
Agent CLI 的外壳:main.ts 怎样守住 stdout 与 stderr、兜住进程错误与退出,全部子命令与全局选项及其限制,-p 无头模式的流程与三种输出格式,Node bundle 与 SEA 两条打包路径,发行包的 --web 分流与安装脚本,以及界面语言检测。
apps/zcode-cli/packages/cli 是 Agent CLI 的外壳:81 个源文件、约 1.06 万行,另有 2700 行构建脚本。它自己几乎不含业务,做的是三件事:在进程层面把输入输出与退出管好(main.ts),把命令行翻译成对 bootstrap 的调用(run.ts 与各个 *-command.ts),以及把整套东西打成可以分发的产物。会话运行时怎样装配见上一篇bootstrap:把运行时拼起来;TUI 的界面细节归终端界面,插件、技能、命令、钩子各有专篇,这里只列命令。
仓库里有三个都叫 zcode 的可执行入口,先分清楚:
| 入口 | 产物 | 用途 |
|---|---|---|
@zcode/cli 的 bin | dist/zcode.cjs(apps/zcode-cli/packages/cli/package.json:6) | Agent CLI 本体,本篇主角;SEA 版把它连同 Node 打成单文件 |
发行包的 bin/zcode.mjs | scripts/zcode-distribution/runner.mjs 复制而来(scripts/build-zcode.mjs:202) | 第一个参数是 --web 时起 Web,其余交给 Agent CLI |
@zcode/server-cli 的 bin | dist/server-cli.js(packages/zcode-server-cli/package.json:4) | 常驻 Web 服务的 serve、status、stop、restart、update、uninstall,其余参数转给旁边的 zcode.cjs(packages/zcode-server-cli/src/cli.ts:92、505),见Web 与服务端 |
main.ts:进程边界
main.ts 只有 122 行(apps/zcode-cli/packages/cli/src/main.ts:16),顺序很讲究:静态导入的只有几个轻量模块,run.ts 以及它背后的 bootstrap、core 都等边界装好之后才动态导入(main.ts:77)。
- 进程名:设为
zcode-cli(apps/zcode-cli/packages/cli/src/process-name.ts:2);带--prepare-storage时跳过,因为那种模式可能跑在宿主的 Worker 里,不能改宿主的进程名(main.ts:18)。 - 环境变量清洗:紧接着剔除用户 shell 注入的
NODE_ENV、代理与证书变量,网络变量封存起来只还给后续的工具子进程(main.ts:20),规则见执行边界:子进程、环境与网络。 - 判断调用类型:协议调用与 TUI 调用都复用
run.ts的同一份参数定义来判断,避免把-p或--cwd的值误认成命令(apps/zcode-cli/packages/cli/src/arguments.ts:108、apps/zcode-cli/packages/cli/src/tui-stderr.ts:11)。参数不合法时,只要第一个词是app-server或agent-server仍按协议保护 stdout(arguments.ts:119)。
接下来装的几道边界(main.ts:30):
// app-server/agent-server 的 stdout 是严格的 ZCode Protocol 帧通道,三方 SDK 的
// console.debug 等普通输出不能直接写入 stdout。必须在加载 run/bootstrap 之前将
// 进程级 console 统一引导到 stderr,否则任意依赖的一行普通日志都会触发传输层 JSON 解析崩溃。
// TUI 同样独占 stdout;AI SDK 的首条提示使用 console.info,不能绕过 stderr 捕获。
const restoreConsole =
isProtocol || isTui ? installStderrConsoleBoundary(process.stderr) : undefined;
const runtimeWarnings = interceptKnownRuntimeWarnings(process.stderr);
const tuiStderr = isTui ? interceptTuiStderr(process.stderr) : undefined;
const stderr = tuiStderr?.passthrough ?? process.stderr;
const disposeProcessErrorBoundary = isProtocol
? installCliProcessErrorBoundary({
stderr,
onFatal: (reason) => {
if (lifecycle)
lifecycle.requestShutdown(new Error("Uncaught process error", { cause: reason }));
else process.exit(1);
},
})
: undefined;| 边界 | 做法 | 适用 |
|---|---|---|
| stdout 帧保护 | 把全局 console 换成一个 stdout 与 stderr 都指向 stderr 的 Console(apps/zcode-cli/packages/cli/src/protocol-console.ts:10) | 协议与 TUI |
| 已知告警过滤 | 吞掉 Node 的 SQLite 实验特性告警、module.register 弃用告警、NODE_TLS_REJECT_UNAUTHORIZED 告警及随后的 trace 提示,以及两条 AI SDK 告警;空写入照常放行,作为退出前的 flush 屏障(apps/zcode-cli/packages/cli/src/runtime-warnings.ts:8、53) | 全部 |
| TUI stderr 拦截 | 原始 stderr 写入一个只保留最近 65536 个字符的缓冲,CLI 自己的错误走 passthrough 直写(tui-stderr.ts:3、60);退出时 restore() 不带 flush(main.ts:96),从代码看,缓冲内容直接丢弃 | TUI |
| 协议 stderr 保护 | 监听真实流的 error 与 close,出口失效后写入变成空操作,避免 EPIPE 引发异常、异常又写 stderr 的自激循环(apps/zcode-cli/packages/cli/src/protocol-stderr.ts:1) | 协议 |
| 进程错误边界 | uncaughtException 与 unhandledRejection 只报告一次:先写一行带 [zcode-process-exception] 前缀的结构化诊断供宿主转发,再请求生命周期有界关闭(apps/zcode-cli/packages/cli/src/process-errors.ts:34、110)。名称、消息、栈分别截到 128、4000、16000 字符(packages/shared/src/process-diagnostic.ts:5) | 协议 |
协议进程还有一个唯一的退出 owner:createProtocolProcessLifecycle 在加载运行时之前就接管 stdin(apps/zcode-cli/packages/cli/src/protocol-lifecycle.ts:10)。stdin 读到 EOF 后先留 100 毫秒排空再中止,收到 SIGINT、SIGTERM、SIGHUP 或 IO 出错则立即中止;无论哪种,1500 毫秒的截止时间一到就强制退出,即使初始化的 Promise 永远不 settle(protocol-lifecycle.ts:4、31)。信号退出码为 130、143、129(protocol-lifecycle.ts:7)。
其余几步:SEA 版随后解出原生搜索工具(main.ts:51,见下文“两条打包路径”);__zcode-plugin-host 在导入 run.ts 之前分流,因为导入 run 会求值 Agent、工具注册表和工作流模块,每个插件 MCP 子进程都背上整套业务依赖(main.ts:60);需要运行时的命令先准备好内置与个人 Provider 配置的路径(main.ts:66、apps/zcode-cli/packages/cli/src/provider-runtime-env.ts:105)。非协议进程在 run() 返回后挂一个 1 秒的退出看门狗:事件循环到点还没排空,就带着原退出码强制退出(main.ts:103、apps/zcode-cli/packages/cli/src/shutdown.ts:5、128);插件宿主成功返回时不挂,因为它的 stdio 句柄正是服务存活的条件(main.ts:100)。构建产物最外层还包了一段横幅代码:参数恰好是 --licenses 时,在任何初始化之前打印第三方声明,SEA 版再附上 Node.js 的许可证(apps/zcode-cli/packages/cli/scripts/build.mjs:228)。
run.ts:命令路由
run() 先处理四类不走全局解析的入口(apps/zcode-cli/packages/cli/src/run.ts:286):
export const run = async (ctx: RunContext, deps: RunDependencies = {}): Promise<number> => {
if (ctx.argv[0] === "__internal-search") {
return runEmbeddedSearchCli(ctx.argv.slice(1), {
cwd: (deps.cwd ?? process.cwd)(),
stderr: ctx.stderr,
stdin: ctx.stdin,
stdout: ctx.stdout,
});
}
if (isPluginHostInvocation(ctx.argv)) {
return await runPluginHostCommand(ctx, ctx.argv.slice(1));
}
// 与 plugin host 同理,且必须同样在 parseArgs 之前:SEA 下 dwf 的沙箱子进程是本二进制的
// 自 re-exec,argv 末位是入口文件路径——交给严格 parseArgs 只会报未知参数。
if (isDwfChildInvocation(ctx.argv)) {
return await runDwfChildCommand(ctx, ctx.argv.slice(1));
}
if (ctx.argv[0] === "hooks") {
return await runHooksCommand(ctx, deps, version);
}全局解析用 Node 的 parseArgs,strict: true(arguments.ts:3、105),不认识的选项一律报错。于是凡是参数形态不归 CLI 管的入口,都必须抢在它之前:
__internal-search:CLI 把自己当成find与grep用。bootstrap 在设置了ZCODE_EMBEDDED_SEARCH_COMMAND时,让嵌入式搜索后端调用<该命令> __internal-search(apps/zcode-cli/packages/bootstrap/src/app/embedded-search-backend.ts:11),后面跟的是 grep 的原生参数;不支持的子命令退出码为 2(apps/zcode-cli/packages/cli/src/internal-search/embedded-search-cli.ts:68)。搜索本身见读、写、改、搜。__zcode-plugin-host <server-path>:在 Agent 子进程里托管官方插件的 MCP 服务器(apps/zcode-cli/packages/contracts/src/plugins/index.ts:9)。它是恢复 Computer Use broker 凭据的最后一道边界:进程里捕获过凭据时,只有插件 id、成对的凭据与node_repl宿主标记全部吻合才放行,校验在 import 插件代码之前完成(apps/zcode-cli/packages/cli/src/plugin-host-command.ts:83),见插件与官方市场。__zcode-dwf-child <entry>:动态工作流沙箱子进程。SEA 单文件不解释 Node 旗标,只能让二进制自己 re-exec;入口文件写在<cwd>/.zcode/workflow-runs/<runId>.mjs,不走 argv 是为了避开 Windows 命令行 32767 字符的上限(apps/zcode-cli/packages/cli/src/dwf-child-command.ts:1),见动态工作流(二)。hooks trust:有自己的--workspace、--hook-digest等选项,自带一套parseArgs(apps/zcode-cli/packages/cli/src/hooks-trust-command.ts:13)。
之后抽出 --disallowedTools,解析其余参数,做完所有校验,再分流到 -p、--target 或子命令(run.ts:310 至 574)。解析失败时打印错误和帮助到 stderr,退出码 1(run.ts:317)。
| 子命令 | 作用 | 详见 |
|---|---|---|
(无)或 tui | 全屏终端界面,缺省命令(run.ts:65) | 终端界面 |
app-server、agent-server | ZCode Protocol 的 stdio 服务端,两个名字等价(run.ts:525);只有开发态才读 .env,免得打包态的协议子进程在握手前就因环境文件出错退出(run.ts:243) | ZCode Protocol V4 |
doctor | 打印运行时与打包信息 | 下文 |
login,可带 zai 或 bigmodel;logout | 浏览器授权登录,缺省 zai(apps/zcode-cli/packages/cli/src/login-command.ts:15) | 账号、Coding Plan 与闲时计划 |
commands、skills | 子命令 list(缺省)与 inspect <name>(apps/zcode-cli/packages/cli/src/commands-command.ts:38、apps/zcode-cli/packages/cli/src/skills-command.ts:34) | 技能与自定义命令 |
plugins,别名 plugin | list、install、uninstall、enable、disable、update、validate、marketplace(apps/zcode-cli/packages/cli/src/plugins-command.ts:41) | 插件与官方市场 |
hooks trust | status(缺省)、review、grant、revoke(hooks-trust-command.ts:48) | 生命周期 Hooks |
help、version | 与 -h、-v 相同(run.ts:519) |
没有位置参数形式的提示词:zcode "修个 bug" 会被当成未知命令,报错并打印帮助,退出码 1(run.ts:570)。全局选项与各自的限制:
| 选项 | 作用 | 限制 |
|---|---|---|
-p, --prompt <text> | 单次无头运行 | 空文本报错;与 --target 互斥(run.ts:401) |
--mode <mode> | build、edit、plan、yolo,大小写不敏感(run.ts:133) | -p 缺省 yolo(run.ts:42);--target 与 TUI 不设缺省,走持久化模式或配置。四种模式的语义见权限模式与规则 |
--disallowedTools、--disallowed-tools | 本次运行从工具面移除整个工具,不改持久化配置 | 在 parseArgs 之前单独抽取,可连续跟多个值;逗号或空格分隔,括号内的除外;web_search 规范成 WebSearch(arguments.ts:127、222);Bash(git *) 也移除整个 Bash(apps/zcode-cli/packages/i18n/src/locales/en-US.ts:41) |
--output-format <fmt> | text、json、stream-json | 其他值直接报错,免得调用方拼错后拿到纯文本却无从察觉(run.ts:114) |
--json | 旧开关,输出 JSON 摘要 | 显式 --output-format 优先(apps/zcode-cli/packages/cli/src/prompt-command.ts:44);doctor、plugins list 等也认它(run.ts:211) |
--attach <path>,可重复 | 给 -p 附本地文件,按扩展名判为图片、视频、PDF 或普通文件(prompt-command.ts:437) | 只有 -p 使用,--target 固定传空列表(run.ts:506) |
--target <text>、--target-replace | 改写成 /goal <text> 或 /goal replace <text> 无头运行(run.ts:175) | 空文本报错;--target-replace 必须配 --target(run.ts:152)。目标模式见目标模式 |
-c, --continue、--resume <sessionId> | 继续当前目录最近的会话,或恢复指定会话 | 二者互斥(run.ts:368) |
--cwd <path> | 换工作目录 | 必须是可访问的已有目录(apps/zcode-cli/packages/cli/src/cwd.ts:11) |
--locale <locale> | en-US、zh-CN、auto | 其他值报错(run.ts:127) |
--force-mcs | 对 Anthropic 系 Provider 强制 mid-conversation system 投影 | 只能配 -p、--target 或 tui(run.ts:448) |
--browser-use=headless、--browser-executable <path> | CLI 自己拉起 Playwright 管理的无头 Chromium(apps/zcode-cli/packages/cli/src/headless-browser.ts:9) | 前者只能配 -p、--target 或 tui(run.ts:436);后者必须配前者(run.ts:358)。见node_repl、Browser Use 与 Computer Use |
--surface <surface> | terminal 或 desktop;后者让系统提示词多一段桌面上下文(apps/zcode-cli/packages/core/src/context/builder.ts:131) | 只能配 -p、--target、app-server、agent-server(run.ts:406) |
--memory-bench | 开启记忆抽取,并在退出前等它完成 | 只能配 -p 且不能有位置参数(run.ts:428);要求项目记忆已启用(prompt-command.ts:251) |
--no-browser | login 只打印授权地址 | |
-f, --force | 非交互终端里卸载插件 | 目前只有 plugins uninstall 读它(apps/zcode-cli/packages/cli/src/plugins-command.ts:343) |
--prepare-storage、--stdio | 前者让 app-server 只准备存储就退出(run.ts:532);后者被接受但 run.ts 不读 | 宿主以 app-server --stdio 启动 Agent(scripts/zcode-distribution/runner.mjs:183) |
-a, --all、--available、--keep-data、-s, --scope、--sparse | 插件子命令的标志,在全局注册后透传(arguments.ts:85) | |
--no-color、--verbose | 关掉 ANSI 颜色;错误时多打原因与调用栈 |
帮助文本(en-US.ts:10)比代码少几样:没有 hooks 子命令、agent-server 别名、--output-format、--prepare-storage、-f 与插件的几个标志,commands 与 skills 也只写了 list。
-p:无头模式
runPrompt(prompt-command.ts:61)的流程:
- 斜杠命令先分流(
prompt-command.ts:79)。/help、不带名字的/skill、/login、/logout在 CLI 里直接处理;/skill <name> <task>改写成强制加载该技能的提示词。创建 App 之后,/expert、/goal(--target就是它)以及解析不出的自定义命令交给命令中心;能解析的自定义命令与/init作为普通提示词提交,由 bootstrap 展开(prompt-command.ts:255、447);其余文本原样交给运行时,core 在回合入口还认/compact、/rewind、/fork(apps/zcode-cli/packages/core/src/runtime/methods/turn.ts:105)。 - 准备进程级资源:注册信号处理(
prompt-command.ts:144),加载.env(从 cwd 向上找第一个.env文件,不覆盖已有变量,apps/zcode-cli/packages/cli/src/env.ts:50、87),解析-c或--resume得到会话 id,准备遥测,启动进程级 Provider Registry,按需拉起无头浏览器(prompt-command.ts:152至211)。 - 创建 App(
prompt-command.ts:212):
app = await createApp({
browserControlPort: browserRuntime?.browserControlPort,
env: appEnv,
// headless 没有交互审批面,core 因此退到 deny broker,于是 CreateWorkflow 的
// alwaysAsk gate 在 -p 下**必然被拒**("No permission client configured")。
// 这个最小 broker 只按工具名放行 CreateWorkflow,其余工具委托回同一个 deny
// broker,语义逐字不变。详见 headless-workflow.ts 的注释。
permissionBroker: createHeadlessPermissionBroker(),
providerRegistry: providerRegistryRuntime.runtime.registryService,
configuredDefaultModelSelection: providerRegistryRuntime.configuredDefaultModelSelection,
// ...
resume: sessionId !== undefined,
runtimeConfig: {
...(mode ? { mode } : {}),
...(toolDisallowlist ? { toolDisallowlist } : {}),
...(forceMcs ? { midConversationSystem: { mode: "force" as const } } : {}),
memory: { extractionEnabled: options.memoryBench === true },
modelStreaming: "on",
presentationSurface,
workingDirectory,
},几个后果:无头模式没有审批界面,除了 CreateWorkflow 与 AmendWorkflow,任何需要审批的工具调用都会被拒,原因是 No permission client configured for <工具名>(apps/zcode-cli/packages/cli/src/headless-workflow.ts:31、apps/zcode-cli/packages/core/src/permission/broker.ts:28),-p 缺省用 yolo,大多数工具因此走不到审批这一步;--target 不带 --mode 时用的是持久化模式或配置(缺省 build),碰到要审批的工具同样会被拒。自动的记忆抽取默认关闭;也不传标题生成配置,TUI 则会开启(apps/zcode-cli/packages/cli/src/tui-command-state.ts:35)。
- 提交与等待:装上跨回合的常驻事件订阅作为唯一的事件写者,调用
app.submitPrompt,--attach的文件按扩展名推断类型后随提示词一起提交(prompt-command.ts:290、293)。如果观察到动态工作流活动,就每 100 毫秒轮询运行时的两个忙碌事实,等在飞的工作流和它们触发的通知回合全部结束,并存的后台 Bash 与子 Agent 任务也一起等;不设超时,Ctrl+C 才能打断(headless-workflow.ts:315、327、331)。--memory-bench时再等记忆抽取排空(prompt-command.ts:322)。 - 输出:
response取最后一个非空回合的文本,多于一个回合时另带turnResponses数组(prompt-command.ts:330)。三种格式:
| 格式 | stdout | stderr |
|---|---|---|
text(缺省) | 各回合文本,空行分隔(prompt-command.ts:416) | 动态工作流进度,节点迁移每 400 毫秒最多一行,开始、结束与 log 不节流(headless-workflow.ts:163、176);工作区钩子被跳过时的诊断 |
json | 一个格式化的 JSON 对象:sessionId、traceId、turnId、response、usage、eventCount、projection,钩子被跳过时另有 workspaceHookTrust(prompt-command.ts:371) | 无 |
stream-json | 每个会话事件一行 NDJSON,信封与协议服务端相同;工作流进度单独定型为 workflow.run.progress;最后一行 type: "result" 是流的终止符(headless-workflow.ts:248、prompt-command.ts:345) | 无 |
走命令中心的那条路径(/expert、/goal 与 --target)不透出事件:json 与 stream-json 都只打印一个含 sessionId、traceId、response 的 JSON 对象(prompt-command.ts:538)。从代码看,--target 配 --output-format stream-json 拿到的并不是 NDJSON。命令中心若需要用户在几个目标之间选择,无头模式无法弹出选择器,直接失败并提示改用 --target-replace(prompt-command.ts:526)。
退出码:成功为 0;参数错误或运行出错为 1,stderr 上是 Error: <消息>,已知 traceId 时附在后面,--verbose 另打原因与调用栈(prompt-command.ts:418);收到 SIGINT、SIGTERM、SIGHUP 时先中止当前回合,清理最多等 2 秒,再以 130、143、129 退出(shutdown.ts:3、6、54)。正常结束时 App、浏览器、遥测依次关闭,每步最多 6 秒,最后释放 Provider Registry(shutdown.ts:4、prompt-command.ts:131)。
doctor
doctor 不读配置、不碰网络,只报告运行时与打包假设(run.ts:188):CLI 名与进程名、版本、Node 版本、平台与架构、是否 SEA,以及“缺省产物是 node-bundle、SEA 可选”这两条打包声明(run.ts:205)。--json 输出完整对象,--verbose 多打 execPath 与 cwd。版本号是构建时注入的 __CLI_VERSION__,取自 apps/zcode-cli/package.json,当前为 0.16.9(build.mjs:9、113、233)。
两条打包路径
普通 Node bundle是缺省路径(build.mjs:204)。esbuild 把 src/main.ts 打成一个 CJS 文件,target 定为 node22:桌面用 Electron 内置的 Node 24,远程 SSH 复用已部署的 Node v22.16,同一份产物要在两端都能跑(build.mjs:257)。@zcode/tui、playwright-core、koffi 保持外置(build.mjs:14):TUI 在运行时经 Node 原生的动态 import 加载,Playwright 依赖运行时的包资源,koffi 按平台动态加载 .node 文件(build.mjs:236)。注释给 TUI 的理由是 Ink 7 与 yoga-layout 用了顶层 await,可 TUI 如今依赖的是 @mbears/opentui-core 与 @mbears/opentui-react(apps/zcode-cli/packages/tui/package.json:22),仓库里已找不到 Ink,这句注释过时了。另有一个 Zod 去重插件,保证打进去的 Zod v4 只有 packages/shared 钉住的那一个版本、一份拷贝(build.mjs:16、74、253),内置 Provider 配置随产物放进 dist/provider(build.mjs:219)。--desktop-agent 模式压缩代码、保留函数名、不带 sourcemap(build.mjs:123),由根目录的 scripts/build-desktop-agent-cli.mjs 依序构建各工作区包后调用,产物暂存进桌面端的 bundled-agents(scripts/build-desktop-agent-cli.mjs:94),见桌面应用。
SEA 单文件是可选路径(apps/zcode-cli/packages/cli/scripts/build-sea.mjs:285)。它要求先有 dist/zcode.cjs 与 postject,目标平台是 darwin、linux、win 各自的 arm64 与 x64 共六种(apps/zcode-cli/packages/cli/scripts/sea-targets.mjs:3),产物名为 zcode-<平台>-<架构>,Windows 的平台段写作 windows 并加 .exe(sea-targets.mjs:49)。每个目标的步骤:
- 收集资源进 SEA blob:TUI 运行时、官方插件(
node-repl-host与browser-use,apps/zcode-cli/packages/cli/scripts/sea-official-plugin-assets.mjs:20)、原生搜索工具(macOS 与 Linux 为 bfs、ugrep、rg,Windows 没有 bfs,scripts/native-search-tools-config.mjs:96)、playwright-core整包、内置 Provider 配置与 Node 许可证;关掉代码缓存与快照(build-sea.mjs:172)。 - 取目标平台的 Node 二进制:可用
--node-binary指定,否则从 nodejs.org 下载与构建机相同版本的发行包,按SHASUMS256.txt校验 sha256,缓存命中也要重新校验(build-sea.mjs:301、apps/zcode-cli/packages/cli/scripts/sea-node-download.mjs:106)。 - 复制二进制,找到
NODE_SEA_FUSE标记;macOS 先去签名,Windows 先剥掉 Authenticode 签名;用postject注入 blob;macOS 再做 ad-hoc 签名(build-sea.mjs:257)。 - 宿主平台的产物用临时存储目录跑一次
--version冒烟测试(build-sea.mjs:231)。
SEA 运行时要把资源解到磁盘上才能用,原生工具、TUI 与 Playwright 写盘前都比对 sha256:
- 原生搜索工具解到
<storage>/cache/runtime_tools/<target>/<tool>/<version>-<sha>,同时校验大小与哈希,然后通过ZCODE_BFS_BINARY、ZCODE_RG_BINARY、ZCODE_UGREP_BINARY告诉后续代码;用户已设置这些变量时不覆盖(apps/zcode-cli/packages/cli/src/sea-runtime-tools.ts:52、74、96、119)。 - TUI 运行时与 Playwright 解到平台缓存目录,macOS 是
~/Library/Caches/zcode/sea-assets,Windows 在LOCALAPPDATA下,其余取XDG_CACHE_HOME或~/.cache,再按 CLI 版本、目标与清单哈希分目录(apps/zcode-cli/packages/cli/src/tui-runtime-loader.ts:46、120、apps/zcode-cli/packages/cli/src/sea-playwright-runtime.ts:43)。Playwright 清单里的路径必须落在node_modules/playwright-core/下(sea-playwright-runtime.ts:108)。 - 内置 Provider 配置从 SEA 资源里物化到
~/.zcode/v2下(provider-runtime-env.ts:137)。
仓库的 CLI README(apps/zcode-cli/README.md:7)仍是早期起步模板的口吻:
Runtime code has zero production dependencies.
实际上 CLI 依赖 dotenv、playwright-core 与十个工作区包(apps/zcode-cli/packages/cli/package.json:26),README 列出的 npm run bootstrap、npm run start、npm test 在 apps/zcode-cli/package.json 里也都没有(apps/zcode-cli/package.json:7)。
发行包 zcode
pnpm build:zcode 运行 scripts/build-zcode.mjs(根目录 package.json:22),把 Agent、Web 前端与后端组成一个不需要 Electron 的发行包(README.md:86)。必须先给下载根地址,ZCODE_DIST_BASE_URL 或 --base-url(build-zcode.mjs:235);随后构建 @zcode/cli 及其依赖、@zcode/server、@zcode/web(build-zcode.mjs:142),再组装目录(build-zcode.mjs:157):
| 路径 | 内容 |
|---|---|
bin/zcode.mjs | 分流入口 |
agent/zcode.cjs、agent/provider/ | Agent CLI 与内置 Provider 配置 |
agent/node_modules/ | 六个目标平台合并后的 TUI 运行时,同一路径哈希不同就构建失败(scripts/zcode-distribution/assets.mjs:32),以及 playwright-core |
server/、web/ | 后端(入口 entry-http.js)与前端静态资源 |
node_modules/ | 后端的运行时依赖,如 hono、ws、ssh2、node-pty(assets.mjs:12),并补上 node-pty 的预编译产物(assets.mjs:176) |
package.json | name: "zcode-runtime" 与版本号(build-zcode.mjs:206) |
输出目录缺省为 dist/zcode/:releases/<version>/zcode-<version>.tar.gz、同目录的 sha256.txt、顶层的 latest.json(字段为 baseUrl、createdAt、name、sha256、tarball、version)与 install.sh(build-zcode.mjs:250、273)。版本缺省取根目录 package.json:3 的产品版本 3.14.0(build-zcode.mjs:243)。
分流逻辑在 runner.mjs 末尾(runner.mjs:252):
try {
const argv = process.argv.slice(2);
if (argv.length === 1 && ["--version", "-v"].includes(argv[0])) {
console.log(version);
} else if (argv[0] === "--web") {
const options = parseArgs(argv.slice(1));
if (options.command === "help") console.log(usage());
else if (options.command === "version") console.log(version);
else await serve(options);
} else {
if (argv.length === 1 && ["--help", "-h"].includes(argv[0])) {
console.log("Web mode: zcode --web [options] (zcode --web --help for details)\n");
}
// CLI 自启动子进程依赖 argv[1];统一指向真正的 Agent 入口,保留 TTY 与所有原始参数。
process.argv[1] = agentEntry;
await import(pathToFileURL(agentEntry).href);
}非 --web 的参数不起子进程,直接在同一进程里 import Agent 入口,TTY 原样保留;argv[1] 改成 Agent 入口,是因为 CLI 自己 re-exec 子进程时依赖它。单独一个 -v 由分流器回答,打印的是发行包版本(缺省 3.14.0),而 zcode version 与 zcode doctor 进到 Agent,报的是 CLI 版本 0.16.9。
--web 模式(runner.mjs:170):监听地址缺省 127.0.0.1(runner.mjs:37),端口缺省由系统挑一个空闲的(runner.mjs:172);监听非本机地址时自动生成 24 字节随机令牌,--token 指定、--no-token 关闭(runner.mjs:109、173);本机地址缺省打开浏览器(runner.mjs:175)。它用当前 Node 起 server/entry-http.js,通过环境变量告诉后端静态资源目录、工作区与令牌,并让后端用 node agent/zcode.cjs app-server --stdio 拉起 Agent(runner.mjs:178)。Ctrl+C 给子进程发 SIGTERM,1.5 秒后退出(runner.mjs:226)。后端怎样托管 Agent,见Web 与服务端。
install.sh 是一段 POSIX sh(scripts/zcode-distribution/installer.mjs:3):要求 node、curl、tar 三个命令存在(installer.mjs:18),读 latest.json 取版本与包名,下载解压到 ~/.zcode/runtime/releases/<version>,把 current 软链接指过去,再在 ~/.local/bin 写一个 exec node .../bin/zcode.mjs 的包装脚本;三个位置可用 ZCODE_DIST_BASE_URL、ZCODE_DIST_HOME、ZCODE_DIST_BIN_DIR 覆盖(installer.mjs:7、45)。README 说运行发行包需要的 Node 版本以 mise.toml 为准(README.md:162),安装脚本却只检查 node 是否存在;latest.json 与 sha256.txt 都带了摘要,安装脚本也没有校验下载的包。
界面语言
文案集中在 @zcode/i18n:en-US 与 zh-CN 两份目录,内容只有 CLI 帮助、一条语言错误和 TUI 文案(apps/zcode-cli/packages/i18n/src/types.ts:5、apps/zcode-cli/packages/i18n/src/index.ts:25),其余命令行报错都是英文。语言检测(apps/zcode-cli/packages/i18n/src/locale.ts:32)依次看 LC_ALL、LC_MESSAGES、LANG、LANGUAGE(后者可以是冒号分隔的列表),再看 Intl 给出的区域;去掉 .UTF-8 这类编码与 @ 修饰,下划线换成连字符,C 与 POSIX 忽略,en 开头归为 en-US,zh 开头归为 zh-CN(locale.ts:5、40)。
检测结果只在请求的语言是 auto 时才用上(locale.ts:24)。而帮助文本只看 --locale(apps/zcode-cli/packages/cli/src/help.ts:3),配置里的 ui.locale 缺省又是 en-US(apps/zcode-cli/packages/contracts/src/config/index.ts:356),所以不加 --locale 时 zcode --help 总是英文,TUI 也要启动时加 --locale,或配置里写了 auto、zh-CN 才显示中文;TUI 里切换语言会写回配置文件(apps/zcode-cli/packages/bootstrap/src/app/session-facade.ts:547)。TUI 启动时自己合并一遍配置来决定语言,因为需要登录的界面在 App 创建之前就要画出来(apps/zcode-cli/packages/cli/src/tui-startup-locale.ts:22)。两份目录也有小出入:/login 一行,英文写的是选择 Z.AI 或 BigModel 登录,中文仍是“使用 Z.AI OAuth 登录”(en-US.ts:56、apps/zcode-cli/packages/i18n/src/locales/zh-CN.ts:56)。
下一篇:终端界面——不带参数运行 zcode 时看到的全屏界面:它的渲染栈、组件与快捷键,以及它为什么不持有任何业务状态。