# UniDesk CLI Reference

UniDesk 的统一 CLI 入口是根目录 `scripts/cli.ts`，运行方式固定为 `bun scripts/cli.ts <command>`。CLI 默认输出 JSON，所有成功和失败路径都必须向 stdout 写出结构化对象，避免无输出造成状态不可观测。

## Command Model

- `help` 输出命令索引，适合作为交互式入口。
- `--main-server-ip <ip> <command>` 默认通过公网 frontend 登录态调用主 server 的同源 API 代理，不要求计算节点持有主 server SSH key；显式提供 `--main-server-key` 或 `--main-server-transport ssh` 时才使用旧 SSH 传输。
- `config show` 读取并校验根目录 `config.json`，不从环境变量、默认值或隐藏文件静默补配置。
- `check` 执行配置校验、文件存在性检查、`scripts/` TypeScript 检查、`src/components/` TypeScript 检查和 Docker Compose 配置检查。
- `server start` 创建异步 job，在后台执行 Docker 构建和启动；命令本身只负责返回 job id、日志路径和启动命令。
- `server stop` 创建异步 job，在后台停止固定 Compose project 中的全部 UniDesk 服务。
- `server status` 查询公开端口、受限宿主端口、内部端口、Compose 容器、core/frontend/provider/database 健康检查和访问 URL；D601 Code Queue 使用的 PostgreSQL/OA Event Flow host mapping 必须出现在受限宿主端口而不是无条件公开入口中。
- `server logs` 返回 `logs/` 文件日志和 Docker 容器日志的尾部，默认限制输出大小，避免日志爆炸。
- `server rebuild <backend-core|frontend|provider-gateway|todo-note|project-manager|baidu-netdisk|oa-event-flow>` 创建异步 job，先构建目标服务镜像，随后在 `.state/locks/server-compose.lock` 串行保护下用 `--no-deps --force-recreate` 替换目标 service 并等待容器 `healthy/running`；该命令用于替代手工删除容器的兜底流程，其中 `todo-note`、`project-manager`、`baidu-netdisk` 和 `oa-event-flow` 只重建主 server 承载的对应后端，不会重建或删除 database 命名卷。Code Queue 部署在 D601，不再由 `server rebuild` 管理。
- `provider attach <providerId> [--master-server URL] [--up] [--force]` 在新计算节点生成两项配置的 provider-gateway 挂载包：`.state/provider-<ID>.env` 默认只包含 `UNIDESK_MASTER_SERVER` 与 `PROVIDER_ID`，`provider-<ID>.yml` 固定 Docker socket、`pid: "host"`、`restart: always`、只读 `/workspace` 和 SSH 维护私钥挂载；`--up` 会立即执行生成的 `docker compose up -d --build`。
- `ssh <providerId> [ssh-like args...]` 通过 backend-core 内网 WebSocket broker 和 provider-gateway 的 Host SSH / WSL SSH 维护桥连接目标节点；无后续参数时进入远端登录 shell，有后续参数时按 ssh 远端命令体验执行并返回远端 exit code。
- `ssh <providerId> apply-patch [tool args...] < patch.diff` 直接调用远端注入的 `apply_patch` 工具，并把本地 stdin 中的标准 `*** Begin Patch` / `*** End Patch` patch 流透传给目标节点。
- `ssh <providerId> py [script-args...] < script.py` 把本地 stdin 落到远端临时 `.py` 文件后再以 `python3 -u` 执行并自动清理，避免再手写 `'python3 -'`、heredoc 或多层引号；`script-args` 会按 argv 安全透传给远端脚本。
- `ssh <providerId> skills [--scope all|wsl|windows] [--limit N]` 发现目标节点上的 WSL/Linux skill 根目录；当 provider 是 WSL 时同一次调用还会扫描 Windows 用户目录下的 `.agents/skills` 与 `.codex/skills`。
- `microservice list/status/health/proxy` 通过 backend-core 内网 API 管理挂载在计算节点 Docker 中的用户服务（底层命令名仍为 microservice）；`health` 和 `proxy` 会走真实 backend-core -> provider-gateway -> 节点本机后端链路，`proxy` 对超大 body 默认输出有界预览，规则见 `docs/reference/microservices.md`。
- `deploy check/plan/apply` 从根目录 `deploy.json` 读取服务 repo 与 commit 期望状态，join `config.json` 和现有 manifest 后使用 target-side build 单一路径校验或更新直管服务与 v3s/k3s 代管服务；规则见 `docs/reference/deploy.md`。
- `codex deploy <commitId>` 是 Code Queue 兼容部署入口，会生成临时 desired manifest 并调用 `deploy apply --service code-queue` 的同一条 target-side build、k3s import、rollout 和 live commit 验证路径；详细规则见 `docs/reference/codex-deploy.md`。
- `codex task <taskId>` 通过 Code Queue 私有代理按任务 ID 查询结构化执行摘要；默认只返回有界 prompt/response 预览、执行 Provider、工作目录、最后 assistant message、最近工具调用摘要、attempt、judge、错误、耗时和 trace 翻页提示，适合在新队列任务中引用历史 session 且避免噪声爆炸。
- `codex task <taskId> --trace --tail|--from-start|--after-seq N|--before-seq N --limit N` 按页拉取 Code Queue 的逻辑 trace；响应会返回 `nextAfterSeq`、`previousBeforeSeq`、`hasMore`、`hasBefore` 和下一页/上一页命令，默认 `--trace` 取最新一页，需要完整 prompt/最后 response 时加 `--full`。
- `codex output <taskId> --tail|--from-start|--after-seq N|--before-seq N --limit N [--full-text]` 按原始 output seq 分页读取底层记录；当 trace 行提示 `commandOmittedLines`、`bodyOmittedLines` 或 `rawSeqs` 时，用该命令按 seq 补取完整信息，默认仍有单条文本预览上限，显式 `--full-text` 才返回该页全文。
- `codex judge <taskId> --attempt N [--dry-run] [--include-prompt]` 通过 Code Queue 私有代理按指定 attempt 单步复现 judge；后端会从 PostgreSQL task JSON 与 output 归档重建该 attempt 在真实队列 worker 中的 `QueueTask`/`CodexRunResult`，再调用同一套 judge prompt builder 和 MiniMax 请求路径。默认会真实调用 MiniMax，`--dry-run` 只返回 prompt/payload 大小、attempt 窗口和重建来源诊断，`--include-prompt` 仅用于本地深度排查。
- Code Queue 多队列 lane 由 `codex` 命令命名空间管理：`queues` 列表、`queue create <queueId>` 创建、`queue merge <sourceQueueId> --into <targetQueueId>` 合并、`move <taskId> --queue <queueId>` 迁移；同一个 queue 内部串行执行，不同 queue 之间并行执行。合并会移动任务归属并自动删除源 queue 记录，只保留合并后的目标 queue；合并后的目标 queue 按任务原 `queueEnteredAt`/`createdAt` 时间顺序串行。迁移 queued/retry_wait 任务后会立即调度目标 queue。
- `job list` 与 `job status` 查询 `.state/jobs/` 文件系统状态，是异步命令的可观测入口。
- `debug health`、`debug dispatch` 与 `debug task` 走真实内部 core、WebSocket、数据库、provider、系统指标、Docker 状态和 Host SSH 维护桥流程，只用于开发调试，不写入 `TEST.md` 的正式验收步骤。
- `e2e run [--only pattern[,pattern...]] [--skip pattern[,pattern...]]` 使用 publicHost 派生的公开 frontend/provider ingress URL，并通过 Docker 内网验证 core API、PostgreSQL、provider self-connection、系统指标曲线、Docker 状态快照、provider.upgrade 预检和 Playwright 前端页面，是交付前的自动化 E2E 门禁；CLI 默认输出 check 状态摘要，完整诊断写入 `resultPath`，日常迭代应优先用 `--only` / `--skip` 跑最小必要集合。

