@testingbot/cli 1.0.0

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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +375 -0
  3. package/dist/auth.d.ts +16 -0
  4. package/dist/auth.d.ts.map +1 -0
  5. package/dist/auth.js +47 -0
  6. package/dist/cli.d.ts +4 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +329 -0
  9. package/dist/index.d.ts +3 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +8 -0
  12. package/dist/logger.d.ts +4 -0
  13. package/dist/logger.d.ts.map +1 -0
  14. package/dist/logger.js +20 -0
  15. package/dist/models/credentials.d.ts +9 -0
  16. package/dist/models/credentials.d.ts.map +1 -0
  17. package/dist/models/credentials.js +20 -0
  18. package/dist/models/espresso_options.d.ts +116 -0
  19. package/dist/models/espresso_options.d.ts.map +1 -0
  20. package/dist/models/espresso_options.js +194 -0
  21. package/dist/models/maestro_options.d.ts +101 -0
  22. package/dist/models/maestro_options.d.ts.map +1 -0
  23. package/dist/models/maestro_options.js +176 -0
  24. package/dist/models/testingbot_error.d.ts +3 -0
  25. package/dist/models/testingbot_error.d.ts.map +1 -0
  26. package/dist/models/testingbot_error.js +5 -0
  27. package/dist/models/xcuitest_options.d.ts +88 -0
  28. package/dist/models/xcuitest_options.d.ts.map +1 -0
  29. package/dist/models/xcuitest_options.js +146 -0
  30. package/dist/providers/espresso.d.ts +67 -0
  31. package/dist/providers/espresso.d.ts.map +1 -0
  32. package/dist/providers/espresso.js +527 -0
  33. package/dist/providers/login.d.ts +18 -0
  34. package/dist/providers/login.d.ts.map +1 -0
  35. package/dist/providers/login.js +284 -0
  36. package/dist/providers/maestro.d.ts +92 -0
  37. package/dist/providers/maestro.d.ts.map +1 -0
  38. package/dist/providers/maestro.js +1010 -0
  39. package/dist/providers/xcuitest.d.ts +67 -0
  40. package/dist/providers/xcuitest.d.ts.map +1 -0
  41. package/dist/providers/xcuitest.js +529 -0
  42. package/dist/upload.d.ts +21 -0
  43. package/dist/upload.d.ts.map +1 -0
  44. package/dist/upload.js +94 -0
  45. package/dist/utils/file-type-detector.d.ts +15 -0
  46. package/dist/utils/file-type-detector.d.ts.map +1 -0
  47. package/dist/utils/file-type-detector.js +38 -0
  48. package/dist/utils/platform.d.ts +26 -0
  49. package/dist/utils/platform.d.ts.map +1 -0
  50. package/dist/utils/platform.js +58 -0
  51. package/dist/utils.d.ts +15 -0
  52. package/dist/utils.d.ts.map +1 -0
  53. package/dist/utils.js +48 -0
  54. package/package.json +78 -0
