@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
@@ -0,0 +1,284 @@
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 node_http_1 = __importDefault(require("node:http"));
7
+ const node_url_1 = require("node:url");
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const node_os_1 = __importDefault(require("node:os"));
11
+ const logger_1 = __importDefault(require("../logger"));
12
+ const AUTH_URL = 'https://testingbot.com/auth';
13
+ class Login {
14
+ server = null;
15
+ port = 0;
16
+ async run() {
17
+ try {
18
+ // Start local server to receive callback
19
+ this.port = await this.startServer();
20
+ // Open browser to auth URL
21
+ const authUrl = `${AUTH_URL}?port=${this.port}&identifier=testingbotctl`;
22
+ logger_1.default.info('Opening browser for authentication...');
23
+ logger_1.default.info(`\nIf the browser does not open automatically, visit:\n\n ${authUrl}\n`);
24
+ await this.openBrowser(authUrl);
25
+ // Wait for callback (handled by server)
26
+ const credentials = await this.waitForCallback();
27
+ // Save credentials
28
+ await this.saveCredentials(credentials.key, credentials.secret);
29
+ logger_1.default.info('Authentication successful!');
30
+ logger_1.default.info(`Credentials saved to ~/.testingbot`);
31
+ return { success: true, message: 'Authentication successful' };
32
+ }
33
+ catch (error) {
34
+ const message = error instanceof Error ? error.message : String(error);
35
+ logger_1.default.error(`Authentication failed: ${message}`);
36
+ return { success: false, message };
37
+ }
38
+ finally {
39
+ this.stopServer();
40
+ }
41
+ }
42
+ startServer() {
43
+ return new Promise((resolve, reject) => {
44
+ this.server = node_http_1.default.createServer();
45
+ this.server.on('error', (err) => {
46
+ reject(new Error(`Failed to start local server: ${err.message}`));
47
+ });
48
+ // Listen on random available port
49
+ this.server.listen(0, '127.0.0.1', () => {
50
+ const address = this.server?.address();
51
+ if (address && typeof address === 'object') {
52
+ resolve(address.port);
53
+ }
54
+ else {
55
+ reject(new Error('Failed to get server port'));
56
+ }
57
+ });
58
+ });
59
+ }
60
+ stopServer() {
61
+ if (this.server) {
62
+ this.server.close();
63
+ this.server = null;
64
+ }
65
+ }
66
+ waitForCallback() {
67
+ return new Promise((resolve, reject) => {
68
+ const timeout = setTimeout(() => {
69
+ reject(new Error('Authentication timed out after 5 minutes'));
70
+ }, 5 * 60 * 1000);
71
+ this.server?.on('request', async (req, res) => {
72
+ if (!req.url) {
73
+ res.writeHead(400);
74
+ res.end('Bad request');
75
+ return;
76
+ }
77
+ const url = new node_url_1.URL(req.url, `http://127.0.0.1:${this.port}`);
78
+ if (url.pathname === '/callback') {
79
+ try {
80
+ // Try to get credentials from query params (GET) or body (POST)
81
+ let key = url.searchParams.get('key');
82
+ let secret = url.searchParams.get('secret');
83
+ let error = url.searchParams.get('error');
84
+ // If not in query params and this is a POST request, parse the body
85
+ if (!key && !secret && !error && req.method === 'POST') {
86
+ const body = await this.parseRequestBody(req);
87
+ key = body.get('key');
88
+ secret = body.get('secret');
89
+ error = body.get('error');
90
+ }
91
+ if (error) {
92
+ clearTimeout(timeout);
93
+ this.sendErrorResponse(res, error);
94
+ reject(new Error(error));
95
+ return;
96
+ }
97
+ if (key && secret) {
98
+ clearTimeout(timeout);
99
+ this.sendSuccessResponse(res);
100
+ resolve({ key, secret });
101
+ }
102
+ else {
103
+ res.writeHead(400);
104
+ res.end('Missing credentials');
105
+ }
106
+ }
107
+ catch {
108
+ res.writeHead(400);
109
+ res.end('Failed to parse request');
110
+ }
111
+ }
112
+ else {
113
+ res.writeHead(404);
114
+ res.end('Not found');
115
+ }
116
+ });
117
+ });
118
+ }
119
+ parseRequestBody(req) {
120
+ return new Promise((resolve, reject) => {
121
+ const chunks = [];
122
+ req.on('data', (chunk) => {
123
+ chunks.push(chunk);
124
+ });
125
+ req.on('end', () => {
126
+ const body = Buffer.concat(chunks).toString();
127
+ const contentType = req.headers['content-type'] || '';
128
+ if (contentType.includes('application/json')) {
129
+ // Parse JSON body
130
+ try {
131
+ const json = JSON.parse(body);
132
+ const params = new URLSearchParams();
133
+ if (json.key)
134
+ params.set('key', json.key);
135
+ if (json.secret)
136
+ params.set('secret', json.secret);
137
+ if (json.error)
138
+ params.set('error', json.error);
139
+ resolve(params);
140
+ }
141
+ catch {
142
+ reject(new Error('Invalid JSON body'));
143
+ }
144
+ }
145
+ else {
146
+ // Parse URL-encoded body (application/x-www-form-urlencoded)
147
+ resolve(new URLSearchParams(body));
148
+ }
149
+ });
150
+ req.on('error', reject);
151
+ });
152
+ }
153
+ sendSuccessResponse(res) {
154
+ const html = `<!DOCTYPE html>
155
+ <html>
156
+ <head>
157
+ <title>Authentication Successful</title>
158
+ <style>
159
+ body {
160
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
161
+ background: #f5f5f5;
162
+ min-height: 100vh;
163
+ display: flex;
164
+ align-items: center;
165
+ justify-content: center;
166
+ margin: 0;
167
+ }
168
+ .container {
169
+ background: white;
170
+ padding: 2rem;
171
+ border-radius: 8px;
172
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
173
+ max-width: 400px;
174
+ text-align: center;
175
+ }
176
+ .icon {
177
+ width: 64px;
178
+ height: 64px;
179
+ margin-bottom: 1rem;
180
+ }
181
+ h1 {
182
+ color: #333;
183
+ margin-bottom: 0.5rem;
184
+ }
185
+ p {
186
+ color: #666;
187
+ }
188
+ </style>
189
+ </head>
190
+ <body>
191
+ <div class="container">
192
+ <svg class="icon" fill="none" stroke="#22c55e" viewBox="0 0 24 24">
193
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
194
+ </svg>
195
+ <h1>Authentication Successful!</h1>
196
+ <p>You can close this window and return to the CLI.</p>
197
+ </div>
198
+ </body>
199
+ </html>`;
200
+ res.writeHead(200, { 'Content-Type': 'text/html' });
201
+ res.end(html);
202
+ }
203
+ sendErrorResponse(res, error) {
204
+ const html = `<!DOCTYPE html>
205
+ <html>
206
+ <head>
207
+ <title>Authentication Failed</title>
208
+ <style>
209
+ body {
210
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
211
+ background: #f5f5f5;
212
+ min-height: 100vh;
213
+ display: flex;
214
+ align-items: center;
215
+ justify-content: center;
216
+ margin: 0;
217
+ }
218
+ .container {
219
+ background: white;
220
+ padding: 2rem;
221
+ border-radius: 8px;
222
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
223
+ max-width: 400px;
224
+ text-align: center;
225
+ }
226
+ .icon {
227
+ width: 64px;
228
+ height: 64px;
229
+ margin-bottom: 1rem;
230
+ }
231
+ h1 {
232
+ color: #333;
233
+ margin-bottom: 0.5rem;
234
+ }
235
+ p {
236
+ color: #666;
237
+ }
238
+ .error {
239
+ color: #ef4444;
240
+ }
241
+ </style>
242
+ </head>
243
+ <body>
244
+ <div class="container">
245
+ <svg class="icon" fill="none" stroke="#ef4444" viewBox="0 0 24 24">
246
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
247
+ </svg>
248
+ <h1>Authentication Failed</h1>
249
+ <p class="error">${error}</p>
250
+ <p>Please try again or contact support.</p>
251
+ </div>
252
+ </body>
253
+ </html>`;
254
+ res.writeHead(200, { 'Content-Type': 'text/html' });
255
+ res.end(html);
256
+ }
257
+ async saveCredentials(key, secret) {
258
+ const filePath = node_path_1.default.join(node_os_1.default.homedir(), '.testingbot');
259
+ await node_fs_1.default.promises.writeFile(filePath, `${key}:${secret}`, { mode: 0o600 });
260
+ }
261
+ async openBrowser(url) {
262
+ const { exec } = await import('node:child_process');
263
+ const { promisify } = await import('node:util');
264
+ const execAsync = promisify(exec);
265
+ const platform = process.platform;
266
+ try {
267
+ if (platform === 'darwin') {
268
+ await execAsync(`open "${url}"`);
269
+ }
270
+ else if (platform === 'win32') {
271
+ await execAsync(`start "" "${url}"`);
272
+ }
273
+ else {
274
+ // Linux and others
275
+ await execAsync(`xdg-open "${url}"`);
276
+ }
277
+ }
278
+ catch {
279
+ // Browser open failed, user will need to open manually
280
+ // Message already displayed in run()
281
+ }
282
+ }
283
+ }
284
+ exports.default = Login;
@@ -0,0 +1,92 @@
1
+ import MaestroOptions from '../models/maestro_options';
2
+ import Credentials from '../models/credentials';
3
+ export interface MaestroRunAssets {
4
+ logs?: string[];
5
+ video?: string | false;
6
+ screenshots?: string[];
7
+ }
8
+ export interface MaestroRunInfo {
9
+ id: number;
10
+ status: 'WAITING' | 'READY' | 'DONE' | 'FAILED';
11
+ capabilities: {
12
+ deviceName: string;
13
+ platformName: string;
14
+ version?: string;
15
+ };
16
+ success: number;
17
+ report?: string;
18
+ options?: Record<string, unknown>;
19
+ assets?: MaestroRunAssets;
20
+ }
21
+ export interface MaestroRunDetails extends MaestroRunInfo {
22
+ completed: boolean;
23
+ assets_synced: boolean;
24
+ }
25
+ export interface MaestroStatusResponse {
26
+ runs: MaestroRunInfo[];
27
+ success: boolean;
28
+ completed: boolean;
29
+ }
30
+ export interface MaestroResult {
31
+ success: boolean;
32
+ runs: MaestroRunInfo[];
33
+ }
34
+ export interface MaestroSocketMessage {
35
+ id: number;
36
+ payload: string;
37
+ }
38
+ export default class Maestro {
39
+ private readonly URL;
40
+ private readonly POLL_INTERVAL_MS;
41
+ private readonly MAX_POLL_ATTEMPTS;
42
+ private credentials;
43
+ private options;
44
+ private upload;
45
+ private appId;
46
+ private detectedPlatform;
47
+ private activeRunIds;
48
+ private isShuttingDown;
49
+ private signalHandler;
50
+ private socket;
51
+ private updateServer;
52
+ private updateKey;
53
+ constructor(credentials: Credentials, options: MaestroOptions);
54
+ private validate;
55
+ private ensureOutputDirectory;
56
+ /**
57
+ * Detect platform from app file content using magic bytes
58
+ */
59
+ private detectPlatform;
60
+ run(): Promise<MaestroResult>;
61
+ private uploadApp;
62
+ private uploadFlows;
63
+ private discoverFlows;
64
+ private discoverDependencies;
65
+ private createFlowsZip;
66
+ private runTests;
67
+ private getStatus;
68
+ private waitForCompletion;
69
+ private displayRunStatus;
70
+ private clearLine;
71
+ private formatElapsedTime;
72
+ private getStatusInfo;
73
+ private fetchReports;
74
+ private getRunDetails;
75
+ private waitForArtifactsSync;
76
+ private downloadFile;
77
+ private generateArtifactZipName;
78
+ private downloadArtifacts;
79
+ private createZipFromDirectory;
80
+ private sleep;
81
+ private extractErrorMessage;
82
+ private setupSignalHandlers;
83
+ private removeSignalHandlers;
84
+ private handleShutdown;
85
+ private stopActiveRuns;
86
+ private stopRun;
87
+ private connectToUpdateServer;
88
+ private disconnectFromUpdateServer;
89
+ private handleMaestroData;
90
+ private handleMaestroError;
91
+ }
92
+ //# sourceMappingURL=maestro.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"maestro.d.ts","sourceRoot":"","sources":["../../src/providers/maestro.ts"],"names":[],"mappings":"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"}