import { spawn } from "node:child_process"; import { type UniDeskConfig, repoRoot } from "./config"; export interface ParsedSshArgs { remoteCommand: string | null; requiresStdin: boolean; invocationKind: SshInvocationKind; } export type SshInvocationKind = "interactive" | "argv" | "helper" | "ssh-like"; 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; } const argvQuotedSshSubcommands = new Set(["rg", "grep", "sed", "nl", "stat", "du", "ls", "cat", "head", "tail", "wc", "pwd"]); const remoteApplyPatchSource = String.raw`#!/usr/bin/env python3 import sys from pathlib import Path def die(message): print(f"apply_patch: {message}", file=sys.stderr) sys.exit(1) def write_file(path, text): target = Path(path) target.parent.mkdir(parents=True, exist_ok=True) target.write_text(text) def move_file(source, destination): source_path = Path(source) destination_path = Path(destination) if not source_path.exists(): die(f"file not found: {source}") if destination_path.exists(): die(f"target file already exists: {destination}") destination_path.parent.mkdir(parents=True, exist_ok=True) source_path.rename(destination_path) def read_file(path): try: return Path(path).read_text() except FileNotFoundError: die(f"file not found: {path}") def apply_update(path, body): old = read_file(path) output = [] search_from = 0 index = 0 while index < len(body): if body[index].startswith("*** End of File"): index += 1 continue if not body[index].startswith("@@"): die(f"expected hunk header in {path}") index += 1 sequence = [] while index < len(body) and not body[index].startswith("@@"): line = body[index] index += 1 if line.startswith("*** End of File"): continue if line.startswith(" "): sequence.append((" ", line[1:])) elif line.startswith("-"): sequence.append(("-", line[1:])) elif line.startswith("+"): sequence.append(("+", line[1:])) elif line.rstrip("\n") == r"\ No newline at end of file": continue else: die(f"bad hunk line in {path}: {line[:80].rstrip()}") search = "".join(text for kind, text in sequence if kind in (" ", "-")) replacement = "".join(text for kind, text in sequence if kind in (" ", "+")) offset = old.find(search, search_from) if offset < 0: offset = old.find(search) if offset < 0: die(f"hunk context not found in {path}") output.append(old[search_from:offset]) output.append(replacement) search_from = offset + len(search) output.append(old[search_from:]) write_file(path, "".join(output)) def main(): if len(sys.argv) > 1 and sys.argv[1] in ("-h", "--help"): print("apply_patch: read *** Begin Patch format from stdin; supports add/update/delete/move") return lines = sys.stdin.read().splitlines(keepends=True) if not lines or lines[0].strip() != "*** Begin Patch": die("patch must start with *** Begin Patch") index = 1 while index < len(lines): line = lines[index].rstrip("\n") if line == "*** End Patch": print("Done!") return if line.startswith("*** Add File: "): path = line[len("*** Add File: "):].strip() index += 1 content = [] while index < len(lines) and not lines[index].startswith("*** "): if not lines[index].startswith("+"): die(f"add file lines must start with + for {path}") content.append(lines[index][1:]) index += 1 if Path(path).exists(): die(f"file already exists: {path}") write_file(path, "".join(content)) continue if line.startswith("*** Delete File: "): path = line[len("*** Delete File: "):].strip() try: Path(path).unlink() except FileNotFoundError: die(f"file not found: {path}") index += 1 continue if line.startswith("*** Update File: "): path = line[len("*** Update File: "):].strip() index += 1 move_to = None if index < len(lines) and lines[index].startswith("*** Move to: "): move_to = lines[index][len("*** Move to: "):].strip() index += 1 body = [] while index < len(lines) and not ( lines[index].startswith("*** Add File: ") or lines[index].startswith("*** Update File: ") or lines[index].startswith("*** Delete File: ") or lines[index].startswith("*** End Patch") ): if lines[index].startswith("*** Move to: "): die("move marker must appear before update hunks") body.append(lines[index]) index += 1 if body: apply_update(path, body) elif move_to is None and not Path(path).exists(): die(f"file not found: {path}") if move_to is not None: move_file(path, move_to) continue die(f"unexpected patch line: {line}") die("missing *** End Patch") if __name__ == "__main__": main() `; const remoteGlobSource = String.raw`#!/usr/bin/env python3 import argparse import glob import os import sys def main(): parser = argparse.ArgumentParser(description="remote glob helper for UniDesk ssh passthrough") parser.add_argument("patterns", nargs="*", help="glob patterns relative to --root unless absolute") parser.add_argument("--root", default=".", help="base directory for relative patterns") parser.add_argument("--pattern", action="append", default=[], help="additional glob pattern") parser.add_argument("--contains", action="append", default=[], help="match path names containing text") parser.add_argument("--icontains", action="append", default=[], help="case-insensitive contains match") parser.add_argument("--type", choices=["any", "f", "d"], default="any", help="filter by any/file/dir") parser.add_argument("--limit", type=int, default=0, help="maximum number of rows to print") parser.add_argument("--sort", action="store_true", help="sort output") parser.add_argument("--absolute", action="store_true", help="print absolute paths") args = parser.parse_args() if args.limit < 0: print("glob: --limit must be >= 0", file=sys.stderr) return 2 root = os.path.abspath(args.root) patterns = list(args.patterns) + list(args.pattern) for text in args.contains: patterns.append(f"**/*{text}*") for text in args.icontains: # Python glob is case-sensitive on Linux, so filter from a broad recursive scan. patterns.append("**/*") if not patterns: patterns = ["*"] seen = set() rows = [] lowered_contains = [text.lower() for text in args.icontains] for pattern in patterns: effective = pattern if os.path.isabs(pattern) else os.path.join(root, pattern) for path in glob.iglob(effective, recursive=True): full = os.path.abspath(path) if full in seen: continue if lowered_contains and not any(text in os.path.basename(full).lower() or text in full.lower() for text in lowered_contains): continue if args.type == "f" and not os.path.isfile(full): continue if args.type == "d" and not os.path.isdir(full): continue seen.add(full) rows.append(full if args.absolute else os.path.relpath(full, root)) if args.sort: rows.sort() if args.limit > 0: rows = rows[:args.limit] for row in rows: print(row) return 0 if __name__ == "__main__": raise SystemExit(main()) `; const remoteSkillDiscoverSource = String.raw`#!/usr/bin/env python3 import argparse import getpass import json import os import platform import socket import sys from datetime import datetime, timezone from pathlib import Path SKIP_PARTS = {"node_modules", ".git", ".state", "logs", "references", "__pycache__"} def is_wsl(): try: release = Path("/proc/sys/kernel/osrelease").read_text(errors="ignore").lower() except Exception: release = "" return "microsoft" in release or "wsl" in release or "WSL_INTEROP" in os.environ def to_windows_path(path): text = str(path) if text.startswith("/mnt/") and len(text) >= 7 and text[5].isalpha() and text[6] == "/": drive = text[5].upper() rest = text[7:].replace("/", "\\") return drive + ":\\" + rest return None def read_bounded(path, limit=16384): try: data = path.read_bytes()[:limit] return data.decode("utf-8", errors="replace") except Exception: return "" def frontmatter_value(line): if ":" not in line: return None key, value = line.split(":", 1) return key.strip().lower(), value.strip().strip("\"'") def parse_skill_metadata(skill_md): text = read_bounded(skill_md) name = skill_md.parent.name description = "" lines = text.splitlines() if lines and lines[0].strip() == "---": for line in lines[1:]: if line.strip() == "---": break item = frontmatter_value(line) if item is None: continue key, value = item if key == "name" and value: name = value if key == "description" and value: description = value if not description: for line in lines: stripped = line.strip() if stripped and not stripped.startswith("---") and not stripped.startswith("#"): description = stripped break return name, description def iter_skill_files(root, max_depth): try: iterator = root.rglob("SKILL.md") for skill_md in iterator: try: rel = skill_md.relative_to(root) except ValueError: continue directory_parts = rel.parts[:-1] if len(directory_parts) == 0 or len(directory_parts) > max_depth: continue if any(part in SKIP_PARTS for part in directory_parts): continue yield skill_md except Exception as exc: raise RuntimeError(str(exc)) from exc def unique_paths(paths): seen = set() output = [] for raw in paths: path = Path(raw).expanduser() key = str(path) if key in seen: continue seen.add(key) output.append(path) return output def default_wsl_roots(): home = Path.home() roots = [home / ".agents" / "skills", home / ".codex" / "skills"] for raw in ("/root/.agents/skills", "/root/.codex/skills"): path = Path(raw) if str(path) not in {str(item) for item in roots}: roots.append(path) return roots def default_windows_roots(): if not is_wsl(): return [] users = Path("/mnt/c/Users") roots = [] try: children = list(users.iterdir()) if users.exists() else [] except Exception: children = [] for child in children: try: if not child.is_dir(): continue except Exception: continue lower = child.name.lower() if lower in {"all users", "default", "default user", "public"}: continue roots.append(child / ".agents" / "skills") roots.append(child / ".codex" / "skills") return roots def scan_root(scope, root, max_depth): try: root_exists = root.exists() root_error = None except Exception as exc: root_exists = False root_error = str(exc) record = { "scope": scope, "path": str(root), "windowsPath": to_windows_path(root), "exists": root_exists, "skillCount": 0, "error": root_error, } skills = [] if not record["exists"]: return record, skills try: for skill_md in iter_skill_files(root, max_depth): name, description = parse_skill_metadata(skill_md) skill = { "scope": scope, "name": name, "description": description, "path": str(skill_md.parent), "skillMd": str(skill_md), "windowsPath": to_windows_path(skill_md.parent), "root": str(root), } skills.append(skill) except Exception as exc: record["error"] = str(exc) record["skillCount"] = len(skills) return record, skills def main(): parser = argparse.ArgumentParser(description="discover WSL/Linux and Windows skill directories from a UniDesk ssh passthrough session") parser.add_argument("--scope", choices=["all", "wsl", "windows"], default="all", help="which skill roots to scan") parser.add_argument("--max-depth", type=int, default=4, help="maximum directory depth below each skill root") parser.add_argument("--limit", type=int, default=0, help="maximum skill rows to return; 0 means unlimited") parser.add_argument("--root", action="append", default=[], help="extra WSL/Linux skill root") parser.add_argument("--windows-root", action="append", default=[], help="extra Windows skill root, expressed as /mnt//...") args = parser.parse_args() if args.max_depth <= 0: print(json.dumps({"ok": False, "error": "--max-depth must be positive"}, ensure_ascii=False)) return 2 if args.limit < 0: print(json.dumps({"ok": False, "error": "--limit must be >= 0"}, ensure_ascii=False)) return 2 roots = [] if args.scope in ("all", "wsl"): roots.extend(("wsl", path) for path in default_wsl_roots()) roots.extend(("wsl", Path(raw).expanduser()) for raw in args.root) if args.scope in ("all", "windows"): roots.extend(("windows", path) for path in default_windows_roots()) roots.extend(("windows", Path(raw).expanduser()) for raw in args.windows_root) seen = set() unique = [] for scope, path in roots: key = (scope, str(path)) if key in seen: continue seen.add(key) unique.append((scope, path)) root_records = [] skills = [] for scope, root in unique: record, found = scan_root(scope, root, args.max_depth) root_records.append(record) skills.extend(found) scope_order = {"wsl": 0, "windows": 1} skills.sort(key=lambda item: (scope_order.get(str(item["scope"]), 9), str(item["name"]).lower(), str(item["path"]))) total_before_limit = len(skills) if args.limit > 0: skills = skills[:args.limit] counts = {"total": len(skills), "totalBeforeLimit": total_before_limit, "wsl": 0, "windows": 0} for skill in skills: scope = str(skill["scope"]) if scope in counts: counts[scope] += 1 payload = { "ok": True, "command": "unidesk ssh skills", "generatedAt": datetime.now(timezone.utc).isoformat(), "node": { "hostname": socket.gethostname(), "user": getpass.getuser(), "home": str(Path.home()), "platform": platform.platform(), "isWsl": is_wsl(), "python": sys.version.split()[0], }, "counts": counts, "roots": root_records, "skills": skills, } print(json.dumps(payload, ensure_ascii=False, indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main()) `; 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[]): ParsedSshArgs { const subcommand = args[0] ?? ""; 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" }; } if (subcommand === "apply-patch" || subcommand === "patch") { const toolArgs = ["apply_patch", ...args.slice(1)]; return { remoteCommand: shellArgv(toolArgs), requiresStdin: true, invocationKind: "helper" }; } if (subcommand === "py") { return { remoteCommand: buildPythonStdinCommand(args.slice(1)), requiresStdin: true, invocationKind: "helper" }; } if (subcommand === "argv" || subcommand === "exec") { const toolArgs = args.slice(1); if (toolArgs.length === 0) throw new Error(`ssh ${subcommand} requires a command`); 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" }; } 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 : remote.join(" "), requiresStdin: false, invocationKind: remote.length === 0 ? "interactive" : "ssh-like", }; } function shellArgv(args: string[]): string { return args.map(shellQuote).join(" "); } 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 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 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("; "); } function remoteToolBootstrapCommand(): string { const encodedApplyPatch = Buffer.from(remoteApplyPatchSource, "utf8").toString("base64"); const encodedGlob = Buffer.from(remoteGlobSource, "utf8").toString("base64"); const encodedSkillDiscover = Buffer.from(remoteSkillDiscoverSource, "utf8").toString("base64"); return [ "UNIDESK_SSH_TOOL_DIR=/tmp/unidesk-ssh-tools", 'mkdir -p "$UNIDESK_SSH_TOOL_DIR"', `printf %s ${shellQuote(encodedApplyPatch)} | base64 -d > "$UNIDESK_SSH_TOOL_DIR/apply_patch"`, `printf %s ${shellQuote(encodedGlob)} | base64 -d > "$UNIDESK_SSH_TOOL_DIR/glob"`, `printf %s ${shellQuote(encodedSkillDiscover)} | base64 -d > "$UNIDESK_SSH_TOOL_DIR/skill-discover"`, 'chmod 700 "$UNIDESK_SSH_TOOL_DIR/apply_patch"', 'chmod 700 "$UNIDESK_SSH_TOOL_DIR/glob"', 'chmod 700 "$UNIDESK_SSH_TOOL_DIR/skill-discover"', 'export PATH="$UNIDESK_SSH_TOOL_DIR:$PATH"', ].join("; "); } export function wrapSshRemoteCommand(command: string | null): string { const bootstrap = remoteToolBootstrapCommand(); if (command === null) return `${bootstrap}; exec "\${SHELL:-/bin/bash}" -l`; return `${bootstrap}; stty -echo 2>/dev/null || true; ${command}`; } function safeProviderId(providerId: string): string { return /^[A-Za-z0-9_.-]{1,64}$/u.test(providerId) ? providerId : ""; } function classifySshLikeFailure(exitCode: number, stderrText: string): SshFailureHint["trigger"] | null { const normalized = stderrText.toLowerCase(); if ( normalized.includes("kex_exchange_identification") || normalized.includes("ssh_exchange_identification") || normalized.includes("connection closed by remote host") || normalized.includes("connection reset by peer") || normalized.includes("connection timed out") || normalized.includes("operation timed out") || normalized.includes("timed out waiting for provider session") || normalized.includes("the operation was aborted") ) { return "timeout-or-kex"; } return exitCode === 255 ? "exit-255" : null; } export function sshFailureHint(providerId: string, parsed: ParsedSshArgs, exitCode: number, stderrText: string): SshFailureHint | null { if (parsed.invocationKind !== "ssh-like") return null; const trigger = classifySshLikeFailure(exitCode, stderrText); if (trigger === null) return null; const shownProviderId = safeProviderId(providerId); return { code: "ssh-like-command-friction", providerId: shownProviderId, trigger, exitCode, message: "ssh-like remote command failed before proving Host SSH is globally unavailable; prefer argv form for non-interactive commands.", try: `bun scripts/cli.ts ssh ${shownProviderId} argv bash -lc ''`, triage: `bun scripts/cli.ts provider triage ${shownProviderId} --observed-scope ssh --observed-error ''`, note: "This hint intentionally does not echo the original remote command.", }; } export function formatSshFailureHint(hint: SshFailureHint): string { return `UNIDESK_SSH_HINT ${JSON.stringify(hint)}\n`; } function brokerSource(): string { return String.raw` const open = JSON.parse(process.argv[2] || process.argv[1] || "{}"); const token = process.env.PROVIDER_TOKEN || process.env.UNIDESK_PROVIDER_TOKEN || ""; const baseUrl = process.env.UNIDESK_SSH_BROKER_URL || "ws://backend-core:8080/ws/ssh"; const url = baseUrl + "?token=" + encodeURIComponent(token); const ws = new WebSocket(url); let exitCode = 255; let canSend = false; let sessionReady = false; let opened = false; const pending = []; const pendingInput = []; const openTimer = setTimeout(() => { if (opened) return; process.stderr.write("unidesk ssh bridge timed out waiting for provider session\n"); try { ws.close(); } catch {} process.exit(255); }, Number(open.openTimeoutMs || 15000)); function send(value) { const text = JSON.stringify(value); if (!canSend || ws.readyState !== WebSocket.OPEN) { pending.push(text); return; } ws.send(text); } function flush() { while (pending.length > 0 && ws.readyState === WebSocket.OPEN) { ws.send(pending.shift()); } } function sendInput(value) { const text = JSON.stringify(value); if (!sessionReady || ws.readyState !== WebSocket.OPEN) { pendingInput.push(text); return; } ws.send(text); } function flushInput() { if (!sessionReady || ws.readyState !== WebSocket.OPEN) return; while (pendingInput.length > 0) { ws.send(pendingInput.shift()); } } function decodeData(data) { return typeof data === "string" ? data : Buffer.from(data).toString("utf8"); } ws.addEventListener("open", () => { canSend = true; send({ type: "ssh.open", providerId: open.providerId, command: open.command || undefined, cwd: open.cwd || undefined, tty: open.tty === true, cols: open.cols || 100, rows: open.rows || 30, }); flush(); }); ws.addEventListener("message", (event) => { const message = JSON.parse(decodeData(event.data)); if (message.type === "ssh.data") { opened = true; const chunk = Buffer.from(message.data || "", "base64"); if (message.stream === "stderr") process.stderr.write(chunk); else process.stdout.write(chunk); return; } if (message.type === "ssh.opened") { opened = true; sessionReady = true; clearTimeout(openTimer); if (open.stdinEotOnEnd === true) setTimeout(flushInput, 200); else flushInput(); return; } if (message.type === "ssh.dispatched") { return; } if (message.type === "ssh.error") { clearTimeout(openTimer); process.stderr.write(String(message.message || "ssh bridge error") + "\n"); exitCode = 255; ws.close(); return; } if (message.type === "ssh.exit") { clearTimeout(openTimer); exitCode = Number.isInteger(message.exitCode) ? message.exitCode : 255; ws.close(); } }); ws.addEventListener("close", () => { process.exit(exitCode); }); ws.addEventListener("error", () => { process.stderr.write("unidesk ssh bridge websocket error\n"); process.exit(255); }); process.stdin.on("data", (chunk) => { sendInput({ type: "ssh.input", data: Buffer.from(chunk).toString("base64"), encoding: "base64" }); }); process.stdin.on("end", () => { if (open.stdinEotOnEnd === true) { sendInput({ type: "ssh.input", data: Buffer.from([4]).toString("base64"), encoding: "base64" }); } sendInput({ type: "ssh.eof" }); }); `; } function terminalSize(): { cols: number; rows: number } { return { cols: Number(process.stdout.columns) > 0 ? Number(process.stdout.columns) : 100, rows: Number(process.stdout.rows) > 0 ? Number(process.stdout.rows) : 30, }; } export async function runSsh(config: UniDeskConfig, providerId: string, args: string[]): Promise { if (!providerId) throw new Error("ssh requires provider id, for example: bun scripts/cli.ts ssh D518"); const parsed = parseSshArgs(args); const size = terminalSize(); const openTimeoutMs = Math.max(15000, Number(process.env.UNIDESK_SSH_OPEN_TIMEOUT_MS || 60000)); const payload = { providerId, command: wrapSshRemoteCommand(parsed.remoteCommand), tty: parsed.remoteCommand === null, stdinEotOnEnd: parsed.remoteCommand !== null, openTimeoutMs, cols: size.cols, rows: size.rows, }; const payloadJson = JSON.stringify(payload); const encodedBrokerSource = Buffer.from(brokerSource(), "utf8").toString("base64"); const script = [ "set -euo pipefail", `payload=${shellQuote(payloadJson)}`, "if command -v backend-core >/dev/null 2>&1; then", ' exec backend-core --ssh-broker "$payload"', "fi", `export UNIDESK_SSH_BROKER_URL=${shellQuote("ws://127.0.0.1:8080/ws/ssh")}`, `printf %s ${shellQuote(encodedBrokerSource)} | base64 -d >/tmp/unidesk-ssh-broker.js`, "exec bun /tmp/unidesk-ssh-broker.js \"$payload\"", ].join("\n"); const child = spawn("docker", [ "exec", "-i", "unidesk-backend-core", "sh", "-lc", script, ], { cwd: repoRoot, stdio: ["pipe", "pipe", "pipe"], }); const rawMode = parsed.remoteCommand === null && process.stdin.isTTY; if (rawMode) process.stdin.setRawMode(true); process.stdin.resume(); process.stdin.pipe(child.stdin); let stderrTail = ""; const appendStderrTail = (chunk: Buffer | string): void => { const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : chunk; stderrTail = (stderrTail + text).slice(-16_384); }; child.stdout.pipe(process.stdout); child.stderr.on("data", (chunk: Buffer) => { appendStderrTail(chunk); process.stderr.write(chunk); }); return await new Promise((resolve) => { const restore = (): void => { process.stdin.unpipe(child.stdin); if (rawMode) process.stdin.setRawMode(false); }; child.on("error", (error) => { restore(); const message = `unidesk ssh failed to start broker: ${error.message}\n`; appendStderrTail(message); process.stderr.write(message); const hint = sshFailureHint(providerId, parsed, 255, stderrTail); if (hint !== null) process.stderr.write(formatSshFailureHint(hint)); resolve(255); }); child.on("close", (code) => { restore(); const exitCode = code ?? 255; const hint = sshFailureHint(providerId, parsed, exitCode, stderrTail); if (hint !== null) process.stderr.write(formatSshFailureHint(hint)); resolve(exitCode); }); }); }