@testingbot/cli 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAwBpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AA6e9B,eAAe,OAAO,CAAC"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAwBpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAyf9B,eAAe,OAAO,CAAC"}
package/dist/cli.js CHANGED
@@ -101,7 +101,11 @@ const espressoCommand = program
101
101
  apiSecret: args.apiSecret,
102
102
  });
103
103
  if (credentials === null) {
104
- throw new Error('Please specify credentials via --api-key/--api-secret, TB_KEY/TB_SECRET environment variables, or ~/.testingbot file');
104
+ throw new Error('No TestingBot credentials found. Please authenticate using one of these methods:\n' +
105
+ ' 1. Run "testingbot login" to authenticate via browser (recommended)\n' +
106
+ ' 2. Use --api-key and --api-secret options\n' +
107
+ ' 3. Set TB_KEY and TB_SECRET environment variables\n' +
108
+ ' 4. Create ~/.testingbot file with content: key:secret');
105
109
  }
106
110
  const espresso = new espresso_1.default(credentials, options);
107
111
  const result = await espresso.run();
@@ -215,7 +219,11 @@ const maestroCommand = program
215
219
  apiSecret: args.apiSecret,
216
220
  });
217
221
  if (credentials === null) {
218
- throw new Error('Please specify credentials via --api-key/--api-secret, TB_KEY/TB_SECRET environment variables, or ~/.testingbot file');
222
+ throw new Error('No TestingBot credentials found. Please authenticate using one of these methods:\n' +
223
+ ' 1. Run "testingbot login" to authenticate via browser (recommended)\n' +
224
+ ' 2. Use --api-key and --api-secret options\n' +
225
+ ' 3. Set TB_KEY and TB_SECRET environment variables\n' +
226
+ ' 4. Create ~/.testingbot file with content: key:secret');
219
227
  }
220
228
  const maestro = new maestro_1.default(credentials, options);
221
229
  const result = await maestro.run();
@@ -296,7 +304,11 @@ const xcuitestCommand = program
296
304
  apiSecret: args.apiSecret,
297
305
  });
298
306
  if (credentials === null) {
299
- throw new Error('Please specify credentials via --api-key/--api-secret, TB_KEY/TB_SECRET environment variables, or ~/.testingbot file');
307
+ throw new Error('No TestingBot credentials found. Please authenticate using one of these methods:\n' +
308
+ ' 1. Run "testingbot login" to authenticate via browser (recommended)\n' +
309
+ ' 2. Use --api-key and --api-secret options\n' +
310
+ ' 3. Set TB_KEY and TB_SECRET environment variables\n' +
311
+ ' 4. Create ~/.testingbot file with content: key:secret');
300
312
  }
301
313
  const xcuitest = new xcuitest_1.default(credentials, options);
302
314
  const result = await xcuitest.run();
