Files
pikasTech-unidesk/scripts/src/ssh.ts
T
2026-06-07 00:28:38 +00:00

2820 lines
117 KiB
TypeScript

import { spawn } from "node:child_process";
import { createHash, randomBytes } from "node:crypto";
import { type UniDeskConfig, repoRoot } from "./config";
import {
decodeApplyPatchV2BulkRead,
formatApplyPatchV2BulkReplacementPayload,
isApplyPatchV2HelpArgs,
runApplyPatchV2,
type ApplyPatchV2BulkReplacementWritePlan,
type ApplyPatchV2Executor,
type ApplyPatchV2FileSystem,
} from "./apply-patch-v2";
import { isSshFileTransferOperation, runSshFileTransferOperation, type SshRemoteCommandExecutor } from "./ssh-file-transfer";
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 SshCaptureResult {
exitCode: number;
stdout: string;
stderr: 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;
}
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;
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 k3sPodRoutePrefixes = ["pod:", "po:", "pods:"];
const legacyK3sOperationRouteSegments = new Set([
"guard",
"kubectl",
"exec",
"script",
"apply-patch",
"apply-patch-v1",
"patch",
"patch-v1",
"v2",
"logs",
"get",
"describe",
"top",
"rollout",
"wait",
"config",
"version",
"cluster-info",
"auth",
"api-resources",
"api-versions",
]);
export const remoteApplyPatchSource = String.raw`#!/bin/sh
set -eu
allow_loose=0
die() {
printf 'apply_patch: %s\n' "$*" >&2
exit 1
}
mk_temp() {
mktemp "${"$"}{TMPDIR:-/tmp}/unidesk-apply-patch.XXXXXX" || exit 1
}
ensure_parent() {
case "$1" in
*/*)
dir=${"$"}{1%/*}
[ -n "$dir" ] || dir=/
mkdir -p "$dir"
;;
esac
}
read_text_preserve_newlines() {
marker="__UNIDESK_APPLY_PATCH_EOF_$$__"
text=$(cat "$1"; printf '%s' "$marker") || die "failed to read $1"
printf '%s' "${"$"}{text%"$marker"}"
}
write_file() {
target=$1
source=$2
ensure_parent "$target"
cp "$source" "$target" || die "failed to write $target"
}
line_number_for_prefix() {
newline_count=$(printf '%s' "$1" | tr -cd '\n' | wc -c | tr -d ' ')
printf '%s\n' $((newline_count + 1))
}
replace_once_with_perl() {
command -v perl >/dev/null 2>&1 || return 127
perl -0777 -e '
use strict;
use warnings;
sub fail {
print STDERR "apply_patch: ", @_, "\n";
exit 1;
}
sub read_all {
my ($path, $label) = @_;
open my $fh, "<", $path or fail("failed to read $label");
binmode $fh;
local $/;
my $data = <$fh>;
close $fh;
return defined $data ? $data : "";
}
my ($target, $search_file, $replace_file, $hunk_id, $allow_loose, $out) = @ARGV;
my $old = read_all($target, $target);
my $search = read_all($search_file, "hunk search");
my $replacement = read_all($replace_file, "hunk replacement");
my $new;
if ($search eq "") {
fail("hunk $hunk_id in $target has no context; add unchanged/deleted anchor lines or pass --allow-loose after manual review") if $allow_loose ne "1";
print STDERR "apply_patch: hunk $hunk_id matched $target:1 (loose)\n";
$new = $replacement . $old;
} else {
my $pos = -1;
my $offset = 0;
my $count = 0;
while (1) {
my $found = index($old, $search, $offset);
last if $found < 0;
$pos = $found if $count == 0;
$count += 1;
last if $count > 1 && $allow_loose ne "1";
$offset = $found + length($search);
}
fail("hunk $hunk_id context not found in $target") if $count == 0;
fail("hunk $hunk_id context matched multiple locations in $target; add more unchanged context or pass --allow-loose after manual review") if $count > 1 && $allow_loose ne "1";
my $prefix = substr($old, 0, $pos);
my $suffix = substr($old, $pos + length($search));
my $line = ($prefix =~ tr/\n//) + 1;
print STDERR "apply_patch: hunk $hunk_id matched $target:$line\n";
$new = $prefix . $replacement . $suffix;
}
open my $ofh, ">", $out or fail("failed to render patched file");
binmode $ofh;
print $ofh $new or fail("failed to render patched file");
close $ofh or fail("failed to render patched file");
' "$@"
}
replace_once() {
target=$1
search_file=$2
replace_file=$3
hunk_id=$4
[ -e "$target" ] || die "file not found: $target"
fast_out=$(mk_temp)
if replace_once_with_perl "$target" "$search_file" "$replace_file" "$hunk_id" "$allow_loose" "$fast_out"; then
write_file "$target" "$fast_out"
rm -f "$fast_out"
return 0
else
status=$?
fi
rm -f "$fast_out"
[ "$status" = 127 ] || exit "$status"
marker="__UNIDESK_APPLY_PATCH_EOF_$$__"
old=$(cat "$target"; printf '%s' "$marker") || die "failed to read $target"
old=${"$"}{old%"$marker"}
search=$(cat "$search_file"; printf '%s' "$marker") || die "failed to read hunk search"
search=${"$"}{search%"$marker"}
replacement=$(cat "$replace_file"; printf '%s' "$marker") || die "failed to read hunk replacement"
replacement=${"$"}{replacement%"$marker"}
if [ -z "$search" ]; then
[ "$allow_loose" = 1 ] || die "hunk $hunk_id in $target has no context; add unchanged/deleted anchor lines or pass --allow-loose after manual review"
printf 'apply_patch: hunk %s matched %s:1 (loose)\n' "$hunk_id" "$target" >&2
new=$replacement$old
else
case "$old" in
*"$search"*)
prefix=${"$"}{old%%"$search"*}
suffix=${"$"}{old#*"$search"}
case "$suffix" in
*"$search"*)
[ "$allow_loose" = 1 ] || die "hunk $hunk_id context matched multiple locations in $target; add more unchanged context or pass --allow-loose after manual review"
;;
esac
printf 'apply_patch: hunk %s matched %s:%s\n' "$hunk_id" "$target" "$(line_number_for_prefix "$prefix")" >&2
new=$prefix$replacement$suffix
;;
*)
die "hunk $hunk_id context not found in $target"
;;
esac
fi
out=$(mk_temp)
printf '%s' "$new" > "$out" || die "failed to render patched file"
write_file "$target" "$out"
rm -f "$out"
}
apply_update() {
target=$1
body=$2
in_hunk=0
search_file=
replace_file=
changed=0
hunk_number=0
has_add=0
has_delete=0
seen_add=0
anchor_before_add=0
anchor_after_add=0
explicit_eof=0
finish_hunk() {
[ "$in_hunk" = 1 ] || return 0
if [ "$allow_loose" != 1 ]; then
[ -s "$search_file" ] || die "hunk $hunk_number in $target has no context; add unchanged/deleted anchor lines or pass --allow-loose after manual review"
if [ "$has_add" = 1 ] && [ "$has_delete" = 0 ]; then
if [ "$anchor_before_add" != 1 ] || { [ "$anchor_after_add" != 1 ] && [ "$explicit_eof" != 1 ]; }; then
die "hunk $hunk_number in $target is insert-only without both leading and trailing context; include nearby unchanged lines after the insertion or pass --allow-loose after manual review"
fi
fi
fi
replace_once "$target" "$search_file" "$replace_file" "$hunk_number"
rm -f "$search_file" "$replace_file"
search_file=
replace_file=
in_hunk=0
changed=1
}
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in
"*** End of File"*)
continue
;;
"@@"*)
finish_hunk
search_file=$(mk_temp)
replace_file=$(mk_temp)
hunk_number=$((hunk_number + 1))
has_add=0
has_delete=0
seen_add=0
anchor_before_add=0
anchor_after_add=0
explicit_eof=0
in_hunk=1
continue
;;
esac
[ "$in_hunk" = 1 ] || die "expected hunk header in $target"
case "$line" in
" "*)
text=${"$"}{line#?}
printf '%s\n' "$text" >> "$search_file"
printf '%s\n' "$text" >> "$replace_file"
if [ "$seen_add" = 1 ]; then anchor_after_add=1; else anchor_before_add=1; fi
;;
"-"*)
text=${"$"}{line#?}
printf '%s\n' "$text" >> "$search_file"
has_delete=1
if [ "$seen_add" = 1 ]; then anchor_after_add=1; else anchor_before_add=1; fi
;;
"+"*)
text=${"$"}{line#?}
printf '%s\n' "$text" >> "$replace_file"
has_add=1
seen_add=1
;;
"\\ No newline at end of file")
;;
"*** End of File"*)
explicit_eof=1
;;
*)
die "bad hunk line in $target: $line"
;;
esac
done < "$body"
finish_hunk
[ "$changed" = 1 ] || [ -e "$target" ] || die "file not found: $target"
}
is_top_header() {
case "$1" in
"*** Add File: "*|"*** Update File: "*|"*** Delete File: "*|"*** End Patch")
return 0
;;
*)
return 1
;;
esac
}
pushed=0
pushed_line=
next_patch_line() {
if [ "$pushed" = 1 ]; then
line=$pushed_line
pushed=0
pushed_line=
return 0
fi
if IFS= read -r line; then
return 0
fi
[ -n "${"$"}{line:-}" ]
}
push_patch_line() {
pushed_line=$1
pushed=1
}
parse_add_file() {
target=$1
[ ! -e "$target" ] || die "file already exists: $target"
tmp=$(mk_temp)
while next_patch_line; do
if is_top_header "$line"; then
push_patch_line "$line"
break
fi
case "$line" in
"+"*)
printf '%s\n' "${"$"}{line#?}" >> "$tmp"
;;
*)
rm -f "$tmp"
die "add file lines must start with + for $target"
;;
esac
done
write_file "$target" "$tmp"
rm -f "$tmp"
}
parse_update_file() {
target=$1
move_to=
body=$(mk_temp)
if next_patch_line; then
case "$line" in
"*** Move to: "*)
move_to=${"$"}{line#"*** Move to: "}
;;
*)
push_patch_line "$line"
;;
esac
fi
while next_patch_line; do
if is_top_header "$line"; then
push_patch_line "$line"
break
fi
case "$line" in
"*** Move to: "*)
rm -f "$body"
die "move marker must appear before update hunks"
;;
*)
printf '%s\n' "$line" >> "$body"
;;
esac
done
if [ -s "$body" ]; then
apply_update "$target" "$body"
elif [ -z "$move_to" ] && [ ! -e "$target" ]; then
rm -f "$body"
die "file not found: $target"
fi
rm -f "$body"
if [ -n "$move_to" ]; then
[ -e "$target" ] || die "file not found: $target"
[ ! -e "$move_to" ] || die "target file already exists: $move_to"
ensure_parent "$move_to"
mv "$target" "$move_to" || die "failed to move $target to $move_to"
fi
}
main() {
while [ "$#" -gt 0 ]; do
case "${"$"}1" in
-h|--help)
printf 'apply_patch: sh-only helper; reads *** Begin Patch format from stdin; --allow-loose bypasses low-context hunk guards\n'
return 0
;;
--allow-loose)
allow_loose=1
shift
;;
*)
die "unsupported option: ${"$"}1"
;;
esac
done
next_patch_line || die "patch must start with *** Begin Patch"
[ "$line" = "*** Begin Patch" ] || die "patch must start with *** Begin Patch"
while next_patch_line; do
case "$line" in
"*** End Patch")
printf 'Done!\n'
return 0
;;
"*** Add File: "*)
parse_add_file "${"$"}{line#"*** Add File: "}"
;;
"*** Delete File: "*)
target=${"$"}{line#"*** Delete File: "}
rm -f "$target" || die "failed to delete $target"
;;
"*** Update File: "*)
parse_update_file "${"$"}{line#"*** Update File: "}"
;;
*)
die "unexpected patch line: $line"
;;
esac
done
die "missing *** End Patch"
}
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/<drive>/...")
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 (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 === "script" || subcommand === "sh") {
return buildShellCommand(args.slice(1));
}
if (subcommand === "shell") {
return buildShellStringCommand(args.slice(1));
}
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 : remote.join(" "),
requiresStdin: false,
invocationKind: remote.length === 0 ? "interactive" : "ssh-like",
};
}
export function normalizeSshOperationArgs(args: string[]): string[] {
return args[0] === "--" ? args.slice(1) : args;
}
export function sshRouteSeparatorCompatibilityHint(rawArgs: string[], normalizedArgs: string[]): string {
if (rawArgs === normalizedArgs || rawArgs[0] !== "--") return "";
const operation = normalizedArgs[0] ?? "";
const operationText = operation.length === 0 ? "operation" : `operation \`${operation}\``;
return `UNIDESK_SSH_HINT route-level -- is ignored before ${operationText}; canonical form is \`trans <route> ${normalizedArgs.join(" ")}\`. Keep -- only inside operations such as \`script -- <command>\` or \`exec -- <command>\`.\n`;
}
function validateDirectArgvCommand(commandName: string, toolArgs: string[]): void {
if (toolArgs.length !== 1) return;
const command = toolArgs[0] ?? "";
if (!looksLikeShellCommandString(command)) return;
throw new Error(
`ssh ${commandName} received one shell-like command string; ${commandName} executes a single process and treats that string as the executable path. ` +
`Use \`trans <route> script -- ${shellQuote(command)}\` for one-line shell logic, or split argv tokens, for example \`trans <route> ${commandName} ls -la\`.`
);
}
function looksLikeShellCommandString(value: string): boolean {
return /\s/u.test(value) || /&&|\|\||[;|<>]/u.test(value);
}
export function parseSshInvocation(target: string, args: string[]): ParsedSshInvocation {
const route = parseSshRoute(target);
const operationArgs = normalizeSshOperationArgs(args);
if (route.plane === "k3s") {
return { providerId: route.providerId, route, parsed: parseK3sRouteArgs(route, operationArgs) };
}
if (route.plane === "win") {
return { providerId: route.providerId, route, parsed: parseWinRouteArgs(route, operationArgs) };
}
if ((operationArgs[0] ?? "") === "k3s") {
throw new Error(`ssh k3s shorthand is unsupported; use route syntax instead: trans ${route.providerId}:k3s ${operationArgs.slice(1).join(" ")}`.trim());
}
return { providerId: route.providerId, route, parsed: parseSshArgs(operationArgs) };
}
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}`);
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 === "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 === "ps" || operation === "powershell" || operation === "powershell.exe") {
const commandArgs = args[1] === "--" ? args.slice(2) : args.slice(1);
if (commandArgs.length === 0) {
return {
remoteCommand: buildWindowsPowerShellInvocation(buildWindowsPowerShellStdinLauncherScript(route.workspace)),
requiresStdin: true,
invocationKind: "helper",
};
}
return {
remoteCommand: buildWindowsPowerShellInvocation(buildWindowsPowerShellInlineLauncherScript(commandArgs.join(" "), route.workspace)),
requiresStdin: false,
invocationKind: "helper",
};
}
if (operation !== "cmd" && operation !== "cmd.exe") {
throw new Error(`unsupported ssh win operation: ${operation}; use ssh ${route.providerId}:win ps, ssh ${route.providerId}:win cmd <command-line>, ssh ${route.providerId}:win apply-patch, or ssh ${route.providerId}:win skills`);
}
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.join(" "), route.workspace))),
requiresStdin: false,
invocationKind: "argv",
};
}
function buildWindowsCmdLine(userCommand: string, cwd: string | null): string {
const parts = [
"chcp 65001>nul",
'set "PYTHONUTF8=1"',
'set "PYTHONIOENCODING=utf-8"',
];
if (cwd !== null) parts.push(`cd /d ${windowsCmdQuote(cwd)}`);
parts.push(userCommand);
return parts.join(" && ");
}
function windowsCmdQuote(value: string): string {
if (/[\r\n"]/u.test(value)) throw new Error("ssh win workspace path must not contain quotes or newlines");
return `"${value}"`;
}
function parseWindowsSkillsOptions(args: string[]): { limit: number; scopes: Array<"agents" | "codex"> } {
let limit = 100;
let scope: "agents" | "codex" | "all" = "agents";
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;
}
throw new Error(`unsupported ssh win skills option: ${arg}`);
}
return { limit, scopes: scope === "all" ? ["agents", "codex"] : [scope] };
}
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};`,
`$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 };",
" }",
" }",
" $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; counts = [pscustomobject]@{ roots = $rootRecords.Count; skills = $skills.Count; limit = $limit }; roots = $rootRecords; skills = @($skills) };",
"$payload | ConvertTo-Json -Depth 6;",
].join(" ");
}
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(" ");
}
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()",
"$env:PYTHONUTF8 = '1'",
"$env:PYTHONIOENCODING = 'utf-8'",
...(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 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 podPrefix = k3sPodRoutePrefixes.find((prefix) => current === prefix.slice(0, -1) || current.startsWith(prefix));
if (podPrefix === undefined) {
normalized.push(current);
continue;
}
if (current === podPrefix.slice(0, -1)) {
const next = rest[index + 1];
if (next === undefined || next.length === 0) throw new Error("ssh k3s pod: route requires a pod name after pod:");
normalized.push(`pod/${next}`);
index += 1;
continue;
}
const podName = current.slice(podPrefix.length);
if (podName.length === 0) throw new Error("ssh k3s pod: route requires a pod name after pod:");
normalized.push(`pod/${podName}`);
}
return normalized;
}
function splitK3sResourceWorkspace(value: string | null): { resource: string | null; workspace: string | null } {
if (value === null || value.length === 0) return { resource: null, workspace: null };
const parts = value.split("/");
if (parts.length <= 1) return { resource: value, workspace: null };
if (isK3sResourceKindAlias(parts[0] ?? "") && (parts[1] ?? "").length > 0) {
const workspaceParts = parts.slice(2);
return {
resource: `${parts[0]}/${parts[1]}`,
workspace: workspaceParts.length > 0 ? `/${workspaceParts.join("/")}` : null,
};
}
return {
resource: parts[0] ?? value,
workspace: parts.length > 1 ? `/${parts.slice(1).join("/")}` : null,
};
}
function splitK3sContainerWorkspace(value: string | null): { container: string | null; workspace: string | null } {
if (value === null || value.length === 0) return { container: null, workspace: null };
const parts = value.split("/");
if (parts.length <= 1) return { container: value, workspace: null };
return { container: parts[0] ?? value, workspace: `/${parts.slice(1).join("/")}` };
}
function combineK3sRouteWorkspace(first: string | null, second: string | null): string | null {
if (first !== null && second !== null && first !== second) throw new Error("ssh k3s route workspace can be specified once, either after the workload or after the container");
return first ?? second;
}
function isK3sResourceKindAlias(value: string): boolean {
return k3sResourceKindAliases.has(value);
}
function k3sOperationInRouteMessage(target: string, operation: string): string {
const providerId = target.split(":")[0] || "<provider>";
if (operation === "v2" || operation === "patch" || operation === "patch-v1") {
return `ssh k3s route must locate a target only; remote patch entrypoints are "apply-patch" for the default v2 engine and "apply-patch-v1" for the legacy helper instead of "${target}"`;
}
const operationExample = operation === "guard" ? "guard" : `${operation} ...`;
return `ssh k3s route must locate a target only; put operation "${operation}" after the route, for example "ssh ${providerId}:k3s ${operationExample}" or "ssh ${providerId}:k3s:<namespace>:<workload> ${operationExample}" instead of "${target}"`;
}
export function shellArgv(args: string[]): string {
return args.map(shellQuote).join(" ");
}
export function shellQuote(value: string): string {
return `'${value.replace(/'/g, `'\\''`)}'`;
}
function positiveInt(value: string, option: string): number {
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${option} must be a positive integer`);
return parsed;
}
function optionalPositiveInt(value: string, option: string): string {
return String(positiveInt(value, option));
}
function findOptionValue(args: string[], index: number, option: string): string {
const value = args[index + 1];
if (value === undefined || value.length === 0) throw new Error(`ssh find ${option} requires a value`);
return value;
}
function buildFindCommand(args: string[]): string {
const paths: string[] = [];
const predicates: string[] = [];
const patternPredicates: Array<[string, string]> = [];
let limit: number | null = null;
let sortOutput = false;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index] ?? "";
if (arg === "--limit") {
const value = findOptionValue(args, index, arg);
limit = positiveInt(value, "ssh find --limit");
index += 1;
continue;
}
if (arg === "--sort") {
sortOutput = true;
continue;
}
if (arg === "--max-depth" || arg === "-maxdepth" || arg === "--min-depth" || arg === "-mindepth") {
const value = findOptionValue(args, index, arg);
const findArg = arg === "--max-depth" ? "-maxdepth" : arg === "--min-depth" ? "-mindepth" : arg;
predicates.push(findArg, String(positiveInt(value, `ssh find ${arg}`)));
index += 1;
continue;
}
if (arg === "--type" || arg === "-type") {
const value = findOptionValue(args, index, arg);
if (!/^[bcdpfls]$/u.test(value)) throw new Error("ssh find --type must be one of: b c d p f l s");
predicates.push("-type", value);
index += 1;
continue;
}
if (arg === "--name" || arg === "-name" || arg === "--iname" || arg === "-iname" || arg === "--path" || arg === "-path" || arg === "--ipath" || arg === "-ipath") {
const value = findOptionValue(args, index, arg);
const findArg = arg.startsWith("--") ? `-${arg.slice(2)}` : arg;
patternPredicates.push([findArg, value]);
index += 1;
continue;
}
if (arg === "--contains" || arg === "--icontains") {
const value = findOptionValue(args, index, arg);
const findArg = arg === "--contains" ? "-name" : "-iname";
patternPredicates.push([findArg, `*${value}*`]);
index += 1;
continue;
}
if (arg === "--mtime" || arg === "-mtime" || arg === "--mmin" || arg === "-mmin" || arg === "--size" || arg === "-size") {
const value = findOptionValue(args, index, arg);
const findArg = arg.startsWith("--") ? `-${arg.slice(2)}` : arg;
predicates.push(findArg, value);
index += 1;
continue;
}
if (arg.startsWith("-")) {
throw new Error(`unsupported ssh find option: ${arg}`);
}
paths.push(arg);
}
const findArgs = ["find", ...(paths.length > 0 ? paths : ["."]), ...predicates];
if (patternPredicates.length === 1) {
const [kind, pattern] = patternPredicates[0]!;
findArgs.push(kind, pattern);
} else if (patternPredicates.length > 1) {
findArgs.push("(");
patternPredicates.forEach(([kind, pattern], index) => {
if (index > 0) findArgs.push("-o");
findArgs.push(kind, pattern);
});
findArgs.push(")");
}
findArgs.push("-print");
let command = shellArgv(findArgs);
if (sortOutput) command = `${command} | sort`;
if (limit !== null) command = `${command} | head -n ${limit}`;
return command;
}
function parseK3sRouteArgs(route: ParsedSshRoute, args: string[]): ParsedSshArgs {
if (route.entry === null && route.namespace === null && route.resource === null) {
return parseK3sControlPlaneOperation(route, args);
}
if (route.namespace === null || route.resource === null) {
throw new Error("ssh k3s target route requires provider:k3s:<namespace>:<deployment|pod:pod-name|pod/resource>");
}
return parseK3sTargetOperation(route, args);
}
function parseK3sControlPlaneOperation(route: ParsedSshRoute, args: string[]): ParsedSshArgs {
const operation = args[0] ?? "guard";
if (operation === "upload" || operation === "download") {
throw new Error(`ssh ${route.providerId}:k3s ${operation} requires a workload route: ssh ${route.providerId}:k3s:<namespace>:<workload> ${operation} ...`);
}
if (operation === "apply-patch" || operation === "apply-patch-v1") {
throw new Error(`ssh ${route.providerId}:k3s apply-patch requires a workload route: ssh ${route.providerId}:k3s:<namespace>:<workload> apply-patch`);
}
if (operation === "patch" || operation === "patch-v1" || operation === "v2") {
throw new Error("remote patch entrypoints are `apply-patch` for the default v2 engine and `apply-patch-v1` for the legacy helper");
}
if (operation === "script" || operation === "sh") {
return buildK3sScriptOperation(args.slice(1));
}
if (operation === "shell") {
const parsed = parseShellStringOperationArgs(args.slice(1), `ssh ${route.providerId}:k3s shell`);
return { remoteCommand: shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, parsed.shell, "-c", shellScriptWithCompatibility(parsed.command)]), requiresStdin: false, invocationKind: "helper" };
}
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, 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") return buildK3sScriptOperation([...targetArgs, ...operationArgs]);
if (operation === "shell") {
const parsed = parseShellStringOperationArgs(operationArgs, `ssh ${route.raw} shell`);
return { remoteCommand: buildK3sExecCommand([...targetArgs, "--", parsed.shell, "-c", shellScriptWithCompatibility(parsed.command)]), requiresStdin: false, invocationKind: "helper" };
}
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]);
}
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");
}
function buildK3sCommand(providerId: 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") {
const parsed = buildK3sScriptOperation(args.slice(1));
if (parsed.remoteCommand === null) throw new Error("ssh k3s script resolved to an interactive command unexpectedly");
return parsed.remoteCommand;
}
if (action === "logs") return buildK3sLogsCommand(args.slice(1));
if (action === "kubectl") {
const kubectlArgs = args.slice(1);
if (kubectlArgs.length === 0) throw new Error("ssh k3s kubectl requires kubectl arguments");
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]);
}
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...args]);
}
function buildK3sGuardCommand(providerId: string): string {
const provider = providerId.toUpperCase();
const providerNodeCheck = provider === "D601"
? "printf '%s\\n' \"$nodes\" | grep -Fx d601 >/dev/null || { printf 'native_k3s_guard=blocked provider=%s reason=d601-node-missing\\n' \"$UNIDESK_K3S_PROVIDER_ID\" >&2; exit 1; }"
: provider === "G14"
? "printf '%s\\n' \"$nodes\" | grep -Fx ubuntu-rog-zephyrus-g14-ga401iv-ga401iv >/dev/null || { printf 'native_k3s_guard=blocked provider=%s reason=g14-node-missing\\n' \"$UNIDESK_K3S_PROVIDER_ID\" >&2; exit 1; }"
: "[ -n \"$nodes\" ] || { printf 'native_k3s_guard=blocked provider=%s reason=node-list-empty\\n' \"$UNIDESK_K3S_PROVIDER_ID\" >&2; exit 1; }";
const script = [
"set -euo pipefail",
`export KUBECONFIG=${shellQuote(nativeK3sKubeconfig)}`,
`export UNIDESK_K3S_PROVIDER_ID=${shellQuote(providerId)}`,
"context=$(kubectl config current-context)",
"server=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')",
"nodes=$(kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{\"\\n\"}{end}')",
"printf 'kubeconfig=%s\\n' \"$KUBECONFIG\"",
"printf 'provider=%s\\n' \"$UNIDESK_K3S_PROVIDER_ID\"",
"printf 'context=%s\\n' \"$context\"",
"printf 'server=%s\\n' \"$server\"",
"printf 'nodes=%s\\n' \"$(printf '%s' \"$nodes\" | tr '\\n' ' ')\"",
providerNodeCheck,
"case \"$server\" in *127.0.0.1:11700*|*docker-desktop*) printf 'native_k3s_guard=blocked reason=docker-desktop-context server=%s\\n' \"$server\" >&2; exit 1;; esac",
"printf 'native_k3s_guard=ok provider=%s\\n' \"$UNIDESK_K3S_PROVIDER_ID\"",
].join("; ");
return shellArgv(["bash", "-c", script]);
}
interface K3sTargetOptions {
namespace: string | null;
resource: string | null;
selector: string | null;
container: string | null;
workspace: string | null;
stdin: boolean;
tty: boolean;
shell: string | null;
command: string[];
kubectlOptions: string[];
}
interface ParseK3sTargetOptionsOptions {
requireCommand: boolean;
allowCommand?: boolean;
allowShell?: boolean;
allowSelector?: boolean;
}
function buildK3sExecCommand(args: string[]): string {
const parsed = parseK3sTargetOptions(args, "ssh k3s exec", { requireCommand: true });
if (parsed.namespace === null) throw new Error("ssh k3s exec requires --namespace <name>");
if (parsed.selector !== null) throw new Error("ssh k3s exec does not support --selector");
if (parsed.resource === null) throw new Error("ssh k3s exec requires --deployment <name>, --pod <name> or --resource <type/name>");
const kubectlArgs = [
"exec",
"-n", parsed.namespace,
parsed.resource,
...(parsed.container === null ? [] : ["-c", parsed.container]),
...(parsed.stdin ? ["-i"] : []),
...(parsed.tty ? ["-t"] : []),
...parsed.kubectlOptions,
"--",
...withK3sWorkspace(parsed, parsed.command),
];
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]);
}
function buildK3sScriptOperation(args: string[]): ParsedSshArgs {
const parsed = parseK3sTargetOptions(args, "ssh k3s script", { requireCommand: false, allowCommand: true, allowShell: true });
if (parsed.shell === null && parsed.command.length > 0) {
return { remoteCommand: buildK3sInlineScriptCommand(parsed), requiresStdin: false, invocationKind: "helper" };
}
return { remoteCommand: buildK3sStdinScriptCommand(parsed), requiresStdin: true, invocationKind: "helper", stdinPrefix: shellScriptStdinPrefix() };
}
function buildK3sStdinScriptCommand(parsed: K3sTargetOptions): string {
if (parsed.namespace === null && parsed.resource === null) return buildK3sHostScriptCommand(parsed);
if (parsed.namespace === null) throw new Error("ssh k3s script target requires --namespace <name>");
if (parsed.selector !== null) throw new Error("ssh k3s script does not support --selector");
if (parsed.resource === null) throw new Error("ssh k3s script target requires --deployment <name>, --pod <name> or --resource <type/name>");
if (parsed.tty) throw new Error("ssh k3s script does not support --tty; stdin is reserved for the script body");
const shell = parsed.shell ?? "sh";
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): string {
if (parsed.command.length === 0) throw new Error("ssh k3s script -- requires a command");
if (parsed.selector !== null) throw new Error("ssh k3s script -- does not support --selector");
if (parsed.tty) throw new Error("ssh k3s script does not support --tty; stdin is reserved for the script body");
if (parsed.stdin) throw new Error("ssh k3s script -- does not accept --stdin");
const command = parsed.command.length === 1 ? ["sh", "-c", shellScriptWithCompatibility(parsed.command[0] ?? "")] : parsed.command;
if (parsed.namespace === null && parsed.resource === null) {
if (parsed.container !== null) throw new Error("ssh k3s script without a workload does not accept --container");
if (parsed.workspace !== null) throw new Error("ssh k3s script without a workload does not accept --workdir");
if (parsed.kubectlOptions.length > 0) throw new Error("ssh k3s script without a workload does not accept kubectl log options");
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, ...command]);
}
if (parsed.namespace === null) throw new Error("ssh k3s script target requires --namespace <name>");
if (parsed.resource === null) throw new Error("ssh k3s script target requires --deployment <name>, --pod <name> or --resource <type/name>");
const kubectlArgs = [
"exec",
"-n", parsed.namespace,
parsed.resource,
...(parsed.container === null ? [] : ["-c", parsed.container]),
...parsed.kubectlOptions,
"--",
...withK3sWorkspace(parsed, command),
];
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]);
}
function buildK3sApplyPatchCommand(args: string[]): ParsedSshArgs {
const parsed = parseK3sTargetOptions(args, "ssh k3s apply-patch", { requireCommand: false, allowCommand: true });
if (parsed.namespace === null) throw new Error("ssh k3s apply-patch requires --namespace <name>");
if (parsed.resource === null) throw new Error("ssh k3s apply-patch requires --deployment <name>, --pod <name> or --resource <type/name>");
if (parsed.tty) throw new Error("ssh k3s apply-patch does not support --tty; stdin is reserved for the patch body");
if (parsed.stdin) throw new Error("ssh k3s apply-patch does not accept --stdin; stdin is always the patch body");
if (parsed.shell !== null) throw new Error("ssh k3s apply-patch does not accept --shell");
if (parsed.kubectlOptions.length > 0) throw new Error("ssh k3s apply-patch does not accept kubectl log options");
const kubectlArgs = [
"exec",
"-i",
"-n", parsed.namespace,
parsed.resource,
...(parsed.container === null ? [] : ["-c", parsed.container]),
"--",
...withK3sWorkspace(parsed, ["sh", "-s", "--", ...parsed.command]),
];
const wrapper = podApplyPatchStdinWrapper();
return {
remoteCommand: shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]),
requiresStdin: true,
invocationKind: "helper",
stdinPrefix: wrapper.prefix,
stdinSuffix: wrapper.suffix,
};
}
function buildK3sHostScriptCommand(parsed: K3sTargetOptions): string {
if (parsed.tty) throw new Error("ssh k3s script does not support --tty; stdin is reserved for the script body");
if (parsed.stdin) throw new Error("ssh k3s script does not accept --stdin; stdin is always the script body");
if (parsed.container !== null) throw new Error("ssh k3s script without a workload does not accept --container");
if (parsed.workspace !== null) throw new Error("ssh k3s script without a workload does not accept --workdir");
if (parsed.kubectlOptions.length > 0) throw new Error("ssh k3s script without a workload does not accept kubectl log options");
const shell = parsed.shell ?? "sh";
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, shell, "-s", "--", ...parsed.command]);
}
function shellStringFromArgs(args: string[], commandName = "ssh shell"): string {
if (args.length === 0) throw new Error(`${commandName} requires a command string`);
return args.join(" ");
}
function parseShellStringOperationArgs(args: string[], commandName: string): { shell: string; command: string } {
let shell = "sh";
const commandArgs: string[] = [];
for (let index = 0; index < args.length; index += 1) {
const arg = args[index] ?? "";
if (arg === "--shell") {
shell = k3sScriptShell(k3sOptionValue(args, index, `${commandName} --shell`), `${commandName} --shell`);
index += 1;
continue;
}
commandArgs.push(arg);
}
return { shell, command: shellStringFromArgs(commandArgs, commandName) };
}
function buildK3sLogsCommand(args: string[]): string {
const parsed = parseK3sTargetOptions(args, "ssh k3s logs", { requireCommand: false, allowSelector: true });
if (parsed.namespace === null) throw new Error("ssh k3s logs requires --namespace <name>");
if (parsed.resource === null && parsed.selector === null) throw new Error("ssh k3s logs requires --deployment <name>, --pod <name>, --resource <type/name> or --selector <label-selector>");
if (parsed.resource !== null && parsed.selector !== null) throw new Error("ssh k3s logs accepts either a resource or --selector, not both");
if (parsed.stdin || parsed.tty) throw new Error("ssh k3s logs does not support --stdin or --tty");
if (parsed.workspace !== null) throw new Error("ssh k3s logs does not accept --workdir");
const kubectlArgs = [
"logs",
"-n", parsed.namespace,
...(parsed.selector === null ? [parsed.resource ?? ""] : ["-l", parsed.selector]),
...(parsed.container === null ? [] : ["-c", parsed.container]),
...parsed.kubectlOptions,
];
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]);
}
function parseK3sTargetOptions(args: string[], commandName: string, options: ParseK3sTargetOptionsOptions): K3sTargetOptions {
let namespace: string | null = null;
let resource: string | null = null;
let selector: string | null = null;
let container: string | null = null;
let workspace: string | null = null;
let stdin = false;
let tty = false;
let shell: string | null = null;
const kubectlOptions: string[] = [];
const command: string[] = [];
let afterDoubleDash = false;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index] ?? "";
if (afterDoubleDash) {
command.push(arg);
continue;
}
if (arg === "--") {
afterDoubleDash = true;
continue;
}
if (arg === "--namespace" || arg === "-n") {
namespace = k3sOptionValue(args, index, `${commandName} ${arg}`);
index += 1;
continue;
}
const namespaceValue = k3sEqualsOptionValue(arg, "--namespace", commandName);
if (namespaceValue !== null) {
namespace = namespaceValue;
continue;
}
if (arg === "--deployment" || arg === "--deploy") {
resource = `deployment/${k3sOptionValue(args, index, `${commandName} ${arg}`)}`;
index += 1;
continue;
}
const deploymentValue = k3sEqualsOptionValue(arg, "--deployment", commandName) ?? k3sEqualsOptionValue(arg, "--deploy", commandName);
if (deploymentValue !== null) {
resource = `deployment/${deploymentValue}`;
continue;
}
if (arg === "--pod") {
resource = `pod/${k3sOptionValue(args, index, `${commandName} ${arg}`)}`;
index += 1;
continue;
}
const podValue = k3sEqualsOptionValue(arg, "--pod", commandName);
if (podValue !== null) {
resource = `pod/${podValue}`;
continue;
}
if (arg === "--resource") {
resource = normalizeK3sResource(k3sOptionValue(args, index, `${commandName} ${arg}`));
index += 1;
continue;
}
const resourceValue = k3sEqualsOptionValue(arg, "--resource", commandName);
if (resourceValue !== null) {
resource = normalizeK3sResource(resourceValue);
continue;
}
if (arg === "--selector" || arg === "-l") {
if (options.allowSelector !== true) throw new Error(`${commandName} does not support ${arg}`);
selector = k3sOptionValue(args, index, `${commandName} ${arg}`);
index += 1;
continue;
}
const selectorValue = k3sEqualsOptionValue(arg, "--selector", commandName) ?? k3sEqualsOptionValue(arg, "-l", commandName);
if (selectorValue !== null) {
if (options.allowSelector !== true) throw new Error(`${commandName} does not support ${arg.split("=", 1)[0]}`);
selector = selectorValue;
continue;
}
if (arg === "--container" || arg === "-c") {
container = k3sOptionValue(args, index, `${commandName} ${arg}`);
index += 1;
continue;
}
const containerValue = k3sEqualsOptionValue(arg, "--container", commandName);
if (containerValue !== null) {
container = containerValue;
continue;
}
if (arg === "--workdir" || arg === "--cwd") {
workspace = k3sWorkspaceValue(k3sOptionValue(args, index, `${commandName} ${arg}`), `${commandName} ${arg}`);
index += 1;
continue;
}
const workdirValue = k3sEqualsOptionValue(arg, "--workdir", commandName) ?? k3sEqualsOptionValue(arg, "--cwd", commandName);
if (workdirValue !== null) {
workspace = k3sWorkspaceValue(workdirValue, `${commandName} ${arg.split("=", 1)[0]}`);
continue;
}
if (arg === "--shell") {
if (!options.allowShell) throw new Error(`${commandName} does not support --shell`);
shell = k3sScriptShell(k3sOptionValue(args, index, `${commandName} ${arg}`), `${commandName} ${arg}`);
index += 1;
continue;
}
const shellValue = k3sEqualsOptionValue(arg, "--shell", commandName);
if (shellValue !== null) {
if (!options.allowShell) throw new Error(`${commandName} does not support --shell`);
shell = k3sScriptShell(shellValue, `${commandName} --shell`);
continue;
}
if (arg === "--stdin" || arg === "-i") {
stdin = true;
continue;
}
if (arg === "--tty" || arg === "-t") {
tty = true;
continue;
}
if (arg === "--tail") {
kubectlOptions.push("--tail", optionalPositiveInt(k3sOptionValue(args, index, `${commandName} ${arg}`), `${commandName} --tail`));
index += 1;
continue;
}
const tailValue = k3sEqualsOptionValue(arg, "--tail", commandName);
if (tailValue !== null) {
kubectlOptions.push("--tail", optionalPositiveInt(tailValue, `${commandName} --tail`));
continue;
}
if (arg === "--since" || arg === "--since-time" || arg === "--limit-bytes") {
kubectlOptions.push(arg, k3sOptionValue(args, index, `${commandName} ${arg}`));
index += 1;
continue;
}
const sinceValue = k3sEqualsOptionValue(arg, "--since", commandName);
const sinceTimeValue = k3sEqualsOptionValue(arg, "--since-time", commandName);
const limitBytesValue = k3sEqualsOptionValue(arg, "--limit-bytes", commandName);
if (sinceValue !== null || sinceTimeValue !== null || limitBytesValue !== null) {
const optionName = sinceValue !== null ? "--since" : sinceTimeValue !== null ? "--since-time" : "--limit-bytes";
const optionValue = sinceValue ?? sinceTimeValue ?? limitBytesValue;
kubectlOptions.push(optionName, optionValue ?? "");
continue;
}
if (arg === "--previous" || arg === "--timestamps" || arg === "--all-containers" || arg === "--prefix") {
kubectlOptions.push(arg);
continue;
}
if (arg === "--follow" || arg === "-f") {
throw new Error(`${commandName} does not support ${arg}; use a bounded logs command and poll again`);
}
if (arg.startsWith("-")) throw new Error(`unsupported ${commandName} option: ${arg}`);
if (resource === null) {
resource = normalizeK3sResource(arg);
continue;
}
throw new Error(`unexpected ${commandName} argument before --: ${arg}`);
}
if (options.requireCommand && command.length === 0) throw new Error(`${commandName} requires -- <command> [args...]`);
if (!options.requireCommand && options.allowCommand !== true && command.length > 0) throw new Error(`${commandName} does not accept a command after --`);
return { namespace, resource, selector, container, workspace, stdin, tty, shell, command, kubectlOptions };
}
function k3sEqualsOptionValue(arg: string, option: string, commandName: string): string | null {
const prefix = `${option}=`;
if (!arg.startsWith(prefix)) return null;
const value = arg.slice(prefix.length);
if (value.length === 0) throw new Error(`${commandName} ${option} requires a value`);
return value;
}
function k3sOptionValue(args: string[], index: number, option: string): string {
const value = args[index + 1];
if (value === undefined || value.length === 0) throw new Error(`${option} requires a value`);
return value;
}
function k3sWorkspaceValue(value: string, option: string): string {
if (!value.startsWith("/")) throw new Error(`${option} must be an absolute pod workspace path`);
return value;
}
function withK3sWorkspace(parsed: K3sTargetOptions, command: string[]): string[] {
if (parsed.workspace === null) return command;
if (command.length === 0) throw new Error("ssh k3s workspace route requires a command to execute");
return ["sh", "-c", 'cd "$1" || exit; shift; exec "$@"', "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 buildShellCommand(args: string[]): ParsedSshArgs {
let shell = "sh";
const scriptArgs: string[] = [];
let afterDoubleDash = false;
let directArgvMode = false;
let shellOptionSeen = false;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index] ?? "";
if (afterDoubleDash) {
scriptArgs.push(arg);
continue;
}
if (arg === "--") {
if (!shellOptionSeen && scriptArgs.length === 0) {
directArgvMode = true;
scriptArgs.push(...args.slice(index + 1));
break;
}
afterDoubleDash = true;
continue;
}
if (arg === "--shell") {
shell = k3sScriptShell(k3sOptionValue(args, index, "ssh script --shell"), "ssh script --shell");
shellOptionSeen = true;
index += 1;
continue;
}
if (arg.startsWith("-")) throw new Error(`unsupported ssh script option: ${arg}`);
scriptArgs.push(arg);
}
if (directArgvMode) {
if (scriptArgs.length === 0) throw new Error("ssh script -- requires a command");
if (scriptArgs.length === 1) return { remoteCommand: shellArgv([shell, "-c", shellScriptWithCompatibility(scriptArgs[0] ?? "")]), requiresStdin: false, invocationKind: "helper" };
return { remoteCommand: shellArgv(scriptArgs), requiresStdin: false, invocationKind: "argv" };
}
return { remoteCommand: shellArgv([shell, "-s", "--", ...scriptArgs]), requiresStdin: true, invocationKind: "helper", stdinPrefix: shellScriptStdinPrefix() };
}
function buildShellStringCommand(args: string[]): ParsedSshArgs {
const parsed = parseShellStringOperationArgs(args, "ssh shell");
return { remoteCommand: shellArgv([parsed.shell, "-c", shellScriptWithCompatibility(parsed.command)]), requiresStdin: false, invocationKind: "helper" };
}
function podApplyPatchStdinWrapper(): { prefix: string; suffix: string } {
const toolMarker = "__UNIDESK_APPLY_PATCH_TOOL__";
const patchMarker = "__UNIDESK_APPLY_PATCH_PAYLOAD__";
if (remoteApplyPatchSource.includes(toolMarker)) throw new Error("remote apply_patch source contains reserved heredoc marker");
return {
prefix: [
"set -eu",
'UNIDESK_SSH_TOOL_DIR="${UNIDESK_SSH_TOOL_DIR:-/tmp/unidesk-ssh-tools}"',
'mkdir -p "$UNIDESK_SSH_TOOL_DIR"',
`cat > "$UNIDESK_SSH_TOOL_DIR/apply_patch" <<'${toolMarker}'`,
remoteApplyPatchSource.trimEnd(),
toolMarker,
'chmod 700 "$UNIDESK_SSH_TOOL_DIR/apply_patch"',
`"$UNIDESK_SSH_TOOL_DIR/apply_patch" "$@" <<'${patchMarker}'`,
].join("\n") + "\n",
suffix: `\n${patchMarker}\n`,
};
}
function buildPythonStdinCommand(args: string[]): string {
const pythonArgs = args.map(shellQuote).join(" ");
const execArgs = pythonArgs.length > 0 ? ` "$UNIDESK_SSH_PY_FILE" ${pythonArgs}` : ' "$UNIDESK_SSH_PY_FILE"';
return [
'UNIDESK_SSH_PY_FILE="$(mktemp /tmp/unidesk-ssh-py.XXXXXX.py)" || exit 1',
`trap 'rm -f "$UNIDESK_SSH_PY_FILE"' EXIT`,
'cat > "$UNIDESK_SSH_PY_FILE"',
`python3 -u${execArgs}`,
].join("; ");
}
const remoteToolSources: Record<SshHelperName, string> = {
apply_patch: remoteApplyPatchSource,
glob: remoteGlobSource,
"skill-discover": remoteSkillDiscoverSource,
};
function remoteToolBootstrapCommand(helpers: readonly SshHelperName[] = []): string {
const uniqueHelpers = [...new Set(helpers)];
if (uniqueHelpers.length === 0) return "";
const commands = [
"UNIDESK_SSH_TOOL_DIR=/tmp/unidesk-ssh-tools",
'mkdir -p "$UNIDESK_SSH_TOOL_DIR"',
];
for (const helper of uniqueHelpers) {
const source = remoteToolSources[helper];
const encoded = Buffer.from(source, "utf8").toString("base64");
commands.push(`printf %s ${shellQuote(encoded)} | base64 -d > "$UNIDESK_SSH_TOOL_DIR/${helper}"`);
commands.push(`chmod 700 "$UNIDESK_SSH_TOOL_DIR/${helper}"`);
}
commands.push('export PATH="$UNIDESK_SSH_TOOL_DIR:$PATH"');
return commands.join("; ");
}
export function wrapSshRemoteCommand(command: string | null, helpers: readonly SshHelperName[] = []): string {
const bootstrap = remoteToolBootstrapCommand(helpers);
const prelude = [sshUserToolPathPrelude, bootstrap].filter((part) => part.length > 0).join("; ");
const prefix = prelude.length > 0 ? `${prelude}; ` : "";
if (command === null) return `${prefix}exec "\${SHELL:-/bin/bash}" -l`;
return `${prefix}stty -echo 2>/dev/null || true; ${command}`;
}
function safeProviderId(providerId: string): string {
return /^[A-Za-z0-9_.-]{1,64}$/u.test(providerId) ? providerId : "<provider>";
}
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 structured argv or stdin script passthrough for non-interactive commands.",
try: `trans ${shownProviderId} script <<'SCRIPT'`,
triage: `bun scripts/cli.ts provider triage ${shownProviderId} --observed-scope ssh --observed-error '<ssh-like timeout or kex failure>'`,
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`;
}
export function sshSlowWarningThresholdMs(env: NodeJS.ProcessEnv = process.env): number {
const raw = env.UNIDESK_SSH_SLOW_WARNING_MS;
const parsed = raw === undefined ? NaN : Number(raw);
if (!Number.isFinite(parsed) || parsed <= 0) return defaultSshSlowWarningMs;
return Math.max(1000, Math.trunc(parsed));
}
export function sshRuntimeTimingHint(options: {
invocation: ParsedSshInvocation;
transport: SshRuntimeTimingHint["transport"];
exitCode: number;
startedAtMs: number;
finishedAtMs?: number;
thresholdMs?: number;
}): SshRuntimeTimingHint {
const finishedAtMs = options.finishedAtMs ?? Date.now();
const thresholdMs = options.thresholdMs ?? sshSlowWarningThresholdMs();
const elapsedMs = Math.max(0, Math.round(finishedAtMs - options.startedAtMs));
const elapsedSeconds = Number((elapsedMs / 1000).toFixed(3));
const slow = elapsedMs > thresholdMs;
const thresholdSeconds = Number((thresholdMs / 1000).toFixed(3));
return {
code: "ssh-runtime-timing",
level: slow ? "warning" : "info",
providerId: safeProviderId(options.invocation.providerId),
route: options.invocation.route.raw,
transport: options.transport,
invocationKind: options.invocation.parsed.invocationKind,
exitCode: options.exitCode,
elapsedMs,
elapsedSeconds,
thresholdMs,
slow,
message: slow
? `ssh operation took ${elapsedSeconds}s, above the ${thresholdSeconds}s warning threshold; consider checking provider/session latency, remote command cost, helper bootstrap, or tran/apply-patch optimization before repeating high-frequency operations.`
: `ssh operation completed in ${elapsedSeconds}s.`,
note: "Timing hint is written to stderr and intentionally does not echo the original remote command.",
};
}
export function formatSshRuntimeTimingHint(hint: SshRuntimeTimingHint): string {
if (!hint.slow) return "";
return `UNIDESK_SSH_TIMING ${JSON.stringify(hint)}\n`;
}
export function sshRuntimeTimeoutMs(env: NodeJS.ProcessEnv = process.env): number {
const raw = env.UNIDESK_SSH_RUNTIME_TIMEOUT_MS ?? env.UNIDESK_TRAN_RUNTIME_TIMEOUT_MS;
const parsed = raw === undefined ? NaN : Number(raw);
if (!Number.isFinite(parsed) || parsed <= 0) return defaultSshRuntimeTimeoutMs;
return Math.min(maxSshRuntimeTimeoutMs, Math.max(1000, Math.trunc(parsed)));
}
export function sshRuntimeTimeoutHint(options: {
invocation: ParsedSshInvocation;
transport: SshRuntimeTimeoutHint["transport"];
timeoutMs: number;
}): SshRuntimeTimeoutHint {
const timeoutSeconds = Number((options.timeoutMs / 1000).toFixed(3));
return {
code: "ssh-runtime-timeout",
level: "warning",
providerId: safeProviderId(options.invocation.providerId),
route: options.invocation.route.raw,
transport: options.transport,
invocationKind: options.invocation.parsed.invocationKind,
timeoutMs: options.timeoutMs,
timeoutSeconds,
message: `ssh/tran operation exceeded the ${timeoutSeconds}s top-level runtime limit and was disconnected.`,
action: "Use short query plus poll semantics; do not keep tran open waiting for long CI/CD, trace, logs, or build progress.",
note: "Timeout hint is written to stderr and intentionally does not echo the original remote command.",
};
}
export function formatSshRuntimeTimeoutHint(hint: SshRuntimeTimeoutHint): string {
return `UNIDESK_SSH_RUNTIME_TIMEOUT ${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));
const runtimeTimeoutMs = Number(open.runtimeTimeoutMs || 60000);
const runtimeTimer = setTimeout(() => {
process.stderr.write("unidesk ssh bridge runtime timeout; use short query plus poll semantics instead of keeping tran open\n");
exitCode = 124;
try { ws.close(); } catch {}
setTimeout(() => process.exit(124), 250).unref?.();
}, runtimeTimeoutMs);
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);
clearTimeout(runtimeTimer);
process.stderr.write(String(message.message || "ssh bridge error") + "\n");
exitCode = 255;
ws.close();
return;
}
if (message.type === "ssh.exit") {
clearTimeout(openTimer);
clearTimeout(runtimeTimer);
exitCode = Number.isInteger(message.exitCode) ? message.exitCode : 255;
ws.close();
}
});
ws.addEventListener("close", () => {
clearTimeout(openTimer);
clearTimeout(runtimeTimer);
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 function remoteCommandForRoute(route: ParsedSshRoute, command: string[], options: { stdin?: boolean } = {}): string {
if (route.plane === "k3s") return buildK3sTargetCommand(route, command, options);
if (route.plane === "win") throw new Error(`ssh apply-patch does not support win routes yet: ${route.raw}`);
return shellArgv(command);
}
type WindowsApplyPatchFsOperation =
| "stat"
| "read-b64-block"
| "read-bulk-b64"
| "apply-replacements-bulk-stdin"
| "write-b64-stdin"
| "write-b64-begin"
| "write-b64-append-stdin"
| "write-b64-commit"
| "delete";
const windowsApplyPatchWriteB64ChunkChars = 12_000;
function createWindowsApplyPatchFileSystem(config: UniDeskConfig, invocation: ParsedSshInvocation): ApplyPatchV2FileSystem {
async function checked(operation: WindowsApplyPatchFsOperation, args: string[], input?: string): Promise<SshCaptureResult> {
const command = buildWindowsPowerShellInvocation(windowsApplyPatchFsScript(invocation.route.workspace, operation, args));
const result = await runSshCaptureRemoteCommand(config, invocation, command, input);
if (result.exitCode === 0) return result;
throw new Error(`windows apply-patch fs operation failed: ${operation} ${JSON.stringify({
route: invocation.route.raw,
args: args.slice(0, 4),
exitCode: result.exitCode,
stdout: result.stdout.slice(-2000),
stderr: result.stderr.slice(-4000),
})}`);
}
return {
async stat(filePath) {
const result = await checked("stat", [filePath]);
const [bytesText, sha256] = result.stdout.trim().split(/\s+/u);
const bytes = Number(bytesText);
if (!Number.isSafeInteger(bytes) || bytes < 0 || !/^[0-9a-f]{64}$/u.test(sha256 ?? "")) {
throw new Error(`windows apply-patch fs stat returned invalid metadata: ${JSON.stringify({ filePath, stdout: result.stdout.slice(0, 500) })}`);
}
return { bytes, sha256: sha256! };
},
async readBlock(filePath, blockIndex, blockBytes) {
const result = await checked("read-b64-block", [filePath, String(blockIndex), String(blockBytes)]);
const encoded = result.stdout.replace(/\s+/gu, "");
return encoded.length === 0 ? Buffer.alloc(0) : Buffer.from(encoded, "base64");
},
async writeFile(filePath, content) {
const encoded = content.toString("base64");
const expectedBytes = String(content.length);
const expectedSha256 = createHash("sha256").update(content).digest("hex");
try {
await checked("write-b64-stdin", [filePath, expectedBytes, expectedSha256], encoded);
return;
} catch {
const token = `${process.pid}-${Date.now()}-${randomBytes(4).toString("hex")}-${expectedSha256.slice(0, 12)}`;
await checked("write-b64-begin", [filePath, token]);
for (const chunk of chunkString(encoded, windowsApplyPatchWriteB64ChunkChars)) {
await checked("write-b64-append-stdin", [filePath, token], chunk);
}
await checked("write-b64-commit", [filePath, token, expectedBytes, expectedSha256]);
}
},
async deleteFile(filePath) {
await checked("delete", [filePath]);
},
async readFiles(filePaths) {
const result = await checked("read-bulk-b64", [String(filePaths.length)], `${filePaths.join("\n")}\n`);
return decodeApplyPatchV2BulkRead(result.stdout, filePaths);
},
async applyReplacementsBulk(filePaths, plans: Map<string, ApplyPatchV2BulkReplacementWritePlan>) {
const { targets, payload } = formatApplyPatchV2BulkReplacementPayload(filePaths, plans);
if (targets.length === 0) return;
await checked("apply-replacements-bulk-stdin", [String(targets.length)], payload);
},
};
}
function windowsApplyPatchFsScript(basePath: string | null, operation: WindowsApplyPatchFsOperation, args: string[]): string {
const target = args[0] ?? "";
const arg1 = args[1] ?? "";
const arg2 = args[2] ?? "";
const arg3 = args[3] ?? "";
return [
"$ErrorActionPreference = 'Stop';",
"$ProgressPreference = 'SilentlyContinue';",
"[Console]::InputEncoding = [System.Text.UTF8Encoding]::new();",
"[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new();",
"$OutputEncoding = [System.Text.UTF8Encoding]::new();",
`$basePath = ${powerShellSingleQuote(basePath ?? "")};`,
`$operation = ${powerShellSingleQuote(operation)};`,
`$targetArg = ${powerShellSingleQuote(target)};`,
`$arg1 = ${powerShellSingleQuote(arg1)};`,
`$arg2 = ${powerShellSingleQuote(arg2)};`,
`$arg3 = ${powerShellSingleQuote(arg3)};`,
"function Fail([string]$Message, [int]$Code) { [Console]::Error.WriteLine($Message); exit $Code }",
"function Resolve-UnideskPath([string]$Raw) { if ([string]::IsNullOrWhiteSpace($Raw)) { Fail 'empty apply-patch path' 2 }; if ([System.IO.Path]::IsPathRooted($Raw)) { return [System.IO.Path]::GetFullPath($Raw) }; if (-not [string]::IsNullOrWhiteSpace($basePath)) { return [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($basePath, $Raw)) }; return [System.IO.Path]::GetFullPath($Raw) }",
"function Ensure-Parent([string]$Target) { $parent = [System.IO.Path]::GetDirectoryName($Target); if (-not [string]::IsNullOrWhiteSpace($parent)) { [System.IO.Directory]::CreateDirectory($parent) | Out-Null } }",
"function Get-Sha256([string]$Path) { return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() }",
"function Get-Sha256Bytes([byte[]]$Bytes) { $sha = [System.Security.Cryptography.SHA256]::Create(); try { return ([System.BitConverter]::ToString($sha.ComputeHash($Bytes))).Replace('-', '').ToLowerInvariant() } finally { $sha.Dispose() } }",
"function Decode-Utf8([string]$Encoded) { return [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Encoded)) }",
"function Encode-Utf8([string]$Value) { return [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Value)) }",
"function Set-TmpPaths([string]$Target, [string]$Token) { if ($Token -notmatch '^[A-Za-z0-9_.-]+$') { Fail 'invalid apply-patch temp token' 2 }; $dir = [System.IO.Path]::GetDirectoryName($Target); if ([string]::IsNullOrWhiteSpace($dir)) { $dir = (Get-Location).ProviderPath }; $base = [System.IO.Path]::GetFileName($Target); $script:tmp = [System.IO.Path]::Combine($dir, '.' + $base + '.unidesk-v2-' + $Token + '.tmp'); $script:tmpB64 = $script:tmp + '.b64' }",
"function Verify-Temp([string]$Target, [string]$Tmp, [Int64]$ExpectedBytes, [string]$ExpectedSha256) { $actualBytes = ([System.IO.FileInfo]$Tmp).Length; if ($actualBytes -ne $ExpectedBytes) { Remove-Item -LiteralPath $Tmp -Force -ErrorAction SilentlyContinue; Fail ('apply-patch byte count mismatch for ' + $Target + ': expected=' + $ExpectedBytes + ' actual=' + $actualBytes) 23 }; $actualSha256 = Get-Sha256 $Tmp; if ($actualSha256 -ne $ExpectedSha256) { Remove-Item -LiteralPath $Tmp -Force -ErrorAction SilentlyContinue; Fail ('apply-patch sha256 mismatch for ' + $Target + ': expected=' + $ExpectedSha256 + ' actual=' + $actualSha256) 24 } }",
"function Decode-ToTarget([string]$Target, [string]$Encoded, [Int64]$ExpectedBytes, [string]$ExpectedSha256) { Ensure-Parent $Target; Set-TmpPaths $Target ([guid]::NewGuid().ToString('N')); try { $bytes = [Convert]::FromBase64String(($Encoded -replace '\\s','')) } catch { Fail ('apply-patch base64 decode failed for ' + $Target + ': ' + $_.Exception.Message) 22 }; [System.IO.File]::WriteAllBytes($script:tmp, $bytes); Verify-Temp $Target $script:tmp $ExpectedBytes $ExpectedSha256; Move-Item -LiteralPath $script:tmp -Destination $Target -Force; $actualSha256 = Get-Sha256 $Target; if ($actualSha256 -ne $ExpectedSha256) { Fail ('apply-patch final sha256 mismatch for ' + $Target) 25 } }",
"$target = Resolve-UnideskPath $targetArg;",
"switch ($operation) {",
" 'stat' { if (-not (Test-Path -LiteralPath $target -PathType Leaf)) { Fail ('file not found: ' + $target) 1 }; $bytes = ([System.IO.FileInfo]$target).Length; $digest = Get-Sha256 $target; [Console]::Out.WriteLine(([string]$bytes) + ' ' + $digest); break }",
" 'read-b64-block' { $blockIndex = [Int64]$arg1; $blockSize = [Int32]$arg2; if ($blockIndex -lt 0 -or $blockSize -le 0) { Fail 'invalid read block args' 2 }; $fs = [System.IO.File]::Open($target, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite); try { [void]$fs.Seek($blockIndex * $blockSize, [System.IO.SeekOrigin]::Begin); $buffer = New-Object byte[] $blockSize; $read = $fs.Read($buffer, 0, $blockSize); if ($read -gt 0) { [Console]::Out.Write([Convert]::ToBase64String($buffer, 0, $read)) } } finally { $fs.Dispose() }; break }",
" 'read-bulk-b64' { $expectedCount = [Int32]$targetArg; $items = @([Console]::In.ReadToEnd() -split \"`r?`n\" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }); if ($items.Count -ne $expectedCount) { Fail ('bulk read record count mismatch: expected=' + $expectedCount + ' actual=' + $items.Count) 23 }; [Console]::Out.WriteLine('UNIDESK_APPLY_PATCH_V2_BULK_READ ' + $items.Count); foreach ($item in $items) { $path = Resolve-UnideskPath $item; if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { Fail ('file not found: ' + $path) 1 }; $bytes = [System.IO.File]::ReadAllBytes($path); $pathB64 = Encode-Utf8 $item; $digest = Get-Sha256Bytes $bytes; [Console]::Out.Write($pathB64 + ' ' + $bytes.Length + ' ' + $digest + ' '); [Console]::Out.Write([System.Convert]::ToBase64String($bytes)); [Console]::Out.WriteLine() }; break }",
" 'apply-replacements-bulk-stdin' { $expectedCount = [Int32]$targetArg; $records = @([Console]::In.ReadToEnd() -split \"`r?`n\" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }); if ($records.Count -ne $expectedCount) { Fail ('bulk replacement record count mismatch: expected=' + $expectedCount + ' actual=' + $records.Count) 23 }; $utf8 = [System.Text.UTF8Encoding]::new($false); $plans = New-Object System.Collections.Generic.List[object]; $index = 0; foreach ($record in $records) { $index += 1; $fields = $record -split '\\s+'; if ($fields.Count -ne 6) { Fail 'bulk replacement malformed record' 23 }; $targetRelative = Decode-Utf8 $fields[0]; $resolved = Resolve-UnideskPath $targetRelative; $originalBytes = [Int64]$fields[1]; $originalSha256 = $fields[2]; $finalBytes = [Int64]$fields[3]; $finalSha256 = $fields[4]; $replacementsText = $fields[5]; if (-not (Test-Path -LiteralPath $resolved -PathType Leaf)) { Fail ('file not found: ' + $resolved) 1 }; $source = [System.IO.File]::ReadAllBytes($resolved); if ($source.Length -ne $originalBytes -or (Get-Sha256Bytes $source) -ne $originalSha256) { Fail ('bulk replacement original integrity mismatch for ' + $resolved) 23 }; $lines = New-Object System.Collections.Generic.List[string]; $sourceText = [System.Text.Encoding]::UTF8.GetString($source); foreach ($line in ($sourceText -split \"`n\", -1)) { $lines.Add($line) | Out-Null }; if ($lines.Count -gt 0 -and $lines[$lines.Count - 1] -eq '') { $lines.RemoveAt($lines.Count - 1) }; $replacements = New-Object System.Collections.Generic.List[object]; if (-not [string]::IsNullOrEmpty($replacementsText)) { foreach ($item in ($replacementsText -split ';')) { if ([string]::IsNullOrWhiteSpace($item)) { continue }; $parts = $item -split ',', 3; if ($parts.Count -ne 3) { Fail 'bulk replacement malformed replacement' 23 }; $newText = Decode-Utf8 $parts[2]; $newLines = New-Object System.Collections.Generic.List[string]; foreach ($line in ($newText -split \"`n\", -1)) { $newLines.Add($line) | Out-Null }; if ($newLines.Count -gt 0 -and $newLines[$newLines.Count - 1] -eq '') { $newLines.RemoveAt($newLines.Count - 1) }; $replacements.Add([pscustomobject]@{ start = [Int32]$parts[0]; oldLength = [Int32]$parts[1]; newLines = [string[]]$newLines }) | Out-Null } }; foreach ($replacement in @($replacements | Sort-Object -Property start -Descending)) { if ($replacement.start -lt 0 -or $replacement.oldLength -lt 0 -or ($replacement.start + $replacement.oldLength) -gt $lines.Count) { Fail ('bulk replacement out of bounds for ' + $resolved) 23 }; $removeCount = $replacement.oldLength; if ($removeCount -gt 0) { $lines.RemoveRange($replacement.start, $removeCount) }; if ($replacement.newLines.Count -gt 0) { $lines.InsertRange($replacement.start, [string[]]$replacement.newLines) } }; $outputText = if ($lines.Count -eq 0) { '' } else { ([string]::Join(\"`n\", [string[]]$lines) + \"`n\") }; $outputBytes = [System.Text.Encoding]::UTF8.GetBytes($outputText); if ($outputBytes.Length -ne $finalBytes -or (Get-Sha256Bytes $outputBytes) -ne $finalSha256) { Fail ('bulk replacement final integrity mismatch for ' + $resolved) 23 }; $tmp = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName($resolved), ('.' + [System.IO.Path]::GetFileName($resolved) + '.unidesk-v2-bulk-' + [guid]::NewGuid().ToString('N') + '.tmp')); [System.IO.File]::WriteAllText($tmp, $outputText, $utf8); $plans.Add([pscustomobject]@{ target = $resolved; tmp = $tmp; sha256 = $finalSha256 }) | Out-Null }; foreach ($plan in $plans) { Ensure-Parent $plan.target; Move-Item -LiteralPath $plan.tmp -Destination $plan.target -Force; if ((Get-Sha256 $plan.target) -ne $plan.sha256) { Fail ('bulk replacement final sha256 mismatch for ' + $plan.target) 25 } }; break }",
" 'write-b64-stdin' { Decode-ToTarget $target ([Console]::In.ReadToEnd()) ([Int64]$arg1) $arg2; break }",
" 'write-b64-begin' { Ensure-Parent $target; Set-TmpPaths $target $arg1; [System.IO.File]::WriteAllText($script:tmpB64, '', [System.Text.Encoding]::ASCII); break }",
" 'write-b64-append-stdin' { Set-TmpPaths $target $arg1; $chunk = ([Console]::In.ReadToEnd()) -replace '\\s',''; [System.IO.File]::AppendAllText($script:tmpB64, $chunk, [System.Text.Encoding]::ASCII); break }",
" 'write-b64-commit' { Set-TmpPaths $target $arg1; $encoded = [System.IO.File]::ReadAllText($script:tmpB64, [System.Text.Encoding]::ASCII); Remove-Item -LiteralPath $script:tmpB64 -Force -ErrorAction SilentlyContinue; Decode-ToTarget $target $encoded ([Int64]$arg2) $arg3; break }",
" 'delete' { Remove-Item -LiteralPath $target -Force -ErrorAction SilentlyContinue; break }",
" default { Fail ('unsupported apply-patch fs op: ' + $operation) 2 }",
"}",
].join(" ");
}
function chunkString(value: string, chunkSize: number): string[] {
const chunks: string[] = [];
for (let index = 0; index < value.length; index += chunkSize) {
chunks.push(value.slice(index, index + chunkSize));
}
return chunks.length > 0 ? chunks : [""];
}
async function runSshCaptureCommand(config: UniDeskConfig, invocation: ParsedSshInvocation, command: string[], input?: string): Promise<SshCaptureResult> {
const remoteCommand = remoteCommandForRoute(invocation.route, command, { stdin: input !== undefined });
return await runSshCaptureRemoteCommand(config, invocation, remoteCommand, input);
}
async function runSshCaptureRemoteCommand(config: UniDeskConfig, invocation: ParsedSshInvocation, remoteCommand: string, input?: string): Promise<SshCaptureResult> {
const startedAtMs = Date.now();
const size = terminalSize();
const runtimeTimeoutMs = sshRuntimeTimeoutMs();
const payload = {
providerId: invocation.providerId,
command: wrapSshRemoteCommand(remoteCommand),
cwd: sshRoutePayloadCwd(invocation.route),
tty: false,
stdinEotOnEnd: false,
openTimeoutMs: Math.max(15000, Number(process.env.UNIDESK_SSH_OPEN_TIMEOUT_MS || 60000)),
runtimeTimeoutMs,
cols: size.cols,
rows: size.rows,
};
const payloadJson = JSON.stringify(payload);
const encodedBrokerSource = Buffer.from(brokerSource(), "utf8").toString("base64");
const script = [
"set -eu",
`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")}`,
'broker_js="$(mktemp "${TMPDIR:-/tmp}/unidesk-ssh-broker.XXXXXX")"',
'trap \'rm -f "$broker_js"\' EXIT',
`printf %s ${shellQuote(encodedBrokerSource)} | base64 -d >"$broker_js"`,
'bun "$broker_js" "$payload"',
].join("\n");
const child = spawn("docker", ["exec", "-i", "unidesk-backend-core", "sh", "-c", script], {
cwd: repoRoot,
stdio: ["pipe", "pipe", "pipe"],
});
if (input !== undefined) {
writeChunkedStdin(child.stdin, input);
} else {
child.stdin.end();
}
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk: Buffer) => {
stdout += chunk.toString("utf8");
});
child.stderr.on("data", (chunk: Buffer) => {
stderr += chunk.toString("utf8");
});
return await new Promise<SshCaptureResult>((resolve) => {
let settled = false;
let killTimer: NodeJS.Timeout | null = null;
const finish = (exitCode: number): void => {
if (settled) return;
settled = true;
clearTimeout(runtimeTimer);
if (killTimer !== null) clearTimeout(killTimer);
const timingHint = formatSshRuntimeTimingHint(sshRuntimeTimingHint({
invocation,
transport: "backend-core-broker",
exitCode,
startedAtMs,
}));
if (timingHint) stderr += timingHint;
resolve({ exitCode, stdout, stderr });
};
const runtimeTimer = setTimeout(() => {
const hint = formatSshRuntimeTimeoutHint(sshRuntimeTimeoutHint({
invocation,
transport: "backend-core-broker",
timeoutMs: runtimeTimeoutMs,
}));
stderr += hint;
try {
child.kill("SIGTERM");
} catch {
// Ignore.
}
killTimer = setTimeout(() => {
try {
child.kill("SIGKILL");
} catch {
// Ignore.
}
}, 2000);
finish(124);
}, runtimeTimeoutMs);
child.on("error", (error) => {
stderr += `unidesk ssh failed to start broker: ${error.message}\n`;
finish(255);
});
child.on("close", (code) => finish(code ?? 255));
});
}
export async function runSshCommandCapture(config: UniDeskConfig, target: string, args: string[], input?: string): Promise<SshCaptureResult> {
const invocation = parseSshInvocation(target, args);
const parsed = invocation.parsed;
if (parsed.remoteCommand === null) throw new Error(`ssh ${target} capture requires a non-interactive operation`);
const stdin = parsed.stdinPrefix !== undefined || parsed.stdinSuffix !== undefined
? `${parsed.stdinPrefix ?? ""}${input ?? ""}${parsed.stdinSuffix ?? ""}`
: input;
return await runSshCaptureRemoteCommand(config, invocation, parsed.remoteCommand, stdin);
}
function writeChunkedStdin(stdin: NodeJS.WritableStream, input: string): void {
const buffer = Buffer.from(input, "utf8");
const chunkSize = 32 * 1024;
for (let offset = 0; offset < buffer.length; offset += chunkSize) {
stdin.write(buffer.subarray(offset, Math.min(buffer.length, offset + chunkSize)));
}
stdin.end();
}
export async function runSsh(config: UniDeskConfig, providerId: string, args: string[]): Promise<number> {
const normalizedArgs = normalizeSshOperationArgs(args);
process.stderr.write(sshRouteSeparatorCompatibilityHint(args, normalizedArgs));
const invocation = parseSshInvocation(providerId, normalizedArgs);
const parsed = invocation.parsed;
const operationName = normalizedArgs[0] ?? "";
if (isSshFileTransferOperation(normalizedArgs)) {
const executor: SshRemoteCommandExecutor = {
runRemoteCommand: (remoteCommand, input) => runSshCaptureRemoteCommand(config, invocation, remoteCommand, input),
};
return await runSshFileTransferOperation(invocation, normalizedArgs, executor, {
buildRouteCommand: remoteCommandForRoute,
buildWindowsPowerShellCommand: buildWindowsPowerShellInvocation,
});
}
if (operationName === "apply-patch") {
const executor: ApplyPatchV2Executor = invocation.route.plane === "win"
? { fs: createWindowsApplyPatchFileSystem(config, invocation) }
: { run: (command, input) => runSshCaptureCommand(config, invocation, command, input) };
return await runApplyPatchV2({
executor,
stdin: process.stdin,
stdout: process.stdout,
stderr: process.stderr,
argv: normalizedArgs.slice(1),
timing: {
providerId: invocation.providerId,
route: invocation.route.raw,
transport: "backend-core-broker",
},
});
}
const startedAtMs = Date.now();
const size = terminalSize();
const openTimeoutMs = Math.max(15000, Number(process.env.UNIDESK_SSH_OPEN_TIMEOUT_MS || 60000));
const runtimeTimeoutMs = sshRuntimeTimeoutMs();
const payload = {
providerId: invocation.providerId,
command: wrapSshRemoteCommand(parsed.remoteCommand, parsed.requiredHelpers),
cwd: sshRoutePayloadCwd(invocation.route),
tty: parsed.remoteCommand === null,
stdinEotOnEnd: parsed.remoteCommand !== null && parsed.requiresStdin !== true,
openTimeoutMs,
runtimeTimeoutMs,
cols: size.cols,
rows: size.rows,
};
const payloadJson = JSON.stringify(payload);
const encodedBrokerSource = Buffer.from(brokerSource(), "utf8").toString("base64");
const script = [
"set -eu",
`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")}`,
'broker_js="$(mktemp "${TMPDIR:-/tmp}/unidesk-ssh-broker.XXXXXX")"',
'trap \'rm -f "$broker_js"\' EXIT',
`printf %s ${shellQuote(encodedBrokerSource)} | base64 -d >"$broker_js"`,
'bun "$broker_js" "$payload"',
].join("\n");
const child = spawn("docker", [
"exec",
"-i",
"unidesk-backend-core",
"sh",
"-c",
script,
], {
cwd: repoRoot,
stdio: ["pipe", "pipe", "pipe"],
});
const rawMode = parsed.remoteCommand === null && process.stdin.isTTY;
if (rawMode) process.stdin.setRawMode(true);
process.stdin.resume();
if (parsed.stdinPrefix !== undefined || parsed.stdinSuffix !== undefined) {
if (parsed.stdinPrefix) child.stdin.write(parsed.stdinPrefix);
process.stdin.pipe(child.stdin, { end: false });
process.stdin.once("end", () => {
if (parsed.stdinSuffix) child.stdin.write(parsed.stdinSuffix);
child.stdin.end();
});
} else {
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<number>((resolve) => {
let settled = false;
let timedOut = false;
let killTimer: NodeJS.Timeout | null = null;
const runtimeTimer = setTimeout(() => {
if (settled) return;
timedOut = true;
const hint = sshRuntimeTimeoutHint({
invocation,
transport: "backend-core-broker",
timeoutMs: runtimeTimeoutMs,
});
const formatted = formatSshRuntimeTimeoutHint(hint);
appendStderrTail(formatted);
process.stderr.write(formatted);
try {
child.stdin.destroy();
} catch {
// Ignore stdin teardown failures on the timeout path.
}
try {
child.kill("SIGTERM");
} catch {
// Ignore kill failures and fall through to finish.
}
killTimer = setTimeout(() => {
try {
child.kill("SIGKILL");
} catch {
// Process may have already exited.
}
}, 2000);
finish(124);
}, runtimeTimeoutMs);
const restore = (): void => {
clearTimeout(runtimeTimer);
if (killTimer && !timedOut) clearTimeout(killTimer);
process.stdin.unpipe(child.stdin);
if (rawMode) process.stdin.setRawMode(false);
};
const finish = (exitCode: number): void => {
if (settled) return;
settled = true;
restore();
const hint = timedOut ? null : sshFailureHint(invocation.providerId, parsed, exitCode, stderrTail);
if (hint !== null) process.stderr.write(formatSshFailureHint(hint));
const timingHint = formatSshRuntimeTimingHint(sshRuntimeTimingHint({
invocation,
transport: "backend-core-broker",
exitCode,
startedAtMs,
}));
if (timingHint) process.stderr.write(timingHint);
resolve(exitCode);
};
child.on("error", (error) => {
const message = `unidesk ssh failed to start broker: ${error.message}\n`;
appendStderrTail(message);
process.stderr.write(message);
finish(255);
});
child.on("close", (code) => {
finish(code ?? 255);
});
});
}