fix: add windows powershell passthrough
This commit is contained in:
@@ -176,6 +176,7 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文
|
||||
## Critical Trans Shell Boundary Rule
|
||||
|
||||
- P0: `trans <route> ...` 后面禁止裸放本地 shell 续接控制符,包括 `&&`、`;` 和 `|`;需要在远端执行多步命令时,必须使用 `trans <route> script -- '远端完整脚本'`、`trans <route> script <<'SCRIPT'` 或等价的单一 stdin/script 参数,避免后半段被本地 shell 执行。`tran` 兼容入口遵守同一规则。`script -- '<单个字符串>'` 会按远端 shell one-liner 执行;`script -- <多个 argv>` 才是 direct argv。`apply-patch`、`apply-patch-v1`、`script`、`py` 等 stdin/capture-backed operation 可以使用 heredoc 或 `< patch.diff` 作为本地输入。
|
||||
- P0: Windows PowerShell 透传必须使用 `trans <provider>:win ps <<'PS' ... PS`;`script` 只表示 host/k3s POSIX shell,`cmd` 只表示 Windows cmd.exe/batch,禁止把 ps、cmd、shell 混写成多层 quoting。
|
||||
- P0: 新增或扩展 `ssh`/`trans`/`tran` 高频 operation 不得把完整实现继续堆进 `scripts/src/ssh.ts`;`ssh.ts` 只保留 route/parser/broker/dispatch,共享能力拆到 `scripts/src/ssh-*.ts` 专门模块,细则见 `docs/reference/cli.md`。
|
||||
|
||||
## Critical Apply Patch Syntax
|
||||
|
||||
+14
-2
@@ -32,7 +32,7 @@ CI/CD、GitOps、rollout、artifact 发布、PR 合并后的 DEV/PROD 滚动、P
|
||||
- `gc plan|run --confirm|db-trace|policy|remote` 是主 server 和受控 provider 的磁盘高水位一次性缓解与长期防膨胀入口。`plan` 只读输出候选、风险、估算收益和保护对象;`run` 必须显式 `--confirm`;`gc remote <providerId> ...` 通过 UniDesk SSH 透传执行远端 GC,`--target-use-percent N` 会在 `summary.target` 中报告目标水位所需释放量、候选估算、预计水位、缺口和 safe-stop 决策。G14/HWLAB registry retention、受限 core dump、保护对象、safe-stop 线和长期收益表的权威规则见 `docs/reference/gc.md`。
|
||||
- `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 迭代不得用 `server rebuild backend-core` 在 master server 编译,规则见 `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`。
|
||||
- `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 cmd 入口固定写 `trans D601:win cmd <command-line>`,需要 Windows cwd 时用 `trans D601:win/c/test cmd cd`,由 CLI 自动设置 `chcp 65001`、`PYTHONUTF8=1` 和 `PYTHONIOENCODING=utf-8`;命名只允许 `win`,不得使用 `win32`。非交互远端命令优先使用 `trans <providerId> argv ...`;需要 shell 脚本、管道、变量或循环时优先使用 quoted heredoc 单步传输,例如 `trans G14 script <<'SCRIPT'`、`trans G14:k3s script <<'SCRIPT'` 或 `trans G14:k3s:<namespace>:<workload> script <<'SCRIPT'`,把脚本走 stdin。`script -- '<单个字符串>'` 是无需 stdin 的远端 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 脚本、管道、变量或循环时优先使用 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> 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。
|
||||
- `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`。
|
||||
@@ -373,7 +373,16 @@ trans D601 glob --root /home/ubuntu/pikapython --pattern '**/*-test.cpp' --limit
|
||||
|
||||
`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` 失败或成功证明。
|
||||
|
||||
当前稳定 plane 包括 `win` 和 `k3s`。`<provider>:win cmd <command-line>` 在 WSL provider 上启动 Windows host 的 `cmd.exe`,CLI 会在命令前固定执行 `chcp 65001>nul`、`set "PYTHONUTF8=1"` 和 `set "PYTHONIOENCODING=utf-8"`,让中文和 UTF-8 输出成为默认行为;需要 Windows 当前目录时使用 slash 路由 `<provider>:win/<drive>/<path>`,例如 `D601:win/c/test cmd cd` 会先在 Windows cmd 内执行 `cd /d "C:\test"`。`win32` 不是合法 plane,调用者必须改用 `win`。
|
||||
当前稳定 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`。典型用法:
|
||||
|
||||
```bash
|
||||
trans D601:win ps <<'PS'
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Get-ChildItem -LiteralPath 'F:\Work' -Directory -Filter '*HWLAB*' | Select-Object -ExpandProperty FullName
|
||||
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` 或宽泛扫描整个用户目录。
|
||||
|
||||
@@ -387,6 +396,9 @@ trans D601 glob --root /home/ubuntu/pikapython --pattern '**/*-test.cpp' --limit
|
||||
trans D601:k3s
|
||||
trans D601:k3s kubectl get pods -n hwlab-dev
|
||||
trans D601:/home/ubuntu/workspace/hwlab-dev git status --short --branch
|
||||
trans D601:win ps <<'PS'
|
||||
$PSVersionTable.PSVersion.ToString()
|
||||
PS
|
||||
trans D601:win cmd ver
|
||||
trans D601:win/c/test cmd cd
|
||||
trans D601:win skills --limit 20
|
||||
|
||||
+8
-4
@@ -168,6 +168,8 @@ export function sshHelp(): unknown {
|
||||
"trans <providerId> find <path...> [--contains TEXT] [--limit N]",
|
||||
"trans <providerId> glob [--root DIR] [--pattern PATTERN]",
|
||||
"trans D601:/home/ubuntu/workspace/hwlab-dev 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",
|
||||
@@ -192,7 +194,9 @@ export function sshHelp(): unknown {
|
||||
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 Windows routes, prefer `trans <provider>:win/<drive>/<path> cmd <<'CMD'` for multi-step cmd.exe logic; `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.",
|
||||
"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 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`.",
|
||||
@@ -201,9 +205,9 @@ export function sshHelp(): unknown {
|
||||
"`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. Its stdout/stderr follows Codex apply_patch text output rather than UniDesk JSON output; on multi-file failure, stderr lists applied hunks before the first failed hunk and the failed hunk, then stops like Codex apply_patch.",
|
||||
"`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. The client falls back from a single stdin payload to bounded chunks before treating provider-gateway limits as a server-side problem.",
|
||||
"`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; use --shell bash only for bash syntax such as pipefail, arrays, 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 cmd <command-line>` to run Windows host cmd.exe with UTF-8 defaults, and `<provider>:win/c/test cmd cd` maps the Windows cwd to `C:\\test`; native k3s providers such as D601 and G14 use <provider>:k3s for the control plane, <provider>:k3s:<namespace>:<workload> for a workload, and <provider>:k3s:<namespace>:<workload>/<pod-workspace> for a pod workspace.",
|
||||
"Use `win`, not `win32`; the win route sets chcp 65001, PYTHONUTF8=1, and PYTHONIOENCODING=utf-8 before running the requested cmd command line.",
|
||||
"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.",
|
||||
"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, <provider>:k3s:<namespace>:<workload> for a workload, and <provider>:k3s:<namespace>:<workload>/<pod-workspace> for a pod workspace.",
|
||||
"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 G14 k3s ...`; write `trans G14:k3s ...` so location and operation stay separated.",
|
||||
|
||||
+66
-1
@@ -1048,8 +1048,23 @@ function parseWinRouteArgs(route: ParsedSshRoute, args: string[]): ParsedSshArgs
|
||||
invocationKind: "helper",
|
||||
};
|
||||
}
|
||||
if (operation === "ps" || operation === "powershell" || operation === "powershell.exe") {
|
||||
const commandArgs = args[1] === "--" ? args.slice(2) : args.slice(1);
|
||||
if (commandArgs.length === 0) {
|
||||
return {
|
||||
remoteCommand: buildWindowsPowerShellInvocation(buildWindowsPowerShellStdinLauncherScript(route.workspace)),
|
||||
requiresStdin: true,
|
||||
invocationKind: "helper",
|
||||
};
|
||||
}
|
||||
return {
|
||||
remoteCommand: buildWindowsPowerShellInvocation(buildWindowsPowerShellInlineLauncherScript(commandArgs.join(" "), route.workspace)),
|
||||
requiresStdin: false,
|
||||
invocationKind: "helper",
|
||||
};
|
||||
}
|
||||
if (operation !== "cmd" && operation !== "cmd.exe") {
|
||||
throw new Error(`unsupported ssh win operation: ${operation}; use ssh ${route.providerId}:win cmd <command-line>, ssh ${route.providerId}:win apply-patch, or ssh ${route.providerId}:win skills`);
|
||||
throw new Error(`unsupported ssh win operation: ${operation}; use ssh ${route.providerId}:win ps, ssh ${route.providerId}:win cmd <command-line>, ssh ${route.providerId}:win apply-patch, or ssh ${route.providerId}:win skills`);
|
||||
}
|
||||
const commandArgs = args[1] === "--" ? args.slice(2) : args.slice(1);
|
||||
if (commandArgs.length === 0) {
|
||||
@@ -1205,6 +1220,56 @@ function buildWindowsCmdStdinLauncherScript(cwd: string | null): string {
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
function windowsPowerShellScriptPrelude(cwd: string | null): string {
|
||||
return [
|
||||
"$ErrorActionPreference = 'Stop'",
|
||||
"$ProgressPreference = 'SilentlyContinue'",
|
||||
"[Console]::InputEncoding = [System.Text.UTF8Encoding]::new()",
|
||||
"[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()",
|
||||
"$OutputEncoding = [System.Text.UTF8Encoding]::new()",
|
||||
"$env:PYTHONUTF8 = '1'",
|
||||
"$env:PYTHONIOENCODING = 'utf-8'",
|
||||
...(cwd === null ? [] : [`Set-Location -LiteralPath ${powerShellSingleQuote(cwd)}`]),
|
||||
].join("\r\n") + "\r\n";
|
||||
}
|
||||
|
||||
function buildWindowsPowerShellInlineLauncherScript(command: string, cwd: string | null): string {
|
||||
if (command.trim().length === 0) throw new Error("ssh win ps requires a command or stdin PowerShell script");
|
||||
return buildWindowsPowerShellScriptRunner(powerShellSingleQuote(command), cwd);
|
||||
}
|
||||
|
||||
function buildWindowsPowerShellStdinLauncherScript(cwd: string | null): string {
|
||||
return buildWindowsPowerShellScriptRunner("[Console]::In.ReadToEnd()", cwd);
|
||||
}
|
||||
|
||||
function buildWindowsPowerShellScriptRunner(scriptExpression: string, cwd: string | null): string {
|
||||
return [
|
||||
"$ErrorActionPreference = 'Stop';",
|
||||
"$ProgressPreference = 'SilentlyContinue';",
|
||||
"[Console]::InputEncoding = [System.Text.UTF8Encoding]::new();",
|
||||
"[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new();",
|
||||
"$OutputEncoding = [System.Text.UTF8Encoding]::new();",
|
||||
`$script = ${scriptExpression};`,
|
||||
"$script = $script -replace \"`r`n\", \"`n\";",
|
||||
"$script = $script -replace \"`r\", \"`n\";",
|
||||
"$script = $script -replace \"`n\", \"`r`n\";",
|
||||
"if (-not $script.EndsWith(\"`r`n\")) { $script += \"`r`n\" }",
|
||||
"if ([string]::IsNullOrWhiteSpace($script)) { [Console]::Error.WriteLine('ssh win ps requires a command or stdin PowerShell script'); exit 2 }",
|
||||
"$script += \"`r`nif (`$global:LASTEXITCODE -is [int] -and `$global:LASTEXITCODE -ne 0) { exit `$global:LASTEXITCODE }`r`n\";",
|
||||
`$prefix = ${powerShellSingleQuote(windowsPowerShellScriptPrelude(cwd))};`,
|
||||
"$temp = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), ('unidesk-win-ps-' + [guid]::NewGuid().ToString('N') + '.ps1'));",
|
||||
"try {",
|
||||
" [System.IO.File]::WriteAllText($temp, $prefix + $script, [System.Text.UTF8Encoding]::new($true));",
|
||||
" & (Join-Path $PSHOME 'powershell.exe') -NoProfile -ExecutionPolicy Bypass -File $temp;",
|
||||
" $code = $LASTEXITCODE;",
|
||||
" if ($null -eq $code) { $code = 0 }",
|
||||
"} finally {",
|
||||
" Remove-Item -LiteralPath $temp -Force -ErrorAction SilentlyContinue;",
|
||||
"}",
|
||||
"exit $code;",
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
export function buildWindowsPowerShellInvocation(script: string): string {
|
||||
return shellArgv([
|
||||
windowsPowerShellExePath,
|
||||
|
||||
@@ -418,23 +418,35 @@ export async function runSshArgvGuidanceContract(): Promise<JsonRecord> {
|
||||
hostRouteSeparatorRg,
|
||||
);
|
||||
|
||||
const winCmd = parseSshInvocation("D601:win", ["cmd", "ver"]);
|
||||
assertCondition(winCmd.route.plane === "win" && winCmd.route.workspace === null, "win route must parse as the Windows cmd plane", winCmd);
|
||||
const winCmdScript = decodeWinEncodedCommand(winCmd.parsed.remoteCommand);
|
||||
const winPs = parseSshInvocation("D601:win", ["ps"]);
|
||||
assertCondition(winPs.route.plane === "win" && winPs.parsed.requiresStdin === true && winPs.parsed.invocationKind === "helper", "win ps without args must read PowerShell from stdin", winPs);
|
||||
const winPsScript = decodeWinEncodedCommand(winPs.parsed.remoteCommand);
|
||||
assertCondition(
|
||||
String(winCmd.parsed.remoteCommand).startsWith("'/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe'")
|
||||
&& winCmdScript.includes("C:\\Windows\\System32\\cmd.exe")
|
||||
&& winCmdScript.includes("chcp 65001>nul")
|
||||
&& winCmdScript.includes('set "PYTHONUTF8=1"')
|
||||
&& winCmdScript.includes('set "PYTHONIOENCODING=utf-8"'),
|
||||
"win route must execute cmd.exe through a UTF-8 Windows launcher without trailing-space cmd set values",
|
||||
{ winCmd, winCmdScript },
|
||||
winPsScript.includes("[Console]::In.ReadToEnd()")
|
||||
&& winPsScript.includes("unidesk-win-ps-")
|
||||
&& winPsScript.includes("$ErrorActionPreference = 'Stop'")
|
||||
&& winPsScript.includes("powershell.exe")
|
||||
&& winPsScript.includes("$global:LASTEXITCODE"),
|
||||
"win ps stdin launcher must execute a UTF-8 temp .ps1 with fail-fast semantics",
|
||||
{ winPs, winPsScript },
|
||||
);
|
||||
|
||||
const winCmdCwd = parseSshInvocation("D601:win/c/test", ["cmd", "echo", "中文"]);
|
||||
assertCondition(winCmdCwd.route.plane === "win" && winCmdCwd.route.workspace === String.raw`C:\test`, "win route slash workspace must map to a Windows drive cwd", winCmdCwd);
|
||||
const winCmdCwdScript = decodeWinEncodedCommand(winCmdCwd.parsed.remoteCommand);
|
||||
assertCondition(winCmdCwdScript.includes('cd /d "C:\\test"') && winCmdCwdScript.includes("echo 中文"), "win route workspace must cd in Windows cmd before running the command", { winCmdCwd, winCmdCwdScript });
|
||||
const winPsCwd = parseSshInvocation("D601:win/c/test", ["ps", "Write-Output", "'中文'"]);
|
||||
assertCondition(winPsCwd.route.plane === "win" && winPsCwd.route.workspace === String.raw`C:\test` && winPsCwd.parsed.requiresStdin === false, "win ps inline route must map slash workspace and not require stdin", winPsCwd);
|
||||
const winPsCwdScript = decodeWinEncodedCommand(winPsCwd.parsed.remoteCommand);
|
||||
assertCondition(
|
||||
winPsCwdScript.includes("Set-Location -LiteralPath ''C:\\test''")
|
||||
&& winPsCwdScript.includes("Write-Output ''中文''")
|
||||
&& !winPsCwdScript.includes("chcp 65001"),
|
||||
"win ps inline launcher must use PowerShell cwd semantics rather than cmd.exe batch setup",
|
||||
{ winPsCwd, winPsCwdScript },
|
||||
);
|
||||
|
||||
assertThrows(
|
||||
() => parseSshInvocation("D601:win", ["script", "Get-Location"]),
|
||||
/unsupported ssh win operation: script.*win ps/u,
|
||||
"win route must reject POSIX script operation and point callers to ps",
|
||||
);
|
||||
|
||||
const winSkills = parseSshInvocation("D601:win", ["skills", "--scope", "all", "--limit", "20"]);
|
||||
assertCondition(winSkills.route.plane === "win" && winSkills.parsed.invocationKind === "helper", "win skills route must be a Windows helper operation", winSkills);
|
||||
@@ -1526,8 +1538,9 @@ export async function runSshArgvGuidanceContract(): Promise<JsonRecord> {
|
||||
assertCondition(helpText.includes("not as a proxy workaround"), "ssh help must reserve --shell bash for bash syntax instead of proxy workarounds", helpText);
|
||||
assertCondition(helpNotes.includes("portable printf headings") && helpNotes.includes('printf "--- section ---\\n"'), "ssh help must document script/shell printf heading compatibility", helpNotes);
|
||||
assertCondition(helpText.includes("trans D601:/home/ubuntu/workspace/hwlab-dev git status --short --branch"), "ssh help must document host workspace routes", helpText);
|
||||
assertCondition(helpText.includes("trans D601:win cmd ver") && helpText.includes("trans D601:win/c/test cmd cd") && helpText.includes("trans D601:win skills"), "ssh help must document Windows cmd and skills win routes", helpText);
|
||||
assertCondition(helpText.includes("Use `win`, not `win32`") && helpText.includes("chcp 65001") && helpText.includes("PYTHONIOENCODING=utf-8"), "ssh help must document win route UTF-8 defaults and naming", helpText);
|
||||
assertCondition(helpText.includes("trans D601:win ps <<'PS'") && helpText.includes("trans D601:win/c/test ps <<'PS'"), "ssh help must document Windows PowerShell ps routes", helpText);
|
||||
assertCondition(helpText.includes("Use `win`, not `win32`") && helpText.includes("win route is a Windows operation plane"), "ssh help must document win route naming and operation boundary", helpText);
|
||||
assertCondition(helpText.includes("Do not use `script` for Windows PowerShell") && helpText.includes("trans <provider>:win ps <<'PS'"), "ssh help must direct Windows PowerShell users to ps rather than script", helpText);
|
||||
assertCondition(helpText.includes("trans D601:k3s kubectl get pods -n hwlab-dev"), "ssh help must document k3s kubectl operation", helpText);
|
||||
assertCondition(helpText.includes("trans G14:k3s kubectl get pipelineruns -n hwlab-ci"), "ssh help must document G14 k3s route operation", helpText);
|
||||
assertCondition(helpText.includes("trans D601:k3s:hwlab-dev:hwlab-cloud-api/app pwd"), "ssh help must document k3s pod workspace route", helpText);
|
||||
@@ -1568,10 +1581,10 @@ export async function runSshArgvGuidanceContract(): Promise<JsonRecord> {
|
||||
assertCondition(frontendRemoteWorkspacePlan.payloadCwd === "/home/ubuntu/workspace/hwlab-dev", "remote frontend host workspace route must pass cwd to host.ssh payload", frontendRemoteWorkspacePlan);
|
||||
assertCondition(frontendRemoteWorkspacePlan.remoteCommand === "'git' 'status' '--short'", "remote frontend host workspace route must keep command argv-quoted", frontendRemoteWorkspacePlan);
|
||||
|
||||
const frontendRemoteWinPlan = remoteSshFrontendPlanForTest("D601:win/c/test", ["cmd", "cd"]);
|
||||
const frontendRemoteWinPlan = remoteSshFrontendPlanForTest("D601:win/c/test", ["ps", "Get-Location"]);
|
||||
assertCondition(frontendRemoteWinPlan.providerId === "D601" && frontendRemoteWinPlan.payloadCwd === "/mnt/c/Windows", "remote frontend win route must dispatch through provider host.ssh from a Windows-mounted cwd", frontendRemoteWinPlan);
|
||||
const frontendRemoteWinScript = decodeWinEncodedCommand(String(frontendRemoteWinPlan.remoteCommand));
|
||||
assertCondition(frontendRemoteWinScript.includes("cmd.exe") && frontendRemoteWinScript.includes("cd /d \"C:\\test\""), "remote frontend win route must assemble Windows cmd cwd internally", { frontendRemoteWinPlan, frontendRemoteWinScript });
|
||||
assertCondition(frontendRemoteWinScript.includes("powershell.exe") && frontendRemoteWinScript.includes("Set-Location -LiteralPath ''C:\\test''") && frontendRemoteWinScript.includes("Get-Location"), "remote frontend win route must assemble Windows PowerShell cwd internally", { frontendRemoteWinPlan, frontendRemoteWinScript });
|
||||
|
||||
const tranScript = readFileSync(new URL("./tran", import.meta.url), "utf8");
|
||||
assertCondition(tranScript.includes("CODE_QUEUE_DEV_CONTAINER_MASTER_HOST") && tranScript.includes("--main-server-ip"), "tran wrapper must auto-select frontend transport inside Code Queue runner pods", tranScript);
|
||||
@@ -1624,7 +1637,7 @@ export async function runSshArgvGuidanceContract(): Promise<JsonRecord> {
|
||||
"legacy operation-in-route forms are rejected in any k3s route segment with canonical route-plus-operation guidance",
|
||||
"post-provider k3s shorthand is rejected so location and operation stay separated",
|
||||
"k3s route stays location-only while operations fix native kubeconfig and assemble kubectl exec as argv",
|
||||
"win route runs Windows cmd.exe with UTF-8 defaults and slash cwd syntax such as D601:win/c/test",
|
||||
"win route supports Windows PowerShell ps heredoc with slash cwd syntax such as D601:win/c/test",
|
||||
"win skills discovers the current Windows user's skill roots without hand-written cmd dir or PowerShell",
|
||||
"top-level remote option parsing preserves command-local -- separators for script -- sed -n style commands",
|
||||
"ssh-like timeout/kex failures emit one structured argv retry hint",
|
||||
|
||||
Reference in New Issue
Block a user