fix: require explicit trans shell operations
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: unidesk-trans
|
||||
description: UniDesk SSH 透传与 apply-patch 语法 — `trans <route> <operation>` 分布式执行入口,包含 route 语法、workspace/k3s/Windows 路由、apply-patch envelope 格式、script/py/upload/download operation 和 60s 短连接约束。用户提到 trans、tran、ssh 透传、远端执行、apply-patch、apply_patch、远端 patch、k3s route、workspace route 时使用。
|
||||
description: UniDesk SSH 透传与 apply-patch 语法 — `trans <route> <operation>` 分布式执行入口,包含 route 语法、workspace/k3s/Windows 路由、apply-patch envelope 格式、sh/bash/py/upload/download operation 和 60s 短连接约束。用户提到 trans、tran、ssh 透传、远端执行、apply-patch、apply_patch、远端 patch、k3s route、workspace route 时使用。
|
||||
---
|
||||
|
||||
# UniDesk Trans(SSH 透传 + apply-patch)
|
||||
@@ -22,13 +22,13 @@ trans <providerId>:<plane>[:<namespace>:<resource>[:<container>]] <operation> [a
|
||||
|
||||
```bash
|
||||
trans G14:/root/hwlab git status --short --branch
|
||||
trans G14:/root/hwlab-v02 script -- 'git fetch origin v0.2 && git pull --ff-only origin v0.2'
|
||||
trans D601:/home/ubuntu/workspace/unidesk-dev script <<'SCRIPT'
|
||||
trans G14:/root/hwlab-v02 sh -- 'git fetch origin v0.2 && git pull --ff-only origin v0.2'
|
||||
trans D601:/home/ubuntu/workspace/unidesk-dev sh <<'SH'
|
||||
...
|
||||
SCRIPT
|
||||
SH
|
||||
```
|
||||
|
||||
**规则**: workspace 路径写在 route 第一个 token,不写进 `cd` 串。反面示例:~~`trans G14 script -- 'cd /root/hwlab && git status'`~~
|
||||
**规则**: workspace 路径写在 route 第一个 token,不写进 `cd` 串。反面示例:~~`trans G14 sh -- 'cd /root/hwlab && git status'`~~
|
||||
|
||||
### k3s route
|
||||
|
||||
@@ -80,24 +80,32 @@ GitHub issue/PR 正文读写也走 `trans gh:/owner/repo/...`。常见读取优
|
||||
|
||||
## Operation
|
||||
|
||||
### script(POSIX shell heredoc)
|
||||
### sh / bash(显式 shell heredoc)
|
||||
|
||||
```bash
|
||||
# heredoc(多行脚本)
|
||||
trans G14:/root/hwlab script <<'SCRIPT'
|
||||
# POSIX /bin/sh heredoc
|
||||
trans G14:/root/hwlab sh <<'SH'
|
||||
echo "step 1"
|
||||
git status --short --branch
|
||||
echo "step 2"
|
||||
SCRIPT
|
||||
SH
|
||||
|
||||
# Bash heredoc:只有用到 pipefail、数组、[[ ... ]]、${var:0:8} 等 Bash 语法时使用
|
||||
trans G14:/root/hwlab bash <<'BASH'
|
||||
set -euo pipefail
|
||||
short="${GITHUB_SHA:0:8}"
|
||||
printf '%s\n' "$short"
|
||||
BASH
|
||||
|
||||
# one-liner
|
||||
trans G14:/root/hwlab script -- 'git fetch origin G14 && git pull --ff-only origin G14'
|
||||
trans G14:/root/hwlab sh -- 'git fetch origin G14 && git pull --ff-only origin G14'
|
||||
|
||||
# direct argv(单进程,带 -- 参数)
|
||||
trans D601:/path script -- sed -n '1,20p' AGENTS.md
|
||||
# 单进程 direct argv 不放在 sh/bash 下,直接用已知子命令或 argv
|
||||
trans D601:/path sed -n '1,20p' AGENTS.md
|
||||
trans D601:/path argv sed -n '1,20p' AGENTS.md
|
||||
```
|
||||
|
||||
`script` 只走 host/k3s POSIX shell。Windows PowerShell 必须用 `ps`。
|
||||
`script` 和 `shell` operation 已移除并会失败;必须在 operation 位置明确写 `sh` 或 `bash`,让调用者和 Agent 都知道脚本语法边界。Windows PowerShell 必须用 `ps`。
|
||||
|
||||
### argv(单命令)
|
||||
|
||||
@@ -293,18 +301,18 @@ PATCH
|
||||
|
||||
```bash
|
||||
# ✅ 异步启动 + 短轮询
|
||||
trans G14:/root/hwlab script -- 'hwlab-cli case run start d601-f103-v2-compile'
|
||||
trans G14:/root/hwlab script -- 'hwlab-cli case run status <runId>'
|
||||
trans G14:/root/hwlab sh -- 'hwlab-cli case run start d601-f103-v2-compile'
|
||||
trans G14:/root/hwlab sh -- 'hwlab-cli case run status <runId>'
|
||||
|
||||
# ❌ 长时间挂着等
|
||||
trans G14:/root/hwlab script -- 'long_running_command && wait'
|
||||
trans G14:/root/hwlab sh -- 'long_running_command && wait'
|
||||
```
|
||||
|
||||
### HINT 信号
|
||||
|
||||
| stderr 输出 | 含义 |
|
||||
|-------------|------|
|
||||
| `UNIDESK_SSH_HINT` | SSH 握手/超时摩擦,提示改用 stdin script |
|
||||
| `UNIDESK_SSH_HINT` | SSH 握手/超时摩擦,提示改用 `sh`/`bash` stdin heredoc |
|
||||
| `UNIDESK_TRAN_TIMEOUT_HINT` | 60s 硬超时触发,提示改用短查询+轮询 |
|
||||
| `UNIDESK_SSH_TIMING` | 慢命令 warning(>10s),用于性能回归监控 |
|
||||
| `UNIDESK_APPLY_PATCH_TIMING` | apply-patch 耗时摘要 |
|
||||
@@ -319,25 +327,25 @@ trans G14:/root/hwlab script -- 'long_running_command && wait'
|
||||
|
||||
```bash
|
||||
# ✅ 推荐
|
||||
trans G14:/root/hwlab script <<'SCRIPT'
|
||||
trans G14:/root/hwlab sh <<'SH'
|
||||
cmd1
|
||||
cmd2
|
||||
SCRIPT
|
||||
SH
|
||||
|
||||
# ❌ 避免
|
||||
trans G14 script -- 'cmd1 && cmd2 && cmd3'
|
||||
trans G14 sh -- 'cmd1 && cmd2 && cmd3'
|
||||
```
|
||||
|
||||
### 本地 shell 运算符陷阱
|
||||
|
||||
`&&` / `;` / `|` 在 `trans` 后面会被本地 shell 截获。需要远端执行多条命令时用 `script` 包裹:
|
||||
`&&` / `;` / `|` 在 `trans` 后面会被本地 shell 截获。需要远端执行多条命令时用 `sh` 或 `bash` 包裹:
|
||||
|
||||
```bash
|
||||
# ❌ 第二个 sed 在 master server 本地执行
|
||||
trans G14:/root/hwlab sed -n '1,20p' a && sed -n '1,20p' b
|
||||
|
||||
# ✅ 两个 sed 都在远端
|
||||
trans G14:/root/hwlab script -- 'sed -n "1,20p" a && sed -n "1,20p" b'
|
||||
trans G14:/root/hwlab sh -- 'sed -n "1,20p" a && sed -n "1,20p" b'
|
||||
```
|
||||
|
||||
### Windows quoting
|
||||
|
||||
+31
-41
@@ -43,14 +43,14 @@ G14/D601 v03 的 bootstrap admin password 是 HWLAB runtime Secret 生命周期
|
||||
- `server rebuild <backend-core|frontend|dev-frontend-proxy|provider-gateway|todo-note|code-queue-mgr|project-manager|baidu-netdisk|oa-event-flow>` 创建异步 job,先构建目标服务镜像,随后在 `.state/locks/server-compose.lock` 串行保护下用 `--no-deps --force-recreate` 替换目标 service 并等待容器 `healthy/running`;该命令用于替代手工删除容器的兜底流程,其中 `dev-frontend-proxy` 只更新主 server dev 入口薄代理,`todo-note`、`code-queue-mgr`、`project-manager`、`baidu-netdisk` 和 `oa-event-flow` 只重建主 server 承载的对应后端,不会重建或删除 database 命名卷。D601 Code Queue 执行面不由 `server rebuild` 管理;Rust backend-core 常规迭代不得用该命令在 master server 编译,只有明确的 backend-core 主 server 上线例外可以按限流、异步轮询和 health 证据执行,规则见 `docs/reference/dev-environment.md`。
|
||||
- `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`。`provider triage <providerId> [--observed-error text] [--observed-scope scope] [--microservice id ...] [--full|--raw]` 是只读多信号健康裁决入口,会把单路径 `provider is not online`、SSH 超时、registry 失败和 service proxy 失败归类成 `runner-local-observation-gap`、`service-degraded`、`provider-degraded` 或 `global-blocker`。默认输出只返回裁决、scope、失败/降级/未知信号和有界 evidence 摘要,完整 evidence 必须显式加 `--full` 或 `--raw`;推荐交叉验证命令仍包含 `debug health`、`debug dispatch <providerId> host.ssh --wait-ms 15000`、`trans <providerId> argv true`、`artifact-registry health --provider-id <providerId>`、`microservice health k3sctl-adapter`、`microservice health code-queue` 和 `codex tasks --view supervisor --limit 20`。
|
||||
- `platform-db postgres plan|status|export-secrets|apply --config config/platform-db/postgres-pk01.yaml` 管理 YAML 声明的 PK01 host-native PostgreSQL。`plan` 和 `status` 只读采集远端 host、PostgreSQL、TLS、DNS alias 和 Secret 形态;`export-secrets --confirm` 只按 YAML 物化本地 Secret source/export 文件,不触碰远端 PostgreSQL;`apply --confirm` 默认创建本地异步 job,`apply --confirm --wait` 用远端 root-owned job 收敛 systemd PostgreSQL、TLS、`pg_hba`、role/database、Secret export 和备份 timer。输出不得打印密码或完整 `DATABASE_URL`。跨节点消费者使用 YAML 中的 `connectionHost` 直连 PK01 公网 endpoint,DNS alias 不作为 `sslmode=require` 切库 blocker,PK01 规则见 `docs/reference/pk01.md`。
|
||||
- `trans <route> [operation args...]` / `tran <route> [operation args...]` 通过 backend-core 内网 WebSocket broker 和 provider-gateway 的 Host SSH / WSL SSH 维护桥连接目标节点;`route` 基础形态是 provider id,例如 `D601` 或 `G14`,也可以扩展为纯定位路径 `provider:plane[:namespace:resource[:container]]`,例如 `D601:win`、`D601:win/c/test`、`G14:k3s`、`D601:k3s` 或 `G14:k3s:<namespace>:<workload>`。WSL provider 的 Windows plane 固定使用 `win`,不得使用 `win32`;Windows operation 必须显式区分:`ps` 执行 Windows PowerShell heredoc 或一行 PowerShell 命令,`cmd` 执行 cmd.exe/batch,`skills` 发现 Windows skill 目录。需要 Windows cwd 时用 `trans D601:win/c/test ps` 或 `trans D601:win/c/test cmd cd`,CLI 自动设置 UTF-8/Python 编码默认值;`cmd` 额外设置 `chcp 65001`。非交互远端命令优先使用 `trans <providerId> argv ...`;需要 POSIX shell 脚本、管道、变量或循环时优先使用 quoted heredoc 单步传输,例如 `trans G14 script <<'SCRIPT'`、`trans G14:k3s script <<'SCRIPT'` 或 `trans G14:k3s:<namespace>:<workload> script <<'SCRIPT'`,把脚本走 stdin。`script` 只表示 host/k3s POSIX shell,不表示 Windows PowerShell;Windows PowerShell 必须写 `trans <provider>:win ps <<'PS'`。`script -- '<单个字符串>'` 是无需 stdin 的远端 POSIX shell one-liner,例如 `trans G14:/root/hwlab script -- 'cd /root/hwlab && git status --short --branch'`;`script -- <多个 argv>` 才是 direct argv,适合 `trans D601:/path script -- sed -n '1,20p' file` 这类带短横线的单进程命令。顶层 remote option parser 必须保留命令已经开始后的 `--`,不得把它吞成全局选项结束符。需要远端改文本文件时默认优先使用 `<route> apply-patch < patch.diff`;需要可靠传输非文本或整文件时使用 `<route> upload <local-file> <remote-file>` 和 `<route> download <remote-file> <local-file>`,CLI 会按字节数与 SHA-256 自动校验并在 provider-gateway stdin/argv 限制下切换客户端分块策略;需要旧 helper 时显式使用 `<provider>:k3s:<namespace>:<workload> apply-patch-v1` 或 `<providerId> apply-patch-v1`。ssh-like 命令遇到 timeout/kex/255 类失败时,CLI 会在 stderr 追加一行 `UNIDESK_SSH_HINT` JSON,提示 stdin script/argv 重试和 provider triage 交叉验证。
|
||||
- `trans <route> [operation args...]` / `tran <route> [operation args...]` 通过 backend-core 内网 WebSocket broker 和 provider-gateway 的 Host SSH / WSL SSH 维护桥连接目标节点;`route` 基础形态是 provider id,例如 `D601` 或 `G14`,也可以扩展为纯定位路径 `provider:plane[:namespace:resource[:container]]`,例如 `D601:win`、`D601:win/c/test`、`G14:k3s`、`D601:k3s` 或 `G14:k3s:<namespace>:<workload>`。WSL provider 的 Windows plane 固定使用 `win`,不得使用 `win32`;Windows operation 必须显式区分:`ps` 执行 Windows PowerShell heredoc 或一行 PowerShell 命令,`cmd` 执行 cmd.exe/batch,`skills` 发现 Windows skill 目录。需要 Windows cwd 时用 `trans D601:win/c/test ps` 或 `trans D601:win/c/test cmd cd`,CLI 自动设置 UTF-8/Python 编码默认值;`cmd` 额外设置 `chcp 65001`。非交互远端命令优先使用 `trans <providerId> argv ...`;需要 POSIX shell 脚本、管道、变量或循环时必须在 operation 位置显式写 `sh` 或 `bash`,例如 `trans G14 sh <<'SH'`、`trans G14:k3s sh <<'SH'`、`trans G14:k3s:<namespace>:<workload> sh <<'SH'` 或 `trans D601:/workspace bash <<'BASH'`。`sh` 明确表示目标 `/bin/sh`,`bash` 明确表示目标 Bash;`script` 和 `shell` operation 已移除并会失败,避免隐藏 shell 方言。Windows PowerShell 必须写 `trans <provider>:win ps <<'PS'`。一行远端 shell 逻辑使用 `sh -- '<单个字符串>'` 或 `bash -- '<单个字符串>'`;顶层 remote option parser 必须保留命令已经开始后的 `--`,不得把它吞成全局选项结束符。需要远端改文本文件时默认优先使用 `<route> apply-patch < patch.diff`;需要可靠传输非文本或整文件时使用 `<route> upload <local-file> <remote-file>` 和 `<route> download <remote-file> <local-file>`,CLI 会按字节数与 SHA-256 自动校验并在 provider-gateway stdin/argv 限制下切换客户端分块策略;需要旧 helper 时显式使用 `<provider>:k3s:<namespace>:<workload> apply-patch-v1` 或 `<providerId> apply-patch-v1`。ssh-like 命令遇到 timeout/kex/255 类失败时,CLI 会在 stderr 追加一行 `UNIDESK_SSH_HINT` JSON,提示改用 `sh`/`bash` stdin heredoc、argv 或 provider triage 交叉验证。
|
||||
- `trans <route> apply-patch < patch.diff` 是默认推荐的远端 patch 入口:本地 TypeScript line-based engine 解析和计算新文件内容,远端 route 只负责读写文件;支持 host workspace、k3s pod workspace、Windows workspace route(例如 `D601:win/c/test`)和 frontend transport,并优先处理长中文/Unicode、低上下文插入、重复块 `@@` 定位等旧 helper 容易失败的场景。`apply-patch` 输出按 Codex 标准文本口径,不套 UniDesk JSON 限制:成功 stdout 为 `Success. Updated the following files:`,失败 stdout 为空、stderr 写失败原因;多文件补丁中途失败时,stderr 只列出第一个失败前已成功执行的 hunk 和失败 hunk,随后按 Codex 语义停止,不继续尝试后续 hunk。v2 兼容常见 MiniMax/MXCX 非标准补丁输入,例如重复 nested `*** Begin Patch` / `*** End Patch` envelope、unified-diff hunk header、Add/Delete 误加 `@@`、Update context 漏掉前导空格,并在 stderr 给出 canonical 写法 hint;parser 或上下文失败时仍坚持唯一 v2 引擎,只提示修正 patch 文本或 hunk context,不自动重试或切换到 `apply-patch-v1`;大块/函数替换因上下文过期失败时,正确动作是重新读取当前目标块、缩小或拆分 Update File hunk 后继续用 `apply-patch`,不得改走 `download`/`upload`、远端 Python/Perl/sed heredoc 或整文件重写。Windows route 复用同一套 v2 核心算法,只把底层读写替换成 PowerShell 文件系统接口;`trans <providerId> apply-patch-v1 [tool args...] < patch.diff` 保留为显式 legacy 入口,直接调用远端注入的 `apply_patch` sh/perl helper;默认 `apply-patch` 不把 v1 当 fallback。
|
||||
- GitHub issue/PR 正文局部修补必须优先走 `trans gh:/owner/repo/issue/<number> apply-patch` 或 `trans gh:/owner/repo/pr/<number> apply-patch`,而不是人工优先整篇 `gh issue update --mode replace` 或底层 `gh issue patch`。`gh:/` 路由把 issue/PR body 暴露为一楼虚拟文件 `body.md`;route 未显式写 `/1` 时默认一楼正文,`apply_patch` 与 `patch-apply` 只作为兼容别名。典型补丁写法是 `*** Update File: body.md`;普通小补丁在已用 `trans gh:/... cat|rg|ls` 确认上下文后可以直接 apply,`--dry-run` 只作为高风险大段修改、关闭前验收文案或并发敏感正文的可选预览。写回仍通过 UniDesk `gh issue/pr update` guard 和 REST API,不使用原生 `gh`、手写 REST 或整篇 shell 拼接正文。单条 issue comment 的局部修补当前仍使用 `bun scripts/cli.ts gh issue comment patch <commentId> --body-patch-stdin`,直到评论楼层 route 成为稳定入口。
|
||||
- `apply-patch` v2 每次结束都会在 stderr 追加一行 `UNIDESK_APPLY_PATCH_TIMING {json}`,字段包含 `durationMs`、`patchBytes`、`fileCount`、`hunkCount`、`changedCount`、`remoteOperationCount`、`remoteOperationCounts`、`remoteElapsedMs`、`remoteFailureCount`、`providerId`、`route` 和 `transport`(可得时)。普通 POSIX host/k3s 和 Windows workspace 远端的多文件 `Update File` patch 会优先合并成 bulk read/write,避免每个文件单独 stat/read/write 的 SSH 往返;Add/Delete/Move 等复杂 patch 保持原有逐步语义。timing 摘要只用于定位慢在 patch 解析、远端 stat/read/write 或 bulk read/write、provider session 还是传输层,不能替代 Codex 标准 stdout/stderr 成功失败文本,也不是门禁或自动判断。
|
||||
- `trans <providerId> py [script-args...] < script.py` 把本地 stdin 落到远端临时 `.py` 文件后再以 `python3 -u` 执行并自动清理,避免再手写 `'python3 -'`、heredoc 或多层引号;`script-args` 会按 argv 安全透传给远端脚本。
|
||||
- `trans <providerId> skills [--scope all|wsl|windows] [--limit N]` 发现目标节点上的 WSL/Linux skill 根目录;当 provider 是 WSL 时同一次调用还会扫描 Windows 用户目录下的 `.agents/skills` 与 `.codex/skills`。
|
||||
- `trans <providerId>:k3s[:namespace:workload[:container]] <operation> ...` 是原生 k3s 结构化 route 入口,route 只定位控制面或 workload,`kubectl`、`logs`、`exec`、`script`、`apply-patch`、旧 `apply-patch-v1` fallback 和普通容器命令作为 operation 放在 route 之后;CLI 固定注入 `KUBECONFIG=/etc/rancher/k3s/k3s.yaml` 并把 kubectl、workload exec、logs 和 pod workspace 读写参数组装成 argv,避免在 Host SSH、bash、kubectl exec 和容器 shell 之间反复手写多层引号;D601 与 G14 都有 provider-specific guard,分别校验 `d601` 和 G14 k3s 节点身份。
|
||||
- Code Queue runner 镜像必须在 PATH 上提供 `/usr/local/bin/tran`。runner 内的 `tran` 检测到 `CODE_QUEUE_*` 或 `KUBERNETES_SERVICE_HOST` 后,默认执行 `bun /root/unidesk/scripts/cli.ts --main-server-ip <public-frontend> ssh ...`,其中 `<public-frontend>` 优先来自 `UNIDESK_MAIN_SERVER_IP` / `UNIDESK_MAIN_SERVER_HOST` / `CODE_QUEUE_DEV_CONTAINER_MASTER_HOST`。runner remote frontend HTTP 客户端默认使用 `curl` 后端,降低 Bun 在部分 runner 内读取非 SSH HTTP response body 时触发 native crash 的风险;显式 `UNIDESK_REMOTE_HTTP_CLIENT=fetch` 可用于诊断。runner 内跨 D601/G14 的分布式访问应优先使用结构化 route/operation,例如 `tran D601 argv ...`、`tran G14 argv ...`、`tran D601:k3s kubectl ...`、`tran D601:k3s:<namespace>:<workload> argv ...`、`tran G14:/absolute/workspace apply-patch ...` 和 `tran <route> upload|download ...`;`apply-patch`、`upload`、`download`、`script`、`py` 和旧 `apply-patch-v1` fallback 经 frontend `/ws/ssh` 通道执行,stdout/stderr 也必须完整直通,不得退回 `/api/dispatch` task JSON。非 UniDesk admin runner 可通过 `UNIDESK_SSH_CLIENT_TOKEN` 使用 scoped `/ws/ssh` client token,frontend 侧用 `UNIDESK_SSH_CLIENT_ROUTE_ALLOWLIST` 约束允许 route;该 token 只授予 ssh passthrough,不下发 provider token、主 server SSH key 或完整 frontend 登录态。
|
||||
- `trans <providerId>:k3s[:namespace:workload[:container]] <operation> ...` 是原生 k3s 结构化 route 入口,route 只定位控制面或 workload,`kubectl`、`logs`、`exec`、`sh`、`bash`、`apply-patch`、旧 `apply-patch-v1` fallback 和普通容器命令作为 operation 放在 route 之后;CLI 固定注入 `KUBECONFIG=/etc/rancher/k3s/k3s.yaml` 并把 kubectl、workload exec、logs 和 pod workspace 读写参数组装成 argv,避免在 Host SSH、bash、kubectl exec 和容器 shell 之间反复手写多层引号;D601 与 G14 都有 provider-specific guard,分别校验 `d601` 和 G14 k3s 节点身份。
|
||||
- Code Queue runner 镜像必须在 PATH 上提供 `/usr/local/bin/tran`。runner 内的 `tran` 检测到 `CODE_QUEUE_*` 或 `KUBERNETES_SERVICE_HOST` 后,默认执行 `bun /root/unidesk/scripts/cli.ts --main-server-ip <public-frontend> ssh ...`,其中 `<public-frontend>` 优先来自 `UNIDESK_MAIN_SERVER_IP` / `UNIDESK_MAIN_SERVER_HOST` / `CODE_QUEUE_DEV_CONTAINER_MASTER_HOST`。runner remote frontend HTTP 客户端默认使用 `curl` 后端,降低 Bun 在部分 runner 内读取非 SSH HTTP response body 时触发 native crash 的风险;显式 `UNIDESK_REMOTE_HTTP_CLIENT=fetch` 可用于诊断。runner 内跨 D601/G14 的分布式访问应优先使用结构化 route/operation,例如 `tran D601 argv ...`、`tran G14 argv ...`、`tran D601:k3s kubectl ...`、`tran D601:k3s:<namespace>:<workload> argv ...`、`tran G14:/absolute/workspace apply-patch ...` 和 `tran <route> upload|download ...`;`apply-patch`、`upload`、`download`、`sh`、`bash`、`py` 和旧 `apply-patch-v1` fallback 经 frontend `/ws/ssh` 通道执行,stdout/stderr 也必须完整直通,不得退回 `/api/dispatch` task JSON。非 UniDesk admin runner 可通过 `UNIDESK_SSH_CLIENT_TOKEN` 使用 scoped `/ws/ssh` client token,frontend 侧用 `UNIDESK_SSH_CLIENT_ROUTE_ALLOWLIST` 约束允许 route;该 token 只授予 ssh passthrough,不下发 provider token、主 server SSH key 或完整 frontend 登录态。
|
||||
- `microservice list/status/health/diagnostics/tunnel-self-test/proxy` 通过 backend-core 内网 API 管理挂载在计算节点 Docker 或 k3s 控制面中的用户服务(底层命令名仍为 microservice);`health`、`status` 和 `diagnostics` 默认返回 compact summary、body 字节数和 `--full|--raw` 展开命令,只有小 body 或无法抽取 summary 时才带有界 body preview,避免 Code Queue/k3s 诊断一次性输出爆炸;`tunnel-self-test` 和 `proxy` 会走真实 backend-core -> provider-gateway 或 k3sctl-adapter -> 节点服务链路。`microservice health code-queue` 使用 commander-safe 专用摘要,必须保留 ok/status、service id、running count、queue count、heartbeat freshness/risk、split-brain/live/degraded 解释和 raw drill-down 命令;需要完整健康 JSON 时显式加 `--raw` 或 `--full`,等价深挖路径是 `microservice proxy code-queue /health --raw --full`。`proxy` 支持受控 JSON 请求体并对超大响应 body 默认输出有界预览,规则见 `docs/reference/microservices.md`。
|
||||
- `decision upload/list/show/health` 通过 backend-core 用户服务代理访问 D601 k3s Decision Center,用于上传会议记录/决议 Markdown、列出权威记录、查看详情和健康检查;`decision list` 默认只返回摘要并省略完整 Markdown body,需要排查大正文时显式加 `--include-body`。正式文书字段通过 records 模型一等字段返回和查询:`--doc-no DC-...`、`--doc-type DCSN|GOAL|PLAN|RPRT|ACTN|ISSU|RETR|RQST|RESP|MINS`、`--doc-priority P0|P1|P2|P3`、`--year YYYY`、`--signer`、`--issued-at`、`--effective-scope`、`--supersedes`、`--superseded-by`;`show` 和 `requirement update` 可使用 `id` 或 `docNo`。`decision requirement list/create/upsert/update/show` 在同一 records 模型上管理 `goal|decision|blocker|debt|experiment` 需求记录,`docNo` 唯一,未传 `--doc-no` 但提供 `--doc-type/--doc-priority/--year` 时由服务分配下一个序号。它们不得直连 D601 Service、NodePort 或 provider-gateway 业务 HTTP。
|
||||
- `decision diary import <markdown-file>` 将带 `# YYYY年M月D日`、`# YYYY-MM-DD` 或 `# YYYY/M/D` 标题的工作日志拆成每天一篇 Markdown 日记,按 `YYYY-MM/YYYY-MM-DD.md` 虚拟路径写入 Decision Center PostgreSQL;`decision diary list/history` 默认只返回摘要,需要完整 Markdown 时显式加 `--include-body`;`decision diary show <YYYY-MM-DD|id> [--source-file path]` 查看单日正文,`--source-file` 用于同一天存在多个导入来源时精确选择;`decision diary edit|upsert <YYYY-MM-DD|id> --body-file <path> [--title text] [--source-file path] [--tag tag]` 通过 `PUT /api/diary/entries/:idOrDate` 创建当天或历史条目并编辑既有条目。
|
||||
@@ -261,35 +261,35 @@ exec /root/unidesk/scripts/trans "$@"
|
||||
|
||||
主 server 上的人工/Codex 分布式敏捷操作必须直接写 `trans ...`,不要在 Codex 工具调用里退回完整 `bun scripts/cli.ts ssh ...` 前缀。例如 `trans D601:/home/ubuntu/workspace/hwlab-dev git status --short --branch`、`trans D601:k3s kubectl get pods -n hwlab-dev` 或 `trans D601:k3s:hwlab-dev:hwlab-cloud-web exec --cwd /tmp -- pwd`。`tran` 是历史兼容 wrapper 和 runner 固化入口;新写长期参考、AGENTS 索引和 CLI help 时优先写 `trans ...`。
|
||||
|
||||
`trans` 同样遵守 route/operation 解析器;route 后面的第一个 token 不是原生 ssh 命令字符串。不要写 `trans G14:/root/hwlab sh -lc '...'`,因为 `sh` 会被解析为 stdin script helper 的别名,`-lc` 会变成不受支持的 script 选项。带变量展开、管道、重定向或多条命令的远端逻辑,默认使用 `trans G14:/root/hwlab script <<'SCRIPT'`;默认 `script` 走目标节点 `/bin/sh`,并继承 provider-gateway/G14 已长期化的 proxy 环境。需要临时单步执行一行远端 shell 逻辑、且不想先创建脚本文件或 heredoc 时,优先使用 `trans G14:/root/hwlab script -- 'sed -n "1,20p" a && sed -n "1,20p" b'`,CLI 会把单个字符串放进目标节点的 `sh -c`,第二个 `sed`、管道和重定向都会留在远端;等价 `shell '<command>'` 仍保留为显式 shell operation。`script` 和 `shell` helper 会在用户 shell 文本前注入一个极小的 POSIX 兼容 `printf` wrapper,使 `printf "--- section ---\n"` 这类高频排障分隔标题在 dash/sh 与 bash 下行为一致;direct argv 形态不注入该 wrapper。`script --` 后跟多个 token 时保持 direct argv,例如 `trans G14:/root/hwlab script -- sed -n '1,20p' AGENTS.md`。只有脚本确实使用 `pipefail`、数组、`[[ ... ]]` 等 bash 专有语义时才加 `--shell bash`,不能把 `--shell bash` 当作 proxy 修复手段。单进程命令才直接写成 argv,例如 `trans G14:/root/hwlab git status --short --branch`。遇到分布式开发摩擦时,优先补强 `trans`/`tran` 的 route/operation、stdin helper 或目标节点环境,并把稳定解法写回长期参考文档,不要退回多层 shell 字符串拼接。
|
||||
`trans` 同样遵守 route/operation 解析器;route 后面的第一个 token 不是原生 ssh 命令字符串。带变量展开、管道、重定向或多条命令的远端逻辑,默认使用 `trans G14:/root/hwlab sh <<'SH'`;`sh` 走目标节点 `/bin/sh`,并继承 provider-gateway/G14 已长期化的 proxy 环境。需要 Bash 专有语法,例如 `set -o pipefail`、数组、`[[ ... ]]` 或 `${var:0:8}` 子串展开时,必须把 operation 写成 `bash`,例如 `trans D601:/home/ubuntu/workspace/hwlab-v03 bash <<'BASH'`。需要临时单步执行一行远端 shell 逻辑、且不想先创建脚本文件或 heredoc 时,使用 `trans G14:/root/hwlab sh -- 'sed -n "1,20p" a && sed -n "1,20p" b'` 或 `bash -- '<bash command>'`,CLI 会把单个字符串放进目标节点对应 shell 的 `-c`,第二个 `sed`、管道和重定向都会留在远端;`sh --` 与 `bash --` 只接受一个 shell command 字符串,多 argv 单进程命令必须走 `argv`、`exec` 或已知直接子命令。`script` 和 `shell` operation 已移除并会失败;不得用 `--shell bash` 或旧名绕过显式 shell 选择。`sh` 和 `bash` helper 会在用户 shell 文本前注入一个极小的 POSIX 兼容 `printf` wrapper,使 `printf "--- section ---\n"` 这类高频排障分隔标题在 dash/sh 与 bash 下行为一致;direct argv 形态不注入该 wrapper。单进程命令才直接写成 argv,例如 `trans G14:/root/hwlab git status --short --branch`。遇到分布式开发摩擦时,优先补强 `trans`/`tran` 的 route/operation、stdin helper 或目标节点环境,并把稳定解法写回长期参考文档,不要退回多层 shell 字符串拼接。
|
||||
|
||||
|
||||
### Standard Workspace-Prefixed Passthrough
|
||||
|
||||
- 长期参考、AGENTS 索引、CLI help、Codex 任务脚本、CI/CD 排障和人工远端操作必须统一把已知的远端 workspace 写在 route 的第一个 token,而不是塞进 `cd` 串。`route` 段只表达分布式定位,operation 段才执行命令;workspace 路径是定位信息,不是命令。
|
||||
- 标准形态是 `trans <provider>:/absolute/workspace <operation> [args...]`:例如 `trans G14:/root/hwlab git status --short --branch`、`trans G14:/root/hwlab-v02 script -- 'git fetch origin v0.2 && git pull --ff-only origin v0.2'`、`trans D601:/home/ubuntu/workspace/unidesk-dev script <<'SCRIPT'`、`trans G14:/root/hwlab apply-patch < patch.diff` 和 `trans G14:/root/hwlab glob --root . --pattern 'web/hwlab-cloud-web/*.ts' --contains session-tabs`。
|
||||
- 反面形态必须删除或迁移:`trans G14 script -- 'cd /root/hwlab && git status --short --branch'`、`trans G14 script <<'SCRIPT' cd /root/hwlab-v02 git fetch origin v0.2 SCRIPT`、`tran G14 script -- 'cd /home/ubuntu/workspace/unidesk-dev && ...'`、`bun scripts/cli.ts ssh G14 -- 'cd /root/hwlab && ...'`。这些写法把已知 workspace 写进 command 字符串,破坏 route/operation 分离,引入本地 shell 二次解析、远端 cwd 漂移和并行 worktree 切换摩擦。
|
||||
- 标准形态是 `trans <provider>:/absolute/workspace <operation> [args...]`:例如 `trans G14:/root/hwlab git status --short --branch`、`trans G14:/root/hwlab-v02 sh -- 'git fetch origin v0.2 && git pull --ff-only origin v0.2'`、`trans D601:/home/ubuntu/workspace/unidesk-dev sh <<'SH'`、`trans D601:/home/ubuntu/workspace/hwlab-v03 bash <<'BASH'`、`trans G14:/root/hwlab apply-patch < patch.diff` 和 `trans G14:/root/hwlab glob --root . --pattern 'web/hwlab-cloud-web/*.ts' --contains session-tabs`。
|
||||
- 反面形态必须删除或迁移:`trans G14 sh -- 'cd /root/hwlab && git status --short --branch'`、`trans G14 sh <<'SH' cd /root/hwlab-v02 git fetch origin v0.2 SH`、`tran G14 sh -- 'cd /home/ubuntu/workspace/unidesk-dev && ...'`、`trans G14 script -- '...'`、`trans G14 shell '...'`、`bun scripts/cli.ts ssh G14 -- 'cd /root/hwlab && ...'`。这些写法把已知 workspace 写进 command 字符串,破坏 route/operation 分离,引入本地 shell 二次解析、远端 cwd 漂移和并行 worktree 切换摩擦,或继续使用已移除的模糊 shell 旧名。
|
||||
- 例外只限于一次性探测、临时 heredoc 草稿或旧文档复用;任何被复用第二次的 `cd <workspace> && ...` 都必须重写成 `trans <provider>:/absolute/workspace` 形式。
|
||||
- 当远端存在多个并行 workspace(例如 `G14:/root/hwlab` 与 `G14:/root/hwlab-v02`)时,route 必须显式带 workspace,CLI 的 `pwd` 输出、后续 `apply-patch` 的相对路径和 `script` 的 cwd 全部跟随该 workspace;切换 workspace 必须切换 route,不允许在同一次 `trans` 链里再 `cd`。
|
||||
- 当远端存在多个并行 workspace(例如 `G14:/root/hwlab` 与 `G14:/root/hwlab-v02`)时,route 必须显式带 workspace,CLI 的 `pwd` 输出、后续 `apply-patch` 的相对路径和 `sh`/`bash` 的 cwd 全部跟随该 workspace;切换 workspace 必须切换 route,不允许在同一次 `trans` 链里再 `cd`。
|
||||
- 本规则覆盖所有 host workspace 形态,包括 `G14:/root/hwlab`、`G14:/root/hwlab-v02`、`G14:/root/agentrun-v01`、`D601:/home/ubuntu/workspace/unidesk-dev`、`D601:/home/ubuntu/workspace/hwlab-dev`;provider-gateway 侧已经把它们注册为 host workspace route。
|
||||
- 与 `k3s` route 的分工不变:定位控制面继续写 `trans G14:k3s`、定位 workload/container 继续写 `trans G14:k3s:<namespace>:<workload>[:<container>]`;pod/container 内 cwd 用 operation 参数 `--cwd /path`,或在已经明确选出 container 后才使用 `/path` route 后缀表达文件系统位置。host workspace 路径里的 `cd` 才需要被替换,控制面或 pod 内的多层 shell 不在本规则的清理范围。
|
||||
|
||||
非交互 `ssh`/`trans`/`tran` 不是登录 shell,不能依赖 `.bashrc`、`.profile` 或交互 alias。
|
||||
CLI 会在 Host route、workspace route、k3s 控制面脚本和 pod route 的 shell/script helper
|
||||
CLI 会在 Host route、workspace route、k3s 控制面脚本和 pod route 的 `sh`/`bash` helper
|
||||
前统一注入用户级工具 PATH:`$HOME/.bun/bin`、`$HOME/.local/bin`、`$HOME/bin`
|
||||
和 `/root/.bun/bin`。因此 G14 这类节点只要已经安装了 `/root/.bun/bin/bun`,
|
||||
`trans G14:/root/hwlab-v02 script -- 'bun --version'` 应该直接可用,
|
||||
`trans G14:/root/hwlab-v02 sh -- 'bun --version'` 应该直接可用,
|
||||
不需要在任务里硬写绝对路径。direct argv 不经过 shell 初始化;如果某个 direct argv 工具找不到,
|
||||
优先改用 `script -- '<command>'` 或补强 CLI 的 argv PATH 处理,
|
||||
优先改用 `sh -- '<command>'` / `bash -- '<command>'` 或补强 CLI 的 argv PATH 处理,
|
||||
不要在业务脚本里长期散落绝对路径 workaround。
|
||||
|
||||
本地 shell 运算符不是 `trans` 可以拦截的内容。`trans G14:/root/hwlab sed -n '1,20p' AGENTS.md && sed -n '1,20p' docs/reference/g14.md` 会先由 master server 的本地 shell 拆成两个命令,只有第一个 `sed` 进入 G14,第二个 `sed` 会在 master server 当前目录执行。需要把两个命令都放到目标节点时,必须写成 `trans G14:/root/hwlab script -- 'sed -n "1,20p" AGENTS.md && sed -n "1,20p" docs/reference/g14.md'`,或者用 `trans G14:/root/hwlab script <<'SCRIPT'` 把多行脚本送到远端。
|
||||
本地 shell 运算符不是 `trans` 可以拦截的内容。`trans G14:/root/hwlab sed -n '1,20p' AGENTS.md && sed -n '1,20p' docs/reference/g14.md` 会先由 master server 的本地 shell 拆成两个命令,只有第一个 `sed` 进入 G14,第二个 `sed` 会在 master server 当前目录执行。需要把两个命令都放到目标节点时,必须写成 `trans G14:/root/hwlab sh -- 'sed -n "1,20p" AGENTS.md && sed -n "1,20p" docs/reference/g14.md'`,或者用 `trans G14:/root/hwlab sh <<'SH'` 把多行脚本送到远端;需要 Bash 专有语法时把 operation 改成 `bash`。
|
||||
|
||||
`trans`/`tran` 不做本地 provider/plane 串行锁;本地目录锁不是 G14 原生 k3s/Tekton/GitOps 的业务协调机制,stale lock 会阻塞所有后续短查询。以后不要在 wrapper 里恢复本地锁。业务并发、发布互斥和 rollout 协调必须交给 k8s/Tekton/Argo/Lease 等原生运行面机制;若 provider session allocator 需要限流,应在服务端实现带 TTL 的队列或 lease,而不是在客户端加目录锁。
|
||||
|
||||
非交互 `trans`/`tran`/`ssh` 有最外层运行时硬超时,默认和最大值都是 60 秒;`UNIDESK_TRAN_RUNTIME_TIMEOUT_SECONDS`、`UNIDESK_TRAN_RUNTIME_TIMEOUT_MS` 或 `UNIDESK_SSH_RUNTIME_TIMEOUT_MS` 只能把超时调小,不能调大超过 60 秒。到点后 wrapper、backend-core broker 或 frontend websocket 路径会主动断开并在 stderr 输出 `UNIDESK_TRAN_TIMEOUT_HINT` 或 `UNIDESK_SSH_RUNTIME_TIMEOUT`,提示改用短查询加轮询。长时间 CI/CD、Tekton/Argo 观察、trace/result、日志 tail、构建下载和硬件任务都必须按 submit-and-poll/短查询语义拆成多次 `trans` 调用;不得让单个 `trans` 挂着等待最终完成。本规则的跟踪 issue 是 [pikasTech/unidesk#187](https://github.com/pikasTech/unidesk/issues/187)。
|
||||
|
||||
长任务的标准形态是:用一次短 `trans <route> script` 启动目标侧 job、后台进程、PipelineRun 或受控命令,并把 job id、日志路径、状态文件或 Kubernetes 对象名写到目标侧;后续用多次短 `trans` 查询 `status`、`logs --tail`、`kubectl get/describe`、trace result 或工具自带 job-status。不要为了观察 Docker build、镜像 push、Keil 下载、串口抓取或 Code Agent turn,把 `trans` 一直挂到结束;超过 60 秒的断开只说明调用方式需要改成轮询,不应立即归因成 provider session、CI/CD 或硬件失败。
|
||||
长任务的标准形态是:用一次短 `trans <route> sh` 或 `trans <route> bash` 启动目标侧 job、后台进程、PipelineRun 或受控命令,并把 job id、日志路径、状态文件或 Kubernetes 对象名写到目标侧;后续用多次短 `trans` 查询 `status`、`logs --tail`、`kubectl get/describe`、trace result 或工具自带 job-status。不要为了观察 Docker build、镜像 push、Keil 下载、串口抓取或 Code Agent turn,把 `trans` 一直挂到结束;超过 60 秒的断开只说明调用方式需要改成轮询,不应立即归因成 provider session、CI/CD 或硬件失败。
|
||||
|
||||
HWLAB `hwlab-cli client agent` 端到端验证经 UniDesk `trans`/`tran`/`ssh` 进入 G14 时,也按短连接 submit-and-poll 执行:在目标 workspace 和锁定 runtime env 下先 `agent send` 提交 turn,避免在可能超过 60 秒的路径上使用 `--wait` 长挂;随后多次短查询 `agent result <traceId>`、`agent trace <traceId> --render web` 或 `agent inspect --trace-id <traceId>` 取证。关闭 context-loss、AgentRun 复用或多轮会话类 issue 时,证据至少应记录 `conversationId`、`sessionId`、`threadId`、`traceId`、`runId`、`commandId` 和关键 event label。需要验证“第二轮继承第一轮上下文”时,显式传入同一 conversation/session/thread 标识,不能依赖旧 Cloud Web workspace 历史或人工印象;`--no-workspace` 这类绕过恢复的实验只可作为定位证据,不能替代默认入口验收。
|
||||
|
||||
@@ -297,7 +297,7 @@ HWLAB Code Agent provider profile 的 `config.toml`、完整 Codex `auth.json`
|
||||
|
||||
HWLAB Cloud Web Workbench 或 Code Agent 装配类 issue 的 CLI 验证必须贴近 Web 路径:优先使用会调用同一 Cloud API/Web dispatcher 的正式 `hwlab-cli client agent` 或等价 UniDesk 高层 CLI,再从 trace/inspect/result 中确认 runner job、AgentRun runtime assembly、`transientEnv`、`runId` 和 `commandId`。当前 HWLAB v0.2 资源装配需求以 UniDesk OA 的 [Runtime装配](../../project-management/PJ2026-01/specs/PJ2026-010202-runtime-assembly.md) 和 [HWLAB接入](../../project-management/PJ2026-01/specs/PJ2026-010205-hwlab-dispatch.md) 为权威:`ResourceBundleRef.kind="gitbundle"`,通过 `bundles[]` 装配 `tools/` 与 `.agents/skills`;不要在本 CLI 文档重复维护旧字段清单。直接调用 AgentRun manager、手写 `dispatchHwlabAgentRun()` 或临时 runner job 只可作为基础设施 canary;它不能替代 Web Workbench 原入口验收,也不能作为关闭 Web issue 的唯一证据。若缺少这种同路径 CLI,先补 CLI 可见性和 submit-and-poll 入口,再继续修复或关闭 issue。
|
||||
|
||||
`trans D518` 应表现为登录 D518 WSL 的 shell;`trans D518 hostname` 应像 `ssh D518 hostname` 一样只输出远端命令结果并返回远端 exit code。Provider ID 前的目标选择由 UniDesk 节点清单决定,`-p`、`-i`、`-l`、`-o` 等传统 ssh 传输参数由 provider-gateway 部署配置统一管理,CLI 会兼容性消费这些参数但不会覆盖节点侧维护桥配置。指挥官、CI 预检和其他非交互流程不要依赖 ssh-like 自由拼接;单进程标准写法是 `trans D601 argv true`,多行 shell 逻辑标准写法是 quoted heredoc 单步调用 `trans D601 script <<'SCRIPT'`。
|
||||
`trans D518` 应表现为登录 D518 WSL 的 shell;`trans D518 hostname` 应像 `ssh D518 hostname` 一样只输出远端命令结果并返回远端 exit code。Provider ID 前的目标选择由 UniDesk 节点清单决定,`-p`、`-i`、`-l`、`-o` 等传统 ssh 传输参数由 provider-gateway 部署配置统一管理,CLI 会兼容性消费这些参数但不会覆盖节点侧维护桥配置。指挥官、CI 预检和其他非交互流程不要依赖 ssh-like 自由拼接;单进程标准写法是 `trans D601 argv true`,多行 shell 逻辑标准写法是 quoted heredoc 单步调用 `trans D601 sh <<'SH'`。
|
||||
|
||||
UniDesk CLI/trans/tran 客户端改进本身是 master server 高频控制入口维护,可以直接在 `/root/unidesk` 轻量开发、提交并推送 `origin/master`;不要为这类客户端小改强制迁移到 D601 worktree。该例外不改变 master server 禁重型验证规则:仓库级 check、Playwright/browser smoke、镜像构建、Rust/Go 编译、Code Queue runner 实测仍必须在 D601、CI runner 或目标运行面执行,backend-core 主 server 上线受控编译例外按 `docs/reference/dev-environment.md` 单独处理。若 `trans`/`tran`/SSH 文件传输遇到 provider-gateway 单次 stdin、argv 或 stdout 限制,先在 CLI 客户端做分块、SHA-256 校验、失败可观测输出和最小真实闭环;只有 client 侧不能解决且有证据时,才改 provider-gateway。文件传输默认按 1MiB raw chunk 读写,避免 100MiB 大文件被 45KiB 小块和 base64 全量暂存拖慢;`upload`/`download` 成功 JSON 中的 `verified=true`、`verification.automatic=true`、`verification.verified=true` 和 `verification.match.{bytes,sha256}=true` 就是端到端完整性证明,调用方不需要再额外手写 `sha256sum` 比对。
|
||||
|
||||
@@ -307,11 +307,11 @@ core 只允许声明了 `host.ssh` capability 的 provider 使用 `host.ssh` dis
|
||||
|
||||
本地 broker 默认等待 provider SSH 会话打开 60000ms,以便在目标节点同时有较多 microservice.http 任务时仍能建立维护会话;需要诊断慢连接时可用 `UNIDESK_SSH_OPEN_TIMEOUT_MS=<ms>` 临时调大,但最小有效值固定为 15000ms,避免把真实离线误判为长时间阻塞。注意 open timeout 只控制“会话打开”阶段,不能绕过 60 秒最外层运行时硬超时。
|
||||
|
||||
ssh-like 远端命令如果出现 `kex_exchange_identification`、`Connection closed by remote host`、provider session timeout 或 exit code 255,CLI 会在原始 stderr 后追加一行 `UNIDESK_SSH_HINT { ... }`。该 JSON 不回显原始远端命令,只包含 `code=ssh-like-command-friction`、`trigger`、`try` 和 `triage`;`try` 固定指向 stdin script 形态,避免把一次 ssh-like 解析/握手摩擦误读成 D601 SSH 整体不可用。`ssh`/`trans`/`tran` 在失败路径识别到 tcp-pool 数据面问题时会追加 `UNIDESK_SSH_TCP_POOL_HINT { ... }`,`failureKind` 固定分为 `provider-data-channel-closed`、`provider-data-channel-missing` 和 `provider-data-pool-exhausted`;这类 hint 表示 transport/data-pool transient,幂等受控操作应先运行 `bun scripts/cli.ts debug ssh-pool <providerId>` 查看 labels,再重试原受控 CLI,不能单独定性为远端 runtime 配置失败。backend-core/provider 控制面返回结构化 `ssh.error` 时 broker 还会输出 `UNIDESK_SSH_ERROR { ... }`,供 `job status` 从旧日志或异步 job 日志中恢复 failureKind。`ssh`/`trans`/`tran` 运行时硬超时会输出 `UNIDESK_SSH_RUNTIME_TIMEOUT { ... }` 或 wrapper 层 `UNIDESK_TRAN_TIMEOUT_HINT { ... }`;这不是远端业务失败,而是调用方需要改成短查询/轮询。`ssh`/`trans`/`tran` 只有在运行耗时超过默认 10000ms 时才会在 stderr 追加一行 `UNIDESK_SSH_TIMING { ... }`,且 `level=warning`;正常短调用不输出 timing 噪声。慢成功命令也必须保留该 warning,因为它是 provider session、远端命令成本、helper bootstrap 和 `trans`/`tran`/远端 patch 性能回归的重要监控信号。warning 包含 `elapsedMs`、`elapsedSeconds`、`transport`、`invocationKind` 和 `exitCode`,提示优先排查 provider/session 延迟、远端命令自身耗时、helper bootstrap 或工具层回归。阈值可用 `UNIDESK_SSH_SLOW_WARNING_MS=<ms>` 临时调节,提示同样不回显原始远端命令。
|
||||
ssh-like 远端命令如果出现 `kex_exchange_identification`、`Connection closed by remote host`、provider session timeout 或 exit code 255,CLI 会在原始 stderr 后追加一行 `UNIDESK_SSH_HINT { ... }`。该 JSON 不回显原始远端命令,只包含 `code=ssh-like-command-friction`、`trigger`、`try` 和 `triage`;`try` 固定指向显式 `sh` stdin heredoc 形态,避免把一次 ssh-like 解析/握手摩擦误读成 D601 SSH 整体不可用。`ssh`/`trans`/`tran` 在失败路径识别到 tcp-pool 数据面问题时会追加 `UNIDESK_SSH_TCP_POOL_HINT { ... }`,`failureKind` 固定分为 `provider-data-channel-closed`、`provider-data-channel-missing` 和 `provider-data-pool-exhausted`;这类 hint 表示 transport/data-pool transient,幂等受控操作应先运行 `bun scripts/cli.ts debug ssh-pool <providerId>` 查看 labels,再重试原受控 CLI,不能单独定性为远端 runtime 配置失败。backend-core/provider 控制面返回结构化 `ssh.error` 时 broker 还会输出 `UNIDESK_SSH_ERROR { ... }`,供 `job status` 从旧日志或异步 job 日志中恢复 failureKind。`ssh`/`trans`/`tran` 运行时硬超时会输出 `UNIDESK_SSH_RUNTIME_TIMEOUT { ... }` 或 wrapper 层 `UNIDESK_TRAN_TIMEOUT_HINT { ... }`;这不是远端业务失败,而是调用方需要改成短查询/轮询。`ssh`/`trans`/`tran` 只有在运行耗时超过默认 10000ms 时才会在 stderr 追加一行 `UNIDESK_SSH_TIMING { ... }`,且 `level=warning`;正常短调用不输出 timing 噪声。慢成功命令也必须保留该 warning,因为它是 provider session、远端命令成本、helper bootstrap 和 `trans`/`tran`/远端 patch 性能回归的重要监控信号。warning 包含 `elapsedMs`、`elapsedSeconds`、`transport`、`invocationKind` 和 `exitCode`,提示优先排查 provider/session 延迟、远端命令自身耗时、helper bootstrap 或工具层回归。阈值可用 `UNIDESK_SSH_SLOW_WARNING_MS=<ms>` 临时调节,提示同样不回显原始远端命令。
|
||||
|
||||
非交互 `ssh`/`trans`/`tran` 远端命令的流式 stdout 默认有本地输出上限,避免远端日志、PowerShell JSON 或错误对象一次性输出过大导致上下文被淹没;交互登录 shell 不套该上限。超过上限时,CLI 只继续读取远端流并把完整内容写入 `/tmp/unidesk-cli-output/*.stdout.bin`,本地 stderr 追加 `UNIDESK_SSH_STDOUT_TRUNCATED { ... }`,其中包含 `thresholdBytes`、`observedBytesAtTruncation`、`dumpPath` 和 `dumpError`;stdout 本身只保留上限内的开头内容。默认上限是 256KiB,可用 `UNIDESK_SSH_STDOUT_STREAM_MAX_BYTES=<bytes>` 或 `UNIDESK_TRAN_STDOUT_STREAM_MAX_BYTES=<bytes>` 临时调整,最小 4KiB,最大 16MiB。该机制只做渐进披露和完整 dump,不替代远端命令失败判断;看到该 hint 时应优先改成 `tail`、分页或更窄的结构化查询。
|
||||
|
||||
`trans <providerId>` 透传只在当前 operation 需要 helper 时才注入 `/tmp/unidesk-ssh-tools`,普通 `argv`、`script`、`kubectl`、`logs` 和默认 `apply-patch` 等路径不得传输无关工具源码。`apply-patch-v1` 只注入 `apply_patch`;`glob` 只注入 `glob`;`skills`/`skill discover` 只注入 `skill-discover`。`apply_patch` 接受标准 `*** Begin Patch` / `*** End Patch` patch 格式,便于通过 SSH 透传编辑远端仓库文件;远端存在 `perl` 时必须走快速精确匹配路径,避免大文件 hunk 被 sh 模式匹配拖成几十秒,缺少 `perl` 时才退回 sh-only 实现。`glob` 和 `skill-discover` 需要远端 `python3`。注入工具只写 `/tmp/unidesk-ssh-tools`,不修改目标仓库。
|
||||
`trans <providerId>` 透传只在当前 operation 需要 helper 时才注入 `/tmp/unidesk-ssh-tools`,普通 `argv`、`sh`/`bash`、`kubectl`、`logs` 和默认 `apply-patch` 等路径不得传输无关工具源码。`apply-patch-v1` 只注入 `apply_patch`;`glob` 只注入 `glob`;`skills`/`skill discover` 只注入 `skill-discover`。`apply_patch` 接受标准 `*** Begin Patch` / `*** End Patch` patch 格式,便于通过 SSH 透传编辑远端仓库文件;远端存在 `perl` 时必须走快速精确匹配路径,避免大文件 hunk 被 sh 模式匹配拖成几十秒,缺少 `perl` 时才退回 sh-only 实现。`glob` 和 `skill-discover` 需要远端 `python3`。注入工具只写 `/tmp/unidesk-ssh-tools`,不修改目标仓库。
|
||||
|
||||
远端文本 patch 默认使用 `apply-patch` 的 v2 引擎:它不把 hunk 解析交给远端 shell/perl helper,而是在本地按行序列匹配,支持长中文/Unicode 行、纯新增 hunk、低上下文插入和 `@@` 上下文定位,再把完整新内容写回远端。v2 的文件操作提交顺序按 Codex 标准 `apply_patch` 语义执行:空 patch 会失败;删除不存在的文件会失败;`Add File` 可覆盖已有文件;`Move to` 可覆盖目标文件;当大 patch 后续 hunk 不匹配时,已成功提交的前序文件操作会保留,并在错误详情中记录 `partialChanges`,调用方应基于当前文件内容继续补一个更小的 patch,而不是期待全量事务回滚。若 stderr 报 `failed to find expected lines` 且显示 partial context match,尤其是大块/函数替换,调用方必须先重读目标文件当前块,再用更少稳定上下文、`@@ <unique anchor>` 或多个小 hunk 重试;该失败不构成改用 `download`/`upload`、远端脚本整文件替换或 `apply-patch-v1` 的理由。`apply_patch` 旧 helper 默认拒绝低上下文 update hunk:空搜索/纯插入无锚点、只在插入点前有上下文而没有插入点后上下文、或同一 hunk search 在目标文件中匹配多个位置时,都会结构化失败并提示补充上下文。成功应用时每个 hunk 会在 stderr 输出 `apply_patch: hunk N matched path:line`,用于复核实际落点;只有人工确认确实需要旧 helper 行为或 `--allow-loose` 时,才显式调用 `apply-patch-v1 --allow-loose`。
|
||||
|
||||
@@ -364,29 +364,19 @@ printf 'import sys\nprint(sys.argv)\n' | trans D601 py foo '--bar=baz'
|
||||
|
||||
`trans <providerId> py` 的附加参数是脚本参数,不是 Python 解释器参数;如需 `-m`、`-X` 或多条 shell 命令,仍使用原始远端命令入口。为了保证 CLI 输出及时可见,helper 固定采用“临时文件 + `python3 -u`”模式;provider 命令模式不分配 TTY,因此脚本内容不应被远端回显。
|
||||
|
||||
如果远端逻辑需要 shell 特性,不要再把整段脚本作为原生 ssh-like 命令字符串传入。正式入口是
|
||||
`trans D601 script`,脚本正文从 stdin 进入;CLI 会把本地 stdin 直接送到远端
|
||||
`sh -s --`,`--shell bash` 可切换为 bash,`--` 后的内容会作为脚本参数传入。
|
||||
`script`/`shell` helper 会在用户脚本文本前注入用户级工具 PATH 和兼容前缀,
|
||||
让 `bun`、`tsx` 等用户级工具在非交互 shell 中可见,也让 `printf "--- section ---\n"`
|
||||
这类分隔标题不再因目标 `/bin/sh` 方言失败;已有 `printf '%s\n' value`、`printf -- ...`
|
||||
和 bash 的 `printf -v` 仍按原语义工作。临时单步执行优先用 quoted heredoc;
|
||||
只有命令很短、明确希望一行内完成时才用 `script -- '<command && command>'`,
|
||||
它会把单个字符串按远端 shell one-liner 执行且不等待 stdin;
|
||||
复用脚本时才用 `< script.sh` 文件重定向。`script -- <多个 argv>` 仍是 direct argv,
|
||||
不经过远端 shell,适合 `script -- sed -n '1,20p' file`。典型用法:
|
||||
如果远端逻辑需要 shell 特性,不要再把整段脚本作为原生 ssh-like 命令字符串传入。正式入口必须在 operation 位置显式声明 shell 方言:`trans D601 sh` 表示目标 `/bin/sh`,`trans D601 bash` 表示目标 Bash。脚本正文从 stdin 进入;operation 后的普通参数会作为脚本参数传入。`sh`/`bash` helper 会在用户脚本文本前注入用户级工具 PATH 和兼容前缀,让 `bun`、`tsx` 等用户级工具在非交互 shell 中可见,也让 `printf "--- section ---\n"` 这类分隔标题不再因目标 `/bin/sh` 方言失败;已有 `printf '%s\n' value`、`printf -- ...` 和 bash 的 `printf -v` 仍按原语义工作。临时单步执行优先用 quoted heredoc;只有命令很短、明确希望一行内完成时才用 `sh -- '<command && command>'` 或 `bash -- '<bash command>'`,它会把单个字符串按所选远端 shell one-liner 执行且不等待 stdin;`sh --` / `bash --` 后出现多个 argv token 会失败,单进程命令必须改用 `argv` 或直接子命令;复用脚本时才用 `< script.sh` 文件重定向。`script` 和 `shell` operation 已移除并会失败,不能用 `--shell bash` 或旧名绕过显式 shell 选择。典型用法:
|
||||
|
||||
```bash
|
||||
cat <<'SCRIPT' | trans D601 script --shell bash -- alpha
|
||||
cat <<'BASH' | trans D601 bash alpha
|
||||
set -euo pipefail
|
||||
printf 'arg=%s\n' "$1"
|
||||
hostname
|
||||
SCRIPT
|
||||
BASH
|
||||
```
|
||||
|
||||
这个入口的目标是分布式调试的“0 shell-command-string”路径:本地 shell 只负责 heredoc/stdin,UniDesk 只负责 provider 路由,远端 shell 只解释脚本正文。脚本正文里仍然要遵守 shell 语言自身的规则,但不再穿过本地 shell、远端 shell、kubectl exec 和容器 shell 的多重字符串转义。
|
||||
|
||||
`trans <providerId> shell '<command>'` 是一行远端 shell 逻辑的逃生阀,不取代 `script`。它的输入必须作为一个 quoted argv 到达 CLI,适合 `sed ... && sed ...`、`kubectl get ... | head` 或一次性环境探测;它仍然只穿过一次目标 shell,不能解决本地 shell 已经拆开的外层 `&&`、`|`、`>`。k3s 控制面同样支持 `trans G14:k3s shell 'kubectl get nodes && kubectl get pods -A'`,并默认注入 `/etc/rancher/k3s/k3s.yaml`;pod route 需要 cwd 时使用 `exec --cwd /path -- sh -c '<command>'`,例如 `trans D601:k3s:hwlab-dev:hwlab-cloud-api exec --cwd /app -- sh -c 'pwd && ls'`。
|
||||
一行远端 shell 逻辑同样必须用显式方言:POSIX 写 `trans <providerId> sh -- '<command>'`,Bash 专有语法写 `trans <providerId> bash -- '<command>'`。它的输入必须作为一个 quoted argv 到达 CLI,适合 `sed ... && sed ...`、`kubectl get ... | head` 或一次性环境探测;它仍然只穿过一次目标 shell,不能解决本地 shell 已经拆开的外层 `&&`、`|`、`>`。k3s 控制面同样支持 `trans G14:k3s sh -- 'kubectl get nodes && kubectl get pods -A'`,并默认注入 `/etc/rancher/k3s/k3s.yaml`;pod route 需要 cwd 时使用 workload route 的 `sh`/`bash` operation 或 `exec --cwd /path -- sh -c '<command>'`,例如 `trans D601:k3s:hwlab-dev:hwlab-cloud-api sh --cwd /app -- 'pwd && ls'`。
|
||||
|
||||
`trans <providerId> skills` 是远端 skill 发现入口,也可写作 `trans <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 挂载层。
|
||||
|
||||
@@ -409,11 +399,11 @@ trans D601 find /home/ubuntu --max-depth 4 --type d --icontains pika --limit 50
|
||||
trans D601 glob --root /home/ubuntu/pikapython --pattern '**/*-test.cpp' --limit 20 --sort
|
||||
```
|
||||
|
||||
`ssh` 的 route 语法是 `{provider}:{plane}[:{scope...}] {operation} [operation-args...]`。第一个 argv token 只负责定位分布式目标,不表达操作;第一个 token 后面的所有 token 才进入 operation 解析器。Host workspace route 使用 `<provider>:/absolute/workspace`,例如 `D601:/home/ubuntu/workspace/hwlab-dev`,CLI 会把该路径作为远端 cwd 传给 Host SSH 维护桥,后续 `pwd`、`git`、`script`、`apply-patch` 和旧 `apply-patch-v1` fallback 等操作仍按同一套 operation parser 执行。`<provider>:host:/absolute/workspace` 是等价长写法;workspace 必须是绝对路径,远端是否存在由维护桥实际 `cd` 失败或成功证明。
|
||||
`ssh` 的 route 语法是 `{provider}:{plane}[:{scope...}] {operation} [operation-args...]`。第一个 argv token 只负责定位分布式目标,不表达操作;第一个 token 后面的所有 token 才进入 operation 解析器。Host workspace route 使用 `<provider>:/absolute/workspace`,例如 `D601:/home/ubuntu/workspace/hwlab-dev`,CLI 会把该路径作为远端 cwd 传给 Host SSH 维护桥,后续 `pwd`、`git`、`sh`、`bash`、`apply-patch` 和旧 `apply-patch-v1` fallback 等操作仍按同一套 operation parser 执行。`<provider>:host:/absolute/workspace` 是等价长写法;workspace 必须是绝对路径,远端是否存在由维护桥实际 `cd` 失败或成功证明。
|
||||
|
||||
当前稳定 plane 包括 `win` 和 `k3s`。`win` plane 的 operation 是 Windows 操作,不是 POSIX shell 别名:`<provider>:win ps` 在 WSL provider 上启动 Windows PowerShell,stdin heredoc 会被写入临时 `.ps1` 后执行;`<provider>:win cmd` 启动 Windows host 的 `cmd.exe`,stdin heredoc 会被写入临时 `.cmd` 后执行;`<provider>:win skills` 发现 Windows skill 目录。需要 Windows 当前目录时使用 slash 路由 `<provider>:win/<drive>/<path>`,例如 `D601:win/c/test ps` 会先在 PowerShell 内 `Set-Location -LiteralPath 'C:\test'`,`D601:win/c/test cmd cd` 会先在 cmd 内执行 `cd /d "C:\test"`。`win32` 不是合法 plane,调用者必须改用 `win`。
|
||||
|
||||
`<provider>:win ps` 是 Windows PowerShell 专用入口,适合管道、变量、`Get-ChildItem`、`Start-Process`、`Test-Path` 和 Windows 路径脚本;不要用 host/k3s 的 `script` operation 表示 PowerShell。`ps` 和 `cmd` 都注入 UTF-8/Python 编码默认值;`cmd` 额外执行 `chcp 65001>nul`。典型用法:
|
||||
`<provider>:win ps` 是 Windows PowerShell 专用入口,适合管道、变量、`Get-ChildItem`、`Start-Process`、`Test-Path` 和 Windows 路径脚本;不要用 host/k3s 的 `sh`/`bash` operation 表示 PowerShell。`ps` 和 `cmd` 都注入 UTF-8/Python 编码默认值;`cmd` 额外执行 `chcp 65001>nul`。典型用法:
|
||||
|
||||
```bash
|
||||
trans D601:win ps <<'PS'
|
||||
@@ -424,11 +414,11 @@ PS
|
||||
|
||||
`<provider>:win skills [--scope agents|codex|all] [--limit N]` 是 Windows 用户 skill 发现入口,默认只读取当前 Windows 用户的 `%USERPROFILE%\.agents\skills`,输出 JSON 中包含 `roots`、`counts` 和每个 skill 的 `name`、`path`、`skillFile`、`description`。需要同时检查 `%USERPROFILE%\.codex\skills` 时显式加 `--scope all`;不要为了列 skill 手写 `cmd dir` 或宽泛扫描整个用户目录。
|
||||
|
||||
`D601:k3s` 或 `G14:k3s` 定位到对应 provider 的原生 k3s 控制面;`<provider>:k3s:<namespace>:<workload>[:container]` 定位到 namespace 下的一个默认 deployment workload;若目标是具体 Pod,标准 workload 段写成 `pod:<podid>`,例如 `D601:k3s:hwlab-v03:pod:hwlab-cloud-api-abc:hwlab-cloud-api`。`pod/<podid>` 不是合法 route 写法,因为 `:` 是分布式路由分隔符,`/` 只表示目标容器里的文件系统 cwd;如果调用者写成 `pod/<podid>/<container>`,CLI 必须在连接运行面前报错并提示改用 `pod:<podid>:<container>` 或 `--container <container>`。若目标是 Deployment,也可以显式写 `deployment:<name>` 或简写 `<name>`;容器选择写 `:<container>` 或 operation 参数 `--container <container>`。pod 内 cwd 推荐用 operation 参数 `--cwd /path`,例如 `D601:k3s:hwlab-dev:hwlab-cloud-api:hwlab-cloud-api exec --cwd /app -- pwd`。`kubectl`、`logs`、`script`、`apply-patch`、旧 `apply-patch-v1` fallback、`exec` 和普通容器命令都是 route 后面的 operation,这样路由子模块和操作子模块可以独立扩展。
|
||||
`D601:k3s` 或 `G14:k3s` 定位到对应 provider 的原生 k3s 控制面;`<provider>:k3s:<namespace>:<workload>[:container]` 定位到 namespace 下的一个默认 deployment workload;若目标是具体 Pod,标准 workload 段写成 `pod:<podid>`,例如 `D601:k3s:hwlab-v03:pod:hwlab-cloud-api-abc:hwlab-cloud-api`。`pod/<podid>` 不是合法 route 写法,因为 `:` 是分布式路由分隔符,`/` 只表示目标容器里的文件系统 cwd;如果调用者写成 `pod/<podid>/<container>`,CLI 必须在连接运行面前报错并提示改用 `pod:<podid>:<container>` 或 `--container <container>`。若目标是 Deployment,也可以显式写 `deployment:<name>` 或简写 `<name>`;容器选择写 `:<container>` 或 operation 参数 `--container <container>`。pod 内 cwd 推荐用 operation 参数 `--cwd /path`,例如 `D601:k3s:hwlab-dev:hwlab-cloud-api:hwlab-cloud-api exec --cwd /app -- pwd`。`kubectl`、`logs`、`sh`、`bash`、`apply-patch`、旧 `apply-patch-v1` fallback、`exec` 和普通容器命令都是 route 后面的 operation,这样路由子模块和操作子模块可以独立扩展。
|
||||
|
||||
`k3s` 必须出现在 route 的 plane 段里,禁止使用 `trans G14 k3s ...` 或 `trans D601 k3s ...` 这类 post-provider shorthand;正确形态是 `trans G14:k3s kubectl ...` 或 `trans D601:k3s kubectl ...`。定位和操作必须保持分离,`kubectl`、`logs`、`script`、`apply-patch`、旧 `apply-patch-v1` fallback、`exec` 等 operation 名也不得放进任何 colon route 段,包括 namespace、workload 或 container 段;新增分布式目标时按 `{provider}:{plane}:{scope}` 扩展 route,而不是在 operation args 中新增另一套定位语法。
|
||||
`k3s` 必须出现在 route 的 plane 段里,禁止使用 `trans G14 k3s ...` 或 `trans D601 k3s ...` 这类 post-provider shorthand;正确形态是 `trans G14:k3s kubectl ...` 或 `trans D601:k3s kubectl ...`。定位和操作必须保持分离,`kubectl`、`logs`、`sh`、`bash`、`apply-patch`、旧 `apply-patch-v1` fallback、`exec` 等 operation 名也不得放进任何 colon route 段,包括 namespace、workload 或 container 段;新增分布式目标时按 `{provider}:{plane}:{scope}` 扩展 route,而不是在 operation args 中新增另一套定位语法。
|
||||
|
||||
该入口解决运行面调试中最常见的多层 shell 引号问题。它不要求升级 provider-gateway,也不新增业务 API,只复用现有 Host SSH 维护桥;CLI 在本地把 Kubernetes 目标、namespace、container、log 限制、容器命令、stdin script、pod cwd `apply-patch` 读写和旧 `apply-patch-v1` fallback 组装成 kubectl argv,并固定远端 `KUBECONFIG=/etc/rancher/k3s/k3s.yaml`。`<provider>:k3s` 无后续参数时执行 native k3s guard;`<provider>:k3s kubectl ...` 接收原始 kubectl argv;`<provider>:k3s script` 执行带 native kubeconfig 的 host stdin 脚本;`<provider>:k3s:<namespace>:<workload>[:container] logs` 读取有界日志;`<provider>:k3s:<namespace>:<workload>[:container] exec ...` 和 `<provider>:k3s:<namespace>:<workload>[:container] <command> ...` 进入目标 workload/container;`<provider>:k3s:<namespace>:<workload>[:container] script` 把本地 stdin 作为 pod 内 shell 脚本执行;`<provider>:k3s:<namespace>:<workload>[:container] apply-patch --cwd /workspace` 是 pod 内文本 patch 默认入口;旧 helper 仅通过 `<provider>:k3s:<namespace>:<workload> apply-patch-v1` 显式调用。典型用法:
|
||||
该入口解决运行面调试中最常见的多层 shell 引号问题。它不要求升级 provider-gateway,也不新增业务 API,只复用现有 Host SSH 维护桥;CLI 在本地把 Kubernetes 目标、namespace、container、log 限制、容器命令、`sh`/`bash` stdin、pod cwd `apply-patch` 读写和旧 `apply-patch-v1` fallback 组装成 kubectl argv,并固定远端 `KUBECONFIG=/etc/rancher/k3s/k3s.yaml`。`<provider>:k3s` 无后续参数时执行 native k3s guard;`<provider>:k3s kubectl ...` 接收原始 kubectl argv;`<provider>:k3s sh` 或 `bash` 执行带 native kubeconfig 的 host stdin 脚本;`<provider>:k3s:<namespace>:<workload>[:container] logs` 读取有界日志;`<provider>:k3s:<namespace>:<workload>[:container] exec ...` 和 `<provider>:k3s:<namespace>:<workload>[:container] <command> ...` 进入目标 workload/container;`<provider>:k3s:<namespace>:<workload>[:container] sh` 或 `bash` 把本地 stdin 作为 pod 内 shell 脚本执行;`<provider>:k3s:<namespace>:<workload>[:container] apply-patch --cwd /workspace` 是 pod 内文本 patch 默认入口;旧 helper 仅通过 `<provider>:k3s:<namespace>:<workload> apply-patch-v1` 显式调用。典型用法:
|
||||
|
||||
```bash
|
||||
trans D601:k3s
|
||||
@@ -442,14 +432,14 @@ trans D601:win/c/test cmd cd
|
||||
trans D601:win skills --limit 20
|
||||
trans G14:k3s
|
||||
trans G14:k3s kubectl get pipelineruns -n hwlab-ci
|
||||
printf 'kubectl get deploy -n hwlab-dev\n' | trans D601:k3s script
|
||||
printf 'kubectl get deploy -n hwlab-dev\n' | trans D601:k3s sh
|
||||
trans D601:k3s:hwlab-dev:hwlab-cloud-api logs --tail 80
|
||||
trans D601:k3s:hwlab-v03:pod:hwlab-cloud-api-abc:hwlab-cloud-api script -- 'curl -fsS http://user-billing/health'
|
||||
trans D601:k3s:hwlab-v03:pod:hwlab-cloud-api-abc:hwlab-cloud-api sh -- 'curl -fsS http://user-billing/health'
|
||||
trans G14:k3s logs --namespace=devops-infra --deployment=git-mirror-http --tail=80
|
||||
trans G14:k3s logs -n agentrun-ci -l tekton.dev/pipelineRun=agentrun-v01-ci-xxxx --tail=120
|
||||
trans D601:k3s:hwlab-dev:hwlab-cloud-api node -e 'console.log(process.version)'
|
||||
trans D601:k3s:hwlab-dev:hwlab-cloud-api exec --cwd /app -- pwd
|
||||
printf 'printf "pod=%s\n" "$HOSTNAME"\n' | trans D601:k3s:hwlab-dev:hwlab-cloud-api script
|
||||
printf 'printf "pod=%s\n" "$HOSTNAME"\n' | trans D601:k3s:hwlab-dev:hwlab-cloud-api sh
|
||||
tar -C /tmp/patched-files -cf - . | trans D601:k3s:unidesk:code-queue exec --cwd /root/unidesk --stdin -- tar -xf - -C /root/unidesk
|
||||
trans D601:k3s:hwlab-dev:hwlab-cloud-api:hwlab-cloud-api apply-patch --cwd /app <<'PATCH'
|
||||
*** Begin Patch
|
||||
@@ -461,17 +451,17 @@ trans D601:k3s:hwlab-dev:hwlab-cloud-api:hwlab-cloud-api apply-patch --cwd /app
|
||||
PATCH
|
||||
```
|
||||
|
||||
`logs` operation 默认是有界读取;`--follow`/`-f` 会被拒绝,防止 CLI 长时间占用维护桥。目标 route 后面直接跟普通命令时,CLI 会把 argv 放到 `kubectl exec --` 后;显式 `exec` operation 可用于让命令边界更清晰。`exec --stdin -- <command> ...` 是 workload route 的通用 stdin 流入口,适合把 tar、patch 以外的任意字节流直接送进容器命令;operation 选项必须放在 `--` 前,容器命令从 `--` 后开始。需要 shell 语法时优先改用 `script` operation,把脚本走 stdin,而不是把 `kubectl exec ... -- sh -c ...` 放进远端命令字符串。pod 内文本热修默认使用 workspace route 加 `apply-patch`,不要求目标容器自带 `python3`、`node` 或仓库里的工具脚本;旧 `apply-patch-v1` operation 仍使用同一个 sh helper,只作为显式 legacy fallback,不用于二进制改写。
|
||||
`logs` operation 默认是有界读取;`--follow`/`-f` 会被拒绝,防止 CLI 长时间占用维护桥。目标 route 后面直接跟普通命令时,CLI 会把 argv 放到 `kubectl exec --` 后;显式 `exec` operation 可用于让命令边界更清晰。`exec --stdin -- <command> ...` 是 workload route 的通用 stdin 流入口,适合把 tar、patch 以外的任意字节流直接送进容器命令;operation 选项必须放在 `--` 前,容器命令从 `--` 后开始。需要 shell 语法时优先改用 `sh` 或 `bash` operation,把脚本走 stdin,而不是把 `kubectl exec ... -- sh -c ...` 放进远端命令字符串。pod 内文本热修默认使用 workspace route 加 `apply-patch`,不要求目标容器自带 `python3`、`node` 或仓库里的工具脚本;旧 `apply-patch-v1` operation 仍使用同一个 sh helper,只作为显式 legacy fallback,不用于二进制改写。
|
||||
|
||||
`trans <providerId> argv <command> [args...]` 是通用 argv 安全拼接入口;`exec` 是同义入口。它是非交互远端单进程命令的默认成功路径,不需要 shell 管道时直接传命令和参数,例如 `trans D601 argv true`。需要管道、重定向、变量展开或多条命令时,优先改用 `trans <providerId> script <<'SCRIPT'`。`apply-patch`、`find`、`glob` 和旧 `apply-patch-v1` fallback 有专用入口;`git`、`rg`、`grep`、`sed`、`nl`、`stat`、`du`、`ls`、`cat`、`head`、`tail`、`wc` 和 `pwd` 可以直接作为 `trans` 子命令使用,CLI 会对每个 argv token 做 shell quoting。旧的自由 ssh-like 远端命令入口只保留为近似原生 ssh 的人工兼容路径。
|
||||
`trans <providerId> argv <command> [args...]` 是通用 argv 安全拼接入口;`exec` 是同义入口。它是非交互远端单进程命令的默认成功路径,不需要 shell 管道时直接传命令和参数,例如 `trans D601 argv true`。需要管道、重定向、变量展开或多条命令时,优先改用 `trans <providerId> sh <<'SH'`。`apply-patch`、`find`、`glob` 和旧 `apply-patch-v1` fallback 有专用入口;`git`、`rg`、`grep`、`sed`、`nl`、`stat`、`du`、`ls`、`cat`、`head`、`tail`、`wc` 和 `pwd` 可以直接作为 `trans` 子命令使用,CLI 会对每个 argv token 做 shell quoting。旧的自由 ssh-like 远端命令入口只保留为近似原生 ssh 的人工兼容路径。
|
||||
|
||||
通过 `trans <providerId>` 执行多行脚本时,优先使用结构化 helper,例如 `trans G14 py < script.py`、`trans G14 script <<'SCRIPT'` 或 `trans G14:k3s script <<'SCRIPT'`。不要在远端命令字符串里再嵌套 heredoc、复杂引号或 `ssh 'python3 - <<EOF ...'` 形态;多层 shell 解析容易把 stdin 绑定到错误进程,结果会打开远端交互解释器并留下悬挂的 broker/SSH 会话。长脚本需要复用时,优先提交到 repo 或通过 stdin 传输到目标节点执行。
|
||||
通过 `trans <providerId>` 执行多行脚本时,优先使用结构化 helper,例如 `trans G14 py < script.py`、`trans G14 sh <<'SH'` 或 `trans G14:k3s sh <<'SH'`。不要在远端命令字符串里再嵌套 heredoc、复杂引号或 `ssh 'python3 - <<EOF ...'` 形态;多层 shell 解析容易把 stdin 绑定到错误进程,结果会打开远端交互解释器并留下悬挂的 broker/SSH 会话。长脚本需要复用时,优先提交到 repo 或通过 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`、`artifact-registry status|health`、`ci publish-user-service --dry-run`、`microservice list/status/health/diagnostics/tunnel-self-test/proxy`、`decision upload/list/show/health`、`decision requirement list/upsert`、`decision diary import/list/history/months/show/edit/upsert`、`codex task <taskId>`、`codex tasks`、`codex unread`、`codex queues`、`codex output <taskId>`、`codex judge <taskId> --attempt N` 和 `ssh <PROVIDER_ID> <remote-command>`。`microservice status/health/diagnostics` 经 frontend 远程传输时也复用本地 CLI 的默认 compact summary,`microservice health code-queue` 只有显式 `--raw` 或 `--full` 才返回完整健康 body。运行中纠偏已切到 AgentRun `send session/<sessionId>`;旧 `codex steer` 属于冻结写入口,不应通过 frontend 远程传输或旧 proxy 绕过。其中 `ssh` 的 remote frontend 传输使用 authenticated frontend `/ws/ssh` WebSocket 代理接入 backend-core SSH bridge,stdout/stderr 按字节流直通到调用端,不经过 `/api/dispatch`、`/api/tasks` 或 task JSON compact;frontend 运行时必须通过 `PROVIDER_TOKEN`/`UNIDESK_PROVIDER_TOKEN` 或 `PROVIDER_TOKEN_FILE`/`UNIDESK_PROVIDER_TOKEN_FILE` 读取 provider token,并且不能把 token 下发给 runner。因此 D601 Code Queue runner 内的 `tran G14 ...` 应与主 server 本机 `trans G14 ...` / `tran G14 ...` 在输出完整性上保持同一语义。非交互单进程命令优先 `trans D601 argv true`;`apply-patch`、stdin script、`py` 和旧 `apply-patch-v1` fallback 也走同一条 `/ws/ssh` 流式通道。交互式登录 shell 仍应在主 server 本机 CLI 使用,或显式切换到旧 SSH 传输后在主 server 上执行。当 backend-core、database、provider-dispatch 或 provider-host-ssh 缺失时,这些 read-only 预检必须返回结构化 `runnerDisposition=infra-blocked` 和缺失通道列表,而不是裸 `No such container`。若确实需要旧行为,可使用 `--main-server-key <key>` 或 `--main-server-transport ssh`,这时 CLI 会通过 SSH 登录主 server 的 `--main-server-root` 目录执行同一个 `bun scripts/cli.ts <command>`。
|
||||
默认 frontend 传输支持 `debug health`、`debug dispatch`、`debug task`、`artifact-registry status|health`、`ci publish-user-service --dry-run`、`microservice list/status/health/diagnostics/tunnel-self-test/proxy`、`decision upload/list/show/health`、`decision requirement list/upsert`、`decision diary import/list/history/months/show/edit/upsert`、`codex task <taskId>`、`codex tasks`、`codex unread`、`codex queues`、`codex output <taskId>`、`codex judge <taskId> --attempt N` 和 `ssh <PROVIDER_ID> <remote-command>`。`microservice status/health/diagnostics` 经 frontend 远程传输时也复用本地 CLI 的默认 compact summary,`microservice health code-queue` 只有显式 `--raw` 或 `--full` 才返回完整健康 body。运行中纠偏已切到 AgentRun `send session/<sessionId>`;旧 `codex steer` 属于冻结写入口,不应通过 frontend 远程传输或旧 proxy 绕过。其中 `ssh` 的 remote frontend 传输使用 authenticated frontend `/ws/ssh` WebSocket 代理接入 backend-core SSH bridge,stdout/stderr 按字节流直通到调用端,不经过 `/api/dispatch`、`/api/tasks` 或 task JSON compact;frontend 运行时必须通过 `PROVIDER_TOKEN`/`UNIDESK_PROVIDER_TOKEN` 或 `PROVIDER_TOKEN_FILE`/`UNIDESK_PROVIDER_TOKEN_FILE` 读取 provider token,并且不能把 token 下发给 runner。因此 D601 Code Queue runner 内的 `tran G14 ...` 应与主 server 本机 `trans G14 ...` / `tran G14 ...` 在输出完整性上保持同一语义。非交互单进程命令优先 `trans D601 argv true`;`apply-patch`、`sh`/`bash` stdin、`py` 和旧 `apply-patch-v1` fallback 也走同一条 `/ws/ssh` 流式通道。交互式登录 shell 仍应在主 server 本机 CLI 使用,或显式切换到旧 SSH 传输后在主 server 上执行。当 backend-core、database、provider-dispatch 或 provider-host-ssh 缺失时,这些 read-only 预检必须返回结构化 `runnerDisposition=infra-blocked` 和缺失通道列表,而不是裸 `No such container`。若确实需要旧行为,可使用 `--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 自测,不能视为交付完成。
|
||||
|
||||
|
||||
+21
-21
@@ -2045,7 +2045,7 @@ async function controlPlaneApply(config: UniDeskConfig, options: LaneConfirmOpti
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
const applied = await capture(config, spec.nodeKubeRoute, ["script", "--", applyYamlScript(manifestYaml, "unidesk-agentrun-control-plane", false)]);
|
||||
const applied = await capture(config, spec.nodeKubeRoute, ["sh", "--", applyYamlScript(manifestYaml, "unidesk-agentrun-control-plane", false)]);
|
||||
const payload = captureJsonPayload(applied);
|
||||
return {
|
||||
ok: applied.exitCode === 0 && payload.ok !== false,
|
||||
@@ -2071,15 +2071,15 @@ async function status(config: UniDeskConfig, options: StatusOptions): Promise<Re
|
||||
|
||||
async function statusYamlLane(config: UniDeskConfig, options: StatusOptions, target: { configPath: string; spec: AgentRunLaneSpec }): Promise<Record<string, unknown>> {
|
||||
const spec = target.spec;
|
||||
const sourceProbe = await timedStatusStage("source", () => capture(config, `${spec.nodeRoute}:${spec.source.workspace}`, ["script", "--", yamlLaneSourceStatusScript(spec)]));
|
||||
const sourceProbe = await timedStatusStage("source", () => capture(config, `${spec.nodeRoute}:${spec.source.workspace}`, ["sh", "--", yamlLaneSourceStatusScript(spec)]));
|
||||
const sourcePayload = captureJsonPayload(sourceProbe.value);
|
||||
const sourceCommit = options.sourceCommit
|
||||
?? stringOrNull(sourcePayload.remoteBranchCommit)
|
||||
?? stringOrNull(sourcePayload.localHead);
|
||||
const pipelineRunName = options.pipelineRun ?? (sourceCommit ? agentRunPipelineRunName(spec, sourceCommit) : null);
|
||||
const [runtimeProbe, mirrorProbe] = await Promise.all([
|
||||
timedStatusStage("runtime", () => capture(config, spec.nodeKubeRoute, ["script", "--", yamlLaneRuntimeStatusScript(spec, pipelineRunName)])),
|
||||
timedStatusStage("git-mirror", () => capture(config, spec.nodeKubeRoute, ["script", "--", yamlLaneGitMirrorStatusScript(spec)])),
|
||||
timedStatusStage("runtime", () => capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneRuntimeStatusScript(spec, pipelineRunName)])),
|
||||
timedStatusStage("git-mirror", () => capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneGitMirrorStatusScript(spec)])),
|
||||
]);
|
||||
const runtimePayload = captureJsonPayload(runtimeProbe.value);
|
||||
const mirrorPayload = captureJsonPayload(mirrorProbe.value);
|
||||
@@ -2218,7 +2218,7 @@ async function restartYamlLane(config: UniDeskConfig, options: LaneConfirmOption
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
const result = await capture(config, spec.nodeKubeRoute, ["script", "--", restartYamlLaneScript(spec)]);
|
||||
const result = await capture(config, spec.nodeKubeRoute, ["sh", "--", restartYamlLaneScript(spec)]);
|
||||
const payload = captureJsonPayload(result);
|
||||
return {
|
||||
ok: result.exitCode === 0 && payload.ok !== false,
|
||||
@@ -2281,7 +2281,7 @@ async function secretSync(config: UniDeskConfig, options: SecretSyncOptions): Pr
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
const result = await capture(config, spec.nodeKubeRoute, ["script", "--", secretSyncScript(spec, values.map(({ spec: item, value }) => ({ targetRef: item.targetRef, value: value.value })))]);
|
||||
const result = await capture(config, spec.nodeKubeRoute, ["sh", "--", secretSyncScript(spec, values.map(({ spec: item, value }) => ({ targetRef: item.targetRef, value: value.value })))]);
|
||||
const payload = captureJsonPayload(result);
|
||||
return {
|
||||
ok: result.exitCode === 0 && payload.ok !== false,
|
||||
@@ -2470,7 +2470,7 @@ async function triggerCurrent(config: UniDeskConfig, options: TriggerOptions): P
|
||||
|
||||
async function triggerCurrentYamlLane(config: UniDeskConfig, options: TriggerOptions, target: { configPath: string; spec: AgentRunLaneSpec }): Promise<Record<string, unknown>> {
|
||||
const spec = target.spec;
|
||||
const probe = await capture(config, spec.nodeRoute, ["script", "--", yamlLaneSourceBootstrapProbeScript(spec)]);
|
||||
const probe = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneSourceBootstrapProbeScript(spec)]);
|
||||
const source = captureJsonPayload(probe);
|
||||
const sourceCommit = stringOrNull(source.sourceCommit);
|
||||
const remoteBranchExists = source.remoteBranchExists === true;
|
||||
@@ -2544,7 +2544,7 @@ async function triggerCurrentYamlLaneConfirmed(config: UniDeskConfig, spec: Agen
|
||||
lane: spec.lane,
|
||||
status: "submitting",
|
||||
});
|
||||
const bootstrapSubmit = await capture(config, spec.nodeRoute, ["script", "--", yamlLaneSourceBootstrapSubmitScript(spec)]);
|
||||
const bootstrapSubmit = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneSourceBootstrapSubmitScript(spec)]);
|
||||
const bootstrapSubmitPayload = captureJsonPayload(bootstrapSubmit);
|
||||
if (bootstrapSubmit.exitCode !== 0 || bootstrapSubmitPayload.ok === false) {
|
||||
return {
|
||||
@@ -2577,7 +2577,7 @@ async function triggerCurrentYamlLaneConfirmed(config: UniDeskConfig, spec: Agen
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
const buildSubmit = await capture(config, spec.nodeRoute, ["script", "--", yamlLaneBuildImageSubmitScript(spec, sourceCommit)]);
|
||||
const buildSubmit = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneBuildImageSubmitScript(spec, sourceCommit)]);
|
||||
const buildSubmitPayload = captureJsonPayload(buildSubmit);
|
||||
if (buildSubmit.exitCode !== 0 || buildSubmitPayload.ok === false) {
|
||||
return {
|
||||
@@ -2618,7 +2618,7 @@ async function triggerCurrentYamlLaneConfirmed(config: UniDeskConfig, spec: Agen
|
||||
}
|
||||
const image = agentRunImageArtifact(spec, { sourceCommit, envIdentity, digest, status: stringOrNull(buildPayload.status) ?? "built" });
|
||||
const renderedFiles = renderAgentRunGitopsFiles(spec, { sourceCommit, image });
|
||||
const gitops = await capture(config, spec.nodeRoute, ["script", "--", yamlLaneGitopsPublishScript(spec, renderedFiles)]);
|
||||
const gitops = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneGitopsPublishScript(spec, renderedFiles)]);
|
||||
const gitopsPayload = captureJsonPayload(gitops);
|
||||
if (gitops.exitCode !== 0 || gitopsPayload.ok === false) {
|
||||
return {
|
||||
@@ -2656,7 +2656,7 @@ async function triggerCurrentYamlLaneConfirmed(config: UniDeskConfig, spec: Agen
|
||||
};
|
||||
}
|
||||
const pipelineRun = agentRunPipelineRunName(spec, sourceCommit);
|
||||
const created = await capture(config, spec.nodeKubeRoute, ["script", "--", yamlLanePipelineRunCreateScript(spec, sourceCommit, pipelineRun)]);
|
||||
const created = await capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLanePipelineRunCreateScript(spec, sourceCommit, pipelineRun)]);
|
||||
const createPayload = captureJsonPayload(created);
|
||||
return {
|
||||
ok: created.exitCode === 0 && createPayload.ok !== false,
|
||||
@@ -2719,7 +2719,7 @@ async function refreshYamlLane(config: UniDeskConfig, options: RefreshOptions):
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
const refreshed = await capture(config, spec.nodeKubeRoute, ["script", "--", refreshYamlLaneScript(spec)]);
|
||||
const refreshed = await capture(config, spec.nodeKubeRoute, ["sh", "--", refreshYamlLaneScript(spec)]);
|
||||
const payload = captureJsonPayload(refreshed);
|
||||
return {
|
||||
ok: refreshed.exitCode === 0 && payload.ok !== false,
|
||||
@@ -2740,7 +2740,7 @@ async function refreshYamlLane(config: UniDeskConfig, options: RefreshOptions):
|
||||
|
||||
async function cleanupRuns(config: UniDeskConfig, options: CleanupRunsOptions): Promise<Record<string, unknown>> {
|
||||
const { configPath, spec } = resolveAgentRunLaneTarget(options);
|
||||
const result = await capture(config, spec.nodeKubeRoute, ["script", "--", cleanupRunsScript(options, spec.ci.namespace, spec.ci.pipelineRunPrefix)]);
|
||||
const result = await capture(config, spec.nodeKubeRoute, ["sh", "--", cleanupRunsScript(options, spec.ci.namespace, spec.ci.pipelineRunPrefix)]);
|
||||
const payload = captureJsonPayload(result);
|
||||
const ok = result.exitCode === 0 && payload.ok !== false;
|
||||
const base = {
|
||||
@@ -2779,7 +2779,7 @@ async function cleanupRuns(config: UniDeskConfig, options: CleanupRunsOptions):
|
||||
|
||||
async function cleanupReleasedPvs(config: UniDeskConfig, options: CleanupReleasedPvOptions): Promise<Record<string, unknown>> {
|
||||
const { configPath, spec } = resolveAgentRunLaneTarget(options);
|
||||
const result = await capture(config, spec.nodeKubeRoute, ["script", "--", cleanupReleasedPvsScript(options, spec.ci.namespace)]);
|
||||
const result = await capture(config, spec.nodeKubeRoute, ["sh", "--", cleanupReleasedPvsScript(options, spec.ci.namespace)]);
|
||||
const payload = captureJsonPayload(result);
|
||||
const ok = result.exitCode === 0 && payload.ok !== false;
|
||||
const base = {
|
||||
@@ -3111,7 +3111,7 @@ async function waitForYamlLaneSourceBootstrap(config: UniDeskConfig, spec: Agent
|
||||
let polls = 0;
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
polls += 1;
|
||||
const probe = await capture(config, spec.nodeRoute, ["script", "--", yamlLaneSourceBootstrapStatusScript(spec, jobId)]);
|
||||
const probe = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneSourceBootstrapStatusScript(spec, jobId)]);
|
||||
const payload = captureJsonPayload(probe);
|
||||
lastPayload = payload;
|
||||
progressEvent("agentrun.yaml-lane.source-bootstrap.progress", {
|
||||
@@ -3231,7 +3231,7 @@ async function waitForYamlLaneBuildImage(config: UniDeskConfig, spec: AgentRunLa
|
||||
let polls = 0;
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
polls += 1;
|
||||
const probe = await capture(config, spec.nodeRoute, ["script", "--", yamlLaneBuildImageStatusScript(spec, jobId)]);
|
||||
const probe = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneBuildImageStatusScript(spec, jobId)]);
|
||||
const payload = captureJsonPayload(probe);
|
||||
lastPayload = payload;
|
||||
progressEvent("agentrun.yaml-lane.image-build.progress", {
|
||||
@@ -3311,7 +3311,7 @@ function yamlLaneGitopsPublishScript(spec: AgentRunLaneSpec, files: readonly { p
|
||||
async function runYamlLaneGitMirrorSyncJob(config: UniDeskConfig, spec: AgentRunLaneSpec): Promise<Record<string, unknown>> {
|
||||
const jobName = `${spec.gitMirror.syncJobPrefix}-${Date.now().toString(36)}`.slice(0, 63);
|
||||
const manifest = yamlLaneGitMirrorJobManifest(spec, "sync", jobName);
|
||||
const created = await capture(config, spec.nodeKubeRoute, ["script", "--", createYamlLaneJobScript(spec.gitMirror.namespace, jobName, manifest)]);
|
||||
const created = await capture(config, spec.nodeKubeRoute, ["sh", "--", createYamlLaneJobScript(spec.gitMirror.namespace, jobName, manifest)]);
|
||||
if (created.exitCode !== 0) {
|
||||
return { ok: false, phase: "create-job", jobName, capture: compactCapture(created, { full: true, stdoutTailChars: 4000, stderrTailChars: 4000 }), valuesPrinted: false };
|
||||
}
|
||||
@@ -3320,7 +3320,7 @@ async function runYamlLaneGitMirrorSyncJob(config: UniDeskConfig, spec: AgentRun
|
||||
let lastProbe: SshCaptureResult | null = null;
|
||||
while (Date.now() - startedAt < 300_000) {
|
||||
polls += 1;
|
||||
lastProbe = await capture(config, spec.nodeKubeRoute, ["script", "--", yamlLaneJobProbeScript(spec.gitMirror.namespace, jobName)]);
|
||||
lastProbe = await capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneJobProbeScript(spec.gitMirror.namespace, jobName)]);
|
||||
const payload = captureJsonPayload(lastProbe);
|
||||
progressEvent("agentrun.yaml-lane.git-mirror.progress", {
|
||||
node: spec.nodeId,
|
||||
@@ -4265,7 +4265,7 @@ async function gitMirrorStatus(config: UniDeskConfig, options: GitMirrorStatusOp
|
||||
|
||||
async function readGitMirrorStatus(config: UniDeskConfig, target: { configPath: string; spec: AgentRunLaneSpec }): Promise<Record<string, unknown> & { result: SshCaptureResult; raw: string; summary: Record<string, unknown> }> {
|
||||
const spec = target.spec;
|
||||
const result = await capture(config, spec.nodeKubeRoute, ["script", "--", yamlLaneGitMirrorStatusScript(spec)]);
|
||||
const result = await capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneGitMirrorStatusScript(spec)]);
|
||||
const raw = result.stdout;
|
||||
const summary = captureJsonPayload(result);
|
||||
return {
|
||||
@@ -4300,7 +4300,7 @@ async function runGitMirrorJob(config: UniDeskConfig, action: "sync" | "flush",
|
||||
next: { confirm: `bun scripts/cli.ts ${command} --node ${spec.nodeId} --lane ${spec.lane} --confirm` },
|
||||
};
|
||||
}
|
||||
const created = await capture(config, spec.nodeKubeRoute, ["script", "--", createYamlLaneJobScript(spec.gitMirror.namespace, jobName, manifest)]);
|
||||
const created = await capture(config, spec.nodeKubeRoute, ["sh", "--", createYamlLaneJobScript(spec.gitMirror.namespace, jobName, manifest)]);
|
||||
if (created.exitCode !== 0 || !options.wait) {
|
||||
const status = await gitMirrorStatus(config, { full: false, raw: false, node: spec.nodeId, lane: spec.lane });
|
||||
return {
|
||||
@@ -4347,7 +4347,7 @@ async function waitForGitMirrorJob(config: UniDeskConfig, spec: AgentRunLaneSpec
|
||||
let polls = 0;
|
||||
while (Date.now() - startedAtMs <= timeoutSeconds * 1000) {
|
||||
polls += 1;
|
||||
lastProbe = await capture(config, spec.nodeKubeRoute, ["script", "--", yamlLaneJobProbeScript(spec.gitMirror.namespace, jobName)]);
|
||||
lastProbe = await capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneJobProbeScript(spec.gitMirror.namespace, jobName)]);
|
||||
const summary = captureJsonPayload(lastProbe);
|
||||
process.stderr.write(`${JSON.stringify({
|
||||
event: `agentrun.git-mirror.${action}.progress`,
|
||||
|
||||
+3
-3
@@ -1077,7 +1077,7 @@ async function remoteApplyManifest(config: UniDeskConfig, path: string, target =
|
||||
"kubectl apply -f \"$tmp\"",
|
||||
].join("\n");
|
||||
emitCiInstallProgress("kubectl-apply", "started", { providerId: target.providerId, manifest: path });
|
||||
const result = await runSshCommandCapture(config, `${target.providerId}:k3s`, ["script"], script);
|
||||
const result = await runSshCommandCapture(config, `${target.providerId}:k3s`, ["sh"], script);
|
||||
if (result.exitCode !== 0) throw new Error(`kubectl apply failed for ${path}: ${result.stderr || result.stdout}`);
|
||||
emitCiInstallProgress("upload-manifest", "succeeded", { providerId: target.providerId, manifest: path, upload: result.stdout.split(/\r?\n/u).find((line) => line.startsWith("manifest_bytes=")) ?? "" });
|
||||
emitCiInstallProgress("kubectl-apply", "succeeded", { providerId: target.providerId, manifest: path });
|
||||
@@ -2987,7 +2987,7 @@ async function logs(config: UniDeskConfig, name: string, target = ciTarget(null)
|
||||
if (/^[a-z0-9]([-a-z0-9]{0,46}[a-z0-9])?$/u.test(name)) {
|
||||
const runScript = remoteRunLogsScript(name, target, options);
|
||||
const result = options.capture === "ssh-stream"
|
||||
? await runSshCommandCapture(config, `${target.providerId}:k3s`, ["script"], runScript)
|
||||
? await runSshCommandCapture(config, `${target.providerId}:k3s`, ["sh"], runScript)
|
||||
: await dispatchSsh(runScript, 60_000, 45_000, true, target);
|
||||
const resultOk = "ok" in result ? result.ok : result.exitCode === 0;
|
||||
if (resultOk || (result.exitCode !== 42 && !result.stderr.includes("no_run_files="))) {
|
||||
@@ -3006,7 +3006,7 @@ async function logs(config: UniDeskConfig, name: string, target = ciTarget(null)
|
||||
}
|
||||
const script = pipelineRunLogsScript(name, options);
|
||||
const result = options.capture === "ssh-stream"
|
||||
? await runSshCommandCapture(config, `${target.providerId}:k3s`, ["script"], script)
|
||||
? await runSshCommandCapture(config, `${target.providerId}:k3s`, ["sh"], script)
|
||||
: await runRemoteKubectl(script, 60_000, 45_000, target);
|
||||
return ciLogsResultFromCapture({
|
||||
ok: result.exitCode === 0,
|
||||
|
||||
+15
-14
@@ -28,13 +28,13 @@ export function rootHelp(): unknown {
|
||||
{ command: "trans <route> upload <local-file> <remote-file> | trans <route> download <remote-file> <local-file>", description: "Transfer whole files through SSH passthrough with automatic endpoint byte/SHA-256 verification; downloads stream stdout over host.ssh.tcp-pool." },
|
||||
{ command: "trans <providerId> apply-patch-v1 [tool args...] < patch.diff", description: "Fallback to the injected legacy remote apply_patch helper directly over SSH passthrough and stream the patch from local stdin." },
|
||||
{ command: "trans <providerId> py [script-args...] < script.py", description: "Run remote Python from local stdin through SSH passthrough without nested shell quoting; extra args become script argv." },
|
||||
{ command: "trans <providerId> script [--shell sh|bash] [script-args...] <<'SCRIPT' ...", description: "Run a remote shell script from local stdin using shell -s; default sh inherits provider proxy env and gets the portable printf helper used by shell/script." },
|
||||
{ command: "trans <providerId> sh|bash [arg...] <<'SH|BASH' ...", description: "Run a remote POSIX sh or Bash body from local stdin; the operation name declares the shell dialect, and removed `script`/`shell` operations fail with a migration hint." },
|
||||
{ command: "trans <providerId> skills [--scope all|wsl|windows] [--limit N]", description: "Discover WSL/Linux and, for WSL providers, Windows skill directories in one SSH passthrough call." },
|
||||
{ command: "trans <providerId> find <path...> [--max-depth N] [--type d|f|l] [--contains TEXT] [--iname PATTERN] [--limit N] [--sort]", description: "Run a structured remote find command without nested shell quoting or parentheses." },
|
||||
{ command: "trans <providerId> glob [--root DIR] [--pattern PATTERN] [--contains TEXT] [--type any|f|d] [--limit N] [--sort]", description: "Run remote glob matching through the injected helper without shell glob expansion." },
|
||||
{ command: "trans <providerId>:/absolute/workspace <operation args...>", description: "Route directly into a host workspace while keeping the operation parser independent from the location." },
|
||||
{ command: "trans <providerId>:k3s[:namespace:workload[:container]] <kubectl|logs|exec|script|apply-patch|apply-patch-v1|command> ...", description: "Locate a native k3s control plane or workload with route syntax, then run a separate operation with KUBECONFIG fixed and argv assembled by the CLI." },
|
||||
{ command: "trans <providerId> argv <command> [args...]", description: "Run a non-interactive remote command with each argv token shell-quoted by UniDesk before SSH passthrough; use `trans <providerId> script` when shell features are required." },
|
||||
{ command: "trans <providerId>:k3s[:namespace:workload[:container]] <kubectl|logs|exec|sh|bash|apply-patch|apply-patch-v1|command> ...", description: "Locate a native k3s control plane or workload with route syntax, then run a separate operation with KUBECONFIG fixed and argv assembled by the CLI." },
|
||||
{ command: "trans <providerId> argv <command> [args...]", description: "Run a non-interactive remote command with each argv token shell-quoted by UniDesk before SSH passthrough; use explicit `sh` or `bash` when shell features are required." },
|
||||
{ command: "microservice list", description: "List UniDesk-managed user services and their provider/runtime mapping." },
|
||||
{ command: "microservice status <id>", description: "Show one user service config, repository reference, backend mapping, and runtime status." },
|
||||
{ command: "microservice health <id> [--compact|--raw|--full]", description: "Probe one user service through backend-core -> provider-gateway HTTP proxy; default output is compact, and code-queue uses a commander-safe liveness summary unless raw/full is requested." },
|
||||
@@ -168,8 +168,8 @@ export function sshHelp(): unknown {
|
||||
"trans <providerId> playwright [--local-dir /tmp] <<'PW'",
|
||||
"trans <providerId> apply-patch-v1 [--allow-loose] < patch.diff",
|
||||
"trans <providerId> py [script-args...] < script.py",
|
||||
"trans <providerId> script [--shell sh|bash] [script-args...] <<'SCRIPT'",
|
||||
"trans <providerId> shell [--shell sh|bash] \"sed -n '1,20p' a && sed -n '1,20p' b\"",
|
||||
"trans <providerId> sh [arg...] <<'SH'",
|
||||
"trans <providerId> bash [arg...] <<'BASH'",
|
||||
"trans <providerId> skills [--scope all|wsl|windows] [--limit N]",
|
||||
"trans <providerId> find <path...> [--contains TEXT] [--limit N]",
|
||||
"trans <providerId> glob [--root DIR] [--pattern PATTERN]",
|
||||
@@ -184,7 +184,7 @@ export function sshHelp(): unknown {
|
||||
"trans D601:k3s kubectl get pods -n hwlab-dev",
|
||||
"trans G14:k3s",
|
||||
"trans G14:k3s kubectl get pipelineruns -n hwlab-ci",
|
||||
"trans D601:k3s script <<'SCRIPT'",
|
||||
"trans D601:k3s sh <<'SH'",
|
||||
"trans D601:k3s:hwlab-dev:hwlab-cloud-api:hwlab-cloud-api exec --cwd /app -- pwd",
|
||||
"trans D601:k3s:hwlab-dev:hwlab-cloud-api:hwlab-cloud-api apply-patch --cwd /app <<'PATCH'",
|
||||
"trans D601:k3s:hwlab-dev:hwlab-cloud-api apply-patch-v1 <<'PATCH'",
|
||||
@@ -193,26 +193,27 @@ export function sshHelp(): unknown {
|
||||
"trans D601:win upload ./tool.mjs F:\\Work\\hwlab\\.tmp\\tool.mjs",
|
||||
"trans D601:win download F:\\Work\\hwlab\\.tmp\\tool.mjs ./tool.mjs",
|
||||
"trans D601:k3s:hwlab-dev:hwlab-cloud-api node -e 'console.log(process.version)'",
|
||||
"trans D601:k3s:hwlab-dev:hwlab-cloud-api script <<'SCRIPT'",
|
||||
"trans D601:k3s:hwlab-dev:hwlab-cloud-api sh <<'SH'",
|
||||
"trans D601:k3s:hwlab-dev:hwlab-cloud-api logs --tail 80",
|
||||
"trans G14:k3s logs -n agentrun-ci -l tekton.dev/pipelineRun=<run> --tail 120",
|
||||
],
|
||||
notes: [
|
||||
"trans --help and trans <route> --help print this JSON help and never open an interactive session; the underlying ssh subcommand keeps the same help behavior.",
|
||||
"For non-interactive remote commands, prefer argv for a single process and script/stdin for shell logic.",
|
||||
"For non-interactive remote commands, prefer argv for a single process and explicit sh/bash stdin for shell logic.",
|
||||
"Windows routes have explicit Windows operations, not POSIX shell aliases: `ps` runs Windows PowerShell from stdin or one inline command, `cmd` runs cmd.exe/batch from stdin or one command line, and `skills` discovers Windows skill directories.",
|
||||
"For Windows PowerShell, use `trans <provider>:win ps <<'PS'`; the PowerShell body is written to a temporary .ps1 with UTF-8 settings and executed by powershell.exe. Do not use `script` for Windows PowerShell.",
|
||||
"For Windows PowerShell, use `trans <provider>:win ps <<'PS'`; the PowerShell body is written to a temporary .ps1 with UTF-8 settings and executed by powershell.exe. Do not use POSIX `sh` or `bash` for Windows PowerShell.",
|
||||
"For Windows cmd.exe, use `trans <provider>:win/<drive>/<path> cmd <<'CMD'`; `cmd` with no command-line arguments reads the UTF-8 batch body from stdin, injects UTF-8/Python encoding defaults, runs it from a temp .cmd file, and deletes the temp file.",
|
||||
"`argv` executes direct argv tokens only: `trans <route> argv ls -la` is valid, but `trans <route> argv 'ls -la'` is rejected because the single string would be treated as an executable path; use `script -- 'ls -la'` for one-line shell logic.",
|
||||
"For one-line remote shell logic without a heredoc, use `script -- '<command && command>'`; outer shell operators written outside trans, such as `trans G14:/repo sed ... && sed ...`, are parsed by the local shell before UniDesk starts and therefore cannot be redirected by the CLI. The explicit `shell '<command>'` operation remains available for the same sh -c path.",
|
||||
"When a one-line shell command is easier to type through the script path, `script -- '<command && command>'` runs that single string through the remote shell without waiting for stdin. When `script --` is followed by multiple tokens, it stays a direct argv form for commands such as `trans D601:/work script -- sed -n '1,20p' file`.",
|
||||
"script and shell helper modes inject a tiny POSIX-compatible printf wrapper before user shell text, so portable printf headings such as `printf \"--- section ---\\n\"` work consistently under dash/sh and bash. Direct argv commands are unchanged.",
|
||||
"`argv` executes direct argv tokens only: `trans <route> argv ls -la` is valid, but `trans <route> argv 'ls -la'` is rejected because the single string would be treated as an executable path; use `sh -- 'ls -la'` or `bash -- 'ls -la'` for one-line shell logic.",
|
||||
"For one-line remote shell logic without a heredoc, use `sh -- '<command && command>'` for POSIX syntax or `bash -- '<command && command>'` for Bash syntax. Outer shell operators written outside trans, such as `trans G14:/repo sed ... && sed ...`, are parsed by the local shell before UniDesk starts and therefore cannot be redirected by the CLI.",
|
||||
"`sh --` and `bash --` accept exactly one shell command string. For direct argv commands with multiple tokens, use `argv`, `exec`, or a known direct subcommand such as `git`, `rg`, `sed`, or `cat`.",
|
||||
"The removed `script` and `shell` operations intentionally fail. The operation name must declare the shell dialect: `sh` maps to target `/bin/sh`, while `bash` maps to target `bash`.",
|
||||
"sh and bash helper modes inject a tiny POSIX-compatible printf wrapper before user shell text, so portable printf headings such as `printf \"--- section ---\\n\"` work consistently under dash/sh and bash. Direct argv commands are unchanged.",
|
||||
"For arbitrary stdin streams into a workload command, use a workload route plus `exec --stdin -- <command> ...`; this keeps the route as location-only and avoids heredoc/base64/tar shell wrapping.",
|
||||
"`apply-patch` is the default remote text patch entry and uses the v2 local line-based patch engine with remote read/write operations, including Windows routes such as `D601:win/c/test`, so long Unicode/Chinese lines and pure insertion hunks avoid the legacy remote shell hunk parser. Plain multi-file Update File patches on POSIX host/k3s and Windows workspace routes use bulk read/write operations to avoid per-file SSH round trips. Its stdout follows Codex apply_patch text output rather than UniDesk JSON output; stderr keeps Codex-style failure text and appends one `UNIDESK_APPLY_PATCH_TIMING` JSON summary with durationMs, patchBytes, fileCount, hunkCount, changedCount, remoteOperationCount, remoteOperationCounts and remoteElapsedMs so slow patch runs can be attributed without changing success stdout.",
|
||||
"`upload` and `download` are the default whole-file transfer entries for non-text and generated files. They write through remote temp files, verify byte count and SHA-256 on both sides, and return `verification.automatic=true`, `verification.verified=true`, and `verification.match.{bytes,sha256}=true`; this JSON is the transfer integrity proof, so callers do not need a separate manual `sha256sum` check. Downloads stream over `host.ssh.tcp-pool`, emit progress JSON, and may receive a caller-supplied `--inactivity-timeout-ms` from async artifact/deploy jobs so active large transfers are not killed by the generic short-command budget.",
|
||||
"`playwright` runs a stdin heredoc on the target POSIX host/workload with a temporary `playwright-cli` wrapper in PATH, submits the remote script as a background job, polls short status commands for the manifest, then downloads image/pdf artifacts to local `/tmp` by default with the same verified SHA-256 transfer path. Canonical syntax is `trans D601 playwright <<'PW' ... PW`; workspace and known UniDesk host workspaces are preferred for wrapper discovery before external skill passthroughs.",
|
||||
"`apply-patch-v1` is the only legacy fallback entry: it rejects low-context update hunks by default, reports the matched file:line for each hunk on stderr, and only accepts --allow-loose when the caller has manually reviewed an intentionally ambiguous insertion.",
|
||||
"script defaults to target /bin/sh and inherits provider proxy variables such as HTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXY; it is for host/k3s POSIX shell only. Use --shell bash only for bash syntax such as pipefail, arrays, or [[ ... ]], not as a proxy workaround.",
|
||||
"`sh` inherits provider proxy variables such as HTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXY and runs target `/bin/sh`; use `bash` only for Bash syntax such as `pipefail`, arrays, substring expansion, or `[[ ... ]]`, not as a proxy workaround.",
|
||||
"Route syntax is `{provider}:{plane}[:{scope...}] {operation} [operation-args...]`: the first argv token locates a distributed target only, and every following token belongs to the operation parser. Host workspace routes use `<provider>:/absolute/workspace`; WSL providers can use `<provider>:win ps` for Windows PowerShell and `<provider>:win cmd` for Windows cmd.exe, with `<provider>:win/c/test ...` mapping the Windows cwd to `C:\\test`; native k3s providers such as D601 and G14 use `<provider>:k3s` for the control plane and `<provider>:k3s:<namespace>:<workload>[:<container>]` for a workload/container. In k3s routes, `:` is the distributed route separator; `/...` is only an in-container filesystem cwd and never selects a container. Prefer operation `--cwd /path` when a container is also specified.",
|
||||
"Use `win`, not `win32`; the win route is a Windows operation plane. `ps` and `cmd` both set UTF-8/Python encoding defaults, while `cmd` also sets `chcp 65001`.",
|
||||
"`<provider>:win skills` discovers the current Windows user's `%USERPROFILE%\\.agents\\skills` by default; use `--scope all` to include `%USERPROFILE%\\.codex\\skills`.",
|
||||
|
||||
+26
-26
@@ -972,11 +972,11 @@ function shortSha(sha: string): string {
|
||||
}
|
||||
|
||||
function precheckWorkspace(): CommandJsonResult {
|
||||
return cliJson(["ssh", `${G14_PROVIDER}:${G14_WORKSPACE}`, "script", "--", "pwd; git fetch origin G14 --prune; git status --short --branch; git remote -v | sed -n '1,4p'"], 120_000);
|
||||
return cliJson(["ssh", `${G14_PROVIDER}:${G14_WORKSPACE}`, "sh", "--", "pwd; git fetch origin G14 --prune; git status --short --branch; git remote -v | sed -n '1,4p'"], 120_000);
|
||||
}
|
||||
|
||||
function g14HostScript(script: string, timeoutMs = 120_000): CommandJsonResult {
|
||||
return cliJson(["ssh", G14_PROVIDER, "script", "--", script], timeoutMs);
|
||||
return cliJson(["ssh", G14_PROVIDER, "sh", "--", script], timeoutMs);
|
||||
}
|
||||
|
||||
function g14K3s(args: string[], timeoutMs = 60_000): CommandJsonResult {
|
||||
@@ -984,7 +984,7 @@ function g14K3s(args: string[], timeoutMs = 60_000): CommandJsonResult {
|
||||
}
|
||||
|
||||
function g14K3sInlineScriptWithInput(script: string, input: string, timeoutMs = 60_000): CommandJsonResult {
|
||||
return commandJsonWithInput(["bun", "scripts/cli.ts", "ssh", `${G14_PROVIDER}:k3s`, "script", "--", script], input, timeoutMs);
|
||||
return commandJsonWithInput(["bun", "scripts/cli.ts", "ssh", `${G14_PROVIDER}:k3s`, "sh", "--", script], input, timeoutMs);
|
||||
}
|
||||
|
||||
function remoteAsyncStatusDir(label: string): string {
|
||||
@@ -1119,7 +1119,7 @@ function parseRemoteAsyncPayload(result: CommandJsonResult): Record<string, unkn
|
||||
|
||||
function runG14K3sRemoteAsync(spec: RemoteAsyncCommandSpec): CommandJsonResult {
|
||||
const startedAtMs = Date.now();
|
||||
const start = g14K3s(["script", "--", remoteAsyncStartScript(spec)], 55_000);
|
||||
const start = g14K3s(["sh", "--", remoteAsyncStartScript(spec)], 55_000);
|
||||
const startPayload = parseRemoteAsyncPayload(start);
|
||||
if (!isCommandSuccess(start) || startPayload.ok !== true) return start;
|
||||
const deadlineMs = startedAtMs + Math.max(1, spec.timeoutSeconds) * 1000;
|
||||
@@ -1129,7 +1129,7 @@ function runG14K3sRemoteAsync(spec: RemoteAsyncCommandSpec): CommandJsonResult {
|
||||
attempts += 1;
|
||||
const wait = runCommand(["sleep", attempts <= 1 ? "1" : "2"], repoRoot, { timeoutMs: 3_000 });
|
||||
if (wait.exitCode !== 0 && wait.timedOut) break;
|
||||
const status = g14K3s(["script", "--", remoteAsyncStatusScript(spec.label, spec.token)], 55_000);
|
||||
const status = g14K3s(["sh", "--", remoteAsyncStatusScript(spec.label, spec.token)], 55_000);
|
||||
lastStatus = status;
|
||||
const payload = parseRemoteAsyncPayload(status);
|
||||
const state = typeof payload.state === "string" ? payload.state : null;
|
||||
@@ -1156,7 +1156,7 @@ function runG14K3sRemoteAsync(spec: RemoteAsyncCommandSpec): CommandJsonResult {
|
||||
};
|
||||
}
|
||||
}
|
||||
const killed = g14K3s(["script", "--", remoteAsyncKillScript(spec.label, spec.token)], 55_000);
|
||||
const killed = g14K3s(["sh", "--", remoteAsyncKillScript(spec.label, spec.token)], 55_000);
|
||||
const payload = parseRemoteAsyncPayload(killed);
|
||||
return {
|
||||
ok: false,
|
||||
@@ -1358,7 +1358,7 @@ function getV02TriggerSnapshot(): V02TriggerSnapshot {
|
||||
"printf 'message\\t%s\\n' \"$(printf '%s\\n' \"$status_text\" | sed -n '3p')\"",
|
||||
"printf 'pipelineRunStderr\\t%s\\n' \"$(one_line < \"$tmp_dir/pipelinerun.err\")\"",
|
||||
].join("\n");
|
||||
const result = g14K3s(["script", "--", script], 180_000);
|
||||
const result = g14K3s(["sh", "--", script], 180_000);
|
||||
return parseV02TriggerSnapshot(result, Date.now());
|
||||
}
|
||||
|
||||
@@ -1629,7 +1629,7 @@ function v02ControlPlaneStatusBundle(target: V02ControlPlaneStatusTarget = {}):
|
||||
`section gitMirrorCache kubectl exec -n ${shellQuote(GIT_MIRROR_NAMESPACE)} deploy/git-mirror-http -- sh -lc ${shellQuote(gitMirrorCacheProbeScript())}`,
|
||||
`section webAssets sh -c ${shellQuote(v02WebAssetsProbeScript())}`,
|
||||
].join("\n");
|
||||
return g14K3s(["script", "--", script], 60_000);
|
||||
return g14K3s(["sh", "--", script], 60_000);
|
||||
}
|
||||
|
||||
function v02SourceHeadsProbeScript(): string {
|
||||
@@ -2373,7 +2373,7 @@ export function v02CloseoutVerdict(status: Record<string, unknown>): Record<stri
|
||||
exitCode: sourceHeadsTarget.ancestorExitCode ?? null,
|
||||
command: sourceCommit === null
|
||||
? null
|
||||
: `trans G14:/root/hwlab-v02 script -- 'git fetch origin v0.2 && git merge-base --is-ancestor ${sourceCommit} origin/v0.2'`,
|
||||
: `trans G14:/root/hwlab-v02 sh -- 'git fetch origin v0.2 && git merge-base --is-ancestor ${sourceCommit} origin/v0.2'`,
|
||||
};
|
||||
const recommendedNext = v02CloseoutRecommendedNext({
|
||||
closeable,
|
||||
@@ -3052,7 +3052,7 @@ function runControlPlaneReleasedPvCleanup(options: G14ControlPlaneOptions): Reco
|
||||
deletion,
|
||||
followUp: {
|
||||
diskPressure: "trans G14:k3s kubectl get node ubuntu-rog-zephyrus-g14-ga401iv-ga401iv -o jsonpath='{range .status.conditions[*]}{.type}{\"=\"}{.status}{\" \"}{.reason}{\"\\n\"}{end}'",
|
||||
storage: "trans G14 script -- 'df -h /; sudo du -xh -d 1 /var/lib/rancher/k3s/storage 2>/dev/null | sort -h | tail -20'",
|
||||
storage: "trans G14 sh -- 'df -h /; sudo du -xh -d 1 /var/lib/rancher/k3s/storage 2>/dev/null | sort -h | tail -20'",
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -3342,7 +3342,7 @@ function applyRuntimeLaneControlPlaneFiles(spec: HwlabRuntimeLaneSpec, renderDir
|
||||
? ["set -eu", shellCommand.replace(/^exec /, ""), ...cleanupCommands].join("\n")
|
||||
: shellCommand;
|
||||
const visibleCommand = cleanupCommands.length > 0
|
||||
? ["bun", "scripts/cli.ts", "ssh", `${G14_PROVIDER}:k3s`, "script", "--", script]
|
||||
? ["bun", "scripts/cli.ts", "ssh", `${G14_PROVIDER}:k3s`, "sh", "--", script]
|
||||
: command;
|
||||
return runG14K3sRemoteAsync({
|
||||
script,
|
||||
@@ -3642,7 +3642,7 @@ function createRuntimeLanePipelineRun(spec: HwlabRuntimeLaneSpec, sourceCommit:
|
||||
"fi",
|
||||
`kubectl get pipelinerun -n ${shellQuote(CI_NAMESPACE)} ${shellQuote(pipelineRun)} -o jsonpath='{.metadata.name}{\"\\n\"}{.metadata.labels.hwlab\\.pikastech\\.local/source-commit}{\"\\n\"}{.status.conditions[0].status}{\"\\n\"}{.status.conditions[0].reason}{\"\\n\"}'`,
|
||||
].join("\n");
|
||||
return g14K3s(["script", "--", script], timeoutSeconds * 1000);
|
||||
return g14K3s(["sh", "--", script], timeoutSeconds * 1000);
|
||||
}
|
||||
|
||||
function createV02PipelineRun(sourceCommit: string, timeoutSeconds: number): CommandJsonResult {
|
||||
@@ -3927,7 +3927,7 @@ function runtimeLaneControlPlaneStatusBundle(spec: HwlabRuntimeLaneSpec, target:
|
||||
shellQuote(pipelineRunRowsJsonPath()),
|
||||
].join(" "),
|
||||
].join("\n");
|
||||
return g14K3s(["script", "--", script], 120_000);
|
||||
return g14K3s(["sh", "--", script], 120_000);
|
||||
}
|
||||
|
||||
function runtimeLanePublicProbeScript(spec: HwlabRuntimeLaneSpec): string {
|
||||
@@ -4174,7 +4174,7 @@ function runtimeLaneArgoRefreshScript(spec: HwlabRuntimeLaneSpec, dryRun: boolea
|
||||
}
|
||||
|
||||
function refreshRuntimeLaneArgoApplication(spec: HwlabRuntimeLaneSpec, options: G14ControlPlaneOptions): Record<string, unknown> {
|
||||
const result = g14K3s(["script", "--", runtimeLaneArgoRefreshScript(spec, options.dryRun)], options.timeoutSeconds * 1000);
|
||||
const result = g14K3s(["sh", "--", runtimeLaneArgoRefreshScript(spec, options.dryRun)], options.timeoutSeconds * 1000);
|
||||
const fields = keyValueLinesFromText(statusText(result));
|
||||
const ok = isCommandSuccess(result);
|
||||
return {
|
||||
@@ -5315,7 +5315,7 @@ function runG14Secret(options: G14SecretOptions): Record<string, unknown> {
|
||||
: null;
|
||||
const result = options.preset === "master-server-admin-api-key"
|
||||
? g14K3sInlineScriptWithInput(script, localAdminApiKey?.key ?? "", options.timeoutSeconds * 1000 + 2000)
|
||||
: g14K3s(["script", "--", script], options.timeoutSeconds * 1000);
|
||||
: g14K3s(["sh", "--", script], options.timeoutSeconds * 1000);
|
||||
const status = v02SecretStatusFromText(statusText(result), isCommandSuccess(result), result.exitCode, result.stderr);
|
||||
const dryRunOk = options.action === "ensure" && options.dryRun && isCommandSuccess(result);
|
||||
const deleteDryRunOk = options.action === "delete" && options.dryRun && isCommandSuccess(result);
|
||||
@@ -5482,7 +5482,7 @@ function legacyG14RetirementStatusScript(): string {
|
||||
}
|
||||
|
||||
function legacyG14RetirementStatus(): Record<string, unknown> {
|
||||
const result = g14K3s(["script", "--", legacyG14RetirementStatusScript()], 60_000);
|
||||
const result = g14K3s(["sh", "--", legacyG14RetirementStatusScript()], 60_000);
|
||||
const sections = parseShellSections(statusText(result));
|
||||
const legacyApplications = parseSectionJsonArray(sections.legacyApplications).map(applicationSummary);
|
||||
const legacyNamespaces = parseSectionJsonArray(sections.legacyNamespaces).map(namespaceSummary);
|
||||
@@ -5628,7 +5628,7 @@ function runLegacyG14Retirement(options: G14LegacyRetirementOptions): Record<str
|
||||
|
||||
const markerBeforeDelete = writeLegacyG14RetirementMarker("retiring", options.reason, { command: "hwlab g14 retirement execute --confirm" });
|
||||
const localJobs = cancelLegacyG14MonitorJobs(false);
|
||||
const deleteResult = g14K3s(["script", "--", legacyG14RetirementDeleteScript()], Math.max(60_000, options.timeoutSeconds * 1000));
|
||||
const deleteResult = g14K3s(["sh", "--", legacyG14RetirementDeleteScript()], Math.max(60_000, options.timeoutSeconds * 1000));
|
||||
const deleteSections = parseShellSections(statusText(deleteResult));
|
||||
const afterDelete = legacyG14RetirementStatus();
|
||||
const wait = options.wait ? waitForLegacyG14Retirement(options.timeoutSeconds) : null;
|
||||
@@ -6387,7 +6387,7 @@ function runGitMirrorStatus(lane: HwlabRuntimeLane = "v02"): Record<string, unkn
|
||||
shellQuote(gitMirrorCacheProbeScript()),
|
||||
].join(" "),
|
||||
].join("\n");
|
||||
const bundle = g14K3s(["script", "--", script], 60_000);
|
||||
const bundle = g14K3s(["sh", "--", script], 60_000);
|
||||
const sections = parseShellSections(statusText(bundle));
|
||||
const resources = sections.resources;
|
||||
const legacyCronJob = sections.legacyCronJob;
|
||||
@@ -6518,7 +6518,7 @@ function runGitMirrorSync(options: G14GitMirrorOptions): Record<string, unknown>
|
||||
`kubectl exec -n ${shellQuote(GIT_MIRROR_NAMESPACE)} deploy/git-mirror-http -- sh -lc ${shellQuote(`cat /cache/HWLAB.last-sync.json 2>/dev/null || true; printf "\\n"; git --git-dir=/cache/pikasTech/HWLAB.git rev-parse refs/heads/${spec.sourceBranch} refs/mirror-stage/heads/${spec.sourceBranch} refs/heads/${spec.gitopsBranch} refs/mirror-stage/heads/${spec.gitopsBranch} 2>/dev/null || true`)}`,
|
||||
].join("\n"),
|
||||
].join("\n");
|
||||
const result = g14K3s(["script", "--", script], options.timeoutSeconds * 1000 + 30_000);
|
||||
const result = g14K3s(["sh", "--", script], options.timeoutSeconds * 1000 + 30_000);
|
||||
const syncCommand = spec.lane === "v02"
|
||||
? "bun scripts/cli.ts hwlab g14 git-mirror sync --lane v02 --confirm"
|
||||
: `bun scripts/cli.ts hwlab nodes git-mirror sync --node ${spec.nodeId} --lane ${spec.lane} --confirm`;
|
||||
@@ -6574,7 +6574,7 @@ function runGitMirrorFlush(options: G14GitMirrorOptions): Record<string, unknown
|
||||
`kubectl exec -n ${shellQuote(GIT_MIRROR_NAMESPACE)} deploy/git-mirror-http -- sh -lc ${shellQuote(`cat /cache/HWLAB.last-flush.json 2>/dev/null || true; printf "\\n"; git --git-dir=/cache/pikasTech/HWLAB.git rev-parse refs/heads/${options.lane === "v02" ? "v0.2-gitops" : hwlabRuntimeLaneSpec(options.lane).gitopsBranch} refs/mirror-stage/heads/${options.lane === "v02" ? "v0.2-gitops" : hwlabRuntimeLaneSpec(options.lane).gitopsBranch} 2>/dev/null || true`)}`,
|
||||
].join("\n"),
|
||||
].join("\n");
|
||||
const result = g14K3s(["script", "--", script], options.timeoutSeconds * 1000 + 30_000);
|
||||
const result = g14K3s(["sh", "--", script], options.timeoutSeconds * 1000 + 30_000);
|
||||
return {
|
||||
ok: isCommandSuccess(result),
|
||||
command: "hwlab g14 git-mirror flush",
|
||||
@@ -7367,7 +7367,7 @@ function g14ObservabilityStatus(): Record<string, unknown> {
|
||||
`section workloadMonitors kubectl get servicemonitor,prometheusrule -n ${shellQuote(V02_RUNTIME_NAMESPACE)} -l hwlab.pikastech.local/monitoring=enabled -o json`,
|
||||
`section query kubectl get --raw ${shellQuote(queryPath)}`,
|
||||
].join("\n");
|
||||
const bundle = g14K3s(["script", "--", script], 120_000);
|
||||
const bundle = g14K3s(["sh", "--", script], 120_000);
|
||||
const sections = parseShellSections(statusText(bundle));
|
||||
const namespace = parseSectionJson(sections.namespace);
|
||||
const discoveryNamespace = parseSectionJson(sections.discoveryNamespace);
|
||||
@@ -7538,7 +7538,7 @@ function runG14ObservabilityApply(options: G14ObservabilityOptions): Record<stri
|
||||
const manifest = g14PrometheusManifest();
|
||||
const manifestB64 = Buffer.from(JSON.stringify(manifest), "utf8").toString("base64");
|
||||
const script = g14ObservabilityApplyScript(options, manifestB64);
|
||||
const result = g14K3s(["script", "--", script], options.timeoutSeconds * 1000 + 90_000);
|
||||
const result = g14K3s(["sh", "--", script], options.timeoutSeconds * 1000 + 90_000);
|
||||
const ok = isCommandSuccess(result);
|
||||
return {
|
||||
ok,
|
||||
@@ -7629,7 +7629,7 @@ function runG14ObservabilityTargets(options: G14ObservabilityOptions): Record<st
|
||||
`section podResourceMetrics kubectl get --raw ${shellQuote(`/apis/metrics.k8s.io/v1beta1/namespaces/${V02_RUNTIME_NAMESPACE}/pods`)}`,
|
||||
...queryCommands,
|
||||
].join("\n");
|
||||
const bundle = g14K3s(["script", "--", script], options.timeoutSeconds * 1000);
|
||||
const bundle = g14K3s(["sh", "--", script], options.timeoutSeconds * 1000);
|
||||
const sections = parseShellSections(statusText(bundle));
|
||||
const pods = k8sItems(parseSectionJson(sections.pods));
|
||||
const monitors = k8sItems(parseSectionJson(sections.monitors));
|
||||
@@ -7730,7 +7730,7 @@ function runG14ObservabilityBoundary(options: G14ObservabilityOptions): Record<s
|
||||
`section workloadMonitoring kubectl get servicemonitor,podmonitor,prometheusrule,prometheus,alertmanager -n ${shellQuote(V02_RUNTIME_NAMESPACE)} -o json`,
|
||||
`section infraControlPlane kubectl get prometheus,alertmanager -n ${shellQuote(G14_OBSERVABILITY_NAMESPACE)} -o json`,
|
||||
].join("\n");
|
||||
const bundle = g14K3s(["script", "--", script], options.timeoutSeconds * 1000);
|
||||
const bundle = g14K3s(["sh", "--", script], options.timeoutSeconds * 1000);
|
||||
const sections = parseShellSections(statusText(bundle));
|
||||
const workloadItems = k8sItems(parseSectionJson(sections.workloadMonitoring));
|
||||
const infraItems = k8sItems(parseSectionJson(sections.infraControlPlane));
|
||||
@@ -7969,7 +7969,7 @@ function startGitMirrorJob(options: G14GitMirrorOptions): Record<string, unknown
|
||||
}
|
||||
|
||||
function g14HostScript(script: string, timeoutMs = 120_000): CommandJsonResult {
|
||||
return cliJson(["ssh", G14_PROVIDER, "script", "--", script], timeoutMs);
|
||||
return cliJson(["ssh", G14_PROVIDER, "sh", "--", script], timeoutMs);
|
||||
}
|
||||
|
||||
function g14CiToolsImage(tag: string): string {
|
||||
@@ -8223,7 +8223,7 @@ function mergePullRequest(number: number, dryRun: boolean): CommandJsonResult {
|
||||
}
|
||||
|
||||
function getG14Head(): string | null {
|
||||
const result = cliJson(["ssh", `${G14_PROVIDER}:${G14_WORKSPACE}`, "script", "--", "git fetch origin G14 --prune >/dev/null 2>&1; git rev-parse origin/G14"], 120_000);
|
||||
const result = cliJson(["ssh", `${G14_PROVIDER}:${G14_WORKSPACE}`, "sh", "--", "git fetch origin G14 --prune >/dev/null 2>&1; git rev-parse origin/G14"], 120_000);
|
||||
if (!isCommandSuccess(result)) return null;
|
||||
const output = String(nested(result.parsed, ["data", "stdout"]) ?? result.stdout).trim();
|
||||
const match = /[0-9a-f]{40}/iu.exec(output);
|
||||
|
||||
@@ -1748,7 +1748,7 @@ function manifestObjectSummary(manifest: readonly Record<string, unknown>[]): Re
|
||||
}
|
||||
|
||||
function runTransK3s(kubeRoute: string, script: string, timeoutSeconds: number): CommandResult {
|
||||
return runCommand(["/root/.local/bin/trans", kubeRoute, "script", "--", script], rootPath(), { timeoutMs: timeoutSeconds * 1000 });
|
||||
return runCommand(["/root/.local/bin/trans", kubeRoute, "sh", "--", script], rootPath(), { timeoutMs: timeoutSeconds * 1000 });
|
||||
}
|
||||
|
||||
function proxyExportBlock(node: ControlPlaneNodeSpec): string {
|
||||
|
||||
@@ -449,7 +449,7 @@ function transPath(): string {
|
||||
}
|
||||
|
||||
function runNodeHostScript(spec: HwlabRuntimeLaneSpec, script: string, timeoutSeconds: number, input = ""): CommandResult {
|
||||
return runCommand([transPath(), spec.nodeRoute, "script", "--", script], repoRoot, { input, timeoutMs: timeoutSeconds * 1000 });
|
||||
return runCommand([transPath(), spec.nodeRoute, "sh", "--", script], repoRoot, { input, timeoutMs: timeoutSeconds * 1000 });
|
||||
}
|
||||
|
||||
function runNodeHostScriptAsync(spec: HwlabRuntimeLaneSpec, script: string, timeoutSeconds: number, label: string): CommandResult {
|
||||
@@ -535,7 +535,7 @@ function runNodeHostScriptAsync(spec: HwlabRuntimeLaneSpec, script: string, time
|
||||
"cat \"$status_path\" 2>/dev/null || true",
|
||||
].join("\n"), 55);
|
||||
return {
|
||||
command: [transPath(), spec.nodeRoute, "script", "--", "<remote async script>"],
|
||||
command: [transPath(), spec.nodeRoute, "sh", "--", "<remote async script>"],
|
||||
cwd: repoRoot,
|
||||
exitCode: 124,
|
||||
stdout: typeof payload.stdout === "string" ? payload.stdout : "",
|
||||
@@ -561,7 +561,7 @@ function parseJsonObject(text: string): Record<string, unknown> {
|
||||
|
||||
function commandResultFromAsync(spec: HwlabRuntimeLaneSpec, payload: Record<string, unknown>, statusPath: string, timedOut: boolean): CommandResult {
|
||||
return {
|
||||
command: [transPath(), spec.nodeRoute, "script", "--", "<remote async script>"],
|
||||
command: [transPath(), spec.nodeRoute, "sh", "--", "<remote async script>"],
|
||||
cwd: repoRoot,
|
||||
exitCode: typeof payload.exitCode === "number" ? payload.exitCode : payload.ok === true ? 0 : 1,
|
||||
stdout: typeof payload.stdout === "string" ? payload.stdout : "",
|
||||
@@ -576,7 +576,7 @@ function runNodeK3sArgs(spec: HwlabRuntimeLaneSpec, args: string[], timeoutSecon
|
||||
}
|
||||
|
||||
function runNodeK3sScript(spec: HwlabRuntimeLaneSpec, script: string, timeoutSeconds: number, input = ""): CommandResult {
|
||||
return runCommand([transPath(), spec.nodeKubeRoute, "script", "--", script], repoRoot, { input, timeoutMs: timeoutSeconds * 1000 });
|
||||
return runCommand([transPath(), spec.nodeKubeRoute, "sh", "--", script], repoRoot, { input, timeoutMs: timeoutSeconds * 1000 });
|
||||
}
|
||||
|
||||
function isCommandSuccess(result: CommandResult): boolean {
|
||||
@@ -3925,7 +3925,7 @@ function runPublicExposureFrpcRecreate(options: NodePublicExposureOptions): Comm
|
||||
&& fields.rolloutStatusExitCode === "0";
|
||||
const stdout = Object.entries(fields).map(([key, value]) => `${key}\t${value}`).join("\n") + "\n";
|
||||
return {
|
||||
command: [transPath(), `${options.node}:k3s`, "script", "--", "<public-exposure-frpc-recreate>"],
|
||||
command: [transPath(), `${options.node}:k3s`, "sh", "--", "<public-exposure-frpc-recreate>"],
|
||||
cwd: repoRoot,
|
||||
exitCode: ok ? 0 : 1,
|
||||
stdout: [stdout.trim(), ...stdoutParts].filter(Boolean).join("\n") + "\n",
|
||||
@@ -4082,11 +4082,11 @@ ${tlsLines} @api path /health* /auth* /v1* /json-rpc* /openapi* /docs* /swagg
|
||||
}
|
||||
|
||||
function runTransScript(node: string, script: string, input: string, timeoutSeconds: number): CommandResult {
|
||||
return runCommand([transPath(), `${node}:k3s`, "script", "--", script], repoRoot, { input, timeoutMs: timeoutSeconds * 1000 });
|
||||
return runCommand([transPath(), `${node}:k3s`, "sh", "--", script], repoRoot, { input, timeoutMs: timeoutSeconds * 1000 });
|
||||
}
|
||||
|
||||
function runTransHostScript(node: string, script: string, input: string, timeoutSeconds: number): CommandResult {
|
||||
return runCommand([transPath(), node, "script", "--", script], repoRoot, { input, timeoutMs: timeoutSeconds * 1000 });
|
||||
return runCommand([transPath(), node, "sh", "--", script], repoRoot, { input, timeoutMs: timeoutSeconds * 1000 });
|
||||
}
|
||||
|
||||
function runObsoleteSecretCleanup(options: NodeSecretOptions, spec: RuntimeSecretSpec): Record<string, unknown> {
|
||||
|
||||
@@ -138,7 +138,7 @@ print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
sys.exit(0 if payload["ok"] else 1)
|
||||
PY
|
||||
`;
|
||||
const result = await runSshCommandCapture(config, exposure.pk01.route, ["script"], script);
|
||||
const result = await runSshCommandCapture(config, exposure.pk01.route, ["sh"], script);
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return parsed ?? { ok: false, capture: compactCapture(result, { full: true }) };
|
||||
}
|
||||
|
||||
@@ -1226,7 +1226,7 @@ function desiredChecks(pg: PostgresHostConfig, facts: RemoteFacts, secrets: Secr
|
||||
}
|
||||
|
||||
async function remoteFacts(config: UniDeskConfig, pg: PostgresHostConfig, secrets: SecretInspection | null): Promise<{ capture: SshCaptureResult; parsed: RemoteFacts | null }> {
|
||||
const capture = await runSshCommandCapture(config, pg.node.route, ["script"], factsScript(pg, appProbes(pg, secrets)));
|
||||
const capture = await runSshCommandCapture(config, pg.node.route, ["sh"], factsScript(pg, appProbes(pg, secrets)));
|
||||
const parsed = parseJsonOutput(capture.stdout) as RemoteFacts | null;
|
||||
return { capture, parsed };
|
||||
}
|
||||
@@ -1486,7 +1486,7 @@ function releaseUrl(pg: PostgresHostConfig): string {
|
||||
async function startRemoteApplyJob(config: UniDeskConfig, pg: PostgresHostConfig, localState: { inspection: SecretInspection }): Promise<RemoteJobStart> {
|
||||
const payload = remoteApplyPayload(pg, localState.inspection);
|
||||
const script = startRemoteApplyJobScript(pg, payload);
|
||||
const capture = await runSshCommandCapture(config, pg.node.route, ["script"], script);
|
||||
const capture = await runSshCommandCapture(config, pg.node.route, ["sh"], script);
|
||||
const parsed = parseJsonOutput(capture.stdout);
|
||||
return {
|
||||
ok: capture.exitCode === 0 && parsed !== null && parsed.ok === true,
|
||||
@@ -1938,7 +1938,7 @@ printf '\\n---LOG---\\n'
|
||||
if [ -f "$log" ]; then tail -80 "$log"; fi
|
||||
ROOT
|
||||
`;
|
||||
const capture = await runSshCommandCapture(config, pg.node.route, ["script"], script);
|
||||
const capture = await runSshCommandCapture(config, pg.node.route, ["sh"], script);
|
||||
const [stateText, logTail = ""] = capture.stdout.split("\n---LOG---\n");
|
||||
const parsed = parseJsonOutput(stateText) ?? {};
|
||||
const statusValue = typeof parsed.status === "string" ? parsed.status : null;
|
||||
|
||||
@@ -500,7 +500,7 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Reco
|
||||
};
|
||||
}
|
||||
if (options.dryRun) {
|
||||
const result = await capture(config, target.route, ["script"], dryRunManifestScript({ yaml, target, fieldManager, manifestName: serviceName }));
|
||||
const result = await capture(config, target.route, ["sh"], dryRunManifestScript({ yaml, target, fieldManager, manifestName: serviceName }));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
||||
@@ -522,7 +522,7 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Reco
|
||||
readTextFile,
|
||||
});
|
||||
const confirmedYaml = renderManifest(langbot, target, secretMaterial.fingerprint);
|
||||
const result = await capture(config, target.route, ["script"], applyScript(confirmedYaml, langbot, target, secretMaterial, frpcSecret));
|
||||
const result = await capture(config, target.route, ["sh"], applyScript(confirmedYaml, langbot, target, secretMaterial, frpcSecret));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
const caddy = await applyPk01CaddyBlock(config, serviceName, target.publicExposure);
|
||||
return {
|
||||
@@ -547,7 +547,7 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Reco
|
||||
async function status(config: UniDeskConfig, options: CommonOptions): Promise<Record<string, unknown>> {
|
||||
const langbot = readLangBotConfig();
|
||||
const target = resolveTarget(langbot, options.targetId);
|
||||
const result = await capture(config, target.route, ["script"], statusScript(langbot, target));
|
||||
const result = await capture(config, target.route, ["sh"], statusScript(langbot, target));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
||||
@@ -562,7 +562,7 @@ async function status(config: UniDeskConfig, options: CommonOptions): Promise<Re
|
||||
async function logs(config: UniDeskConfig, options: LogsOptions): Promise<Record<string, unknown>> {
|
||||
const langbot = readLangBotConfig();
|
||||
const target = resolveTarget(langbot, options.targetId);
|
||||
const result = await capture(config, target.route, ["script"], logsScript(target, options));
|
||||
const result = await capture(config, target.route, ["sh"], logsScript(target, options));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
||||
@@ -579,7 +579,7 @@ async function logs(config: UniDeskConfig, options: LogsOptions): Promise<Record
|
||||
async function validate(config: UniDeskConfig, options: CommonOptions): Promise<Record<string, unknown>> {
|
||||
const langbot = readLangBotConfig();
|
||||
const target = resolveTarget(langbot, options.targetId);
|
||||
const k8s = await capture(config, target.route, ["script"], validateScript(target));
|
||||
const k8s = await capture(config, target.route, ["sh"], validateScript(target));
|
||||
const k8sParsed = parseJsonOutput(k8s.stdout);
|
||||
const publicProbe = publicHttpProbe(target.publicExposure.publicBaseUrl, "/api/v1/system/info");
|
||||
return {
|
||||
|
||||
@@ -289,7 +289,7 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Reco
|
||||
};
|
||||
}
|
||||
if (options.dryRun) {
|
||||
const result = await capture(config, target.route, ["script"], dryRunManifestScript({ yaml, target, fieldManager, manifestName: serviceName }));
|
||||
const result = await capture(config, target.route, ["sh"], dryRunManifestScript({ yaml, target, fieldManager, manifestName: serviceName }));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
||||
@@ -311,7 +311,7 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Reco
|
||||
readTextFile,
|
||||
});
|
||||
const confirmedYaml = renderManifest(n8n, target, secretMaterial.fingerprint);
|
||||
const result = await capture(config, target.route, ["script"], applyScript(confirmedYaml, n8n, target, secretMaterial, frpcSecret));
|
||||
const result = await capture(config, target.route, ["sh"], applyScript(confirmedYaml, n8n, target, secretMaterial, frpcSecret));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
const caddy = await applyPk01CaddyBlock(config, serviceName, target.publicExposure);
|
||||
return {
|
||||
@@ -335,7 +335,7 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Reco
|
||||
async function status(config: UniDeskConfig, options: CommonOptions): Promise<Record<string, unknown>> {
|
||||
const n8n = readN8nConfig();
|
||||
const target = resolveTarget(n8n, options.targetId);
|
||||
const result = await capture(config, target.route, ["script"], statusScript(n8n, target));
|
||||
const result = await capture(config, target.route, ["sh"], statusScript(n8n, target));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
||||
@@ -350,7 +350,7 @@ async function status(config: UniDeskConfig, options: CommonOptions): Promise<Re
|
||||
async function logs(config: UniDeskConfig, options: LogsOptions): Promise<Record<string, unknown>> {
|
||||
const n8n = readN8nConfig();
|
||||
const target = resolveTarget(n8n, options.targetId);
|
||||
const result = await capture(config, target.route, ["script"], logsScript(target, options));
|
||||
const result = await capture(config, target.route, ["sh"], logsScript(target, options));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
||||
@@ -367,7 +367,7 @@ async function logs(config: UniDeskConfig, options: LogsOptions): Promise<Record
|
||||
async function validate(config: UniDeskConfig, options: CommonOptions): Promise<Record<string, unknown>> {
|
||||
const n8n = readN8nConfig();
|
||||
const target = resolveTarget(n8n, options.targetId);
|
||||
const k8s = await capture(config, target.route, ["script"], validateScript(target));
|
||||
const k8s = await capture(config, target.route, ["sh"], validateScript(target));
|
||||
const k8sParsed = parseJsonOutput(k8s.stdout);
|
||||
const publicProbe = publicHttpProbe(target.publicExposure.publicBaseUrl, "/");
|
||||
return {
|
||||
|
||||
@@ -655,7 +655,7 @@ print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
sys.exit(0 if payload["ok"] else 1)
|
||||
PY
|
||||
`;
|
||||
const result = await capture(config, params.targetRoute, ["script"], script);
|
||||
const result = await capture(config, params.targetRoute, ["sh"], script);
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && parsed?.ok === true,
|
||||
|
||||
@@ -1058,7 +1058,7 @@ async function codexPoolValidate(config: UniDeskConfig, options: DisclosureOptio
|
||||
async function codexPoolTrace(config: UniDeskConfig, options: TraceOptions): Promise<Record<string, unknown> | RenderedCliResult> {
|
||||
const pool = readCodexPoolConfig();
|
||||
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
|
||||
const result = await capture(config, runtimeTarget.route, ["script"], traceScript(pool, options, runtimeTarget));
|
||||
const result = await capture(config, runtimeTarget.route, ["sh"], traceScript(pool, options, runtimeTarget));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
const ok = result.exitCode === 0 && boolField(parsed, "ok", false);
|
||||
if (options.raw) {
|
||||
@@ -1081,7 +1081,7 @@ async function codexPoolTrace(config: UniDeskConfig, options: TraceOptions): Pro
|
||||
async function codexPoolSentinelReport(config: UniDeskConfig, options: SentinelReportOptions): Promise<Record<string, unknown> | RenderedCliResult> {
|
||||
const pool = readCodexPoolConfig();
|
||||
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
|
||||
const result = await capture(config, runtimeTarget.route, ["script"], sentinelReportScript(pool, options.events, runtimeTarget));
|
||||
const result = await capture(config, runtimeTarget.route, ["sh"], sentinelReportScript(pool, options.events, runtimeTarget));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
const ok = result.exitCode === 0 && boolField(parsed, "ok", false);
|
||||
if (options.raw) {
|
||||
@@ -1177,7 +1177,7 @@ async function codexPoolCleanupProbes(config: UniDeskConfig, options: ConfirmOpt
|
||||
};
|
||||
}
|
||||
const pool = readCodexPoolConfig();
|
||||
const result = await capture(config, runtimeTarget.route, ["script"], cleanupProbesScript(pool, runtimeTarget));
|
||||
const result = await capture(config, runtimeTarget.route, ["sh"], cleanupProbesScript(pool, runtimeTarget));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
if (options.raw) {
|
||||
return {
|
||||
@@ -1221,7 +1221,7 @@ async function codexPoolExpose(config: UniDeskConfig, options: ConfirmOptions):
|
||||
}
|
||||
const secretMaterial = prepareTargetPublicExposureSecret(runtimeTarget);
|
||||
const caddyResult = await applyPk01CaddyBlock(config, serviceName, runtimeTarget.publicExposure);
|
||||
const remoteResult = await capture(config, runtimeTarget.route, ["script"], targetPublicExposureApplyScript(runtimeTarget, secretMaterial));
|
||||
const remoteResult = await capture(config, runtimeTarget.route, ["sh"], targetPublicExposureApplyScript(runtimeTarget, secretMaterial));
|
||||
const parsed = parseJsonOutput(remoteResult.stdout);
|
||||
const publicProbe = await probePublicModels(pool, "without-api-key", undefined, runtimeTarget);
|
||||
const ok = caddyResult.ok === true && remoteResult.exitCode === 0 && boolField(parsed, "ok", false) && publicProbe.httpStatus === 401;
|
||||
@@ -3420,7 +3420,7 @@ PY
|
||||
}
|
||||
|
||||
async function fetchPoolApiKey(config: UniDeskConfig, pool: CodexPoolConfig, target = codexPoolRuntimeTarget()): Promise<{ apiKey: string | null; error: string | null }> {
|
||||
const result = await capture(config, target.route, ["script"], `
|
||||
const result = await capture(config, target.route, ["sh"], `
|
||||
set -u
|
||||
kubectl -n ${target.namespace} get secret ${pool.apiKeySecretName} -o json
|
||||
`);
|
||||
@@ -7237,14 +7237,14 @@ type RemoteCodexPoolMode = "sync" | "validate" | "sentinel-probe" | "sentinel-im
|
||||
async function runRemoteCodexPoolScript(config: UniDeskConfig, mode: RemoteCodexPoolMode, script: string, target = codexPoolRuntimeTarget()): Promise<SshCaptureResult> {
|
||||
const jobName = `codex-pool-${mode}-${Date.now().toString(36)}`.slice(0, 63);
|
||||
const startedAtMs = Date.now();
|
||||
const start = await capture(config, target.route, ["script"], remoteJobStartScript(jobName, script));
|
||||
const start = await capture(config, target.route, ["sh"], remoteJobStartScript(jobName, script));
|
||||
const started = parseJsonOutput(start.stdout);
|
||||
if (start.exitCode !== 0 || boolField(started, "ok", false) !== true) return start;
|
||||
|
||||
let latest: RemoteCodexPoolJobStatus | null = null;
|
||||
while (Date.now() - startedAtMs <= remoteJobTimeoutMs) {
|
||||
await sleep(remoteJobPollMs);
|
||||
const probe = await capture(config, target.route, ["script"], remoteJobStatusScript(jobName));
|
||||
const probe = await capture(config, target.route, ["sh"], remoteJobStatusScript(jobName));
|
||||
const parsed = parseJsonOutput(probe.stdout);
|
||||
latest = normalizeRemoteJobStatus(parsed);
|
||||
process.stderr.write(`${JSON.stringify({
|
||||
|
||||
@@ -529,7 +529,7 @@ async function collectorImageBuild(config: UniDeskConfig, options: OpsApplyOptio
|
||||
source: collectorSourceSummary(archive),
|
||||
};
|
||||
}
|
||||
const result = await capture(config, archive.personalWechatIngress.target.hostRoute, ["script"], collectorImageBuildScript(archive));
|
||||
const result = await capture(config, archive.personalWechatIngress.target.hostRoute, ["sh"], collectorImageBuildScript(archive));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
||||
@@ -546,7 +546,7 @@ async function collectorImageBuild(config: UniDeskConfig, options: OpsApplyOptio
|
||||
async function collectorImageStatus(config: UniDeskConfig, options: OpsCommonOptions): Promise<Record<string, unknown>> {
|
||||
const archive = readConfig();
|
||||
assertTarget(archive, options.targetId);
|
||||
const result = await capture(config, archive.personalWechatIngress.target.hostRoute, ["script"], collectorImageStatusScript(archive));
|
||||
const result = await capture(config, archive.personalWechatIngress.target.hostRoute, ["sh"], collectorImageStatusScript(archive));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && parsed !== null && parsed.ok === true,
|
||||
@@ -580,7 +580,7 @@ async function collectorApply(config: UniDeskConfig, options: OpsApplyOptions):
|
||||
};
|
||||
}
|
||||
if (options.dryRun) {
|
||||
const result = await capture(config, archive.personalWechatIngress.target.kubeRoute, ["script"], collectorApplyScript(archive, manifest, null, true));
|
||||
const result = await capture(config, archive.personalWechatIngress.target.kubeRoute, ["sh"], collectorApplyScript(archive, manifest, null, true));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
||||
@@ -595,7 +595,7 @@ async function collectorApply(config: UniDeskConfig, options: OpsApplyOptions):
|
||||
};
|
||||
}
|
||||
const token = readArchiveCallbackToken(archive);
|
||||
const result = await capture(config, archive.personalWechatIngress.target.kubeRoute, ["script"], collectorApplyScript(archive, manifest, token.value, false));
|
||||
const result = await capture(config, archive.personalWechatIngress.target.kubeRoute, ["sh"], collectorApplyScript(archive, manifest, token.value, false));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
||||
@@ -624,7 +624,7 @@ async function collectorApply(config: UniDeskConfig, options: OpsApplyOptions):
|
||||
async function collectorStatus(config: UniDeskConfig, options: OpsCommonOptions): Promise<Record<string, unknown>> {
|
||||
const archive = readConfig();
|
||||
assertTarget(archive, options.targetId);
|
||||
const result = await capture(config, archive.personalWechatIngress.target.kubeRoute, ["script"], collectorStatusScript(archive, options.full));
|
||||
const result = await capture(config, archive.personalWechatIngress.target.kubeRoute, ["sh"], collectorStatusScript(archive, options.full));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && parsed !== null && parsed.ok === true,
|
||||
|
||||
@@ -1731,7 +1731,7 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Reco
|
||||
};
|
||||
}
|
||||
if (options.dryRun) {
|
||||
const result = await capture(config, target.route, ["script"], dryRunScript(yaml, target));
|
||||
const result = await capture(config, target.route, ["sh"], dryRunScript(yaml, target));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
||||
@@ -1749,7 +1749,7 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Reco
|
||||
const secretMaterial = target.databaseMode === "external-active" ? prepareExternalActiveSecret(sub2api) : null;
|
||||
const publicExposureSecretMaterial = preparePublicExposureSecret(sub2api, target);
|
||||
const egressProxySecretMaterial = prepareEgressProxySecret(sub2api, target);
|
||||
const result = await capture(config, target.route, ["script"], applyScript(sub2api, yaml, target, secretMaterial, publicExposureSecretMaterial, egressProxySecretMaterial));
|
||||
const result = await capture(config, target.route, ["sh"], applyScript(sub2api, yaml, target, secretMaterial, publicExposureSecretMaterial, egressProxySecretMaterial));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
const pk01Exposure = publicExposureSecretMaterial === null ? null : await applyPk01PublicExposure(config, target);
|
||||
return {
|
||||
@@ -1774,7 +1774,7 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Reco
|
||||
async function status(config: UniDeskConfig, options: DisclosureOptions): Promise<Record<string, unknown>> {
|
||||
const sub2api = readSub2ApiConfig();
|
||||
const target = resolveTarget(sub2api, options.targetId);
|
||||
const result = await capture(config, target.route, ["script"], statusScript(sub2api, target));
|
||||
const result = await capture(config, target.route, ["sh"], statusScript(sub2api, target));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
if (options.raw) {
|
||||
return {
|
||||
@@ -1810,7 +1810,7 @@ async function validate(config: UniDeskConfig, options: DisclosureOptions): Prom
|
||||
: target.databaseMode === "external-active"
|
||||
? validateExternalActiveScript(sub2api, target)
|
||||
: validateScript(sub2api, target);
|
||||
const result = await capture(config, target.route, ["script"], script);
|
||||
const result = await capture(config, target.route, ["sh"], script);
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
if (options.raw) {
|
||||
return {
|
||||
@@ -2333,7 +2333,7 @@ payload = {
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
PY
|
||||
`;
|
||||
const result = await capture(config, exposure.pk01.route, ["script"], script);
|
||||
const result = await capture(config, exposure.pk01.route, ["sh"], script);
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
||||
@@ -2403,7 +2403,7 @@ download_cache="$HOME/.cache/unidesk/platform-infra/caddy-linux-amd64"
|
||||
if [ -f "$download_part" ]; then stat -c 'part_bytes=%s part_mtime=%y' "$download_part"; fi
|
||||
if [ -f "$download_cache" ]; then stat -c 'cache_bytes=%s cache_mtime=%y' "$download_cache"; fi
|
||||
`;
|
||||
const result = await capture(config, exposure.pk01.route, ["script"], script);
|
||||
const result = await capture(config, exposure.pk01.route, ["sh"], script);
|
||||
const [stateText, rest = ""] = result.stdout.split("\n---STDOUT---\n");
|
||||
const [stdoutAndMaybeStderr = "", downloadProgress = ""] = rest.split("\n---DOWNLOAD---\n");
|
||||
const [stdoutTail = "", stderrTail = ""] = stdoutAndMaybeStderr.split("\n---STDERR---\n");
|
||||
|
||||
@@ -492,7 +492,7 @@ function selectedTargets(config: SecretDistributionConfig, options: SecretsOptio
|
||||
async function applyTargetSecrets(config: UniDeskConfig, target: DistributionTarget, secrets: DesiredSecret[], options: SecretsOptions): Promise<Record<string, unknown>> {
|
||||
if (secrets.length === 0) return { ok: true, target: targetSummary(target), mode: "skipped-no-secrets" };
|
||||
const yaml = renderSecretManifest(target, secrets);
|
||||
const result = await capture(config, target.route, ["script"], applySecretScript(target, secrets, yaml));
|
||||
const result = await capture(config, target.route, ["sh"], applySecretScript(target, secrets, yaml));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
||||
@@ -504,7 +504,7 @@ async function applyTargetSecrets(config: UniDeskConfig, target: DistributionTar
|
||||
}
|
||||
|
||||
async function statusTargetSecrets(config: UniDeskConfig, target: DistributionTarget, secrets: DesiredSecret[], options: SecretsOptions): Promise<Record<string, unknown>> {
|
||||
const result = await capture(config, target.route, ["script"], statusSecretScript(target, secrets));
|
||||
const result = await capture(config, target.route, ["sh"], statusSecretScript(target, secrets));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
||||
|
||||
+72
-103
@@ -187,6 +187,9 @@ const legacyK3sOperationRouteSegments = new Set([
|
||||
"kubectl",
|
||||
"exec",
|
||||
"script",
|
||||
"shell",
|
||||
"sh",
|
||||
"bash",
|
||||
"apply-patch",
|
||||
"apply-patch-v1",
|
||||
"patch",
|
||||
@@ -945,11 +948,11 @@ export function parseSshArgs(args: string[]): ParsedSshArgs {
|
||||
if (subcommand === "playwright") {
|
||||
return { remoteCommand: null, requiresStdin: true, invocationKind: "helper" };
|
||||
}
|
||||
if (subcommand === "script" || subcommand === "sh") {
|
||||
return buildShellCommand(args.slice(1));
|
||||
if (subcommand === "script" || subcommand === "shell") {
|
||||
throw removedShellAliasError(subcommand);
|
||||
}
|
||||
if (subcommand === "shell") {
|
||||
return buildShellStringCommand(args.slice(1));
|
||||
if (subcommand === "sh" || subcommand === "bash") {
|
||||
return buildShellCommand(args.slice(1), subcommand, `ssh ${subcommand}`);
|
||||
}
|
||||
if (subcommand === "argv" || subcommand === "exec") {
|
||||
const toolArgs = args.slice(1);
|
||||
@@ -1003,7 +1006,7 @@ export function sshRouteSeparatorCompatibilityHint(rawArgs: string[], normalized
|
||||
if (rawArgs === normalizedArgs || rawArgs[0] !== "--") return "";
|
||||
const operation = normalizedArgs[0] ?? "";
|
||||
const operationText = operation.length === 0 ? "operation" : `operation \`${operation}\``;
|
||||
return `UNIDESK_SSH_HINT route-level -- is ignored before ${operationText}; canonical form is \`trans <route> ${normalizedArgs.join(" ")}\`. Keep -- only inside operations such as \`script -- <command>\` or \`exec -- <command>\`.\n`;
|
||||
return `UNIDESK_SSH_HINT route-level -- is ignored before ${operationText}; canonical form is \`trans <route> ${normalizedArgs.join(" ")}\`. Keep -- only inside operations such as \`sh -- <command>\`, \`bash -- <command>\`, or \`exec -- <command>\`.\n`;
|
||||
}
|
||||
|
||||
function validateDirectArgvCommand(commandName: string, toolArgs: string[]): void {
|
||||
@@ -1012,7 +1015,7 @@ function validateDirectArgvCommand(commandName: string, toolArgs: string[]): voi
|
||||
if (!looksLikeShellCommandString(command)) return;
|
||||
throw new Error(
|
||||
`ssh ${commandName} received one shell-like command string; ${commandName} executes a single process and treats that string as the executable path. ` +
|
||||
`Use \`trans <route> script -- ${shellQuote(command)}\` for one-line shell logic, or split argv tokens, for example \`trans <route> ${commandName} ls -la\`.`
|
||||
`Use \`trans <route> sh -- ${shellQuote(command)}\` or \`trans <route> bash -- ${shellQuote(command)}\` for one-line shell logic, or split argv tokens, for example \`trans <route> ${commandName} ls -la\`.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1533,6 +1536,9 @@ function k3sOperationInRouteMessage(target: string, operation: string): string {
|
||||
if (operation === "v2" || operation === "patch" || operation === "patch-v1") {
|
||||
return `ssh k3s route must locate a target only; remote patch entrypoints are "apply-patch" for the default v2 engine and "apply-patch-v1" for the legacy helper instead of "${target}"`;
|
||||
}
|
||||
if (operation === "script" || operation === "shell") {
|
||||
return `ssh k3s route must locate a target only; operation "${operation}" has been removed because it hides the shell dialect; use explicit "sh" or "bash" after the route instead of "${target}"`;
|
||||
}
|
||||
const operationExample = operation === "guard" ? "guard" : `${operation} ...`;
|
||||
return `ssh k3s route must locate a target only; put operation "${operation}" after the route, for example "ssh ${providerId}:k3s ${operationExample}" or "ssh ${providerId}:k3s:<namespace>:<workload> ${operationExample}" instead of "${target}"`;
|
||||
}
|
||||
@@ -1662,12 +1668,11 @@ function parseK3sControlPlaneOperation(route: ParsedSshRoute, args: string[]): P
|
||||
if (operation === "patch" || operation === "patch-v1" || operation === "v2") {
|
||||
throw new Error("remote patch entrypoints are `apply-patch` for the default v2 engine and `apply-patch-v1` for the legacy helper");
|
||||
}
|
||||
if (operation === "script" || operation === "sh") {
|
||||
return buildK3sScriptOperation(args.slice(1));
|
||||
if (operation === "script" || operation === "shell") {
|
||||
throw removedShellAliasError(operation);
|
||||
}
|
||||
if (operation === "shell") {
|
||||
const parsed = parseShellStringOperationArgs(args.slice(1), `ssh ${route.providerId}:k3s shell`);
|
||||
return { remoteCommand: shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, parsed.shell, "-c", shellScriptWithCompatibility(parsed.command)]), requiresStdin: false, invocationKind: "helper" };
|
||||
if (operation === "sh" || operation === "bash") {
|
||||
return buildK3sScriptOperation(args.slice(1), operation, `ssh ${route.providerId}:k3s ${operation}`);
|
||||
}
|
||||
if (operation === "guard") {
|
||||
if (args.length > 1) throw new Error(`ssh ${route.providerId}:k3s guard does not accept extra arguments`);
|
||||
@@ -1700,10 +1705,11 @@ function parseK3sTargetOperation(route: ParsedSshRoute, args: string[]): ParsedS
|
||||
if (operation === "patch" || operation === "patch-v1" || operation === "v2") {
|
||||
throw new Error("remote patch entrypoints are `apply-patch` for the default v2 engine and `apply-patch-v1` for the legacy helper");
|
||||
}
|
||||
if (operation === "script") return buildK3sScriptOperation([...targetArgs, ...operationArgs]);
|
||||
if (operation === "shell") {
|
||||
const parsed = parseShellStringOperationArgs(operationArgs, `ssh ${route.raw} shell`);
|
||||
return { remoteCommand: buildK3sExecCommand([...targetArgs, "--", parsed.shell, "-c", shellScriptWithCompatibility(parsed.command)]), requiresStdin: false, invocationKind: "helper" };
|
||||
if (operation === "script" || operation === "shell") {
|
||||
throw removedShellAliasError(operation);
|
||||
}
|
||||
if (operation === "sh" || operation === "bash") {
|
||||
return buildK3sScriptOperation([...targetArgs, ...operationArgs], operation, `ssh ${route.raw} ${operation}`);
|
||||
}
|
||||
if (operation === "logs") return { remoteCommand: buildK3sLogsCommand([...targetArgs, ...operationArgs]), requiresStdin: false, invocationKind: "helper" };
|
||||
if (operation === "argv") {
|
||||
@@ -1832,9 +1838,12 @@ function buildK3sCommand(providerId: string, args: string[]): string {
|
||||
}
|
||||
if (action === "guard") return buildK3sGuardCommand(providerId);
|
||||
if (action === "exec") return buildK3sExecCommand(args.slice(1));
|
||||
if (action === "script") {
|
||||
const parsed = buildK3sScriptOperation(args.slice(1));
|
||||
if (parsed.remoteCommand === null) throw new Error("ssh k3s script resolved to an interactive command unexpectedly");
|
||||
if (action === "script" || action === "shell") {
|
||||
throw removedShellAliasError(action);
|
||||
}
|
||||
if (action === "sh" || action === "bash") {
|
||||
const parsed = buildK3sScriptOperation(args.slice(1), action, `ssh k3s ${action}`);
|
||||
if (parsed.remoteCommand === null) throw new Error(`ssh k3s ${action} resolved to an interactive command unexpectedly`);
|
||||
return parsed.remoteCommand;
|
||||
}
|
||||
if (action === "logs") return buildK3sLogsCommand(args.slice(1));
|
||||
@@ -1911,21 +1920,24 @@ function buildK3sExecCommand(args: string[]): string {
|
||||
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]);
|
||||
}
|
||||
|
||||
function buildK3sScriptOperation(args: string[]): ParsedSshArgs {
|
||||
const parsed = parseK3sTargetOptions(args, "ssh k3s script", { requireCommand: false, allowCommand: true, allowShell: true });
|
||||
if (parsed.shell === null && parsed.command.length > 0) {
|
||||
return { remoteCommand: buildK3sInlineScriptCommand(parsed), requiresStdin: false, invocationKind: "helper" };
|
||||
}
|
||||
return { remoteCommand: buildK3sStdinScriptCommand(parsed), requiresStdin: true, invocationKind: "helper", stdinPrefix: shellScriptStdinPrefix() };
|
||||
function removedShellAliasError(operation: string): Error {
|
||||
return new Error(`ssh ${operation} operation has been removed because it hides the shell dialect; use explicit \`sh\` for POSIX /bin/sh or \`bash\` for Bash syntax`);
|
||||
}
|
||||
|
||||
function buildK3sStdinScriptCommand(parsed: K3sTargetOptions): string {
|
||||
if (parsed.namespace === null && parsed.resource === null) return buildK3sHostScriptCommand(parsed);
|
||||
if (parsed.namespace === null) throw new Error("ssh k3s script target requires --namespace <name>");
|
||||
if (parsed.selector !== null) throw new Error("ssh k3s script does not support --selector");
|
||||
if (parsed.resource === null) throw new Error("ssh k3s script target requires --deployment <name>, --pod <name> or --resource <type/name>");
|
||||
if (parsed.tty) throw new Error("ssh k3s script does not support --tty; stdin is reserved for the script body");
|
||||
const shell = parsed.shell ?? "sh";
|
||||
function buildK3sScriptOperation(args: string[], shell: "sh" | "bash", commandName: string): ParsedSshArgs {
|
||||
const parsed = parseK3sTargetOptions(args, commandName, { requireCommand: false, allowCommand: true });
|
||||
if (parsed.command.length > 0) {
|
||||
return { remoteCommand: buildK3sInlineScriptCommand(parsed, shell, commandName), requiresStdin: false, invocationKind: "helper" };
|
||||
}
|
||||
return { remoteCommand: buildK3sStdinScriptCommand(parsed, shell, commandName), requiresStdin: true, invocationKind: "helper", stdinPrefix: shellScriptStdinPrefix() };
|
||||
}
|
||||
|
||||
function buildK3sStdinScriptCommand(parsed: K3sTargetOptions, shell: "sh" | "bash", commandName: string): string {
|
||||
if (parsed.namespace === null && parsed.resource === null) return buildK3sHostScriptCommand(parsed, shell, commandName);
|
||||
if (parsed.namespace === null) throw new Error(`${commandName} target requires --namespace <name>`);
|
||||
if (parsed.selector !== null) throw new Error(`${commandName} does not support --selector`);
|
||||
if (parsed.resource === null) throw new Error(`${commandName} target requires --deployment <name>, --pod <name> or --resource <type/name>`);
|
||||
if (parsed.tty) throw new Error(`${commandName} does not support --tty; stdin is reserved for the shell body`);
|
||||
const kubectlArgs = [
|
||||
"exec",
|
||||
"-i",
|
||||
@@ -1939,20 +1951,21 @@ function buildK3sStdinScriptCommand(parsed: K3sTargetOptions): string {
|
||||
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]);
|
||||
}
|
||||
|
||||
function buildK3sInlineScriptCommand(parsed: K3sTargetOptions): string {
|
||||
if (parsed.command.length === 0) throw new Error("ssh k3s script -- requires a command");
|
||||
if (parsed.selector !== null) throw new Error("ssh k3s script -- does not support --selector");
|
||||
if (parsed.tty) throw new Error("ssh k3s script does not support --tty; stdin is reserved for the script body");
|
||||
if (parsed.stdin) throw new Error("ssh k3s script -- does not accept --stdin");
|
||||
const command = parsed.command.length === 1 ? ["sh", "-c", shellScriptWithCompatibility(parsed.command[0] ?? "")] : parsed.command;
|
||||
function buildK3sInlineScriptCommand(parsed: K3sTargetOptions, shell: "sh" | "bash", commandName: string): string {
|
||||
if (parsed.command.length === 0) throw new Error(`${commandName} -- requires a command`);
|
||||
if (parsed.command.length !== 1) throw new Error(`${commandName} -- requires exactly one shell command string; use exec/argv for direct argv commands`);
|
||||
if (parsed.selector !== null) throw new Error(`${commandName} -- does not support --selector`);
|
||||
if (parsed.tty) throw new Error(`${commandName} does not support --tty; stdin is reserved for the shell body`);
|
||||
if (parsed.stdin) throw new Error(`${commandName} -- does not accept --stdin`);
|
||||
const command = [shell, "-c", shellScriptWithCompatibility(parsed.command[0] ?? "")];
|
||||
if (parsed.namespace === null && parsed.resource === null) {
|
||||
if (parsed.container !== null) throw new Error("ssh k3s script without a workload does not accept --container");
|
||||
if (parsed.workspace !== null) throw new Error("ssh k3s script without a workload does not accept --workdir");
|
||||
if (parsed.kubectlOptions.length > 0) throw new Error("ssh k3s script without a workload does not accept kubectl log options");
|
||||
if (parsed.container !== null) throw new Error(`${commandName} without a workload does not accept --container`);
|
||||
if (parsed.workspace !== null) throw new Error(`${commandName} without a workload does not accept --workdir`);
|
||||
if (parsed.kubectlOptions.length > 0) throw new Error(`${commandName} without a workload does not accept kubectl log options`);
|
||||
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, ...command]);
|
||||
}
|
||||
if (parsed.namespace === null) throw new Error("ssh k3s script target requires --namespace <name>");
|
||||
if (parsed.resource === null) throw new Error("ssh k3s script target requires --deployment <name>, --pod <name> or --resource <type/name>");
|
||||
if (parsed.namespace === null) throw new Error(`${commandName} target requires --namespace <name>`);
|
||||
if (parsed.resource === null) throw new Error(`${commandName} target requires --deployment <name>, --pod <name> or --resource <type/name>`);
|
||||
const kubectlArgs = [
|
||||
"exec",
|
||||
"-n", parsed.namespace,
|
||||
@@ -1992,36 +2005,15 @@ function buildK3sApplyPatchCommand(args: string[]): ParsedSshArgs {
|
||||
};
|
||||
}
|
||||
|
||||
function buildK3sHostScriptCommand(parsed: K3sTargetOptions): string {
|
||||
if (parsed.tty) throw new Error("ssh k3s script does not support --tty; stdin is reserved for the script body");
|
||||
if (parsed.stdin) throw new Error("ssh k3s script does not accept --stdin; stdin is always the script body");
|
||||
if (parsed.container !== null) throw new Error("ssh k3s script without a workload does not accept --container");
|
||||
if (parsed.workspace !== null) throw new Error("ssh k3s script without a workload does not accept --workdir");
|
||||
if (parsed.kubectlOptions.length > 0) throw new Error("ssh k3s script without a workload does not accept kubectl log options");
|
||||
const shell = parsed.shell ?? "sh";
|
||||
function buildK3sHostScriptCommand(parsed: K3sTargetOptions, shell: "sh" | "bash", commandName: string): string {
|
||||
if (parsed.tty) throw new Error(`${commandName} does not support --tty; stdin is reserved for the shell body`);
|
||||
if (parsed.stdin) throw new Error(`${commandName} does not accept --stdin; stdin is always the shell body`);
|
||||
if (parsed.container !== null) throw new Error(`${commandName} without a workload does not accept --container`);
|
||||
if (parsed.workspace !== null) throw new Error(`${commandName} without a workload does not accept --workdir`);
|
||||
if (parsed.kubectlOptions.length > 0) throw new Error(`${commandName} without a workload does not accept kubectl log options`);
|
||||
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, shell, "-s", "--", ...parsed.command]);
|
||||
}
|
||||
|
||||
function shellStringFromArgs(args: string[], commandName = "ssh shell"): string {
|
||||
if (args.length === 0) throw new Error(`${commandName} requires a command string`);
|
||||
return args.join(" ");
|
||||
}
|
||||
|
||||
function parseShellStringOperationArgs(args: string[], commandName: string): { shell: string; command: string } {
|
||||
let shell = "sh";
|
||||
const commandArgs: string[] = [];
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index] ?? "";
|
||||
if (arg === "--shell") {
|
||||
shell = k3sScriptShell(k3sOptionValue(args, index, `${commandName} --shell`), `${commandName} --shell`);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
commandArgs.push(arg);
|
||||
}
|
||||
return { shell, command: shellStringFromArgs(commandArgs, commandName) };
|
||||
}
|
||||
|
||||
function buildK3sLogsCommand(args: string[]): string {
|
||||
const parsed = parseK3sTargetOptions(args, "ssh k3s logs", { requireCommand: false, allowSelector: true });
|
||||
if (parsed.namespace === null) throw new Error("ssh k3s logs requires --namespace <name>");
|
||||
@@ -2264,49 +2256,26 @@ function shellScriptStdinPrefix(): string {
|
||||
return `${shellScriptPrelude()}\n`;
|
||||
}
|
||||
|
||||
function buildShellCommand(args: string[]): ParsedSshArgs {
|
||||
let shell = "sh";
|
||||
function buildShellCommand(args: string[], shell: "sh" | "bash", commandName: string): ParsedSshArgs {
|
||||
const scriptArgs: string[] = [];
|
||||
let afterDoubleDash = false;
|
||||
let directArgvMode = false;
|
||||
let shellOptionSeen = false;
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index] ?? "";
|
||||
if (afterDoubleDash) {
|
||||
scriptArgs.push(arg);
|
||||
continue;
|
||||
}
|
||||
if (arg === "--") {
|
||||
if (!shellOptionSeen && scriptArgs.length === 0) {
|
||||
directArgvMode = true;
|
||||
scriptArgs.push(...args.slice(index + 1));
|
||||
break;
|
||||
if (scriptArgs.length === 0) {
|
||||
const command = args.slice(index + 1);
|
||||
if (command.length === 0) throw new Error(`${commandName} -- requires a command`);
|
||||
if (command.length !== 1) throw new Error(`${commandName} -- requires exactly one shell command string; use argv or a direct subcommand for direct argv commands`);
|
||||
return { remoteCommand: shellArgv([shell, "-c", shellScriptWithCompatibility(command[0] ?? "")]), requiresStdin: false, invocationKind: "helper" };
|
||||
}
|
||||
afterDoubleDash = true;
|
||||
continue;
|
||||
scriptArgs.push(...args.slice(index + 1));
|
||||
break;
|
||||
}
|
||||
if (arg === "--shell") {
|
||||
shell = k3sScriptShell(k3sOptionValue(args, index, "ssh script --shell"), "ssh script --shell");
|
||||
shellOptionSeen = true;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("-")) throw new Error(`unsupported ssh script option: ${arg}`);
|
||||
if (arg.startsWith("-")) throw new Error(`unsupported ${commandName} option: ${arg}`);
|
||||
scriptArgs.push(arg);
|
||||
}
|
||||
if (directArgvMode) {
|
||||
if (scriptArgs.length === 0) throw new Error("ssh script -- requires a command");
|
||||
if (scriptArgs.length === 1) return { remoteCommand: shellArgv([shell, "-c", shellScriptWithCompatibility(scriptArgs[0] ?? "")]), requiresStdin: false, invocationKind: "helper" };
|
||||
return { remoteCommand: shellArgv(scriptArgs), requiresStdin: false, invocationKind: "argv" };
|
||||
}
|
||||
return { remoteCommand: shellArgv([shell, "-s", "--", ...scriptArgs]), requiresStdin: true, invocationKind: "helper", stdinPrefix: shellScriptStdinPrefix() };
|
||||
}
|
||||
|
||||
function buildShellStringCommand(args: string[]): ParsedSshArgs {
|
||||
const parsed = parseShellStringOperationArgs(args, "ssh shell");
|
||||
return { remoteCommand: shellArgv([parsed.shell, "-c", shellScriptWithCompatibility(parsed.command)]), requiresStdin: false, invocationKind: "helper" };
|
||||
}
|
||||
|
||||
function podApplyPatchStdinWrapper(): { prefix: string; suffix: string } {
|
||||
const toolMarker = "__UNIDESK_APPLY_PATCH_TOOL__";
|
||||
const patchMarker = "__UNIDESK_APPLY_PATCH_PAYLOAD__";
|
||||
@@ -2399,8 +2368,8 @@ export function sshFailureHint(providerId: string, parsed: ParsedSshArgs, exitCo
|
||||
providerId: shownProviderId,
|
||||
trigger,
|
||||
exitCode,
|
||||
message: "ssh-like remote command failed before proving Host SSH is globally unavailable; prefer structured argv or stdin script passthrough for non-interactive commands.",
|
||||
try: `trans ${shownProviderId} script <<'SCRIPT'`,
|
||||
message: "ssh-like remote command failed before proving Host SSH is globally unavailable; prefer structured argv or explicit sh/bash stdin passthrough for non-interactive commands.",
|
||||
try: `trans ${shownProviderId} sh <<'SH'`,
|
||||
triage: `bun scripts/cli.ts provider triage ${shownProviderId} --observed-scope ssh --observed-error '<ssh-like timeout or kex failure>'`,
|
||||
note: "This hint intentionally does not echo the original remote command.",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user