## Async Job State

长时操作采用 Fire-and-Forget 模式：CLI 创建 `.state/jobs/{jobId}.json`，后台进程执行真实命令，并将 stdout、stderr 分别写入 `.state/jobs/{jobId}.stdout.log` 与 `.state/jobs/{jobId}.stderr.log`。调用者通过 `bun scripts/cli.ts job status <jobId>` 查询进度和尾部输出。

`server rebuild` 与 `server start`、`server stop` 一样必须通过返回的 job id 确认结果；不要把连续 `server rebuild` 命令理解成“前一个重建已完成”，因为两个命令只是在快速创建异步 job。重建 frontend 的标准流程是运行 `bun scripts/cli.ts server rebuild frontend`，随后轮询 `bun scripts/cli.ts job status <jobId>` 到 `succeeded`，再用 `server status` 或 `e2e run` 验证公网 frontend；重建 Todo Note 后端使用 `bun scripts/cli.ts server rebuild todo-note`，随后用 `microservice health todo-note` 和 `microservice proxy todo-note /api/instances` 验证；重建 Project Manager 后端使用 `bun scripts/cli.ts server rebuild project-manager`，随后用 `microservice health project-manager` 和 `microservice proxy project-manager /api/projects` 验证；重建 Baidu Netdisk 后端使用 `bun scripts/cli.ts server rebuild baidu-netdisk`，随后用 `microservice health baidu-netdisk` 和 `microservice proxy baidu-netdisk /api/transfers` 验证；重建 OA Event Flow 后端使用 `bun scripts/cli.ts server rebuild oa-event-flow`，随后用 `microservice health oa-event-flow` 和 `microservice proxy oa-event-flow /api/diagnostics` 验证。Code Queue 后端由 D601 v3s/k8s 控制面代管，必须使用 `bun scripts/cli.ts deploy apply --service code-queue` 或兼容入口 `bun scripts/cli.ts codex deploy <commitId>` 部署已 push 的 remote commit；部署 job 自身必须通过真实 `/health` 和 k3s Deployment annotation 证明不是旧服务在充数，之后再用 `microservice health code-queue` 和 `microservice proxy code-queue /api/tasks/overview` 做人工复核。不得把 `docker rm` 手工兜底当成正式交付步骤。

