import { randomBytes } from "node:crypto"; import { basename, join, resolve } from "node:path"; import { downloadSshFileVerified, type SshFileTransferCommandBuilders, type SshRemoteCommandExecutor, type SshVerifiedDownloadResult, } from "./ssh-file-transfer"; import type { ParsedSshInvocation, ParsedSshRoute, SshCaptureResult } from "./ssh"; interface SshPlaywrightOptions { localDir: string; remoteDir: string | null; keepRemote: boolean; inactivityTimeoutMs?: number; pollIntervalMs: number; waitTimeoutMs: number; } interface RemotePlaywrightManifest { runId: string; exitCode: number; remoteDir: string; stdoutPath: string; stderrPath: string; cliMode: string | null; cliCwd: string | null; cliBin: string | null; defaultScreenshot: string | null; stdoutTail: string; stderrTail: string; artifacts: Array<{ remotePath: string; bytes: number | null; sha256: string | null; }>; } const manifestBegin = "__UNIDESK_PLAYWRIGHT_MANIFEST_BEGIN__"; const manifestEnd = "__UNIDESK_PLAYWRIGHT_MANIFEST_END__"; export function isSshPlaywrightOperation(args: string[]): boolean { return (args[0] ?? "") === "playwright"; } export async function runSshPlaywrightOperation( invocation: ParsedSshInvocation, args: string[], executor: SshRemoteCommandExecutor, builders: SshFileTransferCommandBuilders, ): Promise { if (invocation.route.plane === "win") { throw new Error(`ssh ${invocation.route.raw} playwright is not supported for Windows routes; use a POSIX host/workspace route`); } if (invocation.route.plane === "k3s" && (invocation.route.namespace === null || invocation.route.resource === null)) { throw new Error(`ssh ${invocation.route.raw} playwright requires a workload route, or use a host workspace route`); } const options = parseSshPlaywrightOptions(args); const userScript = await readAllStdin(); if (userScript.trim().length === 0) { throw new Error("ssh playwright requires a stdin heredoc script; example: trans D601 playwright <<'PW'\nplaywright-cli screenshot https://example.com \"$UNIDESK_PLAYWRIGHT_SCREENSHOT\"\nPW"); } const runId = `unidesk-playwright-${safePathSegment(invocation.providerId)}-${Date.now()}-${randomBytes(4).toString("hex")}`; const remoteDir = options.remoteDir ?? `/tmp/${runId}`; const localDir = resolve(options.localDir); const submitCommand = builders.buildRouteCommand(invocation.route, ["sh", "-c", remotePlaywrightSubmitScript(remoteDir), "unidesk-playwright-submit"], { stdin: true }); const submit = await executor.runRemoteCommand(submitCommand, userScript); const submitRecovery = submit.exitCode !== 0 && isRecoverableSubmitTimeout(submit) ? { recovered: true, reason: "submit-short-connection-timeout", remoteDir, runId, exitCode: submit.exitCode, stdoutTail: submit.stdout.slice(-1000), stderrTail: submit.stderr.slice(-1000), next: "submit may have been cut by the 60s trans runtime limit after the background job was launched; polling remote status by remoteDir/runId", } : null; if (submit.exitCode !== 0 && submitRecovery === null) { throw new Error(`ssh playwright submit failed: exitCode=${submit.exitCode}; remoteDir=${remoteDir}; runId=${runId}; stdoutTail=${JSON.stringify(submit.stdout.slice(-1000))}; stderrTail=${JSON.stringify(submit.stderr.slice(-1000))}`); } const manifest = await pollRemoteManifest(invocation.route, executor, builders, remoteDir, runId, options); const artifacts: Array = []; let downloadFailure: Record | null = null; for (const artifact of manifest.artifacts) { const localPath = join(localDir, `${runId}-${safePathSegment(basename(artifact.remotePath) || "artifact")}`); const maxAttempts = 3; let downloaded = false; let lastError: unknown = null; for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { try { const download = await downloadSshFileVerified( invocation, executor, builders, artifact.remotePath, localPath, options.inactivityTimeoutMs, ); artifacts.push({ ...download, manifestBytes: artifact.bytes, manifestSha256: artifact.sha256 }); downloaded = true; break; } catch (error) { lastError = error; if (attempt < maxAttempts) await sleep(750 * attempt); } } if (!downloaded) { downloadFailure = { remotePath: artifact.remotePath, attempts: maxAttempts, message: lastError instanceof Error ? lastError.message : String(lastError), name: lastError instanceof Error ? lastError.name : undefined, }; break; } } const cleanup = await cleanupRemoteDir(invocation.route, executor, builders, remoteDir, options.keepRemote); const ok = manifest.exitCode === 0 && downloadFailure === null; const payload = { ok, command: "ssh playwright", route: invocation.route.raw, providerId: invocation.providerId, runId, localDir, remote: { exitCode: manifest.exitCode, remoteDir: manifest.remoteDir, stdoutPath: manifest.stdoutPath, stderrPath: manifest.stderrPath, stdoutTail: manifest.stdoutTail, stderrTail: manifest.stderrTail, cliMode: manifest.cliMode, cliCwd: manifest.cliCwd, cliBin: manifest.cliBin, defaultScreenshot: manifest.defaultScreenshot, }, artifacts, artifactCount: artifacts.length, expectedArtifactCount: manifest.artifacts.length, cleanup, downloadFailure, remoteCommand: { exitCode: manifest.exitCode, submitExitCode: submit.exitCode, submitRecovered: submitRecovery !== null, submitRecovery, submitStdoutBytes: Buffer.byteLength(submit.stdout, "utf8"), submitStderrBytes: Buffer.byteLength(submit.stderr, "utf8"), }, }; process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); if (ok) return 0; return manifest.exitCode === 0 ? 1 : manifest.exitCode; } function parseSshPlaywrightOptions(args: string[]): SshPlaywrightOptions { const options: SshPlaywrightOptions = { localDir: "/tmp", remoteDir: null, keepRemote: false, pollIntervalMs: 2_000, waitTimeoutMs: 180_000, }; for (let index = 1; index < args.length; index += 1) { const arg = args[index] ?? ""; const next = args[index + 1]; if (arg === "--help" || arg === "-h" || arg === "help") { process.stdout.write(`${JSON.stringify(playwrightHelp(), null, 2)}\n`); process.exit(0); } if (arg === "--keep-remote") { options.keepRemote = true; continue; } if (arg === "--local-dir") { options.localDir = requireValue(arg, next); index += 1; continue; } if (arg.startsWith("--local-dir=")) { options.localDir = requireValue("--local-dir", arg.slice("--local-dir=".length)); continue; } if (arg === "--remote-dir") { options.remoteDir = requireAbsoluteRemotePath(arg, next); index += 1; continue; } if (arg.startsWith("--remote-dir=")) { options.remoteDir = requireAbsoluteRemotePath("--remote-dir", arg.slice("--remote-dir=".length)); continue; } if (arg === "--inactivity-timeout-ms") { options.inactivityTimeoutMs = parsePositiveInteger(arg, next); index += 1; continue; } if (arg.startsWith("--inactivity-timeout-ms=")) { options.inactivityTimeoutMs = parsePositiveInteger("--inactivity-timeout-ms", arg.slice("--inactivity-timeout-ms=".length)); continue; } if (arg === "--poll-interval-ms") { options.pollIntervalMs = parsePositiveInteger(arg, next); index += 1; continue; } if (arg.startsWith("--poll-interval-ms=")) { options.pollIntervalMs = parsePositiveInteger("--poll-interval-ms", arg.slice("--poll-interval-ms=".length)); continue; } if (arg === "--wait-timeout-ms") { options.waitTimeoutMs = parsePositiveInteger(arg, next); index += 1; continue; } if (arg.startsWith("--wait-timeout-ms=")) { options.waitTimeoutMs = parsePositiveInteger("--wait-timeout-ms", arg.slice("--wait-timeout-ms=".length)); continue; } throw new Error(`unsupported ssh playwright option: ${arg}`); } return options; } function playwrightHelp(): Record { return { ok: true, command: "ssh playwright", usage: [ "trans playwright [--local-dir /tmp] <<'PW'", "playwright-cli screenshot https://example.com \"$UNIDESK_PLAYWRIGHT_SCREENSHOT\" --full-page", "PW", "trans :/absolute/workspace playwright [--local-dir /tmp] <<'PW'", "playwright-cli screenshot http://127.0.0.1:18081/ \"$UNIDESK_PLAYWRIGHT_SCREENSHOT\"", "PW", ], behavior: [ "Reads a POSIX shell heredoc from stdin and runs it on the target route.", "Prepends a temporary playwright-cli wrapper to PATH. The wrapper prefers ./scripts/playwright-cli.ts in the route workspace or known UniDesk host workspaces, then ~/.agents/skills/playwright*/scripts/playwright-cli.ts, then a playwright-cli binary.", "Submits the remote script as a background job and polls short status commands for the manifest, so multi-step Playwright flows do not occupy one SSH connection past the 60s trans runtime limit.", "Sets UNIDESK_PLAYWRIGHT_REMOTE_DIR and UNIDESK_PLAYWRIGHT_SCREENSHOT. Files created under that remote dir with image/pdf extensions are downloaded to --local-dir with byte and sha256 verification.", ], options: { "--local-dir ": "Local directory for returned artifacts. Default: /tmp.", "--remote-dir ": "Remote artifact directory. Default: /tmp/unidesk-playwright---.", "--keep-remote": "Do not remove the remote artifact directory after the transfer.", "--inactivity-timeout-ms ": "Forwarded to verified artifact download when large screenshots are returned.", "--wait-timeout-ms ": "Maximum wall-clock time to wait for the remote Playwright job. Default: 180000.", "--poll-interval-ms ": "Short-query polling interval for the remote job. Default: 2000.", }, }; } function requireValue(name: string, value: string | undefined): string { if (value === undefined || value.length === 0) throw new Error(`ssh playwright ${name} requires a non-empty value`); return value; } function requireAbsoluteRemotePath(name: string, value: string | undefined): string { const pathValue = requireValue(name, value); if (!pathValue.startsWith("/")) throw new Error(`ssh playwright ${name} must be an absolute POSIX path`); return pathValue; } function parsePositiveInteger(name: string, value: string | undefined): number { const parsed = Number(requireValue(name, value)); if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`ssh playwright ${name} must be a positive integer`); return parsed; } async function readAllStdin(): Promise { const chunks: Buffer[] = []; for await (const chunk of process.stdin) { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))); } return Buffer.concat(chunks).toString("utf8"); } async function pollRemoteManifest( route: ParsedSshRoute, executor: SshRemoteCommandExecutor, builders: SshFileTransferCommandBuilders, remoteDir: string, runId: string, options: SshPlaywrightOptions, ): Promise { const deadline = Date.now() + options.waitTimeoutMs; let lastStatus: SshCaptureResult | null = null; while (Date.now() <= deadline) { const command = builders.buildRouteCommand(route, ["sh", "-c", remotePlaywrightStatusScript(), "unidesk-playwright-status", remoteDir]); const status = await executor.runRemoteCommand(command); lastStatus = status; if (status.exitCode === 0 && status.stdout.includes(manifestEnd)) return parseRemoteManifest(status, remoteDir, runId); if (status.exitCode !== 0 && !/status\t(?:pending|running)/u.test(status.stdout)) { throw new Error(`ssh playwright status failed: exitCode=${status.exitCode}; remoteDir=${remoteDir}; runId=${runId}; stdoutTail=${JSON.stringify(status.stdout.slice(-1000))}; stderrTail=${JSON.stringify(status.stderr.slice(-1000))}`); } await sleep(options.pollIntervalMs); } throw new Error(`ssh playwright timed out waiting for remote job after ${options.waitTimeoutMs}ms; remoteDir=${remoteDir}; runId=${runId}; lastStdoutTail=${JSON.stringify(lastStatus?.stdout.slice(-1000) ?? "")}; lastStderrTail=${JSON.stringify(lastStatus?.stderr.slice(-1000) ?? "")}`); } function sleep(ms: number): Promise { return new Promise((resolveSleep) => setTimeout(resolveSleep, ms)); } function isRecoverableSubmitTimeout(submit: SshCaptureResult): boolean { if (submit.exitCode === 124) return true; const combined = `${submit.stdout}\n${submit.stderr}`; return /(?:timed?\s*out|timeout|60s|exitCode=124|signal\s+TERM|signal\s+KILL)/iu.test(combined); } function remotePlaywrightSubmitScript(remoteDir: string): string { const runner = Buffer.from(remotePlaywrightRunnerScript(remoteDir), "utf8").toString("base64"); return [ "set -eu", `remote_dir=${shellQuote(remoteDir)}`, 'mkdir -p -- "$remote_dir"', 'user_script="$remote_dir/user-script.sh"', 'runner_script="$remote_dir/runner.sh"', 'pid_file="$remote_dir/pid"', 'submit_stdout="$remote_dir/submit.out"', 'submit_stderr="$remote_dir/submit.err"', 'if [ -f "$pid_file" ] && kill -0 "$(cat "$pid_file")" >/dev/null 2>&1; then', ' printf "status\\trunning\\nremote_dir\\t%s\\npid\\t%s\\n" "$remote_dir" "$(cat "$pid_file")"', " exit 0", "fi", 'cat > "$user_script"', 'chmod 700 "$user_script"', `printf %s ${shellQuote(runner)} | base64 -d >"$runner_script"`, 'chmod 700 "$runner_script"', 'nohup sh "$runner_script" "$user_script" >"$submit_stdout" 2>"$submit_stderr" < /dev/null &', 'pid=$!', 'printf "%s\\n" "$pid" >"$pid_file"', 'printf "status\\tsubmitted\\nremote_dir\\t%s\\npid\\t%s\\n" "$remote_dir" "$pid"', ].join("\n"); } function remotePlaywrightStatusScript(): string { return [ "set -eu", 'remote_dir="$1"', 'manifest="$remote_dir/manifest.tsv"', 'pid_file="$remote_dir/pid"', 'if [ -f "$manifest" ]; then cat "$manifest"; exit 0; fi', 'if [ -f "$pid_file" ] && kill -0 "$(cat "$pid_file")" >/dev/null 2>&1; then', ' printf "status\\trunning\\nremote_dir\\t%s\\npid\\t%s\\n" "$remote_dir" "$(cat "$pid_file")"', " exit 1", "fi", 'if [ -f "$pid_file" ]; then', ' printf "status\\texited-without-manifest\\nremote_dir\\t%s\\npid\\t%s\\n" "$remote_dir" "$(cat "$pid_file")"', ' if [ -f "$remote_dir/submit.err" ]; then tail -c 1000 "$remote_dir/submit.err" >&2; fi', ' if [ -f "$remote_dir/stderr.log" ]; then tail -c 1000 "$remote_dir/stderr.log" >&2; fi', " exit 2", "fi", 'printf "status\\tpending\\nremote_dir\\t%s\\n" "$remote_dir"', 'if [ -f "$remote_dir/submit.err" ]; then tail -c 1000 "$remote_dir/submit.err" >&2; fi', 'if [ -f "$remote_dir/stderr.log" ]; then tail -c 1000 "$remote_dir/stderr.log" >&2; fi', "exit 1", ].join("\n"); } function remotePlaywrightRunnerScript(remoteDir: string): string { return [ "set -eu", `remote_dir=${shellQuote(remoteDir)}`, 'if [ "$#" -lt 1 ]; then printf "missing user script path\\n" >&2; exit 2; fi', 'user_script="$1"', 'mkdir -p -- "$remote_dir/bin"', 'stdout_file="$remote_dir/stdout.log"', 'stderr_file="$remote_dir/stderr.log"', 'artifacts_file="$remote_dir/artifacts.tsv"', 'manifest_file="$remote_dir/manifest.tsv"', "sha256_file() {", " if command -v sha256sum >/dev/null 2>&1; then sha256sum -- \"$1\" | awk '{print $1}'; return; fi", " if command -v shasum >/dev/null 2>&1; then shasum -a 256 -- \"$1\" | awk '{print $1}'; return; fi", " if command -v openssl >/dev/null 2>&1; then openssl dgst -sha256 -- \"$1\" | awk '{print $NF}'; return; fi", " printf 'missing sha256 tool\\n' >&2; return 127", "}", "resolve_playwright_cli() {", " if command -v bun >/dev/null 2>&1; then", " if [ -f ./scripts/playwright-cli.ts ]; then", " UNIDESK_PLAYWRIGHT_CLI_MODE=repo; UNIDESK_PLAYWRIGHT_CLI_CWD=$(pwd); UNIDESK_PLAYWRIGHT_CLI_BIN=; return 0", " fi", " for dir in \"$HOME/workspace/unidesk-dev\" \"$HOME/unidesk\" \"/home/ubuntu/workspace/unidesk-dev\" \"/root/unidesk\"; do", " if [ -f \"$dir/scripts/playwright-cli.ts\" ]; then", " UNIDESK_PLAYWRIGHT_CLI_MODE=repo; UNIDESK_PLAYWRIGHT_CLI_CWD=$dir; UNIDESK_PLAYWRIGHT_CLI_BIN=; return 0", " fi", " done", " fi", " for dir in \"$HOME/.agents/skills/playwright\" \"$HOME/.agents/skills/playwright-cli\" \"$HOME/.codex/skills/playwright\" \"$HOME/.codex/skills/playwright-cli\"; do", " if command -v bun >/dev/null 2>&1 && [ -f \"$dir/scripts/playwright-cli.ts\" ]; then", " UNIDESK_PLAYWRIGHT_CLI_MODE=skill; UNIDESK_PLAYWRIGHT_CLI_CWD=$dir; UNIDESK_PLAYWRIGHT_CLI_BIN=; return 0", " fi", " done", " if command -v playwright-cli >/dev/null 2>&1; then", " UNIDESK_PLAYWRIGHT_CLI_MODE=bin; UNIDESK_PLAYWRIGHT_CLI_CWD=; UNIDESK_PLAYWRIGHT_CLI_BIN=$(command -v playwright-cli); return 0", " fi", " return 42", "}", "if ! resolve_playwright_cli; then", " printf 'unable to find playwright-cli wrapper: expected ./scripts/playwright-cli.ts, ~/.agents/skills/playwright*/scripts/playwright-cli.ts, or playwright-cli in PATH\\n' >&2", " exit 42", "fi", "export UNIDESK_PLAYWRIGHT_CLI_MODE UNIDESK_PLAYWRIGHT_CLI_CWD UNIDESK_PLAYWRIGHT_CLI_BIN", 'cat > "$remote_dir/bin/playwright-cli" <<\'UNIDESK_PLAYWRIGHT_WRAPPER\'', "#!/bin/sh", "set -eu", "case \"${UNIDESK_PLAYWRIGHT_CLI_MODE:-}\" in", " repo|skill) cd \"$UNIDESK_PLAYWRIGHT_CLI_CWD\"; exec bun scripts/playwright-cli.ts \"$@\" ;;", " bin) exec \"$UNIDESK_PLAYWRIGHT_CLI_BIN\" \"$@\" ;;", " *) printf 'UNIDESK playwright wrapper is not resolved\\n' >&2; exit 42 ;;", "esac", "UNIDESK_PLAYWRIGHT_WRAPPER", 'chmod 700 "$remote_dir/bin/playwright-cli"', "if command -v npx >/dev/null 2>&1; then", " cat > \"$remote_dir/bin/npx.cmd\" <<'UNIDESK_NPX_CMD_WRAPPER'", "#!/bin/sh", "exec npx \"$@\"", "UNIDESK_NPX_CMD_WRAPPER", " chmod 700 \"$remote_dir/bin/npx.cmd\"", "fi", 'export PATH="$remote_dir/bin:$PATH"', 'export UNIDESK_PLAYWRIGHT_REMOTE_DIR="$remote_dir"', 'export UNIDESK_PLAYWRIGHT_SCREENSHOT="$remote_dir/screenshot.png"', "set +e", 'sh "$user_script" >"$stdout_file" 2>"$stderr_file"', "user_rc=$?", ': > "$artifacts_file"', 'find "$remote_dir" -type f | while IFS= read -r file; do', ' case "$file" in "$user_script"|"$stdout_file"|"$stderr_file"|"$artifacts_file"|"$remote_dir/bin/"*) continue ;; esac', ' case "$file" in *.png|*.jpg|*.jpeg|*.webp|*.pdf)', ' bytes=$(wc -c < "$file" | tr -d "[:space:]")', ' digest=$(sha256_file "$file")', ' printf "artifact\\t%s\\t%s\\t%s\\n" "$bytes" "$digest" "$file" >> "$artifacts_file"', " ;;", " esac", "done", "b64_tail() { if [ -f \"$1\" ]; then tail -c 4000 \"$1\" | base64 | tr -d '\\n'; fi; }", `{`, `printf '%s\\n' ${shellQuote(manifestBegin)}`, "printf 'run_id\\t%s\\n' \"${remote_dir##*/}\"", "printf 'exit_code\\t%s\\n' \"$user_rc\"", "printf 'remote_dir\\t%s\\n' \"$remote_dir\"", "printf 'stdout_path\\t%s\\n' \"$stdout_file\"", "printf 'stderr_path\\t%s\\n' \"$stderr_file\"", "printf 'cli_mode\\t%s\\n' \"$UNIDESK_PLAYWRIGHT_CLI_MODE\"", "printf 'cli_cwd\\t%s\\n' \"$UNIDESK_PLAYWRIGHT_CLI_CWD\"", "printf 'cli_bin\\t%s\\n' \"$UNIDESK_PLAYWRIGHT_CLI_BIN\"", "printf 'default_screenshot\\t%s\\n' \"$UNIDESK_PLAYWRIGHT_SCREENSHOT\"", "printf 'stdout_tail_b64\\t%s\\n' \"$(b64_tail \"$stdout_file\")\"", "printf 'stderr_tail_b64\\t%s\\n' \"$(b64_tail \"$stderr_file\")\"", 'cat "$artifacts_file"', `printf '%s\\n' ${shellQuote(manifestEnd)}`, `} > "$manifest_file"`, 'exit "$user_rc"', ].join("\n"); } function parseRemoteManifest(result: SshCaptureResult, fallbackRemoteDir: string, fallbackRunId: string): RemotePlaywrightManifest { const start = result.stdout.indexOf(manifestBegin); const end = result.stdout.indexOf(manifestEnd, start < 0 ? 0 : start); if (start < 0 || end < 0) { throw new Error(`ssh playwright did not emit a manifest; exitCode=${result.exitCode}; stdoutTail=${JSON.stringify(result.stdout.slice(-1000))}; stderrTail=${JSON.stringify(result.stderr.slice(-1000))}`); } const body = result.stdout.slice(start + manifestBegin.length, end).trim(); const manifest: RemotePlaywrightManifest = { runId: fallbackRunId, exitCode: result.exitCode, remoteDir: fallbackRemoteDir, stdoutPath: join(fallbackRemoteDir, "stdout.log"), stderrPath: join(fallbackRemoteDir, "stderr.log"), cliMode: null, cliCwd: null, cliBin: null, defaultScreenshot: null, stdoutTail: "", stderrTail: "", artifacts: [], }; for (const rawLine of body.split(/\r?\n/u)) { const line = rawLine.trimEnd(); if (line.length === 0) continue; const parts = line.split("\t"); const key = parts[0] ?? ""; if (key === "artifact") { const bytes = Number(parts[1] ?? ""); const sha256 = parts[2] ?? ""; const remotePath = parts.slice(3).join("\t"); if (remotePath.length > 0) { manifest.artifacts.push({ remotePath, bytes: Number.isSafeInteger(bytes) && bytes >= 0 ? bytes : null, sha256: /^[0-9a-f]{64}$/u.test(sha256) ? sha256 : null, }); } continue; } const value = parts.slice(1).join("\t"); if (key === "run_id" && value.length > 0) manifest.runId = value; else if (key === "exit_code") { const exitCode = Number(value); manifest.exitCode = Number.isInteger(exitCode) ? exitCode : result.exitCode; } else if (key === "remote_dir" && value.length > 0) manifest.remoteDir = value; else if (key === "stdout_path" && value.length > 0) manifest.stdoutPath = value; else if (key === "stderr_path" && value.length > 0) manifest.stderrPath = value; else if (key === "cli_mode") manifest.cliMode = value || null; else if (key === "cli_cwd") manifest.cliCwd = value || null; else if (key === "cli_bin") manifest.cliBin = value || null; else if (key === "default_screenshot") manifest.defaultScreenshot = value || null; else if (key === "stdout_tail_b64") manifest.stdoutTail = decodeBase64(value); else if (key === "stderr_tail_b64") manifest.stderrTail = decodeBase64(value); } return manifest; } async function cleanupRemoteDir( route: ParsedSshRoute, executor: SshRemoteCommandExecutor, builders: SshFileTransferCommandBuilders, remoteDir: string, keepRemote: boolean, ): Promise> { if (keepRemote) return { attempted: false, kept: true, remoteDir }; const command = builders.buildRouteCommand(route, ["sh", "-c", "rm -rf -- \"$1\"", "unidesk-playwright-cleanup", remoteDir]); const result = await executor.runRemoteCommand(command); return { attempted: true, kept: false, remoteDir, exitCode: result.exitCode, ok: result.exitCode === 0, stderrTail: result.stderr.slice(-1000), }; } function decodeBase64(value: string): string { if (value.length === 0) return ""; try { return Buffer.from(value, "base64").toString("utf8"); } catch { return ""; } } function sleep(ms: number): Promise { return new Promise((resolveSleep) => setTimeout(resolveSleep, ms)); } function safePathSegment(value: string): string { const cleaned = value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, ""); return cleaned.length > 0 ? cleaned.slice(0, 120) : "artifact"; } function shellQuote(value: string): string { return `'${value.replace(/'/g, "'\\''")}'`; }