feat: add trans playwright passthrough
This commit is contained in:
@@ -255,7 +255,7 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文
|
|||||||
- `bun scripts/cli.ts codex judge <taskId> --attempt <n> [--dry-run]`:按指定 task/attempt 用与队列 worker 相同的上下文构建和 MiniMax judge 调用路径单步复现完成判定;`--dry-run` 只输出 prompt/payload 诊断。
|
- `bun scripts/cli.ts codex judge <taskId> --attempt <n> [--dry-run]`:按指定 task/attempt 用与队列 worker 相同的上下文构建和 MiniMax judge 调用路径单步复现完成判定;`--dry-run` 只输出 prompt/payload 诊断。
|
||||||
- `bun scripts/cli.ts codex steer <taskId> [prompt|--prompt-file path|--prompt-stdin] [--steer-id id] [--dry-run] [--no-retry|--retry-attempts N]` / `codex steer-confirm <taskId> --steer-id <id>`:向运行中的 active turn 注入纠偏提示并用 `steerId` 做幂等/trace 确认;真实输出不回显 prompt,遇到 `deliveryUnconfirmed` 先查确认命令,不重复发送同一纠偏。
|
- `bun scripts/cli.ts codex steer <taskId> [prompt|--prompt-file path|--prompt-stdin] [--steer-id id] [--dry-run] [--no-retry|--retry-attempts N]` / `codex steer-confirm <taskId> --steer-id <id>`:向运行中的 active turn 注入纠偏提示并用 `steerId` 做幂等/trace 确认;真实输出不回显 prompt,遇到 `deliveryUnconfirmed` 先查确认命令,不重复发送同一纠偏。
|
||||||
- `bun scripts/cli.ts codex interrupt|cancel <taskId>`:通过 Code Queue 私有代理中断运行任务或取消 queued/retry_wait 任务,规则见 `docs/reference/cli.md`。
|
- `bun scripts/cli.ts codex interrupt|cancel <taskId>`:通过 Code Queue 私有代理中断运行任务或取消 queued/retry_wait 任务,规则见 `docs/reference/cli.md`。
|
||||||
- `bun scripts/playwright-cli.ts screenshot|open|eval ...`:UniDesk 仓库自带的 Playwright 指挥手测 wrapper,默认 headless,可用 `--session <id>` 复用 storageState,适合截图、打开页面和一次性 JS 取值;它不实现长驻浏览器 daemon、element ref `click/fill/snapshot` 会返回结构化 unsupported 和 `xvfb-run`/headless 下一步,规则见 `docs/reference/cli.md`。
|
- `bun scripts/playwright-cli.ts screenshot|open|eval ...` / `trans D601 playwright <<'PW' ...`:UniDesk 仓库自带的 Playwright 指挥手测 wrapper 与跨 host 透传入口,默认 headless,支持 heredoc 执行并把截图等产物回传到本机 `/tmp`;不实现长驻浏览器 daemon,规则见 `docs/reference/cli.md`。
|
||||||
- `bun scripts/cli.ts server stop`:以异步 job 停止固定 Compose 项目中的全部 UniDesk 服务,停止后用 `server status` 复核。
|
- `bun scripts/cli.ts server stop`:以异步 job 停止固定 Compose 项目中的全部 UniDesk 服务,停止后用 `server status` 复核。
|
||||||
- `bun scripts/cli.ts job list [--limit N]` / `bun scripts/cli.ts job status latest [--tail-bytes N]`:分页查询 `.state/jobs/` 中的异步任务状态,状态输出只读日志尾部并保留完整日志路径,job 机制见 `docs/reference/cli.md`。
|
- `bun scripts/cli.ts job list [--limit N]` / `bun scripts/cli.ts job status latest [--tail-bytes N]`:分页查询 `.state/jobs/` 中的异步任务状态,状态输出只读日志尾部并保留完整日志路径,job 机制见 `docs/reference/cli.md`。
|
||||||
- `bun scripts/cli.ts debug health` / `bun scripts/cli.ts debug dispatch` / `bun scripts/cli.ts debug task`:通过 Docker 内网 core、真实 HTTP、WebSocket、系统指标、Docker 状态和 Host SSH 维护桥流程调试健康检查、任务下发与任务结果,调试规则见 `docs/reference/cli.md`。
|
- `bun scripts/cli.ts debug health` / `bun scripts/cli.ts debug dispatch` / `bun scripts/cli.ts debug task`:通过 Docker 内网 core、真实 HTTP、WebSocket、系统指标、Docker 状态和 Host SSH 维护桥流程调试健康检查、任务下发与任务结果,调试规则见 `docs/reference/cli.md`。
|
||||||
|
|||||||
@@ -136,6 +136,20 @@ UniDesk 仓库自带 `scripts/playwright-cli.ts` 作为 host commander 浏览器
|
|||||||
|
|
||||||
外部 agent skill `~/.agents/skills/playwright` 是另一个 source of truth。当前宿主上它可能仍是 `npx playwright` passthrough,但 `SKILL.md` 里描述了更丰富的 `--session`、`snapshot` 和 element-ref 操作。外部 skill 分发更新前,UniDesk/HWLAB 指挥手测应使用本仓库 wrapper;不要把外部 skill 文档当成 daemon/session 能力已经可用的证据。
|
外部 agent skill `~/.agents/skills/playwright` 是另一个 source of truth。当前宿主上它可能仍是 `npx playwright` passthrough,但 `SKILL.md` 里描述了更丰富的 `--session`、`snapshot` 和 element-ref 操作。外部 skill 分发更新前,UniDesk/HWLAB 指挥手测应使用本仓库 wrapper;不要把外部 skill 文档当成 daemon/session 能力已经可用的证据。
|
||||||
|
|
||||||
|
### Playwright Trans Passthrough
|
||||||
|
|
||||||
|
跨 host 浏览器验收优先使用 `trans <route> playwright`,标准形态是不带 workspace 的 host route,例如:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
trans D601 playwright --local-dir /tmp <<'PW'
|
||||||
|
playwright-cli screenshot https://example.com "$UNIDESK_PLAYWRIGHT_SCREENSHOT" --full-page
|
||||||
|
PW
|
||||||
|
```
|
||||||
|
|
||||||
|
`playwright` operation 读取 stdin heredoc,在目标 POSIX host/workload 上临时注入 `playwright-cli` wrapper 到 `PATH`。wrapper 优先使用远端用户的 `~/.agents/skills/playwright*/scripts/playwright-cli.ts`,其次使用 route workspace 中的 `./scripts/playwright-cli.ts`,最后才使用远端 `PATH` 中的 `playwright-cli`。命令会设置 `UNIDESK_PLAYWRIGHT_REMOTE_DIR` 和 `UNIDESK_PLAYWRIGHT_SCREENSHOT`,把远端 run 目录中的 `png/jpg/jpeg/webp/pdf` 产物回传到本机 `--local-dir`,默认 `/tmp`,并返回本地路径、远端路径、字节数、SHA-256、stdout/stderr tail 和 transfer verification。
|
||||||
|
|
||||||
|
该入口只负责短生命周期 Playwright 执行和产物回传,不提供长驻浏览器 daemon。需要多步交互时,把步骤写在同一个 heredoc 内;需要保留远端证据时显式加 `--keep-remote`。
|
||||||
|
|
||||||
## Async Job State
|
## Async Job State
|
||||||
|
|
||||||
长时操作采用 Fire-and-Forget 模式:CLI 创建 `.state/jobs/{jobId}.json`,后台进程执行真实命令,并将 stdout、stderr 分别写入 `.state/jobs/{jobId}.stdout.log` 与 `.state/jobs/{jobId}.stderr.log`。调用者通过 `bun scripts/cli.ts job status <jobId>` 查询进度和尾部输出。
|
长时操作采用 Fire-and-Forget 模式:CLI 创建 `.state/jobs/{jobId}.json`,后台进程执行真实命令,并将 stdout、stderr 分别写入 `.state/jobs/{jobId}.stdout.log` 与 `.state/jobs/{jobId}.stderr.log`。调用者通过 `bun scripts/cli.ts job status <jobId>` 查询进度和尾部输出。
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ export function sshHelp(): unknown {
|
|||||||
"trans <providerId>:/absolute/workspace apply-patch < patch.diff",
|
"trans <providerId>:/absolute/workspace apply-patch < patch.diff",
|
||||||
"trans <route> upload <local-file> <remote-file>",
|
"trans <route> upload <local-file> <remote-file>",
|
||||||
"trans <route> download <remote-file> <local-file>",
|
"trans <route> download <remote-file> <local-file>",
|
||||||
|
"trans <providerId> playwright [--local-dir /tmp] <<'PW'",
|
||||||
"trans <providerId> apply-patch-v1 [--allow-loose] < patch.diff",
|
"trans <providerId> apply-patch-v1 [--allow-loose] < patch.diff",
|
||||||
"trans <providerId> py [script-args...] < script.py",
|
"trans <providerId> py [script-args...] < script.py",
|
||||||
"trans <providerId> script [--shell sh|bash] [script-args...] <<'SCRIPT'",
|
"trans <providerId> script [--shell sh|bash] [script-args...] <<'SCRIPT'",
|
||||||
@@ -209,6 +210,7 @@ export function sshHelp(): unknown {
|
|||||||
"For arbitrary stdin streams into a workload command, use a workload route plus `exec --stdin -- <command> ...`; this keeps the route as location-only and avoids heredoc/base64/tar shell wrapping.",
|
"For arbitrary stdin streams into a workload command, use a workload route plus `exec --stdin -- <command> ...`; this keeps the route as location-only and avoids heredoc/base64/tar shell wrapping.",
|
||||||
"`apply-patch` is the default remote text patch entry and uses the v2 local line-based patch engine with remote read/write operations, including Windows routes such as `D601:win/c/test`, so long Unicode/Chinese lines and pure insertion hunks avoid the legacy remote shell hunk parser. Plain multi-file Update File patches on POSIX host/k3s and Windows workspace routes use bulk read/write operations to avoid per-file SSH round trips. Its stdout follows Codex apply_patch text output rather than UniDesk JSON output; stderr keeps Codex-style failure text and appends one `UNIDESK_APPLY_PATCH_TIMING` JSON summary with durationMs, patchBytes, fileCount, hunkCount, changedCount, remoteOperationCount, remoteOperationCounts and remoteElapsedMs so slow patch runs can be attributed without changing success stdout.",
|
"`apply-patch` is the default remote text patch entry and uses the v2 local line-based patch engine with remote read/write operations, including Windows routes such as `D601:win/c/test`, so long Unicode/Chinese lines and pure insertion hunks avoid the legacy remote shell hunk parser. Plain multi-file Update File patches on POSIX host/k3s and Windows workspace routes use bulk read/write operations to avoid per-file SSH round trips. Its stdout follows Codex apply_patch text output rather than UniDesk JSON output; stderr keeps Codex-style failure text and appends one `UNIDESK_APPLY_PATCH_TIMING` JSON summary with durationMs, patchBytes, fileCount, hunkCount, changedCount, remoteOperationCount, remoteOperationCounts and remoteElapsedMs so slow patch runs can be attributed without changing success stdout.",
|
||||||
"`upload` and `download` are the default whole-file transfer entries for non-text and generated files. They write through remote temp files, verify byte count and SHA-256 on both sides, and return `verification.automatic=true`, `verification.verified=true`, and `verification.match.{bytes,sha256}=true`; this JSON is the transfer integrity proof, so callers do not need a separate manual `sha256sum` check. Downloads stream over `host.ssh.tcp-pool`, emit progress JSON, and may receive a caller-supplied `--inactivity-timeout-ms` from async artifact/deploy jobs so active large transfers are not killed by the generic short-command budget.",
|
"`upload` and `download` are the default whole-file transfer entries for non-text and generated files. They write through remote temp files, verify byte count and SHA-256 on both sides, and return `verification.automatic=true`, `verification.verified=true`, and `verification.match.{bytes,sha256}=true`; this JSON is the transfer integrity proof, so callers do not need a separate manual `sha256sum` check. Downloads stream over `host.ssh.tcp-pool`, emit progress JSON, and may receive a caller-supplied `--inactivity-timeout-ms` from async artifact/deploy jobs so active large transfers are not killed by the generic short-command budget.",
|
||||||
|
"`playwright` runs a stdin heredoc on the target POSIX host/workload with a temporary `playwright-cli` wrapper in PATH, sets `UNIDESK_PLAYWRIGHT_SCREENSHOT` under a remote `/tmp` run directory, then downloads image/pdf artifacts to local `/tmp` by default with the same verified SHA-256 transfer path. Canonical syntax is `trans D601 playwright <<'PW' ... PW`; workspace routes are optional and only affect wrapper discovery.",
|
||||||
"`apply-patch-v1` is the only legacy fallback entry: it rejects low-context update hunks by default, reports the matched file:line for each hunk on stderr, and only accepts --allow-loose when the caller has manually reviewed an intentionally ambiguous insertion.",
|
"`apply-patch-v1` is the only legacy fallback entry: it rejects low-context update hunks by default, reports the matched file:line for each hunk on stderr, and only accepts --allow-loose when the caller has manually reviewed an intentionally ambiguous insertion.",
|
||||||
"script defaults to target /bin/sh and inherits provider proxy variables such as HTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXY; it is for host/k3s POSIX shell only. Use --shell bash only for bash syntax such as pipefail, arrays, or [[ ... ]], not as a proxy workaround.",
|
"script defaults to target /bin/sh and inherits provider proxy variables such as HTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXY; it is for host/k3s POSIX shell only. Use --shell bash only for bash syntax such as pipefail, arrays, or [[ ... ]], not as a proxy workaround.",
|
||||||
"Route syntax is `{provider}:{plane}[:{scope...}] {operation} [operation-args...]`: the first argv token locates a distributed target only, and every following token belongs to the operation parser. Host workspace routes use `<provider>:/absolute/workspace`; WSL providers can use `<provider>:win ps` for Windows PowerShell and `<provider>:win cmd` for Windows cmd.exe, with `<provider>:win/c/test ...` mapping the Windows cwd to `C:\\test`; native k3s providers such as D601 and G14 use `<provider>:k3s` for the control plane and `<provider>:k3s:<namespace>:<workload>[:<container>]` for a workload/container. In k3s routes, `:` is the distributed route separator; `/...` is only an in-container filesystem cwd and never selects a container. Prefer operation `--cwd /path` when a container is also specified.",
|
"Route syntax is `{provider}:{plane}[:{scope...}] {operation} [operation-args...]`: the first argv token locates a distributed target only, and every following token belongs to the operation parser. Host workspace routes use `<provider>:/absolute/workspace`; WSL providers can use `<provider>:win ps` for Windows PowerShell and `<provider>:win cmd` for Windows cmd.exe, with `<provider>:win/c/test ...` mapping the Windows cwd to `C:\\test`; native k3s providers such as D601 and G14 use `<provider>:k3s` for the control plane and `<provider>:k3s:<namespace>:<workload>[:<container>]` for a workload/container. In k3s routes, `:` is the distributed route separator; `/...` is only an in-container filesystem cwd and never selects a container. Prefer operation `--cwd /path` when a container is also specified.",
|
||||||
|
|||||||
@@ -66,6 +66,23 @@ interface SshFileTransferDownloadResult {
|
|||||||
remainingBytes: number;
|
remainingBytes: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SshVerifiedDownloadResult {
|
||||||
|
remotePath: string;
|
||||||
|
localPath: string;
|
||||||
|
bytes: number;
|
||||||
|
sha256: string;
|
||||||
|
verified: true;
|
||||||
|
verification: Record<string, unknown>;
|
||||||
|
transfer: {
|
||||||
|
strategy: SshFileTransferDownloadResult["strategy"];
|
||||||
|
transport: SshFileTransferDownloadResult["transport"];
|
||||||
|
chunks: number;
|
||||||
|
elapsedMs: number;
|
||||||
|
throughputBytesPerSecond: number;
|
||||||
|
remainingBytes: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
class SshFileTransferError extends Error {
|
class SshFileTransferError extends Error {
|
||||||
constructor(message: string, public readonly details: Record<string, unknown> = {}) {
|
constructor(message: string, public readonly details: Record<string, unknown> = {}) {
|
||||||
super(message);
|
super(message);
|
||||||
@@ -117,11 +134,7 @@ export async function runSshFileTransferOperation(
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const read = await downloadRemoteFileVerified(invocation, executor, builders, options.remotePath, localPath, options.inactivityTimeoutMs);
|
const download = await downloadSshFileVerified(invocation, executor, builders, options.remotePath, localPath, options.inactivityTimeoutMs);
|
||||||
const verification = buildTransferVerification(
|
|
||||||
{ side: "remote", path: options.remotePath, ...read.remote },
|
|
||||||
{ side: "local", path: localPath, ...read.local },
|
|
||||||
);
|
|
||||||
process.stdout.write(`${JSON.stringify({
|
process.stdout.write(`${JSON.stringify({
|
||||||
ok: true,
|
ok: true,
|
||||||
command: "ssh download",
|
command: "ssh download",
|
||||||
@@ -129,6 +142,31 @@ export async function runSshFileTransferOperation(
|
|||||||
providerId: invocation.providerId,
|
providerId: invocation.providerId,
|
||||||
remotePath: options.remotePath,
|
remotePath: options.remotePath,
|
||||||
localPath,
|
localPath,
|
||||||
|
bytes: download.bytes,
|
||||||
|
sha256: download.sha256,
|
||||||
|
verified: true,
|
||||||
|
verification: download.verification,
|
||||||
|
transfer: download.transfer,
|
||||||
|
}, null, 2)}\n`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadSshFileVerified(
|
||||||
|
invocation: ParsedSshInvocation,
|
||||||
|
executor: SshRemoteCommandExecutor,
|
||||||
|
builders: SshFileTransferCommandBuilders,
|
||||||
|
remotePath: string,
|
||||||
|
localPath: string,
|
||||||
|
inactivityTimeoutMs?: number,
|
||||||
|
): Promise<SshVerifiedDownloadResult> {
|
||||||
|
const read = await downloadRemoteFileVerified(invocation, executor, builders, remotePath, localPath, inactivityTimeoutMs);
|
||||||
|
const verification = buildTransferVerification(
|
||||||
|
{ side: "remote", path: remotePath, ...read.remote },
|
||||||
|
{ side: "local", path: localPath, ...read.local },
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
remotePath,
|
||||||
|
localPath,
|
||||||
bytes: read.remote.bytes,
|
bytes: read.remote.bytes,
|
||||||
sha256: read.remote.sha256,
|
sha256: read.remote.sha256,
|
||||||
verified: true,
|
verified: true,
|
||||||
@@ -141,8 +179,7 @@ export async function runSshFileTransferOperation(
|
|||||||
throughputBytesPerSecond: read.throughputBytesPerSecond,
|
throughputBytesPerSecond: read.throughputBytesPerSecond,
|
||||||
remainingBytes: read.remainingBytes,
|
remainingBytes: read.remainingBytes,
|
||||||
},
|
},
|
||||||
}, null, 2)}\n`);
|
};
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseSshFileTransferCliOptions(args: string[]): SshFileTransferCliOptions {
|
function parseSshFileTransferCliOptions(args: string[]): SshFileTransferCliOptions {
|
||||||
|
|||||||
@@ -0,0 +1,412 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<number> {
|
||||||
|
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 remoteCommand = builders.buildRouteCommand(invocation.route, ["sh", "-c", remotePlaywrightRunnerScript(remoteDir), "unidesk-playwright"], { stdin: true });
|
||||||
|
const result = await executor.runRemoteCommand(remoteCommand, userScript);
|
||||||
|
const manifest = parseRemoteManifest(result, remoteDir, runId);
|
||||||
|
const artifacts: Array<SshVerifiedDownloadResult & { manifestBytes: number | null; manifestSha256: string | null }> = [];
|
||||||
|
let downloadFailure: Record<string, unknown> | null = null;
|
||||||
|
|
||||||
|
for (const artifact of manifest.artifacts) {
|
||||||
|
const localPath = join(localDir, `${runId}-${safePathSegment(basename(artifact.remotePath) || "artifact")}`);
|
||||||
|
try {
|
||||||
|
const download = await downloadSshFileVerified(
|
||||||
|
invocation,
|
||||||
|
executor,
|
||||||
|
builders,
|
||||||
|
artifact.remotePath,
|
||||||
|
localPath,
|
||||||
|
options.inactivityTimeoutMs,
|
||||||
|
);
|
||||||
|
artifacts.push({ ...download, manifestBytes: artifact.bytes, manifestSha256: artifact.sha256 });
|
||||||
|
} catch (error) {
|
||||||
|
downloadFailure = {
|
||||||
|
remotePath: artifact.remotePath,
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
name: error instanceof Error ? error.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: result.exitCode,
|
||||||
|
stdoutBytes: Buffer.byteLength(result.stdout, "utf8"),
|
||||||
|
stderrBytes: Buffer.byteLength(result.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,
|
||||||
|
};
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
throw new Error(`unsupported ssh playwright option: ${arg}`);
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
function playwrightHelp(): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
command: "ssh playwright",
|
||||||
|
usage: [
|
||||||
|
"trans <providerId> playwright [--local-dir /tmp] <<'PW'",
|
||||||
|
"playwright-cli screenshot https://example.com \"$UNIDESK_PLAYWRIGHT_SCREENSHOT\" --full-page",
|
||||||
|
"PW",
|
||||||
|
"trans <providerId>:/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 ~/.agents/skills/playwright*/scripts/playwright-cli.ts, then ./scripts/playwright-cli.ts in the route workspace, then a playwright-cli binary.",
|
||||||
|
"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 <path>": "Local directory for returned artifacts. Default: /tmp.",
|
||||||
|
"--remote-dir <absolute-path>": "Remote artifact directory. Default: /tmp/unidesk-playwright-<provider>-<timestamp>-<id>.",
|
||||||
|
"--keep-remote": "Do not remove the remote artifact directory after the transfer.",
|
||||||
|
"--inactivity-timeout-ms <n>": "Forwarded to verified artifact download when large screenshots are returned.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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<string> {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
function remotePlaywrightRunnerScript(remoteDir: string): string {
|
||||||
|
return [
|
||||||
|
"set -eu",
|
||||||
|
`remote_dir=${shellQuote(remoteDir)}`,
|
||||||
|
'mkdir -p -- "$remote_dir/bin"',
|
||||||
|
'user_script="$remote_dir/user-script.sh"',
|
||||||
|
'stdout_file="$remote_dir/stdout.log"',
|
||||||
|
'stderr_file="$remote_dir/stderr.log"',
|
||||||
|
'artifacts_file="$remote_dir/artifacts.tsv"',
|
||||||
|
'cat > "$user_script"',
|
||||||
|
'chmod 700 "$user_script"',
|
||||||
|
"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() {",
|
||||||
|
" 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 bun >/dev/null 2>&1 && [ -f ./scripts/playwright-cli.ts ]; then",
|
||||||
|
" UNIDESK_PLAYWRIGHT_CLI_MODE=repo; UNIDESK_PLAYWRIGHT_CLI_CWD=$(pwd); UNIDESK_PLAYWRIGHT_CLI_BIN=; return 0",
|
||||||
|
" fi",
|
||||||
|
" 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=$?",
|
||||||
|
"set -e",
|
||||||
|
': > "$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)}`,
|
||||||
|
'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<Record<string, unknown>> {
|
||||||
|
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 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, "'\\''")}'`;
|
||||||
|
}
|
||||||
@@ -19,6 +19,10 @@ import {
|
|||||||
type SshRemoteCommandExecutor,
|
type SshRemoteCommandExecutor,
|
||||||
type SshRemoteCommandStreamHandlers,
|
type SshRemoteCommandStreamHandlers,
|
||||||
} from "./ssh-file-transfer";
|
} from "./ssh-file-transfer";
|
||||||
|
import {
|
||||||
|
isSshPlaywrightOperation,
|
||||||
|
runSshPlaywrightOperation,
|
||||||
|
} from "./ssh-playwright";
|
||||||
|
|
||||||
export interface ParsedSshArgs {
|
export interface ParsedSshArgs {
|
||||||
remoteCommand: string | null;
|
remoteCommand: string | null;
|
||||||
@@ -938,6 +942,9 @@ export function parseSshArgs(args: string[]): ParsedSshArgs {
|
|||||||
if (subcommand === "py") {
|
if (subcommand === "py") {
|
||||||
return { remoteCommand: buildPythonStdinCommand(args.slice(1)), requiresStdin: true, invocationKind: "helper" };
|
return { remoteCommand: buildPythonStdinCommand(args.slice(1)), requiresStdin: true, invocationKind: "helper" };
|
||||||
}
|
}
|
||||||
|
if (subcommand === "playwright") {
|
||||||
|
return { remoteCommand: null, requiresStdin: true, invocationKind: "helper" };
|
||||||
|
}
|
||||||
if (subcommand === "script" || subcommand === "sh") {
|
if (subcommand === "script" || subcommand === "sh") {
|
||||||
return buildShellCommand(args.slice(1));
|
return buildShellCommand(args.slice(1));
|
||||||
}
|
}
|
||||||
@@ -1126,6 +1133,9 @@ function parseWinRouteArgs(route: ParsedSshRoute, args: string[]): ParsedSshArgs
|
|||||||
invocationKind: "helper",
|
invocationKind: "helper",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (operation === "playwright") {
|
||||||
|
return { remoteCommand: null, requiresStdin: true, invocationKind: "helper" };
|
||||||
|
}
|
||||||
if (operation === "ps" || operation === "powershell" || operation === "powershell.exe") {
|
if (operation === "ps" || operation === "powershell" || operation === "powershell.exe") {
|
||||||
const commandArgs = args[1] === "--" ? args.slice(2) : args.slice(1);
|
const commandArgs = args[1] === "--" ? args.slice(2) : args.slice(1);
|
||||||
if (commandArgs.length >= 2 && (commandArgs[0] === "-File" || commandArgs[0] === "-file")) {
|
if (commandArgs.length >= 2 && (commandArgs[0] === "-File" || commandArgs[0] === "-file")) {
|
||||||
@@ -3314,6 +3324,16 @@ export async function runSsh(config: UniDeskConfig, providerId: string, args: st
|
|||||||
buildWindowsPowerShellCommand: buildWindowsPowerShellInvocation,
|
buildWindowsPowerShellCommand: buildWindowsPowerShellInvocation,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (isSshPlaywrightOperation(normalizedArgs)) {
|
||||||
|
const executor: SshRemoteCommandExecutor = {
|
||||||
|
runRemoteCommand: (remoteCommand, input) => runSshCaptureRemoteCommand(config, invocation, remoteCommand, input),
|
||||||
|
streamRemoteCommand: (remoteCommand, handlers, input, options) => runSshStreamRemoteCommand(config, invocation, remoteCommand, handlers, input, options),
|
||||||
|
};
|
||||||
|
return await runSshPlaywrightOperation(invocation, normalizedArgs, executor, {
|
||||||
|
buildRouteCommand: remoteCommandForRoute,
|
||||||
|
buildWindowsPowerShellCommand: buildWindowsPowerShellInvocation,
|
||||||
|
});
|
||||||
|
}
|
||||||
if (operationName === "apply-patch") {
|
if (operationName === "apply-patch") {
|
||||||
const applyPatch = effectiveApplyPatchV2Invocation(invocation, normalizedArgs.slice(1));
|
const applyPatch = effectiveApplyPatchV2Invocation(invocation, normalizedArgs.slice(1));
|
||||||
const executor: ApplyPatchV2Executor = applyPatch.invocation.route.plane === "win"
|
const executor: ApplyPatchV2Executor = applyPatch.invocation.route.plane === "win"
|
||||||
|
|||||||
Reference in New Issue
Block a user