@@ -112,7 +112,7 @@ export default class Maestro {
112112 throw new TestingBotError ( `flows option is required` ) ;
113113 }
114114
115- // Check if all flows paths exist (can be files, directories, or glob patterns)
115+ // Check if all flows paths exist (can be files, directories or glob patterns)
116116 for ( const flowsPath of this . options . flows ) {
117117 const isGlobPattern =
118118 flowsPath . includes ( '*' ) ||
@@ -128,9 +128,6 @@ export default class Maestro {
128128 }
129129 }
130130
131- // Device is optional - will be inferred from app file type if not provided
132-
133- // Validate report options
134131 if ( this . options . report && ! this . options . reportOutputDir ) {
135132 throw new TestingBotError (
136133 `--report-output-dir is required when --report is specified` ,
@@ -141,7 +138,6 @@ export default class Maestro {
141138 await this . ensureOutputDirectory ( this . options . reportOutputDir ) ;
142139 }
143140
144- // Validate artifact download options - output dir defaults to current directory
145141 if ( this . options . downloadArtifacts && this . options . artifactsOutputDir ) {
146142 await this . ensureOutputDirectory ( this . options . artifactsOutputDir ) ;
147143 }
@@ -159,7 +155,6 @@ export default class Maestro {
159155 }
160156 } catch ( error ) {
161157 if ( ( error as NodeJS . ErrnoException ) . code === 'ENOENT' ) {
162- // Directory doesn't exist, try to create it
163158 try {
164159 await fs . promises . mkdir ( dirPath , { recursive : true } ) ;
165160 } catch ( mkdirError ) {
@@ -221,7 +216,6 @@ export default class Maestro {
221216 return { success : true , runs : [ ] } ;
222217 }
223218
224- // Set up signal handlers before waiting for completion
225219 this . setupSignalHandlers ( ) ;
226220
227221 // Connect to real-time update server (unless --quiet is specified)
@@ -272,7 +266,6 @@ export default class Maestro {
272266 contentType = 'application/octet-stream' ;
273267 }
274268
275- // Check if app already exists (unless checksum check is disabled)
276269 if ( ! this . options . ignoreChecksumCheck ) {
277270 const checksum = await this . upload . calculateChecksum ( appPath ) ;
278271 const existingApp = await this . checkAppChecksum ( checksum ) ;
@@ -290,7 +283,6 @@ export default class Maestro {
290283 logger . info ( 'Uploading Maestro App' ) ;
291284 }
292285
293- // App doesn't exist (or checksum check skipped), upload it
294286 const result = await this . upload . upload ( {
295287 filePath : appPath ,
296288 url : `${ this . URL } /app` ,
@@ -323,7 +315,6 @@ export default class Maestro {
323315 } ,
324316 ) ;
325317
326- // Check for version update notification
327318 const latestVersion = response . headers ?. [ 'x-testingbotctl-version' ] ;
328319 utils . checkForUpdate ( latestVersion ) ;
329320
@@ -351,7 +342,6 @@ export default class Maestro {
351342 const stat = await fs . promises . stat ( singlePath ) . catch ( ( ) => null ) ;
352343 if ( stat ?. isFile ( ) && path . extname ( singlePath ) . toLowerCase ( ) === '.zip' ) {
353344 zipPath = singlePath ;
354- // Upload the zip directly without cleanup
355345 await this . upload . upload ( {
356346 filePath : zipPath ,
357347 url : `${ this . URL } /${ this . appId } /tests` ,
@@ -419,7 +409,6 @@ export default class Maestro {
419409 // If we have a single directory, use it as base; otherwise use common ancestor or flatten
420410 const baseDir = baseDirs . length === 1 ? baseDirs [ 0 ] : undefined ;
421411
422- // Log files being included in the zip
423412 if ( ! this . options . quiet ) {
424413 this . logIncludedFiles ( allFlowFiles , baseDir ) ;
425414 }
@@ -894,6 +883,7 @@ export default class Maestro {
894883 const startTime = Date . now ( ) ;
895884 const previousStatus : Map < number , MaestroRunInfo [ 'status' ] > = new Map ( ) ;
896885 const previousFlowStatus : Map < number , MaestroFlowStatus > = new Map ( ) ;
886+ const urlDisplayed : Set < number > = new Set ( ) ;
897887 let flowsTableDisplayed = false ;
898888 let displayedLineCount = 0 ;
899889
@@ -920,10 +910,22 @@ export default class Maestro {
920910 }
921911 }
922912
913+ // Show realtime URL once per run (before any in-place updates)
914+ for ( const run of status . runs ) {
915+ if ( ! urlDisplayed . has ( run . id ) ) {
916+ console . log (
917+ ` 🔗 Run ${ run . id } (${ run . capabilities . deviceName } ): Watch in realtime:` ,
918+ ) ;
919+ console . log (
920+ ` https://testingbot.com/members/maestro/${ this . appId } /runs/${ run . id } ` ,
921+ ) ;
922+ urlDisplayed . add ( run . id ) ;
923+ }
924+ }
925+
923926 if ( allFlows . length > 0 ) {
924927 if ( ! flowsTableDisplayed ) {
925928 // First time showing flows - display header and initial state
926- this . displayRunStatus ( status . runs , startTime , previousStatus ) ;
927929 console . log ( ) ; // Empty line before flows table
928930 this . displayFlowsTableHeader ( ) ;
929931 displayedLineCount = this . displayFlowsWithLimit ( allFlows , previousFlowStatus ) ;
@@ -969,12 +971,10 @@ export default class Maestro {
969971 }
970972 }
971973
972- // Fetch reports if requested
973974 if ( this . options . report && this . options . reportOutputDir ) {
974975 await this . fetchReports ( status . runs ) ;
975976 }
976977
977- // Download artifacts if requested
978978 if ( this . options . downloadArtifacts ) {
979979 await this . downloadArtifacts ( status . runs ) ;
980980 }
@@ -1015,16 +1015,6 @@ export default class Maestro {
10151015 this . clearLine ( ) ;
10161016 }
10171017
1018- // Show URL when test starts running (transitions from WAITING to READY)
1019- if ( statusChanged && prevStatus === 'WAITING' && run . status === 'READY' ) {
1020- console . log (
1021- ` 🚀 Run ${ run . id } (${ run . capabilities . deviceName } ): Test started` ,
1022- ) ;
1023- console . log (
1024- ` Watch this test in realtime: https://testingbot.com/members/maestro/${ this . appId } /runs/${ run . id } ` ,
1025- ) ;
1026- }
1027-
10281018 previousStatus . set ( run . id , run . status ) ;
10291019
10301020 const statusInfo = this . getStatusInfo ( run . status ) ;
@@ -1046,62 +1036,6 @@ export default class Maestro {
10461036 platformUtil . clearLine ( ) ;
10471037 }
10481038
1049- private displayFlowsProgress (
1050- flows : MaestroFlowInfo [ ] ,
1051- startTime : number ,
1052- isUpdate : boolean ,
1053- ) : void {
1054- const elapsedSeconds = Math . floor ( ( Date . now ( ) - startTime ) / 1000 ) ;
1055- const elapsedStr = this . formatElapsedTime ( elapsedSeconds ) ;
1056-
1057- // Count flows by status
1058- let waiting = 0 ;
1059- let running = 0 ;
1060- let passed = 0 ;
1061- let failed = 0 ;
1062-
1063- for ( const flow of flows ) {
1064- switch ( flow . status ) {
1065- case 'WAITING' :
1066- waiting ++ ;
1067- break ;
1068- case 'READY' :
1069- running ++ ;
1070- break ;
1071- case 'DONE' :
1072- if ( flow . success === 1 ) {
1073- passed ++ ;
1074- } else {
1075- failed ++ ;
1076- }
1077- break ;
1078- case 'FAILED' :
1079- failed ++ ;
1080- break ;
1081- }
1082- }
1083-
1084- const total = flows . length ;
1085- const completed = passed + failed ;
1086-
1087- // Build progress summary with colors
1088- const parts : string [ ] = [ ] ;
1089- if ( waiting > 0 ) parts . push ( colors . white ( `${ waiting } waiting` ) ) ;
1090- if ( running > 0 ) parts . push ( colors . blue ( `${ running } running` ) ) ;
1091- if ( passed > 0 ) parts . push ( colors . green ( `${ passed } passed` ) ) ;
1092- if ( failed > 0 ) parts . push ( colors . red ( `${ failed } failed` ) ) ;
1093-
1094- const progressBar = `[${ completed } /${ total } ]` ;
1095- const message = ` 🔄 Flows ${ progressBar } : ${ parts . join ( ' | ' ) } (${ elapsedStr } )` ;
1096-
1097- if ( isUpdate ) {
1098- // Clear current line and write new progress
1099- process . stdout . write ( `\r\x1b[K${ message } ` ) ;
1100- } else {
1101- process . stdout . write ( message ) ;
1102- }
1103- }
1104-
11051039 private formatElapsedTime ( seconds : number ) : string {
11061040 if ( seconds < 60 ) {
11071041 return `${ seconds } s` ;
@@ -1410,7 +1344,6 @@ export default class Maestro {
14101344 } ,
14111345 } ) ;
14121346
1413- // Check for version update notification
14141347 const latestVersion = response . headers ?. [ 'x-testingbotctl-version' ] ;
14151348 utils . checkForUpdate ( latestVersion ) ;
14161349
@@ -1575,7 +1508,6 @@ export default class Maestro {
15751508 const runDir = path . join ( tempDir , `run_${ run . id } ` ) ;
15761509 await fs . promises . mkdir ( runDir , { recursive : true } ) ;
15771510
1578- // Download logs
15791511 if (
15801512 runDetails . assets . logs &&
15811513 Object . keys ( runDetails . assets . logs ) . length > 0
@@ -1736,10 +1668,17 @@ export default class Maestro {
17361668 // Handle axios errors which have response.data
17371669 const axiosError = cause as {
17381670 response ?: {
1671+ status ?: number ;
17391672 data ?: { error ?: string ; errors ?: string [ ] ; message ?: string } ;
17401673 } ;
17411674 message ?: string ;
17421675 } ;
1676+
1677+ // Check for 429 status code (credits depleted)
1678+ if ( axiosError . response ?. status === 429 ) {
1679+ return 'Your TestingBot credits are depleted. Please upgrade your plan at https://testingbot.com/pricing' ;
1680+ }
1681+
17431682 if ( axiosError . response ?. data ?. errors ) {
17441683 return axiosError . response . data . errors . join ( '\n' ) ;
17451684 }
@@ -1750,12 +1689,10 @@ export default class Maestro {
17501689 return axiosError . response . data . message ;
17511690 }
17521691
1753- // Handle standard Error objects
17541692 if ( cause instanceof Error ) {
17551693 return cause . message ;
17561694 }
17571695
1758- // Handle plain objects with errors array, error, or message property
17591696 const obj = cause as {
17601697 errors ?: string [ ] ;
17611698 error ?: string ;
0 commit comments