新部署入口优先使用 `deploy apply`。旧的 `server rebuild` 和 `codex deploy` 只保留为兼容入口，后续实现应收敛到同一个 reconciler：从 remote commit 导出源码，在目标节点一次性代理构建镜像，部署后用 live commit 校验证明不是旧服务。

## Output Contract

每条命令的最外层 JSON 包含 `ok`、`command` 和 `data` 或 `error`。失败时 CLI 设置非零退出码，但仍然输出 JSON 错误对象；错误对象应包含 `name`、`message` 和可用的 `stack`。

`microservice proxy` 是面向人工验证的私有后端读取入口。正式写入型用户服务操作由 frontend 同源代理或 E2E 直接调用 backend-core 完成，并由 config 中的 `allowedMethods` 限制；CLI `proxy` 默认仍作为 GET/HEAD 读取验证入口，必要时可显式加 `--method POST|PUT|PATCH|DELETE` 调用无需自定义请求体的受控调试/自测端点，例如 `bun scripts/cli.ts microservice proxy baidu-netdisk /api/self-test --method POST --raw`。为了避免 Pipeline snapshot 这类超大业务 JSON 造成 CLI 输出爆炸，响应 body 超过默认阈值时会返回 `bodyOmitted=true`、`bodyPreview`、`bodyBytes` 和 `rawHint`；需要完整 body 时显式添加 `--raw`，或用 `--max-body-bytes <N>` 调整预览阈值。正式 frontend 展示仍应优先使用业务控件和 `__unideskArrayLimit` 这类展示级裁剪参数，而不是默认倾倒完整 JSON。

## Debug Contract

`debug` 子命令必须复用真实模块与真实端点，禁止维护平行实现。`debug health` 会摘要展示 `/api/nodes/system-status` 和 `/api/nodes/docker-status`，避免输出完整快照造成信息爆炸。`debug dispatch` 会在 backend-core 容器内调用内部 `/api/dispatch`，core 再通过 WebSocket 将 `docker.ps`、`provider.upgrade`、`host.ssh`、`microservice.http` 或 `echo` 任务下发给 provider gateway，因此它可以验证核心调度闭环，同时不需要公开 core REST API。`provider.upgrade` 默认使用 `mode: "plan"` 预检；需要验证一键升级时必须显式加 `--mode schedule`，并通过 `--wait-ms` 或 `debug task` 确认任务进入 `succeeded`、result 中包含 updater 容器信息和 `policy: "always-enabled"`。`host.ssh` 默认使用 `mode: "probe"` 做短超时维护桥自检；需要执行明确命令时使用 `--ssh-command` 进入 `mode: "exec"`，并配合 `--wait-ms` 和 `debug task` 查看 stdout、stderr、exitCode 与 probeLine。`microservice.http` 只用于开发调试 provider-gateway 私有 HTTP 代理，正式用户入口应使用 `microservice` CLI 或 frontend 的用户服务页面。

