@@ -80,22 +80,23 @@ export default class Maestro {
8080 ) ;
8181 }
8282
83- if ( this . options . flows === undefined ) {
83+ if ( this . options . flows === undefined || this . options . flows . length === 0 ) {
8484 throw new TestingBotError ( `flows option is required` ) ;
8585 }
8686
87- // Check if flows path exists (can be a file, directory , or glob pattern )
88- const flowsPath = this . options . flows ;
89- const isGlobPattern =
90- flowsPath . includes ( '*' ) ||
91- flowsPath . includes ( '?' ) ||
92- flowsPath . includes ( '{' ) ;
87+ // Check if all flows paths exist (can be files, directories , or glob patterns )
88+ for ( const flowsPath of this . options . flows ) {
89+ const isGlobPattern =
90+ flowsPath . includes ( '*' ) ||
91+ flowsPath . includes ( '?' ) ||
92+ flowsPath . includes ( '{' ) ;
9393
94- if ( ! isGlobPattern ) {
95- try {
96- await fs . promises . access ( flowsPath , fs . constants . R_OK ) ;
97- } catch {
98- throw new TestingBotError ( `flows path does not exist ${ flowsPath } ` ) ;
94+ if ( ! isGlobPattern ) {
95+ try {
96+ await fs . promises . access ( flowsPath , fs . constants . R_OK ) ;
97+ } catch {
98+ throw new TestingBotError ( `flows path does not exist ${ flowsPath } ` ) ;
99+ }
99100 }
100101 }
101102
@@ -251,52 +252,87 @@ export default class Maestro {
251252 }
252253
253254 private async uploadFlows ( ) {
254- const flowsPath = this . options . flows ;
255- const stat = await fs . promises . stat ( flowsPath ) . catch ( ( ) => null ) ;
255+ const flowsPaths = this . options . flows ;
256256
257257 let zipPath : string ;
258258 let shouldCleanup = false ;
259259
260- if ( stat ?. isFile ( ) ) {
261- const ext = path . extname ( flowsPath ) . toLowerCase ( ) ;
262- if ( ext === '.zip' ) {
263- // Already a zip file, upload directly
264- zipPath = flowsPath ;
265- } else if ( ext === '.yaml' || ext === '.yml' ) {
266- // Single flow file, create a zip
267- zipPath = await this . createFlowsZip ( [ flowsPath ] ) ;
268- shouldCleanup = true ;
269- } else {
270- throw new TestingBotError (
271- `Invalid flow file format. Expected .yaml, .yml, or .zip, got ${ ext } ` ,
272- ) ;
273- }
274- } else if ( stat ?. isDirectory ( ) ) {
275- // Directory of flows
276- const flowFiles = await this . discoverFlows ( flowsPath ) ;
277- if ( flowFiles . length === 0 ) {
278- throw new TestingBotError (
279- `No flow files (.yaml, .yml) found in directory ${ flowsPath } ` ,
280- ) ;
260+ // Special case: single zip file - upload directly
261+ if ( flowsPaths . length === 1 ) {
262+ const singlePath = flowsPaths [ 0 ] ;
263+ const stat = await fs . promises . stat ( singlePath ) . catch ( ( ) => null ) ;
264+ if ( stat ?. isFile ( ) && path . extname ( singlePath ) . toLowerCase ( ) === '.zip' ) {
265+ zipPath = singlePath ;
266+ // Upload the zip directly without cleanup
267+ await this . upload . upload ( {
268+ filePath : zipPath ,
269+ url : `${ this . URL } /${ this . appId } /tests` ,
270+ credentials : this . credentials ,
271+ contentType : 'application/zip' ,
272+ showProgress : ! this . options . quiet ,
273+ } ) ;
274+ return true ;
281275 }
282- zipPath = await this . createFlowsZip ( flowFiles , flowsPath ) ;
283- shouldCleanup = true ;
284- } else {
285- // Treat as glob pattern
286- const flowFiles = await glob ( flowsPath ) ;
287- const yamlFiles = flowFiles . filter ( ( f ) => {
288- const ext = path . extname ( f ) . toLowerCase ( ) ;
289- return ext === '.yaml' || ext === '.yml' ;
290- } ) ;
291- if ( yamlFiles . length === 0 ) {
292- throw new TestingBotError (
293- `No flow files found matching pattern ${ flowsPath } ` ,
294- ) ;
276+ }
277+
278+ // Collect all flow files from all paths
279+ const allFlowFiles : string [ ] = [ ] ;
280+ const baseDirs : string [ ] = [ ] ;
281+
282+ for ( const flowsPath of flowsPaths ) {
283+ const stat = await fs . promises . stat ( flowsPath ) . catch ( ( ) => null ) ;
284+
285+ if ( stat ?. isFile ( ) ) {
286+ const ext = path . extname ( flowsPath ) . toLowerCase ( ) ;
287+ if ( ext === '.yaml' || ext === '.yml' ) {
288+ allFlowFiles . push ( flowsPath ) ;
289+ } else if ( ext === '.zip' ) {
290+ throw new TestingBotError (
291+ `Cannot combine .zip files with other flow paths. Use a single .zip file or provide directories/patterns.` ,
292+ ) ;
293+ } else {
294+ throw new TestingBotError (
295+ `Invalid flow file format. Expected .yaml, .yml, or .zip, got ${ ext } ` ,
296+ ) ;
297+ }
298+ } else if ( stat ?. isDirectory ( ) ) {
299+ // Directory of flows
300+ const flowFiles = await this . discoverFlows ( flowsPath ) ;
301+ if ( flowFiles . length === 0 && flowsPaths . length === 1 ) {
302+ throw new TestingBotError (
303+ `No flow files (.yaml, .yml) found in directory ${ flowsPath } ` ,
304+ ) ;
305+ }
306+ allFlowFiles . push ( ...flowFiles ) ;
307+ baseDirs . push ( flowsPath ) ;
308+ } else {
309+ // Treat as glob pattern
310+ const flowFiles = await glob ( flowsPath ) ;
311+ const yamlFiles = flowFiles . filter ( ( f ) => {
312+ const ext = path . extname ( f ) . toLowerCase ( ) ;
313+ return ext === '.yaml' || ext === '.yml' ;
314+ } ) ;
315+ if ( yamlFiles . length === 0 && flowsPaths . length === 1 ) {
316+ throw new TestingBotError (
317+ `No flow files found matching pattern ${ flowsPath } ` ,
318+ ) ;
319+ }
320+ allFlowFiles . push ( ...yamlFiles ) ;
295321 }
296- zipPath = await this . createFlowsZip ( yamlFiles ) ;
297- shouldCleanup = true ;
298322 }
299323
324+ if ( allFlowFiles . length === 0 ) {
325+ throw new TestingBotError (
326+ `No flow files (.yaml, .yml) found in the provided paths` ,
327+ ) ;
328+ }
329+
330+ // Determine base directory for zip structure
331+ // If we have a single directory, use it as base; otherwise use common ancestor or flatten
332+ const baseDir = baseDirs . length === 1 ? baseDirs [ 0 ] : undefined ;
333+ zipPath = await this . createFlowsZip ( allFlowFiles , baseDir ) ;
334+ shouldCleanup = true ;
335+
300336 try {
301337 await this . upload . upload ( {
302338 filePath : zipPath ,
0 commit comments