@zgeoff/atc 2.26.2 → 2.26.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zgeoff/atc",
3
- "version": "2.26.2",
3
+ "version": "2.26.4",
4
4
  "description": "Terminal control tower for coding-agent sessions",
5
5
  "homepage": "https://github.com/zgeoff/atc#readme",
6
6
  "bugs": "https://github.com/zgeoff/atc/issues",
@@ -248,16 +248,8 @@ export class ImpClientPort implements ImpPort {
248
248
  };
249
249
 
250
250
  // oxlint-disable-next-line prefer-readonly-parameter-types -- the command's input bytes have no readonly form
251
- readonly runCommand = async (name: string, command: ImpCommand): Promise<ImpCommandResult> => {
252
- const result = await this.tryCall((client) =>
253
- client.run(name, command.argv, {
254
- ...(command.cwd === undefined ? {} : { cwd: command.cwd }),
255
- ...(command.stdin === undefined ? {} : { stdin: command.stdin }),
256
- }),
257
- );
258
-
259
- return { code: result.code, stdout: result.stdout, stderr: result.stderr };
260
- };
251
+ readonly runCommand = (name: string, command: ImpCommand): Promise<ImpCommandResult> =>
252
+ this.tryCall((client) => runCommandInChunks(client, name, command));
261
253
 
262
254
  readonly openReverseForward = (
263
255
  name: string,
@@ -425,6 +417,51 @@ async function waitForSessionOutcome(outcome: Promise<ExecOutcome>): Promise<Imp
425
417
  return ended;
426
418
  }
427
419
 
420
+ // impd takes no WebSocket message over its size limit and closes the exec
421
+ // when one arrives, so a command's input goes over in pieces well below it.
422
+ const STDIN_CHUNK_BYTES = 1024 * 1024;
423
+
424
+ // Runs a command with its input sent a piece at a time. A command that ends
425
+ // before it takes all its input answers with its own exit, or with the
426
+ // failure that ended it, never with the refused write.
427
+ async function runCommandInChunks(
428
+ client: ImpClient,
429
+ name: string,
430
+
431
+ // oxlint-disable-next-line prefer-readonly-parameter-types -- the command's input bytes have no readonly form
432
+ command: ImpCommand,
433
+ ): Promise<ImpCommandResult> {
434
+ const options = command.cwd === undefined ? {} : { cwd: command.cwd };
435
+
436
+ const handle = await client.openExec(name, command.argv, options);
437
+
438
+ const output = Promise.all([
439
+ new Response(handle.stdout).bytes(),
440
+ new Response(handle.stderr).bytes(),
441
+ ]);
442
+
443
+ try {
444
+ const stdin = command.stdin ?? new Uint8Array(0);
445
+
446
+ for (let at = 0; at < stdin.byteLength; at += STDIN_CHUNK_BYTES) {
447
+ await handle.write(stdin.subarray(at, at + STDIN_CHUNK_BYTES));
448
+ }
449
+
450
+ await handle.closeStdin();
451
+ } catch {
452
+ // A write fails once the exec has ended, and the exit holds why: the
453
+ // command's own exit, or the failure that ended it, which rethrows
454
+ // here. Closing first ends an exec the failed write left running.
455
+ handle.close();
456
+
457
+ await handle.exit;
458
+ }
459
+
460
+ const [exit, [stdout, stderr]] = await Promise.all([handle.exit, output]);
461
+
462
+ return { code: exit.code, stdout, stderr };
463
+ }
464
+
428
465
  // oxlint-disable-next-line prefer-readonly-parameter-types -- a promise is a live handle
429
466
  async function waitForListening(listening: Promise<unknown>): Promise<void> {
430
467
  await listening;