11import { Command } from 'commander' ;
22import logger from './logger' ;
3- import auth from './auth' ;
3+ import Auth from './auth' ;
44import Espresso from './providers/espresso' ;
55import EspressoOptions from './models/espresso_options' ;
66import XCUITestOptions from './models/xcuitest_options' ;
77import XCUITest from './providers/xcuitest' ;
88import packageJson from '../package.json' ;
9- import MaestroOptions from './models/maestro_options' ;
9+ import MaestroOptions , {
10+ Orientation ,
11+ ThrottleNetwork ,
12+ } from './models/maestro_options' ;
1013import Maestro from './providers/maestro' ;
1114
1215const program = new Command ( ) ;
@@ -24,9 +27,11 @@ program
2427 . requiredOption ( '--device <device>' , 'Real device to use for testing.' )
2528 . requiredOption (
2629 '--emulator <emulator>' ,
27- 'Android emulator to use for testing.' ,
30+ 'Android emulator/device to use for testing.' ,
2831 )
2932 . requiredOption ( '--test-app <string>' , 'Path to test application.' )
33+ . option ( '--api-key <key>' , 'TestingBot API key.' )
34+ . option ( '--api-secret <secret>' , 'TestingBot API secret.' )
3035 . action ( async ( args ) => {
3136 try {
3237 const options = new EspressoOptions (
@@ -35,9 +40,14 @@ program
3540 args . device ,
3641 args . emulator ,
3742 ) ;
38- const credentials = await auth . getCredentials ( ) ;
43+ const credentials = await Auth . getCredentials ( {
44+ apiKey : args . apiKey ,
45+ apiSecret : args . apiSecret ,
46+ } ) ;
3947 if ( credentials === null ) {
40- throw new Error ( 'Please specify credentials' ) ;
48+ throw new Error (
49+ 'Please specify credentials via --api-key/--api-secret, TB_KEY/TB_SECRET environment variables, or ~/.testingbot file' ,
50+ ) ;
4151 }
4252 const espresso = new Espresso ( credentials , options ) ;
4353 await espresso . run ( ) ;
@@ -51,24 +61,132 @@ program
5161
5262program
5363 . command ( 'maestro' )
54- . description ( 'Bootstrap a Maestro project.' )
55- . requiredOption ( '--app <string>' , 'Path to application under test.' )
56- . requiredOption (
64+ . description ( 'Run Maestro flows on TestingBot.' )
65+ . argument (
66+ '[appFile]' ,
67+ 'Path to application under test (.apk, .ipa, .app or .zip)' ,
68+ )
69+ . argument (
70+ '[flows]' ,
71+ 'Path to flow file (.yaml/.yml), directory, .zip or glob pattern' ,
72+ )
73+ // App and flows options
74+ . option (
75+ '--app <string>' ,
76+ 'Path to application under test (.apk, .ipa, .app, or .zip).' ,
77+ )
78+ . option (
79+ '--flows <string>' ,
80+ 'Path to flow file (.yaml/.yml), directory of flows, .zip file or glob pattern.' ,
81+ )
82+ // Device configuration
83+ . option (
5784 '--device <device>' ,
58- 'Android emulator or iOS Simulator to use for testing.' ,
85+ 'Device name to use for testing (e.g., "Pixel 8", "iPhone 15"). If not specified, uses "*" for any available device .' ,
5986 )
60- . requiredOption ( '--test-app <string>' , 'Path to test application.' )
61- . action ( async ( args ) => {
87+ . option (
88+ '--platform <platform>' ,
89+ 'Platform name: Android or iOS.' ,
90+ ( val ) => val as 'Android' | 'iOS' ,
91+ )
92+ . option ( '--version <version>' , 'OS version (e.g., "14", "17.2").' )
93+ . option (
94+ '--orientation <orientation>' ,
95+ 'Screen orientation: PORTRAIT or LANDSCAPE.' ,
96+ ( val ) => val . toUpperCase ( ) as Orientation ,
97+ )
98+ . option ( '--locale <locale>' , 'Device locale (e.g., "en_US", "de_DE").' )
99+ . option (
100+ '--timezone <timezone>' ,
101+ 'Device timezone (e.g., "America/New_York", "Europe/London").' ,
102+ )
103+ // Test metadata
104+ . option ( '--name <name>' , 'Test name for identification in dashboard.' )
105+ . option ( '--build <build>' , 'Build identifier for grouping test runs.' )
106+ // Network and geo
107+ . option (
108+ '--throttle-network <speed>' ,
109+ 'Network throttling: 4G, 3G, Edge, airplane, or disable.' ,
110+ ( val ) => val as ThrottleNetwork ,
111+ )
112+ . option (
113+ '--geo-country-code <code>' ,
114+ 'Geographic IP location (ISO country code, e.g., "US", "DE").' ,
115+ )
116+ // Flow filtering
117+ . option (
118+ '--include-tags <tags>' ,
119+ 'Only run flows with these tags (comma-separated).' ,
120+ ( val ) => val . split ( ',' ) . map ( ( t ) => t . trim ( ) ) ,
121+ )
122+ . option (
123+ '--exclude-tags <tags>' ,
124+ 'Exclude flows with these tags (comma-separated).' ,
125+ ( val ) => val . split ( ',' ) . map ( ( t ) => t . trim ( ) ) ,
126+ )
127+ // Environment variables
128+ . option (
129+ '-e, --env <KEY=VALUE>' ,
130+ 'Environment variable to pass to Maestro flows (can be used multiple times).' ,
131+ ( val : string , acc : string [ ] ) => {
132+ acc . push ( val ) ;
133+ return acc ;
134+ } ,
135+ [ ] as string [ ] ,
136+ )
137+ // Authentication
138+ . option ( '--api-key <key>' , 'TestingBot API key.' )
139+ . option ( '--api-secret <secret>' , 'TestingBot API secret.' )
140+ . action ( async ( appFileArg , flowsArg , args ) => {
62141 try {
63- const options = new MaestroOptions (
64- args . app ,
65- args . testApp ,
66- args . device ,
67- args . emulator ,
68- ) ;
69- const credentials = await auth . getCredentials ( ) ;
142+ // Positional arguments take precedence, fall back to options
143+ const app = appFileArg || args . app ;
144+ const flows = flowsArg || args . flows ;
145+
146+ if ( ! app ) {
147+ throw new Error (
148+ 'App file is required. Provide it as first argument or use --app option.' ,
149+ ) ;
150+ }
151+ if ( ! flows ) {
152+ throw new Error (
153+ 'Flows path is required. Provide it as second argument or use --flows option.' ,
154+ ) ;
155+ }
156+
157+ // Parse environment variables from -e KEY=VALUE format
158+ const env : Record < string , string > = { } ;
159+ for ( const envVar of args . env || [ ] ) {
160+ const eqIndex = envVar . indexOf ( '=' ) ;
161+ if ( eqIndex > 0 ) {
162+ const key = envVar . substring ( 0 , eqIndex ) ;
163+ const value = envVar . substring ( eqIndex + 1 ) ;
164+ env [ key ] = value ;
165+ }
166+ }
167+
168+ const options = new MaestroOptions ( app , flows , args . device , {
169+ includeTags : args . includeTags ,
170+ excludeTags : args . excludeTags ,
171+ platformName : args . platform ,
172+ version : args . version ,
173+ name : args . name ,
174+ build : args . build ,
175+ orientation : args . orientation ,
176+ locale : args . locale ,
177+ timeZone : args . timezone ,
178+ throttleNetwork : args . throttleNetwork ,
179+ geoCountryCode : args . geoCountryCode ,
180+ env : Object . keys ( env ) . length > 0 ? env : undefined ,
181+ } ) ;
182+ const credentials = await Auth . getCredentials ( {
183+ apiKey : args . apiKey ,
184+ apiSecret : args . apiSecret ,
185+ } ) ;
70186 if ( credentials === null ) {
71- throw new Error ( 'Please specify credentials' ) ;
187+ throw new Error (
188+ 'Please specify credentials via --api-key/--api-secret, TB_KEY/TB_SECRET environment variables, or ~/.testingbot file' ,
189+ ) ;
72190 }
73191 const maestro = new Maestro ( credentials , options ) ;
74192 await maestro . run ( ) ;
@@ -86,12 +204,19 @@ program
86204 . requiredOption ( '--app <string>' , 'Path to application under test.' )
87205 . requiredOption ( '--device <device>' , 'Real device to use for testing.' )
88206 . requiredOption ( '--test-app <string>' , 'Path to test application.' )
207+ . option ( '--api-key <key>' , 'TestingBot API key.' )
208+ . option ( '--api-secret <secret>' , 'TestingBot API secret.' )
89209 . action ( async ( args ) => {
90210 try {
91211 const options = new XCUITestOptions ( args . app , args . testApp , args . device ) ;
92- const credentials = await auth . getCredentials ( ) ;
212+ const credentials = await Auth . getCredentials ( {
213+ apiKey : args . apiKey ,
214+ apiSecret : args . apiSecret ,
215+ } ) ;
93216 if ( credentials === null ) {
94- throw new Error ( 'Please specify credentials' ) ;
217+ throw new Error (
218+ 'Please specify credentials via --api-key/--api-secret, TB_KEY/TB_SECRET environment variables, or ~/.testingbot file' ,
219+ ) ;
95220 }
96221 const xcuitest = new XCUITest ( credentials , options ) ;
97222 await xcuitest . run ( ) ;
0 commit comments