@testingbot/cli 1.0.6 → 1.0.8

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.
Files changed (45) hide show
  1. package/README.md +25 -0
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +79 -50
  4. package/dist/config/constants.d.ts +15 -0
  5. package/dist/config/constants.d.ts.map +1 -0
  6. package/dist/config/constants.js +17 -0
  7. package/dist/index.js +2 -0
  8. package/dist/models/espresso_options.d.ts +11 -4
  9. package/dist/models/espresso_options.d.ts.map +1 -1
  10. package/dist/models/espresso_options.js +24 -7
  11. package/dist/models/maestro_options.d.ts +13 -3
  12. package/dist/models/maestro_options.d.ts.map +1 -1
  13. package/dist/models/maestro_options.js +25 -2
  14. package/dist/models/xcuitest_options.d.ts +11 -4
  15. package/dist/models/xcuitest_options.d.ts.map +1 -1
  16. package/dist/models/xcuitest_options.js +24 -7
  17. package/dist/providers/base_provider.d.ts +28 -2
  18. package/dist/providers/base_provider.d.ts.map +1 -1
  19. package/dist/providers/base_provider.js +70 -2
  20. package/dist/providers/espresso.d.ts +1 -0
  21. package/dist/providers/espresso.d.ts.map +1 -1
  22. package/dist/providers/espresso.js +82 -35
  23. package/dist/providers/maestro.d.ts +21 -0
  24. package/dist/providers/maestro.d.ts.map +1 -1
  25. package/dist/providers/maestro.js +320 -72
  26. package/dist/providers/xcuitest.d.ts +1 -0
  27. package/dist/providers/xcuitest.d.ts.map +1 -1
  28. package/dist/providers/xcuitest.js +79 -35
  29. package/dist/ui/banner.d.ts +3 -0
  30. package/dist/ui/banner.d.ts.map +1 -0
  31. package/dist/ui/banner.js +82 -0
  32. package/dist/ui/spinner.d.ts +32 -0
  33. package/dist/ui/spinner.d.ts.map +1 -0
  34. package/dist/ui/spinner.js +92 -0
  35. package/dist/ui/terminal-title.d.ts +8 -0
  36. package/dist/ui/terminal-title.d.ts.map +1 -0
  37. package/dist/ui/terminal-title.js +57 -0
  38. package/dist/upload.d.ts +4 -0
  39. package/dist/upload.d.ts.map +1 -1
  40. package/dist/upload.js +70 -12
  41. package/dist/utils/connectivity.js +5 -3
  42. package/dist/utils.d.ts +6 -0
  43. package/dist/utils.d.ts.map +1 -1
  44. package/dist/utils.js +10 -0
  45. package/package.json +5 -3
package/dist/upload.js CHANGED
@@ -26,14 +26,34 @@ class Upload {
26
26
  length: totalSize,
27
27
  time: 100, // Emit progress every 100ms
28
28
  });
