命令行入口、无头模式与打包

Agent CLI 的外壳:main.ts 怎样守住 stdout 与 stderr、兜住进程错误与退出,全部子命令与全局选项及其限制,-p 无头模式的流程与三种输出格式,Node bundle 与 SEA 两条打包路径,发行包的 --web 分流与安装脚本,以及界面语言检测。

作者 David更新于 41 篇(共 47 篇)

apps/zcode-cli/packages/cli 是 Agent CLI 的外壳:81 个源文件、约 1.06 万行,另有 2700 行构建脚本。它自己几乎不含业务,做的是三件事:在进程层面把输入输出与退出管好(main.ts),把命令行翻译成对 bootstrap 的调用(run.ts 与各个 *-command.ts),以及把整套东西打成可以分发的产物。会话运行时怎样装配见上一篇bootstrap:把运行时拼起来;TUI 的界面细节归终端界面,插件、技能、命令、钩子各有专篇,这里只列命令。

仓库里有三个都叫 zcode 的可执行入口,先分清楚:

入口产物用途
@zcode/cli 的 bindist/zcode.cjsapps/zcode-cli/packages/cli/package.json:6Agent CLI 本体,本篇主角;SEA 版把它连同 Node 打成单文件
发行包的 bin/zcode.mjsscripts/zcode-distribution/runner.mjs 复制而来(scripts/build-zcode.mjs:202第一个参数是 --web 时起 Web,其余交给 Agent CLI
@zcode/server-cli 的 bindist/server-cli.jspackages/zcode-server-cli/package.json:4常驻 Web 服务的 serve、status、stop、restart、update、uninstall,其余参数转给旁边的 zcode.cjspackages/zcode-server-cli/src/cli.ts:92505),见Web 与服务端

main.ts:进程边界

图表加载中…

main.ts 只有 122 行(apps/zcode-cli/packages/cli/src/main.ts:16),顺序很讲究:静态导入的只有几个轻量模块,run.ts 以及它背后的 bootstrap、core 都等边界装好之后才动态导入(main.ts:77)。

  • 进程名:设为 zcode-cliapps/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:108apps/zcode-cli/packages/cli/src/tui-stderr.ts:11)。参数不合法时,只要第一个词是 app-serveragent-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 的 Consoleapps/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:853全部
TUI stderr 拦截原始 stderr 写入一个只保留最近 65536 个字符的缓冲,CLI 自己的错误走 passthrough 直写(tui-stderr.ts:360);退出时 restore() 不带 flushmain.ts:96),从代码看,缓冲内容直接丢弃TUI
协议 stderr 保护监听真实流的 errorclose,出口失效后写入变成空操作,避免 EPIPE 引发异常、异常又写 stderr 的自激循环(apps/zcode-cli/packages/cli/src/protocol-stderr.ts:1协议
进程错误边界uncaughtExceptionunhandledRejection 只报告一次:先写一行带 [zcode-process-exception] 前缀的结构化诊断供宿主转发,再请求生命周期有界关闭(apps/zcode-cli/packages/cli/src/process-errors.ts:34110)。名称、消息、栈分别截到 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:431)。信号退出码为 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:66apps/zcode-cli/packages/cli/src/provider-runtime-env.ts:105)。非协议进程在 run() 返回后挂一个 1 秒的退出看门狗:事件循环到点还没排空,就带着原退出码强制退出(main.ts:103apps/zcode-cli/packages/cli/src/shutdown.ts:5128);插件宿主成功返回时不挂,因为它的 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 的 parseArgsstrict: truearguments.ts:3105),不认识的选项一律报错。于是凡是参数形态不归 CLI 管的入口,都必须抢在它之前:

  • __internal-search:CLI 把自己当成 findgrep 用。bootstrap 在设置了 ZCODE_EMBEDDED_SEARCH_COMMAND 时,让嵌入式搜索后端调用 <该命令> __internal-searchapps/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 等选项,自带一套 parseArgsapps/zcode-cli/packages/cli/src/hooks-trust-command.ts:13)。

之后抽出 --disallowedTools,解析其余参数,做完所有校验,再分流到 -p--target 或子命令(run.ts:310574)。解析失败时打印错误和帮助到 stderr,退出码 1(run.ts:317)。

