feat: support Windows cmd stdin scripts
This commit is contained in:
@@ -168,6 +168,7 @@ export function sshHelp(): unknown {
|
|||||||
"trans <providerId> find <path...> [--contains TEXT] [--limit N]",
|
"trans <providerId> find <path...> [--contains TEXT] [--limit N]",
|
||||||
"trans <providerId> glob [--root DIR] [--pattern PATTERN]",
|
"trans <providerId> glob [--root DIR] [--pattern PATTERN]",
|
||||||
"trans D601:/home/ubuntu/workspace/hwlab-dev git status --short --branch",
|
"trans D601:/home/ubuntu/workspace/hwlab-dev git status --short --branch",
|
||||||
|
"trans D601:win/c/test cmd <<'CMD'",
|
||||||
"trans D601:win cmd ver",
|
"trans D601:win cmd ver",
|
||||||
"trans D601:win/c/test cmd cd",
|
"trans D601:win/c/test cmd cd",
|
||||||
"trans D601:win skills [--scope agents|codex|all] [--limit N]",
|
"trans D601:win skills [--scope agents|codex|all] [--limit N]",
|
||||||
@@ -191,6 +192,7 @@ export function sshHelp(): unknown {
|
|||||||
notes: [
|
notes: [
|
||||||
"trans --help and trans <route> --help print this JSON help and never open an interactive session; the underlying ssh subcommand keeps the same help behavior.",
|
"trans --help and trans <route> --help print this JSON help and never open an interactive session; the underlying ssh subcommand keeps the same help behavior.",
|
||||||
"For non-interactive remote commands, prefer argv for a single process and script/stdin for shell logic.",
|
"For non-interactive remote commands, prefer argv for a single process and script/stdin for shell logic.",
|
||||||
|
"For Windows routes, prefer `trans <provider>:win/<drive>/<path> cmd <<'CMD'` for multi-step cmd.exe logic; `cmd` with no command-line arguments reads the UTF-8 batch body from stdin, injects UTF-8/Python encoding defaults, runs it from a temp .cmd file, and deletes the temp file.",
|
||||||
"`argv` executes direct argv tokens only: `trans <route> argv ls -la` is valid, but `trans <route> argv 'ls -la'` is rejected because the single string would be treated as an executable path; use `script -- 'ls -la'` for one-line shell logic.",
|
"`argv` executes direct argv tokens only: `trans <route> argv ls -la` is valid, but `trans <route> argv 'ls -la'` is rejected because the single string would be treated as an executable path; use `script -- 'ls -la'` for one-line shell logic.",
|
||||||
"For one-line remote shell logic without a heredoc, use `script -- '<command && command>'`; outer shell operators written outside trans, such as `trans G14:/repo sed ... && sed ...`, are parsed by the local shell before UniDesk starts and therefore cannot be redirected by the CLI. The explicit `shell '<command>'` operation remains available for the same sh -c path.",
|
"For one-line remote shell logic without a heredoc, use `script -- '<command && command>'`; outer shell operators written outside trans, such as `trans G14:/repo sed ... && sed ...`, are parsed by the local shell before UniDesk starts and therefore cannot be redirected by the CLI. The explicit `shell '<command>'` operation remains available for the same sh -c path.",
|
||||||
"When a one-line shell command is easier to type through the script path, `script -- '<command && command>'` runs that single string through the remote shell without waiting for stdin. When `script --` is followed by multiple tokens, it stays a direct argv form for commands such as `trans D601:/work script -- sed -n '1,20p' file`.",
|
"When a one-line shell command is easier to type through the script path, `script -- '<command && command>'` runs that single string through the remote shell without waiting for stdin. When `script --` is followed by multiple tokens, it stays a direct argv form for commands such as `trans D601:/work script -- sed -n '1,20p' file`.",
|
||||||
|
|||||||
+40
-2
@@ -1052,7 +1052,13 @@ function parseWinRouteArgs(route: ParsedSshRoute, args: string[]): ParsedSshArgs
|
|||||||
throw new Error(`unsupported ssh win operation: ${operation}; use ssh ${route.providerId}:win cmd <command-line>, ssh ${route.providerId}:win apply-patch, or ssh ${route.providerId}:win skills`);
|
throw new Error(`unsupported ssh win operation: ${operation}; use 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);
|
const commandArgs = args[1] === "--" ? args.slice(2) : args.slice(1);
|
||||||
if (commandArgs.length === 0) throw new Error(`ssh ${route.raw} cmd requires a command line, for example: ssh ${route.providerId}:win cmd ver`);
|
if (commandArgs.length === 0) {
|
||||||
|
return {
|
||||||
|
remoteCommand: buildWindowsPowerShellInvocation(buildWindowsCmdStdinLauncherScript(route.workspace)),
|
||||||
|
requiresStdin: true,
|
||||||
|
invocationKind: "helper",
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
remoteCommand: buildWindowsPowerShellInvocation(buildWindowsCmdLauncherScript(buildWindowsCmdLine(commandArgs.join(" "), route.workspace))),
|
remoteCommand: buildWindowsPowerShellInvocation(buildWindowsCmdLauncherScript(buildWindowsCmdLine(commandArgs.join(" "), route.workspace))),
|
||||||
requiresStdin: false,
|
requiresStdin: false,
|
||||||
@@ -1163,6 +1169,38 @@ function buildWindowsCmdLauncherScript(cmdLine: string): string {
|
|||||||
].join(" ");
|
].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();",
|
||||||
|
"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(" ");
|
||||||
|
}
|
||||||
|
|
||||||
export function buildWindowsPowerShellInvocation(script: string): string {
|
export function buildWindowsPowerShellInvocation(script: string): string {
|
||||||
return shellArgv([
|
return shellArgv([
|
||||||
windowsPowerShellExePath,
|
windowsPowerShellExePath,
|
||||||
@@ -2559,7 +2597,7 @@ export async function runSsh(config: UniDeskConfig, providerId: string, args: st
|
|||||||
command: wrapSshRemoteCommand(parsed.remoteCommand, parsed.requiredHelpers),
|
command: wrapSshRemoteCommand(parsed.remoteCommand, parsed.requiredHelpers),
|
||||||
cwd: sshRoutePayloadCwd(invocation.route),
|
cwd: sshRoutePayloadCwd(invocation.route),
|
||||||
tty: parsed.remoteCommand === null,
|
tty: parsed.remoteCommand === null,
|
||||||
stdinEotOnEnd: parsed.remoteCommand !== null,
|
stdinEotOnEnd: parsed.remoteCommand !== null && parsed.requiresStdin !== true,
|
||||||
openTimeoutMs,
|
openTimeoutMs,
|
||||||
runtimeTimeoutMs,
|
runtimeTimeoutMs,
|
||||||
cols: size.cols,
|
cols: size.cols,
|
||||||
|
|||||||
Reference in New Issue
Block a user