Files
pikasTech-unidesk/scripts/src/ssh.ts
T

2077 lines
93 KiB
TypeScript

import { isApplyPatchV2HelpArgs } from "./apply-patch-v2";
import { isSshFileTransferOperation } from "./ssh-file-transfer";
import { isWindowsFsReadOnlyOperation, windowsFsReadOnlyScript } from "./ssh-windows-fs";
import { remoteApplyPatchSource, remoteGlobSource, remoteSkillDiscoverSource } from "./ssh-remote-tools";
import { readTransHostProxyEnvRule, type TransHostProxyEnvRule } from "./trans-host-proxy";
export interface ParsedSshArgs {
remoteCommand: string | null;
requiresStdin: boolean;
invocationKind: SshInvocationKind;
stdinPrefix?: string;
stdinSuffix?: string;
requiredHelpers?: SshHelperName[];
}
export type SshInvocationKind = "interactive" | "argv" | "helper" | "ssh-like";
export type SshHelperName = "apply_patch" | "glob" | "skill-discover";
export interface ParsedSshRoute {
providerId: string;
plane: "host" | "k3s" | "win";
entry: string | null;
namespace: string | null;
resource: string | null;
container: string | null;
workspace: string | null;
raw: string;
}
export interface ParsedSshInvocation {
providerId: string;
route: ParsedSshRoute;
parsed: ParsedSshArgs;
}
export interface EffectiveApplyPatchV2Invocation {
invocation: ParsedSshInvocation;
argv: string[];
}
export interface SshCaptureResult {
exitCode: number;
stdout: string;
stderr: string;
}
export type SshCaptureBackend = "local-backend-core-broker" | "remote-frontend-websocket";
export interface SshCaptureBackendPlan {
backend: SshCaptureBackend;
remoteHost: string | null;
reason: string;
localBackendCore: {
dockerExecutable: boolean;
backendCoreContainer: boolean;
error: string | null;
source?: string;
};
}
export interface SshFailureHint {
code: "ssh-like-command-friction";
providerId: string;
trigger: "timeout-or-kex" | "exit-255";
exitCode: number;
message: string;
try: string;
triage: string;
note: string;
}
export interface SshRuntimeTimingHint {
code: "ssh-runtime-timing";
level: "info" | "warning";
providerId: string;
route: string;
transport: "backend-core-broker" | "frontend-websocket";
invocationKind: SshInvocationKind;
exitCode: number;
elapsedMs: number;
elapsedSeconds: number;
thresholdMs: number;
slow: boolean;
message: string;
note: string;
}
export interface SshRuntimeTimeoutHint {
code: "ssh-runtime-timeout";
level: "warning";
providerId: string;
route: string;
transport: "backend-core-broker" | "frontend-websocket";
invocationKind: SshInvocationKind;
timeoutMs: number;
timeoutSeconds: number;
message: string;
action: string;
note: string;
}
export interface RemovedShellAliasReplacementExamples {
posixSh: string;
bash: string;
templatePosixSh: string;
templateBash: string;
}
export class SshRemovedShellAliasError extends Error {
code = "ssh-removed-shell-alias";
level = "error";
entrypoint: string;
route: string;
operation: string;
replacementExamples: RemovedShellAliasReplacementExamples;
migrationHint: string;
note = "`sh` runs POSIX /bin/sh; `bash` is only for Bash-specific syntax. The removed alias intentionally no longer chooses a hidden shell dialect.";
constructor(operation: string, route: string, operationArgs: string[]) {
const entrypoint = sshDisplayEntrypoint();
const replacementExamples = removedShellAliasReplacementExamples(entrypoint, route, operationArgs);
super(`${entrypoint} ${route} ${operation} operation has been removed because it hides the shell dialect; use explicit sh for POSIX /bin/sh or bash for Bash syntax`);
this.name = "SshRemovedShellAliasError";
this.entrypoint = entrypoint;
this.route = route;
this.operation = operation;
this.replacementExamples = replacementExamples;
this.migrationHint = `Rewrite as ${replacementExamples.posixSh} for POSIX shell, or ${replacementExamples.bash} for Bash syntax.`;
}
}
export class SshWindowsPowerShellLocalExpansionError extends Error {
code = "ssh-windows-powershell-local-expansion";
level = "error";
entrypoint: string;
route: string;
operation = "ps";
replacementExamples: { oneLine: string; pipeline: string; stdin: string };
migrationHint: string;
note = "Local Bash expands $p and $_ inside double quotes before trans receives the PowerShell source.";
constructor(route: string) {
const entrypoint = sshDisplayEntrypoint();
const replacementExamples = {
oneLine: `${entrypoint} ${route} ps '$p = Start-Process python -PassThru; $p.Id'`,
pipeline: `${entrypoint} ${route} ps 'Get-Process | Where-Object { $_.CPU -gt 0 }'`,
stdin: `${entrypoint} ${route} ps <<'PS'`,
};
super("ssh win ps source looks like a PowerShell variable was expanded by the local shell before trans received it");
this.name = "SshWindowsPowerShellLocalExpansionError";
this.entrypoint = entrypoint;
this.route = route;
this.replacementExamples = replacementExamples;
this.migrationHint = `Wrap the complete PowerShell source in local single quotes, for example: ${replacementExamples.oneLine}. Use a quoted PS heredoc for multiline source.`;
}
}
export interface SshStdoutTruncationHint {
code: "ssh-stdout-truncated" | "ssh-stderr-truncated";
level: "warning";
stream: "stdout" | "stderr";
providerId: string;
route: string;
transport: "backend-core-broker" | "frontend-websocket";
invocationKind: SshInvocationKind;
thresholdBytes: number;
observedBytesAtTruncation: number;
forwardedBytes: number;
dumpPath: string | null;
dumpError: string | null;
disclosurePolicy: {
name: "unified-cli-dump-preview";
configPath: string;
maxPreviewBytes: number;
dumpDir: string;
};
recommendedRerun: string[];
message: string;
action: string;
note: string;
}
export interface SshStreamTruncationSummary {
stream: "stdout" | "stderr";
thresholdBytes: number;
observedBytesAtTruncation: number;
forwardedBytes: number;
dumpPath: string | null;
dumpError: string | null;
}
export interface SshTruncationCompletionSummary {
code: "ssh-truncation-summary";
level: "info";
providerId: string;
route: string;
transport: "backend-core-broker" | "frontend-websocket";
invocationKind: SshInvocationKind;
exitCode: number;
timedOut: boolean;
elapsedMs: number;
elapsedSeconds: number;
commandOmitted: true;
stdout: SshStreamTruncationSummary | null;
stderr: SshStreamTruncationSummary | null;
message: string;
action: string;
}
export type SshTcpPoolFailureKind =
| "provider-data-channel-closed"
| "provider-data-channel-missing"
| "provider-data-pool-exhausted";
export interface SshTcpPoolHint {
code: "ssh-tcp-pool-transient";
level: "warning";
providerId: string;
route: string;
transport: "backend-core-broker" | "frontend-websocket";
invocationKind: SshInvocationKind;
failureKind: SshTcpPoolFailureKind;
exitCode: number;
message: string;
action: string;
retry: string;
diagnostics: {
poolStatus: string;
fullHealth: string;
};
note: string;
}
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 defaultSshRuntimeTimeoutMs = 60_000;
const maxSshRuntimeTimeoutMs = 60_000;
const defaultSshBackendCoreDetectTimeoutMs = 15_000;
const maxSshBackendCoreDetectTimeoutMs = 60_000;
const minSshStdoutStreamMaxBytes = 4 * 1024;
const maxSshStdoutStreamMaxBytes = 16 * 1024 * 1024;
export const sshShellCompatibilityPrelude = 'printf(){ if [ "${1+x}" = x ] && [ "$1" = "-v" ] && [ -n "${BASH_VERSION:-}" ]; then command printf "$@"; return $?; fi; if [ "${1+x}" = x ] && [ "$1" = "--" ]; then shift; fi; command printf -- "$@"; }';
export const sshUserToolPathPrelude = [
'for unidesk_path_dir in "$HOME/.bun/bin" "$HOME/.local/bin" "$HOME/bin" "/root/.bun/bin"; do',
' [ -d "$unidesk_path_dir" ] || continue',
' case ":$PATH:" in',
' *":$unidesk_path_dir:"*) ;;',
' *) PATH="$unidesk_path_dir:$PATH" ;;',
" esac",
"done",
"export PATH",
].join("\n");
const k3sResourceKindAliases = new Set(["pod", "po", "pods", "deployment", "deploy", "deployments", "statefulset", "sts", "daemonset", "ds", "job", "jobs"]);
const legacyK3sOperationRouteSegments = new Set([
"guard",
"kubectl",
"exec",
"script",
"shell",
"sh",
"bash",
"apply-patch",
"apply-patch-v1",
"patch",
"patch-v1",
"v2",
"logs",
"get",
"describe",
"top",
"rollout",
"wait",
"config",
"version",
"cluster-info",
"auth",
"api-resources",
"api-versions",
]);
const sshOptionsWithValue = new Set([
"-B", "-b", "-c", "-D", "-E", "-e", "-F", "-I", "-i", "-J", "-L", "-l", "-m", "-O", "-o", "-p", "-Q", "-R", "-S", "-W", "-w",
]);
export function isSshSkillDiscoveryArgs(args: string[]): boolean {
const subcommand = args[0] ?? "";
return subcommand === "skills" || subcommand === "skill-discover" || subcommand === "discover-skills" || (subcommand === "skill" && args[1] === "discover");
}
export function parseSshArgs(args: string[], routeRaw = "<route>"): ParsedSshArgs {
const subcommand = args[0] ?? "";
if (isSshFileTransferOperation(args)) {
return { remoteCommand: null, requiresStdin: false, invocationKind: "helper" };
}
if (isSshSkillDiscoveryArgs(args)) {
const toolArgs = subcommand === "skill" ? ["skill-discover", ...args.slice(2)] : ["skill-discover", ...args.slice(1)];
return { remoteCommand: shellArgv(toolArgs), requiresStdin: false, invocationKind: "helper", requiredHelpers: ["skill-discover"] };
}
if (subcommand === "apply-patch") {
if (isApplyPatchV2HelpArgs(args.slice(1))) {
return { remoteCommand: null, requiresStdin: false, invocationKind: "helper" };
}
return { remoteCommand: null, requiresStdin: true, invocationKind: "helper" };
}
if (subcommand === "apply-patch-v1") {
const toolArgs = ["apply_patch", ...args.slice(1)];
return { remoteCommand: shellArgv(toolArgs), requiresStdin: true, invocationKind: "helper", requiredHelpers: ["apply_patch"] };
}
if (subcommand === "patch" || subcommand === "patch-v1" || subcommand === "v2") {
throw new Error("remote patch entrypoints are `apply-patch` for the default v2 engine and `apply-patch-v1` for the legacy helper");
}
if (subcommand === "py") {
return { remoteCommand: buildPythonStdinCommand(args.slice(1)), requiresStdin: true, invocationKind: "helper" };
}
if (subcommand === "playwright") {
throw new Error("The remote Playwright operation has been removed; use `bun scripts/cli.ts web-probe screenshot ...` or `web-probe sentinel dashboard screenshot ...` for remote screenshots");
}
if (subcommand === "script" || subcommand === "shell") {
throw removedShellAliasError(subcommand, routeRaw, args.slice(1));
}
if (subcommand === "sh" || subcommand === "bash") {
return buildShellCommand(args.slice(1), subcommand, `ssh ${subcommand}`);
}
if (subcommand === "argv" || subcommand === "exec") {
const toolArgs = args.slice(1);
if (toolArgs.length === 0) throw new Error(`ssh ${subcommand} requires a command`);
validateDirectArgvCommand(subcommand, toolArgs);
return { remoteCommand: shellArgv(toolArgs), requiresStdin: false, invocationKind: "argv" };
}
if (subcommand === "find") {
return { remoteCommand: buildFindCommand(args.slice(1)), requiresStdin: false, invocationKind: "helper" };
}
if (subcommand === "glob") {
return { remoteCommand: shellArgv(["glob", ...args.slice(1)]), requiresStdin: false, invocationKind: "helper", requiredHelpers: ["glob"] };
}
if (subcommand === "k3s") {
throw new Error("ssh k3s shorthand is unsupported; put k3s in the route, for example: trans D601:k3s kubectl get nodes");
}
if (argvQuotedSshSubcommands.has(subcommand)) {
return { remoteCommand: shellArgv(args), requiresStdin: false, invocationKind: "argv" };
}
const remote: string[] = [];
let remoteStarted = false;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index] ?? "";
if (remoteStarted) {
remote.push(arg);
continue;
}
if (arg === "--") {
remoteStarted = true;
continue;
}
if (arg.startsWith("-") && arg !== "-") {
if (sshOptionsWithValue.has(arg) && index + 1 < args.length) index += 1;
continue;
}
remoteStarted = true;
remote.push(arg);
}
return {
remoteCommand: remote.length === 0 ? null : shellArgv(remote),
requiresStdin: false,
invocationKind: remote.length === 0 ? "interactive" : "ssh-like",
};
}
export function normalizeSshOperationArgs(args: string[]): string[] {
return args[0] === "--" ? args.slice(1) : args;
}
export function sshRouteSeparatorCompatibilityHint(rawArgs: string[], normalizedArgs: string[]): string {
if (rawArgs === normalizedArgs || rawArgs[0] !== "--") return "";
const operation = normalizedArgs[0] ?? "";
const operationText = operation.length === 0 ? "operation" : `operation \`${operation}\``;
return `UNIDESK_SSH_HINT route-level -- is ignored before ${operationText}; canonical form is \`trans <route> ${normalizedArgs.join(" ")}\`. Keep -- only inside operations such as \`sh -- <command>\`, \`bash -- <command>\`, or \`exec -- <command>\`.\n`;
}
function validateDirectArgvCommand(commandName: string, toolArgs: string[]): void {
if (toolArgs.length !== 1) return;
const command = toolArgs[0] ?? "";
if (!looksLikeShellCommandString(command)) return;
throw new Error(
`ssh ${commandName} received one shell-like command string; ${commandName} executes a single process and treats that string as the executable path. ` +
`Use \`trans <route> sh -- ${shellQuote(command)}\` or \`trans <route> bash -- ${shellQuote(command)}\` for one-line shell logic, or split argv tokens, for example \`trans <route> ${commandName} ls -la\`.`
);
}
function looksLikeShellCommandString(value: string): boolean {
return /\s/u.test(value) || /&&|\|\||[;|<>]/u.test(value);
}
export function parseSshInvocation(target: string, args: string[]): ParsedSshInvocation {
const route = parseSshRoute(target);
const operationArgs = normalizeSshOperationArgs(args);
if (route.plane === "k3s") {
return { providerId: route.providerId, route, parsed: parseK3sRouteArgs(route, operationArgs) };
}
if (route.plane === "win") {
return { providerId: route.providerId, route, parsed: parseWinRouteArgs(route, operationArgs) };
}
if ((operationArgs[0] ?? "") === "k3s") {
throw new Error(`ssh k3s shorthand is unsupported; use route syntax instead: trans ${route.providerId}:k3s ${operationArgs.slice(1).join(" ")}`.trim());
}
const parsed = parseSshArgs(operationArgs, route.raw);
return { providerId: route.providerId, route, parsed: withTransHostEnvironment(route, parsed) };
}
export function parseSshRoute(target: string): ParsedSshRoute {
if (!target) throw new Error("ssh requires provider id, for example: trans D601");
const firstColon = target.indexOf(":");
if (firstColon < 0) {
return hostSshRoute(target, target, null);
}
const providerId = target.slice(0, firstColon);
const tail = target.slice(firstColon + 1);
if (!providerId) throw new Error("ssh route requires a provider id before ':'");
if (tail.length === 0) {
return hostSshRoute(providerId, target, null);
}
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: trans ${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;
if (workspace !== null && !workspace.startsWith("/")) throw new Error("ssh host workspace route requires an absolute path after provider:host:");
return hostSshRoute(providerId, target, workspace);
}
if (plane !== "k3s") throw new Error(`unsupported ssh route plane: ${plane}`);
rejectK3sSlashKindRouteSegments(target, rest);
const normalizedRest = normalizeK3sRouteRestSegments(rest);
const [first, second, third, fourth] = normalizedRest;
const operationInRoute = [first, second, third].map((segment) => segment === undefined ? undefined : routeSegmentHead(segment)).find((segment) => segment !== undefined && legacyK3sOperationRouteSegments.has(segment));
if (operationInRoute !== undefined) throw new Error(k3sOperationInRouteMessage(target, operationInRoute));
if (fourth !== undefined) throw new Error("ssh k3s target route supports at most provider:k3s:namespace:resource:container");
const targetRoute = parseK3sRouteTargetSegments(second ?? null, third ?? null);
return {
providerId,
plane: "k3s",
entry: null,
namespace: first && first.length > 0 ? first : null,
resource: targetRoute.resource,
container: targetRoute.container,
workspace: targetRoute.workspace,
raw: target,
};
}
function hostSshRoute(providerId: string, raw: string, workspace: string | null): ParsedSshRoute {
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 or ssh ${route.providerId}:win skills`);
}
if (operation === "win") {
throw new Error(windowsRouteRepeatedWinOperationMessage(route, args.slice(1)));
}
if (operation === "upload" || operation === "download") {
return { remoteCommand: null, requiresStdin: false, invocationKind: "helper" };
}
if (operation === "apply-patch") {
if (isApplyPatchV2HelpArgs(args.slice(1))) {
return { remoteCommand: null, requiresStdin: false, invocationKind: "helper" };
}
return { remoteCommand: null, requiresStdin: true, invocationKind: "helper" };
}
if (operation === "apply-patch-v1") {
throw new Error(`ssh ${route.raw} apply-patch-v1 is not supported for Windows routes; use apply-patch v2`);
}
if (operation === "patch" || operation === "patch-v1" || operation === "v2") {
throw new Error("remote patch entrypoints are `apply-patch` for the default v2 engine and `apply-patch-v1` for the legacy helper");
}
if (operation === "skills" || operation === "skill-discover" || operation === "discover-skills") {
return {
remoteCommand: buildWindowsPowerShellInvocation(buildWindowsSkillsDiscoveryScript(args.slice(1))),
requiresStdin: false,
invocationKind: "helper",
};
}
if (operation === "playwright") {
throw new Error("The remote Playwright operation has been removed; use `bun scripts/cli.ts web-probe screenshot ...` or `web-probe sentinel dashboard screenshot ...` for remote screenshots");
}
if (isWindowsFsReadOnlyOperation(operation)) {
return {
remoteCommand: buildWindowsPowerShellInvocation(windowsFsReadOnlyScript(route.workspace, operation, args.slice(1))),
requiresStdin: false,
invocationKind: "helper",
};
}
if (operation === "git") {
return parseWindowsGitInvocation(route, args.slice(1));
}
if (operation === "ps" || operation === "powershell" || operation === "powershell.exe") {
const commandArgs = args[1] === "--" ? args.slice(2) : args.slice(1);
if (commandArgs.length >= 2 && (commandArgs[0] === "-File" || commandArgs[0] === "-file")) {
const fileArgs = [commandArgs[1]?.replace(/\\/g, "/") ?? "", ...commandArgs.slice(2)];
return {
remoteCommand: buildWindowsPowerShellInvocation(buildWindowsPowerShellArgvLauncherScript(fileArgs, route.workspace)),
requiresStdin: false,
invocationKind: "argv",
};
}
if (commandArgs.length === 0) {
return {
remoteCommand: buildWindowsPowerShellInvocation(buildWindowsPowerShellStdinLauncherScript(route.workspace)),
requiresStdin: true,
invocationKind: "helper",
};
}
if (commandArgs.length > 1) {
return {
remoteCommand: buildWindowsPowerShellInvocation(buildWindowsPowerShellArgvLauncherScript(commandArgs, route.workspace)),
requiresStdin: false,
invocationKind: "argv",
};
}
const command = commandArgs[0] ?? "";
if (looksLikeLocallyExpandedPowerShellSource(command)) throw new SshWindowsPowerShellLocalExpansionError(route.raw);
return {
remoteCommand: buildWindowsPowerShellInvocation(buildWindowsPowerShellInlineLauncherScript(command, route.workspace)),
requiresStdin: false,
invocationKind: "helper",
};
}
if (operation !== "cmd" && operation !== "cmd.exe") {
throw new Error(windowsUnsupportedOperationMessage(route, operation));
}
const commandArgs = args[1] === "--" ? args.slice(2) : args.slice(1);
if (commandArgs.length === 0) {
return {
remoteCommand: buildWindowsPowerShellInvocation(buildWindowsCmdStdinLauncherScript(route.workspace)),
requiresStdin: true,
invocationKind: "helper",
};
}
return {
remoteCommand: buildWindowsPowerShellInvocation(buildWindowsCmdLauncherScript(buildWindowsCmdLine(commandArgs.map((value) => windowsCmdArgument(value, route)).join(" "), route.workspace))),
requiresStdin: false,
invocationKind: "argv",
};
}
function looksLikeLocallyExpandedPowerShellSource(command: string): boolean {
return /(?:^|[;{}]\s*)=\s*[^=]/u.test(command);
}
function windowsRouteRepeatedWinOperationMessage(route: ParsedSshRoute, nextArgs: string[]): string {
const entrypoint = sshDisplayEntrypoint();
const suffix = nextArgs.length > 0 ? ` ${nextArgs.join(" ")}` : " ps";
return `unsupported ssh win operation: win; route ${route.raw} already selects the Windows plane. ` +
`Write the operation directly after the route, for example \`${entrypoint} ${route.raw}${suffix}\`, not \`${entrypoint} ${route.raw} win${suffix}\`.`;
}
function windowsUnsupportedOperationMessage(route: ParsedSshRoute, operation: string): string {
const entrypoint = sshDisplayEntrypoint();
const gitHint = operation === "sed" || operation === "bash" || operation === "sh"
? ` For repository commands on Windows, use \`${entrypoint} ${route.raw} git status --short --branch\`, \`${entrypoint} ${route.raw} git commit -m <message>\`, or wrap custom commands with \`${entrypoint} ${route.raw} ps <<'PS'\`.`
: ` If you meant to run a repository command, use \`${entrypoint} ${route.raw} git status --short --branch\`, \`${entrypoint} ${route.raw} git commit -m <message>\`, or wrap it with \`${entrypoint} ${route.raw} ps <<'PS'\`.`;
return `unsupported ssh win operation: ${operation}; Windows route operations are ps, cmd, skills, apply-patch, upload/download, git, and read-only fs helpers pwd|ls|cat|head|tail|stat|wc|rg.${gitHint}`;
}
function parseWindowsGitInvocation(route: ParsedSshRoute, gitArgs: string[]): ParsedSshArgs {
const subcommand = gitArgs[0] ?? "";
if (subcommand.length === 0) throw new Error(`ssh ${route.raw} git requires a git subcommand, for example: ${sshDisplayEntrypoint()} ${route.raw} git status --short --branch`);
if (subcommand === "exact-commit") {
return {
remoteCommand: buildWindowsPowerShellInvocation(windowsGitExactCommitScript(route, gitArgs.slice(1))),
requiresStdin: false,
invocationKind: "helper",
};
}
if (subcommand === "commit") validateWindowsGitCommitArgs(route, gitArgs);
const commandLine = ["git", ...gitArgs].map((value) => windowsCmdArgument(value, route)).join(" ");
return {
remoteCommand: buildWindowsPowerShellInvocation(buildWindowsCmdLauncherScript(buildWindowsCmdLine(commandLine, route.workspace))),
requiresStdin: false,
invocationKind: "argv",
};
}
function windowsGitExactCommitScript(route: ParsedSshRoute, args: string[]): string {
const operation = args[0] ?? "";
if (!new Set(["status", "plan", "run", "gc-auto"]).has(operation)) {
throw new Error("ssh win git exact-commit requires status, plan, run, or gc-auto");
}
const paths: string[] = [];
let message = "";
let confirm = false;
for (let index = 1; index < args.length; index += 1) {
const arg = args[index] ?? "";
if (arg === "--path") {
const value = args[index + 1];
if (!value) throw new Error("git exact-commit --path requires a repository-relative path");
if (/^(?:[A-Za-z]:|[\\/])|(?:^|[\\/])\.\.(?:[\\/]|$)/u.test(value)) throw new Error("git exact-commit --path must stay inside the repository");
paths.push(value.replace(/\\/gu, "/"));
index += 1;
continue;
}
if (arg === "--message") {
const value = args[index + 1];
if (!value) throw new Error("git exact-commit --message requires a value");
message = value;
index += 1;
continue;
}
if (arg === "--confirm") {
confirm = true;
continue;
}
throw new Error(`unsupported git exact-commit option: ${arg}`);
}
if ((operation === "plan" || operation === "run") && paths.length === 0) throw new Error(`git exact-commit ${operation} requires at least one --path`);
if (operation === "run" && message.length === 0) throw new Error("git exact-commit run requires --message");
if ((operation === "run" || operation === "gc-auto") && !confirm) throw new Error(`git exact-commit ${operation} requires --confirm`);
const cwd = powerShellSingleQuote(route.workspace ?? ".");
const pathList = paths.map((value) => powerShellSingleQuote(value)).join(",");
return `$ErrorActionPreference='Stop'
$ProgressPreference='SilentlyContinue'
[Console]::InputEncoding=[System.Text.UTF8Encoding]::new()
[Console]::OutputEncoding=[System.Text.UTF8Encoding]::new()
$OutputEncoding=[System.Text.UTF8Encoding]::new()
Set-Location -LiteralPath ${cwd}
$operation=${powerShellSingleQuote(operation)}
$paths=@(${pathList})
$message=${powerShellSingleQuote(message)}
$gitWarnings=[Collections.Generic.List[string]]::new()
function Invoke-Git([string[]]$GitArgs) {
$previousErrorActionPreference=$ErrorActionPreference
try {
$ErrorActionPreference='Continue'
$records=@(& git @GitArgs 2>&1)
$nativeExitCode=$LASTEXITCODE
$stderr=(($records | Where-Object { $_ -is [Management.Automation.ErrorRecord] } | ForEach-Object { $_.Exception.Message }) -join [Environment]::NewLine).Trim()
$stdout=(($records | Where-Object { $_ -isnot [Management.Automation.ErrorRecord] } | ForEach-Object { $_.ToString() }) -join [Environment]::NewLine).Trim()
if($nativeExitCode -ne 0){throw ((@($stdout,$stderr) | Where-Object { $_ }) -join [Environment]::NewLine)}
if($stderr){[void]$gitWarnings.Add($stderr)}
return $stdout
} finally {
$ErrorActionPreference=$previousErrorActionPreference
}
}
function Object-Status {
$values=@{}
(Invoke-Git @('count-objects','-v')).Split([Environment]::NewLine) | ForEach-Object { $pair=$_.Split(':',2); if($pair.Count -eq 2){$values[$pair[0].Trim()]=[int64]$pair[1].Trim()} }
return $values
}
$before=Object-Status
$head=Invoke-Git @('rev-parse','--verify','HEAD^{commit}')
$branch=Invoke-Git @('symbolic-ref','-q','HEAD')
if($operation -eq 'status') {
[ordered]@{ok=$true;operation=$operation;head=$head;branch=$branch;objects=$before;warnings=@($gitWarnings);safeStop='read-only; no objects were pruned'} | ConvertTo-Json -Depth 8 -Compress
exit 0
}
if($operation -eq 'gc-auto') {
$gcOutput=Invoke-Git @('gc','--auto')
$after=Object-Status
[ordered]@{ok=$true;operation=$operation;head=$head;before=$before;after=$after;gcOutput=$gcOutput;warnings=@($gitWarnings);safeStop='git gc --auto only; Git recent-object and reflog retention remain authoritative; no direct prune'} | ConvertTo-Json -Depth 8 -Compress
exit 0
}
$gitDir=Invoke-Git @('rev-parse','--path-format=absolute','--git-dir')
$indexPath=Invoke-Git @('rev-parse','--path-format=absolute','--git-path','index')
$unrelatedPathspec=@('.')+@($paths | ForEach-Object { ':(exclude)'+$_ })
$unrelatedBefore=Invoke-Git (@('status','--porcelain=v2','--')+$unrelatedPathspec)
$tempIndex=Join-Path $gitDir ('unidesk-exact-index-'+[Guid]::NewGuid().ToString('N'))
try {
$env:GIT_INDEX_FILE=$tempIndex
[void](Invoke-Git @('read-tree',$head))
[void](Invoke-Git (@('add','--')+$paths))
$tree=Invoke-Git @('write-tree')
$parentTree=Invoke-Git @('rev-parse',($head+'^{tree}'))
if($tree -eq $parentTree){throw 'selected paths do not change the HEAD tree'}
if($operation -eq 'plan') {
[ordered]@{ok=$true;operation=$operation;head=$head;branch=$branch;tree=$tree;paths=$paths;tempIndex=$tempIndex;objectsBefore=$before;warnings=@($gitWarnings);safeStop='plan only; ref and original index unchanged'} | ConvertTo-Json -Depth 8 -Compress
exit 0
}
$commit=($message | & git commit-tree $tree -p $head).Trim()
if($LASTEXITCODE -ne 0 -or !$commit){throw 'git commit-tree failed'}
[void](Invoke-Git @('update-ref','-m',('unidesk exact-commit: '+$message),$branch,$commit,$head))
Remove-Item Env:GIT_INDEX_FILE -ErrorAction SilentlyContinue
[void](Invoke-Git (@('reset',$commit,'--')+$paths))
$unrelatedAfter=Invoke-Git (@('status','--porcelain=v2','--')+$unrelatedPathspec)
if($unrelatedAfter -ne $unrelatedBefore){throw 'unselected worktree/index fingerprint changed after exact commit'}
$after=Object-Status
[ordered]@{ok=$true;operation=$operation;oldHead=$head;commit=$commit;tree=$tree;branch=$branch;paths=$paths;tempIndex=$tempIndex;unselectedStateUnchanged=$true;selectedPathsIndexUpdated=$true;objectsBefore=$before;objectsAfter=$after;warnings=@($gitWarnings);safeStop='ref updated with old-value CAS; no prune performed'} | ConvertTo-Json -Depth 8 -Compress
} finally {
Remove-Item Env:GIT_INDEX_FILE -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $tempIndex -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath ($tempIndex+'.lock') -Force -ErrorAction SilentlyContinue
}
exit 0`;
}
function validateWindowsGitCommitArgs(route: ParsedSshRoute, gitArgs: string[]): void {
const hasNonInteractiveMessage = gitArgs.slice(1).some((arg) => (
arg === "--dry-run" ||
arg === "--no-edit" ||
arg === "-m" ||
arg.startsWith("-m") ||
arg === "--message" ||
arg.startsWith("--message=") ||
arg === "-F" ||
arg.startsWith("-F") ||
arg === "--file" ||
arg.startsWith("--file=") ||
arg === "-C" ||
arg.startsWith("-C") ||
arg === "--reuse-message" ||
arg.startsWith("--reuse-message=") ||
arg.startsWith("--fixup=")
));
if (hasNonInteractiveMessage) return;
const entrypoint = sshDisplayEntrypoint();
throw new Error(`ssh win git commit would open an editor; use ${entrypoint} ${route.raw} git commit -m <message>, ${entrypoint} ${route.raw} git commit --no-edit, or wrap an interactive command with ${entrypoint} ${route.raw} ps <<'PS'`);
}
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 windowsCmdArgument(value: string, route: ParsedSshRoute): string {
if (value.length === 0) return "\"\"";
if (/[\r\n"]/u.test(value) || /[&|<>^%!]/u.test(value)) {
const entrypoint = sshDisplayEntrypoint();
throw new Error(`ssh win git argument contains characters that require shell review; use ${entrypoint} ${route.raw} ps <<'PS' for this command`);
}
return /\s/u.test(value) ? `"${value}"` : value;
}
function parseWindowsSkillsOptions(args: string[]): { limit: number; scopes: Array<"agents" | "codex">; name: string | null; filter: string | null } {
let limit = 100;
let scope: "agents" | "codex" | "all" = "agents";
let name: string | null = null;
let filter: string | null = null;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index] ?? "";
if (arg === "--limit") {
const value = args[index + 1];
if (value === undefined) throw new Error("ssh win skills --limit requires a value");
limit = positiveInt(value, "ssh win skills --limit");
index += 1;
continue;
}
if (arg === "--scope") {
const value = args[index + 1];
if (value === undefined) throw new Error("ssh win skills --scope requires a value");
if (value !== "agents" && value !== "codex" && value !== "all") throw new Error("ssh win skills --scope must be one of: agents, codex, all");
scope = value;
index += 1;
continue;
}
if (arg === "--all") {
scope = "all";
continue;
}
if (arg === "--name" || arg === "--filter") {
const value = args[index + 1];
if (value === undefined || value.length === 0) throw new Error(`ssh win skills ${arg} requires a value`);
if (arg === "--name") name = value;
else filter = value;
index += 1;
continue;
}
throw new Error(`unsupported ssh win skills option: ${arg}`);
}
if (name !== null && filter !== null) throw new Error("ssh win skills accepts only one of --name or --filter");
return { limit, scopes: scope === "all" ? ["agents", "codex"] : [scope], name, filter };
}
function buildWindowsSkillsDiscoveryScript(args: string[]): string {
const options = parseWindowsSkillsOptions(args);
const rootEntries = options.scopes.map((scope) => {
const relative = scope === "agents" ? ".agents\\skills" : ".codex\\skills";
return `@{ Scope = ${powerShellSingleQuote(scope)}; Path = (Join-Path $env:USERPROFILE ${powerShellSingleQuote(relative)}) }`;
}).join(", ");
return [
"$ErrorActionPreference = 'Stop';",
"$ProgressPreference = 'SilentlyContinue';",
"[Console]::InputEncoding = [System.Text.UTF8Encoding]::new();",
"[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new();",
"$OutputEncoding = [System.Text.UTF8Encoding]::new();",
`$limit = ${options.limit};`,
`$exactName = ${powerShellSingleQuote(options.name ?? "")};`,
`$filter = ${powerShellSingleQuote(options.filter ?? "")};`,
`$roots = @(${rootEntries});`,
"$rootRecords = @();",
"$skills = @();",
"foreach ($root in $roots) {",
" $exists = Test-Path -LiteralPath $root.Path -PathType Container;",
" $rootRecords += [pscustomobject]@{ scope = $root.Scope; path = $root.Path; exists = $exists };",
" if (-not $exists) { continue };",
" foreach ($dir in @(Get-ChildItem -LiteralPath $root.Path -Directory -Force | Sort-Object Name)) {",
" if ($skills.Count -ge $limit) { break };",
" $skillFile = Join-Path $dir.FullName 'SKILL.md';",
" $hasSkillMd = Test-Path -LiteralPath $skillFile -PathType Leaf;",
" if (-not $hasSkillMd) { continue };",
" $name = $dir.Name;",
" $description = '';",
" if ($hasSkillMd) {",
" foreach ($line in @(Get-Content -LiteralPath $skillFile -Encoding UTF8 -TotalCount 80)) {",
" if ($line -match '^name:\\s*(.+)$') { $name = $Matches[1].Trim(); continue };",
" if ($line -match '^description:\\s*(.+)$') { $description = $Matches[1].Trim(); continue };",
" }",
" }",
" if (-not [string]::IsNullOrWhiteSpace($exactName) -and $name -ine $exactName -and $dir.Name -ine $exactName) { continue };",
" if (-not [string]::IsNullOrWhiteSpace($filter) -and $name.IndexOf($filter, [StringComparison]::OrdinalIgnoreCase) -lt 0 -and $dir.Name.IndexOf($filter, [StringComparison]::OrdinalIgnoreCase) -lt 0 -and $description.IndexOf($filter, [StringComparison]::OrdinalIgnoreCase) -lt 0) { continue };",
" $skills += [pscustomobject]@{ scope = $root.Scope; name = $name; directoryName = $dir.Name; path = $dir.FullName; skillFile = $skillFile; hasSkillMd = $hasSkillMd; description = $description };",
" }",
"}",
"$payload = [pscustomobject]@{ ok = $true; command = 'unidesk ssh win skills'; generatedAt = (Get-Date).ToUniversalTime().ToString('o'); user = $env:USERNAME; userProfile = $env:USERPROFILE; query = [pscustomobject]@{ name = $exactName; filter = $filter }; counts = [pscustomobject]@{ roots = $rootRecords.Count; skills = $skills.Count; limit = $limit }; roots = $rootRecords; skills = @($skills) };",
"$payload | ConvertTo-Json -Depth 6;",
].join(" ");
}
export function powerShellSingleQuote(value: string): string {
return `'${value.replace(/'/g, "''")}'`;
}
function buildWindowsCmdLauncherScript(cmdLine: string): string {
return [
"$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(" ");
}
function buildWindowsCmdStdinLauncherScript(cwd: string | null): string {
const prefixLines = [
"@echo off",
"chcp 65001>nul",
'set "PYTHONUTF8=1"',
'set "PYTHONIOENCODING=utf-8"',
...(cwd === null ? [] : [`cd /d ${windowsCmdQuote(cwd)}`]),
];
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';",
"$script = [Console]::In.ReadToEnd();",
"$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 cmd requires a command line or stdin batch script'); exit 2 }",
`$prefix = ${powerShellSingleQuote(`${prefixLines.join("\r\n")}\r\n`)};`,
"$temp = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), ('unidesk-win-cmd-' + [guid]::NewGuid().ToString('N') + '.cmd'));",
"try {",
" [System.IO.File]::WriteAllText($temp, $prefix + $script, [System.Text.UTF8Encoding]::new($false));",
" $cmdArg = '\"' + $temp + '\"';",
` & ${powerShellSingleQuote(windowsCmdExeNativePath)} /d /s /c $cmdArg;`,
" $code = $LASTEXITCODE;",
"} finally {",
" Remove-Item -LiteralPath $temp -Force -ErrorAction SilentlyContinue;",
"}",
"exit $code;",
].join(" ");
}
const windowsPowerShellJsonSafetyPreludeLines = [
"function ConvertTo-UniDeskPlainJsonValue {",
" param([object]$Value, [int]$Depth = 6, [int]$Level = 0)",
" if ($null -eq $Value) { return $null }",
" if ($Value -is [string]) { return [string]$Value }",
" if ($Value -is [char]) { return [string]$Value }",
" if ($Value -is [bool] -or $Value -is [byte] -or $Value -is [sbyte] -or $Value -is [int16] -or $Value -is [uint16] -or $Value -is [int] -or $Value -is [uint32] -or $Value -is [long] -or $Value -is [uint64] -or $Value -is [single] -or $Value -is [double] -or $Value -is [decimal]) { return $Value }",
" if ($Value -is [datetime]) { return $Value.ToUniversalTime().ToString('o') }",
" if ($Level -ge $Depth) { return [string]$Value }",
" if ($Value -is [System.Collections.IDictionary]) {",
" $map = [ordered]@{}",
" foreach ($key in $Value.Keys) { $map[[string]$key] = ConvertTo-UniDeskPlainJsonValue -Value $Value[$key] -Depth $Depth -Level ($Level + 1) }",
" return $map",
" }",
" if ($Value -is [System.Collections.IEnumerable] -and -not ($Value -is [string])) {",
" $items = New-Object System.Collections.ArrayList",
" foreach ($item in $Value) { [void]$items.Add((ConvertTo-UniDeskPlainJsonValue -Value $item -Depth $Depth -Level ($Level + 1))) }",
" return $items.ToArray()",
" }",
" $skip = @('PSPath','PSParentPath','PSChildName','PSDrive','PSProvider','ReadCount')",
" $props = @($Value.PSObject.Properties | Where-Object { @('NoteProperty','Property','AliasProperty') -contains $_.MemberType.ToString() -and -not ($skip -contains $_.Name) })",
" if ($props.Count -eq 0) { return [string]$Value }",
" $out = [ordered]@{}",
" foreach ($prop in $props) {",
" try { $out[$prop.Name] = ConvertTo-UniDeskPlainJsonValue -Value $prop.Value -Depth $Depth -Level ($Level + 1) }",
" catch { $out[$prop.Name] = '<unreadable>' }",
" }",
" return $out",
"}",
"function ConvertTo-Json {",
" [CmdletBinding()]",
" param(",
" [Parameter(ValueFromPipeline=$true)] [object]$InputObject,",
" [int]$Depth = 2,",
" [switch]$Compress,",
" [switch]$EnumsAsStrings",
" )",
" begin { $items = New-Object System.Collections.ArrayList }",
" process { [void]$items.Add($InputObject) }",
" end {",
" if ($items.Count -eq 0) { $value = $null } elseif ($items.Count -eq 1) { $value = $items[0] } else { $value = $items.ToArray() }",
" $plain = ConvertTo-UniDeskPlainJsonValue -Value $value -Depth $Depth -Level 0",
" if ($Compress) {",
" Microsoft.PowerShell.Utility\\ConvertTo-Json -InputObject $plain -Depth $Depth -Compress",
" } else {",
" Microsoft.PowerShell.Utility\\ConvertTo-Json -InputObject $plain -Depth $Depth",
" }",
" }",
"}",
];
export 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()",
"$PSDefaultParameterValues['Get-Content:Encoding'] = 'utf8'",
"$env:PYTHONUTF8 = '1'",
"$env:PYTHONIOENCODING = 'utf-8'",
...windowsPowerShellJsonSafetyPreludeLines,
...(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 buildWindowsPowerShellArgvLauncherScript(commandArgs: string[], cwd: string | null): string {
const command = commandArgs[0] ?? "";
if (command.trim().length === 0) throw new Error("ssh win ps requires a command before argv arguments");
const args = commandArgs.slice(1);
const payloadJsonB64 = Buffer.from(JSON.stringify({
command,
args,
nativeArguments: args.map(windowsNativeProcessArgument).join(" "),
}), "utf8").toString("base64");
const script = [
`$payloadJsonB64 = ${powerShellSingleQuote(payloadJsonB64)};`,
"$payload = ConvertFrom-Json -InputObject ([System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($payloadJsonB64)));",
"$command = [string]$payload.command;",
"$commandArgs = @();",
"if ($null -ne $payload.args) { foreach ($item in @($payload.args)) { $commandArgs += [string]$item } }",
"$resolvedCommand = Get-Command -Name $command -ErrorAction Stop;",
"if ($resolvedCommand.CommandType -eq 'Application') {",
" $startInfo = [Diagnostics.ProcessStartInfo]::new();",
" $startInfo.FileName = $resolvedCommand.Source;",
" $startInfo.Arguments = [string]$payload.nativeArguments;",
" $startInfo.WorkingDirectory = (Get-Location).ProviderPath;",
" $startInfo.UseShellExecute = $false;",
" $process = [Diagnostics.Process]::new(); $process.StartInfo = $startInfo;",
" if (-not $process.Start()) { throw ('failed to start native command: ' + $command) }",
" $process.WaitForExit(); $global:LASTEXITCODE = $process.ExitCode;",
"} else { & $command @commandArgs; }",
].join(" ");
return buildWindowsPowerShellScriptRunner(powerShellSingleQuote(script), cwd);
}
function windowsNativeProcessArgument(value: string): string {
if (value.length === 0) return '""';
let quoted = '"';
let backslashes = 0;
for (const character of value) {
if (character === "\\") {
backslashes += 1;
continue;
}
if (character === '"') {
quoted += "\\".repeat(backslashes * 2 + 1) + '"';
backslashes = 0;
continue;
}
quoted += "\\".repeat(backslashes) + character;
backslashes = 0;
}
return quoted + "\\".repeat(backslashes * 2) + '"';
}
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,
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-EncodedCommand",
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;
}
function parseK3sRouteTargetSegments(rawResource: string | null, rawContainer: string | null): { resource: string | null; container: string | null; workspace: string | null } {
const resourceParts = splitK3sResourceWorkspace(rawResource);
const containerParts = splitK3sContainerWorkspace(rawContainer);
return {
resource: resourceParts.resource,
container: containerParts.container,
workspace: combineK3sRouteWorkspace(resourceParts.workspace, containerParts.workspace),
};
}
function normalizeK3sRouteRestSegments(rest: string[]): string[] {
const normalized: string[] = [];
for (let index = 0; index < rest.length; index += 1) {
const current = rest[index] ?? "";
const kindRoute = k3sColonKindRoute(current);
if (kindRoute === null) {
normalized.push(current);
continue;
}
if (kindRoute.name === null) {
const next = rest[index + 1];
if (next === undefined || next.length === 0) throw new Error(`ssh k3s ${kindRoute.kind}: route requires a resource name after ${kindRoute.kind}:`);
normalized.push(`${kindRoute.kind}/${next}`);
index += 1;
continue;
}
normalized.push(`${kindRoute.kind}/${kindRoute.name}`);
}
return normalized;
}
function rejectK3sSlashKindRouteSegments(target: string, rest: string[]): void {
const resourceSegment = rest[1];
if (resourceSegment === undefined || resourceSegment.length === 0) return;
const slashIndex = resourceSegment.indexOf("/");
if (slashIndex < 0) return;
const head = resourceSegment.slice(0, slashIndex);
if (!isK3sResourceKindAlias(head)) return;
throw new Error(
`ssh k3s route segment "${resourceSegment}" in "${target}" uses "/" between Kubernetes route parts. ` +
`":" is the distributed route separator and "/" is only for the in-container filesystem cwd. ` +
`Use "trans <provider>:k3s:<namespace>:${head}:<name>[:<container>] ..." for ${head} routes, ` +
`and use "--cwd /path" or a route cwd suffix only for filesystem paths.`
);
}
function k3sColonKindRoute(value: string): { kind: string; name: string | null } | null {
const colonIndex = value.indexOf(":");
const head = colonIndex < 0 ? value : value.slice(0, colonIndex);
if (!isK3sResourceKindAlias(head)) return null;
if (colonIndex < 0) return { kind: head, name: null };
const name = value.slice(colonIndex + 1);
if (name.length === 0) throw new Error(`ssh k3s ${head}: route requires a resource name after ${head}:`);
return { kind: head, name };
}
function splitK3sResourceWorkspace(value: string | null): { resource: string | null; workspace: string | null } {
if (value === null || value.length === 0) return { resource: null, workspace: null };
const parts = value.split("/");
if (parts.length <= 1) return { resource: value, workspace: null };
if (isK3sResourceKindAlias(parts[0] ?? "") && (parts[1] ?? "").length > 0) {
const workspaceParts = parts.slice(2);
return {
resource: `${parts[0]}/${parts[1]}`,
workspace: workspaceParts.length > 0 ? `/${workspaceParts.join("/")}` : null,
};
}
return {
resource: parts[0] ?? value,
workspace: parts.length > 1 ? `/${parts.slice(1).join("/")}` : null,
};
}
function splitK3sContainerWorkspace(value: string | null): { container: string | null; workspace: string | null } {
if (value === null || value.length === 0) return { container: null, workspace: null };
const parts = value.split("/");
if (parts.length <= 1) return { container: value, workspace: null };
return { container: parts[0] ?? value, workspace: `/${parts.slice(1).join("/")}` };
}
function combineK3sRouteWorkspace(first: string | null, second: string | null): string | null {
if (first !== null && second !== null && first !== second) throw new Error("ssh k3s route workspace can be specified once, either after the workload or after the container");
return first ?? second;
}
function isK3sResourceKindAlias(value: string): boolean {
return k3sResourceKindAliases.has(value);
}
function k3sOperationInRouteMessage(target: string, operation: string): string {
const providerId = target.split(":")[0] || "<provider>";
if (operation === "v2" || operation === "patch" || operation === "patch-v1") {
return `ssh k3s route must locate a target only; remote patch entrypoints are "apply-patch" for the default v2 engine and "apply-patch-v1" for the legacy helper instead of "${target}"`;
}
if (operation === "script" || operation === "shell") {
return `ssh k3s route must locate a target only; operation "${operation}" has been removed because it hides the shell dialect; use explicit "sh" or "bash" after the route instead of "${target}"`;
}
const operationExample = operation === "guard" ? "guard" : `${operation} ...`;
return `ssh k3s route must locate a target only; put operation "${operation}" after the route, for example "ssh ${providerId}:k3s ${operationExample}" or "ssh ${providerId}:k3s:<namespace>:<workload> ${operationExample}" instead of "${target}"`;
}
export function shellArgv(args: string[]): string {
return args.map(shellQuote).join(" ");
}
export function shellQuote(value: string): string {
return `'${value.replace(/'/g, `'\\''`)}'`;
}
function positiveInt(value: string, option: string): number {
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${option} must be a positive integer`);
return parsed;
}
function optionalPositiveInt(value: string, option: string): string {
return String(positiveInt(value, option));
}
function findOptionValue(args: string[], index: number, option: string): string {
const value = args[index + 1];
if (value === undefined || value.length === 0) throw new Error(`ssh find ${option} requires a value`);
return value;
}
function buildFindCommand(args: string[]): string {
const paths: string[] = [];
const predicates: string[] = [];
const patternPredicates: Array<[string, string]> = [];
let limit: number | null = null;
let sortOutput = false;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index] ?? "";
if (arg === "--limit") {
const value = findOptionValue(args, index, arg);
limit = positiveInt(value, "ssh find --limit");
index += 1;
continue;
}
if (arg === "--sort") {
sortOutput = true;
continue;
}
if (arg === "--max-depth" || arg === "-maxdepth" || arg === "--min-depth" || arg === "-mindepth") {
const value = findOptionValue(args, index, arg);
const findArg = arg === "--max-depth" ? "-maxdepth" : arg === "--min-depth" ? "-mindepth" : arg;
predicates.push(findArg, String(positiveInt(value, `ssh find ${arg}`)));
index += 1;
continue;
}
if (arg === "--type" || arg === "-type") {
const value = findOptionValue(args, index, arg);
if (!/^[bcdpfls]$/u.test(value)) throw new Error("ssh find --type must be one of: b c d p f l s");
predicates.push("-type", value);
index += 1;
continue;
}
if (arg === "--name" || arg === "-name" || arg === "--iname" || arg === "-iname" || arg === "--path" || arg === "-path" || arg === "--ipath" || arg === "-ipath") {
const value = findOptionValue(args, index, arg);
const findArg = arg.startsWith("--") ? `-${arg.slice(2)}` : arg;
patternPredicates.push([findArg, value]);
index += 1;
continue;
}
if (arg === "--contains" || arg === "--icontains") {
const value = findOptionValue(args, index, arg);
const findArg = arg === "--contains" ? "-name" : "-iname";
patternPredicates.push([findArg, `*${value}*`]);
index += 1;
continue;
}
if (arg === "--mtime" || arg === "-mtime" || arg === "--mmin" || arg === "-mmin" || arg === "--size" || arg === "-size") {
const value = findOptionValue(args, index, arg);
const findArg = arg.startsWith("--") ? `-${arg.slice(2)}` : arg;
predicates.push(findArg, value);
index += 1;
continue;
}
if (arg.startsWith("-")) {
throw new Error(`unsupported ssh find option: ${arg}`);
}
paths.push(arg);
}
const findArgs = ["find", ...(paths.length > 0 ? paths : ["."]), ...predicates];
if (patternPredicates.length === 1) {
const [kind, pattern] = patternPredicates[0]!;
findArgs.push(kind, pattern);
} else if (patternPredicates.length > 1) {
findArgs.push("(");
patternPredicates.forEach(([kind, pattern], index) => {
if (index > 0) findArgs.push("-o");
findArgs.push(kind, pattern);
});
findArgs.push(")");
}
findArgs.push("-print");
let command = shellArgv(findArgs);
if (sortOutput) command = `${command} | sort`;
if (limit !== null) command = `${command} | head -n ${limit}`;
return command;
}
function parseK3sRouteArgs(route: ParsedSshRoute, args: string[]): ParsedSshArgs {
if (route.entry === null && route.namespace === null && route.resource === null) {
return parseK3sControlPlaneOperation(route, args);
}
if (route.namespace === null || route.resource === null) {
throw new Error("ssh k3s target route requires provider:k3s:<namespace>:<workload|pod:pod-name>[:<container>]; use --cwd /path for an in-container filesystem cwd");
}
return parseK3sTargetOperation(route, args);
}
function parseK3sControlPlaneOperation(route: ParsedSshRoute, args: string[]): ParsedSshArgs {
const operation = args[0] ?? "guard";
if (operation === "upload" || operation === "download") {
throw new Error(`ssh ${route.providerId}:k3s ${operation} requires a workload route: ssh ${route.providerId}:k3s:<namespace>:<workload> ${operation} ...`);
}
if (operation === "apply-patch" || operation === "apply-patch-v1") {
throw new Error(`ssh ${route.providerId}:k3s apply-patch requires a workload route: ssh ${route.providerId}:k3s:<namespace>:<workload> apply-patch`);
}
if (operation === "patch" || operation === "patch-v1" || operation === "v2") {
throw new Error("remote patch entrypoints are `apply-patch` for the default v2 engine and `apply-patch-v1` for the legacy helper");
}
if (operation === "script" || operation === "shell") {
throw removedShellAliasError(operation, route.raw, args.slice(1));
}
if (operation === "sh" || operation === "bash") {
return buildK3sScriptOperation(args.slice(1), operation, `ssh ${route.providerId}:k3s ${operation}`);
}
if (operation === "guard") {
if (args.length > 1) throw new Error(`ssh ${route.providerId}:k3s guard does not accept extra arguments`);
return { remoteCommand: buildK3sGuardCommand(route.providerId), requiresStdin: false, invocationKind: "helper" };
}
return { remoteCommand: buildK3sCommand(route.providerId, route.raw, args), requiresStdin: false, invocationKind: "helper" };
}
function parseK3sTargetOperation(route: ParsedSshRoute, args: string[]): ParsedSshArgs {
const targetArgs = k3sRouteTargetArgs(route);
if (args.length === 0) {
return {
remoteCommand: buildK3sTargetObjectCommand("get", route, ["-o", "wide"]),
requiresStdin: false,
invocationKind: "helper",
};
}
const operation = args[0] ?? "";
const operationArgs = args.slice(1);
if (operation === "upload" || operation === "download") {
return { remoteCommand: null, requiresStdin: false, invocationKind: "helper" };
}
if (operation === "apply-patch") {
if (isApplyPatchV2HelpArgs(operationArgs)) {
return { remoteCommand: null, requiresStdin: false, invocationKind: "helper" };
}
return { remoteCommand: null, requiresStdin: true, invocationKind: "helper" };
}
if (operation === "apply-patch-v1") return buildK3sApplyPatchCommand([...targetArgs, ...operationArgs]);
if (operation === "patch" || operation === "patch-v1" || operation === "v2") {
throw new Error("remote patch entrypoints are `apply-patch` for the default v2 engine and `apply-patch-v1` for the legacy helper");
}
if (operation === "script" || operation === "shell") {
throw removedShellAliasError(operation, route.raw, operationArgs);
}
if (operation === "sh" || operation === "bash") {
return buildK3sScriptOperation([...targetArgs, ...operationArgs], operation, `ssh ${route.raw} ${operation}`);
}
if (operation === "logs") return { remoteCommand: buildK3sLogsCommand([...targetArgs, ...operationArgs]), requiresStdin: false, invocationKind: "helper" };
if (operation === "argv") {
validateDirectArgvCommand(operation, operationArgs);
return { remoteCommand: buildK3sExecCommand([...targetArgs, ...k3sRouteCommandArgs(operationArgs)]), requiresStdin: false, invocationKind: "argv" };
}
if (operation === "get" || operation === "describe") {
return { remoteCommand: buildK3sTargetObjectCommand(operation, route, operationArgs), requiresStdin: false, invocationKind: "helper" };
}
if (operation === "kubectl") throw new Error(`ssh k3s kubectl is a control-plane operation; use ssh ${route.providerId}:k3s kubectl ...`);
if (operation === "exec") {
const execArgs = k3sRouteExecOperationArgs(operationArgs);
return {
remoteCommand: buildK3sExecCommand([...targetArgs, ...execArgs]),
requiresStdin: execArgs.includes("--stdin") || execArgs.includes("-i"),
invocationKind: "helper",
};
}
return { remoteCommand: buildK3sExecCommand([...targetArgs, ...k3sRouteCommandArgs(args)]), requiresStdin: false, invocationKind: "helper" };
}
function buildK3sTargetObjectCommand(action: "get" | "describe", route: ParsedSshRoute, args: string[]): string {
if (route.namespace === null || route.resource === null) throw new Error(`ssh k3s ${action} target requires namespace and workload route`);
if (args.includes("--follow") || args.includes("-f")) throw new Error(`ssh k3s target ${action} does not support follow mode`);
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", action, "-n", route.namespace, normalizeK3sRouteResource(route.resource), ...args]);
}
export function buildK3sTargetCommand(route: ParsedSshRoute, command: string[], options: { stdin?: boolean } = {}): string {
return buildK3sExecCommand([...k3sRouteTargetArgs(route), ...(options.stdin === true ? ["--stdin"] : []), "--", ...command]);
}
function k3sRouteTargetArgs(route: ParsedSshRoute): string[] {
if (route.namespace === null) throw new Error(`ssh route ${route.raw} requires a namespace segment`);
if (route.resource === null) throw new Error(`ssh route ${route.raw} requires a workload or pod segment`);
return [
"--namespace", route.namespace,
"--resource", normalizeK3sRouteResource(route.resource),
...(route.container === null ? [] : ["--container", route.container]),
...(route.workspace === null ? [] : ["--workdir", route.workspace]),
];
}
function k3sRouteCommandArgs(args: string[]): string[] {
if (args.length === 0) throw new Error("ssh k3s target route requires a command to exec");
return args[0] === "--" ? args : ["--", ...args];
}
function k3sRouteExecOperationArgs(args: string[]): string[] {
if (args.length === 0) throw new Error("ssh k3s target exec operation requires a command to exec");
const result: string[] = [];
for (let index = 0; index < args.length; index += 1) {
const arg = args[index] ?? "";
if (arg === "--") {
if (index === args.length - 1) throw new Error("ssh k3s target exec operation requires a command after --");
return [...result, ...args.slice(index)];
}
if (arg === "--stdin" || arg === "-i" || arg === "--tty" || arg === "-t") {
result.push(arg);
continue;
}
if (arg === "--container" || arg === "-c" || arg === "--workdir" || arg === "--cwd") {
const value = args[index + 1];
if (value === undefined || value.length === 0) throw new Error(`ssh k3s target exec ${arg} requires a value`);
result.push(arg, value);
index += 1;
continue;
}
return [...result, "--", ...args.slice(index)];
}
throw new Error("ssh k3s target exec operation requires a command to exec");
}
export function effectiveApplyPatchV2Invocation(invocation: ParsedSshInvocation, argv: string[]): EffectiveApplyPatchV2Invocation {
if (invocation.route.plane !== "k3s" || isApplyPatchV2HelpArgs(argv)) return { invocation, argv };
let container: string | null = null;
let workspace: string | null = null;
const remaining: string[] = [];
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index] ?? "";
if (arg === "--container" || arg === "-c") {
container = k3sOptionValue(argv, index, `ssh ${invocation.route.raw} apply-patch ${arg}`);
index += 1;
continue;
}
const containerValue = k3sEqualsOptionValue(arg, "--container", `ssh ${invocation.route.raw} apply-patch`);
if (containerValue !== null) {
container = containerValue;
continue;
}
if (arg === "--workdir" || arg === "--cwd") {
workspace = k3sWorkspaceValue(k3sOptionValue(argv, index, `ssh ${invocation.route.raw} apply-patch ${arg}`), `ssh ${invocation.route.raw} apply-patch ${arg}`);
index += 1;
continue;
}
const workdirValue = k3sEqualsOptionValue(arg, "--workdir", `ssh ${invocation.route.raw} apply-patch`) ?? k3sEqualsOptionValue(arg, "--cwd", `ssh ${invocation.route.raw} apply-patch`);
if (workdirValue !== null) {
workspace = k3sWorkspaceValue(workdirValue, `ssh ${invocation.route.raw} apply-patch ${arg.split("=", 1)[0]}`);
continue;
}
remaining.push(arg);
}
const route = invocation.route;
if (container !== null && route.container !== null && container !== route.container) {
throw new Error("ssh k3s apply-patch container can be specified once, either in the route :<container> segment or with --container");
}
return {
invocation: {
...invocation,
route: {
...route,
container: route.container ?? container,
workspace: combineK3sRouteWorkspace(route.workspace, workspace),
},
},
argv: remaining,
};
}
function buildK3sCommand(providerId: string, routeRaw: string, args: string[]): string {
const action = args[0] ?? "";
if (action.length === 0 || action === "--help" || action === "-h" || action === "help") {
throw new Error("ssh k3s requires a subcommand: guard, kubectl, get, describe, logs or exec");
}
if (action === "guard") return buildK3sGuardCommand(providerId);
if (action === "exec") return buildK3sExecCommand(args.slice(1));
if (action === "script" || action === "shell") {
throw removedShellAliasError(action, routeRaw, args.slice(1));
}
if (action === "sh" || action === "bash") {
const parsed = buildK3sScriptOperation(args.slice(1), action, `ssh k3s ${action}`);
if (parsed.remoteCommand === null) throw new Error(`ssh k3s ${action} resolved to an interactive command unexpectedly`);
return parsed.remoteCommand;
}
if (action === "logs") return buildK3sLogsCommand(args.slice(1));
if (action === "kubectl") {
const kubectlArgs = args.slice(1);
if (kubectlArgs.length === 0) throw new Error("ssh k3s kubectl requires kubectl arguments");
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]);
}
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...args]);
}
function buildK3sGuardCommand(providerId: string): string {
const provider = providerId.toUpperCase();
const providerNodeCheck = provider === "D601"
? "printf '%s\\n' \"$nodes\" | grep -Fx d601 >/dev/null || { printf 'native_k3s_guard=blocked provider=%s reason=d601-node-missing\\n' \"$UNIDESK_K3S_PROVIDER_ID\" >&2; exit 1; }"
: provider === "G14"
? "printf '%s\\n' \"$nodes\" | grep -Fx ubuntu-rog-zephyrus-g14-ga401iv-ga401iv >/dev/null || { printf 'native_k3s_guard=blocked provider=%s reason=g14-node-missing\\n' \"$UNIDESK_K3S_PROVIDER_ID\" >&2; exit 1; }"
: "[ -n \"$nodes\" ] || { printf 'native_k3s_guard=blocked provider=%s reason=node-list-empty\\n' \"$UNIDESK_K3S_PROVIDER_ID\" >&2; exit 1; }";
const script = [
"set -euo pipefail",
`export KUBECONFIG=${shellQuote(nativeK3sKubeconfig)}`,
`export UNIDESK_K3S_PROVIDER_ID=${shellQuote(providerId)}`,
"context=$(kubectl config current-context)",
"server=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')",
"nodes=$(kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{\"\\n\"}{end}')",
"printf 'kubeconfig=%s\\n' \"$KUBECONFIG\"",
"printf 'provider=%s\\n' \"$UNIDESK_K3S_PROVIDER_ID\"",
"printf 'context=%s\\n' \"$context\"",
"printf 'server=%s\\n' \"$server\"",
"printf 'nodes=%s\\n' \"$(printf '%s' \"$nodes\" | tr '\\n' ' ')\"",
providerNodeCheck,
"case \"$server\" in *127.0.0.1:11700*|*docker-desktop*) printf 'native_k3s_guard=blocked reason=docker-desktop-context server=%s\\n' \"$server\" >&2; exit 1;; esac",
"printf 'native_k3s_guard=ok provider=%s\\n' \"$UNIDESK_K3S_PROVIDER_ID\"",
].join("; ");
return shellArgv(["bash", "-c", script]);
}
interface K3sTargetOptions {
namespace: string | null;
resource: string | null;
selector: string | null;
container: string | null;
workspace: string | null;
stdin: boolean;
tty: boolean;
shell: string | null;
command: string[];
kubectlOptions: string[];
}
interface ParseK3sTargetOptionsOptions {
requireCommand: boolean;
allowCommand?: boolean;
allowShell?: boolean;
allowSelector?: boolean;
}
function buildK3sExecCommand(args: string[]): string {
const parsed = parseK3sTargetOptions(args, "ssh k3s exec", { requireCommand: true });
if (parsed.namespace === null) throw new Error("ssh k3s exec requires --namespace <name>");
if (parsed.selector !== null) throw new Error("ssh k3s exec does not support --selector");
if (parsed.resource === null) throw new Error("ssh k3s exec requires --deployment <name>, --pod <name> or --resource <type/name>");
const kubectlArgs = [
"exec",
"-n", parsed.namespace,
parsed.resource,
...(parsed.container === null ? [] : ["-c", parsed.container]),
...(parsed.stdin ? ["-i"] : []),
...(parsed.tty ? ["-t"] : []),
...parsed.kubectlOptions,
"--",
...withK3sWorkspace(parsed, parsed.command),
];
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]);
}
function removedShellAliasError(operation: string, routeRaw: string, operationArgs: string[]): Error {
return new SshRemovedShellAliasError(operation, routeRaw, operationArgs);
}
function sshDisplayEntrypoint(): string {
const raw = process.env.UNIDESK_SSH_ENTRYPOINT?.trim();
return raw === "trans" || raw === "tran" || raw === "ssh" ? raw : "ssh";
}
function removedShellAliasReplacementExamples(entrypoint: string, route: string, operationArgs: string[]): RemovedShellAliasReplacementExamples {
const command = removedShellAliasInlineCommand(operationArgs);
const commandText = command === null ? "<command>" : command;
return {
posixSh: `${entrypoint} ${route} sh -- ${shellQuote(commandText)}`,
bash: `${entrypoint} ${route} bash -- ${shellQuote(commandText)}`,
templatePosixSh: `${entrypoint} ${route} sh -- '<command>'`,
templateBash: `${entrypoint} ${route} bash -- '<command>'`,
};
}
function removedShellAliasInlineCommand(operationArgs: string[]): string | null {
if (operationArgs.length === 0) return null;
const separatorIndex = operationArgs.indexOf("--");
if (separatorIndex >= 0) {
const command = operationArgs.slice(separatorIndex + 1);
return command.length === 1 ? command[0] ?? null : null;
}
return operationArgs.length === 1 && !(operationArgs[0] ?? "").startsWith("-") ? operationArgs[0] ?? null : null;
}
function buildK3sScriptOperation(args: string[], shell: "sh" | "bash", commandName: string): ParsedSshArgs {
const parsed = parseK3sTargetOptions(args, commandName, { requireCommand: false, allowCommand: true });
if (parsed.command.length > 0) {
return { remoteCommand: buildK3sInlineScriptCommand(parsed, shell, commandName), requiresStdin: false, invocationKind: "helper" };
}
return { remoteCommand: buildK3sStdinScriptCommand(parsed, shell, commandName), requiresStdin: true, invocationKind: "helper", stdinPrefix: shellScriptStdinPrefix() };
}
function buildK3sStdinScriptCommand(parsed: K3sTargetOptions, shell: "sh" | "bash", commandName: string): string {
if (parsed.namespace === null && parsed.resource === null) return buildK3sHostScriptCommand(parsed, shell, commandName);
if (parsed.namespace === null) throw new Error(`${commandName} target requires --namespace <name>`);
if (parsed.selector !== null) throw new Error(`${commandName} does not support --selector`);
if (parsed.resource === null) throw new Error(`${commandName} target requires --deployment <name>, --pod <name> or --resource <type/name>`);
if (parsed.tty) throw new Error(`${commandName} does not support --tty; stdin is reserved for the shell body`);
const kubectlArgs = [
"exec",
"-i",
"-n", parsed.namespace,
parsed.resource,
...(parsed.container === null ? [] : ["-c", parsed.container]),
...parsed.kubectlOptions,
"--",
...withK3sWorkspace(parsed, [shell, "-s", "--", ...parsed.command]),
];
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]);
}
function buildK3sInlineScriptCommand(parsed: K3sTargetOptions, shell: "sh" | "bash", commandName: string): string {
if (parsed.command.length === 0) throw new Error(`${commandName} -- requires a command`);
if (parsed.command.length !== 1) throw new Error(`${commandName} -- requires exactly one shell command string; use exec/argv for direct argv commands`);
if (parsed.selector !== null) throw new Error(`${commandName} -- does not support --selector`);
if (parsed.tty) throw new Error(`${commandName} does not support --tty; stdin is reserved for the shell body`);
if (parsed.stdin) throw new Error(`${commandName} -- does not accept --stdin`);
const command = [shell, "-c", shellScriptWithCompatibility(parsed.command[0] ?? "")];
if (parsed.namespace === null && parsed.resource === null) {
if (parsed.container !== null) throw new Error(`${commandName} without a workload does not accept --container`);
if (parsed.workspace !== null) throw new Error(`${commandName} without a workload does not accept --workdir`);
if (parsed.kubectlOptions.length > 0) throw new Error(`${commandName} without a workload does not accept kubectl log options`);
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, ...command]);
}
if (parsed.namespace === null) throw new Error(`${commandName} target requires --namespace <name>`);
if (parsed.resource === null) throw new Error(`${commandName} target requires --deployment <name>, --pod <name> or --resource <type/name>`);
const kubectlArgs = [
"exec",
"-n", parsed.namespace,
parsed.resource,
...(parsed.container === null ? [] : ["-c", parsed.container]),
...parsed.kubectlOptions,
"--",
...withK3sWorkspace(parsed, command),
];
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]);
}
function buildK3sApplyPatchCommand(args: string[]): ParsedSshArgs {
const parsed = parseK3sTargetOptions(args, "ssh k3s apply-patch", { requireCommand: false, allowCommand: true });
if (parsed.namespace === null) throw new Error("ssh k3s apply-patch requires --namespace <name>");
if (parsed.resource === null) throw new Error("ssh k3s apply-patch requires --deployment <name>, --pod <name> or --resource <type/name>");
if (parsed.tty) throw new Error("ssh k3s apply-patch does not support --tty; stdin is reserved for the patch body");
if (parsed.stdin) throw new Error("ssh k3s apply-patch does not accept --stdin; stdin is always the patch body");
if (parsed.shell !== null) throw new Error("ssh k3s apply-patch does not accept --shell");
if (parsed.kubectlOptions.length > 0) throw new Error("ssh k3s apply-patch does not accept kubectl log options");
const kubectlArgs = [
"exec",
"-i",
"-n", parsed.namespace,
parsed.resource,
...(parsed.container === null ? [] : ["-c", parsed.container]),
"--",
...withK3sWorkspace(parsed, ["sh", "-s", "--", ...parsed.command]),
];
const wrapper = podApplyPatchStdinWrapper();
return {
remoteCommand: shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]),
requiresStdin: true,
invocationKind: "helper",
stdinPrefix: wrapper.prefix,
stdinSuffix: wrapper.suffix,
};
}
function buildK3sHostScriptCommand(parsed: K3sTargetOptions, shell: "sh" | "bash", commandName: string): string {
if (parsed.tty) throw new Error(`${commandName} does not support --tty; stdin is reserved for the shell body`);
if (parsed.stdin) throw new Error(`${commandName} does not accept --stdin; stdin is always the shell body`);
if (parsed.container !== null) throw new Error(`${commandName} without a workload does not accept --container`);
if (parsed.workspace !== null) throw new Error(`${commandName} without a workload does not accept --workdir`);
if (parsed.kubectlOptions.length > 0) throw new Error(`${commandName} without a workload does not accept kubectl log options`);
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, shell, "-s", "--", ...parsed.command]);
}
function buildK3sLogsCommand(args: string[]): string {
const parsed = parseK3sTargetOptions(args, "ssh k3s logs", { requireCommand: false, allowSelector: true });
if (parsed.namespace === null) throw new Error("ssh k3s logs requires --namespace <name>");
if (parsed.resource === null && parsed.selector === null) throw new Error("ssh k3s logs requires --deployment <name>, --pod <name>, --resource <type/name> or --selector <label-selector>");
if (parsed.resource !== null && parsed.selector !== null) throw new Error("ssh k3s logs accepts either a resource or --selector, not both");
if (parsed.stdin || parsed.tty) throw new Error("ssh k3s logs does not support --stdin or --tty");
if (parsed.workspace !== null) throw new Error("ssh k3s logs does not accept --workdir");
const kubectlArgs = [
"logs",
"-n", parsed.namespace,
...(parsed.selector === null ? [parsed.resource ?? ""] : ["-l", parsed.selector]),
...(parsed.container === null ? [] : ["-c", parsed.container]),
...parsed.kubectlOptions,
];
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]);
}
function parseK3sTargetOptions(args: string[], commandName: string, options: ParseK3sTargetOptionsOptions): K3sTargetOptions {
let namespace: string | null = null;
let resource: string | null = null;
let selector: string | null = null;
let container: string | null = null;
let workspace: string | null = null;
let stdin = false;
let tty = false;
let shell: string | null = null;
const kubectlOptions: string[] = [];
const command: string[] = [];
let afterDoubleDash = false;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index] ?? "";
if (afterDoubleDash) {
command.push(arg);
continue;
}
if (arg === "--") {
afterDoubleDash = true;
continue;
}
if (arg === "--namespace" || arg === "-n") {
namespace = k3sOptionValue(args, index, `${commandName} ${arg}`);
index += 1;
continue;
}
const namespaceValue = k3sEqualsOptionValue(arg, "--namespace", commandName);
if (namespaceValue !== null) {
namespace = namespaceValue;
continue;
}
if (arg === "--deployment" || arg === "--deploy") {
resource = `deployment/${k3sOptionValue(args, index, `${commandName} ${arg}`)}`;
index += 1;
continue;
}
const deploymentValue = k3sEqualsOptionValue(arg, "--deployment", commandName) ?? k3sEqualsOptionValue(arg, "--deploy", commandName);
if (deploymentValue !== null) {
resource = `deployment/${deploymentValue}`;
continue;
}
if (arg === "--pod") {
resource = `pod/${k3sOptionValue(args, index, `${commandName} ${arg}`)}`;
index += 1;
continue;
}
const podValue = k3sEqualsOptionValue(arg, "--pod", commandName);
if (podValue !== null) {
resource = `pod/${podValue}`;
continue;
}
if (arg === "--resource") {
resource = normalizeK3sResource(k3sOptionValue(args, index, `${commandName} ${arg}`));
index += 1;
continue;
}
const resourceValue = k3sEqualsOptionValue(arg, "--resource", commandName);
if (resourceValue !== null) {
resource = normalizeK3sResource(resourceValue);
continue;
}
if (arg === "--selector" || arg === "-l") {
if (options.allowSelector !== true) throw new Error(`${commandName} does not support ${arg}`);
selector = k3sOptionValue(args, index, `${commandName} ${arg}`);
index += 1;
continue;
}
const selectorValue = k3sEqualsOptionValue(arg, "--selector", commandName) ?? k3sEqualsOptionValue(arg, "-l", commandName);
if (selectorValue !== null) {
if (options.allowSelector !== true) throw new Error(`${commandName} does not support ${arg.split("=", 1)[0]}`);
selector = selectorValue;
continue;
}
if (arg === "--container" || arg === "-c") {
container = k3sOptionValue(args, index, `${commandName} ${arg}`);
index += 1;
continue;
}
const containerValue = k3sEqualsOptionValue(arg, "--container", commandName);
if (containerValue !== null) {
container = containerValue;
continue;
}
if (arg === "--workdir" || arg === "--cwd") {
workspace = k3sWorkspaceValue(k3sOptionValue(args, index, `${commandName} ${arg}`), `${commandName} ${arg}`);
index += 1;
continue;
}
const workdirValue = k3sEqualsOptionValue(arg, "--workdir", commandName) ?? k3sEqualsOptionValue(arg, "--cwd", commandName);
if (workdirValue !== null) {
workspace = k3sWorkspaceValue(workdirValue, `${commandName} ${arg.split("=", 1)[0]}`);
continue;
}
if (arg === "--shell") {
if (!options.allowShell) throw new Error(`${commandName} does not support --shell`);
shell = k3sScriptShell(k3sOptionValue(args, index, `${commandName} ${arg}`), `${commandName} ${arg}`);
index += 1;
continue;
}
const shellValue = k3sEqualsOptionValue(arg, "--shell", commandName);
if (shellValue !== null) {
if (!options.allowShell) throw new Error(`${commandName} does not support --shell`);
shell = k3sScriptShell(shellValue, `${commandName} --shell`);
continue;
}
if (arg === "--stdin" || arg === "-i") {
stdin = true;
continue;
}
if (arg === "--tty" || arg === "-t") {
tty = true;
continue;
}
if (arg === "--tail") {
kubectlOptions.push("--tail", optionalPositiveInt(k3sOptionValue(args, index, `${commandName} ${arg}`), `${commandName} --tail`));
index += 1;
continue;
}
const tailValue = k3sEqualsOptionValue(arg, "--tail", commandName);
if (tailValue !== null) {
kubectlOptions.push("--tail", optionalPositiveInt(tailValue, `${commandName} --tail`));
continue;
}
if (arg === "--since" || arg === "--since-time" || arg === "--limit-bytes") {
kubectlOptions.push(arg, k3sOptionValue(args, index, `${commandName} ${arg}`));
index += 1;
continue;
}
const sinceValue = k3sEqualsOptionValue(arg, "--since", commandName);
const sinceTimeValue = k3sEqualsOptionValue(arg, "--since-time", commandName);
const limitBytesValue = k3sEqualsOptionValue(arg, "--limit-bytes", commandName);
if (sinceValue !== null || sinceTimeValue !== null || limitBytesValue !== null) {
const optionName = sinceValue !== null ? "--since" : sinceTimeValue !== null ? "--since-time" : "--limit-bytes";
const optionValue = sinceValue ?? sinceTimeValue ?? limitBytesValue;
kubectlOptions.push(optionName, optionValue ?? "");
continue;
}
if (arg === "--previous" || arg === "--timestamps" || arg === "--all-containers" || arg === "--prefix") {
kubectlOptions.push(arg);
continue;
}
if (arg === "--follow" || arg === "-f") {
throw new Error(`${commandName} does not support ${arg}; use a bounded logs command and poll again`);
}
if (arg.startsWith("-")) throw new Error(`unsupported ${commandName} option: ${arg}`);
if (resource === null) {
resource = normalizeK3sResource(arg);
continue;
}
throw new Error(`unexpected ${commandName} argument before --: ${arg}`);
}
if (options.requireCommand && command.length === 0) throw new Error(`${commandName} requires -- <command> [args...]`);
if (!options.requireCommand && options.allowCommand !== true && command.length > 0) throw new Error(`${commandName} does not accept a command after --`);
return { namespace, resource, selector, container, workspace, stdin, tty, shell, command, kubectlOptions };
}
function k3sEqualsOptionValue(arg: string, option: string, commandName: string): string | null {
const prefix = `${option}=`;
if (!arg.startsWith(prefix)) return null;
const value = arg.slice(prefix.length);
if (value.length === 0) throw new Error(`${commandName} ${option} requires a value`);
return value;
}
function k3sOptionValue(args: string[], index: number, option: string): string {
const value = args[index + 1];
if (value === undefined || value.length === 0) throw new Error(`${option} requires a value`);
return value;
}
function k3sWorkspaceValue(value: string, option: string): string {
if (!value.startsWith("/")) throw new Error(`${option} must be an absolute pod workspace path`);
return value;
}
function withK3sWorkspace(parsed: K3sTargetOptions, command: string[]): string[] {
if (parsed.workspace === null) return command;
if (command.length === 0) throw new Error("ssh k3s workspace route requires a command to execute");
const cwdScript = [
"if ! cd \"$1\"; then",
"rc=$?",
"printf 'UNIDESK_K3S_CWD_HINT cwd=%s message=%s\\n' \"$1\" '/... is interpreted as remote cwd, not container; use :<container> or --container <container> for container selection.' >&2",
"exit \"$rc\"",
"fi",
"shift",
"exec \"$@\"",
].join("\n");
return ["sh", "-c", cwdScript, "unidesk-cwd", parsed.workspace, ...command];
}
function normalizeK3sResource(value: string): string {
if (value.includes("/")) return value;
return `pod/${value}`;
}
function normalizeK3sRouteResource(value: string): string {
if (value.startsWith("deploy/")) return `deployment/${value.slice("deploy/".length)}`;
if (value.startsWith("po/")) return `pod/${value.slice("po/".length)}`;
if (value.startsWith("pod:")) return `pod/${value.slice("pod:".length)}`;
if (value.startsWith("po:")) return `pod/${value.slice("po:".length)}`;
if (value.startsWith("pods:")) return `pod/${value.slice("pods:".length)}`;
if (value.includes("/")) return value;
return `deployment/${value}`;
}
function k3sScriptShell(value: string, option: string): string {
if (!/^[A-Za-z0-9_./-]+$/u.test(value)) throw new Error(`${option} must be a shell executable name or path without whitespace`);
return value;
}
function shellScriptPrelude(): string {
return `${sshUserToolPathPrelude}\n${sshShellCompatibilityPrelude}`;
}
function shellScriptWithCompatibility(command: string): string {
return `${shellScriptPrelude()}\n${command}`;
}
function shellScriptStdinPrefix(): string {
return `${shellScriptPrelude()}\n`;
}
function withTransHostEnvironment(route: ParsedSshRoute, parsed: ParsedSshArgs): ParsedSshArgs {
if (route.plane !== "host" || parsed.remoteCommand === null) return parsed;
const preludes: string[] = [];
const ownershipPrelude = transHostWorkspaceOwnershipPrelude(route.workspace);
if (ownershipPrelude !== null) preludes.push(ownershipPrelude);
const rule = readTransHostProxyEnvRule(route.providerId);
if (rule !== null && rule.applyTo === "host-posix-commands") preludes.push(transHostProxyEnvPrelude(rule));
if (preludes.length === 0) return parsed;
return { ...parsed, remoteCommand: `${preludes.join("; ")}; ${parsed.remoteCommand}` };
}
function transHostWorkspaceOwnershipPrelude(workspace: string | null): string | null {
if (workspace === null || !/^\/mnt\/[A-Za-z](?:\/|$)/u.test(workspace)) return null;
return [
'if [ "$(id -u)" -eq 0 ]; then',
' UNIDESK_TRANS_HOST_WORKSPACE_OWNER_UID="$(stat -c %u . 2>/dev/null || true)"',
' case "$UNIDESK_TRANS_HOST_WORKSPACE_OWNER_UID" in',
' ""|*[!0-9]*|0) ;;',
' *) export SUDO_UID="$UNIDESK_TRANS_HOST_WORKSPACE_OWNER_UID" ;;',
' esac',
' export UNIDESK_TRANS_HOST_WORKSPACE_OWNER_UID',
'fi',
].join("\n");
}
function transHostProxyEnvPrelude(rule: TransHostProxyEnvRule): string {
const envFile = shellQuote(rule.envFile);
return [
`UNIDESK_TRANS_HOST_PROXY_ENV=${envFile}`,
'if [ -f "$UNIDESK_TRANS_HOST_PROXY_ENV" ]; then set -a; . "$UNIDESK_TRANS_HOST_PROXY_ENV"; unidesk_trans_host_proxy_env_rc=$?; set +a; [ "$unidesk_trans_host_proxy_env_rc" -eq 0 ] || exit "$unidesk_trans_host_proxy_env_rc"; fi',
`export UNIDESK_TRANS_HOST_PROXY_ENV_SOURCE=${shellQuote(rule.configPathLabel)}`,
].join("; ");
}
function buildShellCommand(args: string[], shell: "sh" | "bash", commandName: string): ParsedSshArgs {
const scriptArgs: string[] = [];
for (let index = 0; index < args.length; index += 1) {
const arg = args[index] ?? "";
if (arg === "--") {
if (scriptArgs.length === 0) {
const command = args.slice(index + 1);
if (command.length === 0) throw new Error(`${commandName} -- requires a command`);
if (command.length !== 1) throw new Error(`${commandName} -- requires exactly one shell command string; use argv or a direct subcommand for direct argv commands`);
return { remoteCommand: shellArgv([shell, "-c", shellScriptWithCompatibility(command[0] ?? "")]), requiresStdin: false, invocationKind: "helper" };
}
scriptArgs.push(...args.slice(index + 1));
break;
}
if (arg.startsWith("-")) throw new Error(`unsupported ${commandName} option: ${arg}`);
scriptArgs.push(arg);
}
return { remoteCommand: shellArgv([shell, "-s", "--", ...scriptArgs]), requiresStdin: true, invocationKind: "helper", stdinPrefix: shellScriptStdinPrefix() };
}
function podApplyPatchStdinWrapper(): { prefix: string; suffix: string } {
const toolMarker = "__UNIDESK_APPLY_PATCH_TOOL__";
const patchMarker = "__UNIDESK_APPLY_PATCH_PAYLOAD__";
if (remoteApplyPatchSource.includes(toolMarker)) throw new Error("remote apply_patch source contains reserved heredoc marker");
return {
prefix: [
"set -eu",
'UNIDESK_SSH_TOOL_DIR="${UNIDESK_SSH_TOOL_DIR:-/tmp/unidesk-ssh-tools}"',
'mkdir -p "$UNIDESK_SSH_TOOL_DIR"',
`cat > "$UNIDESK_SSH_TOOL_DIR/apply_patch" <<'${toolMarker}'`,
remoteApplyPatchSource.trimEnd(),
toolMarker,
'chmod 700 "$UNIDESK_SSH_TOOL_DIR/apply_patch"',
`"$UNIDESK_SSH_TOOL_DIR/apply_patch" "$@" <<'${patchMarker}'`,
].join("\n") + "\n",
suffix: `\n${patchMarker}\n`,
};
}
function buildPythonStdinCommand(args: string[]): string {
const pythonArgs = args.map(shellQuote).join(" ");
const execArgs = pythonArgs.length > 0 ? ` "$UNIDESK_SSH_PY_FILE" ${pythonArgs}` : ' "$UNIDESK_SSH_PY_FILE"';
return [
'UNIDESK_SSH_PY_FILE="$(mktemp /tmp/unidesk-ssh-py.XXXXXX.py)" || exit 1',
`trap 'rm -f "$UNIDESK_SSH_PY_FILE"' EXIT`,
'cat > "$UNIDESK_SSH_PY_FILE"',
`python3 -u${execArgs}`,
].join("; ");
}
const remoteToolSources: Record<SshHelperName, string> = {
apply_patch: remoteApplyPatchSource,
glob: remoteGlobSource,
"skill-discover": remoteSkillDiscoverSource,
};
function remoteToolBootstrapCommand(helpers: readonly SshHelperName[] = []): string {
const uniqueHelpers = [...new Set(helpers)];
if (uniqueHelpers.length === 0) return "";
const commands = [
"UNIDESK_SSH_TOOL_DIR=/tmp/unidesk-ssh-tools",
'mkdir -p "$UNIDESK_SSH_TOOL_DIR"',
];
for (const helper of uniqueHelpers) {
const source = remoteToolSources[helper];
const encoded = Buffer.from(source, "utf8").toString("base64");
commands.push(`printf %s ${shellQuote(encoded)} | base64 -d > "$UNIDESK_SSH_TOOL_DIR/${helper}"`);
commands.push(`chmod 700 "$UNIDESK_SSH_TOOL_DIR/${helper}"`);
}
commands.push('export PATH="$UNIDESK_SSH_TOOL_DIR:$PATH"');
return commands.join("; ");
}
export function wrapSshRemoteCommand(command: string | null, helpers: readonly SshHelperName[] = []): string {
const bootstrap = remoteToolBootstrapCommand(helpers);
const prelude = [sshUserToolPathPrelude, bootstrap].filter((part) => part.length > 0).join("; ");
const prefix = prelude.length > 0 ? `${prelude}; ` : "";
if (command === null) return `${prefix}exec "\${SHELL:-/bin/bash}" -l`;
return `${prefix}stty -echo 2>/dev/null || true; ${command}`;
}
export function safeProviderId(providerId: string): string {
return /^[A-Za-z0-9_.-]{1,64}$/u.test(providerId) ? providerId : "<provider>";
}
export {
classifySshTcpPoolFailure,
createPosixApplyPatchFileSystem,
createSshStderrForwarder,
createSshStdoutForwarder,
createWindowsApplyPatchFileSystem,
formatSshFailureHint,
formatSshRuntimeTimeoutHint,
formatSshRuntimeTimingHint,
formatSshStdoutTruncationHint,
formatSshTcpPoolHint,
formatSshTruncationCompletionSummary,
remoteCommandForRoute,
runSsh,
runSshCommandCapture,
sshCaptureBackendPlan,
sshFailureHint,
sshRuntimeTimeoutHint,
sshRuntimeTimeoutMs,
sshRuntimeTimingHint,
sshSlowWarningThresholdMs,
sshStderrStreamMaxBytes,
sshStdoutStreamMaxBytes,
sshStdoutTruncationHint,
sshTcpPoolHint,
sshTruncationCompletionSummary,
sshWindowsPlaneHint,
sshWindowsPowerShellPipelineHint,
} from "./ssh-runtime";
export type { SshStreamForwarder } from "./ssh-runtime";
export { windowsFsReadOnlyScript } from "./ssh-windows-fs";