fix: improve trans windows route hints

This commit is contained in:
Codex
2026-06-27 02:05:42 +00:00
parent 3b2399c902
commit 4b71bed594
6 changed files with 100 additions and 5 deletions
+4
View File
@@ -193,6 +193,9 @@ export function sshHelp(): unknown {
"trans D601:win/c/test stat README.md",
"trans D601:win/c/test wc README.md",
"trans D601:win/c/test rg -i needle .",
"trans D601:win/c/test git status --short --branch",
"trans D601:win/c/test git diff --check",
"trans D601:win/c/test git commit -m 'fix: update docs'",
"trans D601:win skills [--scope agents|codex|all] [--limit N]",
"trans D601:k3s",
"trans D601:k3s kubectl get pods -n hwlab-dev",
@@ -215,6 +218,7 @@ export function sshHelp(): unknown {
"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 explicit sh/bash stdin for shell logic.",
"Windows routes have explicit Windows operations, not POSIX shell aliases: `ps` runs Windows PowerShell from stdin or one inline command, `cmd` runs cmd.exe/batch from stdin or one command line, and `skills` discovers Windows skill directories.",
"On Windows routes, the route already contains `win`; write `trans D601:win/c/test ps`, not `trans D601:win/c/test win ps`. Repository commands can use the git convenience wrapper, including `git status`, `git diff`, and non-interactive `git commit -m ...`; commands that need shell review should use `ps` or `cmd`.",
"Windows routes include read-only filesystem convenience operations `pwd`, `ls`, `cat`, `head`, `tail`, `stat`, `wc`, and a bounded UTF-8 `rg` subset. These are implemented through a Windows fs backend with UTF-8/binary checks and bounded output; they do not imply POSIX `sh`/`bash` availability.",
"For Windows PowerShell, use `trans <provider>:win ps <<'PS'`; the PowerShell body is written to a temporary .ps1 with UTF-8 settings and executed by powershell.exe. Do not use POSIX `sh` or `bash` for Windows PowerShell.",
"For Windows cmd.exe, use `trans <provider>:win/<drive>/<path> cmd <<'CMD'`; `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.",
+21
View File
@@ -42,6 +42,22 @@ describe("ssh windows fs read-only operations", () => {
expect(rgInvocation.parsed.requiresStdin).toBe(false);
});
test("routes git status, diff, and commit through Windows cmd convenience", () => {
const statusInvocation = parseSshInvocation("D601:win/F/Work/ConStart", ["git", "status", "--short", "--branch"]);
const diffInvocation = parseSshInvocation("D601:win/F/Work/ConStart", ["git", "diff", "--check"]);
const commitInvocation = parseSshInvocation("D601:win/F/Work/ConStart", ["git", "commit", "-m", "fix: update docs"]);
expect(statusInvocation.route.plane).toBe("win");
expect(statusInvocation.route.workspace).toBe("F:\\Work\\ConStart");
expect(statusInvocation.parsed.invocationKind).toBe("argv");
expect(statusInvocation.parsed.requiresStdin).toBe(false);
expect(statusInvocation.parsed.remoteCommand).toContain("powershell.exe");
expect(diffInvocation.parsed.invocationKind).toBe("argv");
expect(diffInvocation.parsed.requiresStdin).toBe(false);
expect(commitInvocation.parsed.invocationKind).toBe("argv");
expect(commitInvocation.parsed.requiresStdin).toBe(false);
});
test("builds bounded UTF-8 scripts for cat, ls, and rg", () => {
const catScript = windowsFsReadOnlyScript("F:\\Work\\demo", "cat", ["--max-bytes", "4096", "中文.md"]);
const lsScript = windowsFsReadOnlyScript("F:\\Work\\demo", "ls", ["-la", "--limit=5"]);
@@ -63,6 +79,11 @@ describe("ssh windows fs read-only operations", () => {
test("rejects unsupported Windows read operations instead of treating them as POSIX", () => {
expect(() => parseSshInvocation("D601:win/c/test", ["sed", "-n", "1p", "hello.md"])).toThrow("unsupported ssh win operation: sed");
});
test("reports route-aware hints for repeated win operation and interactive git commit", () => {
expect(() => parseSshInvocation("D601:win/F/Work/ConStart", ["win", "ps"])).toThrow("route D601:win/F/Work/ConStart already selects the Windows plane");
expect(() => parseSshInvocation("D601:win/F/Work/ConStart", ["git", "commit"])).toThrow("ssh win git commit would open an editor");
});
});
describe("ssh host apply-patch fs backend", () => {
+66 -1
View File
@@ -1147,6 +1147,9 @@ function parseWinRouteArgs(route: ParsedSshRoute, args: string[]): ParsedSshArgs
if (operation.length === 0) {
throw new Error(`ssh ${route.raw} requires a Windows operation, for example: ssh ${route.providerId}:win cmd ver or ssh ${route.providerId}:win skills`);
}
if (operation === "win") {
throw new Error(windowsRouteRepeatedWinOperationMessage(route, args.slice(1)));
}
if (operation === "upload" || operation === "download") {
return { remoteCommand: null, requiresStdin: false, invocationKind: "helper" };
}
@@ -1179,6 +1182,9 @@ function parseWinRouteArgs(route: ParsedSshRoute, args: string[]): ParsedSshArgs
invocationKind: "helper",
};
}
if (operation === "git") {
return parseWindowsGitInvocation(route, args.slice(1));
}
if (operation === "ps" || operation === "powershell" || operation === "powershell.exe") {
const commandArgs = args[1] === "--" ? args.slice(2) : args.slice(1);
if (commandArgs.length >= 2 && (commandArgs[0] === "-File" || commandArgs[0] === "-file")) {
@@ -1203,7 +1209,7 @@ function parseWinRouteArgs(route: ParsedSshRoute, args: string[]): ParsedSshArgs
};
}
if (operation !== "cmd" && operation !== "cmd.exe") {
throw new Error(`unsupported ssh win operation: ${operation}; use ssh ${route.providerId}:win ps, ssh ${route.providerId}:win cmd <command-line>, ssh ${route.providerId}:win <pwd|ls|cat|head|tail|stat|wc|rg>, ssh ${route.providerId}:win apply-patch, or ssh ${route.providerId}:win skills`);
throw new Error(windowsUnsupportedOperationMessage(route, operation));
}
const commandArgs = args[1] === "--" ? args.slice(2) : args.slice(1);
if (commandArgs.length === 0) {
@@ -1220,6 +1226,56 @@ function parseWinRouteArgs(route: ParsedSshRoute, args: string[]): ParsedSshArgs
};
}
function windowsRouteRepeatedWinOperationMessage(route: ParsedSshRoute, nextArgs: string[]): string {
const entrypoint = sshDisplayEntrypoint();
const suffix = nextArgs.length > 0 ? ` ${nextArgs.join(" ")}` : " ps";
return `unsupported ssh win operation: win; route ${route.raw} already selects the Windows plane. ` +
`Write the operation directly after the route, for example \`${entrypoint} ${route.raw}${suffix}\`, not \`${entrypoint} ${route.raw} win${suffix}\`.`;
}
function windowsUnsupportedOperationMessage(route: ParsedSshRoute, operation: string): string {
const entrypoint = sshDisplayEntrypoint();
const gitHint = operation === "sed" || operation === "bash" || operation === "sh"
? ` For repository commands on Windows, use \`${entrypoint} ${route.raw} git status --short --branch\`, \`${entrypoint} ${route.raw} git commit -m <message>\`, or wrap custom commands with \`${entrypoint} ${route.raw} ps <<'PS'\`.`
: ` If you meant to run a repository command, use \`${entrypoint} ${route.raw} git status --short --branch\`, \`${entrypoint} ${route.raw} git commit -m <message>\`, or wrap it with \`${entrypoint} ${route.raw} ps <<'PS'\`.`;
return `unsupported ssh win operation: ${operation}; Windows route operations are ps, cmd, skills, apply-patch, upload/download, git, and read-only fs helpers pwd|ls|cat|head|tail|stat|wc|rg.${gitHint}`;
}
function parseWindowsGitInvocation(route: ParsedSshRoute, gitArgs: string[]): ParsedSshArgs {
const subcommand = gitArgs[0] ?? "";
if (subcommand.length === 0) throw new Error(`ssh ${route.raw} git requires a git subcommand, for example: ${sshDisplayEntrypoint()} ${route.raw} git status --short --branch`);
if (subcommand === "commit") validateWindowsGitCommitArgs(route, gitArgs);
const commandLine = ["git", ...gitArgs].map((value) => windowsCmdArgument(value, route)).join(" ");
return {
remoteCommand: buildWindowsPowerShellInvocation(buildWindowsCmdLauncherScript(buildWindowsCmdLine(commandLine, route.workspace))),
requiresStdin: false,
invocationKind: "argv",
};
}
function validateWindowsGitCommitArgs(route: ParsedSshRoute, gitArgs: string[]): void {
const hasNonInteractiveMessage = gitArgs.slice(1).some((arg) => (
arg === "--dry-run" ||
arg === "--no-edit" ||
arg === "-m" ||
arg.startsWith("-m") ||
arg === "--message" ||
arg.startsWith("--message=") ||
arg === "-F" ||
arg.startsWith("-F") ||
arg === "--file" ||
arg.startsWith("--file=") ||
arg === "-C" ||
arg.startsWith("-C") ||
arg === "--reuse-message" ||
arg.startsWith("--reuse-message=") ||
arg.startsWith("--fixup=")
));
if (hasNonInteractiveMessage) return;
const entrypoint = sshDisplayEntrypoint();
throw new Error(`ssh win git commit would open an editor; use ${entrypoint} ${route.raw} git commit -m <message>, ${entrypoint} ${route.raw} git commit --no-edit, or wrap an interactive command with ${entrypoint} ${route.raw} ps <<'PS'`);
}
type WindowsFsReadOnlyOperation = "pwd" | "ls" | "cat" | "head" | "tail" | "stat" | "wc" | "rg";
const windowsFsReadOnlyOperations = new Set<string>(["pwd", "ls", "cat", "head", "tail", "stat", "wc", "rg"]);
const windowsFsReadOnlyDefaultMaxBytes = 256 * 1024;
@@ -1281,6 +1337,15 @@ function windowsCmdQuote(value: string): string {
return `"${value}"`;
}
function windowsCmdArgument(value: string, route: ParsedSshRoute): string {
if (value.length === 0) return "\"\"";
if (/[\r\n"]/u.test(value) || /[&|<>^%!]/u.test(value)) {
const entrypoint = sshDisplayEntrypoint();
throw new Error(`ssh win git argument contains characters that require shell review; use ${entrypoint} ${route.raw} ps <<'PS' for this command`);
}
return /\s/u.test(value) ? `"${value}"` : value;
}
function parseWindowsSkillsOptions(args: string[]): { limit: number; scopes: Array<"agents" | "codex"> } {
let limit = 100;
let scope: "agents" | "codex" | "all" = "agents";