子命令作用详见
(无)或 tui全屏终端界面,缺省命令(run.ts:65终端界面
app-serveragent-serverZCode Protocol 的 stdio 服务端,两个名字等价(run.ts:525);只有开发态才读 .env,免得打包态的协议子进程在握手前就因环境文件出错退出(run.ts:243ZCode Protocol V4
doctor打印运行时与打包信息下文
login,可带 zaibigmodellogout浏览器授权登录,缺省 zaiapps/zcode-cli/packages/cli/src/login-command.ts:15账号、Coding Plan 与闲时计划
commandsskills子命令 list(缺省)与 inspect <name>apps/zcode-cli/packages/cli/src/commands-command.ts:38apps/zcode-cli/packages/cli/src/skills-command.ts:34技能与自定义命令
plugins,别名 pluginlist、install、uninstall、enable、disable、update、validate、marketplace(apps/zcode-cli/packages/cli/src/plugins-command.ts:41插件与官方市场
hooks truststatus(缺省)、review、grant、revoke(hooks-trust-command.ts:48生命周期 Hooks
helpversion-h-v 相同(run.ts:519

没有位置参数形式的提示词:zcode "修个 bug" 会被当成未知命令,报错并打印帮助,退出码 1(run.ts:570)。全局选项与各自的限制:

选项作用限制
-p, --prompt <text>单次无头运行空文本报错;与 --target 互斥(run.ts:401
--mode <mode>buildeditplanyolo,大小写不敏感(run.ts:133-p 缺省 yolorun.ts:42);--target 与 TUI 不设缺省,走持久化模式或配置。四种模式的语义见权限模式与规则
--disallowedTools--disallowed-tools本次运行从工具面移除整个工具,不改持久化配置parseArgs 之前单独抽取,可连续跟多个值;逗号或空格分隔,括号内的除外;web_search 规范成 WebSearcharguments.ts:127222);Bash(git *) 也移除整个 Bash(apps/zcode-cli/packages/i18n/src/locales/en-US.ts:41
--output-format <fmt>textjsonstream-json其他值直接报错,免得调用方拼错后拿到纯文本却无从察觉(run.ts:114
--json旧开关,输出 JSON 摘要显式 --output-format 优先(apps/zcode-cli/packages/cli/src/prompt-command.ts:44);doctorplugins 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 必须配 --targetrun.ts:152)。目标模式见目标模式
-c, --continue--resume <sessionId>继续当前目录最近的会话,或恢复指定会话二者互斥(run.ts:368
--cwd <path>换工作目录必须是可访问的已有目录(apps/zcode-cli/packages/cli/src/cwd.ts:11
--locale <locale>en-USzh-CNauto其他值报错(run.ts:127
--force-mcs对 Anthropic 系 Provider 强制 mid-conversation system 投影只能配 -p--targettuirun.ts:448
--browser-use=headless--browser-executable <path>CLI 自己拉起 Playwright 管理的无头 Chromium(apps/zcode-cli/packages/cli/src/headless-browser.ts:9前者只能配 -p--targettuirun.ts:436);后者必须配前者(run.ts:358)。见node_repl、Browser Use 与 Computer Use
--surface <surface>terminaldesktop;后者让系统提示词多一段桌面上下文(apps/zcode-cli/packages/core/src/context/builder.ts:131只能配 -p--targetapp-serveragent-serverrun.ts:406
--memory-bench开启记忆抽取,并在退出前等它完成只能配 -p 且不能有位置参数(run.ts:428);要求项目记忆已启用(prompt-command.ts:251
--no-browserlogin 只打印授权地址
-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 与插件的几个标志,commandsskills 也只写了 list

-p:无头模式

runPromptprompt-command.ts:61)的流程:

  1. 斜杠命令先分流prompt-command.ts:79)。/help、不带名字的 /skill/login/logout 在 CLI 里直接处理;/skill <name> <task> 改写成强制加载该技能的提示词。创建 App 之后,/expert/goal--target 就是它)以及解析不出的自定义命令交给命令中心;能解析的自定义命令与 /init 作为普通提示词提交,由 bootstrap 展开(prompt-command.ts:255447);其余文本原样交给运行时,core 在回合入口还认 /compact/rewind/forkapps/zcode-cli/packages/core/src/runtime/methods/turn.ts:105)。
  2. 准备进程级资源:注册信号处理(prompt-command.ts:144),加载 .env(从 cwd 向上找第一个 .env 文件,不覆盖已有变量,apps/zcode-cli/packages/cli/src/env.ts:5087),解析 -c--resume 得到会话 id,准备遥测,启动进程级 Provider Registry,按需拉起无头浏览器(prompt-command.ts:152211)。
  3. 创建 Appprompt-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,
      },

几个后果:无头模式没有审批界面,除了 CreateWorkflowAmendWorkflow,任何需要审批的工具调用都会被拒,原因是 No permission client configured for <工具名>apps/zcode-cli/packages/cli/src/headless-workflow.ts:31apps/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)。

  1. 提交与等待:装上跨回合的常驻事件订阅作为唯一的事件写者,调用 app.submitPrompt--attach 的文件按扩展名推断类型后随提示词一起提交(prompt-command.ts:290293)。如果观察到动态工作流活动,就每 100 毫秒轮询运行时的两个忙碌事实,等在飞的工作流和它们触发的通知回合全部结束,并存的后台 Bash 与子 Agent 任务也一起等;不设超时,Ctrl+C 才能打断(headless-workflow.ts:315327331)。--memory-bench 时再等记忆抽取排空(prompt-command.ts:322)。
  2. 输出response 取最后一个非空回合的文本,多于一个回合时另带 turnResponses 数组(prompt-command.ts:330)。三种格式:
格式stdoutstderr
text(缺省)各回合文本,空行分隔(prompt-command.ts:416动态工作流进度,节点迁移每 400 毫秒最多一行,开始、结束与 log 不节流(headless-workflow.ts:163176);工作区钩子被跳过时的诊断
json一个格式化的 JSON 对象:sessionIdtraceIdturnIdresponseusageeventCountprojection,钩子被跳过时另有 workspaceHookTrustprompt-command.ts:371
stream-json每个会话事件一行 NDJSON,信封与协议服务端相同;工作流进度单独定型为 workflow.run.progress;最后一行 type: "result" 是流的终止符(headless-workflow.ts:248prompt-command.ts:345

走命令中心的那条路径(/expert/goal--target)不透出事件:jsonstream-json 都只打印一个含 sessionIdtraceIdresponse 的 JSON 对象(prompt-command.ts:538)。从代码看,--target--output-format stream-json 拿到的并不是 NDJSON。命令中心若需要用户在几个目标之间选择,无头模式无法弹出选择器,直接失败并提示改用 --target-replaceprompt-command.ts:526)。

退出码:成功为 0;参数错误或运行出错为 1,stderr 上是 Error: <消息>,已知 traceId 时附在后面,--verbose 另打原因与调用栈(prompt-command.ts:418);收到 SIGINT、SIGTERM、SIGHUP 时先中止当前回合,清理最多等 2 秒,再以 130、143、129 退出(shutdown.ts:3654)。正常结束时 App、浏览器、遥测依次关闭,每步最多 6 秒,最后释放 Provider Registry(shutdown.ts:4prompt-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:9113233)。

两条打包路径

图表加载中…

普通 Node bundle是缺省路径(build.mjs:204)。esbuild 把 src/main.ts 打成一个 CJS 文件,target 定为 node22:桌面用 Electron 内置的 Node 24,远程 SSH 复用已部署的 Node v22.16,同一份产物要在两端都能跑(build.mjs:257)。@zcode/tuiplaywright-corekoffi 保持外置(build.mjs:14):TUI 在运行时经 Node 原生的动态 import 加载,Playwright 依赖运行时的包资源,koffi 按平台动态加载 .node 文件(build.mjs:236)。注释给 TUI 的理由是 Ink 7 与 yoga-layout 用了顶层 await,可 TUI 如今依赖的是 @mbears/opentui-core@mbears/opentui-reactapps/zcode-cli/packages/tui/package.json:22),仓库里已找不到 Ink,这句注释过时了。另有一个 Zod 去重插件,保证打进去的 Zod v4 只有 packages/shared 钉住的那一个版本、一份拷贝(build.mjs:1674253),内置 Provider 配置随产物放进 dist/providerbuild.mjs:219)。--desktop-agent 模式压缩代码、保留函数名、不带 sourcemap(build.mjs:123),由根目录的 scripts/build-desktop-agent-cli.mjs 依序构建各工作区包后调用,产物暂存进桌面端的 bundled-agentsscripts/build-desktop-agent-cli.mjs:94),见桌面应用

SEA 单文件是可选路径(apps/zcode-cli/packages/cli/scripts/build-sea.mjs:285)。它要求先有 dist/zcode.cjspostject,目标平台是 darwin、linux、win 各自的 arm64 与 x64 共六种(apps/zcode-cli/packages/cli/scripts/sea-targets.mjs:3),产物名为 zcode-<平台>-<架构>,Windows 的平台段写作 windows 并加 .exesea-targets.mjs:49)。每个目标的步骤:

  1. 收集资源进 SEA blob:TUI 运行时、官方插件(node-repl-hostbrowser-useapps/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)。
  2. 取目标平台的 Node 二进制:可用 --node-binary 指定,否则从 nodejs.org 下载与构建机相同版本的发行包,按 SHASUMS256.txt 校验 sha256,缓存命中也要重新校验(build-sea.mjs:301apps/zcode-cli/packages/cli/scripts/sea-node-download.mjs:106)。
  3. 复制二进制,找到 NODE_SEA_FUSE 标记;macOS 先去签名,Windows 先剥掉 Authenticode 签名;用 postject 注入 blob;macOS 再做 ad-hoc 签名(build-sea.mjs:257)。
  4. 宿主平台的产物用临时存储目录跑一次 --version 冒烟测试(build-sea.mjs:231)。

SEA 运行时要把资源解到磁盘上才能用,原生工具、TUI 与 Playwright 写盘前都比对 sha256:

  • 原生搜索工具解到 <storage>/cache/runtime_tools/<target>/<tool>/<version>-<sha>,同时校验大小与哈希,然后通过 ZCODE_BFS_BINARYZCODE_RG_BINARYZCODE_UGREP_BINARY 告诉后续代码;用户已设置这些变量时不覆盖(apps/zcode-cli/packages/cli/src/sea-runtime-tools.ts:527496119)。
  • 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:46120apps/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 依赖 dotenvplaywright-core 与十个工作区包(apps/zcode-cli/packages/cli/package.json:26),README 列出的 npm run bootstrapnpm run startnpm testapps/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-urlbuild-zcode.mjs:235);随后构建 @zcode/cli 及其依赖、@zcode/server@zcode/webbuild-zcode.mjs:142),再组装目录(build-zcode.mjs:157):

