@@ -48,16 +48,43 @@ export default class Upload {
4848 time : 100 , // Emit progress every 100ms
4949 } ) ;
5050
51- let lastPercent = 0 ;
51+ const interactive = utils . isInteractive ( ) ;
52+ let lastPercent = - 1 ;
53+ let lastBucket = - 1 ;
54+ const BUCKET_SIZE = 10 ;
5255
5356 if ( showProgress ) {
54- this . drawProgressBar ( fileName , totalSize , 0 ) ;
57+ if ( interactive ) {
58+ this . drawProgressBar ( fileName , totalSize , 0 ) ;
59+ }
5560
5661 progressTracker . on ( 'progress' , ( prog ) => {
5762 const percent = Math . round ( prog . percentage ) ;
58- if ( percent !== lastPercent ) {
59- lastPercent = percent ;
60- this . drawProgressBar ( fileName , totalSize , percent ) ;
63+ if ( interactive ) {
64+ // Redraw whenever percent changes; speed/ETA still update because
65+ // progress-stream emits every 100ms.
66+ if ( percent !== lastPercent ) {
67+ lastPercent = percent ;
68+ this . drawProgressBar (
69+ fileName ,
70+ totalSize ,
71+ percent ,
72+ prog . speed ,
73+ prog . eta ,
74+ ) ;
75+ }
76+ } else {
77+ // Non-TTY: one line per 10% bucket, no ANSI, no \r. 100% is
78+ // reported by the success path.
79+ const bucket = Math . floor ( percent / BUCKET_SIZE ) ;
80+ if ( bucket !== lastBucket && percent < 100 ) {
81+ lastBucket = bucket ;
82+ const transferred = this . formatFileSize (
83+ ( percent / 100 ) * totalSize ,
84+ ) ;
85+ const total = this . formatFileSize ( totalSize ) ;
86+ console . log ( ` ${ fileName } : ${ percent } % (${ transferred } /${ total } )` ) ;
87+ }
6188 }
6289 } ) ;
6390 }
@@ -98,21 +125,35 @@ export default class Upload {
98125 const result = response . data ;
99126 if ( result . id ) {
100127 if ( showProgress ) {
101- this . drawProgressBar ( fileName , totalSize , 100 ) ;
102- console . log ( '' ) ;
128+ if ( interactive ) {
129+ this . drawProgressBar ( fileName , totalSize , 100 ) ;
130+ console . log ( '' ) ;
131+ } else {
132+ console . log (
133+ ` ${ fileName } : done (${ this . formatFileSize ( totalSize ) } )` ,
134+ ) ;
135+ }
103136 }
104137 return { id : result . id } ;
105138 } else {
106139 if ( showProgress ) {
107- console . log ( ' Failed' ) ;
140+ if ( interactive ) {
141+ console . log ( ' Failed' ) ;
142+ } else {
143+ console . log ( ` ${ fileName } : failed` ) ;
144+ }
108145 }
109146 throw new TestingBotError (
110147 `Upload failed: ${ result . error || 'Unknown error' } ` ,
111148 ) ;
112149 }
113150 } catch ( error ) {
114151 if ( showProgress ) {
115- console . log ( ' Failed' ) ;
152+ if ( interactive ) {
153+ console . log ( ' Failed' ) ;
154+ } else {
155+ console . log ( ` ${ fileName } : failed` ) ;
156+ }
116157 }
117158 if ( error instanceof TestingBotError ) {
118159 throw error ;
@@ -139,6 +180,8 @@ export default class Upload {
139180 fileName : string ,
140181 totalBytes : number ,
141182 percent : number ,
183+ bytesPerSec ?: number ,
184+ etaSeconds ?: number ,
142185 ) : void {
143186 const barWidth = 30 ;
144187 const filled = Math . round ( ( barWidth * percent ) / 100 ) ;
@@ -149,8 +192,24 @@ export default class Upload {
149192 const transferred = this . formatFileSize ( transferredBytes ) ;
150193 const total = this . formatFileSize ( totalBytes ) ;
151194
195+ let suffix = '' ;
196+ if (
197+ typeof bytesPerSec === 'number' &&
198+ Number . isFinite ( bytesPerSec ) &&
199+ bytesPerSec > 0
200+ ) {
201+ suffix += ` • ${ this . formatFileSize ( bytesPerSec ) } /s` ;
202+ }
203+ if (
204+ typeof etaSeconds === 'number' &&
205+ Number . isFinite ( etaSeconds ) &&
206+ etaSeconds > 0
207+ ) {
208+ suffix += ` • ETA ${ this . formatDuration ( etaSeconds ) } ` ;
209+ }
210+
152211 process . stdout . write (
153- `\r ${ fileName } : [${ bar } ] ${ percent } % (${ transferred } /${ total } )` ,
212+ `\r ${ fileName } : [${ bar } ] ${ percent } % (${ transferred } /${ total } )${ suffix } ` ,
154213 ) ;
155214 }
156215
@@ -159,14 +218,24 @@ export default class Upload {
159218 */
160219 private formatFileSize ( bytes : number ) : string {
161220 if ( bytes < 1024 ) {
162- return `${ bytes } B` ;
221+ return `${ Math . round ( bytes ) } B` ;
163222 } else if ( bytes < 1024 * 1024 ) {
164223 return `${ ( bytes / 1024 ) . toFixed ( 1 ) } KB` ;
165224 } else {
166225 return `${ ( bytes / ( 1024 * 1024 ) ) . toFixed ( 2 ) } MB` ;
167226 }
168227 }
169228
229+ /**
230+ * Format seconds as compact duration: "12s" or "3m04s".
231+ */
232+ private formatDuration ( seconds : number ) : string {
233+ if ( seconds < 60 ) return `${ Math . round ( seconds ) } s` ;
234+ const m = Math . floor ( seconds / 60 ) ;
235+ const s = Math . round ( seconds % 60 ) ;
236+ return `${ m } m${ s . toString ( ) . padStart ( 2 , '0' ) } s` ;
237+ }
238+
170239 private async validateFile ( filePath : string ) : Promise < void > {
171240 try {
172241 await fs . promises . access ( filePath , fs . constants . R_OK ) ;
0 commit comments