* Write file contents atomically via exec. * Uses temp file + mv for atomic write.
(filePath: string, abortSignal?: AbortSignal)
| 389 | * Uses temp file + mv for atomic write. |
| 390 | */ |
| 391 | writeFile(filePath: string, abortSignal?: AbortSignal): WritableStream<Uint8Array> { |
| 392 | const quotedPath = this.quoteForRemote(filePath); |
| 393 | const tempPath = getAtomicWriteTempPath(filePath); |
| 394 | const quotedTempPath = this.quoteForRemote(tempPath); |
| 395 | |
| 396 | // Build write command - subclasses can override buildWriteCommand for special handling |
| 397 | const writeCommand = this.buildWriteCommand(quotedPath, quotedTempPath); |
| 398 | |
| 399 | let execPromise: Promise<ExecStream> | null = null; |
| 400 | const writeAbortController = new AbortController(); |
| 401 | const abortWrite = () => writeAbortController.abort(); |
| 402 | if (abortSignal?.aborted) { |
| 403 | writeAbortController.abort(); |
| 404 | } else { |
| 405 | abortSignal?.addEventListener("abort", abortWrite, { once: true }); |
| 406 | } |
| 407 | const cleanupAbortForwarder = () => { |
| 408 | abortSignal?.removeEventListener("abort", abortWrite); |
| 409 | }; |
| 410 | |
| 411 | const getExecStream = () => { |
| 412 | execPromise ??= this.exec(writeCommand, { |
| 413 | cwd: this.getBasePath(), |
| 414 | timeout: 300, |
| 415 | abortSignal: writeAbortController.signal, |
| 416 | }); |
| 417 | return execPromise; |
| 418 | }; |
| 419 | |
| 420 | return new WritableStream<Uint8Array>({ |
| 421 | write: async (chunk: Uint8Array) => { |
| 422 | const stream = await getExecStream(); |
| 423 | const writer = stream.stdin.getWriter(); |
| 424 | try { |
| 425 | await writer.write(chunk); |
| 426 | } finally { |
| 427 | writer.releaseLock(); |
| 428 | } |
| 429 | }, |
| 430 | close: async () => { |
| 431 | try { |
| 432 | const stream = await getExecStream(); |
| 433 | await stream.stdin.close(); |
| 434 | const exitCode = await stream.exitCode; |
| 435 | |
| 436 | if (exitCode !== 0) { |
| 437 | const stderr = await streamToString(stream.stderr); |
| 438 | throw new RuntimeError(`Failed to write file ${filePath}: ${stderr}`, "file_io"); |
| 439 | } |
| 440 | } finally { |
| 441 | cleanupAbortForwarder(); |
| 442 | } |
| 443 | }, |
| 444 | abort: async (reason?: unknown) => { |
| 445 | writeAbortController.abort(); |
| 446 | if (execPromise) { |
| 447 | try { |
| 448 | const stream = await execPromise; |