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 = ""): 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 ${normalizedArgs.join(" ")}\`. Keep -- only inside operations such as \`sh -- \`, \`bash -- \`, or \`exec -- \`.\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 sh -- ${shellQuote(command)}\` or \`trans bash -- ${shellQuote(command)}\` for one-line shell logic, or split argv tokens, for example \`trans ${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 \`, 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 \`, 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)} function Invoke-Git([string[]]$GitArgs) { $output = @(& git @GitArgs 2>&1) if ($LASTEXITCODE -ne 0) { throw (($output | ForEach-Object { $_.ToString() }) -join [Environment]::NewLine) } return (($output | ForEach-Object { $_.ToString() }) -join [Environment]::NewLine).Trim() } 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;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;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;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;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 }`; } 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 , ${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] = '' }", " }", " 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 :k3s::${head}:[:] ..." 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] || ""; 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:: ${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::[:]; 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:: ${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:: 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 : 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 "); 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 , --pod or --resource "); 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; return { posixSh: `${entrypoint} ${route} sh -- ${shellQuote(commandText)}`, bash: `${entrypoint} ${route} bash -- ${shellQuote(commandText)}`, templatePosixSh: `${entrypoint} ${route} sh -- ''`, templateBash: `${entrypoint} ${route} bash -- ''`, }; } 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 `); if (parsed.selector !== null) throw new Error(`${commandName} does not support --selector`); if (parsed.resource === null) throw new Error(`${commandName} target requires --deployment , --pod or --resource `); 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 `); if (parsed.resource === null) throw new Error(`${commandName} target requires --deployment , --pod or --resource `); 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 "); if (parsed.resource === null) throw new Error("ssh k3s apply-patch requires --deployment , --pod or --resource "); 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 "); if (parsed.resource === null && parsed.selector === null) throw new Error("ssh k3s logs requires --deployment , --pod , --resource or --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 -- [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 : or --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 = { 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 : ""; } 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, } from "./ssh-runtime"; export type { SshStreamForwarder } from "./ssh-runtime"; export { windowsFsReadOnlyScript } from "./ssh-windows-fs";