@@ -99,6 +99,7 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
9999 private updateServer : string | null = null ;
100100 private updateKey : string | null = null ;
101101 private socketFallbackWarned = false ;
102+ private otherAppUrls : string [ ] = [ ] ;
102103
103104 private flowAnimationFrame = 0 ;
104105 private flowAnimationTimer : NodeJS . Timeout | null = null ;
@@ -141,6 +142,23 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
141142 ) ;
142143 }
143144
145+ // Validate other-apps count and extensions
146+ const otherApps = this . options . otherApps ;
147+ if ( otherApps . length > 4 ) {
148+ throw new TestingBotError (
149+ `Too many other apps (${ otherApps . length } ). Maximum is 4.` ,
150+ ) ;
151+ }
152+ for ( const otherAppPath of otherApps ) {
153+ const otherExt = path . extname ( otherAppPath ) . toLowerCase ( ) ;
154+ if ( ! Maestro . SUPPORTED_APP_EXTENSIONS . includes ( otherExt ) ) {
155+ throw new TestingBotError (
156+ `Unsupported other-app file format: ${ otherExt || '(no extension)' } for ${ otherAppPath } . ` +
157+ `Supported formats: ${ Maestro . SUPPORTED_APP_EXTENSIONS . join ( ', ' ) } ` ,
158+ ) ;
159+ }
160+ }
161+
144162 // Build list of all file checks to run in parallel
145163 const fileChecks : Promise < void > [ ] = [
146164 fs . promises . access ( this . options . app , fs . constants . R_OK ) . catch ( ( ) => {
@@ -150,6 +168,16 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
150168 } ) ,
151169 ] ;
152170
171+ for ( const otherAppPath of otherApps ) {
172+ fileChecks . push (
173+ fs . promises . access ( otherAppPath , fs . constants . R_OK ) . catch ( ( ) => {
174+ throw new TestingBotError (
175+ `Provided other-app path does not exist ${ otherAppPath } ` ,
176+ ) ;
177+ } ) ,
178+ ) ;
179+ }
180+
153181 if ( this . options . configFile ) {
154182 fileChecks . push (
155183 fs . promises
@@ -221,6 +249,8 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
221249 // Process flows to show actual zip structure
222250 const flowResult = await this . collectFlows ( ) ;
223251
252+ const otherApps = this . options . otherApps ;
253+
224254 this . printDryRunSummary ( {
225255 provider : 'Maestro' ,
226256 apiUrl : this . URL ,
@@ -230,6 +260,11 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
230260 filePath : this . options . app ,
231261 endpoint : `${ this . URL } /app` ,
232262 } ,
263+ ...otherApps . map ( ( p , i ) => ( {
264+ label : `Other App ${ i + 1 } ` ,
265+ filePath : p ,
266+ endpoint : `${ this . URL } /other-apps` ,
267+ } ) ) ,
233268 {
234269 label : 'Flows' ,
235270 filePath : this . options . flows . join ( ', ' ) ,
@@ -243,6 +278,11 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
243278 shardSplit : this . options . shardSplit ,
244279 } ) ,
245280 ...( metadata && { metadata } ) ,
281+ ...( otherApps . length > 0 && {
282+ otherApps : otherApps . map (
283+ ( _ , i ) => `<tb://appkey-other-app-${ i + 1 } >` ,
284+ ) ,
285+ } ) ,
246286 } ,
247287 } ) ;
248288
@@ -279,6 +319,11 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
279319 setTitle ( 'maestro · uploading app' ) ;
280320 await this . uploadApp ( ) ;
281321
322+ if ( this . options . otherApps . length > 0 ) {
323+ setTitle ( 'maestro · uploading other apps' ) ;
324+ await this . uploadOtherApps ( ) ;
325+ }
326+
282327 if ( ! this . options . quiet ) {
283328 logger . info ( 'Uploading Maestro Flows' ) ;
284329 }
@@ -422,6 +467,48 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
422467 }
423468 }
424469
470+ private async uploadOtherApps ( ) : Promise < void > {
471+ const others = this . options . otherApps ;
472+ if ( ! others || others . length === 0 ) return ;
473+
474+ if ( ! this . options . quiet ) {
475+ logger . info ( `Uploading ${ others . length } other app(s)` ) ;
476+ }
477+
478+ for ( let i = 0 ; i < others . length ; i ++ ) {
479+ const appPath = others [ i ] ;
480+ const ext = path . extname ( appPath ) . toLowerCase ( ) ;
481+ const contentType =
482+ ext === '.apk'
483+ ? 'application/vnd.android.package-archive'
484+ : ext === '.ipa'
485+ ? 'application/octet-stream'
486+ : ext === '.zip' || ext === '.app'
487+ ? 'application/zip'
488+ : 'application/octet-stream' ;
489+
490+ if ( ! this . options . quiet ) {
491+ logger . info ( ` [${ i + 1 } /${ others . length } ] ${ path . basename ( appPath ) } ` ) ;
492+ }
493+
494+ const result = await this . upload . upload ( {
495+ filePath : appPath ,
496+ url : `${ this . URL } /other-apps` ,
497+ credentials : this . credentials ,
498+ contentType,
499+ showProgress : ! this . options . quiet ,
500+ validateZipFormat : ext === '.zip' || ext === '.app' ,
501+ } ) ;
502+
503+ if ( ! result . app_url ) {
504+ throw new TestingBotError (
505+ `Other-app upload returned no app_url for ${ appPath } ` ,
506+ ) ;
507+ }
508+ this . otherAppUrls . push ( result . app_url ) ;
509+ }
510+ }
511+
425512 /**
426513 * Zip a .app bundle directory into a temporary zip file
427514 */
@@ -1583,6 +1670,9 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
15831670 shardSplit : this . options . shardSplit ,
15841671 } ) ,
15851672 ...( metadata && { metadata } ) ,
1673+ ...( this . otherAppUrls . length > 0 && {
1674+ otherApps : this . otherAppUrls ,
1675+ } ) ,
15861676 } ,
15871677 {
15881678 headers : {
@@ -2696,11 +2786,7 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
26962786 if ( flow . report ) {
26972787 const flowReportPath = path . join ( flowDir , 'report.xml' ) ;
26982788 try {
2699- await fs . promises . writeFile (
2700- flowReportPath ,
2701- flow . report ,
2702- 'utf-8' ,
2703- ) ;
2789+ await fs . promises . writeFile ( flowReportPath , flow . report , 'utf-8' ) ;
27042790 if ( ! this . options . quiet ) {
27052791 logger . info ( ` Saved ${ flowDirName } /report.xml` ) ;
27062792 }
0 commit comments