fix: add windows powershell passthrough
This commit is contained in:
+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,
|
||||
|
||||
Reference in New Issue
Block a user