129 lines
5.1 KiB
TypeScript
129 lines
5.1 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import type { UniDeskConfig } from "../config";
|
|
import type { RenderedCliResult } from "../output";
|
|
import { resolveAgentRunLaneTarget } from "../agentrun-lanes";
|
|
import { runSshCommandCapture } from "../ssh";
|
|
import { readAgentRunClientConfig } from "./config";
|
|
import { renderMachine, renderedCliResult } from "./render";
|
|
import type { AgentRunResourceOptions } from "./utils";
|
|
|
|
export async function runAgentRunResourceThroughTarget(
|
|
config: UniDeskConfig | null,
|
|
command: string,
|
|
canonicalArgs: string[],
|
|
options: AgentRunResourceOptions,
|
|
): Promise<RenderedCliResult | null> {
|
|
const client = readAgentRunClientConfig();
|
|
if (client.client.transport !== "target-trans") return null;
|
|
if (config === null) return null;
|
|
const execution = client.client.targetExecution;
|
|
if (execution === null) throw new Error(`${client.sourcePath}: client.targetExecution is required for target-trans`);
|
|
const { configPath, spec } = resolveAgentRunLaneTarget({ node: options.node, lane: options.lane });
|
|
const activeTarget = process.env[execution.markerEnv] ?? null;
|
|
if (activeTarget === spec.nodeId) return null;
|
|
if (activeTarget !== null) {
|
|
throw new Error(`${execution.markerEnv}=${activeTarget} cannot re-enter a second Target ${spec.nodeId}`);
|
|
}
|
|
const route = `${spec.nodeRoute}:${spec.nodeUnideskWorkspace}`;
|
|
const targetArgs = [...canonicalArgs];
|
|
if (!hasOption(targetArgs, "target")) targetArgs.push("--target", spec.nodeId);
|
|
if (!hasOption(targetArgs, "lane")) targetArgs.push("--lane", spec.lane);
|
|
const stdin = targetExecutionStdin(targetArgs);
|
|
const capture = await runSshCommandCapture(config, route, [
|
|
"argv",
|
|
"env",
|
|
`${execution.markerEnv}=${spec.nodeId}`,
|
|
execution.executable,
|
|
execution.cliPath,
|
|
"agentrun",
|
|
...targetArgs,
|
|
], stdin);
|
|
const disclosure = {
|
|
transport: "target-trans",
|
|
target: spec.nodeId,
|
|
lane: spec.lane,
|
|
route,
|
|
configPath,
|
|
unideskWorkspace: spec.nodeUnideskWorkspace,
|
|
markerEnv: execution.markerEnv,
|
|
mutation: false,
|
|
valuesPrinted: false,
|
|
};
|
|
if (capture.exitCode !== 0) {
|
|
const detail = boundedRemoteFailure(capture.stderr || capture.stdout);
|
|
return renderedCliResult(
|
|
false,
|
|
command,
|
|
[
|
|
`ERROR target-trans/${spec.nodeId}`,
|
|
`route: ${route}`,
|
|
`exitCode: ${capture.exitCode}`,
|
|
`message: ${detail || "Target CLI did not return output"}`,
|
|
].join("\n"),
|
|
);
|
|
}
|
|
|
|
const output = capture.stdout.trimEnd();
|
|
if (options.raw || options.output === "json") {
|
|
const payload = parseTargetMachineOutput(output, "json");
|
|
return renderMachine(command, attachTargetExecution(payload, disclosure), "json", machineOutputOk(payload));
|
|
}
|
|
if (options.output === "yaml") {
|
|
const payload = parseTargetMachineOutput(output, "yaml");
|
|
return renderMachine(command, attachTargetExecution(payload, disclosure), "yaml", machineOutputOk(payload));
|
|
}
|
|
return renderedCliResult(
|
|
true,
|
|
command,
|
|
[`TargetExecution: trans ${route} lane=${spec.lane}`, output].filter(Boolean).join("\n"),
|
|
);
|
|
}
|
|
|
|
function hasOption(args: string[], name: string): boolean {
|
|
const flag = `--${name}`;
|
|
return args.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
|
|
}
|
|
|
|
function targetExecutionStdin(args: string[]): string | undefined {
|
|
const stdinFlag = args.some((arg) => arg === "--prompt-stdin" || arg === "--stdin"
|
|
|| arg === "--json-stdin" || arg === "--yaml-stdin");
|
|
const stdinFile = optionIsStdin(args, "prompt-file") || optionIsStdin(args, "json-file")
|
|
|| optionIsStdin(args, "yaml-file") || optionIsStdin(args, "file") || optionIsStdin(args, "filename")
|
|
|| args.some((arg, index) => arg === "-f" && args[index + 1] === "-");
|
|
return stdinFlag || stdinFile ? readFileSync(0, "utf8") : undefined;
|
|
}
|
|
|
|
function optionIsStdin(args: string[], name: string): boolean {
|
|
const flag = `--${name}`;
|
|
return args.some((arg, index) => (arg === flag && args[index + 1] === "-") || arg === `${flag}=-`);
|
|
}
|
|
|
|
function parseTargetMachineOutput(output: string, mode: "json" | "yaml"): unknown {
|
|
if (output.length === 0) throw new Error(`Target CLI returned empty ${mode} output`);
|
|
try {
|
|
return mode === "json" ? JSON.parse(output) : Bun.YAML.parse(output);
|
|
} catch (error) {
|
|
throw new Error(`Target CLI returned invalid ${mode} output: ${error instanceof Error ? error.message : String(error)}`);
|
|
}
|
|
}
|
|
|
|
function attachTargetExecution(value: unknown, disclosure: Record<string, unknown>): Record<string, unknown> {
|
|
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
return { ...(value as Record<string, unknown>), targetExecution: disclosure };
|
|
}
|
|
return { data: value, targetExecution: disclosure };
|
|
}
|
|
|
|
function machineOutputOk(value: unknown): boolean {
|
|
return !(typeof value === "object" && value !== null && (value as Record<string, unknown>).ok === false);
|
|
}
|
|
|
|
function boundedRemoteFailure(value: string): string {
|
|
return value
|
|
.replace(/https?:\/\/[^@\s]+@/gu, "https://[redacted]@")
|
|
.replace(/(token|password|authorization)[=:][^\s]+/giu, "$1=[redacted]")
|
|
.replace(/\s+/gu, " ")
|
|
.trim()
|
|
.slice(0, 600);
|
|
}
|