@@ -20,7 +20,7 @@ class Login {
20
20
  // Open browser to auth URL
21
21
  const authUrl = `${AUTH_URL}?port=${this.port}&identifier=testingbotctl`;
22
22
  logger_1.default.info('Opening browser for authentication...');
23
- logger_1.default.info(`\nIf the browser does not open automatically, visit:\n\n ${authUrl}\n`);
23
+ logger_1.default.info(`If the browser does not open automatically, visit:\n\n ${authUrl}\n`);
24
24
  await this.openBrowser(authUrl);
25
25
  // Wait for callback (handled by server)
26
26
  const credentials = await this.waitForCallback();
@@ -1,7 +1,7 @@
1
1
  import MaestroOptions from '../models/maestro_options';
2
2
  import Credentials from '../models/credentials';
3
3
  export interface MaestroRunAssets {
4
- logs?: string[];
4
+ logs?: Record<string, string>;
5
5
  video?: string | false;
6
6
  screenshots?: string[];
7
7
  }
@@ -1 +1 @@
1
- {"version":3,"file":"maestro.d.ts","sourceRoot":"","sources":["../../src/providers/maestro.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"maestro.d.ts","sourceRoot":"","sources":["../../src/providers/maestro.ts"],"names":[],"mappings":"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"}
@@ -523,7 +523,6 @@ class Maestro {
523
523
  const statusEmoji = run.success === 1 ? '✅' : '❌';
524
524
  const statusText = run.success === 1 ? 'Test completed successfully' : 'Test failed';
525
525
  console.log(` ${statusEmoji} Run ${run.id} (${run.capabilities.deviceName}): ${statusText}`);
526
- console.log(` View results: https://testingbot.com/members/maestro/${this.appId}/runs/${run.id}`);
527
526
  }
528
527
  }
529
528
  const allSucceeded = status.runs.every((run) => run.success === 1);
@@ -544,7 +543,7 @@ class Maestro {
544
543
  await this.fetchReports(status.runs);
545
544
  }
546
545
  // Download artifacts if requested
547
- if (this.options.downloadArtifacts && this.options.artifactsOutputDir) {
546
+ if (this.options.downloadArtifacts) {
548
547
  await this.downloadArtifacts(status.runs);
549
548
  }
550
549
  return {
@@ -569,6 +568,11 @@ class Maestro {
569
568
  (prevStatus === 'WAITING' || prevStatus === 'READY')) {
570
569
  this.clearLine();
571
570
  }
571
+ // Show URL when test starts running (transitions from WAITING to READY)
572
+ if (statusChanged && prevStatus === 'WAITING' && run.status === 'READY') {
573
+ console.log(` 🚀 Run ${run.id} (${run.capabilities.deviceName}): Test started`);
574
+ console.log(` Watch this test in realtime: https://testingbot.com/members/maestro/${this.appId}/runs/${run.id}`);
575
+ }
572
576
  previousStatus.set(run.id, run.status);
573
577
  const statusInfo = this.getStatusInfo(run.status);
574
578
  if (run.status === 'WAITING' || run.status === 'READY') {
@@ -680,31 +684,73 @@ class Maestro {
680
684
  }
681
685
  throw new testingbot_error_1.default(`Timed out waiting for artifacts to sync for run ${runId}`);
682
686
  }
683
- async downloadFile(url, filePath) {
684
- try {
685
- const response = await axios_1.default.get(url, {
686
- responseType: 'arraybuffer',
687
- headers: {
688
- 'User-Agent': utils_1.default.getUserAgent(),
689
- },
690
- });
691
- await node_fs_1.default.promises.writeFile(filePath, response.data);
687
+ async downloadFile(url, filePath, retries = 3) {
688
+ let lastError;
689
+ for (let attempt = 1; attempt <= retries; attempt++) {
690
+ try {
691
+ const response = await axios_1.default.get(url, {
692
+ responseType: 'arraybuffer',
693
+ timeout: 60000, // 60 second timeout for large files
694
+ });
695
+ await node_fs_1.default.promises.writeFile(filePath, response.data);
696
+ return;
697
+ }
698
+ catch (error) {
699
+ lastError = error;
700
+ // Don't retry on 4xx errors (client errors like 403, 404)
701
+ if (axios_1.default.isAxiosError(error) && error.response?.status) {
702
+ const status = error.response.status;
703
+ if (status >= 400 && status < 500) {
704
+ break;
705
+ }
706
+ }
707
+ // Wait before retrying (exponential backoff)
708
+ if (attempt < retries) {
709
+ await this.sleep(1000 * attempt);
710
+ }
711
+ }
692
712
  }
693
- catch (error) {
694
- throw new testingbot_error_1.default(`Failed to download file from ${url}`, {
695
- cause: error,
696
- });
713
+ // Extract detailed error message
714
+ let errorDetail = '';
715
+ if (axios_1.default.isAxiosError(lastError)) {
716
+ if (lastError.response) {
717
+ errorDetail = `HTTP ${lastError.response.status}: ${lastError.response.statusText}`;
718
+ }
719
+ else if (lastError.code) {
720
+ errorDetail = lastError.code;
721
+ }
722
+ else if (lastError.message) {
723
+ errorDetail = lastError.message;
724
+ }
697
725
  }
726
+ else if (lastError instanceof Error) {
727
+ errorDetail = lastError.message;
728
+ }
729
+ else if (lastError) {
730
+ errorDetail = String(lastError);
731
+ }
732
+ throw new testingbot_error_1.default(`Failed to download file${errorDetail ? `: ${errorDetail}` : ''}`, {
733
+ cause: lastError,
734
+ });
698
735
  }
699
- generateArtifactZipName() {
700
- // Use --build option if provided, otherwise generate timestamp-based name
701
- if (this.options.build) {
702
- const sanitizedBuild = this.options.build.replace(/[^a-zA-Z0-9_-]/g, '_');
703
- return `${sanitizedBuild}.zip`;
736
+ async generateArtifactZipName(outputDir) {
737
+ if (!this.options.build) {
738
+ // Generate unique name with timestamp
739
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
740
+ return `maestro_artifacts_${timestamp}.zip`;
741
+ }
742
+ const baseName = this.options.build.replace(/[^a-zA-Z0-9_-]/g, '_');
743
+ const fileName = `${baseName}.zip`;
744
+ const filePath = node_path_1.default.join(outputDir, fileName);
745
+ try {
746
+ await node_fs_1.default.promises.access(filePath);
747
+ // File exists, append timestamp
748
+ return `${baseName}_${Date.now()}.zip`;
749
+ }
750
+ catch {
751
+ // File doesn't exist, use base name
752
+ return fileName;
704
753
  }
705
- // Generate unique name with timestamp
706
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
707
- return `maestro_artifacts_${timestamp}.zip`;
708
754
  }
709
755
  async downloadArtifacts(runs) {
710
756
  if (!this.options.downloadArtifacts)
@@ -730,12 +776,12 @@ class Maestro {
730
776
  const runDir = node_path_1.default.join(tempDir, `run_${run.id}`);
731
777
  await node_fs_1.default.promises.mkdir(runDir, { recursive: true });
732
778
  // Download logs
733
- if (runDetails.assets.logs && runDetails.assets.logs.length > 0) {
779
+ if (runDetails.assets.logs &&
780
+ Object.keys(runDetails.assets.logs).length > 0) {
734
781
  const logsDir = node_path_1.default.join(runDir, 'logs');
735
782
  await node_fs_1.default.promises.mkdir(logsDir, { recursive: true });
736
- for (let i = 0; i < runDetails.assets.logs.length; i++) {
737
- const logUrl = runDetails.assets.logs[i];
738
- const logFileName = node_path_1.default.basename(logUrl) || `log_${i}.txt`;
783
+ for (const [logName, logUrl] of Object.entries(runDetails.assets.logs)) {
784
+ const logFileName = `${logName}.txt`;
739
785
  const logPath = node_path_1.default.join(logsDir, logFileName);
740
786
  try {
741
787
  await this.downloadFile(logUrl, logPath);
@@ -753,7 +799,7 @@ class Maestro {
753
799
  const videoDir = node_path_1.default.join(runDir, 'video');
754
800
  await node_fs_1.default.promises.mkdir(videoDir, { recursive: true });
755
801
  const videoUrl = runDetails.assets.video;
756
- const videoFileName = node_path_1.default.basename(videoUrl) || 'video.mp4';
802
+ const videoFileName = 'video.mp4';
757
803
  const videoPath = node_path_1.default.join(videoDir, videoFileName);
758
804
  try {
759
805
  await this.downloadFile(videoUrl, videoPath);
@@ -771,7 +817,7 @@ class Maestro {
771
817
  await node_fs_1.default.promises.mkdir(screenshotsDir, { recursive: true });
772
818
  for (let i = 0; i < runDetails.assets.screenshots.length; i++) {
773
819
  const screenshotUrl = runDetails.assets.screenshots[i];
774
- const screenshotFileName = node_path_1.default.basename(screenshotUrl) || `screenshot_${i}.png`;
820
+ const screenshotFileName = `screenshot_${i}.png`;
775
821
  const screenshotPath = node_path_1.default.join(screenshotsDir, screenshotFileName);
776
822
  try {
777
823
  await this.downloadFile(screenshotUrl, screenshotPath);
@@ -804,7 +850,7 @@ class Maestro {
804
850
  logger_1.default.error(`Failed to download artifacts for run ${run.id}: ${error instanceof Error ? error.message : error}`);
805
851
  }
806
852
  }
807
- const zipFileName = this.generateArtifactZipName();
853
+ const zipFileName = await this.generateArtifactZipName(outputDir);
808
854
  const zipFilePath = node_path_1.default.join(outputDir, zipFileName);
809
855
  if (!this.options.quiet) {
810
856
  logger_1.default.info(`Creating artifacts zip: ${zipFileName}`);
package/dist/upload.d.ts CHANGED
@@ -11,11 +11,8 @@ export interface UploadResult {
11
11
  id: number;
12
12
  }
13
13
  export default class Upload {
14
- private lastProgressPercent;
15
14
  upload(options: UploadOptions): Promise<UploadResult>;
15
+ private drawProgressBar;
16
16
  private validateFile;
17
- private handleProgress;
18
- private displayProgress;
19
- private clearProgressLine;
20
17
  }
21
18
  //# sourceMappingURL=upload.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"AAIA,OAAO,WAAW,MAAM,sBAAsB,CAAC;AAI/C,MAAM,MAAM,WAAW,GACnB,yCAAyC,GACzC,0BAA0B,GAC1B,iBAAiB,CAAC;AAEtB,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,WAAW,CAAC;IACzB,WAAW,EAAE,WAAW,CAAC;IACzB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,CAAC,OAAO,OAAO,MAAM;IACzB,OAAO,CAAC,mBAAmB,CAAa;IAE3B,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC;YA+DpD,YAAY;IAQ1B,OAAO,CAAC,cAAc;IAetB,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,iBAAiB;CAI1B"}
1
+ {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"AAKA,OAAO,WAAW,MAAM,sBAAsB,CAAC;AAI/C,MAAM,MAAM,WAAW,GACnB,yCAAyC,GACzC,0BAA0B,GAC1B,iBAAiB,CAAC;AAEtB,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,WAAW,CAAC;IACzB,WAAW,EAAE,WAAW,CAAC;IACzB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,CAAC,OAAO,OAAO,MAAM;IACZ,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC;IAuGlE,OAAO,CAAC,eAAe;YAgBT,YAAY;CAO3B"}
package/dist/upload.js CHANGED
@@ -7,23 +7,47 @@ const axios_1 = __importDefault(require("axios"));
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
9
  const form_data_1 = __importDefault(require("form-data"));
10
+ const progress_stream_1 = __importDefault(require("progress-stream"));
10
11
  const testingbot_error_1 = __importDefault(require("./models/testingbot_error"));
11
12
  const utils_1 = __importDefault(require("./utils"));
12
13
  class Upload {
13
- lastProgressPercent = 0;
14
14
  async upload(options) {
15
- const { filePath, url, credentials, contentType, showProgress = false, } = options;
15
+ const { filePath, url, credentials, showProgress = false, } = options;
16
16
  await this.validateFile(filePath);
17
17
  const fileName = node_path_1.default.basename(filePath);
18
18
  const fileStats = await node_fs_1.default.promises.stat(filePath);
19
+ const totalSize = fileStats.size;
20
+ const sizeMB = (totalSize / (1024 * 1024)).toFixed(2);
21
+ // Create progress tracker
22
+ const progressTracker = (0, progress_stream_1.default)({
23
+ length: totalSize,
24
+ time: 100, // Emit progress every 100ms
25
+ });
26
+ let lastPercent = 0;
27
+ if (showProgress) {
28
+ // Draw initial progress bar
29
+ this.drawProgressBar(fileName, sizeMB, 0);
30
+ progressTracker.on('progress', (prog) => {
31
+ const percent = Math.round(prog.percentage);
32
+ if (percent !== lastPercent) {
33
+ lastPercent = percent;
34
+ this.drawProgressBar(fileName, sizeMB, percent);
35
+ }
36
+ });
37
+ }
38
+ // Create file stream and pipe through progress tracker
19
39
  const fileStream = node_fs_1.default.createReadStream(filePath);
40
+ const trackedStream = fileStream.pipe(progressTracker);
20
41
  const formData = new form_data_1.default();
21
- formData.append('file', fileStream);
42
+ formData.append('file', trackedStream, {
43
+ filename: fileName,
44
+ contentType: options.contentType,
45
+ knownLength: totalSize,
46
+ });
22
47
  try {
23
48
  const response = await axios_1.default.post(url, formData, {
24
49
  headers: {
25
- 'Content-Type': contentType,
26
- 'Content-Disposition': `attachment; filename=${fileName}`,
50
+ ...formData.getHeaders(),
27
51
  'User-Agent': utils_1.default.getUserAgent(),
28
52
  },
29
53
  auth: {
@@ -32,34 +56,52 @@ class Upload {
32
56
  },
33
57
  maxContentLength: Infinity,
34
58
  maxBodyLength: Infinity,
35
- onUploadProgress: showProgress
36
- ? (progressEvent) => {
37
- this.handleProgress(progressEvent, fileStats.size, fileName);
38
- }
39
- : undefined,
59
+ maxRedirects: 0, // Recommended for stream uploads to avoid buffering
40
60
  });
41
61
  const result = response.data;
42
62
  if (result.id) {
43
63
  if (showProgress) {
44
- this.clearProgressLine();
64
+ this.drawProgressBar(fileName, sizeMB, 100);
65
+ console.log('');
45
66
  }
46
67
  return { id: result.id };
47
68
  }
48
69
  else {
70
+ if (showProgress) {
71
+ console.log(' Failed');
72
+ }
49
73
  throw new testingbot_error_1.default(`Upload failed: ${result.error || 'Unknown error'}`);
50
74
  }
51
75
  }
52
76
  catch (error) {
77
+ if (showProgress) {
78
+ console.log(' Failed');
79
+ }
53
80
  if (error instanceof testingbot_error_1.default) {
54
81
  throw error;
55
82
  }
56
83
  if (axios_1.default.isAxiosError(error)) {
84
+ if (error.response?.status === 401) {
85
+ throw new testingbot_error_1.default('Invalid TestingBot credentials. Please check your API key and secret.\n' +
86
+ 'You can update your credentials by running "testingbot login" or by using:\n' +
87
+ ' --api-key and --api-secret options\n' +
88
+ ' TB_KEY and TB_SECRET environment variables\n' +
89
+ ' ~/.testingbot file with content: key:secret');
90
+ }
57
91
  const message = error.response?.data?.error || error.message;
58
92
  throw new testingbot_error_1.default(`Upload failed: ${message}`);
59
93
  }
60
94
  throw new testingbot_error_1.default(`Upload failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
61
95
  }
62
96
  }
97
+ drawProgressBar(fileName, sizeMB, percent) {
98
+ const barWidth = 30;
99
+ const filled = Math.round((barWidth * percent) / 100);
100
+ const empty = barWidth - filled;
101
+ const bar = '█'.repeat(filled) + '░'.repeat(empty);
102
+ const transferred = ((percent / 100) * parseFloat(sizeMB)).toFixed(2);
103
+ process.stdout.write(`\r ${fileName}: [${bar}] ${percent}% (${transferred}/${sizeMB} MB)`);
104
+ }
63
105
  async validateFile(filePath) {
64
106
  try {
65
107
  await node_fs_1.default.promises.access(filePath, node_fs_1.default.constants.R_OK);
@@ -68,27 +110,5 @@ class Upload {
68
110
  throw new testingbot_error_1.default(`File not found or not readable: ${filePath}`);
69
111
  }
70
112
  }
71
- handleProgress(progressEvent, totalSize, fileName) {
72
- const loaded = progressEvent.loaded;
73
- const total = progressEvent.total || totalSize;
74
- const percent = Math.round((loaded / total) * 100);
75
- if (percent !== this.lastProgressPercent) {
76
- this.lastProgressPercent = percent;
77
- this.displayProgress(fileName, percent, loaded, total);
78
- }
79
- }
80
- displayProgress(fileName, percent, loaded, total) {
81
- const barWidth = 30;
82
- const filledWidth = Math.round((percent / 100) * barWidth);
83
- const emptyWidth = barWidth - filledWidth;
84
- const bar = '█'.repeat(filledWidth) + '░'.repeat(emptyWidth);
85
- const loadedMB = (loaded / (1024 * 1024)).toFixed(2);
86
- const totalMB = (total / (1024 * 1024)).toFixed(2);
87
- process.stdout.write(`\r ${fileName}: [${bar}] ${percent}% (${loadedMB}/${totalMB} MB)`);
88
- }
89
- clearProgressLine() {
90
- process.stdout.write('\r' + ' '.repeat(80) + '\r');
91
- this.lastProgressPercent = 0;
92
- }
93
113
  }
94
114
  exports.default = Upload;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testingbot/cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "CLI tool to run Espresso, XCUITest, and Maestro tests on TestingBot's cloud infrastructure",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -55,6 +55,7 @@
55
55
  "form-data": "^4.0.5",
56
56
  "glob": "^13.0.0",
57
57
  "js-yaml": "^4.1.1",
58
+ "progress-stream": "^2.0.0",
58
59
  "socket.io-client": "^4.8.1",
59
60
  "tracer": "^1.3.0"
60
61
  },
@@ -65,6 +66,7 @@
65
66
  "@types/jest": "^29.5.14",
66
67
  "@types/js-yaml": "^4.0.9",
67
68
  "@types/node": "^20.19.0",
69
+ "@types/progress-stream": "^2.0.5",
68
70
  "babel-jest": "^29.7.0",
69
71
  "eslint": "^9.39.1",
70
72
  "eslint-config-prettier": "^10.1.8",