## SSH Command

`ssh <providerId> [ssh-like args...]` 是面向人的终端透传入口，不包装 JSON 输出。CLI 会在宿主机启动一个 `docker exec -i unidesk-backend-core bun -e ...` broker，broker 只连接 backend-core 的 Docker 内网 `/ws/ssh`，core 再把 stdin/stdout/stderr 流量通过目标 provider 的既有 WebSocket 转发到 provider-gateway，provider-gateway 最终执行维护用 SSH 连接宿主或 WSL sshd。TTY 策略固定为交互登录 shell 使用 `ssh -tt`，带远端命令的会话使用 `ssh -T`；脚本 stdin、`apply-patch` 和 `py` 这类命令模式不得被伪终端回显或注入控制字符。该入口不新增 core 公网端口，不暴露 database，也不改变 frontend/provider ingress 之外的公网边界。

`bun scripts/cli.ts ssh D518` 应表现为登录 D518 WSL 的 shell；`bun scripts/cli.ts ssh D518 hostname` 应像 `ssh D518 hostname` 一样只输出远端命令结果并返回远端 exit code。Provider ID 前的目标选择由 UniDesk 节点清单决定，`-p`、`-i`、`-l`、`-o` 等传统 ssh 传输参数由 provider-gateway 部署配置统一管理，CLI 会兼容性消费这些参数但不会覆盖节点侧维护桥配置。

core 只允许声明了 `host.ssh` capability 的 provider 使用 `ssh` 透传或 `host.ssh` dispatch；旧 provider 不支持该能力时必须快速失败并输出错误，不能把未知命令误判成 `echo` 成功。

本地 broker 默认等待 provider SSH 会话打开 60000ms，以便在目标节点同时有较多 microservice.http 任务时仍能建立维护会话；需要诊断慢连接时可用 `UNIDESK_SSH_OPEN_TIMEOUT_MS=<ms>` 临时调大，但最小有效值固定为 15000ms，避免把真实离线误判为长时间阻塞。

`ssh <providerId>` 会在远端会话启动时注入 `/tmp/unidesk-ssh-tools/apply_patch`、`/tmp/unidesk-ssh-tools/glob` 和 `/tmp/unidesk-ssh-tools/skill-discover`，并把该目录加入远端 `PATH`。`apply_patch` 接受标准 `*** Begin Patch` / `*** End Patch` patch 格式，便于通过 SSH 透传编辑远端仓库文件；`glob` 在远端用 Python 执行路径匹配，避免依赖 shell glob 展开；`skill-discover` 用于列出远端 Linux/WSL 与 Windows skill。目标节点需要具备 `python3` 和 `base64`。注入工具只写 `/tmp/unidesk-ssh-tools`，不修改目标仓库，交互式 shell 和远端命令都可以直接调用这些工具。

如果只是远端打小补丁，不需要再手写 `ssh D601 'apply_patch' < patch.diff` 这种命令拼接；正式入口是 `bun scripts/cli.ts ssh D601 apply-patch < patch.diff`。`apply-patch` 与 `patch` 等价，附加参数会原样透传给远端 `apply_patch`，例如 `bun scripts/cli.ts ssh D601 apply-patch --help`。标准单命令用法如下，不需要先创建本地 patch 临时文件：

```bash
bun scripts/cli.ts ssh D601 apply-patch <<'PATCH'
*** Begin Patch
*** Update File: /home/ubuntu/pipeline/scripts/src/nodeControl.ts
@@
-const value = "old";
+const value = "new";
*** End Patch
PATCH
```

