@@ -68,7 +68,7 @@ export class SSHRuntime implements Runtime {
6868 /**
6969 * Execute command over SSH with streaming I/O
7070 */
71- exec ( command : string , options : ExecOptions ) : ExecStream {
71+ async exec ( command : string , options : ExecOptions ) : Promise < ExecStream > {
7272 const startTime = performance . now ( ) ;
7373
7474 // Build command parts
@@ -184,15 +184,15 @@ export class SSHRuntime implements Runtime {
184184 * Read file contents over SSH as a stream
185185 */
186186 readFile ( path : string ) : ReadableStream < Uint8Array > {
187- const stream = this . exec ( `cat ${ shescape . quote ( path ) } ` , {
188- cwd : this . config . workdir ,
189- timeout : 300 , // 5 minutes - reasonable for large files
190- } ) ;
191-
192- // Return stdout, but wrap to handle errors from exit code
187+ // Return stdout, but wrap to handle errors from exec() and exit code
193188 return new ReadableStream < Uint8Array > ( {
194- async start ( controller : ReadableStreamDefaultController < Uint8Array > ) {
189+ start : async ( controller : ReadableStreamDefaultController < Uint8Array > ) => {
195190 try {
191+ const stream = await this . exec ( `cat ${ shescape . quote ( path ) } ` , {
192+ cwd : this . config . workdir ,
193+ timeout : 300 , // 5 minutes - reasonable for large files
194+ } ) ;
195+
196196 const reader = stream . stdout . getReader ( ) ;
197197 const exitCode = stream . exitCode ;
198198
@@ -237,22 +237,32 @@ export class SSHRuntime implements Runtime {
237237 // Use shescape.quote for safe path escaping
238238 const writeCommand = `mkdir -p $(dirname ${ shescape . quote ( path ) } ) && cat > ${ shescape . quote ( tempPath ) } && chmod 600 ${ shescape . quote ( tempPath ) } && mv ${ shescape . quote ( tempPath ) } ${ shescape . quote ( path ) } ` ;
239239
240- const stream = this . exec ( writeCommand , {
241- cwd : this . config . workdir ,
242- timeout : 300 , // 5 minutes - reasonable for large files
243- } ) ;
240+ // Need to get the exec stream in async callbacks
241+ let execPromise : Promise < ExecStream > | null = null ;
242+
243+ const getExecStream = ( ) => {
244+ if ( ! execPromise ) {
245+ execPromise = this . exec ( writeCommand , {
246+ cwd : this . config . workdir ,
247+ timeout : 300 , // 5 minutes - reasonable for large files
248+ } ) ;
249+ }
250+ return execPromise ;
251+ } ;
244252
245253 // Wrap stdin to handle errors from exit code
246254 return new WritableStream < Uint8Array > ( {
247- async write ( chunk : Uint8Array ) {
255+ write : async ( chunk : Uint8Array ) => {
256+ const stream = await getExecStream ( ) ;
248257 const writer = stream . stdin . getWriter ( ) ;
249258 try {
250259 await writer . write ( chunk ) ;
251260 } finally {
252261 writer . releaseLock ( ) ;
253262 }
254263 } ,
255- async close ( ) {
264+ close : async ( ) => {
265+ const stream = await getExecStream ( ) ;
256266 // Close stdin and wait for command to complete
257267 await stream . stdin . close ( ) ;
258268 const exitCode = await stream . exitCode ;
@@ -262,7 +272,8 @@ export class SSHRuntime implements Runtime {
262272 throw new RuntimeErrorClass ( `Failed to write file ${ path } : ${ stderr } ` , "file_io" ) ;
263273 }
264274 } ,
265- async abort ( reason ?: unknown ) {
275+ abort : async ( reason ?: unknown ) => {
276+ const stream = await getExecStream ( ) ;
266277 await stream . stdin . abort ( ) ;
267278 throw new RuntimeErrorClass ( `Failed to write file ${ path } : ${ String ( reason ) } ` , "file_io" ) ;
268279 } ,
@@ -275,7 +286,7 @@ export class SSHRuntime implements Runtime {
275286 async stat ( path : string ) : Promise < FileStat > {
276287 // Use stat with format string to get: size, mtime, type
277288 // %s = size, %Y = mtime (seconds since epoch), %F = file type
278- const stream = this . exec ( `stat -c '%s %Y %F' ${ shescape . quote ( path ) } ` , {
289+ const stream = await this . exec ( `stat -c '%s %Y %F' ${ shescape . quote ( path ) } ` , {
279290 cwd : this . config . workdir ,
280291 timeout : 10 , // 10 seconds - stat should be fast
281292 } ) ;
@@ -397,7 +408,7 @@ export class SSHRuntime implements Runtime {
397408 // git doesn't expand tilde when it's quoted, so we need to expand it ourselves
398409 const cloneDestPath = expandTildeForSSH ( this . config . workdir ) ;
399410
400- const cloneStream = this . exec ( `git clone --quiet ${ bundleTempPath } ${ cloneDestPath } ` , {
411+ const cloneStream = await this . exec ( `git clone --quiet ${ bundleTempPath } ${ cloneDestPath } ` , {
401412 cwd : "~" ,
402413 timeout : 300 , // 5 minutes for clone
403414 } ) ;
@@ -414,7 +425,7 @@ export class SSHRuntime implements Runtime {
414425
415426 // Step 3: Remove bundle file
416427 initLogger . logStep ( `Cleaning up bundle file...` ) ;
417- const rmStream = this . exec ( `rm ${ bundleTempPath } ` , {
428+ const rmStream = await this . exec ( `rm ${ bundleTempPath } ` , {
418429 cwd : "~" ,
419430 timeout : 10 ,
420431 } ) ;
@@ -428,7 +439,7 @@ export class SSHRuntime implements Runtime {
428439 } catch ( error ) {
429440 // Try to clean up bundle file on error
430441 try {
431- const rmStream = this . exec ( `rm -f ${ bundleTempPath } ` , {
442+ const rmStream = await this . exec ( `rm -f ${ bundleTempPath } ` , {
432443 cwd : "~" ,
433444 timeout : 10 ,
434445 } ) ;
@@ -461,7 +472,7 @@ export class SSHRuntime implements Runtime {
461472
462473 // Run hook remotely and stream output
463474 // No timeout - user init hooks can be arbitrarily long
464- const hookStream = this . exec ( hookCommand , {
475+ const hookStream = await this . exec ( hookCommand , {
465476 cwd : this . config . workdir ,
466477 timeout : 3600 , // 1 hour - generous timeout for init hooks
467478 } ) ;
@@ -543,7 +554,7 @@ export class SSHRuntime implements Runtime {
543554 }
544555 }
545556
546- const mkdirStream = this . exec ( parentDirCommand , {
557+ const mkdirStream = await this . exec ( parentDirCommand , {
547558 cwd : "/tmp" ,
548559 timeout : 10 ,
549560 } ) ;
@@ -602,7 +613,7 @@ export class SSHRuntime implements Runtime {
602613 initLogger . logStep ( `Checking out branch: ${ branchName } ` ) ;
603614 const checkoutCmd = `(git checkout ${ JSON . stringify ( branchName ) } 2>/dev/null || git checkout -b ${ JSON . stringify ( branchName ) } HEAD)` ;
604615
605- const checkoutStream = this . exec ( checkoutCmd , {
616+ const checkoutStream = await this . exec ( checkoutCmd , {
606617 cwd : this . config . workdir ,
607618 timeout : 300 , // 5 minutes for git checkout (can be slow on large repos)
608619 } ) ;
0 commit comments