路径内容
bin/zcode.mjs分流入口
agent/zcode.cjsagent/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.jsonname: "zcode-runtime" 与版本号(build-zcode.mjs:206

输出目录缺省为 dist/zcode/releases/<version>/zcode-<version>.tar.gz、同目录的 sha256.txt、顶层的 latest.json(字段为 baseUrlcreatedAtnamesha256tarballversion)与 install.shbuild-zcode.mjs:250273)。版本缺省取根目录 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 versionzcode doctor 进到 Agent,报的是 CLI 版本 0.16.9。

--web 模式(runner.mjs:170):监听地址缺省 127.0.0.1runner.mjs:37),端口缺省由系统挑一个空闲的(runner.mjs:172);监听非本机地址时自动生成 24 字节随机令牌,--token 指定、--no-token 关闭(runner.mjs:109173);本机地址缺省打开浏览器(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):要求 nodecurltar 三个命令存在(installer.mjs:18),读 latest.json 取版本与包名,下载解压到 ~/.zcode/runtime/releases/<version>,把 current 软链接指过去,再在 ~/.local/bin 写一个 exec node .../bin/zcode.mjs 的包装脚本;三个位置可用 ZCODE_DIST_BASE_URLZCODE_DIST_HOMEZCODE_DIST_BIN_DIR 覆盖(installer.mjs:745)。README 说运行发行包需要的 Node 版本以 mise.toml 为准(README.md:162),安装脚本却只检查 node 是否存在;latest.jsonsha256.txt 都带了摘要,安装脚本也没有校验下载的包。

