fix: support windows ssh apply-patch

This commit is contained in:
Codex
2026-05-27 11:28:54 +00:00
parent 18f49922ba
commit c0eddacd8d
4 changed files with 194 additions and 7 deletions
+64 -1
View File
@@ -32,7 +32,20 @@ export interface ApplyPatchV2Options {
}
export interface ApplyPatchV2Executor {
run(command: string[], input?: string): Promise<ApplyPatchV2RemoteResult>;
run?: (command: string[], input?: string) => Promise<ApplyPatchV2RemoteResult>;
fs?: ApplyPatchV2FileSystem;
}
export interface ApplyPatchV2FileSystem {
stat(path: string): Promise<ApplyPatchV2FileStat>;
readBlock(path: string, blockIndex: number, blockBytes: number): Promise<Buffer>;
writeFile(path: string, content: Buffer): Promise<void>;
deleteFile(path: string): Promise<void>;
}
export interface ApplyPatchV2FileStat {
bytes: number;
sha256: string;
}
export interface ApplyPatchV2RemoteResult {
@@ -258,6 +271,10 @@ async function executePlannedOperation(executor: ApplyPatchV2Executor, operation
await writeRemoteText(executor, operation.path, operation.content);
return;
}
if (executor.fs) {
await executor.fs.deleteFile(operation.path);
return;
}
await checkedRemoteV2(executor, "delete", [operation.path]);
}
@@ -266,6 +283,45 @@ const writeB64ArgvLimit = 48_000;
const writeB64ChunkChars = 12_000;
async function readRemoteText(executor: ApplyPatchV2Executor, target: string): Promise<string> {
if (executor.fs) {
const stat = await executor.fs.stat(target);
if (!Number.isSafeInteger(stat.bytes) || stat.bytes < 0 || !/^[0-9a-f]{64}$/u.test(stat.sha256)) {
throw new ApplyPatchV2Error("remote apply-patch v2 fs stat returned invalid metadata", { path: target, stat });
}
const chunks: Buffer[] = [];
let actualBytes = 0;
for (let blockIndex = 0; actualBytes < stat.bytes; blockIndex += 1) {
const chunk = await executor.fs.readBlock(target, blockIndex, readBlockBytes);
if (chunk.length === 0) {
throw new ApplyPatchV2Error("remote apply-patch v2 fs read returned an empty block before EOF", {
path: target,
blockIndex,
expectedBytes: stat.bytes,
actualBytes,
});
}
chunks.push(chunk);
actualBytes += chunk.length;
}
const contentBuffer = Buffer.concat(chunks);
if (contentBuffer.length !== stat.bytes) {
throw new ApplyPatchV2Error("remote apply-patch v2 fs read byte count mismatch", {
path: target,
expectedBytes: stat.bytes,
actualBytes: contentBuffer.length,
});
}
const actualSha256 = sha256Hex(contentBuffer);
if (actualSha256 !== stat.sha256) {
throw new ApplyPatchV2Error("remote apply-patch v2 fs read sha256 mismatch", {
path: target,
expectedSha256: stat.sha256,
actualSha256,
});
}
return contentBuffer.toString("utf8");
}
const stat = await checkedRemoteV2(executor, "stat", [target]);
const [bytesText, expectedSha256] = stat.stdout.trim().split(/\s+/u);
const expectedBytes = Number(bytesText);
@@ -312,6 +368,10 @@ async function readRemoteText(executor: ApplyPatchV2Executor, target: string): P
async function writeRemoteText(executor: ApplyPatchV2Executor, target: string, content: string): Promise<void> {
const contentBuffer = Buffer.from(content, "utf8");
if (executor.fs) {
await executor.fs.writeFile(target, contentBuffer);
return;
}
const encoded = contentBuffer.toString("base64");
const expectedBytes = String(contentBuffer.length);
const expectedSha256 = sha256Hex(contentBuffer);
@@ -347,6 +407,9 @@ type RemoteV2Operation =
| "move";
async function checkedRemoteV2(executor: ApplyPatchV2Executor, operation: RemoteV2Operation, args: string[], input?: string): Promise<{ stdout: string }> {
if (!executor.run) {
throw new ApplyPatchV2Error("remote apply-patch v2 executor does not provide a command runner", { operation, args });
}
const result = await executor.run(remoteV2Script(operation, args), input);
if (result.exitCode === 0) return result;
throw new ApplyPatchV2Error("remote apply-patch v2 operation failed", {