@devicecloud.dev/dcd 4.0.4 → 4.1.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.
@@ -19,6 +19,8 @@ export default class Cloud extends Command {
19
19
  'app-file': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
20
20
  'artifacts-path': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
21
21
  'junit-path': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
22
+ 'allure-path': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
23
+ 'html-path': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
22
24
  async: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
23
25
  config: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
24
26
  debug: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
@@ -54,4 +56,17 @@ export default class Cloud extends Command {
54
56
  };
55
57
  private versionCheck;
56
58
  run(): Promise<any>;
59
+ /**
60
+ * Handle downloading reports based on the report type specified
61
+ * @param reportType The type of report to download ('junit', 'allure', or 'html')
62
+ * @param apiUrl The base URL for the API server
63
+ * @param apiKey The API key for authentication
64
+ * @param uploadId The unique identifier for the test upload
65
+ * @param junitPath Optional file path where the JUnit report should be saved
66
+ * @param allurePath Optional file path where the Allure report should be saved
67
+ * @param htmlPath Optional file path where the HTML report should be saved
68
+ * @param debug Whether debug logging is enabled
69
+ * @returns Promise that resolves when all reports have been downloaded
70
+ */
71
+ private handleReportDownloads;
57
72
  }
@@ -9,12 +9,12 @@ const node_child_process_1 = require("node:child_process");
9
9
  const fs = require("node:fs");
10
10
  const os = require("node:os");
11
11
  const path = require("node:path");
12
- const streamZip = require("node-stream-zip");
13
12
  const constants_1 = require("../constants");
14
13
  const api_gateway_1 = require("../gateways/api-gateway");
15
14
  const methods_1 = require("../methods");
16
15
  const plan_1 = require("../plan");
17
16
  const compatibility_1 = require("../utils/compatibility");