界面语言

文案集中在 @zcode/i18nen-USzh-CN 两份目录,内容只有 CLI 帮助、一条语言错误和 TUI 文案(apps/zcode-cli/packages/i18n/src/types.ts:5apps/zcode-cli/packages/i18n/src/index.ts:25),其余命令行报错都是英文。语言检测(apps/zcode-cli/packages/i18n/src/locale.ts:32)依次看 LC_ALLLC_MESSAGESLANGLANGUAGE(后者可以是冒号分隔的列表),再看 Intl 给出的区域;去掉 .UTF-8 这类编码与 @ 修饰,下划线换成连字符,CPOSIX 忽略,en 开头归为 en-USzh 开头归为 zh-CNlocale.ts:540)。

检测结果只在请求的语言是 auto 时才用上(locale.ts:24)。而帮助文本只看 --localeapps/zcode-cli/packages/cli/src/help.ts:3),配置里的 ui.locale 缺省又是 en-USapps/zcode-cli/packages/contracts/src/config/index.ts:356),所以不加 --localezcode --help 总是英文,TUI 也要启动时加 --locale,或配置里写了 autozh-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:56apps/zcode-cli/packages/i18n/src/locales/zh-CN.ts:56)。

下一篇:终端界面——不带参数运行 zcode 时看到的全屏界面:它的渲染栈、组件与快捷键,以及它为什么不持有任何业务状态。

本页目录