@@ -294,6 +294,13 @@ async function buildCmd(
294294 scaffold : opts . scaffold ,
295295 projectId : opts . projectId ,
296296 } ) ;
297+ if ( opts . projectId && started . continued !== true ) {
298+ await client . cancelBuild ( started . sessionId ) . catch ( ( ) => undefined ) ;
299+ throw new ProjectClientError (
300+ `web build did not confirm continuation of project ${ opts . projectId } ; the unexpected build was cancelled` ,
301+ 409 ,
302+ ) ;
303+ }
297304 handoffStore ?. save ( {
298305 sessionId : started . sessionId ,
299306 projectId : started . projectId ,
@@ -314,7 +321,8 @@ async function buildCmd(
314321
315322 out ( "" ) ;
316323 out ( "waiting for build to finish..." ) ;
317- const status = await waitForBuild ( client , started . sessionId , opts . timeoutMs , opts . pollMs , sleep ) ;
324+ const reportProgress = createBuildProgressReporter ( out ) ;
325+ const status = await waitForBuild ( client , started . sessionId , opts . timeoutMs , opts . pollMs , sleep , reportProgress ) ;
318326 handoffStore ?. update ( { sessionId : started . sessionId , status : status . status , model : status . model } ) ;
319327 printBuildStatus ( status , out ) ;
320328 if ( status . status === "completed" ) {
@@ -331,6 +339,7 @@ async function waitForBuild(
331339 timeoutMs : number ,
332340 pollMs : number ,
333341 sleep : ( ms : number ) => Promise < void > ,
342+ onProgress ?: ( status : BuildStatusResponse ) => void ,
334343) : Promise < BuildStatusResponse > {
335344 const deadline = Date . now ( ) + timeoutMs ;
336345 let last : BuildStatusResponse | undefined ;
@@ -346,6 +355,7 @@ async function waitForBuild(
346355 }
347356 throw err ;
348357 }
358+ onProgress ?.( last ) ;
349359 if ( last . status !== "building" ) return last ;
350360 const remaining = deadline - Date . now ( ) ;
351361 if ( remaining <= 0 ) break ;
@@ -356,6 +366,35 @@ async function waitForBuild(
356366 ) ;
357367}
358368
369+ function createBuildProgressReporter ( out : ( msg : string ) => void ) : ( status : BuildStatusResponse ) => void {
370+ const seenFiles = new Set < string > ( ) ;
371+ let seenTimelineItems = 0 ;
372+ return ( status ) => {
373+ let emitted = false ;
374+ for ( const file of uniqueStrings ( status . filesCreated ) ) {
375+ if ( seenFiles . has ( file ) ) continue ;
376+ seenFiles . add ( file ) ;
377+ out ( ` wrote: ${ file } ` ) ;
378+ emitted = true ;
379+ }
380+
381+ const timeline = status . timeline ?? [ ] ;
382+ for ( const item of timeline . slice ( seenTimelineItems ) ) {
383+ const summary = formatTimelineItem ( item ) ;
384+ if ( ! summary ) continue ;
385+ out ( ` phase: ${ summary } ` ) ;
386+ emitted = true ;
387+ }
388+ seenTimelineItems = Math . max ( seenTimelineItems , timeline . length ) ;
389+
390+ if ( ! emitted && status . status === "building" ) {
391+ out (
392+ ` still building (${ seenFiles . size } file${ seenFiles . size === 1 ? "" : "s" } , ${ timeline . length } phase${ timeline . length === 1 ? "" : "s" } )` ,
393+ ) ;
394+ }
395+ } ;
396+ }
397+
359398async function statusCmd (
360399 client : ProjectClient ,
361400 sessionId : string | undefined ,
@@ -435,9 +474,47 @@ function printBuildStatus(status: BuildStatusResponse, out: (msg: string) => voi
435474 out ( `build ${ status . sessionId } : ${ status . status } ` ) ;
436475 if ( status . projectId ) out ( ` project: ${ status . projectId } ` ) ;
437476 if ( status . model ) out ( ` model: ${ status . model } ` ) ;
438- if ( status . filesCreated ?. length ) out ( ` files: ${ status . filesCreated . join ( ", " ) } ` ) ;
439- if ( status . timeline ?. length )
440- out ( ` events: ${ status . timeline . length } timeline item${ status . timeline . length === 1 ? "" : "s" } ` ) ;
477+ const files = uniqueStrings ( status . filesCreated ) ;
478+ if ( files . length ) out ( ` files: ${ files . join ( ", " ) } ` ) ;
479+ for ( const item of status . timeline ?? [ ] ) {
480+ const summary = formatTimelineItem ( item ) ;
481+ if ( summary ) out ( ` phase: ${ summary } ` ) ;
482+ }
483+ }
484+
485+ function uniqueStrings ( values : string [ ] | undefined ) : string [ ] {
486+ return [
487+ ...new Set (
488+ ( values ?? [ ] ) . filter ( ( value ) => typeof value === "string" && value . trim ( ) ) . map ( ( value ) => value . trim ( ) ) ,
489+ ) ,
490+ ] ;
491+ }
492+
493+ function formatTimelineItem ( value : unknown ) : string | undefined {
494+ if ( ! value || typeof value !== "object" ) return undefined ;
495+ const item = value as Record < string , unknown > ;
496+ if ( typeof item . phase !== "string" || ! item . phase . trim ( ) ) return undefined ;
497+ const parts = [ cleanTimelineText ( item . phase ) ] ;
498+ if ( typeof item . durationMs === "number" && Number . isFinite ( item . durationMs ) && item . durationMs > 0 ) {
499+ parts . push ( formatTimelineDuration ( item . durationMs ) ) ;
500+ }
501+ if ( typeof item . skippedReason === "string" && item . skippedReason . trim ( ) ) {
502+ parts . push ( `[skipped: ${ cleanTimelineText ( item . skippedReason ) } ]` ) ;
503+ } else if ( item . success === false ) {
504+ parts . push ( "[failed]" ) ;
505+ } else if ( item . success === true ) {
506+ parts . push ( "[ok]" ) ;
507+ }
508+ return parts . join ( " " ) ;
509+ }
510+
511+ function cleanTimelineText ( value : string ) : string {
512+ return value . replace ( / \s + / g, " " ) . trim ( ) . slice ( 0 , 100 ) ;
513+ }
514+
515+ function formatTimelineDuration ( durationMs : number ) : string {
516+ if ( durationMs < 1000 ) return `${ Math . round ( durationMs ) } ms` ;
517+ return `${ ( durationMs / 1000 ) . toFixed ( durationMs < 10_000 ? 1 : 0 ) } s` ;
441518}
442519
443520async function printPreview (
0 commit comments