fix: 收敛 trans 顶层帮助输出

This commit is contained in:
Codex
2026-07-11 20:33:39 +02:00
parent 53d1532805
commit bfb825b0a0
2 changed files with 252 additions and 132 deletions
+166 -89
View File
@@ -169,111 +169,188 @@ export function serverHelp(action: string | undefined = undefined): unknown {
};
}
export type SshHelpScope = "download";
export type SshHelpScope =
| "argv" | "exec" | "git" | "node" | "kubectl" | "logs"
| "sh" | "bash" | "py" | "ps" | "cmd"
| "pwd" | "ls" | "cat" | "head" | "tail" | "stat" | "wc" | "rg" | "find" | "glob" | "skills"
| "apply-patch" | "apply-patch-v1" | "upload" | "download";
type SshOperationHelp = {
group: "process" | "shell" | "filesystem" | "patch" | "transfer";
description: string;
usage: string[];
notes?: string[];
};
const SSH_OPERATION_HELP: Record<SshHelpScope, SshOperationHelp> = {
argv: {
group: "process",
description: "以原始 argv 边界运行一个远端进程;需要管道、变量或重定向时改用 sh/bash。",
usage: ["trans <route> argv <command> [args...]", "trans D601:/workspace argv git status --short --branch"],
},
exec: {
group: "process",
description: "在 k3s workload route 中运行进程,并可显式透传 stdin 或指定容器内 cwd。",
usage: ["trans <provider>:k3s:<namespace>:<workload>[:<container>] exec [--cwd /path] [--stdin] -- <command> [args...]"],
},
git: {
group: "process",
description: "在 host/workspace 或 Windows workspace route 中运行 Git convenience wrapper。",
usage: ["trans <provider>:/workspace git status --short --branch", "trans <provider>:win/<drive>/<path> git diff --check"],
},
node: {
group: "process",
description: "在目标 route 中直接运行 Node.js argv。",
usage: ["trans <route> node -e 'console.log(process.version)'"],
},
kubectl: {
group: "process",
description: "在 k3s control-plane route 上执行有界 kubectl 诊断。",
usage: ["trans <provider>:k3s kubectl get pods -n <namespace>"],
},
logs: {
group: "process",
description: "从 k3s control-plane 或 workload route 读取有界日志。",
usage: ["trans <provider>:k3s:<namespace>:<workload> logs --tail 80", "trans <provider>:k3s logs -n <namespace> -l <selector> --tail 120"],
},
sh: {
group: "shell",
description: "从 stdin 或单个命令字符串执行远端 POSIX /bin/sh。",
usage: ["trans <route> sh <<'SH'", "trans <route> sh -- '<command && command>'"],
notes: ["sh -- 只接受一个 shell command string;单进程多 argv 使用 argv。"],
},
bash: {
group: "shell",
description: "仅在需要 pipefail、数组、[[ ]] 等 Bash 语法时执行远端 Bash。",
usage: ["trans <route> bash <<'BASH'", "trans <route> bash -- '<bash command>'"],
},
py: {
group: "shell",
description: "从本地 stdin 执行远端 Python 源码,并保留 script argv。",
usage: ["trans <route> py [script-args...] < script.py"],
},
ps: {
group: "shell",
description: "在 Windows route 中执行 PowerShell 源码或 argvroute 已包含 win。",
usage: ["trans <provider>:win[/<drive>/<path>] ps <<'PS'", "trans <provider>:win ps '<PowerShell source>'"],
},
cmd: {
group: "shell",
description: "在 Windows route 中以 UTF-8 环境执行 cmd.exe/batch。",
usage: ["trans <provider>:win[/<drive>/<path>] cmd <<'CMD'", "trans <provider>:win cmd ver"],
},
pwd: { group: "filesystem", description: "读取 Windows workspace 当前目录。", usage: ["trans <provider>:win/<drive>/<path> pwd"] },
ls: { group: "filesystem", description: "有界列出 Windows workspace 文件。", usage: ["trans <provider>:win/<drive>/<path> ls --limit 50"] },
cat: { group: "filesystem", description: "读取远端文本文件;二进制文件使用 download。", usage: ["trans <route> cat <path>"] },
head: { group: "filesystem", description: "按行读取远端文本文件开头。", usage: ["trans <route> head -n 40 <path>"] },
tail: { group: "filesystem", description: "按行读取远端文本文件结尾。", usage: ["trans <route> tail -n 40 <path>"] },
stat: { group: "filesystem", description: "读取远端文件元数据。", usage: ["trans <route> stat <path>"] },
wc: { group: "filesystem", description: "统计远端文本的行、词、字符或字节。", usage: ["trans <route> wc [-l|-w|-m|-c] <path>"] },
rg: { group: "filesystem", description: "执行有界远端 ripgrepWindows route 使用受控 UTF-8 子集。", usage: ["trans <route> rg [options] <pattern> [path...]"] },
find: { group: "filesystem", description: "不依赖 shell quoting 的结构化远端 find。", usage: ["trans <route> find <path...> [--contains TEXT] [--limit N]"] },
glob: { group: "filesystem", description: "通过远端 helper 执行有界 glob 匹配。", usage: ["trans <route> glob [--root DIR] [--pattern PATTERN] [--limit N]"] },
skills: { group: "filesystem", description: "发现 WSL/Linux 及可选 Windows skill 目录。", usage: ["trans <provider> skills [--scope all|wsl|windows] [--limit N]"] },
"apply-patch": {
group: "patch",
description: "默认远端文本修改入口;本地 v2 engine 计算变更,route 仅负责读写。",
usage: ["trans <route> apply-patch [--cwd /path] < patch.diff"],
notes: ["失败后重读当前目标块并缩小 hunk;不得自动回退 apply-patch-v1 或整文件 upload。"],
},
"apply-patch-v1": {
group: "patch",
description: "显式调用 legacy remote apply_patch helper;只作为人工确认后的兼容入口。",
usage: ["trans <route> apply-patch-v1 [--allow-loose] < patch.diff"],
},
upload: {
group: "transfer",
description: "上传必要的二进制文件或生成物并自动核对字节数与 SHA-256。",
usage: ["trans <route> upload <local-file> <remote-file>"],
notes: ["远端文本读取使用 cat/rg,修改使用 apply-patch。"],
},
download: {
group: "transfer",
description: "下载必要的二进制文件或生成物并自动核对字节数与 SHA-256。",
usage: ["trans <route> download <remote-file> <local-file>"],
},
};
const SSH_OPERATION_GROUPS = {
process: ["argv", "exec", "git", "node", "kubectl", "logs"],
shell: ["sh", "bash", "py", "ps", "cmd"],
filesystem: ["pwd", "ls", "cat", "head", "tail", "stat", "wc", "rg", "find", "glob", "skills"],
patch: ["apply-patch", "apply-patch-v1"],
transfer: ["upload", "download"],
} as const satisfies Record<SshOperationHelp["group"], readonly SshHelpScope[]>;
const SSH_HELP_SCOPES = Object.keys(SSH_OPERATION_HELP) as SshHelpScope[];
const SSH_HELP_SCOPE_SET = new Set<string>(SSH_HELP_SCOPES);
function sshEntrypoint(): "trans" | "tran" | "ssh" {
const rawEntrypoint = process.env.UNIDESK_SSH_ENTRYPOINT?.trim();
return rawEntrypoint === "trans" || rawEntrypoint === "tran" || rawEntrypoint === "ssh" ? rawEntrypoint : "ssh";
}
export function sshHelpScope(args: string[]): SshHelpScope | undefined {
const [top, helpToken, scope] = args;
if (top !== "ssh" || args.length !== 3 || !isHelpToken(helpToken)) return undefined;
return scope === "download" ? scope : undefined;
if (top !== "ssh" || args.length !== 3 || !isHelpToken(helpToken) || !SSH_HELP_SCOPE_SET.has(scope ?? "")) return undefined;
return scope as SshHelpScope;
}
export function sshHelp(scope: SshHelpScope | undefined = undefined): unknown {
if (scope === "download") return sshDownloadHelp();
if (scope !== undefined) return sshOperationHelp(scope);
const entrypoint = sshEntrypoint();
const operationGroups = Object.fromEntries(
Object.entries(SSH_OPERATION_GROUPS).map(([group, operations]) => [group, operations.join(" | ")]),
);
return {
command: "ssh",
command: `${entrypoint} --help`,
output: "json",
description: "Open a Host SSH / WSL SSH maintenance session through the provider-gateway bridge.",
usage: [
"trans <route>",
"trans <providerId> argv <command> [args...]",
"trans <providerId>:/absolute/workspace apply-patch < patch.diff",
"trans <route> upload <local-file> <remote-file>",
"trans <route> download <remote-file> <local-file>",
"trans <providerId> apply-patch-v1 [--allow-loose] < patch.diff",
"trans <providerId> py [script-args...] < script.py",
"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]",
"trans NC01:/root/hwlab-v03 git status --short --branch",
"trans D601:win ps <<'PS'",
"trans D601:win/c/test ps <<'PS'",
"trans D601:win/c/test cmd <<'CMD'",
"trans D601:win cmd ver",
"trans D601:win/c/test cmd cd",
"trans D601:win/c/test pwd",
"trans D601:win/c/test ls --limit 50",
"trans D601:win/c/test cat README.md",
"trans D601:win/c/test head -n 40 README.md",
"trans D601:win/c/test tail -n 40 README.md",
"trans D601:win/c/test stat README.md",
"trans D601:win/c/test wc README.md",
"trans D601:win/c/test rg -i needle .",
"trans D601:win/c/test git status --short --branch",
"trans D601:win/c/test git diff --check",
"trans D601:win/c/test git commit -m 'fix: update docs'",
"trans D601:win/c/test git exact-commit status",
"trans D601:win/c/test git exact-commit plan --path src/main.c",
"trans D601:win/c/test git exact-commit run --path src/main.c --message 'fix: exact change' --confirm",
"trans D601:win skills [--scope agents|codex|all] [--limit N]",
"trans NC01:k3s",
"trans NC01:k3s kubectl get pods -n hwlab-dev",
"trans NC01:k3s",
"trans NC01:k3s kubectl get pipelineruns -n hwlab-ci",
"trans NC01:k3s sh <<'SH'",
"trans NC01:k3s:hwlab-dev:hwlab-cloud-api:hwlab-cloud-api exec --cwd /app -- pwd",
"trans NC01:k3s:hwlab-dev:hwlab-cloud-api:hwlab-cloud-api apply-patch --cwd /app <<'PATCH'",
"trans NC01:k3s:hwlab-dev:hwlab-cloud-api apply-patch-v1 <<'PATCH'",
"tar -C /path/to/files -cf - . | trans NC01:k3s:unidesk:code-queue exec --cwd /root/unidesk --stdin -- tar -xf - -C /root/unidesk",
"trans D601:win/c/test apply-patch <<'PATCH'",
"trans D601:win upload ./tool.mjs C:\\Temp\\tool.mjs",
"trans D601:win download C:\\Temp\\tool.mjs ./tool.mjs",
"trans NC01:k3s:hwlab-dev:hwlab-cloud-api node -e 'console.log(process.version)'",
"trans NC01:k3s:hwlab-dev:hwlab-cloud-api sh <<'SH'",
"trans NC01:k3s:hwlab-dev:hwlab-cloud-api logs --tail 80",
"trans NC01: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 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.",
"On Windows routes, the route already contains `win`; write `trans D601:win/c/test ps`, not `trans D601:win/c/test win ps`. Repository commands can use the git convenience wrapper, including `git status`, `git diff`, and non-interactive `git commit -m ...`; commands that need shell review should use `ps` or `cmd`.",
"Windows routes include read-only filesystem convenience operations `pwd`, `ls`, `cat`, `head`, `tail`, `stat`, `wc`, and a bounded UTF-8 `rg` subset. These are implemented through a Windows fs backend with UTF-8/binary checks and bounded output; they do not imply POSIX `sh`/`bash` availability.",
"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 `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 NC01:/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` 和 `download` 只用于必要的二进制文件或生成物;远端文本应避免反复传输,读取使用 `cat`/`rg`,修改使用 `apply-patch`。成功传输的 JSON 会在 `hint` 中重复该提示,通过远端临时文件写入,自动核对两端字节数和 SHA-256,并返回 `verification.automatic=true`、`verification.verified=true` 和 `verification.match.{bytes,sha256}=true`;该 JSON 已是完整性证据,无需额外执行 `sha256sum`。下载通过 `host.ssh.tcp-pool` 流式传输并输出进度 JSON;受控异步产物任务可传入 `--inactivity-timeout-ms`,只要数据持续流动就不受普通短命令预算影响。Windows route 下载(例如 `trans D601:win download C:\\Temp\\tool.mjs ./tool.mjs`)会自动把盘符路径映射到同一 provider 的 WSL `/mnt/<drive>` host route,同时仍返回字节数和 SHA-256 校验证据。",
"`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.",
"`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`.",
"Do not put operation names in any colon route segment, including nested k3s namespace/workload/container segments.",
"Do not use post-provider shorthand such as `trans NC01 k3s ...`; write `trans NC01:k3s ...` so location and operation stay separated.",
"If an ssh-like remote command fails with timeout/kex/exit-255 friction, stderr includes one low-noise UNIDESK_SSH_HINT JSON line with the argv retry command.",
"Ordinary non-interactive ssh/trans/tran remote commands have a hard top-level runtime timeout capped at 60s. Timeout writes UNIDESK_SSH_RUNTIME_TIMEOUT or UNIDESK_TRAN_TIMEOUT_HINT and disconnects the broker; long CI/CD, trace, logs, build, or hardware work must use submit-and-poll / short query loops instead of keeping trans open. Whole-file `download` is the narrow exception: controlled async callers can pass `--inactivity-timeout-ms` for a verified progress-emitting tcp-pool transfer.",
"Only slow ssh/trans/tran runtime writes UNIDESK_SSH_TIMING JSON to stderr; operations over 10s are marked level=warning even when they succeed, because slow successful calls are a distributed performance monitoring signal. Check provider latency, remote command cost, helper bootstrap, or remote patch optimization before repeating high-frequency work. Routine short calls do not emit timing noise.",
"The local trans/tran wrapper must not add provider/plane directory locks; rely on k8s/Tekton/Argo/Lease or server-side TTL queues for coordination.",
"Use -- before a remote command that intentionally starts with a dash.",
description: "通过 provider-gateway bridge 访问 Host SSH、WSL、Windows 或 k3s 目标。",
routeSyntax: {
general: `${entrypoint} <route> <operation> [operation-args...]`,
hostWorkspace: "<provider>:/absolute/workspace",
k3sControlPlane: "<provider>:k3s",
k3sWorkload: "<provider>:k3s:<namespace>:<workload>[:<container>]",
windowsWorkspace: "<provider>:win[/<drive>/<path>]",
githubContent: "gh:/<owner>/<repo>[/issue|/pr]/<number>",
},
operationGroups,
scopedHelp: {
command: `${entrypoint} --help <operation>`,
availableOperationCount: SSH_HELP_SCOPES.length,
examples: [`${entrypoint} --help apply-patch`, `${entrypoint} --help download`, `${entrypoint} --help ps`],
},
boundaries: [
"route 只定位目标,route 后的 token 全部交给 operation parser。",
"单进程优先 argvshell 逻辑显式使用 sh/bashWindows 使用 ps/cmd。",
"远端文本读取优先 cat/rg,修改优先 apply-patchupload/download 仅用于必要的二进制或生成物。",
"普通 trans 短连接上限保持 60s;长流程使用受控 submit-and-poll 入口。",
],
};
}
function sshOperationHelp(scope: Exclude<SshHelpScope, "download">): unknown {
const entrypoint = sshEntrypoint();
const detail = SSH_OPERATION_HELP[scope];
return {
command: `${entrypoint} --help ${scope}`,
output: "json",
scope,
group: detail.group,
description: detail.description,
routeSyntax: `${entrypoint} <route> ${scope} [operation-args...]`,
usage: detail.usage.map((value) => value.replace(/^trans\b/u, entrypoint)),
notes: detail.notes ?? [],
next: [`${entrypoint} --help`, `${entrypoint} --help ${scope}`],
};
}
function sshDownloadHelp(): unknown {
const configuredRepoRoot = process.env.UNIDESK_TRANS_REPO_ROOT?.trim() || null;
const rawEntrypoint = process.env.UNIDESK_SSH_ENTRYPOINT?.trim();
const entrypoint = rawEntrypoint === "trans" || rawEntrypoint === "tran" || rawEntrypoint === "ssh"
? rawEntrypoint
: "ssh";
const entrypoint = sshEntrypoint();
return {
command: `${entrypoint} download`,
command: `${entrypoint} --help download`,
output: "json",
scope: "download",
description: "下载一个必要的二进制文件或生成物,并自动核对远端/本地字节数与 SHA-256;远端文本改用 cat/rg/apply-patch。",