package/dist/upload.js ADDED
@@ -0,0 +1,94 @@
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 axios_1 = __importDefault(require("axios"));
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const form_data_1 = __importDefault(require("form-data"));
10
+ const testingbot_error_1 = __importDefault(require("./models/testingbot_error"));
11
+ const utils_1 = __importDefault(require("./utils"));
12
+ class Upload {
13
+ lastProgressPercent = 0;
14
+ async upload(options) {
15
+ const { filePath, url, credentials, contentType, showProgress = false, } = options;
16
+ await this.validateFile(filePath);
17
+ const fileName = node_path_1.default.basename(filePath);
18
+ const fileStats = await node_fs_1.default.promises.stat(filePath);
19
+ const fileStream = node_fs_1.default.createReadStream(filePath);
20
+ const formData = new form_data_1.default();
21
+ formData.append('file', fileStream);
22
+ try {
23
+ const response = await axios_1.default.post(url, formData, {
24
+ headers: {
25
+ 'Content-Type': contentType,
26
+ 'Content-Disposition': `attachment; filename=${fileName}`,
27
+ 'User-Agent': utils_1.default.getUserAgent(),
28
+ },
29
+ auth: {
30
+ username: credentials.userName,
31
+ password: credentials.accessKey,
32
+ },
33
+ maxContentLength: Infinity,
34
+ maxBodyLength: Infinity,
35
+ onUploadProgress: showProgress
36
+ ? (progressEvent) => {
37
+ this.handleProgress(progressEvent, fileStats.size, fileName);
38
+ }
39
+ : undefined,
40
+ });
41
+ const result = response.data;
42
+ if (result.id) {
43
+ if (showProgress) {
44
+ this.clearProgressLine();
45
+ }
46
+ return { id: result.id };
47
+ }
48
+ else {
49
+ throw new testingbot_error_1.default(`Upload failed: ${result.error || 'Unknown error'}`);
50
+ }
51
+ }
52
+ catch (error) {
53
+ if (error instanceof testingbot_error_1.default) {
54
+ throw error;
55
+ }
56
+ if (axios_1.default.isAxiosError(error)) {
57
+ const message = error.response?.data?.error || error.message;
58
+ throw new testingbot_error_1.default(`Upload failed: ${message}`);
59
+ }
60
+ throw new testingbot_error_1.default(`Upload failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
61
+ }
62
+ }
63
+ async validateFile(filePath) {
64
+ try {
65
+ await node_fs_1.default.promises.access(filePath, node_fs_1.default.constants.R_OK);
66
+ }
67
+ catch {
68
+ throw new testingbot_error_1.default(`File not found or not readable: ${filePath}`);
69
+ }
70
+ }
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
+ }
94
+ exports.default = Upload;
@@ -0,0 +1,15 @@
1
+ export interface FileTypeResult {
2
+ ext: string;
3
+ mime: string;
4
+ }
5
+ /**
6
+ * Detect file type from file content using magic bytes.
7
+ * Returns undefined if the file type cannot be determined.
8
+ */
9
+ export declare function detectFileType(filePath: string): Promise<FileTypeResult | undefined>;
10
+ /**
11
+ * Detect platform (Android or iOS) from app file.
12
+ * Uses magic bytes for content detection, with extension fallback for zip-based formats.
13
+ */
14
+ export declare function detectPlatformFromFile(filePath: string): Promise<'Android' | 'iOS' | undefined>;
15
+ //# sourceMappingURL=file-type-detector.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectFileType = detectFileType;
4
+ exports.detectPlatformFromFile = detectPlatformFromFile;
5
+ /**
6
+ * Detect file type from file content using magic bytes.
7
+ * Returns undefined if the file type cannot be determined.
8
+ */
9
+ async function detectFileType(filePath) {
10
+ try {
11
+ // Dynamic import for ESM-only file-type package
12
+ const { fileTypeFromFile } = await import('file-type');
13
+ const result = await fileTypeFromFile(filePath);
14
+ return result;
15
+ }
16
+ catch {
17
+ return undefined;
18
+ }
19
+ }
20
+ /**
21
+ * Detect platform (Android or iOS) from app file.
22
+ * Uses magic bytes for content detection, with extension fallback for zip-based formats.
23
+ */
24
+ async function detectPlatformFromFile(filePath) {
25
+ const fileType = await detectFileType(filePath);
26
+ if (fileType) {
27
+ // APK files are detected as 'application/zip' with ext 'apk'
28
+ // or as 'application/vnd.android.package-archive'
29
+ if (fileType.ext === 'apk' ||
30
+ fileType.mime === 'application/vnd.android.package-archive') {
31
+ return 'Android';
32
+ }
33
+ if (fileType.ext === 'zip' || fileType.mime === 'application/zip') {
34
+ return 'iOS';
35
+ }
36
+ }
37
+ return undefined;
38
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Cross-platform utilities for terminal operations and signal handling
3
+ */
4
+ /**
5
+ * Clear the current line in the terminal.
6
+ * Uses ANSI escape codes on Unix/macOS, and space overwrite on Windows.
7
+ */
8
+ export declare function clearLine(): void;
9
+ /**
10
+ * Setup signal handlers for graceful shutdown.
11
+ * SIGINT (Ctrl+C) works on all platforms.
12
+ * SIGTERM only works on Unix/macOS.
13
+ */
14
+ export declare function setupSignalHandlers(handler: () => void): void;
15
+ /**
16
+ * Remove signal handlers.
17
+ */
18
+ export declare function removeSignalHandlers(handler: () => void): void;
19
+ declare const _default: {
20
+ isWindows: boolean;
21
+ clearLine: typeof clearLine;
22
+ setupSignalHandlers: typeof setupSignalHandlers;
23
+ removeSignalHandlers: typeof removeSignalHandlers;
24
+ };
25
+ export default _default;
26
+ //# sourceMappingURL=platform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../../src/utils/platform.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH;;;GAGG;AACH,wBAAgB,SAAS,IAAI,IAAI,CAehC;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,CAO7D;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,CAM9D;;;;;;;AAED,wBAKE"}
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ /**
3
+ * Cross-platform utilities for terminal operations and signal handling
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.clearLine = clearLine;
7
+ exports.setupSignalHandlers = setupSignalHandlers;
8
+ exports.removeSignalHandlers = removeSignalHandlers;
9
+ const isWindows = process.platform === 'win32';
10
+ /**
11
+ * Clear the current line in the terminal.
12
+ * Uses ANSI escape codes on Unix/macOS, and space overwrite on Windows.
13
+ */
14
+ function clearLine() {
15
+ if (isWindows) {
16
+ // Windows fallback: overwrite with spaces and return to start
17
+ // Use readline if available for better Windows support
18
+ if (process.stdout.clearLine && process.stdout.cursorTo) {
19
+ process.stdout.clearLine(0);
20
+ process.stdout.cursorTo(0);
21
+ }
22
+ else {
23
+ // Fallback: write spaces to clear typical line width
24
+ process.stdout.write('\r' + ' '.repeat(120) + '\r');
25
+ }
26
+ }
27
+ else {
28
+ // Unix/macOS: ANSI escape sequence
29
+ process.stdout.write('\r\x1b[K');
30
+ }
31
+ }
32
+ /**
33
+ * Setup signal handlers for graceful shutdown.
34
+ * SIGINT (Ctrl+C) works on all platforms.
35
+ * SIGTERM only works on Unix/macOS.
36
+ */
37
+ function setupSignalHandlers(handler) {
38
+ process.on('SIGINT', handler);
39
+ // SIGTERM is not supported on Windows
40
+ if (!isWindows) {
41
+ process.on('SIGTERM', handler);
42
+ }
43
+ }
44
+ /**
45
+ * Remove signal handlers.
46
+ */
47
+ function removeSignalHandlers(handler) {
48
+ process.removeListener('SIGINT', handler);
49
+ if (!isWindows) {
50
+ process.removeListener('SIGTERM', handler);
51
+ }
52
+ }
53
+ exports.default = {
54
+ isWindows,
55
+ clearLine,
56
+ setupSignalHandlers,
57
+ removeSignalHandlers,
58
+ };
@@ -0,0 +1,15 @@
1
+ declare const _default: {
2
+ getUserAgent(): string;
3
+ getCurrentVersion(): string;
4
+ /**
5
+ * Compare two semver version strings
6
+ * Returns: -1 if v1 < v2, 0 if equal, 1 if v1 > v2
7
+ */
8
+ compareVersions(v1: string, v2: string): number;
9
+ /**
10
+ * Check if a newer version is available and display update notice
11
+ */
12
+ checkForUpdate(latestVersion: string | undefined): void;
13
+ };
14
+ export default _default;
15
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":";oBAOkB,MAAM;yBAID,MAAM;IAI3B;;;OAGG;wBACiB,MAAM,MAAM,MAAM,GAAG,MAAM;IAa/C;;OAEG;kCAC2B,MAAM,GAAG,SAAS,GAAG,IAAI;;AA7BzD,wBAiDE"}
package/dist/utils.js ADDED
@@ -0,0 +1,48 @@
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 package_json_1 = __importDefault(require("../package.json"));
7
+ const logger_1 = __importDefault(require("./logger"));
8
+ const colors_1 = __importDefault(require("colors"));
9
+ let versionCheckDisplayed = false;
10
+ exports.default = {
11
+ getUserAgent() {
12
+ return `TestingBot-CTL-${package_json_1.default.version}`;
13
+ },
14
+ getCurrentVersion() {
15
+ return package_json_1.default.version;
16
+ },
17
+ /**
18
+ * Compare two semver version strings
19
+ * Returns: -1 if v1 < v2, 0 if equal, 1 if v1 > v2
20
+ */
21
+ compareVersions(v1, v2) {
22
+ const parts1 = v1.split('.').map(Number);
23
+ const parts2 = v2.split('.').map(Number);
24
+ for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
25
+ const p1 = parts1[i] || 0;
26
+ const p2 = parts2[i] || 0;
27
+ if (p1 < p2)
28
+ return -1;
29
+ if (p1 > p2)
30
+ return 1;
31
+ }
32
+ return 0;
33
+ },
34
+ /**
35
+ * Check if a newer version is available and display update notice
36
+ */
37
+ checkForUpdate(latestVersion) {
38
+ if (!latestVersion || versionCheckDisplayed) {
39
+ return;
40
+ }
41
+ const currentVersion = this.getCurrentVersion();
42
+ if (this.compareVersions(currentVersion, latestVersion) < 0) {
43
+ versionCheckDisplayed = true;
44
+ logger_1.default.warn(colors_1.default.yellow(`\n📦 A new version of testingbotctl is available: ${colors_1.default.green(latestVersion)} (current: ${currentVersion})`));
45
+ logger_1.default.warn(colors_1.default.yellow(` Run ${colors_1.default.cyan('npm update -g testingbotctl')} to update.\n`));
46
+ }
47
+ },
48
+ };
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@testingbot/cli",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool to run Espresso, XCUITest, and Maestro tests on TestingBot's cloud infrastructure",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "testingbot": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=20"
15
+ },
16
+ "scripts": {
17
+ "lint": "prettier --check '**/*.{js,ts}' && eslint src/",
18
+ "build": "tsc",
19
+ "clean": "rm -rf dist",
20
+ "start": "node dist/index.js",
21
+ "format": "prettier --write '**/*.{js,ts}'",
22
+ "test": "jest"
23
+ },
24
+ "keywords": [
25
+ "testingbot",
26
+ "mobile-testing",
27
+ "espresso",
28
+ "xcuitest",
29
+ "maestro",
30
+ "android",
31
+ "ios",
32
+ "test-automation",
33
+ "cloud-testing",
34
+ "cli",
35
+ "real-devices",
36
+ "emulators",
37
+ "simulators"
38
+ ],
39
+ "author": "TestingBot",
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/testingbot/testingbotctl.git"
44
+ },
45
+ "homepage": "https://testingbot.com",
46
+ "bugs": {
47
+ "url": "https://github.com/testingbot/testingbotctl/issues"
48
+ },
49
+ "dependencies": {
50
+ "archiver": "^7.0.1",
51
+ "axios": "^1.13.2",
52
+ "colors": "^1.4.0",
53
+ "commander": "^14.0.2",
54
+ "file-type": "^21.1.1",
55
+ "form-data": "^4.0.5",
56
+ "glob": "^13.0.0",
57
+ "js-yaml": "^4.1.1",
58
+ "socket.io-client": "^4.8.1",
59
+ "tracer": "^1.3.0"
60
+ },
61
+ "devDependencies": {
62
+ "@eslint/js": "^9.39.1",
63
+ "@tsconfig/node20": "^20.1.8",
64
+ "@types/archiver": "^7.0.0",
65
+ "@types/jest": "^29.5.14",
66
+ "@types/js-yaml": "^4.0.9",
67
+ "@types/node": "^20.19.0",
68
+ "babel-jest": "^29.7.0",
69
+ "eslint": "^9.39.1",
70
+ "eslint-config-prettier": "^10.1.8",
71
+ "eslint-plugin-prettier": "^5.5.4",
72
+ "jest": "^29.7.0",
73
+ "prettier": "^3.7.4",
74
+ "ts-jest": "^29.4.6",
75
+ "typescript": "^5.9.3",
76
+ "typescript-eslint": "^8.48.1"
77
+ }
78
+ }