29
- let lastPercent = 0;
29
+ const interactive = utils_1.default.isInteractive();
30
+ let lastPercent = -1;
31
+ let lastBucket = -1;
32
+ const BUCKET_SIZE = 10;
30
33
  if (showProgress) {
31
- this.drawProgressBar(fileName, totalSize, 0);
34
+ if (interactive) {
35
+ this.drawProgressBar(fileName, totalSize, 0);
36
+ }
32
37
  progressTracker.on('progress', (prog) => {
33
38
  const percent = Math.round(prog.percentage);
34
- if (percent !== lastPercent) {
35
- lastPercent = percent;
36
- this.drawProgressBar(fileName, totalSize, percent);
39
+ if (interactive) {
40
+ // Redraw whenever percent changes; speed/ETA still update because
41
+ // progress-stream emits every 100ms.
42
+ if (percent !== lastPercent) {
43
+ lastPercent = percent;
44
+ this.drawProgressBar(fileName, totalSize, percent, prog.speed, prog.eta);
45
+ }
46
+ }
47
+ else {
48
+ // Non-TTY: one line per 10% bucket, no ANSI, no \r. 100% is
49
+ // reported by the success path.
50
+ const bucket = Math.floor(percent / BUCKET_SIZE);
51
+ if (bucket !== lastBucket && percent < 100) {
52
+ lastBucket = bucket;
53
+ const transferred = this.formatFileSize((percent / 100) * totalSize);
54
+ const total = this.formatFileSize(totalSize);
55
+ console.log(` ${fileName}: ${percent}% (${transferred}/${total})`);
56
+ }
37
57
  }
38
58
  });
39
59
  }
@@ -68,21 +88,36 @@ class Upload {
68
88
  const result = response.data;
69
89
  if (result.id) {
70
90
  if (showProgress) {
71
- this.drawProgressBar(fileName, totalSize, 100);
72
- console.log('');
91
+ if (interactive) {
92
+ this.drawProgressBar(fileName, totalSize, 100);
93
+ console.log('');
94
+ }
95
+ else {
96
+ console.log(` ${fileName}: done (${this.formatFileSize(totalSize)})`);
97
+ }
73
98
  }
74
99
  return { id: result.id };
75
100
  }
76
101
  else {
77
102
  if (showProgress) {
78
- console.log(' Failed');
103
+ if (interactive) {
104
+ console.log(' Failed');
105
+ }
106
+ else {
107
+ console.log(` ${fileName}: failed`);
108
+ }
79
109
  }
80
110
  throw new testingbot_error_1.default(`Upload failed: ${result.error || 'Unknown error'}`);
81
111
  }
82
112
  }
83
113
  catch (error) {
84
114
  if (showProgress) {
85
- console.log(' Failed');
115
+ if (interactive) {
116
+ console.log(' Failed');
117
+ }
118
+ else {
119
+ console.log(` ${fileName}: failed`);
120
+ }
86
121
  }
87
122
  if (error instanceof testingbot_error_1.default) {
88
123
  throw error;
@@ -98,7 +133,7 @@ class Upload {
98
133
  throw new testingbot_error_1.default(`Upload failed: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error instanceof Error ? error : undefined });
99
134
  }
100
135
  }
101
- drawProgressBar(fileName, totalBytes, percent) {
136
+ drawProgressBar(fileName, totalBytes, percent, bytesPerSec, etaSeconds) {
102
137
  const barWidth = 30;
103
138
  const filled = Math.round((barWidth * percent) / 100);
104
139
  const empty = barWidth - filled;
@@ -106,14 +141,27 @@ class Upload {
106
141
  const transferredBytes = (percent / 100) * totalBytes;
107
142
  const transferred = this.formatFileSize(transferredBytes);
108
143
  const total = this.formatFileSize(totalBytes);
109
- process.stdout.write(`\r ${fileName}: [${bar}] ${percent}% (${transferred}/${total})`);
144
+ let suffix = '';
145
+ if (typeof bytesPerSec === 'number' &&
146
+ Number.isFinite(bytesPerSec) &&
147
+ bytesPerSec > 0) {
148
+ suffix += ` • ${this.formatFileSize(bytesPerSec)}/s`;
149
+ }
150
+ if (typeof etaSeconds === 'number' &&
151
+ Number.isFinite(etaSeconds) &&
152
+ etaSeconds > 0) {
153
+ suffix += ` • ETA ${this.formatDuration(etaSeconds)}`;
154
+ }
155
+ // `\x1b[K` clears from cursor to end of line so residue from a longer
156
+ // previous frame (e.g. "ETA 16s" → "ETA 1s") can't leak through.
157
+ process.stdout.write(`\r ${fileName}: [${bar}] ${percent}% (${transferred}/${total})${suffix}\x1b[K`);
110
158
  }
111
159
  /**
112
160
  * Format file size in human-readable format (KB for small files, MB for larger)
113
161
  */
114
162
  formatFileSize(bytes) {
115
163
  if (bytes < 1024) {
116
- return `${bytes} B`;
164
+ return `${Math.round(bytes)} B`;
117
165
  }
118
166
  else if (bytes < 1024 * 1024) {
119
167
  return `${(bytes / 1024).toFixed(1)} KB`;
@@ -122,6 +170,16 @@ class Upload {
122
170
  return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
123
171
  }
124
172
  }
173
+ /**
174
+ * Format seconds as compact duration: "12s" or "3m04s".
175
+ */
176
+ formatDuration(seconds) {
177
+ if (seconds < 60)
178
+ return `${Math.round(seconds)}s`;
179
+ const m = Math.floor(seconds / 60);
180
+ const s = Math.round(seconds % 60);
181
+ return `${m}m${s.toString().padStart(2, '0')}s`;
182
+ }
125
183
  async validateFile(filePath) {
126
184
  try {
127
185
  await node_fs_1.default.promises.access(filePath, node_fs_1.default.constants.R_OK);
@@ -10,15 +10,14 @@ exports.formatConnectivityResults = formatConnectivityResults;
10
10
  */
11
11
  async function testEndpoint(url, description) {
12
12
  const startTime = Date.now();
13
+ const controller = new AbortController();
14
+ const timeoutId = setTimeout(() => controller.abort(), 3000);
13
15
  try {
14
- const controller = new AbortController();
15
- const timeoutId = setTimeout(() => controller.abort(), 3000);
16
16
  const response = await fetch(url, {
17
17
  method: 'HEAD',
18
18
  signal: controller.signal,
19
19
  redirect: 'manual',
20
20
  });
21
- clearTimeout(timeoutId);
22
21
  const latencyMs = Date.now() - startTime;
23
22
  return {
24
23
  endpoint: `${description} (${url})`,
@@ -60,6 +59,9 @@ async function testEndpoint(url, description) {
60
59
  latencyMs,
61
60
  };
62
61
  }
62
+ finally {
63
+ clearTimeout(timeoutId);
64
+ }
63
65
  }
64
66
  /**
65
67
  * Check if the system has internet connectivity by testing against
package/dist/utils.d.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  declare const _default: {
2
2
  getUserAgent(): string;
3
+ /**
4
+ * True when stdout is attached to an interactive terminal. CI environments
5
+ * (CI=truthy) are always treated as non-interactive so progress animations
6
+ * don't spam log aggregators.
7
+ */
8
+ isInteractive(): boolean;
3
9
  getCurrentVersion(): string;
4
10
  /**
5
11
  * Compare two semver version strings
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":";oBAQkB,MAAM;yBAID,MAAM;IAI3B;;;OAGG;wBACiB,MAAM,MAAM,MAAM,GAAG,MAAM;IAa/C;;OAEG;6BACsB,MAAM,GAAG,SAAS,GAAG,OAAO;IAWrD;;OAEG;+BACwB,MAAM,GAAG,SAAS,GAAG,OAAO;IAWvD;;OAEG;qCAC8B;QAC/B,UAAU,EAAE,OAAO,CAAC;QACpB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GAAG,IAAI;IAyDR;;OAEG;kCAC2B,MAAM,GAAG,SAAS,GAAG,IAAI;;AA3HzD,wBAqJE"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":";oBAQkB,MAAM;IAItB;;;;OAIG;qBACc,OAAO;yBAKH,MAAM;IAI3B;;;OAGG;wBACiB,MAAM,MAAM,MAAM,GAAG,MAAM;IAa/C;;OAEG;6BACsB,MAAM,GAAG,SAAS,GAAG,OAAO;IAWrD;;OAEG;+BACwB,MAAM,GAAG,SAAS,GAAG,OAAO;IAWvD;;OAEG;qCAC8B;QAC/B,UAAU,EAAE,OAAO,CAAC;QACpB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GAAG,IAAI;IAyDR;;OAEG;kCAC2B,MAAM,GAAG,SAAS,GAAG,IAAI;;AArIzD,wBA+JE"}
package/dist/utils.js CHANGED
@@ -12,6 +12,16 @@ exports.default = {
12
12
  getUserAgent() {
13
13
  return `TestingBot-CTL-${package_json_1.default.version}`;
14
14
  },
15
+ /**
16
+ * True when stdout is attached to an interactive terminal. CI environments
17
+ * (CI=truthy) are always treated as non-interactive so progress animations
18
+ * don't spam log aggregators.
19
+ */
20
+ isInteractive() {
21
+ if (process.env.CI)
22
+ return false;
23
+ return Boolean(process.stdout.isTTY);
24
+ },
15
25
  getCurrentVersion() {
16
26
  return package_json_1.default.version;
17
27
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testingbot/cli",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
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": {
@@ -16,10 +16,11 @@
16
16
  "scripts": {
17
17
  "lint": "prettier --check '**/*.{js,ts}' && eslint src/",
18
18
  "build": "tsc",
19
- "clean": "rm -rf dist",
19
+ "clean": "node -e \"require('fs').rmSync('dist', {recursive: true, force: true})\"",
20
20
  "start": "node dist/index.js",
21
21
  "format": "prettier --write '**/*.{js,ts}'",
22
- "test": "jest"
22
+ "test": "jest",
23
+ "test:coverage": "jest --coverage"
23
24
  },
24
25
  "keywords": [
25
26
  "testingbot",
@@ -57,6 +58,7 @@
57
58
  "picocolors": "^1.1.1",
58
59
  "progress-stream": "^2.0.0",
59
60
  "socket.io-client": "^4.8.1",
61
+ "testingbot-tunnel-launcher": "^1.1.18",
60
62
  "tracer": "^1.3.0"
61
63
  },
62
64
  "devDependencies": {