feat: add tran win cmd route
This commit is contained in:
+5
-2
@@ -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.",
|
||||
|
||||
@@ -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
@@ -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