66import * as fs from 'node:fs' ;
77import * as path from 'node:path' ;
88import { logger } from '@elizaos/core' ;
9- import { execa } from 'execa ' ;
9+ import { bunExec } from '../../../utils/bun-exec ' ;
1010import crypto from 'node:crypto' ;
1111import ora from 'ora' ;
1212
@@ -49,9 +49,10 @@ export interface DockerPushResult {
4949 */
5050export async function checkDockerAvailable ( ) : Promise < boolean > {
5151 try {
52- await execa ( 'docker' , [ '--version' ] ) ;
53- await execa ( 'docker' , [ 'info' ] ) ;
54- return true ;
52+ const versionResult = await bunExec ( 'docker' , [ '--version' ] ) ;
53+ if ( ! versionResult . success ) return false ;
54+ const infoResult = await bunExec ( 'docker' , [ 'info' ] ) ;
55+ return infoResult . success ;
5556 } catch {
5657 return false ;
5758 }
@@ -196,13 +197,22 @@ export async function buildDockerImage(options: DockerBuildOptions): Promise<Doc
196197
197198 // Execute Docker build
198199 const startTime = Date . now ( ) ;
199- const { stdout } = await execa ( 'docker' , buildArgs , {
200+ const buildResult = await bunExec ( 'docker' , buildArgs , {
200201 env : {
201- ...process . env ,
202202 DOCKER_DEFAULT_PLATFORM : platform ,
203203 DOCKER_BUILDKIT : '1' ,
204204 } ,
205205 } ) ;
206+
207+ if ( ! buildResult . success ) {
208+ return {
209+ success : false ,
210+ imageTag : options . imageTag ,
211+ error : buildResult . stderr || 'Docker build failed' ,
212+ } ;
213+ }
214+
215+ const stdout = buildResult . stdout ;
206216 const buildTime = Date . now ( ) - startTime ;
207217
208218 logger . debug (
@@ -216,12 +226,20 @@ export async function buildDockerImage(options: DockerBuildOptions): Promise<Doc
216226 }
217227
218228 // Get image info
219- const inspectResult = await execa ( 'docker' , [
229+ const inspectResult = await bunExec ( 'docker' , [
220230 'inspect' ,
221231 options . imageTag ,
222232 '--format={{.Id}}|{{.Size}}' ,
223233 ] ) ;
224234
235+ if ( ! inspectResult . success ) {
236+ return {
237+ success : false ,
238+ imageTag : options . imageTag ,
239+ error : inspectResult . stderr || 'Failed to inspect Docker image' ,
240+ } ;
241+ }
242+
225243 const [ imageId , sizeStr ] = inspectResult . stdout . split ( '|' ) ;
226244 const size = parseInt ( sizeStr , 10 ) ;
227245
@@ -272,11 +290,26 @@ async function loginToECR(registryUrl: string, authToken: string): Promise<void>
272290 'Logging in to ECR registry'
273291 ) ;
274292
275- // Docker login
276- await execa ( 'docker' , [ 'login' , '--username' , username , '--password-stdin' , cleanRegistryUrl ] , {
277- input : password ,
293+ // Docker login - use Bun.spawn directly for stdin input
294+ const proc = Bun . spawn ( [ 'docker' , 'login' , '--username' , username , '--password-stdin' , cleanRegistryUrl ] , {
295+ stdin : 'pipe' ,
296+ stdout : 'pipe' ,
297+ stderr : 'pipe' ,
278298 } ) ;
279299
300+ // Write password to stdin using FileSink API
301+ if ( proc . stdin ) {
302+ proc . stdin . write ( password ) ;
303+ proc . stdin . end ( ) ;
304+ }
305+
306+ const exitCode = await proc . exited ;
307+
308+ if ( exitCode !== 0 ) {
309+ const stderr = await new Response ( proc . stderr ) . text ( ) ;
310+ throw new Error ( `Docker login failed: ${ stderr } ` ) ;
311+ }
312+
280313 logger . info ( { src : 'cli' , util : 'docker-build' } , 'Logged in to ECR' ) ;
281314}
282315
@@ -286,7 +319,11 @@ async function loginToECR(registryUrl: string, authToken: string): Promise<void>
286319async function tagImageForECR ( localTag : string , ecrImageUri : string ) : Promise < void > {
287320 logger . info ( { src : 'cli' , util : 'docker-build' , ecrImageUri } , 'Tagging image for ECR' ) ;
288321
289- await execa ( 'docker' , [ 'tag' , localTag , ecrImageUri ] ) ;
322+ const result = await bunExec ( 'docker' , [ 'tag' , localTag , ecrImageUri ] ) ;
323+
324+ if ( ! result . success ) {
325+ throw new Error ( `Failed to tag image: ${ result . stderr } ` ) ;
326+ }
290327
291328 logger . debug ( { src : 'cli' , util : 'docker-build' , localTag, ecrImageUri } , 'Image tagged for ECR' ) ;
292329}
@@ -338,66 +375,90 @@ export async function pushDockerImage(options: DockerPushOptions): Promise<Docke
338375 let completedLayers = 0 ;
339376 const layerProgress = new Map < string , { current : number ; total : number } > ( ) ;
340377
341- const pushProcess = execa ( 'docker' , [ 'push' , ecrImageUri ] ) ;
378+ // Use Bun.spawn for the push process with streaming stderr
379+ const pushProcess = Bun . spawn ( [ 'docker' , 'push' , ecrImageUri ] , {
380+ stdout : 'pipe' ,
381+ stderr : 'pipe' ,
382+ } ) ;
342383
343- // Track progress from stderr (Docker outputs progress to stderr)
344- if ( pushProcess . stderr ) {
345- pushProcess . stderr . on ( 'data' , ( data : Buffer ) => {
346- const output = data . toString ( ) ;
384+ // Process stderr stream for progress tracking
385+ const processStderr = async ( ) => {
386+ if ( ! pushProcess . stderr ) return ;
347387
348- // Parse Docker layer progress
349- // Format: "layer-id: Pushing [==> ] 15.5MB/100MB"
350- const lines = output . split ( '\n' ) ;
388+ const reader = pushProcess . stderr . getReader ( ) ;
389+ const decoder = new TextDecoder ( ) ;
351390
352- for ( const line of lines ) {
353- const layerMatch = line . match (
354- / ^ ( [ a - f 0 - 9 ] + ) : \s * ( \w + ) \s * \[ ( [ = > ] + ) \s * \] \s + ( [ \d . ] + ) ( [ K M G T ] ? B ) \/ ( [ \d . ] + ) ( [ K M G T ] ? B ) /
355- ) ;
391+ try {
392+ while ( true ) {
393+ const { done , value } = await reader . read ( ) ;
394+ if ( done ) break ;
356395
357- if ( layerMatch ) {
358- const [ , layerId , , , currentStr , currentUnit , totalStr , totalUnit ] = layerMatch ;
396+ const output = decoder . decode ( value , { stream : true } ) ;
359397
360- // Convert to bytes for accurate progress
361- const current = parseSize ( currentStr , currentUnit ) ;
362- const total = parseSize ( totalStr , totalUnit ) ;
398+ // Parse Docker layer progress
399+ // Format: "layer-id: Pushing [==> ] 15.5MB/100MB"
400+ const lines = output . split ( '\n' ) ;
363401
364- layerProgress . set ( layerId , { current, total } ) ;
402+ for ( const line of lines ) {
403+ const layerMatch = line . match (
404+ / ^ ( [ a - f 0 - 9 ] + ) : \s * ( \w + ) \s * \[ ( [ = > ] + ) \s * \] \s + ( [ \d . ] + ) ( [ K M G T ] ? B ) \/ ( [ \d . ] + ) ( [ K M G T ] ? B ) /
405+ ) ;
365406
366- // Calculate overall progress
367- let totalBytes = 0 ;
368- let uploadedBytes = 0 ;
407+ if ( layerMatch ) {
408+ const [ , layerId , , , currentStr , currentUnit , totalStr , totalUnit ] = layerMatch ;
369409
370- for ( const [ , progress ] of layerProgress ) {
371- totalBytes += progress . total ;
372- uploadedBytes += progress . current ;
373- }
410+ // Convert to bytes for accurate progress
411+ const current = parseSize ( currentStr , currentUnit ) ;
412+ const total = parseSize ( totalStr , totalUnit ) ;
374413
375- const overallPercent =
376- totalBytes > 0 ? Math . floor ( ( uploadedBytes / totalBytes ) * 100 ) : 0 ;
377- const uploadedMB = ( uploadedBytes / 1024 / 1024 ) . toFixed ( 1 ) ;
378- const totalMB = ( totalBytes / 1024 / 1024 ) . toFixed ( 1 ) ;
414+ layerProgress . set ( layerId , { current, total } ) ;
379415
380- spinner . text = `Pushing to ECR... ${ overallPercent } % (${ uploadedMB } /${ totalMB } MB, ${ layerProgress . size } layers)` ;
381- }
416+ // Calculate overall progress
417+ let totalBytes = 0 ;
418+ let uploadedBytes = 0 ;
382419
383- // Check for pushed layers
384- if ( line . includes ( ': Pushed' ) ) {
385- completedLayers ++ ;
386- }
420+ for ( const [ , progress ] of layerProgress ) {
421+ totalBytes += progress . total ;
422+ uploadedBytes += progress . current ;
423+ }
424+
425+ const overallPercent =
426+ totalBytes > 0 ? Math . floor ( ( uploadedBytes / totalBytes ) * 100 ) : 0 ;
427+ const uploadedMB = ( uploadedBytes / 1024 / 1024 ) . toFixed ( 1 ) ;
428+ const totalMB = ( totalBytes / 1024 / 1024 ) . toFixed ( 1 ) ;
387429
388- // Check for completion digest
389- const digestMatch = line . match ( / d i g e s t : ( s h a 2 5 6 : [ a - f 0 - 9 ] + ) / ) ;
390- if ( digestMatch ) {
391- imageDigest = digestMatch [ 1 ] ;
430+ spinner . text = `Pushing to ECR... ${ overallPercent } % (${ uploadedMB } /${ totalMB } MB, ${ layerProgress . size } layers)` ;
431+ }
432+
433+ // Check for pushed layers
434+ if ( line . includes ( ': Pushed' ) ) {
435+ completedLayers ++ ;
436+ }
437+
438+ // Check for completion digest
439+ const digestMatch = line . match ( / d i g e s t : ( s h a 2 5 6 : [ a - f 0 - 9 ] + ) / ) ;
440+ if ( digestMatch ) {
441+ imageDigest = digestMatch [ 1 ] ;
442+ }
392443 }
393444 }
394- } ) ;
395- }
445+ } catch {
446+ // Ignore stream errors during processing
447+ }
448+ } ;
396449
397450 try {
398- await pushProcess ;
451+ // Process stderr in parallel with waiting for exit
452+ await Promise . all ( [ processStderr ( ) , pushProcess . exited ] ) ;
453+
454+ const exitCode = pushProcess . exitCode ;
399455 const pushTime = Date . now ( ) - startTime ;
400456
457+ if ( exitCode !== 0 ) {
458+ spinner . fail ( 'Failed to push image to ECR' ) ;
459+ throw new Error ( 'Docker push failed' ) ;
460+ }
461+
401462 spinner . succeed (
402463 `Image pushed in ${ ( pushTime / 1000 ) . toFixed ( 1 ) } s (${ completedLayers } layers)`
403464 ) ;
@@ -477,8 +538,15 @@ export async function cleanupLocalImages(imageTags: string[]): Promise<void> {
477538 ) ;
478539
479540 try {
480- await execa ( 'docker' , [ 'rmi' , ...imageTags , '--force' ] ) ;
481- logger . info ( { src : 'cli' , util : 'docker-build' } , 'Local images cleaned up' ) ;
541+ const result = await bunExec ( 'docker' , [ 'rmi' , ...imageTags , '--force' ] ) ;
542+ if ( result . success ) {
543+ logger . info ( { src : 'cli' , util : 'docker-build' } , 'Local images cleaned up' ) ;
544+ } else {
545+ logger . warn (
546+ { src : 'cli' , util : 'docker-build' , error : result . stderr } ,
547+ 'Failed to clean up some images'
548+ ) ;
549+ }
482550 } catch ( error ) {
483551 const errorMessage = error instanceof Error ? error . message : String ( error ) ;
484552 logger . warn (
0 commit comments