17
+ const StreamZip = require("node-stream-zip");
18
18
  exports.mimeTypeLookupByExtension = {
19
19
  apk: 'application/vnd.android.package-archive',
20
20
  yaml: 'application/x-yaml',
@@ -71,7 +71,7 @@ class Cloud extends core_1.Command {
71
71
  let jsonFile = false;
72
72
  try {
73
73
  const { args, flags, raw } = await this.parse(Cloud);
74
- let { 'additional-app-binary-ids': nonFlatAdditionalAppBinaryIds, 'additional-app-files': nonFlatAdditionalAppFiles, 'android-api-level': androidApiLevel, 'android-device': androidDevice, apiKey: apiKeyFlag, apiUrl, 'app-binary-id': appBinaryId, 'app-file': appFile, 'artifacts-path': artifactsPath, 'junit-path': junitPath, async, config: configFile, debug, 'device-locale': deviceLocale, 'download-artifacts': downloadArtifacts, 'dry-run': dryRun, env, 'exclude-flows': excludeFlows, 'exclude-tags': excludeTags, flows, 'google-play': googlePlay, 'ignore-sha-check': ignoreShaCheck, 'include-tags': includeTags, 'ios-device': iOSDevice, 'ios-version': iOSVersion, json, 'json-file-name': jsonFileName, 'maestro-version': maestroVersion, metadata, mitmHost, mitmPath, 'moropo-v1-api-key': moropoApiKey, name, orientation, quiet, report, retry, 'runner-type': runnerType, 'x86-arch': x86Arch, ...rest } = flags;
74
+ let { 'additional-app-binary-ids': nonFlatAdditionalAppBinaryIds, 'additional-app-files': nonFlatAdditionalAppFiles, 'android-api-level': androidApiLevel, 'android-device': androidDevice, apiKey: apiKeyFlag, apiUrl, 'app-binary-id': appBinaryId, 'app-file': appFile, 'artifacts-path': artifactsPath, 'junit-path': junitPath, 'allure-path': allurePath, 'html-path': htmlPath, async, config: configFile, debug, 'device-locale': deviceLocale, 'download-artifacts': downloadArtifacts, 'dry-run': dryRun, env, 'exclude-flows': excludeFlows, 'exclude-tags': excludeTags, flows, 'google-play': googlePlay, 'ignore-sha-check': ignoreShaCheck, 'include-tags': includeTags, 'ios-device': iOSDevice, 'ios-version': iOSVersion, json, 'json-file-name': jsonFileName, 'maestro-version': maestroVersion, metadata, mitmHost, mitmPath, 'moropo-v1-api-key': moropoApiKey, name, orientation, quiet, report, retry, 'runner-type': runnerType, 'x86-arch': x86Arch, ...rest } = flags;
75
75
  // Resolve "latest" maestro version to actual version
76
76
  const resolvedMaestroVersion = (0, constants_1.resolveMaestroVersion)(maestroVersion);
77
77
  // Store debug flag for use in catch block
@@ -170,7 +170,6 @@ class Cloud extends core_1.Command {
170
170
  core_1.ux.action.status = 'Extracting tests...';
171
171
  }
172
172
  // Extract zip file
173
- const StreamZip = streamZip;
174
173
  // eslint-disable-next-line new-cap
175
174
  const zip = new StreamZip.async({ file: zipPath });
176
175
  await zip.extract(null, moropoDir);
@@ -222,7 +221,6 @@ class Cloud extends core_1.Command {
222
221
  }
223
222
  if (debug) {
224
223
  this.log(`DEBUG: API URL: ${apiUrl}`);
225
- this.log(`DEBUG: API Key provided: ${apiKey ? 'Yes' : 'No'}`);
226
224
  }
227
225
  if (retry && retry > 2) {
228
226
  this.log("Retries are now free of charge but limited to 2. If you're test is still failing after 2 retries, please ask for help on Discord.");
@@ -375,8 +373,9 @@ class Cloud extends core_1.Command {
375
373
  }
376
374
  await (0, methods_1.verifyAdditionalAppFiles)(additionalAppFiles);
377
375
  const flagLogs = [];
376
+ const sensitiveFlags = new Set(['api-key', 'apiKey', 'moropo-v1-api-key']);
378
377
  for (const [k, v] of Object.entries(flags)) {
379
- if (v && v.toString().length > 0) {
378
+ if (v && v.toString().length > 0 && !sensitiveFlags.has(k)) {
380
379
  flagLogs.push(`${k}: ${v}`);
381
380
  }
382
381
  }
@@ -691,33 +690,9 @@ class Cloud extends core_1.Command {
691
690
  this.warn('Failed to download artifacts');
692
691
  }
693
692
  }
694
- // Handle report download separately if --report junit is specified
695
- if (report === 'junit') {
696
- try {
697
- if (debug) {
698
- this.log('DEBUG: Downloading JUNIT report');
699
- }
700
- const reportPath = path.resolve(process.cwd(), junitPath || 'report.xml');
701
- await api_gateway_1.ApiGateway.downloadReport(apiUrl, apiKey, results[0].test_upload_id, reportPath);
702
- this.log('\n');
703
- this.log(`JUNIT test report has been downloaded to ${reportPath}`);
704
- }
705
- catch (error) {
706
- if (debug) {
707
- this.log(`DEBUG: Error downloading JUNIT report: ${error}`);
708
- }
709
- const errorMessage = error instanceof Error ? error.message : String(error);
710
- this.warn(`Failed to download JUNIT report: ${errorMessage}`);
711
- if (errorMessage.includes('404')) {
712
- this.warn('No JUNIT reports found for this upload. Make sure your tests were run with --report junit flag.');
713
- }
714
- else if (errorMessage.includes('EACCES') || errorMessage.includes('EPERM')) {
715
- this.warn('Permission denied. Check write permissions for the current directory.');
716
- }
717
- else if (errorMessage.includes('ENOENT')) {
718
- this.warn('Directory does not exist. Make sure you have write access to the current directory.');
719
- }
720
- }
693
+ // Handle report downloads based on --report flag
694
+ if (report && ['allure', 'html', 'junit'].includes(report)) {
695
+ await this.handleReportDownloads(report, apiUrl, apiKey, results[0].test_upload_id, junitPath, allurePath, htmlPath, debug);
721
696
  }
722
697
  const resultsWithoutEarlierTries = updatedResults.filter((result) => {
723
698
  const originalTryId = result.retry_of || result.id;
@@ -818,5 +793,65 @@ class Cloud extends core_1.Command {
818
793
  }
819
794
  }
820
795
  }
796
+ /**
797
+ * Handle downloading reports based on the report type specified
798
+ * @param reportType The type of report to download ('junit', 'allure', or 'html')
799
+ * @param apiUrl The base URL for the API server
800
+ * @param apiKey The API key for authentication
801
+ * @param uploadId The unique identifier for the test upload
802
+ * @param junitPath Optional file path where the JUnit report should be saved
803
+ * @param allurePath Optional file path where the Allure report should be saved
804
+ * @param htmlPath Optional file path where the HTML report should be saved
805
+ * @param debug Whether debug logging is enabled
806
+ * @returns Promise that resolves when all reports have been downloaded
807
+ */
808
+ // eslint-disable-next-line max-params
809
+ async handleReportDownloads(reportType, apiUrl, apiKey, uploadId, junitPath, allurePath, htmlPath, debug) {
810
+ const downloadReport = async (type, filePath) => {
811
+ try {
812
+ if (debug) {
813
+ this.log(`DEBUG: Downloading ${type.toUpperCase()} report`);
814
+ }
815
+ await api_gateway_1.ApiGateway.downloadReportGeneric(apiUrl, apiKey, uploadId, type, filePath);
816
+ this.log(`${type.toUpperCase()} test report has been downloaded to ${filePath}`);
817
+ }
818
+ catch (error) {
819
+ if (debug) {
820
+ this.log(`DEBUG: Error downloading ${type.toUpperCase()} report: ${error}`);
821
+ }
822
+ const errorMessage = error instanceof Error ? error.message : String(error);
823
+ this.warn(`Failed to download ${type.toUpperCase()} report: ${errorMessage}`);
824
+ if (errorMessage.includes('404')) {
825
+ this.warn(`No ${type.toUpperCase()} reports found for this upload. Make sure your tests generated results.`);
826
+ }
827
+ else if (errorMessage.includes('EACCES') || errorMessage.includes('EPERM')) {
828
+ this.warn('Permission denied. Check write permissions for the current directory.');
829
+ }
830
+ else if (errorMessage.includes('ENOENT')) {
831
+ this.warn('Directory does not exist. Make sure you have write access to the current directory.');
832
+ }
833
+ }
834
+ };
835
+ switch (reportType) {
836
+ case 'junit': {
837
+ const reportPath = path.resolve(process.cwd(), junitPath || 'report.xml');
838
+ await downloadReport('junit', reportPath);
839
+ break;
840
+ }
841
+ case 'allure': {
842
+ const reportPath = path.resolve(process.cwd(), allurePath || 'report.html');
843
+ await downloadReport('allure', reportPath);
844
+ break;
845
+ }
846
+ case 'html': {
847
+ const htmlReportPath = path.resolve(process.cwd(), htmlPath || 'report.html');
848
+ await downloadReport('html', htmlReportPath);
849
+ break;
850
+ }
851
+ default: {
852
+ this.warn(`Unknown report type: ${reportType}`);
853
+ }
854
+ }
855
+ }
821
856
  }
822
857
  exports.default = Cloud;
@@ -13,6 +13,8 @@ export declare const flags: {
13
13
  'app-file': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
14
14
  'artifacts-path': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
15
15
  'junit-path': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
16
+ 'allure-path': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
17
+ 'html-path': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
16
18
  async: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
17
19
  config: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
18
20
  debug: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
package/dist/constants.js CHANGED
@@ -75,6 +75,14 @@ exports.flags = {
75
75
  dependsOn: ['report'],
76
76
  description: 'Custom file path for downloaded JUnit report (default: ./report.xml)',
77
77
  }),
78
+ 'allure-path': core_1.Flags.string({
79
+ dependsOn: ['report'],
80
+ description: 'Custom file path for downloaded Allure report (default: ./report.html)',
81
+ }),
82
+ 'html-path': core_1.Flags.string({
83
+ dependsOn: ['report'],
84
+ description: 'Custom file path for downloaded HTML report (default: ./report.html)',
85
+ }),
78
86
  async: core_1.Flags.boolean({
79
87
  description: 'Immediately return (exit code 0) from the command without waiting for the results of the run (useful for saving CI minutes)',
80
88
  }),
@@ -192,8 +200,8 @@ exports.flags = {
192
200
  }),
193
201
  report: core_1.Flags.string({
194
202
  aliases: ['format'],
195
- description: 'Runs Maestro with the --format flag, this will generate a report in the specified format',
196
- options: ['junit', 'html'],
203
+ description: 'Generate and download test reports in the specified format. Use "allure" for a complete HTML report.',
204
+ options: ['allure', 'junit', 'html'],
197
205
  }),
198
206
  retry: core_1.Flags.integer({
199
207
  description: 'Automatically retry the run up to the number of times specified (same as pressing retry in the UI) - this is free of charge',
@@ -1,10 +1,16 @@
1
1
  import { TAppMetadata } from '../types';
2
2
  export declare const ApiGateway: {
3
+ /**
4
+ * Standardized error handling for API responses
5
+ * @param res - The fetch response object
6
+ * @param operation - Description of the operation that failed
7
+ * @returns Never returns, always throws
8
+ */
9
+ handleApiError(res: Response, operation: string): Promise<never>;
3
10
  checkForExistingUpload(baseUrl: string, apiKey: string, sha: string): Promise<{
4
11
  appBinaryId: string;
5
12
  exists: boolean;
6
13
  }>;
7
- downloadReport(baseUrl: string, apiKey: string, uploadId: string, reportPath?: string): Promise<void>;
8
14
  downloadArtifactsZip(baseUrl: string, apiKey: string, uploadId: string, results: "ALL" | "FAILED", artifactsPath?: string): Promise<void>;
9
15
  finaliseUpload(baseUrl: string, apiKey: string, id: string, metadata: TAppMetadata, path: string, sha: string): Promise<Record<string, never>>;
10
16
  getBinaryUploadUrl(baseUrl: string, apiKey: string, platform: "android" | "ios"): Promise<{
@@ -33,4 +39,14 @@ export declare const ApiGateway: {
33
39
  message?: string;
34
40
  results?: import("../types/schema.types").components["schemas"]["IDBResult"][];
35
41
  }>;
42
+ /**
43
+ * Generic report download method that handles both junit and allure reports
44
+ * @param baseUrl - API base URL
45
+ * @param apiKey - API key for authentication
46
+ * @param uploadId - Upload ID to download report for
47
+ * @param reportType - Type of report to download ('junit' or 'allure')
48
+ * @param reportPath - Optional custom path for the downloaded report
49
+ * @returns Promise that resolves when download is complete
50
+ */
51
+ downloadReportGeneric(baseUrl: string, apiKey: string, uploadId: string, reportType: "allure" | "html" | "junit", reportPath?: string): Promise<void>;
36
52
  };
@@ -1,9 +1,50 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ApiGateway = void 0;
4
- const fs = require("node:fs/promises");
5
4
  const path = require("node:path");
6
5
  exports.ApiGateway = {
6
+ /**
7
+ * Standardized error handling for API responses
8
+ * @param res - The fetch response object
9
+ * @param operation - Description of the operation that failed
10
+ * @returns Never returns, always throws
11
+ */
12
+ async handleApiError(res, operation) {
13
+ const errorText = await res.text();
14
+ let userMessage;
15
+ // Parse common API error formats
16
+ try {
17
+ const errorData = JSON.parse(errorText);
18
+ userMessage = errorData.message || errorData.error || errorText;
19
+ }
20
+ catch {
21
+ userMessage = errorText;
22
+ }
23
+ // Add context and improve readability
24
+ switch (res.status) {
25
+ case 400: {
26
+ throw new Error(`Invalid request: ${userMessage}`);
27
+ }
28
+ case 401: {
29
+ throw new Error(`Authentication failed. Please check your API key.`);
30
+ }
31
+ case 403: {
32
+ throw new Error(`Access denied. ${userMessage}`);
33
+ }
34
+ case 404: {
35
+ throw new Error(`Resource not found. ${userMessage}`);
36
+ }
37
+ case 429: {
38
+ throw new Error(`Rate limit exceeded. Please try again later.`);
39
+ }
40
+ case 500: {
41
+ throw new Error(`Server error occurred. Please try again or contact support.`);
42
+ }
43
+ default: {
44
+ throw new Error(`${operation} failed: ${userMessage} (HTTP ${res.status})`);
45
+ }
46
+ }
47
+ },
7
48
  async checkForExistingUpload(baseUrl, apiKey, sha) {
8
49
  const res = await fetch(`${baseUrl}/uploads/checkForExistingUpload`, {
9
50
  body: JSON.stringify({ sha }),
@@ -15,25 +56,6 @@ exports.ApiGateway = {
15
56
  });
16
57
  return res.json();
17
58
  },
18
- async downloadReport(baseUrl, apiKey, uploadId, reportPath) {
19
- const finalReportPath = reportPath || path.resolve(process.cwd(), `report-${uploadId}.xml`);
20
- const url = `${baseUrl}/results/${uploadId}/report`;
21
- const res = await fetch(url, {
22
- headers: {
23
- 'x-app-api-key': apiKey,
24
- },
25
- method: 'GET',
26
- });
27
- if (!res.ok) {
28
- const errorText = await res.text();
29
- if (res.status === 404) {
30
- throw new Error(`Upload ID '${uploadId}' not found or no results available for this upload`);
31
- }
32
- throw new Error(`Failed to download report: ${res.status} ${errorText}`);
33
- }
34
- const buffer = await res.arrayBuffer();
35
- await fs.writeFile(finalReportPath, Buffer.from(buffer));
36
- },
37
59
  async downloadArtifactsZip(baseUrl, apiKey, uploadId, results, artifactsPath = './artifacts.zip') {
38
60
  const res = await fetch(`${baseUrl}/results/${uploadId}/download`, {
39
61
  body: JSON.stringify({ results }),
@@ -44,7 +66,7 @@ exports.ApiGateway = {
44
66
  method: 'POST',
45
67
  });
46
68
  if (!res.ok) {
47
- throw new Error(await res.text());
69
+ await this.handleApiError(res, 'Failed to download artifacts');
48
70
  }
49
71
  // Handle tilde expansion for home directory
50
72
  if (artifactsPath.startsWith('~/') || artifactsPath === '~') {
@@ -87,7 +109,7 @@ exports.ApiGateway = {
87
109
  method: 'POST',
88
110
  });
89
111
  if (!res.ok) {
90
- throw new Error(await res.text());
112
+ await this.handleApiError(res, 'Failed to finalize upload');
91
113
  }
92
114
  return res.json();
93
115
  },
@@ -101,7 +123,7 @@ exports.ApiGateway = {
101
123
  method: 'POST',
102
124
  });
103
125
  if (!res.ok) {
104
- throw new Error(await res.text());
126
+ await this.handleApiError(res, 'Failed to get upload URL');
105
127
  }
106
128
  return res.json();
107
129
  },
@@ -111,7 +133,7 @@ exports.ApiGateway = {
111
133
  headers: { 'x-app-api-key': apiKey },
112
134
  });
113
135
  if (!res.ok) {
114
- throw new Error(await res.text());
136
+ await this.handleApiError(res, 'Failed to get results');
115
137
  }
116
138
  return res.json();
117
139
  },
@@ -129,7 +151,7 @@ exports.ApiGateway = {
129
151
  },
130
152
  });
131
153
  if (!response.ok) {
132
- throw new Error(`Failed to fetch status: ${response.statusText}`);
154
+ await this.handleApiError(response, 'Failed to get upload status');
133
155
  }
134
156
  return response.json();
135
157
  },
@@ -142,8 +164,88 @@ exports.ApiGateway = {
142
164
  method: 'POST',
143
165
  });
144
166
  if (!res.ok) {
145
- throw new Error(await res.text());
167
+ await this.handleApiError(res, 'Failed to upload test flows');
146
168
  }
147
169
  return res.json();
148
170
  },
