fix: improve trans windows route hints
This commit is contained in:
@@ -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.",
|
||||
|
||||
@@ -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
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user