如果只是想远端执行 Python 脚本，不要再手写 `bun scripts/cli.ts ssh D601 'python3 -' < script.py`。正式入口是 `bun scripts/cli.ts ssh D601 py < script.py`；CLI 会先把本地 stdin 写入远端临时 `.py` 文件，再以无缓冲模式执行并自动清理，同时对额外脚本参数逐个做 shell quoting，避免字符串转义问题。典型用法：

```bash
printf 'import sys\nprint(sys.argv)\n' | bun scripts/cli.ts ssh D601 py foo '--bar=baz'
```

`ssh <providerId> py` 的附加参数是脚本参数，不是 Python 解释器参数；如需 `-m`、`-X` 或多条 shell 命令，仍使用原始远端命令入口。为了保证 CLI 输出及时可见，helper 固定采用“临时文件 + `python3 -u`”模式；provider 命令模式不分配 TTY，因此脚本内容不应被远端回显。

`ssh <providerId> skills` 是远端 skill 发现入口，也可写作 `ssh <providerId> skill discover`。输出固定为 JSON，包含 `node`、`roots`、`counts` 和 `skills`：`roots` 会显示每个候选 skill 根目录是否存在、扫描到多少 skill 以及错误；`skills` 会给出 `scope`、`name`、`description`、`path`、`skillMd` 和可转换时的 `windowsPath`。默认扫描远端用户的 `~/.agents/skills`、`~/.codex/skills`、可访问的 `/root/.agents/skills`、`/root/.codex/skills`；如果目标是 WSL，还会扫描 `/mnt/c/Users/*/.agents/skills` 与 `/mnt/c/Users/*/.codex/skills`，从而一次性看清 WSL 和 Windows 两套 skill。常用参数是 `--scope wsl`、`--scope windows`、`--limit N`、`--max-depth N`、`--root <path>` 和 `--windows-root <path>`；不要用宽泛的 Linux `find /mnt/*` 扫 Windows 盘，优先用这个结构化入口避免卡在 Windows 挂载层。

```bash
bun scripts/cli.ts ssh D601 skills --limit 80
bun scripts/cli.ts ssh D601 skills --scope windows --limit 40
```

Windows 工具链透传的 wrapper、路径转换、是否修改 skill、是否额外安装依赖等长期规则见 `docs/reference/windows-passthrough.md`；`ssh skills` 本身只负责发现，不会修改远端 skill。

`ssh <providerId> find` 是常用远端搜索的结构化入口，避免在 Host SSH / WSL SSH 透传里手写 `find \( ... \)`、`*`、管道和多层引号。它会把路径、谓词和 pattern 作为 argv 安全拼接，并支持重复 `--name`、`--iname`、`--path` 或 `--ipath`，重复 pattern 默认按 OR 组合。稳定参数包括 `--max-depth`/`-maxdepth`、`--min-depth`/`-mindepth`、`--type`/`-type`、`--contains`、`--icontains`、`--name`/`-name`、`--iname`/`-iname`、`--path`/`-path`、`--ipath`/`-ipath`、`--mtime`/`-mtime`、`--mmin`/`-mmin`、`--size`/`-size`、`--sort` 和 `--limit N`。典型用法：

```bash
bun scripts/cli.ts ssh D601 find /home/ubuntu --max-depth 4 --type d --icontains pika --limit 50 --sort
```

`ssh <providerId> glob` 是远端 glob 匹配入口，支持 `--root DIR`、`--pattern PATTERN`、`--contains TEXT`、`--icontains TEXT`、`--type any|f|d`、`--limit N`、`--sort` 和 `--absolute`。`--contains` 与 `--icontains` 可避免在本地 shell 中输入 `*`；若显式使用 `--pattern '**/*.ts'` 这类 pattern，仍应按本地 shell 规则加引号，防止参数到达 CLI 前已被本地 shell 展开。典型用法：

```bash
bun scripts/cli.ts ssh D601 glob --root /home/ubuntu/pikapython --pattern '**/*-test.cpp' --limit 20 --sort
```