171
+ /**
172
+ * Generic report download method that handles both junit and allure reports
173
+ * @param baseUrl - API base URL
174
+ * @param apiKey - API key for authentication
175
+ * @param uploadId - Upload ID to download report for
176
+ * @param reportType - Type of report to download ('junit' or 'allure')
177
+ * @param reportPath - Optional custom path for the downloaded report
178
+ * @returns Promise that resolves when download is complete
179
+ */
180
+ async downloadReportGeneric(baseUrl, apiKey, uploadId, reportType, reportPath) {
181
+ // Define endpoint and default filename mappings
182
+ const config = {
183
+ junit: {
184
+ endpoint: `/results/${uploadId}/report`,
185
+ defaultFilename: `report-${uploadId}.xml`,
186
+ notFoundMessage: `Upload ID '${uploadId}' not found or no results available for this upload`,
187
+ errorPrefix: 'Failed to download report',
188
+ },
189
+ allure: {
190
+ endpoint: `/allure/${uploadId}/download`,
191
+ defaultFilename: `report-${uploadId}.html`,
192
+ notFoundMessage: `Upload ID '${uploadId}' not found or no Allure report available for this upload`,
193
+ errorPrefix: 'Failed to download Allure report',
194
+ },
195
+ html: {
196
+ endpoint: `/results/${uploadId}/html-report`,
197
+ defaultFilename: `report-${uploadId}.html`,
198
+ notFoundMessage: `Upload ID '${uploadId}' not found or no HTML report available for this upload`,
199
+ errorPrefix: 'Failed to download HTML report',
200
+ },
201
+ };
202
+ const { endpoint, defaultFilename, notFoundMessage, errorPrefix } = config[reportType];
203
+ const finalReportPath = reportPath || path.resolve(process.cwd(), defaultFilename);
204
+ const url = `${baseUrl}${endpoint}`;
205
+ // Make the download request
206
+ const res = await fetch(url, {
207
+ headers: {
208
+ 'x-app-api-key': apiKey,
209
+ },
210
+ method: 'GET',
211
+ });
212
+ if (!res.ok) {
213
+ const errorText = await res.text();
214
+ if (res.status === 404) {
215
+ throw new Error(notFoundMessage);
216
+ }
217
+ throw new Error(`${errorPrefix}: ${res.status} ${errorText}`);
218
+ }
219
+ // Handle tilde expansion for home directory (applies to all report types)
220
+ let expandedPath = finalReportPath;
221
+ if (finalReportPath.startsWith('~/') || finalReportPath === '~') {
222
+ expandedPath = finalReportPath.replace(/^~/,
223
+ // eslint-disable-next-line unicorn/prefer-module
224
+ require('node:os').homedir());
225
+ }
226
+ // Create directory structure if it doesn't exist
227
+ // eslint-disable-next-line unicorn/prefer-module
228
+ const { dirname } = require('node:path');
229
+ // eslint-disable-next-line unicorn/prefer-module
230
+ const { createWriteStream, mkdirSync } = require('node:fs');
231
+ // eslint-disable-next-line unicorn/prefer-module
232
+ const { finished } = require('node:stream/promises');
233
+ // eslint-disable-next-line unicorn/prefer-module
234
+ const { Readable } = require('node:stream');
235
+ const directory = dirname(expandedPath);
236
+ if (directory !== '.') {
237
+ try {
238
+ mkdirSync(directory, { recursive: true });
239
+ }
240
+ catch (error) {
241
+ // Ignore EEXIST errors (directory already exists)
242
+ if (error.code !== 'EEXIST') {
243
+ throw error;
244
+ }
245
+ }
246
+ }
247
+ // Write the file using streaming for better memory efficiency
248
+ const fileStream = createWriteStream(expandedPath, { flags: 'wx' });
249
+ await finished(Readable.fromWeb(res.body).pipe(fileStream));
250
+ },
149
251
  };
