@testingbot/cli 1.0.7 → 1.0.9

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 +29 -0
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +79 -46
  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 +7 -0
  9. package/dist/models/espresso_options.d.ts.map +1 -1
  10. package/dist/models/espresso_options.js +18 -0
  11. package/dist/models/maestro_options.d.ts +20 -2
  12. package/dist/models/maestro_options.d.ts.map +1 -1
  13. package/dist/models/maestro_options.js +38 -1
  14. package/dist/models/xcuitest_options.d.ts +7 -0
  15. package/dist/models/xcuitest_options.d.ts.map +1 -1
  16. package/dist/models/xcuitest_options.js +18 -0
  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 +31 -0
  24. package/dist/providers/maestro.d.ts.map +1 -1
  25. package/dist/providers/maestro.js +399 -149
  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
@@ -36,6 +36,7 @@ export default class XCUITest extends BaseProvider<XCUITestOptions> {
36
36
  private socket;
37
37
  private updateServer;
38
38
  private updateKey;
39
+ private socketFallbackWarned;
39
40
  constructor(credentials: Credentials, options: XCUITestOptions);
40
41
  private validate;
41
42
  run(): Promise<XCUITestResult>;
@@ -1 +1 @@
1
- {"version":3,"file":"xcuitest.d.ts","sourceRoot":"","sources":["../../src/providers/xcuitest.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,MAAM,4BAA4B,CAAC;AAEzD,OAAO,WAAW,MAAM,uBAAuB,CAAC;AAOhD,OAAO,YAAY,MAAM,iBAAiB,CAAC;AAE3C,MAAM,WAAW,sBAAsB;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;IAChD,YAAY,EAAE;QACZ,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;QACrB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,WAAW,CAAC,EAAE,sBAAsB,CAAC;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,eAAe,EAAE,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,eAAe,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,CAAC,OAAO,OAAO,QAAS,SAAQ,YAAY,CAAC,eAAe,CAAC;IACjE,SAAS,CAAC,QAAQ,CAAC,GAAG,yDACkC;IAExD,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,SAAS,CAAuB;gBAErB,WAAW,EAAE,WAAW,EAAE,OAAO,EAAE,eAAe;YAIvD,QAAQ;IAuCT,GAAG,IAAI,OAAO,CAAC,cAAc,CAAC;YA6F7B,SAAS;YAcT,aAAa;YAab,QAAQ;YA0DR,SAAS;YA4BT,iBAAiB;IAwE/B,OAAO,CAAC,gBAAgB;IAmCxB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,aAAa;YAkBP,YAAY;IA2D1B,OAAO,CAAC,qBAAqB;IAqC7B,OAAO,CAAC,0BAA0B;IAOlC,OAAO,CAAC,kBAAkB;IAc1B,OAAO,CAAC,mBAAmB;CAa5B"}
1
+ {"version":3,"file":"xcuitest.d.ts","sourceRoot":"","sources":["../../src/providers/xcuitest.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,MAAM,4BAA4B,CAAC;AAEzD,OAAO,WAAW,MAAM,uBAAuB,CAAC;AAQhD,OAAO,YAAY,MAAM,iBAAiB,CAAC;AAI3C,MAAM,WAAW,sBAAsB;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;IAChD,YAAY,EAAE;QACZ,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;QACrB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,WAAW,CAAC,EAAE,sBAAsB,CAAC;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,eAAe,EAAE,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,eAAe,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,CAAC,OAAO,OAAO,QAAS,SAAQ,YAAY,CAAC,eAAe,CAAC;IACjE,SAAS,CAAC,QAAQ,CAAC,GAAG,yDACkC;IAExD,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,oBAAoB,CAAS;gBAElB,WAAW,EAAE,WAAW,EAAE,OAAO,EAAE,eAAe;YAIvD,QAAQ;IAuCT,GAAG,IAAI,OAAO,CAAC,cAAc,CAAC;YA6G7B,SAAS;YAcT,aAAa;YAab,QAAQ;YA0DR,SAAS;YA4BT,iBAAiB;IA8F/B,OAAO,CAAC,gBAAgB;IAwCxB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,aAAa;YAkBP,YAAY;IA2D1B,OAAO,CAAC,qBAAqB;IA6C7B,OAAO,CAAC,0BAA0B;IAOlC,OAAO,CAAC,kBAAkB;IAc1B,OAAO,CAAC,mBAAmB;CAa5B"}
@@ -7,15 +7,19 @@ const logger_1 = __importDefault(require("../logger"));
7
7
  const axios_1 = __importDefault(require("axios"));
8
8
  const node_fs_1 = __importDefault(require("node:fs"));
9
9
  const node_path_1 = __importDefault(require("node:path"));
10
+ const picocolors_1 = __importDefault(require("picocolors"));
10
11
  const socket_io_client_1 = require("socket.io-client");
11
12
  const testingbot_error_1 = __importDefault(require("../models/testingbot_error"));
12
13
  const utils_1 = __importDefault(require("../utils"));
13
14
  const base_provider_1 = __importDefault(require("./base_provider"));
15
+ const terminal_title_1 = require("../ui/terminal-title");
16
+ const constants_1 = require("../config/constants");
14
17
  class XCUITest extends base_provider_1.default {
15
18
  URL = 'https://api.testingbot.com/v1/app-automate/xcuitest';
16
19
  socket = null;
17
20
  updateServer = null;
18
21
  updateKey = null;
22
+ socketFallbackWarned = false;
19
23
  constructor(credentials, options) {
20
24
  super(credentials, options);
21
25
  }
@@ -79,17 +83,25 @@ class XCUITest extends base_provider_1.default {
79
83
  try {
80
84
  // Quick connectivity check before starting uploads
81
85
  await this.ensureConnectivity();
86
+ (0, terminal_title_1.setTitle)('xcuitest');
82
87
  if (!this.options.quiet) {
83
88
  logger_1.default.info('Uploading XCUITest App');
84
89
  }
90
+ (0, terminal_title_1.setTitle)('xcuitest · uploading app');
85
91
  await this.uploadApp();
86
92
  if (!this.options.quiet) {
87
93
  logger_1.default.info('Uploading XCUITest Test App');
88
94
  }
95
+ (0, terminal_title_1.setTitle)('xcuitest · uploading test app');
89
96
  await this.uploadTestApp();
97
+ if (this.options.tunnel && this.options.async) {
98
+ throw new testingbot_error_1.default('Cannot use --tunnel with --async mode. The tunnel would close when the CLI exits. Use a standalone tunnel instead.');
99
+ }
100
+ await this.startTunnel();
90
101
  if (!this.options.quiet) {
91
102
  logger_1.default.info('Running XCUITests');
92
103
  }
104
+ (0, terminal_title_1.setTitle)('xcuitest · queued');
93
105
  await this.runTests();
94
106
  if (this.options.async) {
95
107
  if (!this.options.quiet) {
@@ -108,12 +120,16 @@ class XCUITest extends base_provider_1.default {
108
120
  // Clean up
109
121
  this.disconnectFromUpdateServer();
110
122
  this.removeSignalHandlers();
123
+ await this.stopTunnel();
111
124
  return result;
112
125
  }
113
126
  catch (error) {
114
127
  // Clean up on error
128
+ this.spinner.stop();
115
129
  this.disconnectFromUpdateServer();
116
130
  this.removeSignalHandlers();
131
+ await this.stopTunnel();
132
+ (0, terminal_title_1.setTitle)('xcuitest · ✘ error');
117
133
  logger_1.default.error(error instanceof Error ? error.message : error);
118
134
  if (error instanceof Error && error.cause) {
119
135
  const causeMessage = this.extractErrorMessage(error.cause);
@@ -165,7 +181,7 @@ class XCUITest extends base_provider_1.default {
165
181
  username: this.credentials.userName,
166
182
  password: this.credentials.accessKey,
167
183
  },
168
- timeout: 30000, // 30 second timeout
184
+ timeout: constants_1.HTTP.TIMEOUT_MS,
169
185
  });
170
186
  // Check for version update notification
171
187
  const latestVersion = response.headers?.['x-testingbotctl-version'];
@@ -202,7 +218,7 @@ class XCUITest extends base_provider_1.default {
202
218
  username: this.credentials.userName,
203
219
  password: this.credentials.accessKey,
204
220
  },
205
- timeout: 30000, // 30 second timeout
221
+ timeout: constants_1.HTTP.TIMEOUT_MS,
206
222
  });
207
223
  // Check for version update notification
208
224
  const latestVersion = response.headers?.['x-testingbotctl-version'];
@@ -215,10 +231,11 @@ class XCUITest extends base_provider_1.default {
215
231
  }
216
232
  }
217
233
  async waitForCompletion() {
218
- let attempts = 0;
219
234
  const startTime = Date.now();
220
235
  const previousStatus = new Map();
221
- while (attempts < this.MAX_POLL_ATTEMPTS) {
236
+ let pollInterval = this.MIN_POLL_INTERVAL_MS;
237
+ let previousSignature = null;
238
+ while (true) {
222
239
  if (this.isShuttingDown) {
223
240
  throw new testingbot_error_1.default('Test run cancelled by user');
224
241
  }
@@ -231,24 +248,34 @@ class XCUITest extends base_provider_1.default {
231
248
  if (!this.options.quiet) {
232
249
  this.displayRunStatus(status.runs, startTime, previousStatus);
233
250
  }
251
+ const running = status.runs.find((r) => r.status === 'READY');
252
+ if (running) {
253
+ const device = running.environment?.name || running.capabilities.deviceName;
254
+ (0, terminal_title_1.setTitle)(`xcuitest · running · ${device}`);
255
+ }
234
256
  if (status.completed) {
235
- // Clear the updating line and print final status
257
+ // Stop the spinner and print final status
236
258
  if (!this.options.quiet) {
237
- this.clearLine();
259
+ this.spinner.stop();
238
260
  for (const run of status.runs) {
239
- const statusEmoji = run.success === 1 ? '✅' : '❌';
240
- const statusText = run.success === 1 ? 'Test completed successfully' : 'Test failed';
241
- console.log(` ${statusEmoji} Run ${run.id} (${this.getRunDisplayName(run)}): ${statusText}`);
261
+ const passed = run.success === 1;
262
+ const symbol = passed ? picocolors_1.default.green('') : picocolors_1.default.red('');
263
+ const statusText = passed
264
+ ? picocolors_1.default.green('Test completed successfully')
265
+ : picocolors_1.default.red('Test failed');
266
+ console.log(` ${symbol} Run ${run.id} ${picocolors_1.default.dim(`(${this.getRunDisplayName(run)})`)}: ${statusText}`);
242
267
  }
243
268
  }
244
269
  const allSucceeded = status.runs.every((run) => run.success === 1);
245
270
  if (allSucceeded) {
271
+ (0, terminal_title_1.setTitle)('xcuitest · ✔ passed');
246
272
  if (!this.options.quiet) {
247
273
  logger_1.default.info('All tests completed successfully!');
248
274
  }
249
275
  }
250
276
  else {
251
277
  const failedRuns = status.runs.filter((run) => run.success !== 1);
278
+ (0, terminal_title_1.setTitle)(`xcuitest · ✘ ${failedRuns.length} failed`);
252
279
  logger_1.default.error(`${failedRuns.length} test run(s) failed:`);
253
280
  for (const run of failedRuns) {
254
281
  logger_1.default.error(` - Run ${run.id} (${this.getRunDisplayName(run)}): ${run.report || 'No report available'}`);
@@ -263,32 +290,45 @@ class XCUITest extends base_provider_1.default {
263
290
  runs: status.runs,
264
291
  };
265
292
  }
266
- attempts++;
267
- await this.sleep(this.POLL_INTERVAL_MS);
293
+ // Checked after getStatus() so a run that completes during the final
294
+ // sleep is returned as success on the next iteration instead of being
295
+ // misreported as a timeout.
296
+ if (Date.now() - startTime >= this.MAX_POLL_DURATION_MS) {
297
+ throw new testingbot_error_1.default(`Test timed out after ${this.MAX_POLL_DURATION_MS / 1000 / 60} minutes`);
298
+ }
299
+ const signature = JSON.stringify(status.runs.map((r) => [r.id, r.status, r.success]));
300
+ const changed = signature !== previousSignature;
301
+ previousSignature = signature;
302
+ pollInterval = this.computeNextPollInterval(pollInterval, changed);
303
+ await this.sleep(pollInterval);
268
304
  }
269
- throw new testingbot_error_1.default(`Test timed out after ${(this.MAX_POLL_ATTEMPTS * this.POLL_INTERVAL_MS) / 1000 / 60} minutes`);
270
305
  }
271
306
  displayRunStatus(runs, startTime, previousStatus) {
272
307
  const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);
273
308
  const elapsedStr = this.formatElapsedTime(elapsedSeconds);
309
+ const activeMessages = [];
274
310
  for (const run of runs) {
275
311
  const prevStatus = previousStatus.get(run.id);
276
312
  const statusChanged = prevStatus !== run.status;
277
- if (statusChanged &&
278
- prevStatus &&
279
- (prevStatus === 'WAITING' || prevStatus === 'READY')) {
280
- this.clearLine();
281
- }
282
313
  previousStatus.set(run.id, run.status);
283
314
  const statusInfo = this.getStatusInfo(run.status);
284
315
  if (run.status === 'WAITING' || run.status === 'READY') {
285
- const message = ` ${statusInfo.emoji} Run ${run.id} (${this.getRunDisplayName(run)}): ${statusInfo.text} (${elapsedStr})`;
286
- process.stdout.write(`\r${message}`);
316
+ const label = run.status === 'WAITING'
317
+ ? picocolors_1.default.yellow(statusInfo.text)
318
+ : picocolors_1.default.cyan(statusInfo.text);
319
+ activeMessages.push(`${label} ${picocolors_1.default.dim(`• Run ${run.id} (${this.getRunDisplayName(run)}) • ${elapsedStr}`)}`);
287
320
  }
288
321
  else if (statusChanged) {
289
- console.log(` ${statusInfo.emoji} Run ${run.id} (${this.getRunDisplayName(run)}): ${statusInfo.text}`);
322
+ this.spinner.clearLine();
323
+ console.log(` ${statusInfo.symbol} Run ${run.id} ${picocolors_1.default.dim(`(${this.getRunDisplayName(run)})`)}: ${statusInfo.text}`);
290
324
  }
291
325
  }
326
+ if (activeMessages.length > 0) {
327
+ this.spinner.setMessage(activeMessages.join(picocolors_1.default.dim(' ┊ ')));
328
+ }
329
+ else {
330
+ this.spinner.stop();
331
+ }
292
332
  }
293
333
  /**
294
334
  * Get the display name for a run, preferring environment.name over capabilities.deviceName
@@ -300,15 +340,15 @@ class XCUITest extends base_provider_1.default {
300
340
  getStatusInfo(status) {
301
341
  switch (status) {
302
342
  case 'WAITING':
303
- return { emoji: '', text: 'Waiting for test to start' };
343
+ return { symbol: picocolors_1.default.yellow(''), text: 'Waiting for test to start' };
304
344
  case 'READY':
305
- return { emoji: '🔄', text: 'Running test' };
345
+ return { symbol: picocolors_1.default.cyan(''), text: 'Running test' };
306
346
  case 'DONE':
307
- return { emoji: '', text: 'Test has finished running' };
347
+ return { symbol: picocolors_1.default.green(''), text: 'Test has finished running' };
308
348
  case 'FAILED':
309
- return { emoji: '', text: 'Test failed' };
349
+ return { symbol: picocolors_1.default.red(''), text: 'Test failed' };
310
350
  default:
311
- return { emoji: '', text: status };
351
+ return { symbol: picocolors_1.default.dim('?'), text: status };
312
352
  }
313
353
  }
314
354
  async fetchReports(runs) {
@@ -334,7 +374,7 @@ class XCUITest extends base_provider_1.default {
334
374
  password: this.credentials.accessKey,
335
375
  },
336
376
  responseType: reportFormat === 'html' ? 'arraybuffer' : 'text',
337
- timeout: 30000, // 30 second timeout
377
+ timeout: constants_1.HTTP.TIMEOUT_MS,
338
378
  });
339
379
  // Check for version update notification
340
380
  const latestVersion = response.headers?.['x-testingbotctl-version'];
@@ -365,9 +405,9 @@ class XCUITest extends base_provider_1.default {
365
405
  this.socket = (0, socket_io_client_1.io)(this.updateServer, {
366
406
  transports: ['websocket'],
367
407
  reconnection: true,
368
- reconnectionAttempts: 3,
369
- reconnectionDelay: 1000,
370
- timeout: 10000,
408
+ reconnectionAttempts: constants_1.SOCKET.RECONNECTION_ATTEMPTS,
409
+ reconnectionDelay: constants_1.SOCKET.RECONNECTION_DELAY_MS,
410
+ timeout: constants_1.SOCKET.TIMEOUT_MS,
371
411
  });
372
412
  this.socket.on('connect', () => {
373
413
  // Join the room for this test run
@@ -379,8 +419,12 @@ class XCUITest extends base_provider_1.default {
379
419
  this.socket.on('xcuitest_error', (data) => {
380
420
  this.handleXCUITestError(data);
381
421
  });
382
- this.socket.on('connect_error', () => {
383
- // Silently fail - real-time updates are optional
422
+ this.socket.on('connect_error', (err) => {
423
+ if (!this.socketFallbackWarned) {
424
+ this.socketFallbackWarned = true;
425
+ logger_1.default.warn('Real-time log stream unavailable, falling back to polling.');
426
+ logger_1.default.debug(`Socket connect_error: ${err?.message ?? 'unknown error'}`);
427
+ }
384
428
  this.disconnectFromUpdateServer();
385
429
  });
386
430
  }
@@ -399,8 +443,8 @@ class XCUITest extends base_provider_1.default {
399
443
  try {
400
444
  const message = JSON.parse(data);
401
445
  if (message.payload) {
402
- // Clear the status line before printing output
403
- this.clearLine();
446
+ // Clear the spinner line before printing output
447
+ this.spinner.clearLine();
404
448
  // Print the XCUITest output
405
449
  process.stdout.write(message.payload);
406
450
  }
@@ -413,8 +457,8 @@ class XCUITest extends base_provider_1.default {
413
457
  try {
414
458
  const message = JSON.parse(data);
415
459
  if (message.payload) {
416
- // Clear the status line before printing error
417
- this.clearLine();
460
+ // Clear the spinner line before printing error
461
+ this.spinner.clearLine();
418
462
  // Print the error output
419
463
  process.stderr.write(message.payload);
420
464
  }
@@ -0,0 +1,3 @@
1
+ export declare function printBanner(): void;
2
+ export default printBanner;
3
+ //# sourceMappingURL=banner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"banner.d.ts","sourceRoot":"","sources":["../../src/ui/banner.ts"],"names":[],"mappings":"AAsDA,wBAAgB,WAAW,IAAI,IAAI,CA8BlC;AAED,eAAe,WAAW,CAAC"}
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.printBanner = printBanner;
7
+ const picocolors_1 = __importDefault(require("picocolors"));
8
+ const node_os_1 = __importDefault(require("node:os"));
9
+ const utils_1 = __importDefault(require("../utils"));
10
+ const package_json_1 = __importDefault(require("../../package.json"));
11
+ // Hand-crafted 3-row block letters for TESTINGBOT using half-block
12
+ // characters (▀ ▄ █) so each text row encodes two pixel rows — same
13
+ // readability as a 5-row font, half the vertical space.
14
+ const LETTERS = {
15
+ T: ['▀█▀', ' █ ', ' ▀ '],
16
+ E: ['█▀▀', '█▀ ', '▀▀▀'],
17
+ S: ['▄▀▀▀', ' ▀▀▄', '▀▀▀ '],
18
+ I: ['▀█▀', ' █ ', '▀▀▀'],
19
+ N: ['█▄ █', '█ ▀█', '▀ ▀'],
20
+ G: ['▄▀▀▀', '█ ▀█', ' ▀▀▀'],
21
+ B: ['█▀▀▄', '█▀▀▄', '▀▀▀ '],
22
+ O: ['█▀▀█', '█ █', '▀▀▀▀'],
23
+ };
24
+ const WORD = 'TESTINGBOT';
25
+ const LETTER_GAP = ' ';
26
+ const LETTER_ROWS = 3;
27
+ function buildBigText() {
28
+ const rows = [];
29
+ for (let r = 0; r < LETTER_ROWS; r++) {
30
+ const parts = WORD.split('').map((ch) => LETTERS[ch][r]);
31
+ rows.push(parts.join(LETTER_GAP));
32
+ }
33
+ return rows;
34
+ }
35
+ function visibleLen(s) {
36
+ return Array.from(s).length;
37
+ }
38
+ function shouldSkipBanner(argv) {
39
+ if (!utils_1.default.isInteractive())
40
+ return true;
41
+ for (const arg of argv) {
42
+ if (arg === '--help' ||
43
+ arg === '-h' ||
44
+ arg === '--version' ||
45
+ arg === '-v' ||
46
+ arg === '-V' ||
47
+ arg === '--quiet' ||
48
+ arg === '-q') {
49
+ return true;
50
+ }
51
+ }
52
+ return false;
53
+ }
54
+ function printBanner() {
55
+ if (shouldSkipBanner(process.argv.slice(2)))
56
+ return;
57
+ const home = node_os_1.default.homedir();
58
+ const rawCwd = process.cwd();
59
+ const cwd = rawCwd.startsWith(home)
60
+ ? '~' + rawCwd.slice(home.length)
61
+ : rawCwd;
62
+ const version = `v${package_json_1.default.version}`;
63
+ const bigText = buildBigText();
64
+ const textWidth = Math.max(...bigText.map((l) => visibleLen(l)));
65
+ const infoLines = [
66
+ `${picocolors_1.default.dim('Espresso · XCUITest · Maestro')} ${picocolors_1.default.dim('·')} ${picocolors_1.default.dim(version)}`,
67
+ picocolors_1.default.dim(cwd),
68
+ ];
69
+ console.log();
70
+ for (const line of bigText) {
71
+ console.log(' ' + picocolors_1.default.blue(line));
72
+ }
73
+ // Info lines sit flush-left under the big text, roughly aligned with its
74
+ // left edge, so both read as one block.
75
+ void textWidth;
76
+ console.log();
77
+ for (const line of infoLines) {
78
+ console.log(' ' + line);
79
+ }
80
+ console.log();
81
+ }
82
+ exports.default = printBanner;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Single-line animated spinner for long-running status updates. Writes
3
+ * `\r{frame} {message}` on each tick to rewrite the current line in place.
4
+ *
5
+ * In non-interactive environments (no TTY, or CI=1) the spinner is a no-op —
6
+ * providers already `console.log` status transitions, so CI logs stay readable.
7
+ */
8
+ export default class Spinner {
9
+ private frame;
10
+ private timer;
11
+ private message;
12
+ private active;
13
+ private readonly interactive;
14
+ constructor();
15
+ /** Starts (or refreshes) the spinner with the given message. */
16
+ start(message: string): void;
17
+ /** Updates the visible message; starts the spinner if it wasn't running. */
18
+ setMessage(message: string): void;
19
+ /**
20
+ * Clears the current spinner line without stopping the timer. Call before
21
+ * printing unrelated output that shouldn't be overwritten by the next tick.
22
+ */
23
+ clearLine(): void;
24
+ /**
25
+ * Stops the animation, clears the line, and optionally prints a final
26
+ * non-animated line in its place.
27
+ */
28
+ stop(finalLine?: string): void;
29
+ isActive(): boolean;
30
+ private render;
31
+ }
32
+ //# sourceMappingURL=spinner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spinner.d.ts","sourceRoot":"","sources":["../../src/ui/spinner.ts"],"names":[],"mappings":"AAOA;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,OAAO,OAAO;IAC1B,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,KAAK,CAA+B;IAC5C,OAAO,CAAC,OAAO,CAAM;IACrB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAU;;IAMtC,gEAAgE;IACzD,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAiBnC,4EAA4E;IACrE,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAUxC;;;OAGG;IACI,SAAS,IAAI,IAAI;IAKxB;;;OAGG;IACI,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IAc9B,QAAQ,IAAI,OAAO;IAI1B,OAAO,CAAC,MAAM;CAKf"}
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const picocolors_1 = __importDefault(require("picocolors"));
7
+ const utils_1 = __importDefault(require("../utils"));
8
+ const platform_1 = __importDefault(require("../utils/platform"));
9
+ const BRAILLE_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
10
+ const TICK_MS = 80;
11
+ /**
12
+ * Single-line animated spinner for long-running status updates. Writes
13
+ * `\r{frame} {message}` on each tick to rewrite the current line in place.
14
+ *
15
+ * In non-interactive environments (no TTY, or CI=1) the spinner is a no-op —
16
+ * providers already `console.log` status transitions, so CI logs stay readable.
17
+ */
18
+ class Spinner {
19
+ frame = 0;
20
+ timer = null;
21
+ message = '';
22
+ active = false;
23
+ interactive;
24
+ constructor() {
25
+ this.interactive = utils_1.default.isInteractive();
26
+ }
27
+ /** Starts (or refreshes) the spinner with the given message. */
28
+ start(message) {
29
+ this.message = message;
30
+ if (!this.interactive)
31
+ return;
32
+ if (this.active) {
33
+ this.render();
34
+ return;
35
+ }
36
+ this.active = true;
37
+ this.render();
38
+ this.timer = setInterval(() => {
39
+ this.frame = (this.frame + 1) % BRAILLE_FRAMES.length;
40
+ this.render();
41
+ }, TICK_MS);
42
+ // Don't keep the event loop alive on this timer.
43
+ this.timer.unref?.();
44
+ }
45
+ /** Updates the visible message; starts the spinner if it wasn't running. */
46
+ setMessage(message) {
47
+ if (!this.active) {
48
+ this.start(message);
49
+ return;
50
+ }
51
+ if (this.message === message)
52
+ return;
53
+ this.message = message;
54
+ this.render();
55
+ }
56
+ /**
57
+ * Clears the current spinner line without stopping the timer. Call before
58
+ * printing unrelated output that shouldn't be overwritten by the next tick.
59
+ */
60
+ clearLine() {
61
+ if (!this.interactive || !this.active)
62
+ return;
63
+ platform_1.default.clearLine();
64
+ }
65
+ /**
66
+ * Stops the animation, clears the line, and optionally prints a final
67
+ * non-animated line in its place.
68
+ */
69
+ stop(finalLine) {
70
+ if (this.timer) {
71
+ clearInterval(this.timer);
72
+ this.timer = null;
73
+ }
74
+ if (this.active && this.interactive) {
75
+ platform_1.default.clearLine();
76
+ }
77
+ this.active = false;
78
+ if (finalLine !== undefined) {
79
+ console.log(finalLine);
80
+ }
81
+ }
82
+ isActive() {
83
+ return this.active;
84
+ }
85
+ render() {
86
+ if (!this.interactive)
87
+ return;
88
+ const frame = picocolors_1.default.cyan(BRAILLE_FRAMES[this.frame]);
89
+ process.stdout.write(`\r${frame} ${this.message}`);
90
+ }
91
+ }
92
+ exports.default = Spinner;
@@ -0,0 +1,8 @@
1
+ export declare function setTitle(title: string): void;
2
+ export declare function resetTitle(): void;
3
+ declare const _default: {
4
+ setTitle: typeof setTitle;
5
+ resetTitle: typeof resetTitle;
6
+ };
7
+ export default _default;
8
+ //# sourceMappingURL=terminal-title.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terminal-title.d.ts","sourceRoot":"","sources":["../../src/ui/terminal-title.ts"],"names":[],"mappings":"AAsCA,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAO5C;AAED,wBAAgB,UAAU,IAAI,IAAI,CAIjC;;;;;AAED,wBAAwC"}
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.setTitle = setTitle;
7
+ exports.resetTitle = resetTitle;
8
+ const utils_1 = __importDefault(require("../utils"));
9
+ /**
10
+ * Updates the terminal tab/window title via the OSC 2 escape sequence.
11
+ * No-op in CI or when stdout isn't a TTY — CI logs shouldn't carry control
12
+ * sequences, and non-interactive consumers wouldn't see the effect anyway.
13
+ *
14
+ * An exit handler (installed lazily) resets the title so the user's shell
15
+ * doesn't keep our last status after the CLI exits.
16
+ */
17
+ const RESET_TITLE = 'Terminal';
18
+ let exitHookInstalled = false;
19
+ let lastSetTitle = null;
20
+ function canWriteTitle() {
21
+ return utils_1.default.isInteractive();
22
+ }
23
+ function writeOsc(title) {
24
+ // OSC 0 sets both the icon/tab title AND the window title. iTerm2 shows
25
+ // the session name in the tab and ignores OSC 2 (window-title-only), so
26
+ // OSC 0 is the portable choice across Terminal.app, iTerm2, most Linux
27
+ // terminals, and Windows Terminal.
28
+ process.stdout.write(`\x1b]0;${title}\x07`);
29
+ }
30
+ function installExitHook() {
31
+ if (exitHookInstalled)
32
+ return;
33
+ exitHookInstalled = true;
34
+ const reset = () => {
35
+ if (canWriteTitle() && lastSetTitle !== null) {
36
+ writeOsc(RESET_TITLE);
37
+ }
38
+ };
39
+ process.on('exit', reset);
40
+ }
41
+ function setTitle(title) {
42
+ if (!canWriteTitle())
43
+ return;
44
+ const full = `testingbot · ${title}`;
45
+ if (full === lastSetTitle)
46
+ return;
47
+ lastSetTitle = full;
48
+ writeOsc(full);
49
+ installExitHook();
50
+ }
51
+ function resetTitle() {
52
+ if (!canWriteTitle())
53
+ return;
54
+ lastSetTitle = null;
55
+ writeOsc(RESET_TITLE);
56
+ }
57
+ exports.default = { setTitle, resetTitle };
package/dist/upload.d.ts CHANGED
@@ -20,6 +20,10 @@ export default class Upload {
20
20
  * Format file size in human-readable format (KB for small files, MB for larger)
21
21
  */
22
22
  private formatFileSize;
23
+ /**
24
+ * Format seconds as compact duration: "12s" or "3m04s".
25
+ */
26
+ private formatDuration;
23
27
  private validateFile;
24
28
  /**
25
29
  * Validate that the file is a valid zip-based archive.
@@ -1 +1 @@
1
- {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"AAMA,OAAO,WAAW,MAAM,sBAAsB,CAAC;AAK/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;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;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;IAyGlE,OAAO,CAAC,eAAe;IAmBvB;;OAEG;IACH,OAAO,CAAC,cAAc;YAUR,YAAY;IAQ1B;;;OAGG;YACW,iBAAiB;IAqB/B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAuB3B;;;OAGG;IACU,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAUlE"}
1
+ {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"AAMA,OAAO,WAAW,MAAM,sBAAsB,CAAC;AAK/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;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;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;IAkJlE,OAAO,CAAC,eAAe;IAuCvB;;OAEG;IACH,OAAO,CAAC,cAAc;IAUtB;;OAEG;IACH,OAAO,CAAC,cAAc;YAOR,YAAY;IAQ1B;;;OAGG;YACW,iBAAiB;IAqB/B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAuB3B;;;OAGG;IACU,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAUlE"}