`ssh <providerId> argv <command> [args...]` 是通用 argv 安全拼接入口；`exec` 是同义入口。它适合不需要 shell 管道的常用命令。`find`、`glob` 和 `apply-patch` 有专用入口；`rg`、`grep`、`sed`、`nl`、`stat`、`du`、`ls`、`cat`、`head`、`tail`、`wc` 和 `pwd` 可以直接作为 `ssh` 子命令使用，CLI 会对每个 argv token 做 shell quoting。需要管道、重定向、变量展开或多条命令时仍使用旧的自由远端命令入口，并把整段远端 shell 脚本作为一个本地参数传入。

通过 `ssh <providerId>` 执行多行脚本时，优先使用结构化 helper，例如 `bun scripts/cli.ts ssh D601 py < script.py` 或 `printf ... | (bun scripts/cli.ts ssh D601 'bash -s')` 这种单层 stdin 传输。不要在远端命令字符串里再嵌套 heredoc、复杂引号或 `ssh 'python3 - <<EOF ...'` 形态；多层 shell 解析容易把 stdin 绑定到错误进程，结果会打开远端交互解释器并留下悬挂的 broker/SSH 会话。长脚本需要复用时，优先通过 stdin 写入目标节点的临时脚本，再在同一个远端命令中显式执行并清理。

## Remote Main Server Passthrough

`--main-server-ip` 是一个全局前缀，必须放在需要透传的命令同一次调用中，例如 `bun scripts/cli.ts --main-server-ip 74.48.78.17 debug health`。默认传输是公网 frontend：本地 CLI 读取本仓库 `config.json` 中的 frontend 登录账号密码，登录 `http://<ip>:<frontendPort>/` 获取 HttpOnly session cookie，然后通过 frontend 的 `/api/*` 同源代理访问 backend-core 内网 API；因此计算节点只需要能访问公网 frontend，不需要主 server SSH key，也不需要打开 backend-core REST API 或 PostgreSQL 端口。

默认 frontend 传输支持 `debug health`、`debug dispatch`、`debug task`、`microservice list/status/health/proxy`、`codex task <taskId>`、`codex output <taskId>`、`codex judge <taskId> --attempt N` 和 `ssh <PROVIDER_ID> <remote-command>`。其中 `ssh` 的 remote frontend 传输使用 `host.ssh` dispatch 执行有界远端命令，适合 `ssh D601 hostname` 和 `ssh D601 skills` 这类自测；交互式登录 shell 仍应在主 server 本机 CLI 使用，或显式切换到旧 SSH 传输后在主 server 上执行。frontend 远程透传不会流式转发本地 stdin，因此 `ssh py < script.py`、`ssh apply-patch < patch.diff` 这类 stdin-backed helper 必须在主 server 本机运行，或显式切换到 `--main-server-transport ssh`。若确实需要旧行为，可使用 `--main-server-key <key>` 或 `--main-server-transport ssh`，这时 CLI 会通过 SSH 登录主 server 的 `--main-server-root` 目录执行同一个 `bun scripts/cli.ts <command>`。

计算节点可以用该入口测试自身的远程升级闭环，而不需要在计算节点公开 core REST API 或 database。标准顺序是：先运行 `bun scripts/cli.ts --main-server-ip 74.48.78.17 debug health` 确认主 server 看到当前 Provider 在线，且该 Provider labels 中 `unideskCapabilities` 包含 `host.ssh`、`hostSshConfigured=true`、`hostSshKeyPresent=true`；再运行 `bun scripts/cli.ts --main-server-ip 74.48.78.17 debug dispatch <PROVIDER_ID> provider.upgrade --mode schedule --wait-ms 15000` 触发真实 `provider.upgrade`；随后再次运行 `debug health` 确认节点重新上线；最后运行 `bun scripts/cli.ts --main-server-ip 74.48.78.17 debug dispatch <PROVIDER_ID> host.ssh --wait-ms 15000` 和 `bun scripts/cli.ts --main-server-ip 74.48.78.17 ssh <PROVIDER_ID> hostname` 验证 SSH 透传能力。provider-gateway 新部署或升级后没有完成这组 remote CLI 自测，不能视为交付完成。

远程透传的安全边界是公网 frontend 登录态和 frontend 到 backend-core 的内网代理；不要把 provider token、数据库端口或 backend-core REST API 暴露给计算节点。旧 SSH 传输只作为兼容路径保留，不得把“必须提供主 server SSH key”作为计算节点自测的前置条件。
