@qate/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.
package/dist/cli.js ADDED
@@ -0,0 +1,1269 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ var __importDefault = (this && this.__importDefault) || function (mod) {
37
+ return (mod && mod.__esModule) ? mod : { "default": mod };
38
+ };
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ const commander_1 = require("commander");
41
+ const chalk_1 = __importDefault(require("chalk"));
42
+ const client_1 = require("./client");
43
+ const PlaywrightGenerator_1 = require("./PlaywrightGenerator");
44
+ const AxiosGenerator_1 = require("./AxiosGenerator");
45
+ const RestApiExecutor_1 = require("./RestApiExecutor");
46
+ const fs = __importStar(require("fs"));
47
+ const path = __importStar(require("path"));
48
+ const os = __importStar(require("os"));
49
+ const VERSION = '1.0.0';
50
+ const program = new commander_1.Command();
51
+ // Helper to handle API errors with detailed feedback
52
+ function handleApiError(error) {
53
+ const status = error.response?.status;
54
+ const data = error.response?.data;
55
+ // Handle 409 Conflict - ambiguous name (multiple matches)
56
+ if (status === 409 && data?.matches) {
57
+ console.error(chalk_1.default.red(`Error: ${data.message || 'Ambiguous name'}`));
58
+ console.log();
59
+ console.log(chalk_1.default.yellow('Available options:'));
60
+ console.log(chalk_1.default.gray('─────────────────────────────────────'));
61
+ data.matches.forEach((match, idx) => {
62
+ const appInfo = match.application;
63
+ console.log(` ${idx + 1}. ${chalk_1.default.white(match.name)}`);
64
+ console.log(` Application: ${chalk_1.default.cyan(appInfo.name)} (${appInfo.type})`);
65
+ console.log(` Use: ${chalk_1.default.green(`--app ${appInfo.id}`)}`);
66
+ });
67
+ console.log();
68
+ console.log(chalk_1.default.gray('Example:'));
69
+ if (data.matches.length > 0) {
70
+ const firstMatch = data.matches[0];
71
+ console.log(chalk_1.default.white(` qate run -n "${firstMatch.name}" --app ${firstMatch.application.id}`));
72
+ }
73
+ process.exit(1);
74
+ }
75
+ // Handle 404 Not Found
76
+ if (status === 404) {
77
+ console.error(chalk_1.default.red(`Error: ${data?.message || 'Not found'}`));
78
+ console.log(chalk_1.default.gray('Check the name and try again, or run "qate list" to see available items.'));
79
+ process.exit(1);
80
+ }
81
+ // Handle 401 Unauthorized
82
+ if (status === 401) {
83
+ console.error(chalk_1.default.red(`Error: ${data?.message || 'Authentication failed'}`));
84
+ console.log(chalk_1.default.gray('Check your API key and try again.'));
85
+ console.log(chalk_1.default.gray('Generate a new key at: Settings > CI/CD API Keys'));
86
+ process.exit(1);
87
+ }
88
+ // Handle 429 Rate Limit - monthly execution limit reached
89
+ if (status === 429) {
90
+ console.error(chalk_1.default.red(`Error: ${data?.message || 'Monthly execution limit reached'}`));
91
+ if (data?.current !== undefined && data?.limit !== undefined) {
92
+ console.log(chalk_1.default.gray(`Usage: ${data.current}/${data.limit} executions this month`));
93
+ }
94
+ console.log(chalk_1.default.gray('Upgrade your plan at: https://app.qate.ai/settings/billing'));
95
+ process.exit(1);
96
+ }
97
+ // Handle 403 Forbidden - plan restriction
98
+ if (status === 403) {
99
+ console.error(chalk_1.default.red(`Error: ${data?.message || 'Access denied'}`));
100
+ process.exit(1);
101
+ }
102
+ // Handle 400 Bad Request
103
+ if (status === 400) {
104
+ console.error(chalk_1.default.red(`Error: ${data?.message || 'Bad request'}`));
105
+ if (data?.error) {
106
+ console.log(chalk_1.default.gray(`Details: ${data.error}`));
107
+ }
108
+ process.exit(1);
109
+ }
110
+ // Generic error handling
111
+ console.error(chalk_1.default.red('Error:'), data?.message || error.message);
112
+ process.exit(1);
113
+ }
114
+ // Helper to get API key and base URL
115
+ function getConfig(options) {
116
+ const apiKey = options.apiKey || process.env.QATE_API_KEY || process.env.TESTMIND_API_KEY;
117
+ const baseUrl = options.url || process.env.QATE_API_URL || process.env.TESTMIND_API_URL || 'https://api.qate.ai';
118
+ if (!apiKey) {
119
+ console.error(chalk_1.default.red('Error: API key is required.'));
120
+ console.error(chalk_1.default.gray('Use --api-key option or set QATE_API_KEY environment variable.'));
121
+ console.error(chalk_1.default.gray('Generate an API key at: Settings > CI/CD API Keys'));
122
+ process.exit(1);
123
+ }
124
+ if (!apiKey.startsWith('qate_')) {
125
+ console.error(chalk_1.default.yellow('Warning: API key should start with "qate_". Legacy keys are no longer supported.'));
126
+ }
127
+ return { apiKey, baseUrl };
128
+ }
129
+ // Helper to display execution status
130
+ function displayStatus(status, type) {
131
+ const name = type === 'testset' ? status.testSetName : status.sequenceName;
132
+ console.log();
133
+ console.log(chalk_1.default.blue(`Execution Status`));
134
+ console.log(chalk_1.default.gray(`─────────────────────────────────────`));
135
+ console.log(` Execution ID: ${chalk_1.default.cyan(status.executionId)}`);
136
+ console.log(` ${type === 'testset' ? 'Test Set' : 'Sequence'}: ${chalk_1.default.white(name)}`);
137
+ const statusColor = status.status === 'passed' ? chalk_1.default.green :
138
+ status.status === 'failed' ? chalk_1.default.red :
139
+ status.status === 'running' ? chalk_1.default.yellow : chalk_1.default.gray;
140
+ console.log(` Status: ${statusColor(status.status.toUpperCase())}`);
141
+ console.log();
142
+ console.log(chalk_1.default.blue(`Summary`));
143
+ console.log(chalk_1.default.gray(`─────────────────────────────────────`));
144
+ console.log(` Total: ${status.summary.total}`);
145
+ console.log(` Passed: ${chalk_1.default.green(status.summary.passed.toString())}`);
146
+ console.log(` Failed: ${chalk_1.default.red(status.summary.failed.toString())}`);
147
+ console.log(` Error: ${chalk_1.default.red(status.summary.error.toString())}`);
148
+ console.log(` Pending: ${chalk_1.default.yellow(status.summary.pending.toString())}`);
149
+ if (status.stoppedAt !== undefined && status.stoppedAt !== null) {
150
+ console.log();
151
+ console.log(chalk_1.default.yellow(`Execution stopped at test #${status.stoppedAt + 1}`));
152
+ if (status.stoppedReason) {
153
+ console.log(chalk_1.default.yellow(`Reason: ${status.stoppedReason}`));
154
+ }
155
+ }
156
+ if (status.tests && status.tests.length > 0) {
157
+ console.log();
158
+ console.log(chalk_1.default.blue(`Test Results`));
159
+ console.log(chalk_1.default.gray(`─────────────────────────────────────`));
160
+ status.tests.forEach((test, idx) => {
161
+ const icon = test.status === 'passed' ? chalk_1.default.green('✓') :
162
+ test.status === 'failed' ? chalk_1.default.red('✗') :
163
+ test.status === 'error' ? chalk_1.default.red('!') : chalk_1.default.gray('○');
164
+ const time = test.executionTime ? `(${(test.executionTime / 1000).toFixed(1)}s)` : '';
165
+ console.log(` ${icon} ${test.testName} ${chalk_1.default.gray(time)}`);
166
+ });
167
+ }
168
+ console.log();
169
+ console.log(chalk_1.default.gray(`Last updated: ${status.lastUpdated}`));
170
+ }
171
+ program
172
+ .name('qate')
173
+ .description('Qate CLI for CI/CD pipeline integration\n\nGenerate and run Playwright tests from your Qate test definitions.')
174
+ .version(VERSION)
175
+ .addHelpText('after', `
176
+ Examples:
177
+ $ qate list List all test sets
178
+ $ qate generate -n "My Tests" -o ./e2e Generate Playwright tests (web apps)
179
+ $ qate run -n "API Tests" --wait Run REST/SOAP tests locally
180
+ $ qate export:testset -n "Tests" -o ./out Export as Playwright or Axios tests
181
+ $ qate status -e ci_xxx_xxx Check execution status
182
+
183
+ Documentation:
184
+ https://docs.qate.io/cli
185
+
186
+ Environment Variables:
187
+ QATE_API_KEY Your Qate API key (required)
188
+ QATE_API_URL Qate API URL (default: https://api.qate.ai)
189
+ `);
190
+ // ================== TEST SET COMMANDS ==================
191
+ program
192
+ .command('run:testset')
193
+ .alias('run')
194
+ .description('Execute a test set locally (REST API and SOAP only - use "generate" for web apps)')
195
+ .requiredOption('-n, --name <name>', 'Name of the test set to execute')
196
+ .option('-a, --api-key <key>', 'API key (or use QATE_API_KEY env var)')
197
+ .option('-u, --url <url>', 'Qate API URL (or use QATE_API_URL env var)')
198
+ .option('--app <applicationId>', 'Application ID (if test set name is not unique)')
199
+ .option('-w, --wait', 'Wait for execution to complete')
200
+ .option('--timeout <seconds>', 'Timeout for waiting (default: 600)', '600')
201
+ .option('--json', 'Output results as JSON')
202
+ .option('-v, --verbose', 'Show detailed step-by-step output')
203
+ .action(async (options) => {
204
+ try {
205
+ const { apiKey, baseUrl } = getConfig(options);
206
+ const client = new client_1.QateClient(baseUrl, apiKey);
207
+ if (!options.json) {
208
+ console.log(chalk_1.default.blue(`Fetching test set: ${options.name}`));
209
+ }
210
+ // First, export the test set to check application type
211
+ const exportData = await client.exportTestSet(options.name, options.app);
212
+ const appType = exportData.application?.type || 'web';
213
+ // REST API apps execute locally in the CLI
214
+ if ((0, RestApiExecutor_1.isRestApiApp)(appType)) {
215
+ if (!options.json) {
216
+ console.log(chalk_1.default.cyan(`REST API application detected - executing locally`));
217
+ console.log(chalk_1.default.gray(`Application: ${exportData.application.name}`));
218
+ console.log(chalk_1.default.gray(`Base URL: ${exportData.application.url}`));
219
+ console.log(chalk_1.default.gray(`Tests: ${exportData.tests.length}`));
220
+ console.log();
221
+ }
222
+ // Create execution record for tracking
223
+ let executionId;
224
+ try {
225
+ const generateResult = await client.createGenerateExecution(options.name, undefined, options.app, 'cli');
226
+ executionId = generateResult.executionId;
227
+ if (!options.json) {
228
+ console.log(chalk_1.default.gray(`Execution ID: ${executionId}`));
229
+ }
230
+ }
231
+ catch (err) {
232
+ if (!options.json) {
233
+ console.log(chalk_1.default.yellow('Note: Could not create execution record'));
234
+ }
235
+ }
236
+ // Execute tests locally
237
+ if (!options.json) {
238
+ console.log(chalk_1.default.blue(`\nExecuting ${exportData.tests.length} tests...\n`));
239
+ }
240
+ const results = await (0, RestApiExecutor_1.executeTests)(exportData.tests, exportData.application.url, { verbose: options.verbose },
241
+ // Test progress callback
242
+ (testResult, index, total) => {
243
+ if (!options.json) {
244
+ console.log((0, RestApiExecutor_1.formatTestResult)(testResult, options.verbose));
245
+ }
246
+ },
247
+ // Step progress callback (for verbose mode)
248
+ options.verbose ? (progress, stepResult) => {
249
+ // Already handled in formatTestResult
250
+ } : undefined);
251
+ // Calculate summary
252
+ const summary = (0, RestApiExecutor_1.calculateSummary)(results);
253
+ // Report results to backend
254
+ if (executionId) {
255
+ try {
256
+ await client.reportResults(executionId, results, summary, 'cli', {
257
+ os: os.platform(),
258
+ nodeVersion: process.version
259
+ });
260
+ if (!options.json) {
261
+ console.log(chalk_1.default.gray(`\nResults reported to Qate`));
262
+ }
263
+ }
264
+ catch (err) {
265
+ if (!options.json) {
266
+ console.log(chalk_1.default.yellow('Note: Could not report results to backend'));
267
+ }
268
+ }
269
+ }
270
+ // Display final summary
271
+ if (options.json) {
272
+ console.log(JSON.stringify({
273
+ executionId,
274
+ testSetName: exportData.testSet?.name,
275
+ status: summary.status,
276
+ summary: {
277
+ total: summary.total,
278
+ passed: summary.passed,
279
+ failed: summary.failed,
280
+ error: summary.error
281
+ },
282
+ tests: results.map(r => ({
283
+ testId: r.testId,
284
+ testName: r.testName,
285
+ status: r.status,
286
+ duration: r.duration
287
+ }))
288
+ }, null, 2));
289
+ }
290
+ else {
291
+ console.log();
292
+ console.log(chalk_1.default.blue(`Summary`));
293
+ console.log(chalk_1.default.gray(`─────────────────────────────────────`));
294
+ console.log(` Total: ${summary.total}`);
295
+ console.log(` Passed: ${chalk_1.default.green(summary.passed.toString())}`);
296
+ console.log(` Failed: ${chalk_1.default.red(summary.failed.toString())}`);
297
+ console.log(` Error: ${chalk_1.default.red(summary.error.toString())}`);
298
+ console.log();
299
+ const statusColor = summary.status === 'passed' ? chalk_1.default.green :
300
+ summary.status === 'failed' ? chalk_1.default.red : chalk_1.default.red;
301
+ console.log(`Status: ${statusColor(summary.status.toUpperCase())}`);
302
+ }
303
+ process.exit(summary.status === 'passed' ? 0 : 1);
304
+ }
305
+ else if (appType === 'desktop') {
306
+ // Desktop apps: trigger server-side execution via connected agent, then poll
307
+ if (!options.json) {
308
+ console.log(chalk_1.default.cyan('Desktop application detected - executing via connected agent'));
309
+ console.log(chalk_1.default.gray(`Application: ${exportData.application.name}`));
310
+ }
311
+ const result = await client.executeTestSet(options.name, options.app);
312
+ if (!options.json) {
313
+ console.log(chalk_1.default.gray(`Execution ID: ${result.executionId}`));
314
+ console.log(chalk_1.default.blue('\nWaiting for execution to complete...\n'));
315
+ }
316
+ const timeoutMs = parseInt(options.timeout) * 1000;
317
+ const status = await client.pollExecution(result.executionId, 'testset', timeoutMs);
318
+ if (options.json) {
319
+ console.log(JSON.stringify(status, null, 2));
320
+ }
321
+ else {
322
+ displayStatus(status, 'testset');
323
+ }
324
+ process.exit(status.status === 'passed' ? 0 : 1);
325
+ }
326
+ else {
327
+ // Web apps should use 'qate generate' to create Playwright tests
328
+ console.error(chalk_1.default.red('\nError: "qate run" is not supported for web applications.'));
329
+ console.log();
330
+ console.log(chalk_1.default.yellow('For web applications, use "qate generate" to create Playwright tests:'));
331
+ console.log();
332
+ console.log(chalk_1.default.white(` qate generate -n "${options.name}" -o ./e2e`));
333
+ console.log(chalk_1.default.white(` cd ./e2e && npm install`));
334
+ console.log(chalk_1.default.white(` npx playwright test`));
335
+ console.log();
336
+ console.log(chalk_1.default.gray('This generates Playwright test files that you can run locally or in CI/CD.'));
337
+ console.log(chalk_1.default.gray('Results are automatically reported back to Qate.'));
338
+ process.exit(1);
339
+ }
340
+ }
341
+ catch (error) {
342
+ handleApiError(error);
343
+ }
344
+ });
345
+ program
346
+ .command('status:testset')
347
+ .description('Get status of a test set execution')
348
+ .requiredOption('-e, --execution-id <id>', 'Execution ID')
349
+ .option('-a, --api-key <key>', 'API key (or use QATE_API_KEY env var)')
350
+ .option('-u, --url <url>', 'Qate API URL (or use QATE_API_URL env var)')
351
+ .option('--json', 'Output as JSON')
352
+ .action(async (options) => {
353
+ try {
354
+ const { apiKey, baseUrl } = getConfig(options);
355
+ const client = new client_1.QateClient(baseUrl, apiKey);
356
+ const status = await client.getTestSetStatus(options.executionId);
357
+ if (options.json) {
358
+ console.log(JSON.stringify(status, null, 2));
359
+ }
360
+ else {
361
+ displayStatus(status, 'testset');
362
+ }
363
+ process.exit(status.status === 'passed' ? 0 : status.status === 'running' ? 0 : 1);
364
+ }
365
+ catch (error) {
366
+ handleApiError(error);
367
+ }
368
+ });
369
+ program
370
+ .command('list:testsets')
371
+ .alias('list')
372
+ .description('List all test sets')
373
+ .option('-a, --api-key <key>', 'API key (or use QATE_API_KEY env var)')
374
+ .option('-u, --url <url>', 'Qate API URL (or use QATE_API_URL env var)')
375
+ .option('--json', 'Output as JSON')
376
+ .action(async (options) => {
377
+ try {
378
+ const { apiKey, baseUrl } = getConfig(options);
379
+ const client = new client_1.QateClient(baseUrl, apiKey);
380
+ const result = await client.listTestSets();
381
+ if (options.json) {
382
+ console.log(JSON.stringify(result, null, 2));
383
+ return;
384
+ }
385
+ console.log(chalk_1.default.blue(`\nTest Sets (${result.testSets.length})`));
386
+ console.log(chalk_1.default.gray(`─────────────────────────────────────`));
387
+ if (result.testSets.length === 0) {
388
+ console.log(chalk_1.default.gray(' No test sets found'));
389
+ return;
390
+ }
391
+ result.testSets.forEach((testSet) => {
392
+ const statusIcon = testSet.lastExecutionStatus === 'passed' ? chalk_1.default.green('✓') :
393
+ testSet.lastExecutionStatus === 'failed' ? chalk_1.default.red('✗') :
394
+ chalk_1.default.gray('○');
395
+ console.log(`\n ${statusIcon} ${chalk_1.default.white(testSet.name)}`);
396
+ if (testSet.description) {
397
+ console.log(chalk_1.default.gray(` ${testSet.description}`));
398
+ }
399
+ console.log(chalk_1.default.gray(` App: ${testSet.application.name} | Tests: ${testSet.testCount}`));
400
+ if (testSet.lastExecuted) {
401
+ console.log(chalk_1.default.gray(` Last run: ${new Date(testSet.lastExecuted).toLocaleString()}`));
402
+ }
403
+ });
404
+ console.log();
405
+ }
406
+ catch (error) {
407
+ handleApiError(error);
408
+ }
409
+ });
410
+ program
411
+ .command('export:testset')
412
+ .description('Export a test set as runnable test code (Playwright for web apps, Axios for REST/SOAP APIs)')
413
+ .requiredOption('-n, --name <name>', 'Name of the test set')
414
+ .requiredOption('-o, --output <dir>', 'Output directory for generated files')
415
+ .option('-a, --api-key <key>', 'API key (or use QATE_API_KEY env var)')
416
+ .option('-u, --url <url>', 'Qate API URL (or use QATE_API_URL env var)')
417
+ .option('--app <applicationId>', 'Application ID (if name is not unique)')
418
+ .action(async (options) => {
419
+ try {
420
+ const { apiKey, baseUrl } = getConfig(options);
421
+ const client = new client_1.QateClient(baseUrl, apiKey);
422
+ console.log(chalk_1.default.blue(`Fetching test set: ${options.name}`));
423
+ const exportData = await client.exportTestSet(options.name, options.app);
424
+ const appType = exportData.application?.type || 'web';
425
+ if (appType === 'web') {
426
+ // Web apps → Playwright tests
427
+ console.log((0, PlaywrightGenerator_1.generateSummary)(exportData));
428
+ const generatorOptions = {
429
+ outputDir: options.output,
430
+ provider: 'local',
431
+ orchestration: 'websocket',
432
+ browsers: ['chromium'],
433
+ browser: 'chrome',
434
+ browserVersion: 'latest',
435
+ os: 'windows',
436
+ osVersion: '11',
437
+ apiUrl: baseUrl,
438
+ };
439
+ const files = (0, PlaywrightGenerator_1.generatePlaywrightTests)(exportData, generatorOptions);
440
+ writeGeneratedFiles(files, options.output);
441
+ console.log(chalk_1.default.green('\nPlaywright tests exported successfully!'));
442
+ console.log(chalk_1.default.gray('\nNext steps:'));
443
+ console.log(chalk_1.default.white(` cd ${options.output}`));
444
+ console.log(chalk_1.default.white(` npm install`));
445
+ console.log(chalk_1.default.white(` npx playwright install`));
446
+ console.log(chalk_1.default.white(` npx playwright test`));
447
+ }
448
+ else if ((0, RestApiExecutor_1.isRestApiApp)(appType)) {
449
+ // REST API / SOAP → Axios tests
450
+ console.log((0, AxiosGenerator_1.generateAxiosSummary)(exportData));
451
+ const axiosOptions = {
452
+ outputDir: options.output,
453
+ };
454
+ const files = (0, AxiosGenerator_1.generateAxiosTests)(exportData, axiosOptions);
455
+ writeAxiosFiles(files, options.output);
456
+ console.log(chalk_1.default.green('\nAxios tests exported successfully!'));
457
+ console.log(chalk_1.default.gray('\nNext steps:'));
458
+ console.log(chalk_1.default.white(` cd ${options.output}`));
459
+ console.log(chalk_1.default.white(` npm install`));
460
+ console.log(chalk_1.default.white(` npx vitest run`));
461
+ }
462
+ else {
463
+ // Desktop or unsupported type
464
+ console.error(chalk_1.default.red(`\nError: Export is not supported for ${appType} applications.`));
465
+ console.log();
466
+ console.log(chalk_1.default.gray('Export currently supports:'));
467
+ console.log(chalk_1.default.white(' - Web applications (exports Playwright tests)'));
468
+ console.log(chalk_1.default.white(' - REST API applications (exports Axios tests)'));
469
+ console.log(chalk_1.default.white(' - SOAP services (exports Axios tests)'));
470
+ process.exit(1);
471
+ }
472
+ }
473
+ catch (error) {
474
+ handleApiError(error);
475
+ }
476
+ });
477
+ // ================== TEST SEQUENCE COMMANDS ==================
478
+ program
479
+ .command('run:sequence')
480
+ .description('Execute a test sequence locally (REST API and SOAP only - use "generate:sequence" for web apps)')
481
+ .requiredOption('-n, --name <name>', 'Name of the test sequence to execute')
482
+ .option('-a, --api-key <key>', 'API key (or use QATE_API_KEY env var)')
483
+ .option('-u, --url <url>', 'Qate API URL (or use QATE_API_URL env var)')
484
+ .option('--app <applicationId>', 'Application ID (if sequence name is not unique)')
485
+ .option('-w, --wait', 'Wait for execution to complete')
486
+ .option('--timeout <seconds>', 'Timeout for waiting (default: 600)', '600')
487
+ .option('--json', 'Output results as JSON')
488
+ .option('-v, --verbose', 'Show detailed step-by-step output')
489
+ .action(async (options) => {
490
+ try {
491
+ const { apiKey, baseUrl } = getConfig(options);
492
+ const client = new client_1.QateClient(baseUrl, apiKey);
493
+ if (!options.json) {
494
+ console.log(chalk_1.default.blue(`Fetching test sequence: ${options.name}`));
495
+ }
496
+ // First, export the test sequence to check application type
497
+ const exportData = await client.exportTestSequence(options.name, options.app);
498
+ const appType = exportData.application?.type || 'web';
499
+ // REST API apps execute locally in the CLI
500
+ if ((0, RestApiExecutor_1.isRestApiApp)(appType)) {
501
+ if (!options.json) {
502
+ console.log(chalk_1.default.cyan(`REST API application detected - executing locally`));
503
+ console.log(chalk_1.default.gray(`Application: ${exportData.application.name}`));
504
+ console.log(chalk_1.default.gray(`Base URL: ${exportData.application.url}`));
505
+ console.log(chalk_1.default.gray(`Tests: ${exportData.tests.length} (sequential)`));
506
+ console.log();
507
+ }
508
+ // Create execution record for tracking
509
+ let executionId;
510
+ try {
511
+ const generateResult = await client.createGenerateExecution(undefined, options.name, options.app, 'cli');
512
+ executionId = generateResult.executionId;
513
+ if (!options.json) {
514
+ console.log(chalk_1.default.gray(`Execution ID: ${executionId}`));
515
+ }
516
+ }
517
+ catch (err) {
518
+ if (!options.json) {
519
+ console.log(chalk_1.default.yellow('Note: Could not create execution record'));
520
+ }
521
+ }
522
+ // Execute tests locally (sequentially - stop on first failure)
523
+ if (!options.json) {
524
+ console.log(chalk_1.default.blue(`\nExecuting ${exportData.tests.length} tests sequentially...\n`));
525
+ }
526
+ const results = [];
527
+ let stoppedEarly = false;
528
+ for (let i = 0; i < exportData.tests.length; i++) {
529
+ const test = exportData.tests[i];
530
+ const testResults = await (0, RestApiExecutor_1.executeTests)([test], exportData.application.url, { verbose: options.verbose }, (testResult) => {
531
+ if (!options.json) {
532
+ console.log((0, RestApiExecutor_1.formatTestResult)(testResult, options.verbose));
533
+ }
534
+ });
535
+ results.push(...testResults);
536
+ // Stop on failure for sequences (tests are dependent)
537
+ if (testResults[0].status !== 'passed') {
538
+ stoppedEarly = true;
539
+ if (!options.json) {
540
+ console.log(chalk_1.default.yellow(`\nSequence stopped at test ${i + 1} due to failure`));
541
+ }
542
+ break;
543
+ }
544
+ }
545
+ // Calculate summary
546
+ const summary = (0, RestApiExecutor_1.calculateSummary)(results);
547
+ // Report results to backend
548
+ if (executionId) {
549
+ try {
550
+ await client.reportResults(executionId, results, summary, 'cli', {
551
+ os: os.platform(),
552
+ nodeVersion: process.version
553
+ });
554
+ if (!options.json) {
555
+ console.log(chalk_1.default.gray(`\nResults reported to Qate`));
556
+ }
557
+ }
558
+ catch (err) {
559
+ if (!options.json) {
560
+ console.log(chalk_1.default.yellow('Note: Could not report results to backend'));
561
+ }
562
+ }
563
+ }
564
+ // Display final summary
565
+ if (options.json) {
566
+ console.log(JSON.stringify({
567
+ executionId,
568
+ testSequenceName: exportData.testSequence?.name,
569
+ status: summary.status,
570
+ stoppedEarly,
571
+ summary: {
572
+ total: exportData.tests.length,
573
+ executed: summary.total,
574
+ passed: summary.passed,
575
+ failed: summary.failed,
576
+ error: summary.error,
577
+ skipped: exportData.tests.length - summary.total
578
+ },
579
+ tests: results.map(r => ({
580
+ testId: r.testId,
581
+ testName: r.testName,
582
+ status: r.status,
583
+ duration: r.duration
584
+ }))
585
+ }, null, 2));
586
+ }
587
+ else {
588
+ console.log();
589
+ console.log(chalk_1.default.blue(`Summary`));
590
+ console.log(chalk_1.default.gray(`─────────────────────────────────────`));
591
+ console.log(` Total: ${exportData.tests.length}`);
592
+ console.log(` Executed: ${summary.total}`);
593
+ console.log(` Passed: ${chalk_1.default.green(summary.passed.toString())}`);
594
+ console.log(` Failed: ${chalk_1.default.red(summary.failed.toString())}`);
595
+ console.log(` Error: ${chalk_1.default.red(summary.error.toString())}`);
596
+ if (stoppedEarly) {
597
+ console.log(` Skipped: ${chalk_1.default.yellow((exportData.tests.length - summary.total).toString())}`);
598
+ }
599
+ console.log();
600
+ const statusColor = summary.status === 'passed' ? chalk_1.default.green :
601
+ summary.status === 'failed' ? chalk_1.default.red : chalk_1.default.red;
602
+ console.log(`Status: ${statusColor(summary.status.toUpperCase())}`);
603
+ }
604
+ process.exit(summary.status === 'passed' ? 0 : 1);
605
+ }
606
+ else if (appType === 'desktop') {
607
+ // Desktop apps: trigger server-side execution via connected agent, then poll
608
+ if (!options.json) {
609
+ console.log(chalk_1.default.cyan('Desktop application detected - executing via connected agent'));
610
+ console.log(chalk_1.default.gray(`Application: ${exportData.application.name}`));
611
+ }
612
+ const result = await client.executeTestSequence(options.name, options.app);
613
+ if (!options.json) {
614
+ console.log(chalk_1.default.gray(`Execution ID: ${result.executionId}`));
615
+ console.log(chalk_1.default.blue('\nWaiting for execution to complete...\n'));
616
+ }
617
+ const timeoutMs = parseInt(options.timeout) * 1000;
618
+ const status = await client.pollExecution(result.executionId, 'sequence', timeoutMs);
619
+ if (options.json) {
620
+ console.log(JSON.stringify(status, null, 2));
621
+ }
622
+ else {
623
+ displayStatus(status, 'sequence');
624
+ }
625
+ process.exit(status.status === 'passed' ? 0 : 1);
626
+ }
627
+ else {
628
+ // Web apps should use 'qate generate:sequence' to create Playwright tests
629
+ console.error(chalk_1.default.red('\nError: "qate run:sequence" is not supported for web applications.'));
630
+ console.log();
631
+ console.log(chalk_1.default.yellow('For web applications, use "qate generate:sequence" to create Playwright tests:'));
632
+ console.log();
633
+ console.log(chalk_1.default.white(` qate generate:sequence -n "${options.name}" -o ./e2e`));
634
+ console.log(chalk_1.default.white(` cd ./e2e && npm install`));
635
+ console.log(chalk_1.default.white(` npx playwright test`));
636
+ console.log();
637
+ console.log(chalk_1.default.gray('This generates Playwright test files that you can run locally or in CI/CD.'));
638
+ console.log(chalk_1.default.gray('Results are automatically reported back to Qate.'));
639
+ process.exit(1);
640
+ }
641
+ }
642
+ catch (error) {
643
+ handleApiError(error);
644
+ }
645
+ });
646
+ program
647
+ .command('status:sequence')
648
+ .description('Get status of a test sequence execution')
649
+ .requiredOption('-e, --execution-id <id>', 'Execution ID')
650
+ .option('-a, --api-key <key>', 'API key (or use QATE_API_KEY env var)')
651
+ .option('-u, --url <url>', 'Qate API URL (or use QATE_API_URL env var)')
652
+ .option('--json', 'Output as JSON')
653
+ .action(async (options) => {
654
+ try {
655
+ const { apiKey, baseUrl } = getConfig(options);
656
+ const client = new client_1.QateClient(baseUrl, apiKey);
657
+ const status = await client.getTestSequenceStatus(options.executionId);
658
+ if (options.json) {
659
+ console.log(JSON.stringify(status, null, 2));
660
+ }
661
+ else {
662
+ displayStatus(status, 'sequence');
663
+ }
664
+ process.exit(status.status === 'passed' ? 0 : status.status === 'running' ? 0 : 1);
665
+ }
666
+ catch (error) {
667
+ handleApiError(error);
668
+ }
669
+ });
670
+ program
671
+ .command('list:sequences')
672
+ .description('List all test sequences')
673
+ .option('-a, --api-key <key>', 'API key (or use QATE_API_KEY env var)')
674
+ .option('-u, --url <url>', 'Qate API URL (or use QATE_API_URL env var)')
675
+ .option('--json', 'Output as JSON')
676
+ .action(async (options) => {
677
+ try {
678
+ const { apiKey, baseUrl } = getConfig(options);
679
+ const client = new client_1.QateClient(baseUrl, apiKey);
680
+ const result = await client.listTestSequences();
681
+ if (options.json) {
682
+ console.log(JSON.stringify(result, null, 2));
683
+ return;
684
+ }
685
+ console.log(chalk_1.default.blue(`\nTest Sequences (${result.testSequences.length})`));
686
+ console.log(chalk_1.default.gray(`─────────────────────────────────────`));
687
+ if (result.testSequences.length === 0) {
688
+ console.log(chalk_1.default.gray(' No test sequences found'));
689
+ return;
690
+ }
691
+ result.testSequences.forEach((seq) => {
692
+ const statusIcon = seq.lastExecutionStatus === 'passed' ? chalk_1.default.green('✓') :
693
+ seq.lastExecutionStatus === 'failed' ? chalk_1.default.red('✗') :
694
+ chalk_1.default.gray('○');
695
+ console.log(`\n ${statusIcon} ${chalk_1.default.white(seq.name)}`);
696
+ if (seq.description) {
697
+ console.log(chalk_1.default.gray(` ${seq.description}`));
698
+ }
699
+ console.log(chalk_1.default.gray(` App: ${seq.application.name} | Tests: ${seq.testCount} (sequential)`));
700
+ if (seq.lastExecuted) {
701
+ console.log(chalk_1.default.gray(` Last run: ${new Date(seq.lastExecuted).toLocaleString()}`));
702
+ }
703
+ });
704
+ console.log();
705
+ }
706
+ catch (error) {
707
+ handleApiError(error);
708
+ }
709
+ });
710
+ program
711
+ .command('export:sequence')
712
+ .description('Export a test sequence as runnable test code (Playwright for web apps, Axios for REST/SOAP APIs)')
713
+ .requiredOption('-n, --name <name>', 'Name of the test sequence')
714
+ .requiredOption('-o, --output <dir>', 'Output directory for generated files')
715
+ .option('-a, --api-key <key>', 'API key (or use QATE_API_KEY env var)')
716
+ .option('-u, --url <url>', 'Qate API URL (or use QATE_API_URL env var)')
717
+ .option('--app <applicationId>', 'Application ID (if name is not unique)')
718
+ .action(async (options) => {
719
+ try {
720
+ const { apiKey, baseUrl } = getConfig(options);
721
+ const client = new client_1.QateClient(baseUrl, apiKey);
722
+ console.log(chalk_1.default.blue(`Fetching test sequence: ${options.name}`));
723
+ const exportData = await client.exportTestSequence(options.name, options.app);
724
+ const appType = exportData.application?.type || 'web';
725
+ if (appType === 'web') {
726
+ // Web apps → Playwright tests
727
+ console.log((0, PlaywrightGenerator_1.generateSummary)(exportData));
728
+ const generatorOptions = {
729
+ outputDir: options.output,
730
+ provider: 'local',
731
+ orchestration: 'websocket',
732
+ browsers: ['chromium'],
733
+ browser: 'chrome',
734
+ browserVersion: 'latest',
735
+ os: 'windows',
736
+ osVersion: '11',
737
+ apiUrl: baseUrl,
738
+ };
739
+ const files = (0, PlaywrightGenerator_1.generatePlaywrightTests)(exportData, generatorOptions);
740
+ writeGeneratedFiles(files, options.output);
741
+ console.log(chalk_1.default.green('\nPlaywright tests exported successfully!'));
742
+ console.log(chalk_1.default.gray('\nNext steps:'));
743
+ console.log(chalk_1.default.white(` cd ${options.output}`));
744
+ console.log(chalk_1.default.white(` npm install`));
745
+ console.log(chalk_1.default.white(` npx playwright install`));
746
+ console.log(chalk_1.default.white(` npx playwright test`));
747
+ }
748
+ else if ((0, RestApiExecutor_1.isRestApiApp)(appType)) {
749
+ // REST API / SOAP → Axios tests
750
+ console.log((0, AxiosGenerator_1.generateAxiosSummary)(exportData));
751
+ const axiosOptions = {
752
+ outputDir: options.output,
753
+ };
754
+ const files = (0, AxiosGenerator_1.generateAxiosTests)(exportData, axiosOptions);
755
+ writeAxiosFiles(files, options.output);
756
+ console.log(chalk_1.default.green('\nAxios tests exported successfully!'));
757
+ console.log(chalk_1.default.gray('\nNext steps:'));
758
+ console.log(chalk_1.default.white(` cd ${options.output}`));
759
+ console.log(chalk_1.default.white(` npm install`));
760
+ console.log(chalk_1.default.white(` npx vitest run`));
761
+ }
762
+ else {
763
+ // Desktop or unsupported type
764
+ console.error(chalk_1.default.red(`\nError: Export is not supported for ${appType} applications.`));
765
+ console.log();
766
+ console.log(chalk_1.default.gray('Export currently supports:'));
767
+ console.log(chalk_1.default.white(' - Web applications (exports Playwright tests)'));
768
+ console.log(chalk_1.default.white(' - REST API applications (exports Axios tests)'));
769
+ console.log(chalk_1.default.white(' - SOAP services (exports Axios tests)'));
770
+ process.exit(1);
771
+ }
772
+ }
773
+ catch (error) {
774
+ handleApiError(error);
775
+ }
776
+ });
777
+ // ================== GENERIC STATUS COMMAND ==================
778
+ program
779
+ .command('status')
780
+ .description('Get status of any execution (auto-detects type from execution ID)')
781
+ .requiredOption('-e, --execution-id <id>', 'Execution ID')
782
+ .option('-a, --api-key <key>', 'API key (or use QATE_API_KEY env var)')
783
+ .option('-u, --url <url>', 'Qate API URL (or use QATE_API_URL env var)')
784
+ .option('--json', 'Output as JSON')
785
+ .action(async (options) => {
786
+ try {
787
+ const { apiKey, baseUrl } = getConfig(options);
788
+ const client = new client_1.QateClient(baseUrl, apiKey);
789
+ // Detect type from execution ID prefix
790
+ const isSequence = options.executionId.startsWith('seq_');
791
+ const isGenerated = options.executionId.startsWith('gen_');
792
+ if (isGenerated) {
793
+ // Handle generated test executions
794
+ const status = await client.getGenerateStatus(options.executionId);
795
+ if (options.json) {
796
+ console.log(JSON.stringify(status, null, 2));
797
+ }
798
+ else {
799
+ console.log();
800
+ console.log(chalk_1.default.blue(`Generated Execution Status`));
801
+ console.log(chalk_1.default.gray(`─────────────────────────────────────`));
802
+ console.log(` Execution ID: ${chalk_1.default.cyan(status.executionId)}`);
803
+ console.log(` Source: ${chalk_1.default.white(status.sourceName)} (${status.sourceType})`);
804
+ const statusColor = status.status === 'passed' ? chalk_1.default.green :
805
+ status.status === 'failed' ? chalk_1.default.red :
806
+ status.status === 'pending' ? chalk_1.default.gray :
807
+ status.status === 'running' ? chalk_1.default.yellow : chalk_1.default.gray;
808
+ console.log(` Status: ${statusColor(status.status.toUpperCase())}`);
809
+ console.log();
810
+ console.log(chalk_1.default.blue(`Summary`));
811
+ console.log(chalk_1.default.gray(`─────────────────────────────────────`));
812
+ console.log(` Total: ${status.summary.total}`);
813
+ console.log(` Passed: ${chalk_1.default.green(status.summary.passed.toString())}`);
814
+ console.log(` Failed: ${chalk_1.default.red(status.summary.failed.toString())}`);
815
+ console.log(` Error: ${chalk_1.default.red(status.summary.error.toString())}`);
816
+ console.log(` Pending: ${chalk_1.default.yellow(status.summary.pending.toString())}`);
817
+ if (status.performance.totalDuration > 0) {
818
+ console.log();
819
+ console.log(chalk_1.default.blue(`Performance`));
820
+ console.log(chalk_1.default.gray(`─────────────────────────────────────`));
821
+ console.log(` Total Duration: ${(status.performance.totalDuration / 1000).toFixed(1)}s`);
822
+ console.log(` Avg Test Duration: ${(status.performance.avgTestDuration / 1000).toFixed(1)}s`);
823
+ }
824
+ if (status.tests && status.tests.length > 0) {
825
+ console.log();
826
+ console.log(chalk_1.default.blue(`Test Results`));
827
+ console.log(chalk_1.default.gray(`─────────────────────────────────────`));
828
+ status.tests.forEach((test) => {
829
+ const icon = test.status === 'passed' ? chalk_1.default.green('✓') :
830
+ test.status === 'failed' ? chalk_1.default.red('✗') :
831
+ test.status === 'error' ? chalk_1.default.red('!') : chalk_1.default.gray('○');
832
+ const time = test.duration ? `(${(test.duration / 1000).toFixed(1)}s)` : '';
833
+ const browser = test.browser ? chalk_1.default.gray(` [${test.browser}]`) : '';
834
+ console.log(` ${icon} ${test.testName} ${chalk_1.default.gray(time)}${browser}`);
835
+ });
836
+ }
837
+ console.log();
838
+ console.log(chalk_1.default.gray(`Last updated: ${status.lastUpdated}`));
839
+ }
840
+ process.exit(status.status === 'passed' ? 0 : status.status === 'running' || status.status === 'pending' ? 0 : 1);
841
+ }
842
+ else {
843
+ // Handle test set or sequence executions
844
+ const status = isSequence
845
+ ? await client.getTestSequenceStatus(options.executionId)
846
+ : await client.getTestSetStatus(options.executionId);
847
+ if (options.json) {
848
+ console.log(JSON.stringify(status, null, 2));
849
+ }
850
+ else {
851
+ displayStatus(status, isSequence ? 'sequence' : 'testset');
852
+ }
853
+ process.exit(status.status === 'passed' ? 0 : status.status === 'running' ? 0 : 1);
854
+ }
855
+ }
856
+ catch (error) {
857
+ handleApiError(error);
858
+ }
859
+ });
860
+ // ================== HELP COMMAND ==================
861
+ program
862
+ .command('help [command]')
863
+ .description('Display help for a command')
864
+ .action((commandName) => {
865
+ if (commandName) {
866
+ const cmd = program.commands.find(c => c.name() === commandName || c.aliases().includes(commandName));
867
+ if (cmd) {
868
+ cmd.outputHelp();
869
+ }
870
+ else {
871
+ console.error(chalk_1.default.red(`Unknown command: ${commandName}`));
872
+ console.log(chalk_1.default.gray('Run "qate --help" to see available commands'));
873
+ process.exit(1);
874
+ }
875
+ }
876
+ else {
877
+ console.log(chalk_1.default.blue.bold('\n Qate CLI - Test Automation for CI/CD Pipelines\n'));
878
+ console.log(chalk_1.default.white(' Generate and run Playwright tests from your Qate test definitions.\n'));
879
+ console.log(chalk_1.default.yellow(' GETTING STARTED'));
880
+ console.log(chalk_1.default.gray(' ─────────────────────────────────────────────────────────────'));
881
+ console.log(' 1. Get your API key from: Settings > CI/CD API Keys');
882
+ console.log(' 2. Set the environment variable:');
883
+ console.log(chalk_1.default.cyan(' export QATE_API_KEY=qate_xxxxxxxxxxxx'));
884
+ console.log(' 3. Run your first command:');
885
+ console.log(chalk_1.default.cyan(' qate list\n'));
886
+ console.log(chalk_1.default.yellow(' TEST SET COMMANDS (Parallel Execution)'));
887
+ console.log(chalk_1.default.gray(' ─────────────────────────────────────────────────────────────'));
888
+ console.log(' qate list List all test sets');
889
+ console.log(' qate generate -n <name> -o <dir> Generate Playwright tests (web)');
890
+ console.log(' qate run -n <name> [--wait] Execute REST/SOAP tests locally');
891
+ console.log(' qate status:testset -e <id> Check test set execution status');
892
+ console.log(' qate export:testset -n <name> -o . Export as Playwright/Axios tests\n');
893
+ console.log(chalk_1.default.yellow(' TEST SEQUENCE COMMANDS (Sequential Execution)'));
894
+ console.log(chalk_1.default.gray(' ─────────────────────────────────────────────────────────────'));
895
+ console.log(' qate list:sequences List all test sequences');
896
+ console.log(' qate generate:sequence -n <name> Generate Playwright tests (web)');
897
+ console.log(' qate run:sequence -n <name> Execute REST/SOAP tests locally');
898
+ console.log(' qate status:sequence -e <id> Check sequence execution status');
899
+ console.log(' qate export:sequence -n <name> -o . Export as Playwright/Axios tests\n');
900
+ console.log(chalk_1.default.yellow(' UTILITY COMMANDS'));
901
+ console.log(chalk_1.default.gray(' ─────────────────────────────────────────────────────────────'));
902
+ console.log(' qate status -e <id> Auto-detect and show status');
903
+ console.log(' qate help [command] Show help for a command');
904
+ console.log(' qate --version Show CLI version\n');
905
+ console.log(chalk_1.default.yellow(' EXAMPLES'));
906
+ console.log(chalk_1.default.gray(' ─────────────────────────────────────────────────────────────'));
907
+ console.log(chalk_1.default.gray(' # Run tests and wait for completion'));
908
+ console.log(' qate run -n "Regression Suite" --wait\n');
909
+ console.log(chalk_1.default.gray(' # Generate Playwright tests for local execution'));
910
+ console.log(' qate generate -n "Regression Suite" -o ./e2e\n');
911
+ console.log(chalk_1.default.gray(' # Generate tests for BrowserStack'));
912
+ console.log(' qate generate -n "Regression Suite" -o ./e2e --provider browserstack\n');
913
+ console.log(chalk_1.default.gray(' # Generate tests for LambdaTest'));
914
+ console.log(' qate generate -n "Regression Suite" -o ./e2e --provider lambdatest\n');
915
+ console.log(chalk_1.default.gray(' # Export as standalone test code (auto-detects app type)'));
916
+ console.log(' qate export:testset -n "Regression Suite" -o ./exported-tests\n');
917
+ console.log(chalk_1.default.yellow(' CONFIGURATION'));
918
+ console.log(chalk_1.default.gray(' ─────────────────────────────────────────────────────────────'));
919
+ console.log(' The API key can be provided in two ways:');
920
+ console.log(' • Environment variable: ' + chalk_1.default.cyan('export QATE_API_KEY=qate_xxx'));
921
+ console.log(' • Command line option: ' + chalk_1.default.cyan('qate list --api-key qate_xxx') + '\n');
922
+ console.log(chalk_1.default.yellow(' ENVIRONMENT VARIABLES'));
923
+ console.log(chalk_1.default.gray(' ─────────────────────────────────────────────────────────────'));
924
+ console.log(' QATE_API_KEY ' + chalk_1.default.white('Your Qate API key (required)'));
925
+ console.log(' QATE_API_URL ' + chalk_1.default.gray('API URL (default: https://api.qate.ai)'));
926
+ console.log(' BROWSERSTACK_USERNAME ' + chalk_1.default.gray('BrowserStack username'));
927
+ console.log(' BROWSERSTACK_ACCESS_KEY ' + chalk_1.default.gray('BrowserStack access key'));
928
+ console.log(' SAUCE_USERNAME ' + chalk_1.default.gray('Sauce Labs username'));
929
+ console.log(' SAUCE_ACCESS_KEY ' + chalk_1.default.gray('Sauce Labs access key'));
930
+ console.log(' LT_USERNAME ' + chalk_1.default.gray('LambdaTest username'));
931
+ console.log(' LT_ACCESS_KEY ' + chalk_1.default.gray('LambdaTest access key') + '\n');
932
+ console.log(chalk_1.default.gray(' For more details on a command, run: qate help <command>'));
933
+ console.log(chalk_1.default.gray(' Documentation: https://docs.qate.io/cli\n'));
934
+ }
935
+ });
936
+ // ================== PLAYWRIGHT GENERATION COMMANDS ==================
937
+ /**
938
+ * Helper function to write generated files to output directory
939
+ */
940
+ function writeGeneratedFiles(files, outputDir, silent = false) {
941
+ // Create output directory
942
+ if (!fs.existsSync(outputDir)) {
943
+ fs.mkdirSync(outputDir, { recursive: true });
944
+ }
945
+ // Create tests subdirectory
946
+ const testsDir = path.join(outputDir, 'tests');
947
+ if (!fs.existsSync(testsDir)) {
948
+ fs.mkdirSync(testsDir, { recursive: true });
949
+ }
950
+ // Write config and package.json
951
+ fs.writeFileSync(path.join(outputDir, 'playwright.config.ts'), files['playwright.config.ts']);
952
+ fs.writeFileSync(path.join(outputDir, 'package.json'), files['package.json']);
953
+ // Write Qate reporter for result reporting
954
+ if (files['qate-reporter.ts']) {
955
+ fs.writeFileSync(path.join(outputDir, 'qate-reporter.ts'), files['qate-reporter.ts']);
956
+ }
957
+ // Write qate-runtime.ts (always needed for step timing functions used by reporter)
958
+ if (files['qate-runtime.ts']) {
959
+ fs.writeFileSync(path.join(outputDir, 'qate-runtime.ts'), files['qate-runtime.ts']);
960
+ }
961
+ // Write Sauce Labs config if present
962
+ if (files['.sauce/config.yml']) {
963
+ const sauceDir = path.join(outputDir, '.sauce');
964
+ if (!fs.existsSync(sauceDir)) {
965
+ fs.mkdirSync(sauceDir, { recursive: true });
966
+ }
967
+ fs.writeFileSync(path.join(sauceDir, 'config.yml'), files['.sauce/config.yml']);
968
+ }
969
+ // Write test files
970
+ for (const [filename, content] of Object.entries(files.tests)) {
971
+ fs.writeFileSync(path.join(testsDir, filename), content);
972
+ }
973
+ if (!silent) {
974
+ console.log(chalk_1.default.green(`\nGenerated files in ${outputDir}:`));
975
+ console.log(chalk_1.default.gray(` playwright.config.ts`));
976
+ console.log(chalk_1.default.gray(` package.json`));
977
+ console.log(chalk_1.default.gray(` qate-reporter.ts`));
978
+ console.log(chalk_1.default.gray(` qate-runtime.ts`));
979
+ if (files['.sauce/config.yml']) {
980
+ console.log(chalk_1.default.gray(` .sauce/config.yml`));
981
+ }
982
+ console.log(chalk_1.default.gray(` tests/`));
983
+ Object.keys(files.tests).forEach(filename => {
984
+ console.log(chalk_1.default.gray(` ${filename}`));
985
+ });
986
+ }
987
+ }
988
+ /**
989
+ * Helper function to write generated Axios test files to output directory
990
+ */
991
+ function writeAxiosFiles(files, outputDir, silent = false) {
992
+ // Create output directory
993
+ if (!fs.existsSync(outputDir)) {
994
+ fs.mkdirSync(outputDir, { recursive: true });
995
+ }
996
+ // Create tests subdirectory
997
+ const testsDir = path.join(outputDir, 'tests');
998
+ if (!fs.existsSync(testsDir)) {
999
+ fs.mkdirSync(testsDir, { recursive: true });
1000
+ }
1001
+ // Write config and package.json
1002
+ fs.writeFileSync(path.join(outputDir, 'package.json'), files['package.json']);
1003
+ fs.writeFileSync(path.join(outputDir, 'vitest.config.ts'), files['vitest.config.ts']);
1004
+ // Write test files
1005
+ for (const [filename, content] of Object.entries(files.tests)) {
1006
+ fs.writeFileSync(path.join(testsDir, filename), content);
1007
+ }
1008
+ if (!silent) {
1009
+ console.log(chalk_1.default.green(`\nGenerated files in ${outputDir}:`));
1010
+ console.log(chalk_1.default.gray(` package.json`));
1011
+ console.log(chalk_1.default.gray(` vitest.config.ts`));
1012
+ console.log(chalk_1.default.gray(` tests/`));
1013
+ Object.keys(files.tests).forEach(filename => {
1014
+ console.log(chalk_1.default.gray(` ${filename}`));
1015
+ });
1016
+ }
1017
+ }
1018
+ program
1019
+ .command('generate:testset')
1020
+ .alias('generate')
1021
+ .description('Generate Playwright test files from a test set')
1022
+ .requiredOption('-n, --name <name>', 'Name of the test set')
1023
+ .requiredOption('-o, --output <dir>', 'Output directory for generated files')
1024
+ .option('-a, --api-key <key>', 'API key (or use QATE_API_KEY env var)')
1025
+ .option('-u, --url <url>', 'Qate API URL (or use QATE_API_URL env var)')
1026
+ .option('--app <applicationId>', 'Application ID (if name is not unique)')
1027
+ .option('--provider <provider>', 'Test provider: local, browserstack, saucelabs, lambdatest', 'local')
1028
+ .option('--orchestration <mode>', 'Orchestration mode: websocket (run from CI) or cli (provider orchestrates)', 'websocket')
1029
+ .option('--browsers <browsers>', 'Comma-separated list of browsers (for local provider)', 'chromium')
1030
+ .option('--browser <browser>', 'Browser for cloud providers: chrome, firefox, safari, edge', 'chrome')
1031
+ .option('--browser-version <version>', 'Browser version for cloud providers', 'latest')
1032
+ .option('--os <os>', 'Operating system for cloud providers: windows, macos', 'windows')
1033
+ .option('--os-version <version>', 'OS version for cloud providers (e.g., 11, Sonoma)', '11')
1034
+ .option('--no-tracking', 'Disable result reporting to Qate')
1035
+ .action(async (options) => {
1036
+ try {
1037
+ const { apiKey, baseUrl } = getConfig(options);
1038
+ const client = new client_1.QateClient(baseUrl, apiKey);
1039
+ console.log(chalk_1.default.blue(`Fetching test set: ${options.name}`));
1040
+ // Create execution record for tracking (unless disabled)
1041
+ let executionId;
1042
+ if (options.tracking !== false) {
1043
+ try {
1044
+ const generateResult = await client.createGenerateExecution(options.name, undefined, options.app, options.provider);
1045
+ executionId = generateResult.executionId;
1046
+ console.log(chalk_1.default.gray(`Execution ID: ${executionId}`));
1047
+ }
1048
+ catch (err) {
1049
+ console.log(chalk_1.default.yellow('Note: Could not create execution record for tracking'));
1050
+ console.log(chalk_1.default.gray(' Results will not be reported to Qate'));
1051
+ }
1052
+ }
1053
+ const exportData = await client.exportTestSet(options.name, options.app);
1054
+ // Show summary
1055
+ console.log((0, PlaywrightGenerator_1.generateSummary)(exportData));
1056
+ // Generate files with execution ID for result reporting
1057
+ const generatorOptions = {
1058
+ outputDir: options.output,
1059
+ provider: options.provider,
1060
+ orchestration: options.orchestration,
1061
+ browsers: options.browsers.split(',').map((b) => b.trim()),
1062
+ browser: options.browser,
1063
+ browserVersion: options.browserVersion,
1064
+ os: options.os,
1065
+ osVersion: options.osVersion,
1066
+ executionId,
1067
+ apiUrl: baseUrl,
1068
+ };
1069
+ const files = (0, PlaywrightGenerator_1.generatePlaywrightTests)(exportData, generatorOptions);
1070
+ // Write files
1071
+ writeGeneratedFiles(files, options.output);
1072
+ console.log(chalk_1.default.green('\nPlaywright tests generated successfully!'));
1073
+ if (executionId) {
1074
+ console.log(chalk_1.default.cyan(`\nExecution tracking enabled`));
1075
+ console.log(chalk_1.default.gray(` Results will be reported to Qate when tests complete`));
1076
+ console.log(chalk_1.default.gray(` Check status: qate status -e ${executionId}`));
1077
+ }
1078
+ // Show platform targeting and orchestration info for cloud providers
1079
+ if (options.provider !== 'local') {
1080
+ const effectiveOrchestration = options.provider === 'saucelabs' ? 'cli' : options.orchestration;
1081
+ console.log(chalk_1.default.cyan(`\nPlatform targeting:`));
1082
+ console.log(chalk_1.default.gray(` Browser: ${options.browser} (${options.browserVersion})`));
1083
+ console.log(chalk_1.default.gray(` OS: ${options.os} ${options.osVersion}`));
1084
+ console.log(chalk_1.default.gray(` Orchestration: ${effectiveOrchestration}`));
1085
+ }
1086
+ console.log(chalk_1.default.gray('\nNext steps:'));
1087
+ console.log(chalk_1.default.white(` cd ${options.output}`));
1088
+ console.log(chalk_1.default.white(` npm install`));
1089
+ // Show provider-specific run commands based on orchestration mode
1090
+ const effectiveOrchestration = options.provider === 'saucelabs' ? 'cli' : options.orchestration;
1091
+ if (options.provider === 'saucelabs') {
1092
+ console.log(chalk_1.default.white(` npm install -g saucectl`));
1093
+ console.log(chalk_1.default.white(` saucectl run`));
1094
+ }
1095
+ else if (options.provider === 'browserstack' && effectiveOrchestration === 'cli') {
1096
+ console.log(chalk_1.default.white(` npm install -D browserstack-node-sdk`));
1097
+ console.log(chalk_1.default.white(` npx browserstack-node-sdk playwright test`));
1098
+ }
1099
+ else if (options.provider === 'lambdatest' && effectiveOrchestration === 'cli') {
1100
+ console.log(chalk_1.default.white(` # Download HyperExecute CLI from LambdaTest`));
1101
+ console.log(chalk_1.default.white(` ./hyperexecute --config hyperexecute.yaml`));
1102
+ }
1103
+ else {
1104
+ console.log(chalk_1.default.white(` npx playwright install`));
1105
+ console.log(chalk_1.default.white(` npx playwright test`));
1106
+ }
1107
+ }
1108
+ catch (error) {
1109
+ handleApiError(error);
1110
+ }
1111
+ });
1112
+ program
1113
+ .command('generate:sequence')
1114
+ .description('Generate Playwright test files from a test sequence')
1115
+ .requiredOption('-n, --name <name>', 'Name of the test sequence')
1116
+ .requiredOption('-o, --output <dir>', 'Output directory for generated files')
1117
+ .option('-a, --api-key <key>', 'API key (or use QATE_API_KEY env var)')
1118
+ .option('-u, --url <url>', 'Qate API URL (or use QATE_API_URL env var)')
1119
+ .option('--app <applicationId>', 'Application ID (if name is not unique)')
1120
+ .option('--provider <provider>', 'Test provider: local, browserstack, saucelabs, lambdatest', 'local')
1121
+ .option('--orchestration <mode>', 'Orchestration mode: websocket (run from CI) or cli (provider orchestrates)', 'websocket')
1122
+ .option('--browsers <browsers>', 'Comma-separated list of browsers (for local provider)', 'chromium')
1123
+ .option('--browser <browser>', 'Browser for cloud providers: chrome, firefox, safari, edge', 'chrome')
1124
+ .option('--browser-version <version>', 'Browser version for cloud providers', 'latest')
1125
+ .option('--os <os>', 'Operating system for cloud providers: windows, macos', 'windows')
1126
+ .option('--os-version <version>', 'OS version for cloud providers (e.g., 11, Sonoma)', '11')
1127
+ .option('--no-tracking', 'Disable result reporting to Qate')
1128
+ .action(async (options) => {
1129
+ try {
1130
+ const { apiKey, baseUrl } = getConfig(options);
1131
+ const client = new client_1.QateClient(baseUrl, apiKey);
1132
+ console.log(chalk_1.default.blue(`Fetching test sequence: ${options.name}`));
1133
+ // Create execution record for tracking (unless disabled)
1134
+ let executionId;
1135
+ if (options.tracking !== false) {
1136
+ try {
1137
+ const generateResult = await client.createGenerateExecution(undefined, options.name, options.app, options.provider);
1138
+ executionId = generateResult.executionId;
1139
+ console.log(chalk_1.default.gray(`Execution ID: ${executionId}`));
1140
+ }
1141
+ catch (err) {
1142
+ console.log(chalk_1.default.yellow('Note: Could not create execution record for tracking'));
1143
+ console.log(chalk_1.default.gray(' Results will not be reported to Qate'));
1144
+ }
1145
+ }
1146
+ const exportData = await client.exportTestSequence(options.name, options.app);
1147
+ // Show summary
1148
+ console.log((0, PlaywrightGenerator_1.generateSummary)(exportData));
1149
+ // Generate files with execution ID for result reporting
1150
+ const generatorOptions = {
1151
+ outputDir: options.output,
1152
+ provider: options.provider,
1153
+ orchestration: options.orchestration,
1154
+ browsers: options.browsers.split(',').map((b) => b.trim()),
1155
+ browser: options.browser,
1156
+ browserVersion: options.browserVersion,
1157
+ os: options.os,
1158
+ osVersion: options.osVersion,
1159
+ executionId,
1160
+ apiUrl: baseUrl,
1161
+ };
1162
+ const files = (0, PlaywrightGenerator_1.generatePlaywrightTests)(exportData, generatorOptions);
1163
+ // Write files
1164
+ writeGeneratedFiles(files, options.output);
1165
+ console.log(chalk_1.default.green('\nPlaywright tests generated successfully!'));
1166
+ if (executionId) {
1167
+ console.log(chalk_1.default.cyan(`\nExecution tracking enabled`));
1168
+ console.log(chalk_1.default.gray(` Results will be reported to Qate when tests complete`));
1169
+ console.log(chalk_1.default.gray(` Check status: qate status -e ${executionId}`));
1170
+ }
1171
+ // Show platform targeting and orchestration info for cloud providers
1172
+ if (options.provider !== 'local') {
1173
+ const effectiveOrchestration = options.provider === 'saucelabs' ? 'cli' : options.orchestration;
1174
+ console.log(chalk_1.default.cyan(`\nPlatform targeting:`));
1175
+ console.log(chalk_1.default.gray(` Browser: ${options.browser} (${options.browserVersion})`));
1176
+ console.log(chalk_1.default.gray(` OS: ${options.os} ${options.osVersion}`));
1177
+ console.log(chalk_1.default.gray(` Orchestration: ${effectiveOrchestration}`));
1178
+ }
1179
+ console.log(chalk_1.default.gray('\nNext steps:'));
1180
+ console.log(chalk_1.default.white(` cd ${options.output}`));
1181
+ console.log(chalk_1.default.white(` npm install`));
1182
+ // Show provider-specific run commands based on orchestration mode
1183
+ const effectiveOrchestration = options.provider === 'saucelabs' ? 'cli' : options.orchestration;
1184
+ if (options.provider === 'saucelabs') {
1185
+ console.log(chalk_1.default.white(` npm install -g saucectl`));
1186
+ console.log(chalk_1.default.white(` saucectl run`));
1187
+ }
1188
+ else if (options.provider === 'browserstack' && effectiveOrchestration === 'cli') {
1189
+ console.log(chalk_1.default.white(` npm install -D browserstack-node-sdk`));
1190
+ console.log(chalk_1.default.white(` npx browserstack-node-sdk playwright test`));
1191
+ }
1192
+ else if (options.provider === 'lambdatest' && effectiveOrchestration === 'cli') {
1193
+ console.log(chalk_1.default.white(` # Download HyperExecute CLI from LambdaTest`));
1194
+ console.log(chalk_1.default.white(` ./hyperexecute --config hyperexecute.yaml`));
1195
+ }
1196
+ else {
1197
+ console.log(chalk_1.default.white(` npx playwright install`));
1198
+ console.log(chalk_1.default.white(` npx playwright test`));
1199
+ }
1200
+ }
1201
+ catch (error) {
1202
+ handleApiError(error);
1203
+ }
1204
+ });
1205
+ // ================== INSTALL COMMAND ==================
1206
+ program
1207
+ .command('install')
1208
+ .description('Install a build on a connected desktop agent')
1209
+ .requiredOption('--app <applicationId>', 'Application ID')
1210
+ .requiredOption('--path <installerPath>', 'Path to installer on the agent machine')
1211
+ .option('--args <arguments>', 'Installer arguments')
1212
+ .option('-a, --api-key <key>', 'API key (or use QATE_API_KEY env var)')
1213
+ .option('-u, --url <url>', 'Qate API URL (or use QATE_API_URL env var)')
1214
+ .option('--timeout <seconds>', 'Timeout in seconds (default: 300)', '300')
1215
+ .option('--json', 'Output as JSON')
1216
+ .action(async (options) => {
1217
+ try {
1218
+ const { apiKey, baseUrl } = getConfig(options);
1219
+ const client = new client_1.QateClient(baseUrl, apiKey);
1220
+ if (!options.json) {
1221
+ console.log(chalk_1.default.blue('Sending install request to desktop agent...'));
1222
+ console.log(chalk_1.default.gray(`Application: ${options.app}`));
1223
+ console.log(chalk_1.default.gray(`Installer: ${options.path}`));
1224
+ if (options.args) {
1225
+ console.log(chalk_1.default.gray(`Arguments: ${options.args}`));
1226
+ }
1227
+ }
1228
+ const installResult = await client.installApplication(options.app, options.path, options.args);
1229
+ if (!options.json) {
1230
+ console.log(chalk_1.default.gray(`Request ID: ${installResult.requestId}`));
1231
+ console.log(chalk_1.default.blue('\nWaiting for install to complete...\n'));
1232
+ }
1233
+ const timeoutMs = parseInt(options.timeout) * 1000;
1234
+ const status = await client.pollInstall(installResult.requestId, timeoutMs);
1235
+ if (options.json) {
1236
+ console.log(JSON.stringify(status, null, 2));
1237
+ }
1238
+ else {
1239
+ if (status.status === 'completed') {
1240
+ console.log(chalk_1.default.green('Install completed successfully'));
1241
+ if (status.result) {
1242
+ console.log(chalk_1.default.gray(typeof status.result === 'string' ? status.result : JSON.stringify(status.result)));
1243
+ }
1244
+ }
1245
+ else if (status.status === 'failed') {
1246
+ console.log(chalk_1.default.red('Install failed'));
1247
+ if (status.error) {
1248
+ console.log(chalk_1.default.red(`Error: ${status.error}`));
1249
+ }
1250
+ }
1251
+ else {
1252
+ console.log(chalk_1.default.yellow(`Install status: ${status.status}`));
1253
+ }
1254
+ }
1255
+ process.exit(status.status === 'completed' ? 0 : 1);
1256
+ }
1257
+ catch (error) {
1258
+ handleApiError(error);
1259
+ }
1260
+ });
1261
+ // Show help if no command provided
1262
+ if (process.argv.length <= 2) {
1263
+ // Trigger the help command
1264
+ program.parse(['node', 'qate', 'help']);
1265
+ }
1266
+ else {
1267
+ program.parse();
1268
+ }
1269
+ //# sourceMappingURL=cli.js.map