新 CLI 与守护进程
packages/cli 的 lildax 如何用 Effect CLI 声明命令并按需加载处理器,如何在后台拉起、认证并复用 V2 服务,以及它与旧 opencode CLI 共用同一套 TUI。
旧的 opencode 命令在 packages/opencode,由 yargs 驱动,内嵌 worker 与服务端,跑的是 V1 会话主循环(见启动链路)。packages/cli 是它的继任者雏形:包名 @opencode-ai/cli,package.json 里的可执行文件叫 lildax,根命令描述是 “OpenCode 2.0 preview command line interface”。整个包只有约 700 行,因为它几乎什么都不做:服务在 packages/server,内核在 packages/core,界面在 packages/tui。
命令一览
命令树在 packages/cli/src/commands/commands.ts。根命令名取编译期常量 OPENCODE_CLI_NAME,构建脚本注入 lildax,直接跑源码时回退成 opencode。
| 命令 | 作用 | 实现要点 |
|---|---|---|
| (无子命令) | 启动 TUI | 向守护进程要连接信息,再调用共享 TUI |
serve | 启动 V2 API 服务 | --hostname 默认 127.0.0.1;不给 --port 就从 4096 往上找空闲端口;--register 写注册文件 |
service start、stop、status、restart | 管理后台服务 | 见下文 |
service password [value] | 读取或设置服务密码 | 设置新值前先停服务 |
api | 直接调用服务端接口 | 参数可以是 OpenAPI 的 operationId,也可以是方法加路径;支持 -d、-H、--param |
debug agents | 列出所有 agent | 用 SDK 调 v2.agent.list |
migrate | v1 数据迁移到 v2 | 目前只打印 No migrations to run. |
api 的做法值得一提:给出 operationId 时,它先从服务端拉 /openapi.json 找到对应路径,把 --param 填进路径参数,剩下的放进查询串。协议即文档,CLI 不必为每个接口写子命令。
命令框架:声明与懒加载
framework/spec.ts 薄薄包了一层 effect/unstable/cli 的 Command;framework/runtime.ts 把命令树和处理器表逐个对上($ 表示节点本身),处理器一律动态 import:
const Handlers = Runtime.handlers(Commands, {
$: () => import("./commands/handlers/default"),
api: () => import("./commands/handlers/api"),
// ...
serve: () => import("./commands/handlers/serve"),
})
Runtime.run(Commands, Handlers, { version: "local" }).pipe(
Effect.provide(Daemon.layer),
Effect.provide(NodeServices.layer),
Effect.scoped,
NodeRuntime.runMain,
)packages/cli/src/index.ts:10(删减后)。启动时只加载真正要跑的那个处理器。TUI 抽包规格(specs/tui-package.md)要求具名的非 TUI 子命令不得启动或急切初始化 TUI,懒加载就是实现手段。
守护进程:在后台拉起并复用服务
services/daemon.ts 是这个包的核心。它在 Global.Path.state 下维护两个文件:server.json(记录服务的 id、version、url、pid)和 password,都先写临时文件再 rename,权限 0o600。密码第一次用 32 字节随机数生成,之后一直复用,注释说明了原因:
Keep one private credential across server restarts so discovered clients can reconnect without exposing a password flag or environment variable.
start 的流程:
const existing = yield* healthy().pipe(Effect.option)
const found = Option.getOrUndefined(existing)
const compiled = path.basename(process.execPath).replace(/\.exe$/, "") !== "bun"
if (found?.version === InstallationVersion && compiled) return found.url
if (found) yield* stopProcess(found).pipe(Effect.ignore)
// ...
spawn(process.execPath, [...(entrypoint ? [entrypoint] : []), "serve", "--register"], {
detached: true,
stdio: "ignore",
}).unref()
// ...
return yield* compatible().pipe(
Effect.retry(Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)))),packages/cli/src/services/daemon.ts:111(删减后)。健康检查用 SDK 调 v2.health.get,2 秒超时,请求带 HTTP Basic 认证(默认用户名 opencode)。编译产物遇到同版本的健康服务就直接复用;直接用 bun 跑源码时则每次重启服务,方便开发。新服务以 detached 方式启动,CLI 每 50 毫秒探测一次、最多 100 次,直到服务健康且版本一致。
停服务同样谨慎:
A stale registration may point at a PID that has since been reused by another process. Only signal the PID after authenticating the server.
stop 先确认注册的服务能通过认证的健康检查才发 SIGTERM,约 5 秒不退出再发 SIGKILL。服务一侧,serve --register 写完注册文件后每 10 秒核对一次:文件已指向别的实例(被新服务取代)就给自己发 SIGTERM;退出时只在文件仍属于自己时才删除它。
serve 本身把 packages/server 的 createRoutes(password) 挂到 Node HTTP 服务器上,再补上凭据与保存的权限两个节点;路由内部如何组装 V2 服务见 Effect 运行时与 Location。设了密码时,服务端的授权中间件强制校验 Basic 认证,也接受 auth_token 查询参数。
同一个 TUI,两个宿主
packages/cli/src/tui.ts 只有 37 行:拿到守护进程给的 url 与 headers,调用 @opencode-ai/tui 导出的 run,插件宿主是空实现。关键在它自带的 gracefulFetch:新服务端只实现 /api/... 这套 V2 协议,TUI 仍会请求的几个旧接口(/config/providers、/provider、/agent、/config)返回 404 时,就用空的默认值兜底。这对应 TUI 抽包规格第 9 节的描述:缺失的旧 provider、config 接口暂时退化到共享的“连接 provider”界面。
旧 CLI 这边,opencode [project] 与 attach <url> 通过 packages/opencode/src/cli/tui/layer.ts 调用同一个 run,只是传入完整的旧版插件宿主、堆快照回调等。规格第 9、10 节均已标记完成:TUI 是独立包,两个 CLI 都是薄适配器,后端数据走 SDK(详见终端界面)。不过规格要求 TUI 不引入 @opencode-ai/core,而 v1.18.30 的 packages/tui 仍从 core 引入 Global、Flag、安装版本等少量工具模块,所以两个宿主调用 run 时都要提供 Global.node。
分发
bin/lildax.cjs 是一个 Node 启动脚本:按平台、架构、是否支持 AVX2、是否 musl,挑出 @opencode-ai/cli-<平台>-<架构> 这一族包里的二进制(也可用 OPENCODE_BIN_PATH 指定),并转发 SIGINT、SIGTERM、SIGHUP。二进制由 script/build.ts 调用 Bun.build 的 compile 模式产出,覆盖 Linux、macOS、Windows 共 12 个目标。
与旧 CLI 的关系
opencode(旧) | lildax(新) | |
|---|---|---|
| 命令解析 | yargs | effect/unstable/cli |
| 服务端 | 进程内 worker,或 attach 远程 | 常驻后台守护进程,按版本复用 |
| 会话内核 | V1 主循环 | V2(/api/session) |
| TUI | @opencode-ai/tui | 同一个包 |
从代码能读出迁移方向:新 CLI 只做“连接 + 管理守护进程”,业务全部经由 HTTP 协议与 SDK;默认命令已能用同一个 TUI 连上 V2 服务端;而 migrate 还是空实现,旧数据迁入 V2 尚未落地,新 CLI 仍然是 preview。
下一篇:收尾复盘——回看两代架构并存时的全局取舍。