@testingbot/cli 1.0.2 → 1.0.3

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/README.md CHANGED
@@ -1,9 +1,56 @@
1
- [![Run Tests](https://github.com/testingbot/testingbotctl/actions/workflows/test.yml/badge.svg)](https://github.com/testingbot/testingbotctl/actions/workflows/test.yml)
2
- [![npm version](https://img.shields.io/npm/v/@testingbot/cli.svg)](https://www.npmjs.com/package/@testingbot/cli)
1
+ <p align="center">
2
+ <a href="https://testingbot.com">
3
+ <img src="resources/logo.svg" alt="Maestro Cloud Testing" width="400">
4
+ </a>
5
+ <br>
6
+ <strong>Run mobile tests on real devices in the cloud</strong>
7
+ </p>
8
+
9
+ <p align="center">
10
+ <a href="https://github.com/testingbot/testingbotctl/actions/workflows/test.yml">
11
+ <img src="https://github.com/testingbot/testingbotctl/actions/workflows/test.yml/badge.svg" alt="Run Tests">
12
+ </a>
13
+ <a href="https://www.npmjs.com/package/@testingbot/cli">
14
+ <img src="https://img.shields.io/npm/v/@testingbot/cli.svg" alt="npm version">
15
+ </a>
16
+ <a href="https://www.npmjs.com/package/@testingbot/cli">
17
+ <img src="https://img.shields.io/npm/dm/@testingbot/cli.svg" alt="npm downloads">
18
+ </a>
19
+ <a href="https://github.com/testingbot/testingbotctl/blob/main/LICENSE">
20
+ <img src="https://img.shields.io/npm/l/@testingbot/cli.svg" alt="license">
21
+ </a>
22
+ </p>
23
+
24
+ <p align="center">
25
+ <a href="https://testingbot.com">Website</a>
26
+ ·
27
+ <a href="https://testingbot.com/support/app-automate/maestro">Documentation</a>
28
+ ·
29
+ <a href="https://www.npmjs.com/package/@testingbot/cli">npm</a>
30
+ </p>
3
31
 
4
- # TestingBot CLI
32
+ ---
33
+
34
+ <p align="center">
35
+ <img src="demo/demo.gif" alt="TestingBot CLI Demo" width="700">
36
+ </p>
37
+
38
+ ---
39
+
40
+ Run **Espresso**, **XCUITest** and **Maestro** tests on real devices in the cloud.
5
41
 
6
- CLI tool to run Espresso, XCUITest and Maestro tests on [TestingBot's](https://testingbot.com) cloud infrastructure.
42
+ - **Real Devices** Test on thousands of real iOS and Android devices
43
+ - **Emulators & Simulators** — Fast feedback with virtual devices
44
+ - **Parallel Execution** — Split tests across multiple devices with sharding
45
+ - **CI/CD Ready** — Integrates with GitHub Actions, Jenkins, and more
46
+ - **Live Results** — Watch tests run in real-time
47
+ - **Artifacts** — Download videos, screenshots, and logs
48
+
49
+ <p align="center">
50
+ <a href="#installation"><strong>Get Started →</strong></a>
51
+ </p>
52
+
53
+ ---
7
54
 
8
55
  ## Installation
9
56
 
@@ -83,9 +130,25 @@ testingbot maestro <app> <flows...> [options]
83
130
  | `-q, --quiet` | Suppress progress output |
84
131
  | `--report <format>` | Download report after completion: html or junit |
85
132
  | `--report-output-dir <path>` | Directory to save reports (required with --report) |
86
- | `--download-artifacts` | Download test artifacts (logs, screenshots, video) |
133
+ | `--download-artifacts [mode]` | Download test artifacts (logs, screenshots, video). Mode: `all` (default) or `failed` |
87
134
  | `--artifacts-output-dir <path>` | Directory to save artifacts zip (defaults to current directory) |
88
135
 
136
+ **Advanced Options:**
137
+
138
+ | Option | Description |
139
+ |--------|-------------|
140
+ | `--shard-split <number>` | Split flows into N parallel sessions for faster execution |
141
+ | `--ignore-checksum-check` | Skip checksum verification and always upload the app |
142
+
143
+ **CI/CD Integration:**
144
+
145
+ | Option | Description |
146
+ |--------|-------------|
147
+ | `--commit-sha <sha>` | Git commit SHA associated with this test run |
148
+ | `--pull-request-id <id>` | Pull request ID this test run originated from |
149
+ | `--repo-name <name>` | Repository name (e.g., GitHub repo slug) |
150
+ | `--repo-owner <owner>` | Repository owner (e.g., GitHub organization or username) |
151
+
89
152
  **Examples:**
90
153
 
91
154
  ```sh
@@ -110,8 +173,21 @@ testingbot maestro app.apk ./flows --report junit --report-output-dir ./reports
110
173
  # Download all artifacts (logs, screenshots, video)
111
174
  testingbot maestro app.apk ./flows --download-artifacts --build "build-123"
112
175
 
176
+ # Download artifacts only for failed tests
177
+ testingbot maestro app.apk ./flows --download-artifacts failed --artifacts-output-dir ./artifacts
178
+
113
179
  # Run in background (async)
114
180
  testingbot maestro app.apk ./flows --async
181
+
182
+ # Split flows across 3 shards, grouping all flows over 3 parallel sessions
183
+ testingbot maestro app.apk ./flows --shard-split 3
184
+
185
+ # CI/CD integration with Git metadata
186
+ testingbot maestro app.apk ./flows \
187
+ --commit-sha "abc123def" \
188
+ --pull-request-id "42" \
189
+ --repo-owner "myorg" \
190
+ --repo-name "myapp"
115
191
  ```
116
192
 
117
193
  ---
@@ -325,7 +401,7 @@ testingbot xcuitest app.ipa app-test.zip \
325
401
  ### Real-time Progress
326
402
 
327
403
  By default, the CLI shows real-time progress updates including:
328
- - Test status updates
404
+ - Test status updates with actual device names (even when using wildcards)
329
405
  - Device allocation status
330
406
  - Live output from Maestro flows
331
407
 
package/dist/logger.js CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- const colors_1 = __importDefault(require("colors"));
6
+ const picocolors_1 = __importDefault(require("picocolors"));
7
7
  const tracer_1 = __importDefault(require("tracer"));
8
8
  const logger = tracer_1.default.colorConsole({
9
9
  level: 'info',
@@ -11,9 +11,9 @@ const logger = tracer_1.default.colorConsole({
11
11
  dateformat: 'HH:MM:ss.L',
12
12
  filters: [
13
13
  {
14
- warn: colors_1.default.red,
15
- debug: colors_1.default.blue,
16
- error: [colors_1.default.red, colors_1.default.bold],
14
+ warn: picocolors_1.default.red,
15
+ debug: picocolors_1.default.blue,
16
+ error: (text) => picocolors_1.default.bold(picocolors_1.default.red(text)),
17
17
  },
18
18
  ],
19
19
  });
@@ -33,6 +33,7 @@ export interface MaestroRunOptions {
33
33
  version?: string;
34
34
  }
35
35
  export default class MaestroOptions {
36
+ private static isIpaFile;
36
37
  private _app;
37
38
  private _flows;
38
39
  private _device?;
@@ -1 +1 @@
1
- {"version":3,"file":"maestro_options.d.ts","sourceRoot":"","sources":["../../src/models/maestro_options.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"maestro_options.d.ts","sourceRoot":"","sources":["../../src/models/maestro_options.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  class MaestroOptions {
4
+ static isIpaFile(app) {
5
+ return app?.toLowerCase().endsWith('.ipa') ?? false;
6
+ }
4
7
  _app;
5
8
  _flows;
6
9
  _device;
@@ -47,7 +50,8 @@ class MaestroOptions {
47
50
  this._async = options?.async ?? false;
48
51
  this._report = options?.report;
49
52
  this._reportOutputDir = options?.reportOutputDir;
50
- this._realDevice = options?.realDevice ?? false;
53
+ // IPA files can only be tested on real iOS devices, so automatically enable realDevice
54
+ this._realDevice = MaestroOptions.isIpaFile(app) || (options?.realDevice ?? false);
51
55
  this._downloadArtifacts = options?.downloadArtifacts;
52
56
  this._artifactsOutputDir = options?.artifactsOutputDir;
53
57
  this._ignoreChecksumCheck = options?.ignoreChecksumCheck ?? false;
@@ -23,28 +23,26 @@ class Espresso extends base_provider_1.default {
23
23
  if (this.options.app === undefined) {
24
24
  throw new testingbot_error_1.default(`app option is required`);
25
25
  }
26
- try {
27
- await node_fs_1.default.promises.access(this.options.app, node_fs_1.default.constants.R_OK);
28
- }
29
- catch {
30
- throw new testingbot_error_1.default(`Provided app path does not exist ${this.options.app}`);
31
- }
32
26
  if (this.options.testApp === undefined) {
33
27
  throw new testingbot_error_1.default(`testApp option is required`);
34
28
  }
35
- try {
36
- await node_fs_1.default.promises.access(this.options.testApp, node_fs_1.default.constants.R_OK);
37
- }
38
- catch {
39
- throw new testingbot_error_1.default(`testApp path does not exist ${this.options.testApp}`);
40
- }
41
29
  // Validate report options
42
30
  if (this.options.report && !this.options.reportOutputDir) {
43
31
  throw new testingbot_error_1.default(`--report-output-dir is required when --report is specified`);
44
32
  }
33
+ // Validate file access in parallel for better performance
34
+ const fileChecks = [
35
+ node_fs_1.default.promises.access(this.options.app, node_fs_1.default.constants.R_OK).catch(() => {
36
+ throw new testingbot_error_1.default(`Provided app path does not exist ${this.options.app}`);
37
+ }),
38
+ node_fs_1.default.promises.access(this.options.testApp, node_fs_1.default.constants.R_OK).catch(() => {
39
+ throw new testingbot_error_1.default(`testApp path does not exist ${this.options.testApp}`);
40
+ }),
41
+ ];
45
42
  if (this.options.reportOutputDir) {
46
- await this.ensureOutputDirectory(this.options.reportOutputDir);
43
+ fileChecks.push(this.ensureOutputDirectory(this.options.reportOutputDir));
47
44
  }
45
+ await Promise.all(fileChecks);
48
46
  return true;
49
47
  }
50
48
  async run() {
@@ -56,6 +56,11 @@ export interface MaestroSocketMessage {
56
56
  id: number;
57
57
  payload: string;
58
58
  }
59
+ export interface MissingFileReference {
60
+ flowFile: string;
61
+ referencedFile: string;
62
+ resolvedPath: string;
63
+ }
59
64
  export default class Maestro extends BaseProvider<MaestroOptions> {
60
65
  protected readonly URL = "https://api.testingbot.com/v1/app-automate/maestro";
61
66
  private detectedPlatform;
@@ -63,6 +68,7 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
63
68
  private updateServer;
64
69
  private updateKey;
65
70
  constructor(credentials: Credentials, options: MaestroOptions);
71
+ private static readonly SUPPORTED_APP_EXTENSIONS;
66
72
  private validate;
67
73
  /**
68
74
  * Detect platform from app file content using magic bytes
@@ -86,6 +92,20 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
86
92
  * Recursively extract file paths from any value in the YAML structure
87
93
  */
88
94
  private extractPathsFromValue;
95
+ /**
96
+ * Find all file references in flow files that don't exist on disk.
97
+ * This validates that all referenced files (runScript, runFlow, addMedia, etc.)
98
+ * will be included in the zip.
99
+ */
100
+ findMissingReferences(flowFiles: string[], allIncludedFiles: string[], baseDir?: string): Promise<MissingFileReference[]>;
101
+ /**
102
+ * Recursively find missing file references in a YAML value
103
+ */
104
+ private findMissingInValue;
105
+ /**
106
+ * Log warnings for missing file references
107
+ */
108
+ private logMissingReferences;
89
109
  private logIncludedFiles;
90
110
  private createFlowsZip;
91
111
  private runTests;
@@ -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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"}
@@ -48,7 +48,7 @@ const socket_io_client_1 = require("socket.io-client");
48
48
  const testingbot_error_1 = __importDefault(require("../models/testingbot_error"));
49
49
  const utils_1 = __importDefault(require("../utils"));
50
50
  const file_type_detector_1 = require("../utils/file-type-detector");
51
- const colors_1 = __importDefault(require("colors"));
51
+ const picocolors_1 = __importDefault(require("picocolors"));
52
52
  const base_provider_1 = __importDefault(require("./base_provider"));
53
53
  class Maestro extends base_provider_1.default {
54
54
  URL = 'https://api.testingbot.com/v1/app-automate/maestro';
@@ -59,42 +59,53 @@ class Maestro extends base_provider_1.default {
59
59
  constructor(credentials, options) {
60
60
  super(credentials, options);
61
61
  }
62
+ static SUPPORTED_APP_EXTENSIONS = [
63
+ '.apk',
64
+ '.apks',
65
+ '.ipa',
66
+ '.app',
67
+ '.zip',
68
+ ];
62
69
  async validate() {
63
70
  if (this.options.app === undefined) {
64
71
  throw new testingbot_error_1.default(`app option is required`);
65
72
  }
66
- try {
67
- await node_fs_1.default.promises.access(this.options.app, node_fs_1.default.constants.R_OK);
68
- }
69
- catch {
70
- throw new testingbot_error_1.default(`Provided app path does not exist ${this.options.app}`);
73
+ // Validate app file extension
74
+ const appExt = node_path_1.default.extname(this.options.app).toLowerCase();
75
+ if (!Maestro.SUPPORTED_APP_EXTENSIONS.includes(appExt)) {
76
+ throw new testingbot_error_1.default(`Unsupported app file format: ${appExt || '(no extension)'}. ` +
77
+ `Supported formats: ${Maestro.SUPPORTED_APP_EXTENSIONS.join(', ')}`);
71
78
  }
72
79
  if (this.options.flows === undefined || this.options.flows.length === 0) {
73
80
  throw new testingbot_error_1.default(`flows option is required`);
74
81
  }
82
+ if (this.options.report && !this.options.reportOutputDir) {
83
+ throw new testingbot_error_1.default(`--report-output-dir is required when --report is specified`);
84
+ }
85
+ // Build list of all file checks to run in parallel
86
+ const fileChecks = [
87
+ node_fs_1.default.promises.access(this.options.app, node_fs_1.default.constants.R_OK).catch(() => {
88
+ throw new testingbot_error_1.default(`Provided app path does not exist ${this.options.app}`);
89
+ }),
90
+ ];
75
91
  // Check if all flows paths exist (can be files, directories or glob patterns)
76
92
  for (const flowsPath of this.options.flows) {
77
93
  const isGlobPattern = flowsPath.includes('*') ||
78
94
  flowsPath.includes('?') ||
79
95
  flowsPath.includes('{');
80
96
  if (!isGlobPattern) {
81
- try {
82
- await node_fs_1.default.promises.access(flowsPath, node_fs_1.default.constants.R_OK);
83
- }
84
- catch {
97
+ fileChecks.push(node_fs_1.default.promises.access(flowsPath, node_fs_1.default.constants.R_OK).catch(() => {
85
98
  throw new testingbot_error_1.default(`flows path does not exist ${flowsPath}`);
86
- }
99
+ }));
87
100
  }
88
101
  }
89
- if (this.options.report && !this.options.reportOutputDir) {
90
- throw new testingbot_error_1.default(`--report-output-dir is required when --report is specified`);
91
- }
92
102
  if (this.options.reportOutputDir) {
93
- await this.ensureOutputDirectory(this.options.reportOutputDir);
103
+ fileChecks.push(this.ensureOutputDirectory(this.options.reportOutputDir));
94
104
  }
95
105
  if (this.options.downloadArtifacts && this.options.artifactsOutputDir) {
96
- await this.ensureOutputDirectory(this.options.artifactsOutputDir);
106
+ fileChecks.push(this.ensureOutputDirectory(this.options.artifactsOutputDir));
97
107
  }
108
+ await Promise.all(fileChecks);
98
109
  return true;
99
110
  }
100
111
  /**
@@ -293,6 +304,11 @@ class Maestro extends base_provider_1.default {
293
304
  if (!this.options.quiet) {
294
305
  this.logIncludedFiles(allFlowFiles, baseDir);
295
306
  }
307
+ // Check for missing file references and warn the user
308
+ const missingReferences = await this.findMissingReferences(allFlowFiles, allFlowFiles, baseDir);
309
+ if (!this.options.quiet && missingReferences.length > 0) {
310
+ this.logMissingReferences(missingReferences, baseDir);
311
+ }
296
312
  zipPath = await this.createFlowsZip(allFlowFiles, baseDir);
297
313
  shouldCleanup = true;
298
314
  try {
@@ -539,6 +555,205 @@ class Maestro extends base_provider_1.default {
539
555
  }
540
556
  return dependencies;
541
557
  }
558
+ /**
559
+ * Find all file references in flow files that don't exist on disk.
560
+ * This validates that all referenced files (runScript, runFlow, addMedia, etc.)
561
+ * will be included in the zip.
562
+ */
563
+ async findMissingReferences(flowFiles, allIncludedFiles, baseDir) {
564
+ const missingReferences = [];
565
+ const includedFilesSet = new Set(allIncludedFiles.map((f) => node_path_1.default.resolve(f)));
566
+ for (const flowFile of flowFiles) {
567
+ const ext = node_path_1.default.extname(flowFile).toLowerCase();
568
+ if (ext !== '.yaml' && ext !== '.yml') {
569
+ continue;
570
+ }
571
+ try {
572
+ const content = await node_fs_1.default.promises.readFile(flowFile, 'utf-8');
573
+ const documents = [];
574
+ yaml.loadAll(content, (doc) => documents.push(doc));
575
+ for (const flowData of documents) {
576
+ if (flowData !== null && typeof flowData === 'object') {
577
+ const missing = await this.findMissingInValue(flowData, flowFile, includedFilesSet);
578
+ missingReferences.push(...missing);
579
+ }
580
+ }
581
+ }
582
+ catch {
583
+ // Ignore parsing errors
584
+ }
585
+ }
586
+ return missingReferences;
587
+ }
588
+ /**
589
+ * Recursively find missing file references in a YAML value
590
+ */
591
+ async findMissingInValue(value, flowFile, includedFiles) {
592
+ const missingReferences = [];
593
+ if (typeof value === 'string') {
594
+ if (this.looksLikePath(value)) {
595
+ const resolvedPath = node_path_1.default.resolve(node_path_1.default.dirname(flowFile), value);
596
+ // Check if the file is in included files OR exists on disk
597
+ if (!includedFiles.has(resolvedPath)) {
598
+ try {
599
+ await node_fs_1.default.promises.access(resolvedPath);
600
+ // File exists on disk but won't be included - also warn
601
+ }
602
+ catch {
603
+ // File doesn't exist
604
+ missingReferences.push({
605
+ flowFile,
606
+ referencedFile: value,
607
+ resolvedPath,
608
+ });
609
+ }
610
+ }
611
+ }
612
+ }
613
+ else if (Array.isArray(value)) {
614
+ for (const item of value) {
615
+ const missing = await this.findMissingInValue(item, flowFile, includedFiles);
616
+ missingReferences.push(...missing);
617
+ }
618
+ }
619
+ else if (value !== null && typeof value === 'object') {
620
+ const obj = value;
621
+ const handledKeys = new Set();
622
+ // Handle runScript - extract file reference but don't recurse
623
+ // (runScript objects only contain file, env, when - no nested file refs)
624
+ if ('runScript' in obj) {
625
+ handledKeys.add('runScript');
626
+ const runScript = obj.runScript;
627
+ const scriptFile = typeof runScript === 'string'
628
+ ? runScript
629
+ : runScript?.file;
630
+ if (typeof scriptFile === 'string') {
631
+ const resolved = node_path_1.default.resolve(node_path_1.default.dirname(flowFile), scriptFile);
632
+ if (!includedFiles.has(resolved)) {
633
+ try {
634
+ await node_fs_1.default.promises.access(resolved);
635
+ }
636
+ catch {
637
+ missingReferences.push({
638
+ flowFile,
639
+ referencedFile: scriptFile,
640
+ resolvedPath: resolved,
641
+ });
642
+ }
643
+ }
644
+ }
645
+ // Don't recurse into runScript - it only has file, env, when (no nested file refs)
646
+ }
647
+ // Handle runFlow - extract file reference and recurse only into commands
648
+ if ('runFlow' in obj) {
649
+ handledKeys.add('runFlow');
650
+ const runFlow = obj.runFlow;
651
+ const flowRef = typeof runFlow === 'string'
652
+ ? runFlow
653
+ : runFlow?.file;
654
+ if (typeof flowRef === 'string') {
655
+ const resolved = node_path_1.default.resolve(node_path_1.default.dirname(flowFile), flowRef);
656
+ if (!includedFiles.has(resolved)) {
657
+ try {
658
+ await node_fs_1.default.promises.access(resolved);
659
+ }
660
+ catch {
661
+ missingReferences.push({
662
+ flowFile,
663
+ referencedFile: flowRef,
664
+ resolvedPath: resolved,
665
+ });
666
+ }
667
+ }
668
+ }
669
+ // Only recurse into 'commands' if present (for inline commands)
670
+ if (typeof runFlow === 'object' &&
671
+ runFlow !== null &&
672
+ 'commands' in runFlow) {
673
+ const commands = runFlow.commands;
674
+ if (Array.isArray(commands)) {
675
+ const nestedMissing = await this.findMissingInValue(commands, flowFile, includedFiles);
676
+ missingReferences.push(...nestedMissing);
677
+ }
678
+ }
679
+ }
680
+ // Handle addMedia
681
+ if ('addMedia' in obj) {
682
+ handledKeys.add('addMedia');
683
+ const addMedia = obj.addMedia;
684
+ const mediaFiles = Array.isArray(addMedia) ? addMedia : [addMedia];
685
+ for (const mediaFile of mediaFiles) {
686
+ if (typeof mediaFile === 'string') {
687
+ const resolved = node_path_1.default.resolve(node_path_1.default.dirname(flowFile), mediaFile);
688
+ if (!includedFiles.has(resolved)) {
689
+ try {
690
+ await node_fs_1.default.promises.access(resolved);
691
+ }
692
+ catch {
693
+ missingReferences.push({
694
+ flowFile,
695
+ referencedFile: mediaFile,
696
+ resolvedPath: resolved,
697
+ });
698
+ }
699
+ }
700
+ }
701
+ }
702
+ }
703
+ // Handle file property
704
+ if ('file' in obj && typeof obj.file === 'string') {
705
+ handledKeys.add('file');
706
+ const resolved = node_path_1.default.resolve(node_path_1.default.dirname(flowFile), obj.file);
707
+ if (!includedFiles.has(resolved)) {
708
+ try {
709
+ await node_fs_1.default.promises.access(resolved);
710
+ }
711
+ catch {
712
+ missingReferences.push({
713
+ flowFile,
714
+ referencedFile: obj.file,
715
+ resolvedPath: resolved,
716
+ });
717
+ }
718
+ }
719
+ }
720
+ // Handle onFlowStart, onFlowComplete, commands
721
+ for (const key of ['onFlowStart', 'onFlowComplete', 'commands']) {
722
+ if (key in obj) {
723
+ handledKeys.add(key);
724
+ const nested = obj[key];
725
+ if (Array.isArray(nested)) {
726
+ const nestedMissing = await this.findMissingInValue(nested, flowFile, includedFiles);
727
+ missingReferences.push(...nestedMissing);
728
+ }
729
+ }
730
+ }
731
+ // Recursively check remaining properties
732
+ for (const [key, propValue] of Object.entries(obj)) {
733
+ if (!handledKeys.has(key)) {
734
+ const nestedMissing = await this.findMissingInValue(propValue, flowFile, includedFiles);
735
+ missingReferences.push(...nestedMissing);
736
+ }
737
+ }
738
+ }
739
+ return missingReferences;
740
+ }
741
+ /**
742
+ * Log warnings for missing file references
743
+ */
744
+ logMissingReferences(missingReferences, baseDir) {
745
+ if (missingReferences.length === 0) {
746
+ return;
747
+ }
748
+ logger_1.default.warn(`Warning: ${missingReferences.length} referenced file(s) not found:`);
749
+ for (const ref of missingReferences) {
750
+ const flowRelative = baseDir
751
+ ? node_path_1.default.relative(baseDir, ref.flowFile)
752
+ : node_path_1.default.basename(ref.flowFile);
753
+ logger_1.default.warn(` In ${flowRelative}: ${ref.referencedFile}`);
754
+ }
755
+ logger_1.default.warn('These files will not be included in the upload and may cause test failures.');
756
+ }
542
757
  logIncludedFiles(files, baseDir) {
543
758
  // Get relative paths for display
544
759
  const relativePaths = files
@@ -753,9 +968,9 @@ class Maestro extends base_provider_1.default {
753
968
  process.stdout.write(`\x1b[${linesToMove}A`);
754
969
  // Clear header line, write new header, then clear separator line
755
970
  process.stdout.write('\x1b[2K');
756
- console.log(colors_1.default.dim(` ${'Duration'.padEnd(10)} ${'Status'.padEnd(8)} Flow Fail reason`));
971
+ console.log(picocolors_1.default.dim(` ${'Duration'.padEnd(10)} ${'Status'.padEnd(8)} Flow Fail reason`));
757
972
  process.stdout.write('\x1b[2K');
758
- console.log(colors_1.default.dim(` ${'─'.repeat(10)} ${'─'.repeat(8)} ${'─'.repeat(30)} ${'─'.repeat(80)}`));
973
+ console.log(picocolors_1.default.dim(` ${'─'.repeat(10)} ${'─'.repeat(8)} ${'─'.repeat(30)} ${'─'.repeat(80)}`));
759
974
  // Redraw all flows with error messages
760
975
  for (const flow of allFlows) {
761
976
  // Clear the line before writing
@@ -848,18 +1063,18 @@ class Maestro extends base_provider_1.default {
848
1063
  getFlowStatusDisplay(flow) {
849
1064
  switch (flow.status) {
850
1065
  case 'WAITING':
851
- return { text: 'WAITING', colored: colors_1.default.white('WAITING') };
1066
+ return { text: 'WAITING', colored: picocolors_1.default.white('WAITING') };
852
1067
  case 'READY':
853
- return { text: 'RUNNING', colored: colors_1.default.blue('RUNNING') };
1068
+ return { text: 'RUNNING', colored: picocolors_1.default.blue('RUNNING') };
854
1069
  case 'DONE':
855
1070
  if (flow.success === 1) {
856
- return { text: 'PASSED', colored: colors_1.default.green('PASSED') };
1071
+ return { text: 'PASSED', colored: picocolors_1.default.green('PASSED') };
857
1072
  }
858
1073
  else {
859
- return { text: 'FAILED', colored: colors_1.default.red('FAILED') };
1074
+ return { text: 'FAILED', colored: picocolors_1.default.red('FAILED') };
860
1075
  }
861
1076
  case 'FAILED':
862
- return { text: 'FAILED', colored: colors_1.default.red('FAILED') };
1077
+ return { text: 'FAILED', colored: picocolors_1.default.red('FAILED') };
863
1078
  default:
864
1079
  return { text: flow.status, colored: flow.status };
865
1080
  }
@@ -927,13 +1142,13 @@ class Maestro extends base_provider_1.default {
927
1142
  }
928
1143
  const parts = [];
929
1144
  if (waiting > 0)
930
- parts.push(colors_1.default.white(`${waiting} waiting`));
1145
+ parts.push(picocolors_1.default.white(`${waiting} waiting`));
931
1146
  if (running > 0)
932
- parts.push(colors_1.default.blue(`${running} running`));
1147
+ parts.push(picocolors_1.default.blue(`${running} running`));
933
1148
  if (passed > 0)
934
- parts.push(colors_1.default.green(`${passed} passed`));
1149
+ parts.push(picocolors_1.default.green(`${passed} passed`));
935
1150
  if (failed > 0)
936
- parts.push(colors_1.default.red(`${failed} failed`));
1151
+ parts.push(picocolors_1.default.red(`${failed} failed`));
937
1152
  return ` ... and ${remaining.length} more: ${parts.join(', ')}`;
938
1153
  }
939
1154
  displayFlowsWithLimit(flows, previousFlowStatus, hasFailures = false) {
@@ -947,7 +1162,7 @@ class Maestro extends base_provider_1.default {
947
1162
  // Show summary for remaining flows
948
1163
  if (flows.length > maxFlows) {
949
1164
  const summary = this.getRemainingSummary(flows, maxFlows);
950
- console.log(colors_1.default.dim(summary));
1165
+ console.log(picocolors_1.default.dim(summary));
951
1166
  linesWritten++;
952
1167
  }
953
1168
  return linesWritten;
@@ -959,8 +1174,8 @@ class Maestro extends base_provider_1.default {
959
1174
  header += ' Fail reason';
960
1175
  separator += ` ${'─'.repeat(80)}`;
961
1176
  }
962
- console.log(colors_1.default.dim(header));
963
- console.log(colors_1.default.dim(separator));
1177
+ console.log(picocolors_1.default.dim(header));
1178
+ console.log(picocolors_1.default.dim(separator));
964
1179
  }
965
1180
  displayFlowRow(flow, isUpdate = false, hasFailures = false) {
966
1181
  const duration = this.calculateFlowDuration(flow).padEnd(10);
@@ -976,7 +1191,7 @@ class Maestro extends base_provider_1.default {
976
1191
  let row = ` ${duration} ${statusPadded} ${name}`;
977
1192
  // Add first error message on the same line if failed and has errors
978
1193
  if (hasFailures && isFailed && errorMessages.length > 0) {
979
- row += ` ${colors_1.default.red(errorMessages[0])}`;
1194
+ row += ` ${picocolors_1.default.red(errorMessages[0])}`;
980
1195
  }
981
1196
  if (isUpdate) {
982
1197
  process.stdout.write(`\r${row}`);
@@ -990,7 +1205,7 @@ class Maestro extends base_provider_1.default {
990
1205
  // Indent to align with the Fail reason column: Duration(11) + Status(9) + Test(31) = 51 chars
991
1206
  const indent = ' '.repeat(51);
992
1207
  for (let i = 1; i < errorMessages.length; i++) {
993
- console.log(`${indent} ${colors_1.default.red(errorMessages[i])}`);
1208
+ console.log(`${indent} ${picocolors_1.default.red(errorMessages[i])}`);
994
1209
  linesWritten++;
995
1210
  }
996
1211
  }
@@ -1035,7 +1250,7 @@ class Maestro extends base_provider_1.default {
1035
1250
  // Update or add summary line for remaining flows
1036
1251
  if (hasRemaining) {
1037
1252
  const summary = this.getRemainingSummary(flows, maxFlows);
1038
- process.stdout.write(`\r\x1b[K${colors_1.default.dim(summary)}\n`);
1253
+ process.stdout.write(`\r\x1b[K${picocolors_1.default.dim(summary)}\n`);
1039
1254
  linesWritten++;
1040
1255
  }
1041
1256
  // Return the number of lines we wrote
@@ -23,28 +23,26 @@ class XCUITest extends base_provider_1.default {
23
23
  if (this.options.app === undefined) {
24
24
  throw new testingbot_error_1.default(`app option is required`);
25
25
  }
26
- try {
27
- await node_fs_1.default.promises.access(this.options.app, node_fs_1.default.constants.R_OK);
28
- }
29
- catch {
30
- throw new testingbot_error_1.default(`Provided app path does not exist ${this.options.app}`);
31
- }
32
26
  if (this.options.testApp === undefined) {
33
27
  throw new testingbot_error_1.default(`testApp option is required`);
34
28
  }
35
- try {
36
- await node_fs_1.default.promises.access(this.options.testApp, node_fs_1.default.constants.R_OK);
37
- }
38
- catch {
39
- throw new testingbot_error_1.default(`testApp path does not exist ${this.options.testApp}`);
40
- }
41
29
  // Validate report options
42
30
  if (this.options.report && !this.options.reportOutputDir) {
43
31
  throw new testingbot_error_1.default(`--report-output-dir is required when --report is specified`);
44
32
  }
33
+ // Validate file access in parallel for better performance
34
+ const fileChecks = [
35
+ node_fs_1.default.promises.access(this.options.app, node_fs_1.default.constants.R_OK).catch(() => {
36
+ throw new testingbot_error_1.default(`Provided app path does not exist ${this.options.app}`);
37
+ }),
38
+ node_fs_1.default.promises.access(this.options.testApp, node_fs_1.default.constants.R_OK).catch(() => {
39
+ throw new testingbot_error_1.default(`testApp path does not exist ${this.options.testApp}`);
40
+ }),
41
+ ];
45
42
  if (this.options.reportOutputDir) {
46
- await this.ensureOutputDirectory(this.options.reportOutputDir);
43
+ fileChecks.push(this.ensureOutputDirectory(this.options.reportOutputDir));
47
44
  }
45
+ await Promise.all(fileChecks);
48
46
  return true;
49
47
  }
50
48
  async run() {
@@ -15,7 +15,8 @@ export interface ConnectivityCheckResult {
15
15
  }
16
16
  /**
17
17
  * Check if the system has internet connectivity by testing against
18
- * multiple reliable third-party endpoints with detailed diagnostics.
18
+ * multiple reliable third-party endpoints in parallel.
19
+ * Returns as soon as one endpoint succeeds, reducing latency significantly.
19
20
  */
20
21
  export declare function checkInternetConnectivity(): Promise<ConnectivityCheckResult>;
21
22
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"connectivity.d.ts","sourceRoot":"","sources":["../../src/utils/connectivity.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,cAAc,EAAE,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAsB,yBAAyB,IAAI,OAAO,CAAC,uBAAuB,CAAC,CAmFlF;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,uBAAuB,GAC9B,MAAM,CAuBR"}
1
+ {"version":3,"file":"connectivity.d.ts","sourceRoot":"","sources":["../../src/utils/connectivity.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,cAAc,EAAE,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;CACjB;AA4DD;;;;GAIG;AACH,wBAAsB,yBAAyB,IAAI,OAAO,CAAC,uBAAuB,CAAC,CA4ClF;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,uBAAuB,GAC9B,MAAM,CAuBR"}
@@ -5,9 +5,66 @@
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.checkInternetConnectivity = checkInternetConnectivity;
7
7
  exports.formatConnectivityResults = formatConnectivityResults;
8
+ /**
9
+ * Test a single endpoint and return the result
10
+ */
11
+ async function testEndpoint(url, description) {
12
+ const startTime = Date.now();
13
+ try {
14
+ const controller = new AbortController();
15
+ const timeoutId = setTimeout(() => controller.abort(), 3000);
16
+ const response = await fetch(url, {
17
+ method: 'HEAD',
18
+ signal: controller.signal,
19
+ redirect: 'manual',
20
+ });
21
+ clearTimeout(timeoutId);
22
+ const latencyMs = Date.now() - startTime;
23
+ return {
24
+ endpoint: `${description} (${url})`,
25
+ success: true,
26
+ statusCode: response.status,
27
+ latencyMs,
28
+ };
29
+ }
30
+ catch (error) {
31
+ const latencyMs = Date.now() - startTime;
32
+ let errorMessage = 'Unknown error';
33
+ if (error instanceof Error) {
34
+ if (error.name === 'AbortError') {
35
+ errorMessage = 'Request timeout (>3s)';
36
+ }
37
+ else if (error.message.includes('fetch failed')) {
38
+ errorMessage = 'Network request failed (DNS/connection error)';
39
+ }
40
+ else if (error.message.includes('ENOTFOUND')) {
41
+ errorMessage = 'DNS resolution failed';
42
+ }
43
+ else if (error.message.includes('ECONNREFUSED')) {
44
+ errorMessage = 'Connection refused';
45
+ }
46
+ else if (error.message.includes('ETIMEDOUT')) {
47
+ errorMessage = 'Connection timeout';
48
+ }
49
+ else if (error.message.includes('ENETUNREACH')) {
50
+ errorMessage = 'Network unreachable';
51
+ }
52
+ else {
53
+ errorMessage = error.message;
54
+ }
55
+ }
56
+ return {
57
+ endpoint: `${description} (${url})`,
58
+ success: false,
59
+ error: errorMessage,
60
+ latencyMs,
61
+ };
62
+ }
63
+ }
8
64
  /**
9
65
  * Check if the system has internet connectivity by testing against
10
- * multiple reliable third-party endpoints with detailed diagnostics.
66
+ * multiple reliable third-party endpoints in parallel.
67
+ * Returns as soon as one endpoint succeeds, reducing latency significantly.
11
68
  */
12
69
  async function checkInternetConnectivity() {
13
70
  const testEndpoints = [
@@ -18,79 +75,35 @@ async function checkInternetConnectivity() {
18
75
  },
19
76
  { url: 'https://1.1.1.1/', description: 'Cloudflare DNS' },
20
77
  ];
21
- const endpointResults = [];
22
- let anySuccess = false;
23
- for (const { url, description } of testEndpoints) {
24
- const startTime = Date.now();
25
- try {
26
- const controller = new AbortController();
27
- const timeoutId = setTimeout(() => controller.abort(), 3000);
28
- const response = await fetch(url, {
29
- method: 'HEAD',
30
- signal: controller.signal,
31
- redirect: 'manual',
32
- });
33
- clearTimeout(timeoutId);
34
- const latencyMs = Date.now() - startTime;
35
- if (response) {
36
- anySuccess = true;
37
- endpointResults.push({
38
- endpoint: `${description} (${url})`,
39
- success: true,
40
- statusCode: response.status,
41
- latencyMs,
42
- });
43
- break;
78
+ // Test all endpoints in parallel
79
+ const endpointPromises = testEndpoints.map(({ url, description }) => testEndpoint(url, description));
80
+ // Use Promise.any to return on first success, or collect all failures
81
+ try {
82
+ // Create promises that only resolve on success
83
+ const successPromises = endpointPromises.map(async (promise) => {
84
+ const result = await promise;
85
+ if (result.success) {
86
+ return result;
44
87
  }
45
- }
46
- catch (error) {
47
- const latencyMs = Date.now() - startTime;
48
- let errorMessage = 'Unknown error';
49
- if (error instanceof Error) {
50
- if (error.name === 'AbortError') {
51
- errorMessage = 'Request timeout (>3s)';
52
- }
53
- else if (error.message.includes('fetch failed')) {
54
- errorMessage = 'Network request failed (DNS/connection error)';
55
- }
56
- else if (error.message.includes('ENOTFOUND')) {
57
- errorMessage = 'DNS resolution failed';
58
- }
59
- else if (error.message.includes('ECONNREFUSED')) {
60
- errorMessage = 'Connection refused';
61
- }
62
- else if (error.message.includes('ETIMEDOUT')) {
63
- errorMessage = 'Connection timeout';
64
- }
65
- else if (error.message.includes('ENETUNREACH')) {
66
- errorMessage = 'Network unreachable';
67
- }
68
- else {
69
- errorMessage = error.message;
70
- }
71
- }
72
- endpointResults.push({
73
- endpoint: `${description} (${url})`,
74
- success: false,
75
- error: errorMessage,
76
- latencyMs,
77
- });
78
- }
88
+ throw result; // Throw failures so Promise.any continues to next
89
+ });
90
+ const successResult = await Promise.any(successPromises);
91
+ return {
92
+ connected: true,
93
+ endpointResults: [successResult],
94
+ message: `Internet connectivity verified via ${successResult.endpoint} (${successResult.latencyMs}ms)`,
95
+ };
79
96
  }
80
- let message;
81
- if (anySuccess) {
82
- const successfulEndpoint = endpointResults.find((r) => r.success);
83
- message = `Internet connectivity verified via ${successfulEndpoint?.endpoint} (${successfulEndpoint?.latencyMs}ms)`;
84
- }
85
- else {
97
+ catch (aggregateError) {
98
+ // All endpoints failed - collect all results
99
+ const endpointResults = await Promise.all(endpointPromises);
86
100
  const testedEndpoints = endpointResults.map((r) => r.endpoint).join(', ');
87
- message = `No internet connectivity detected. Tested endpoints: ${testedEndpoints}`;
101
+ return {
102
+ connected: false,
103
+ endpointResults,
104
+ message: `No internet connectivity detected. Tested endpoints: ${testedEndpoints}`,
105
+ };
88
106
  }
89
- return {
90
- connected: anySuccess,
91
- endpointResults,
92
- message,
93
- };
94
107
  }
95
108
  /**
96
109
  * Format connectivity check results for display
@@ -9,7 +9,10 @@ export interface FileTypeResult {
9
9
  export declare function detectFileType(filePath: string): Promise<FileTypeResult | undefined>;
10
10
  /**
11
11
  * Detect platform (Android or iOS) from app file.
12
- * Uses magic bytes for content detection, with extension fallback for zip-based formats.
12
+ * Uses a combination of magic bytes and file extension for reliable detection.
13
+ *
14
+ * Android: .apk, .apks files
15
+ * iOS: .ipa, .app, .zip files (when not APK)
13
16
  */
14
17
  export declare function detectPlatformFromFile(filePath: string): Promise<'Android' | 'iOS' | undefined>;
15
18
  //# sourceMappingURL=file-type-detector.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"file-type-detector.d.ts","sourceRoot":"","sources":["../../src/utils/file-type-detector.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CASrC;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,SAAS,GAAG,KAAK,GAAG,SAAS,CAAC,CAmBxC"}
1
+ {"version":3,"file":"file-type-detector.d.ts","sourceRoot":"","sources":["../../src/utils/file-type-detector.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CASrC;AASD;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAC1C,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,SAAS,GAAG,KAAK,GAAG,SAAS,CAAC,CAgCxC"}
@@ -1,7 +1,11 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.detectFileType = detectFileType;
4
7
  exports.detectPlatformFromFile = detectPlatformFromFile;
8
+ const node_path_1 = __importDefault(require("node:path"));
5
9
  /**
6
10
  * Detect file type from file content using magic bytes.
7
11
  * Returns undefined if the file type cannot be determined.
@@ -17,22 +21,42 @@ async function detectFileType(filePath) {
17
21
  return undefined;
18
22
  }
19
23
  }
24
+ /**
25
+ * Get file extension in lowercase without the dot
26
+ */
27
+ function getExtension(filePath) {
28
+ return node_path_1.default.extname(filePath).toLowerCase().slice(1);
29
+ }
20
30
  /**
21
31
  * Detect platform (Android or iOS) from app file.
22
- * Uses magic bytes for content detection, with extension fallback for zip-based formats.
32
+ * Uses a combination of magic bytes and file extension for reliable detection.
33
+ *
34
+ * Android: .apk, .apks files
35
+ * iOS: .ipa, .app, .zip files (when not APK)
23
36
  */
24
37
  async function detectPlatformFromFile(filePath) {
38
+ const ext = getExtension(filePath);
25
39
  const fileType = await detectFileType(filePath);
40
+ // Check for Android APK files
26
41
  if (fileType) {
27
- // APK files are detected as 'application/zip' with ext 'apk'
28
- // or as 'application/vnd.android.package-archive'
29
42
  if (fileType.ext === 'apk' ||
30
43
  fileType.mime === 'application/vnd.android.package-archive') {
31
44
  return 'Android';
32
45
  }
33
- if (fileType.ext === 'zip' || fileType.mime === 'application/zip') {
34
- return 'iOS';
35
- }
46
+ }
47
+ // Extension-based detection (more reliable for mobile apps)
48
+ // APK and APKS are Android
49
+ if (ext === 'apk' || ext === 'apks') {
50
+ return 'Android';
51
+ }
52
+ // IPA, APP are iOS
53
+ if (ext === 'ipa' || ext === 'app') {
54
+ return 'iOS';
55
+ }
56
+ // ZIP files could be either, but commonly used for iOS simulator builds
57
+ // If magic bytes detected it as zip and extension is .zip, assume iOS
58
+ if (ext === 'zip' && fileType?.mime === 'application/zip') {
59
+ return 'iOS';
36
60
  }
37
61
  return undefined;
38
62
  }
package/dist/utils.js CHANGED
@@ -5,7 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const package_json_1 = __importDefault(require("../package.json"));
7
7
  const logger_1 = __importDefault(require("./logger"));
8
- const colors_1 = __importDefault(require("colors"));
8
+ const picocolors_1 = __importDefault(require("picocolors"));
9
9
  let versionCheckDisplayed = false;
10
10
  exports.default = {
11
11
  getUserAgent() {
@@ -42,12 +42,12 @@ exports.default = {
42
42
  if (this.compareVersions(currentVersion, latestVersion) < 0) {
43
43
  versionCheckDisplayed = true;
44
44
  const border = '─'.repeat(80);
45
- logger_1.default.info(`\nCLI Version: ${colors_1.default.cyan(currentVersion)}\n`);
46
- logger_1.default.warn(colors_1.default.yellow(border));
47
- logger_1.default.warn(colors_1.default.yellow('⚠ Update Available'));
48
- logger_1.default.warn(colors_1.default.yellow(` A new version of the TestingBot CLI is available: ${colors_1.default.green(latestVersion)}`));
49
- logger_1.default.warn(colors_1.default.yellow(` Run: ${colors_1.default.cyan('npm install -g @testingbot/cli@latest')}`));
50
- logger_1.default.warn(colors_1.default.yellow(border) + '\n');
45
+ logger_1.default.info(`\nCLI Version: ${picocolors_1.default.cyan(currentVersion)}\n`);
46
+ logger_1.default.warn(picocolors_1.default.yellow(border));
47
+ logger_1.default.warn(picocolors_1.default.yellow('⚠ Update Available'));
48
+ logger_1.default.warn(picocolors_1.default.yellow(` A new version of the TestingBot CLI is available: ${picocolors_1.default.green(latestVersion)}`));
49
+ logger_1.default.warn(picocolors_1.default.yellow(` Run: ${picocolors_1.default.cyan('npm install -g @testingbot/cli@latest')}`));
50
+ logger_1.default.warn(picocolors_1.default.yellow(border) + '\n');
51
51
  }
52
52
  },
53
53
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testingbot/cli",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
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": {
@@ -49,12 +49,12 @@
49
49
  "dependencies": {
50
50
  "archiver": "^7.0.1",
51
51
  "axios": "^1.13.2",
52
- "colors": "^1.4.0",
53
52
  "commander": "^14.0.2",
54
53
  "file-type": "^21.1.1",
55
54
  "form-data": "^4.0.5",
56
55
  "glob": "^13.0.0",
57
56
  "js-yaml": "^4.1.1",
57
+ "picocolors": "^1.1.1",
58
58
  "progress-stream": "^2.0.0",
59
59
  "socket.io-client": "^4.8.1",
60
60
  "tracer": "^1.3.0"