@@ -23,7 +23,8 @@ export declare enum EAndroidDevices {
23
23
  export declare enum EiOSVersions {
24
24
  'eighteen' = "18",
25
25
  'seventeen' = "17",
26
- 'sixteen' = "16"
26
+ 'sixteen' = "16",
27
+ 'twentySix' = "26"
27
28
  }
28
29
  export declare enum EAndroidApiLevels {
29
30
  'thirty' = "30",
@@ -30,6 +30,7 @@ var EiOSVersions;
30
30
  EiOSVersions["eighteen"] = "18";
31
31
  EiOSVersions["seventeen"] = "17";
32
32
  EiOSVersions["sixteen"] = "16";
33
+ EiOSVersions["twentySix"] = "26";
33
34
  })(EiOSVersions || (exports.EiOSVersions = EiOSVersions = {}));
34
35
  var EAndroidApiLevels;
35
36
  (function (EAndroidApiLevels) {
@@ -3,9 +3,6 @@
3
3
  * Do not make direct changes to the file.
4
4
  */
5
5
  export interface paths {
6
- "/admin/cleanupOldUploads": {
7
- post: operations["AdminController_cleanupOldUploads"];
8
- };
9
6
  "/uploads/binary": {
10
7
  post: operations["UploadsController_createBinary"];
11
8
  };
@@ -39,15 +36,36 @@ export interface paths {
39
36
  "/results/{uploadId}/download": {
40
37
  post: operations["ResultsController_getTestRunArtifacts"];
41
38
  };
42
- "/results": {
43
- post: operations["ResultsController_createResult"];
44
- };
45
39
  "/results/notify/{uploadId}": {
46
40
  post: operations["ResultsController_notifyTestRunComplete"];
47
41
  };
42
+ "/results/{uploadId}/report": {
43
+ get: operations["ResultsController_downloadReport"];
44
+ };
45
+ "/results/{uploadId}/html-report": {
46
+ get: operations["ResultsController_downloadHtmlReport"];
47
+ };
48
48
  "/results/compatibility/data": {
49
49
  get: operations["ResultsController_getCompatibilityData"];
50
50
  };
51
+ "/allure/{uploadId}/download": {
52
+ /**
53
+ * Download Allure report as HTML
54
+ * @description Downloads a single-file Allure report as HTML containing all test results. Report is generated once and stored in Supabase Storage for subsequent downloads.
55
+ */
56
+ get: operations["AllureController_downloadAllureReport"];
57
+ };
58
+ "/webhooks": {
59
+ get: operations["WebhooksController_getWebhook"];
60
+ post: operations["WebhooksController_setWebhook"];
61
+ delete: operations["WebhooksController_deleteWebhook"];
62
+ };
63
+ "/webhooks/regenerate-secret": {
64
+ post: operations["WebhooksController_regenerateWebhookSecret"];
65
+ };
66
+ "/webhooks/test": {
67
+ post: operations["WebhooksController_testWebhook"];
68
+ };
51
69
  "/org/increase_credits": {
52
70
  post: operations["OrgController_paddleTransactionCompleted"];
53
71
  };
@@ -60,12 +78,6 @@ export interface paths {
60
78
  "/org/accept-invite": {
61
79
  post: operations["OrgController_acceptInvite"];
62
80
  };
63
- "/public/stats": {
64
- get: operations["StatsController_getPublicStats"];
65
- };
66
- "/moropo/run-scheduled-jobs": {
67
- get: operations["MoropoController_runScheduledJobs"];
68
- };
69
81
  "/frontend/check-domain-saml": {
70
82
  post: operations["FrontendController_checkDomainSaml"];
71
83
  };
@@ -137,7 +149,7 @@ export interface components {
137
149
  apiUrl?: string;
138
150
  appFile?: string;
139
151
  /** @enum {string} */
140
- iOSVersion?: "16" | "17" | "18";
152
+ iOSVersion?: "16" | "17" | "18" | "26";
141
153
  /** @enum {string} */
142
154
  iOSDevice?: "iphone-14" | "iphone-14-pro" | "iphone-15" | "iphone-15-pro" | "iphone-16" | "iphone-16-plus" | "iphone-16-pro" | "iphone-16-pro-max" | "ipad-pro-6th-gen";
143
155
  platform?: string;
@@ -182,32 +194,6 @@ export interface components {
182
194
  export type $defs = Record<string, never>;
183
195
  export type external = Record<string, never>;
184
196
  export interface operations {
185
- AdminController_cleanupOldUploads: {
186
- parameters: {
187
- query?: {
188
- /** @description If true, shows what would be deleted without actually deleting */
189
- dryRun?: boolean;
190
- };
191
- };
192
- responses: {
193
- /** @description Cleanup of old uploads has been triggered. */
194
- 201: {
195
- content: {
196
- "application/json": {
197
- message?: string;
198
- deletedCount?: number;
199
- uploads?: ({
200
- id?: string;
201
- uploadType?: string;
202
- createdAt?: string;
203
- lastUsedDate?: string | null;
204
- })[];
205
- dryRun?: boolean;
206
- };
207
- };
208
- };
209
- };
210
- };
211
197
  UploadsController_createBinary: {
212
198
  parameters: {
213
199
  header: {
@@ -445,19 +431,25 @@ export interface operations {
445
431
  };
446
432
  };
447
433
  };
448
- ResultsController_createResult: {
434
+ ResultsController_notifyTestRunComplete: {
449
435
  parameters: {
450
436
  header: {
451
437
  "x-app-api-key": string;
452
438
  };
439
+ path: {
440
+ uploadId: string;
441
+ };
453
442
  };
454
443
  responses: {
444
+ /** @description Send results summary email. */
455
445
  201: {
456
- content: never;
446
+ content: {
447
+ "application/json": string;
448
+ };
457
449
  };
458
450
  };
459
451
  };
460
- ResultsController_notifyTestRunComplete: {
452
+ ResultsController_downloadReport: {
461
453
  parameters: {
462
454
  header: {
463
455
  "x-app-api-key": string;
@@ -467,8 +459,26 @@ export interface operations {
467
459
  };
468
460
  };
469
461
  responses: {
470
- /** @description Send results summary email. */
471
- 201: {
462
+ /** @description Download combined JUNIT test report (report.xml) for the upload */
463
+ 200: {
464
+ content: {
465
+ "application/json": string;
466
+ };
467
+ };
468
+ };
469
+ };
470
+ ResultsController_downloadHtmlReport: {
471
+ parameters: {
472
+ header: {
473
+ "x-app-api-key": string;
474
+ };
475
+ path: {
476
+ uploadId: string;
477
+ };
478
+ };
479
+ responses: {
480
+ /** @description Download combined HTML test report (report.html) for the upload */
481
+ 200: {
472
482
  content: {
473
483
  "application/json": string;
474
484
  };
@@ -497,6 +507,141 @@ export interface operations {
497
507
  };
498
508
  };
499
509
  };
510
+ /**
511
+ * Download Allure report as HTML
512
+ * @description Downloads a single-file Allure report as HTML containing all test results. Report is generated once and stored in Supabase Storage for subsequent downloads.
513
+ */
514
+ AllureController_downloadAllureReport: {
515
+ parameters: {
516
+ header: {
517
+ "x-app-api-key": string;
518
+ };
519
+ path: {
520
+ /** @description The upload ID to generate Allure report for */
521
+ uploadId: string;
522
+ };
523
+ };
524
+ responses: {
525
+ /** @description Allure report HTML file download */
526
+ 200: {
527
+ content: {
528
+ "text/html": string;
529
+ };
530
+ };
531
+ /** @description Upload not found or no results available */
532
+ 404: {
533
+ content: never;
534
+ };
535
+ };
536
+ };
537
+ WebhooksController_getWebhook: {
538
+ parameters: {
539
+ query?: {
540
+ /** @description Set to true to return full secret instead of masked version */
541
+ show_secret?: boolean;
542
+ };
543
+ header: {
544
+ "x-app-api-key": string;
545
+ };
546
+ };
547
+ responses: {
548
+ /** @description Current webhook configuration */
549
+ 200: {
550
+ content: {
551
+ "application/json": {
552
+ webhook_url?: string;
553
+ /** @description Full secret (only when show_secret=true) */
554
+ secret_key?: string;
555
+ /** @description Masked secret (default) */
556
+ secret_key_masked?: string;
557
+ /** Format: date-time */
558
+ created_at?: string;
559
+ /** Format: date-time */
560
+ updated_at?: string;
561
+ };
562
+ };
563
+ };
564
+ };
565
+ };
566
+ WebhooksController_setWebhook: {
567
+ parameters: {
568
+ header: {
569
+ "x-app-api-key": string;
570
+ };
571
+ };
572
+ requestBody: {
573
+ content: {
574
+ "application/json": {
575
+ /**
576
+ * Format: uri
577
+ * @example https://api.example.com/webhook
578
+ */
579
+ url: string;
580
+ };
581
+ };
582
+ };
583
+ responses: {
584
+ /** @description Webhook URL set successfully */
585
+ 201: {
586
+ content: {
587
+ "application/json": Record<string, never>;
588
+ };
589
+ };
590
+ };
591
+ };
592
+ WebhooksController_deleteWebhook: {
593
+ parameters: {
594
+ header: {
595
+ "x-app-api-key": string;
596
+ };
597
+ };
598
+ responses: {
599
+ /** @description Webhook configuration deleted successfully */
600
+ 200: {
601
+ content: {
602
+ "application/json": Record<string, never>;
603
+ };
604
+ };
605
+ };
606
+ };
607
+ WebhooksController_regenerateWebhookSecret: {
608
+ parameters: {
609
+ header: {
610
+ "x-app-api-key": string;
611
+ };
612
+ };
613
+ responses: {
614
+ /** @description Webhook secret regenerated successfully */
615
+ 201: {
616
+ content: {
617
+ "application/json": Record<string, never>;
618
+ };
619
+ };
620
+ };
621
+ };
622
+ WebhooksController_testWebhook: {
623
+ parameters: {
624
+ header: {
625
+ "x-app-api-key": string;
626
+ };
627
+ };
628
+ requestBody: {
629
+ content: {
630
+ "application/json": {
631
+ /** Format: uri */
632
+ url?: string;
633
+ };
634
+ };
635
+ };
636
+ responses: {
637
+ /** @description Test webhook sent successfully */
638
+ 201: {
639
+ content: {
640
+ "application/json": Record<string, never>;
641
+ };
642
+ };
643
+ };
644
+ };
500
645
  OrgController_paddleTransactionCompleted: {
501
646
  parameters: {
502
647
  header: {
@@ -583,41 +728,6 @@ export interface operations {
583
728
  };
584
729
  };
585
730
  };
586
- StatsController_getPublicStats: {
587
- responses: {
588
- 200: {
589
- content: never;
590
- };
591
- };
592
- };
593
- StatsController_sendDailyReport: {
594
- responses: {
595
- 201: {
596
- content: never;
597
- };
598
- };
599
- };
600
- StatsController_reportSlowTestsDev: {
601
- responses: {
602
- 201: {
603
- content: never;
604
- };
605
- };
606
- };
607
- StatsController_reportSlowTestsProd: {
608
- responses: {
609
- 201: {
610
- content: never;
611
- };
612
- };
613
- };
614
- MoropoController_runScheduledJobs: {
615
- responses: {
616
- 200: {
617
- content: never;
618
- };
619
- };
620
- };
621
731
  FrontendController_checkDomainSaml: {
622
732
  /** @description Domain to check for SAML configuration */
623
733
  requestBody: {
@@ -134,6 +134,26 @@
134
134
  "multiple": false,
135
135
  "type": "option"
136
136
  },
137
+ "allure-path": {
138
+ "dependsOn": [
139
+ "report"
140
+ ],
141
+ "description": "Custom file path for downloaded Allure report (default: ./report.html)",
142
+ "name": "allure-path",
143
+ "hasDynamicHelp": false,
144
+ "multiple": false,
145
+ "type": "option"
146
+ },
147
+ "html-path": {
148
+ "dependsOn": [
149
+ "report"
150
+ ],
151
+ "description": "Custom file path for downloaded HTML report (default: ./report.html)",
152
+ "name": "html-path",
153
+ "hasDynamicHelp": false,
154
+ "multiple": false,
155
+ "type": "option"
156
+ },
137
157
  "async": {
138
158
  "description": "Immediately return (exit code 0) from the command without waiting for the results of the run (useful for saving CI minutes)",
139
159
  "name": "async",
@@ -263,7 +283,8 @@
263
283
  "options": [
264
284
  "18",
265
285
  "17",
266
- "16"
286
+ "16",
287
+ "26"
267
288
  ],
268
289
  "type": "option"
269
290
  },
@@ -372,11 +393,12 @@
372
393
  "aliases": [
373
394
  "format"
374
395
  ],
375
- "description": "Runs Maestro with the --format flag, this will generate a report in the specified format",
396
+ "description": "Generate and download test reports in the specified format. Use \"allure\" for a complete HTML report.",
376
397
  "name": "report",
377
398
  "hasDynamicHelp": false,
378
399
  "multiple": false,
379
400
  "options": [
401
+ "allure",
380
402
  "junit",
381
403
  "html"
382
404
  ],
@@ -579,5 +601,5 @@
579
601
  ]
580
602
  }
581
603
  },
582
- "version": "4.0.4"
604
+ "version": "4.1.0"
583
605
  }
package/package.json CHANGED
@@ -72,7 +72,7 @@
72
72
  "type": "git",
73
73
  "url": "https://devicecloud.dev"
74
74
  },
75
- "version": "4.0.4",
75
+ "version": "4.1.0",
76
76
  "bugs": {
77
77
  "url": "https://discord.gg/gm3mJwcNw8"
78
78
  },
@@ -89,7 +89,7 @@
89
89
  "dcd": "./bin/dev.js",
90
90
  "prod": "./bin/run.js",
91
91
  "build": "shx rm -rf dist && tsc -b",
92
- "lint": "NODE_OPTIONS='--no-deprecation' eslint . --ext .ts",
92
+ "lint": "eslint . --ext .ts",
93
93
  "version": "oclif readme && git add README.md",
94
94
  "test": "node scripts/test-runner.mjs"
95
95
  }