fix: make apply-patch default to v2

This commit is contained in:
Codex
2026-05-27 02:14:17 +00:00
parent 98715ce2be
commit aa5c61e278
10 changed files with 135 additions and 78 deletions
+18 -6
View File
@@ -75,18 +75,18 @@ export function isApplyPatchV2HelpArgs(args: string[] = []): boolean {
export function applyPatchV2HelpPayload() {
return {
ok: true,
command: "ssh <route> v2 < patch.diff",
command: "ssh <route> apply-patch < patch.diff",
summary: "Apply a standard apply_patch patch to a remote route with the local v2 line-based engine.",
usage: [
"tran G14:/root/hwlab/.worktree/task v2 < patch.diff",
"bun scripts/cli.ts ssh D601:/tmp v2 < patch.diff"
"tran G14:/root/hwlab/.worktree/task apply-patch < patch.diff",
"bun scripts/cli.ts ssh D601:/tmp apply-patch < patch.diff"
],
input: {
required: true,
firstLine: beginMarker,
lastLine: endMarker
},
note: "v2 reads patch text from stdin. Use `script -- ...` for ordinary remote shell commands."
note: "apply-patch reads patch text from stdin and uses the v2 engine by default. Use `apply-patch-v1` only for the legacy helper."
};
}
@@ -167,13 +167,25 @@ export async function runApplyPatchV2(options: ApplyPatchV2Options): Promise<num
options.stdout.write(`${JSON.stringify(applyPatchV2HelpPayload(), null, 2)}\n`);
return 0;
}
if ((options.argv?.length ?? 0) > 0) {
options.stdout.write(`${JSON.stringify({
ok: false,
error: {
code: "apply_patch_unsupported_args",
message: "ssh apply-patch uses the v2 engine and accepts no helper flags. Use apply-patch-v1 for legacy helper options."
},
unsupportedArgs: options.argv,
help: applyPatchV2HelpPayload()
}, null, 2)}\n`);
return 2;
}
const patchText = await readStreamText(options.stdin);
if (!patchText.trim()) {
options.stdout.write(`${JSON.stringify({
ok: false,
error: {
code: "v2_patch_stdin_required",
message: "ssh v2 requires patch text on stdin."
code: "apply_patch_stdin_required",
message: "ssh apply-patch requires patch text on stdin."
},
help: applyPatchV2HelpPayload()
}, null, 2)}\n`);
+9 -9
View File
@@ -20,15 +20,15 @@ export function rootHelp(): unknown {
{ command: "server rebuild <backend-core|frontend|dev-frontend-proxy|provider-gateway|todo-note|code-queue-mgr|project-manager|baidu-netdisk|oa-event-flow>", description: "Maintenance-only local Compose rebuild for reviewed main-server services; frontend standard release must use CI artifact plus deploy apply dev/prod artifact consumers." },
{ command: "provider attach <providerId> [--master-server URL] [--up] [--force] | provider triage <providerId> [--observed-error text] [--observed-scope scope] [--microservice id ...] [--full|--raw]", description: "Generate the minimal external provider-gateway env/compose bundle or run the low-noise read-only provider health triage contract." },
{ command: "ssh <route> [operation args...]", description: "Open a Host SSH / WSL SSH maintenance session through the provider-gateway bridge; route syntax such as `G14:k3s` or `D601:win/c/test` only locates distributed targets." },
{ command: "ssh <route> v2 < patch.diff", description: "Preferred remote text patch entry: apply a standard patch with the local TypeScript v2 engine while the remote route only reads and writes files." },
{ command: "ssh <providerId> apply-patch [tool args...] < patch.diff", description: "Fallback to the injected remote apply_patch helper directly over SSH passthrough and stream the patch from local stdin." },
{ command: "ssh <route> apply-patch < patch.diff", description: "Default remote text patch entry: apply a standard patch with the local TypeScript v2 engine while the remote route only reads and writes files." },
{ command: "ssh <providerId> apply-patch-v1 [tool args...] < patch.diff", description: "Fallback to the injected legacy remote apply_patch helper directly over SSH passthrough and stream the patch from local stdin." },
{ command: "ssh <providerId> py [script-args...] < script.py", description: "Run remote Python from local stdin through SSH passthrough without nested shell quoting; extra args become script argv." },
{ command: "ssh <providerId> script [--shell sh|bash] [script-args...] <<'SCRIPT' ...", description: "Run a remote shell script from local stdin using shell -s; the default sh inherits provider proxy env, so --shell bash is only for bash-specific syntax." },
{ command: "ssh <providerId> skills [--scope all|wsl|windows] [--limit N]", description: "Discover WSL/Linux and, for WSL providers, Windows skill directories in one SSH passthrough call." },
{ command: "ssh <providerId> find <path...> [--max-depth N] [--type d|f|l] [--contains TEXT] [--iname PATTERN] [--limit N] [--sort]", description: "Run a structured remote find command without nested shell quoting or parentheses." },
{ command: "ssh <providerId> glob [--root DIR] [--pattern PATTERN] [--contains TEXT] [--type any|f|d] [--limit N] [--sort]", description: "Run remote glob matching through the injected helper without shell glob expansion." },
{ command: "ssh <providerId>:/absolute/workspace <operation args...>", description: "Route directly into a host workspace while keeping the operation parser independent from the location." },
{ command: "ssh <providerId>:k3s[:namespace:workload[:container]] <kubectl|logs|exec|script|v2|apply-patch|command> ...", description: "Locate a native k3s control plane or workload with route syntax, then run a separate operation with KUBECONFIG fixed and argv assembled by the CLI." },
{ command: "ssh <providerId>:k3s[:namespace:workload[:container]] <kubectl|logs|exec|script|apply-patch|apply-patch-v1|command> ...", description: "Locate a native k3s control plane or workload with route syntax, then run a separate operation with KUBECONFIG fixed and argv assembled by the CLI." },
{ command: "ssh <providerId> argv <command> [args...]", description: "Run a non-interactive remote command with each argv token shell-quoted by UniDesk before SSH passthrough; use `ssh <providerId> script` when shell features are required." },
{ command: "microservice list", description: "List UniDesk-managed user services and their provider/runtime mapping." },
{ command: "microservice status <id>", description: "Show one user service config, repository reference, backend mapping, and runtime status." },
@@ -149,8 +149,8 @@ export function sshHelp(): unknown {
usage: [
"bun scripts/cli.ts ssh <route>",
"bun scripts/cli.ts ssh <providerId> argv <command> [args...]",
"bun scripts/cli.ts ssh <providerId>:/absolute/workspace v2 < patch.diff",
"bun scripts/cli.ts ssh <providerId> apply-patch [--allow-loose] < patch.diff",
"bun scripts/cli.ts ssh <providerId>:/absolute/workspace apply-patch < patch.diff",
"bun scripts/cli.ts ssh <providerId> apply-patch-v1 [--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\"",
@@ -167,8 +167,8 @@ export function sshHelp(): unknown {
"bun scripts/cli.ts ssh G14:k3s kubectl get pipelineruns -n hwlab-ci",
"bun scripts/cli.ts ssh D601:k3s script <<'SCRIPT'",
"bun scripts/cli.ts ssh D601:k3s:hwlab-dev:hwlab-cloud-api/app pwd",
"bun scripts/cli.ts ssh D601:k3s:hwlab-dev:hwlab-cloud-api/app v2 <<'PATCH'",
"bun scripts/cli.ts ssh D601:k3s:hwlab-dev:hwlab-cloud-api apply-patch <<'PATCH'",
"bun scripts/cli.ts ssh D601:k3s:hwlab-dev:hwlab-cloud-api/app apply-patch <<'PATCH'",
"bun scripts/cli.ts ssh D601:k3s:hwlab-dev:hwlab-cloud-api apply-patch-v1 <<'PATCH'",
"tar -C /path/to/files -cf - . | bun scripts/cli.ts ssh D601:k3s:unidesk:code-queue/root/unidesk exec --stdin -- tar -xf - -C /root/unidesk",
"bun scripts/cli.ts ssh D601:k3s:hwlab-dev:hwlab-cloud-api node -e 'console.log(process.version)'",
"bun scripts/cli.ts ssh D601:k3s:hwlab-dev:hwlab-cloud-api script <<'SCRIPT'",
@@ -180,8 +180,8 @@ export function sshHelp(): unknown {
"For one-line remote shell logic without a heredoc, use `script -- '<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. 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 `tran D601:/work script -- sed -n '1,20p' file`.",
"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.",
"For remote text patches, prefer `v2`; it keeps the legacy apply-patch command unchanged but uses a local line-based patch engine and remote read/write operations, so long Unicode/Chinese lines and pure insertion hunks avoid the legacy remote shell hunk parser.",
"Legacy apply-patch is a fallback: it 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.",
"`apply-patch` is the default remote text patch entry and uses the v2 local line-based patch engine with remote read/write operations, so long Unicode/Chinese lines and pure insertion hunks avoid the legacy remote shell hunk parser.",
"`apply-patch-v1` is the only legacy fallback entry: it 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.",
"script defaults to target /bin/sh and inherits provider proxy variables such as HTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXY; use --shell bash only for bash syntax such as pipefail, arrays, or [[ ... ]], not as a proxy workaround.",
"Route syntax is `{provider}:{plane}[:{scope...}] {operation} [operation-args...]`: the first argv token locates a distributed target only, and every following token belongs to the operation parser. Host workspace routes use `<provider>:/absolute/workspace`; WSL providers can use `<provider>:win cmd <command-line>` to run Windows host cmd.exe with UTF-8 defaults, and `<provider>:win/c/test cmd cd` maps the Windows cwd to `C:\\test`; native k3s providers such as D601 and G14 use <provider>:k3s for the control plane, <provider>:k3s:<namespace>:<workload> for a workload, and <provider>:k3s:<namespace>:<workload>/<pod-workspace> for a pod workspace.",
"Use `win`, not `win32`; the win route sets chcp 65001, PYTHONUTF8=1, and PYTHONIOENCODING=utf-8 before running the requested cmd command line.",
+1 -1
View File
@@ -1253,7 +1253,7 @@ export function remoteSshFrontendPlanForTest(target: string, args: string[]): Re
async function runRemoteSshOverFrontend(session: FrontendSession, target: string | undefined, args: string[]): Promise<number> {
if (!target) throw new Error("remote ssh requires a route, for example: bun scripts/cli.ts --main-server-ip 74.48.78.17 ssh D601 hostname");
const invocation = parseSshInvocation(target, args);
if ((args[0] ?? "") === "v2") {
if ((args[0] ?? "") === "apply-patch") {
const executor: ApplyPatchV2Executor = {
run: (command, input) => runRemoteSshWebSocketCapture(session, invocation, command, input),
};
+29 -12
View File
@@ -93,6 +93,9 @@ const legacyK3sOperationRouteSegments = new Set([
"exec",
"script",
"apply-patch",
"apply-patch-v1",
"patch",
"patch-v1",
"v2",
"logs",
"get",
@@ -825,16 +828,19 @@ export function parseSshArgs(args: string[]): ParsedSshArgs {
const toolArgs = subcommand === "skill" ? ["skill-discover", ...args.slice(2)] : ["skill-discover", ...args.slice(1)];
return { remoteCommand: shellArgv(toolArgs), requiresStdin: false, invocationKind: "helper", requiredHelpers: ["skill-discover"] };
}
if (subcommand === "apply-patch" || subcommand === "patch") {
const toolArgs = ["apply_patch", ...args.slice(1)];
return { remoteCommand: shellArgv(toolArgs), requiresStdin: true, invocationKind: "helper", requiredHelpers: ["apply_patch"] };
}
if (subcommand === "v2") {
if (subcommand === "apply-patch") {
if (isApplyPatchV2HelpArgs(args.slice(1))) {
return { remoteCommand: null, requiresStdin: false, invocationKind: "helper" };
}
return { remoteCommand: null, requiresStdin: true, invocationKind: "helper" };
}
if (subcommand === "apply-patch-v1") {
const toolArgs = ["apply_patch", ...args.slice(1)];
return { remoteCommand: shellArgv(toolArgs), requiresStdin: true, invocationKind: "helper", requiredHelpers: ["apply_patch"] };
}
if (subcommand === "patch" || subcommand === "patch-v1" || subcommand === "v2") {
throw new Error("remote patch entrypoints are `apply-patch` for the default v2 engine and `apply-patch-v1` for the legacy helper");
}
if (subcommand === "py") {
return { remoteCommand: buildPythonStdinCommand(args.slice(1)), requiresStdin: true, invocationKind: "helper" };
}
@@ -1163,6 +1169,9 @@ function isK3sResourceKindAlias(value: string): boolean {
function k3sOperationInRouteMessage(target: string, operation: string): string {
const providerId = target.split(":")[0] || "<provider>";
if (operation === "v2" || operation === "patch" || operation === "patch-v1") {
return `ssh k3s route must locate a target only; remote patch entrypoints are "apply-patch" for the default v2 engine and "apply-patch-v1" for the legacy helper instead of "${target}"`;
}
const operationExample = operation === "guard" ? "guard" : `${operation} ...`;
return `ssh k3s route must locate a target only; put operation "${operation}" after the route, for example "ssh ${providerId}:k3s ${operationExample}" or "ssh ${providerId}:k3s:<namespace>:<workload> ${operationExample}" instead of "${target}"`;
}
@@ -1283,11 +1292,11 @@ function parseK3sRouteArgs(route: ParsedSshRoute, args: string[]): ParsedSshArgs
function parseK3sControlPlaneOperation(route: ParsedSshRoute, args: string[]): ParsedSshArgs {
const operation = args[0] ?? "guard";
if (operation === "apply-patch" || operation === "patch") {
if (operation === "apply-patch" || operation === "apply-patch-v1") {
throw new Error(`ssh ${route.providerId}:k3s apply-patch requires a workload route: ssh ${route.providerId}:k3s:<namespace>:<workload> apply-patch`);
}
if (operation === "v2") {
throw new Error(`ssh ${route.providerId}:k3s v2 requires a workload route: ssh ${route.providerId}:k3s:<namespace>:<workload> v2`);
if (operation === "patch" || operation === "patch-v1" || operation === "v2") {
throw new Error("remote patch entrypoints are `apply-patch` for the default v2 engine and `apply-patch-v1` for the legacy helper");
}
if (operation === "script" || operation === "sh") {
return { remoteCommand: buildK3sScriptCommand(args.slice(1)), requiresStdin: true, invocationKind: "helper" };
@@ -1314,8 +1323,16 @@ function parseK3sTargetOperation(route: ParsedSshRoute, args: string[]): ParsedS
}
const operation = args[0] ?? "";
const operationArgs = args.slice(1);
if (operation === "apply-patch" || operation === "patch") return buildK3sApplyPatchCommand([...targetArgs, ...operationArgs]);
if (operation === "v2") return { remoteCommand: null, requiresStdin: true, invocationKind: "helper" };
if (operation === "apply-patch") {
if (isApplyPatchV2HelpArgs(operationArgs)) {
return { remoteCommand: null, requiresStdin: false, invocationKind: "helper" };
}
return { remoteCommand: null, requiresStdin: true, invocationKind: "helper" };
}
if (operation === "apply-patch-v1") return buildK3sApplyPatchCommand([...targetArgs, ...operationArgs]);
if (operation === "patch" || operation === "patch-v1" || operation === "v2") {
throw new Error("remote patch entrypoints are `apply-patch` for the default v2 engine and `apply-patch-v1` for the legacy helper");
}
if (operation === "script") return { remoteCommand: buildK3sScriptCommand([...targetArgs, ...operationArgs]), requiresStdin: true, invocationKind: "helper" };
if (operation === "shell") {
const parsed = parseShellStringOperationArgs(operationArgs, `ssh ${route.raw} shell`);
@@ -2057,7 +2074,7 @@ function terminalSize(): { cols: number; rows: number } {
export function remoteCommandForRoute(route: ParsedSshRoute, command: string[]): string {
if (route.plane === "k3s") return buildK3sTargetCommand(route, command);
if (route.plane === "win") throw new Error(`ssh v2 does not support win routes yet: ${route.raw}`);
if (route.plane === "win") throw new Error(`ssh apply-patch does not support win routes yet: ${route.raw}`);
return shellArgv(command);
}
@@ -2167,7 +2184,7 @@ export async function runSsh(config: UniDeskConfig, providerId: string, args: st
const invocation = parseSshInvocation(providerId, args);
const parsed = invocation.parsed;
const operationName = args[0] ?? "";
if (operationName === "v2") {
if (operationName === "apply-patch") {
const executor: ApplyPatchV2Executor = { run: (command, input) => runSshCaptureCommand(config, invocation, command, input) };
return await runApplyPatchV2({
executor,