feat: add ssh shell passthrough
This commit is contained in:
@@ -151,6 +151,7 @@ export function sshHelp(): unknown {
|
||||
"bun scripts/cli.ts ssh <providerId> apply-patch [--allow-loose] < patch.diff",
|
||||
"bun scripts/cli.ts ssh <providerId> py [script-args...] < script.py",
|
||||
"bun scripts/cli.ts ssh <providerId> script [--shell sh|bash] [script-args...] <<'SCRIPT'",
|
||||
"bun scripts/cli.ts ssh <providerId> shell [--shell sh|bash] \"sed -n '1,20p' a && sed -n '1,20p' b\"",
|
||||
"bun scripts/cli.ts ssh <providerId> skills [--scope all|wsl|windows] [--limit N]",
|
||||
"bun scripts/cli.ts ssh <providerId> find <path...> [--contains TEXT] [--limit N]",
|
||||
"bun scripts/cli.ts ssh <providerId> glob [--root DIR] [--pattern PATTERN]",
|
||||
@@ -170,6 +171,7 @@ export function sshHelp(): unknown {
|
||||
notes: [
|
||||
"ssh --help and ssh <route> --help print this JSON help and never open an interactive session.",
|
||||
"For non-interactive remote commands, prefer argv for a single process and script/stdin for shell logic.",
|
||||
"For one-line remote shell logic without a heredoc, use `shell '<command && command>'`; outer shell operators written outside tran, such as `tran G14:/repo sed ... && sed ...`, are parsed by the local shell before UniDesk starts and therefore cannot be redirected by the CLI.",
|
||||
"When a short remote command is easier to type through the script path and needs dash-prefixed argv, write `script -- <command> [args...]`; this direct form does not wait for stdin, and the command-local `--` is preserved by local and remote `tran` parsing, so examples such as `tran D601:/work script -- sed -n '1,20p' file` do not require a heredoc just to pass `-n`.",
|
||||
"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 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.",
|
||||
|
||||
@@ -808,6 +808,9 @@ export function parseSshArgs(args: string[]): ParsedSshArgs {
|
||||
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`);
|
||||
@@ -1080,6 +1083,10 @@ function parseK3sControlPlaneOperation(route: ParsedSshRoute, args: string[]): P
|
||||
if (operation === "script" || operation === "sh") {
|
||||
return { remoteCommand: buildK3sScriptCommand(args.slice(1)), requiresStdin: true, invocationKind: "helper" };
|
||||
}
|
||||
if (operation === "shell") {
|
||||
const parsed = parseShellStringOperationArgs(args.slice(1), `ssh ${route.providerId}:k3s shell`);
|
||||
return { remoteCommand: shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, parsed.shell, "-c", 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" };
|
||||
@@ -1100,6 +1107,10 @@ function parseK3sTargetOperation(route: ParsedSshRoute, args: string[]): ParsedS
|
||||
const operationArgs = args.slice(1);
|
||||
if (operation === "apply-patch" || operation === "patch") return buildK3sApplyPatchCommand([...targetArgs, ...operationArgs]);
|
||||
if (operation === "script") return { remoteCommand: buildK3sScriptCommand([...targetArgs, ...operationArgs]), requiresStdin: true, invocationKind: "helper" };
|
||||
if (operation === "shell") {
|
||||
const parsed = parseShellStringOperationArgs(operationArgs, `ssh ${route.raw} shell`);
|
||||
return { remoteCommand: buildK3sExecCommand([...targetArgs, "--", parsed.shell, "-c", parsed.command]), requiresStdin: false, invocationKind: "helper" };
|
||||
}
|
||||
if (operation === "logs") return { remoteCommand: buildK3sLogsCommand([...targetArgs, ...operationArgs]), requiresStdin: false, invocationKind: "helper" };
|
||||
if (operation === "argv") return { remoteCommand: buildK3sExecCommand([...targetArgs, ...k3sRouteCommandArgs(operationArgs)]), requiresStdin: false, invocationKind: "argv" };
|
||||
if (operation === "get" || operation === "describe") {
|
||||
@@ -1300,6 +1311,26 @@ function buildK3sHostScriptCommand(parsed: K3sTargetOptions): string {
|
||||
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 });
|
||||
if (parsed.namespace === null) throw new Error("ssh k3s logs requires --namespace <name>");
|
||||
@@ -1483,6 +1514,11 @@ function buildShellCommand(args: string[]): ParsedSshArgs {
|
||||
return { remoteCommand: shellArgv([shell, "-s", "--", ...scriptArgs]), requiresStdin: true, invocationKind: "helper" };
|
||||
}
|
||||
|
||||
function buildShellStringCommand(args: string[]): ParsedSshArgs {
|
||||
const parsed = parseShellStringOperationArgs(args, "ssh shell");
|
||||
return { remoteCommand: shellArgv([parsed.shell, "-c", parsed.command]), requiresStdin: false, invocationKind: "helper" };
|
||||
}
|
||||
|
||||
function podApplyPatchStdinWrapper(): { prefix: string; suffix: string } {
|
||||
const toolMarker = "__UNIDESK_APPLY_PATCH_TOOL__";
|
||||
const patchMarker = "__UNIDESK_APPLY_PATCH_PAYLOAD__";
|
||||
|
||||
Reference in New Issue
Block a user