feat: add tran win cmd route

This commit is contained in:
Codex
2026-05-25 18:02:40 +00:00
parent 974a7ac666
commit a1841adcaa
8 changed files with 160 additions and 11 deletions
+5 -2
View File
@@ -19,7 +19,7 @@ export function rootHelp(): unknown {
{ command: "server cleanup plan [--min-age-hours N] [--limit N]", description: "Dry-run Docker image cleanup plan only: list active/protected images, stale candidates older than the default 24h threshold, risk, estimated reclaim, and manual review commands without deleting anything." },
{ command: "server rebuild <backend-core|frontend|dev-frontend-proxy|provider-gateway|todo-note|code-queue-mgr|project-manager|baidu-netdisk|oa-event-flow>", description: "Maintenance-only local Compose rebuild for reviewed main-server services; frontend standard release must use CI artifact plus deploy apply dev/prod artifact consumers." },
{ command: "provider attach <providerId> [--master-server URL] [--up] [--force] | provider triage <providerId> [--observed-error text] [--observed-scope scope] [--microservice id ...] [--full|--raw]", description: "Generate the minimal external provider-gateway env/compose bundle or run the low-noise read-only provider health triage contract." },
{ command: "ssh <route> [operation args...]", description: "Open a Host SSH / WSL SSH maintenance session through the provider-gateway bridge; route syntax such as `G14:k3s` only locates distributed targets." },
{ command: "ssh <route> [operation args...]", description: "Open a Host SSH / WSL SSH maintenance session through the provider-gateway bridge; route syntax such as `G14:k3s` or `D601:win/c/test` only locates distributed targets." },
{ command: "ssh <providerId> apply-patch [tool args...] < patch.diff", description: "Invoke the injected remote apply_patch helper directly over SSH passthrough and stream the patch from local stdin." },
{ command: "ssh <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: "ssh <providerId> script [--shell sh|bash] [script-args...] <<'SCRIPT' ...", description: "Run a remote shell script from local stdin using shell -s; the default sh inherits provider proxy env, so --shell bash is only for bash-specific syntax." },
@@ -156,6 +156,8 @@ export function sshHelp(): unknown {
"bun scripts/cli.ts ssh <providerId> find <path...> [--contains TEXT] [--limit N]",
"bun scripts/cli.ts ssh <providerId> glob [--root DIR] [--pattern PATTERN]",
"bun scripts/cli.ts ssh D601:/home/ubuntu/workspace/hwlab-dev git status --short --branch",
"bun scripts/cli.ts ssh D601:win cmd ver",
"bun scripts/cli.ts ssh D601:win/c/test cmd cd",
"bun scripts/cli.ts ssh D601:k3s",
"bun scripts/cli.ts ssh D601:k3s kubectl get pods -n hwlab-dev",
"bun scripts/cli.ts ssh G14:k3s",
@@ -176,7 +178,8 @@ export function sshHelp(): unknown {
"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 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`; 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.",
"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.",
"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 `ssh G14 k3s ...`; write `ssh G14: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.",
+3 -3
View File
@@ -6,7 +6,7 @@ import { type UniDeskConfig } from "./config";
import { type DebugDispatchCommand, isDebugDispatchCommand } from "./debug";
import { summarizeMicroserviceHealthResponse, summarizeMicroserviceObservation, summarizeMicroserviceProxyResponse } from "./microservices";
import { parseNetworkPerfOptions, runNetworkPerf } from "./network-perf";
import { formatSshFailureHint, formatSshRuntimeTimingHint, parseSshInvocation, sshFailureHint, sshRuntimeTimingHint, wrapSshRemoteCommand } from "./ssh";
import { formatSshFailureHint, formatSshRuntimeTimingHint, parseSshInvocation, sshFailureHint, sshRoutePayloadCwd, sshRuntimeTimingHint, wrapSshRemoteCommand } from "./ssh";
import { codexJudgeQueryAsync, codexOutputQueryAsync, codexPrPreflightQueryAsync, codexQueuesQueryAsync, codexTaskQueryAsync, codexTasksQueryAsync, codexUnreadTriageAsync } from "./code-queue";
import { runDecisionCenterCommandAsync } from "./decision-center";
import {
@@ -901,7 +901,7 @@ async function runRemoteSshWebSocket(
const payload = {
providerId: invocation.providerId,
command: wrapSshRemoteCommand(parsed.remoteCommand, parsed.requiredHelpers),
cwd: invocation.route.plane === "host" ? invocation.route.workspace ?? undefined : undefined,
cwd: sshRoutePayloadCwd(invocation.route),
tty: parsed.remoteCommand === null,
stdinEotOnEnd: parsed.remoteCommand !== null,
openTimeoutMs,
@@ -1048,7 +1048,7 @@ export function remoteSshFrontendPlanForTest(target: string, args: string[]): Re
wrappedRemoteCommand: wrapSshRemoteCommand(invocation.parsed.remoteCommand, invocation.parsed.requiredHelpers),
requiresStdin: invocation.parsed.requiresStdin,
invocationKind: invocation.parsed.invocationKind,
payloadCwd: invocation.route.plane === "host" ? invocation.route.workspace : null,
payloadCwd: sshRoutePayloadCwd(invocation.route) ?? null,
};
}
+98 -2
View File
@@ -15,7 +15,7 @@ export type SshHelperName = "apply_patch" | "glob" | "skill-discover";
export interface ParsedSshRoute {
providerId: string;
plane: "host" | "k3s";
plane: "host" | "k3s" | "win";
entry: string | null;
namespace: string | null;
resource: string | null;
@@ -59,6 +59,9 @@ export interface SshRuntimeTimingHint {
const argvQuotedSshSubcommands = new Set(["git", "rg", "grep", "sed", "nl", "stat", "du", "ls", "cat", "head", "tail", "wc", "pwd"]);
const nativeK3sKubeconfig = "/etc/rancher/k3s/k3s.yaml";
const windowsBridgeCwd = "/mnt/c/Windows";
const windowsPowerShellExePath = "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe";
const windowsCmdExeNativePath = "C:\\Windows\\System32\\cmd.exe";
const defaultSshSlowWarningMs = 10_000;
const k3sResourceKindAliases = new Set(["pod", "po", "pods", "deployment", "deploy", "deployments", "statefulset", "sts", "daemonset", "ds", "job", "jobs"]);
const legacyK3sOperationRouteSegments = new Set([
@@ -859,6 +862,9 @@ export function parseSshInvocation(target: string, args: string[]): ParsedSshInv
if (route.plane === "k3s") {
return { providerId: route.providerId, route, parsed: parseK3sRouteArgs(route, args) };
}
if (route.plane === "win") {
return { providerId: route.providerId, route, parsed: parseWinRouteArgs(route, args) };
}
if ((args[0] ?? "") === "k3s") {
throw new Error(`ssh k3s shorthand is unsupported; use route syntax instead: ssh ${route.providerId}:k3s ${args.slice(1).join(" ")}`.trim());
}
@@ -880,6 +886,15 @@ export function parseSshRoute(target: string): ParsedSshRoute {
if (tail.startsWith("/")) {
return hostSshRoute(providerId, target, tail);
}
if (tail === "win32" || tail.startsWith("win32/") || tail.startsWith("win32:")) {
throw new Error(`unsupported ssh route plane: win32; use ${providerId}:win or ${providerId}:win/c/path`);
}
if (tail === "win" || tail.startsWith("win/")) {
return winSshRoute(providerId, target, parseWinRouteWorkspace(providerId, tail));
}
if (tail.startsWith("win:")) {
throw new Error(`ssh win workspace route uses slash syntax, for example: ssh ${providerId}:win/c/test cmd cd`);
}
const [plane, ...rest] = tail.split(":");
if (plane === undefined || plane.length === 0 || plane === "host") {
const workspace = rest.length > 0 ? rest.join(":") : null;
@@ -908,6 +923,87 @@ function hostSshRoute(providerId: string, raw: string, workspace: string | null)
return { providerId, plane: "host", entry: null, namespace: null, resource: null, container: null, workspace, raw };
}
function winSshRoute(providerId: string, raw: string, workspace: string | null): ParsedSshRoute {
return { providerId, plane: "win", entry: null, namespace: null, resource: null, container: null, workspace, raw };
}
function parseWinRouteWorkspace(providerId: string, tail: string): string | null {
if (tail === "win") return null;
const suffix = tail.slice("win/".length);
const slashIndex = suffix.indexOf("/");
const drive = slashIndex < 0 ? suffix : suffix.slice(0, slashIndex);
if (!/^[A-Za-z]$/u.test(drive)) {
throw new Error(`ssh win workspace route requires a drive letter, for example: ssh ${providerId}:win/c/test cmd cd`);
}
const rest = slashIndex < 0 ? "" : suffix.slice(slashIndex + 1);
const segments = rest.split("/").filter((segment) => segment.length > 0);
return `${drive.toUpperCase()}:\\${segments.join("\\")}`;
}
function parseWinRouteArgs(route: ParsedSshRoute, args: string[]): ParsedSshArgs {
const operation = args[0] ?? "";
if (operation.length === 0) {
throw new Error(`ssh ${route.raw} requires a Windows operation, for example: ssh ${route.providerId}:win cmd ver`);
}
if (operation !== "cmd" && operation !== "cmd.exe") {
throw new Error(`unsupported ssh win operation: ${operation}; use ssh ${route.providerId}:win cmd <command-line>`);
}
const commandArgs = args[1] === "--" ? args.slice(2) : args.slice(1);
if (commandArgs.length === 0) throw new Error(`ssh ${route.raw} cmd requires a command line, for example: ssh ${route.providerId}:win cmd ver`);
return {
remoteCommand: shellArgv([
windowsPowerShellExePath,
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-EncodedCommand",
buildWindowsPowerShellEncodedCommand(buildWindowsCmdLine(commandArgs.join(" "), route.workspace)),
]),
requiresStdin: false,
invocationKind: "argv",
};
}
function buildWindowsCmdLine(userCommand: string, cwd: string | null): string {
const parts = [
"chcp 65001>nul",
"set PYTHONUTF8=1",
"set PYTHONIOENCODING=utf-8",
];
if (cwd !== null) parts.push(`cd /d ${windowsCmdQuote(cwd)}`);
parts.push(userCommand);
return parts.join(" && ");
}
function windowsCmdQuote(value: string): string {
if (/[\r\n"]/u.test(value)) throw new Error("ssh win workspace path must not contain quotes or newlines");
return `"${value}"`;
}
function powerShellSingleQuote(value: string): string {
return `'${value.replace(/'/g, "''")}'`;
}
function buildWindowsPowerShellEncodedCommand(cmdLine: string): string {
const script = [
"$ErrorActionPreference = 'Stop';",
"[Console]::InputEncoding = [System.Text.UTF8Encoding]::new();",
"[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new();",
"$OutputEncoding = [System.Text.UTF8Encoding]::new();",
"$env:PYTHONUTF8 = '1';",
"$env:PYTHONIOENCODING = 'utf-8';",
`& ${powerShellSingleQuote(windowsCmdExeNativePath)} /d /s /c ${powerShellSingleQuote(cmdLine)};`,
"exit $LASTEXITCODE;",
].join(" ");
return Buffer.from(script, "utf16le").toString("base64");
}
export function sshRoutePayloadCwd(route: ParsedSshRoute): string | undefined {
if (route.plane === "host") return route.workspace ?? undefined;
if (route.plane === "win") return windowsBridgeCwd;
return undefined;
}
function routeSegmentHead(segment: string): string {
return segment.split("/")[0] ?? segment;
}
@@ -1806,7 +1902,7 @@ export async function runSsh(config: UniDeskConfig, providerId: string, args: st
const payload = {
providerId: invocation.providerId,
command: wrapSshRemoteCommand(parsed.remoteCommand, parsed.requiredHelpers),
cwd: invocation.route.plane === "host" ? invocation.route.workspace ?? undefined : undefined,
cwd: sshRoutePayloadCwd(invocation.route),
tty: parsed.remoteCommand === null,
stdinEotOnEnd: parsed.remoteCommand !== null,
openTimeoutMs,