feat: add tran win cmd route
This commit is contained in:
+98
-2
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user