@devicecloud.dev/dcd 3.7.12-beta.1 → 4.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.
@@ -2,68 +2,62 @@ import { Command } from '@oclif/core';
2
2
  export declare const mimeTypeLookupByExtension: Record<string, string>;
3
3
  export default class Cloud extends Command {
4
4
  static args: {
5
- firstFile: import("@oclif/core/lib/interfaces").Arg<string | undefined, Record<string, unknown>>;
6
- secondFile: import("@oclif/core/lib/interfaces").Arg<string | undefined, Record<string, unknown>>;
5
+ firstFile: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
6
+ secondFile: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
7
7
  };
8
8
  static description: string;
9
+ static enableJsonFlag: boolean;
9
10
  static examples: string[];
10
11
  static flags: {
11
12
  'additional-app-binary-ids': import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
12
13
  'additional-app-files': import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
13
- 'android-api-level': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
14
- 'android-device': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
15
- 'skip-chrome-onboarding': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
16
- 'show-crosshairs': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
17
- apiKey: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
14
+ 'android-api-level': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
15
+ 'android-device': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
16
+ apiKey: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
18
17
  apiUrl: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
19
- 'app-binary-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
20
- 'app-file': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
18
+ 'app-binary-id': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
19
+ 'app-file': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
20
+ 'artifacts-path': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
21
21
  async: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
22
- config: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
23
- 'device-locale': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
24
- 'download-artifacts': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
25
- 'artifacts-path': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
22
+ config: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
26
23
  debug: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
27
- env: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
24
+ 'device-locale': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
25
+ 'download-artifacts': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
26
+ 'dry-run': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
27
+ env: import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
28
28
  'exclude-flows': import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
29
29
  'exclude-tags': import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
30
- flows: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
30
+ flows: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
31
31
  'google-play': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
32
- 'include-tags': import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
33
32
  'ignore-sha-check': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
34
- 'ios-device': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
35
- 'ios-version': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
36
- 'x86-arch': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
33
+ 'include-tags': import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
34
+ 'ios-device': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
35
+ 'ios-version': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
36
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
37
+ 'json-file': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
38
+ 'json-file-name': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
37
39
  'maestro-version': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
38
- mitmHost: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
39
- mitmPath: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
40
- name: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
41
- orientation: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
40
+ metadata: import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
41
+ mitmHost: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
42
+ mitmPath: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
43
+ 'moropo-v1-api-key': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
44
+ name: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
45
+ orientation: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
42
46
  quiet: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
43
- retry: import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
47
+ report: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
48
+ retry: import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces").CustomOptions>;
44
49
  'runner-type': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
45
- report: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
46
- json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
47
- 'json-file': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
48
- 'moropo-v1-api-key': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
49
- 'dry-run': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
50
+ 'show-crosshairs': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
51
+ 'skip-chrome-onboarding': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
52
+ 'x86-arch': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
50
53
  };
51
- static enableJsonFlag: boolean;
52
54
  private versionCheck;
55
+ run(): Promise<any>;
53
56
  /**
54
- * Generate the JSON output file path based on name or upload ID
55
- * @param name - Optional custom name for the file
56
- * @param uploadId - Upload ID to use if name is not provided
57
+ * Generate the JSON output file path based on upload ID or custom filename
58
+ * @param uploadId - Upload ID to use if custom filename is not provided
59
+ * @param jsonFileName - Optional custom filename (can include relative path)
57
60
  * @returns Path to the JSON output file
58
61
  */
59
62
  private getJsonOutputPath;
60
- run(): Promise<{
61
- uploadId: string;
62
- consoleUrl: string;
63
- status: string;
64
- tests: {
65
- name: string;
66
- status: string;
67
- }[];
68
- } | undefined>;
69
63
  }
@@ -5,15 +5,15 @@ exports.mimeTypeLookupByExtension = void 0;
5
5
  const core_1 = require("@oclif/core");
6
6
  const cli_ux_1 = require("@oclif/core/lib/cli-ux");
7
7
  const errors_1 = require("@oclif/core/lib/errors");
8
- const path = require("node:path");
9
8
  const fs = require("node:fs");
10
- const os = require("os");
11
- const StreamZip = require("node-stream-zip");
9
+ const os = require("node:os");
10
+ const path = require("node:path");
11
+ const streamZip = require("node-stream-zip");
12
12
  const constants_1 = require("../constants");
13
- const compatibility_1 = require("../utils/compatibility");
13
+ const api_gateway_1 = require("../gateways/api-gateway");
14
14
  const methods_1 = require("../methods");
15
15
  const plan_1 = require("../plan");
16
- const ApiGateway_1 = require("../gateways/ApiGateway");
16
+ const compatibility_1 = require("../utils/compatibility");
17
17
  exports.mimeTypeLookupByExtension = {
18
18
  apk: 'application/vnd.android.package-archive',
19
19
  yaml: 'application/x-yaml',
@@ -24,13 +24,9 @@ process.removeAllListeners('warning');
24
24
  process.on('warning', (warning) => {
25
25
  if (warning.name === 'DeprecationWarning' &&
26
26
  warning.message.includes('punycode')) {
27
- return;
27
+ // Ignore punycode deprecation warnings
28
28
  }
29
29
  });
30
- const escapeShellValue = (value) => {
31
- // Escape special characters that could cause shell interpretation issues
32
- return value.replace(/(["\\'$`!\s\[\]{}()&|;<>*?#^~])/g, '\\$1');
33
- };
34
30
  class Cloud extends core_1.Command {
35
31
  static args = {
36
32
  firstFile: core_1.Args.string({
@@ -45,9 +41,9 @@ class Cloud extends core_1.Command {
45
41
  }),
46
42
  };
47
43
  static description = `Test a Flow or set of Flows on devicecloud.dev (https://devicecloud.dev)\nProvide your application file and a folder with Maestro flows to run them in parallel on multiple devices in devicecloud.dev\nThe command will block until all analyses have completed`;
44
+ static enableJsonFlag = true;
48
45
  static examples = ['<%= config.bin %> <%= command.id %>'];
49
46
  static flags = constants_1.flags;
50
- static enableJsonFlag = true;
51
47
  versionCheck = async () => {
52
48
  const versionResponse = await fetch('https://registry.npmjs.org/@devicecloud.dev/dcd/latest');
53
49
  const versionResponseJson = await versionResponse.json();
@@ -61,15 +57,6 @@ class Cloud extends core_1.Command {
61
57
  `);
62
58
  }
63
59
  };
64
- /**
65
- * Generate the JSON output file path based on name or upload ID
66
- * @param name - Optional custom name for the file
67
- * @param uploadId - Upload ID to use if name is not provided
68
- * @returns Path to the JSON output file
69
- */
70
- getJsonOutputPath(name, uploadId) {
71
- return name ? `${name}_dcd.json` : `${uploadId}_dcd.json`;
72
- }
73
60
  async run() {
74
61
  let output = null;
75
62
  // Store debug flag outside try/catch to access it in catch block
@@ -77,7 +64,9 @@ class Cloud extends core_1.Command {
77
64
  let jsonFile = false;
78
65
  try {
79
66
  const { args, flags, raw } = await this.parse(Cloud);
80
- 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, async, config: configFile, debug, 'device-locale': deviceLocale, 'download-artifacts': downloadArtifacts, env, 'exclude-flows': excludeFlows, 'exclude-tags': excludeTags, flows, 'google-play': googlePlay, 'include-tags': includeTags, 'ignore-sha-check': ignoreShaCheck, 'ios-device': iOSDevice, 'ios-version': iOSVersion, 'maestro-version': maestroVersion, name, orientation, quiet, retry, report, 'runner-type': runnerType, 'show-crosshairs': showCrosshairs, 'x86-arch': x86Arch, json, mitmHost, mitmPath, 'moropo-v1-api-key': moropoApiKey, 'dry-run': dryRun, ...rest } = flags;
67
+ 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, 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;
68
+ // Resolve "latest" maestro version to actual version
69
+ const resolvedMaestroVersion = (0, constants_1.resolveMaestroVersion)(maestroVersion);
81
70
  // Store debug flag for use in catch block
82
71
  debugFlag = debug === true;
83
72
  jsonFile = flags['json-file'] === true;
@@ -134,7 +123,9 @@ class Cloud extends core_1.Command {
134
123
  throw new Error(`Failed to download Moropo tests: ${response.statusText}`);
135
124
  }
136
125
  const contentLength = response.headers.get('content-length');
137
- const totalSize = contentLength ? parseInt(contentLength, 10) : 0;
126
+ const totalSize = contentLength
127
+ ? Number.parseInt(contentLength, 10)
128
+ : 0;
138
129
  let downloadedSize = 0;
139
130
  const moropoDir = path.join(os.tmpdir(), `moropo-tests-${Date.now()}`);
140
131
  if (debug) {
@@ -151,23 +142,29 @@ class Cloud extends core_1.Command {
151
142
  if (!reader) {
152
143
  throw new Error('Failed to get response reader');
153
144
  }
154
- while (true) {
155
- const { done, value } = await reader.read();
156
- if (done)
157
- break;
145
+ let readerResult = await reader.read();
146
+ while (!readerResult.done) {
147
+ const { value } = readerResult;
158
148
  downloadedSize += value.length;
159
149
  if (!quiet && !json && totalSize) {
160
150
  const progress = Math.round((downloadedSize / totalSize) * 100);
161
151
  core_1.ux.action.status = `Downloading: ${progress}%`;
162
152
  }
163
153
  fileStream.write(value);
154
+ readerResult = await reader.read();
164
155
  }
165
156
  fileStream.end();
166
- await new Promise((resolve) => fileStream.on('finish', () => resolve()));
157
+ await new Promise((resolve) => {
158
+ fileStream.on('finish', () => {
159
+ resolve();
160
+ });
161
+ });
167
162
  if (!quiet && !json) {
168
163
  core_1.ux.action.status = 'Extracting tests...';
169
164
  }
170
165
  // Extract zip file
166
+ const StreamZip = streamZip;
167
+ // eslint-disable-next-line new-cap
171
168
  const zip = new StreamZip.async({ file: zipPath });
172
169
  await zip.extract(null, moropoDir);
173
170
  await zip.close();
@@ -258,12 +255,13 @@ class Cloud extends core_1.Command {
258
255
  }
259
256
  if (iOSVersion || iOSDevice) {
260
257
  const iOSDeviceID = iOSDevice || 'iphone-14';
261
- const supportediOSVersions = compatibilityData.ios[iOSDeviceID] || [];
258
+ const supportediOSVersions = compatibilityData?.ios?.[iOSDeviceID] || [];
262
259
  const version = iOSVersion || '17';
263
260
  if (supportediOSVersions.length === 0) {
264
261
  throw new Error(`Device ${iOSDeviceID} is not supported. Please check the docs for supported devices: https://docs.devicecloud.dev/getting-started/devices-configuration`);
265
262
  }
266
- if (!supportediOSVersions.includes(version)) {
263
+ if (Array.isArray(supportediOSVersions) &&
264
+ !supportediOSVersions.includes(version)) {
267
265
  throw new Error(`${iOSDeviceID} only supports these iOS versions: ${supportediOSVersions.join(', ')}`);
268
266
  }
269
267
  if (debug) {
@@ -277,12 +275,13 @@ class Cloud extends core_1.Command {
277
275
  const lookup = googlePlay
278
276
  ? compatibilityData.androidPlay
279
277
  : compatibilityData.android;
280
- const supportedAndroidVersions = lookup[androidDeviceID] || [];
278
+ const supportedAndroidVersions = lookup?.[androidDeviceID] || [];
281
279
  const version = androidApiLevel || '34';
282
280
  if (supportedAndroidVersions.length === 0) {
283
281
  throw new Error(`We don't support that device configuration - please check the docs for supported devices: https://docs.devicecloud.dev/getting-started/devices-configuration`);
284
282
  }
285
- if (!supportedAndroidVersions.includes(version)) {
283
+ if (Array.isArray(supportedAndroidVersions) &&
284
+ !supportedAndroidVersions.includes(version)) {
286
285
  throw new Error(`${androidDeviceID} ${googlePlay ? '(Play Store) ' : ''}only supports these Android API levels: ${supportedAndroidVersions.join(', ')}`);
287
286
  }
288
287
  if (debug) {
@@ -321,7 +320,7 @@ class Cloud extends core_1.Command {
321
320
  }
322
321
  throw error;
323
322
  }
324
- const { allExcludeTags, allIncludeTags, flowsToRun: testFileNames, flowMetadata, referencedFiles, sequence, workspaceConfig, } = executionPlan;
323
+ const { allExcludeTags, allIncludeTags, flowMetadata, flowsToRun: testFileNames, referencedFiles, sequence, workspaceConfig, } = executionPlan;
325
324
  if (debug) {
326
325
  this.log(`DEBUG: All include tags: ${allIncludeTags?.join(', ') || 'none'}`);
327
326
  this.log(`DEBUG: All exclude tags: ${allExcludeTags?.join(', ') || 'none'}`);
@@ -437,9 +436,19 @@ class Cloud extends core_1.Command {
437
436
  acc[key] = value.join('=');
438
437
  return acc;
439
438
  }, {});
439
+ // eslint-disable-next-line unicorn/no-array-reduce
440
+ const metadataObject = (metadata ?? []).reduce((acc, cur) => {
441
+ const [key, ...value] = cur.split('=');
442
+ // handle case where value includes an equals sign
443
+ acc[key] = value.join('=');
444
+ return acc;
445
+ }, {});
440
446
  if (debug && Object.keys(envObject).length > 0) {
441
447
  this.log(`DEBUG: Environment variables: ${JSON.stringify(envObject)}`);
442
448
  }
449
+ if (debug && Object.keys(metadataObject).length > 0) {
450
+ this.log(`DEBUG: User metadata: ${JSON.stringify(metadataObject)}`);
451
+ }
443
452
  if (debug) {
444
453
  this.log(`DEBUG: Compressing files from path: ${flowFile}`);
445
454
  }
@@ -459,6 +468,10 @@ class Cloud extends core_1.Command {
459
468
  type: exports.mimeTypeLookupByExtension.zip,
460
469
  });
461
470
  testFormData.set('file', blob, 'flowFile.zip');
471
+ // finalBinaryId should always be defined after validation - fail fast if not
472
+ if (!finalBinaryId) {
473
+ throw new Error('Internal error: finalBinaryId should be defined after validation');
474
+ }
462
475
  testFormData.set('appBinaryId', finalBinaryId);
463
476
  testFormData.set('testFileNames', JSON.stringify(testFileNames.map((t) => t.replaceAll(commonRoot, '.').split(path.sep).join('/'))));
464
477
  testFormData.set('flowMetadata', JSON.stringify(Object.fromEntries(Object.entries(flowMetadata).map(([key, value]) => [
@@ -474,17 +487,17 @@ class Cloud extends core_1.Command {
474
487
  autoRetriesRemaining: retry,
475
488
  continueOnFailure,
476
489
  deviceLocale,
477
- maestroVersion,
490
+ maestroVersion: resolvedMaestroVersion,
491
+ mitmHost,
492
+ mitmPath,
478
493
  orientation,
479
- x86Arch,
480
- report,
481
494
  raw: JSON.stringify(raw),
495
+ report,
496
+ showCrosshairs: flags['show-crosshairs'],
497
+ skipChromeOnboarding: flags['skip-chrome-onboarding'],
482
498
  uploadedBinaryIds,
483
499
  version: this.config.version,
484
- skipChromeOnboarding: flags['skip-chrome-onboarding'],
485
- showCrosshairs: flags['show-crosshairs'],
486
- mitmHost,
487
- mitmPath,
500
+ x86Arch,
488
501
  };
489
502
  if (finalAdditionalBinaryIds?.length > 0) {
490
503
  config.additionalAppBinaryIds = finalAdditionalBinaryIds;
@@ -493,6 +506,13 @@ class Cloud extends core_1.Command {
493
506
  config.uploadedBinaryIds = uploadedBinaryIds;
494
507
  }
495
508
  testFormData.set('config', JSON.stringify(config));
509
+ if (Object.keys(metadataObject).length > 0) {
510
+ const metadataPayload = { userMetadata: metadataObject };
511
+ testFormData.set('metadata', JSON.stringify(metadataPayload));
512
+ if (debug) {
513
+ this.log(`DEBUG: Sending metadata to API: ${JSON.stringify(metadataPayload)}`);
514
+ }
515
+ }
496
516
  if (androidApiLevel)
497
517
  testFormData.set('androidApiLevel', androidApiLevel.toString());
498
518
  if (androidDevice)
@@ -515,7 +535,7 @@ class Cloud extends core_1.Command {
515
535
  if (debug) {
516
536
  this.log(`DEBUG: Submitting flow upload request to ${apiUrl}/uploads/flow`);
517
537
  }
518
- const { message, results } = await ApiGateway_1.ApiGateway.uploadFlow(apiUrl, apiKey, testFormData);
538
+ const { message, results } = await api_gateway_1.ApiGateway.uploadFlow(apiUrl, apiKey, testFormData);
519
539
  if (debug) {
520
540
  this.log(`DEBUG: Flow upload response received`);
521
541
  this.log(`DEBUG: Message: ${message}`);
@@ -539,16 +559,16 @@ class Cloud extends core_1.Command {
539
559
  this.log(`DEBUG: Async flag is set, not waiting for results`);
540
560
  }
541
561
  const jsonOutput = {
542
- uploadId: results[0].test_upload_id,
543
562
  consoleUrl: url,
544
563
  status: 'PENDING',
545
564
  tests: results.map((r) => ({
546
565
  name: r.test_file_name,
547
566
  status: r.status,
548
567
  })),
568
+ uploadId: results[0].test_upload_id,
549
569
  };
550
570
  if (flags['json-file']) {
551
- const jsonFilePath = this.getJsonOutputPath(name, results[0].test_upload_id);
571
+ const jsonFilePath = this.getJsonOutputPath(results[0].test_upload_id, jsonFileName);
552
572
  (0, methods_1.writeJSONFile)(jsonFilePath, jsonOutput, this);
553
573
  }
554
574
  if (json) {
@@ -574,7 +594,7 @@ class Cloud extends core_1.Command {
574
594
  if (debug) {
575
595
  this.log(`DEBUG: Polling for results: ${results[0].test_upload_id}`);
576
596
  }
577
- const { results: updatedResults } = await ApiGateway_1.ApiGateway.getResultsForUpload(apiUrl, apiKey, results[0].test_upload_id);
597
+ const { results: updatedResults } = await api_gateway_1.ApiGateway.getResultsForUpload(apiUrl, apiKey, results[0].test_upload_id);
578
598
  if (!updatedResults) {
579
599
  throw new Error('no results');
580
600
  }
@@ -606,10 +626,11 @@ class Cloud extends core_1.Command {
606
626
  },
607
627
  duration: {
608
628
  get: (row) => row.duration_seconds
609
- ? (0, methods_1.formatDurationSeconds)(row.duration_seconds)
610
- : '',
629
+ ? (0, methods_1.formatDurationSeconds)(Number(row.duration_seconds))
630
+ : '-',
611
631
  },
612
632
  ...(hasFailedTests && {
633
+ // eslint-disable-next-line camelcase
613
634
  fail_reason: {
614
635
  get: (row) => row.status === 'FAILED' && row.fail_reason
615
636
  ? row.fail_reason
@@ -628,7 +649,7 @@ class Cloud extends core_1.Command {
628
649
  if (debug) {
629
650
  this.log(`DEBUG: Downloading artifacts: ${downloadArtifacts}`);
630
651
  }
631
- await ApiGateway_1.ApiGateway.downloadArtifactsZip(apiUrl, apiKey, results[0].test_upload_id, downloadArtifacts, artifactsPath);
652
+ await api_gateway_1.ApiGateway.downloadArtifactsZip(apiUrl, apiKey, results[0].test_upload_id, downloadArtifacts, artifactsPath);
632
653
  this.log('\n');
633
654
  this.log(`Test artifacts have been downloaded to ${artifactsPath || './artifacts.zip'}`);
634
655
  }
@@ -649,20 +670,20 @@ class Cloud extends core_1.Command {
649
670
  this.log(`DEBUG: Some tests failed, returning failed status`);
650
671
  }
651
672
  const jsonOutput = {
652
- uploadId: results[0].test_upload_id,
653
673
  consoleUrl: url,
654
674
  status: 'FAILED',
655
675
  tests: resultsWithoutEarlierTries.map((r) => ({
656
676
  name: r.test_file_name,
657
677
  status: r.status,
658
- duration_seconds: r.duration_seconds,
659
- fail_reason: r.status === 'FAILED'
678
+ durationSeconds: r.duration_seconds,
679
+ failReason: r.status === 'FAILED'
660
680
  ? r.fail_reason || 'No reason provided'
661
681
  : undefined,
662
682
  })),
683
+ uploadId: results[0].test_upload_id,
663
684
  };
664
685
  if (flags['json-file']) {
665
- const jsonFilePath = this.getJsonOutputPath(name, results[0].test_upload_id);
686
+ const jsonFilePath = this.getJsonOutputPath(results[0].test_upload_id, jsonFileName);
666
687
  (0, methods_1.writeJSONFile)(jsonFilePath, jsonOutput, this);
667
688
  }
668
689
  if (json) {
@@ -675,20 +696,20 @@ class Cloud extends core_1.Command {
675
696
  this.log(`DEBUG: All tests passed, returning success status`);
676
697
  }
677
698
  const jsonOutput = {
678
- uploadId: results[0].test_upload_id,
679
699
  consoleUrl: url,
680
700
  status: 'PASSED',
681
701
  tests: resultsWithoutEarlierTries.map((r) => ({
682
702
  name: r.test_file_name,
683
703
  status: r.status,
684
- duration_seconds: r.duration_seconds,
685
- fail_reason: r.status === 'FAILED'
704
+ durationSeconds: r.duration_seconds,
705
+ failReason: r.status === 'FAILED'
686
706
  ? r.fail_reason || 'No reason provided'
687
707
  : undefined,
688
708
  })),
709
+ uploadId: results[0].test_upload_id,
689
710
  };
690
711
  if (flags['json-file']) {
691
- const jsonFilePath = this.getJsonOutputPath(name, results[0].test_upload_id);
712
+ const jsonFilePath = this.getJsonOutputPath(results[0].test_upload_id, jsonFileName);
692
713
  (0, methods_1.writeJSONFile)(jsonFilePath, jsonOutput, this);
693
714
  }
694
715
  if (json) {
@@ -733,9 +754,22 @@ class Cloud extends core_1.Command {
733
754
  }
734
755
  finally {
735
756
  if (output) {
757
+ // eslint-disable-next-line no-unsafe-finally
736
758
  return output;
737
759
  }
738
760
  }
739
761
  }
762
+ /**
763
+ * Generate the JSON output file path based on upload ID or custom filename
764
+ * @param uploadId - Upload ID to use if custom filename is not provided
765
+ * @param jsonFileName - Optional custom filename (can include relative path)
766
+ * @returns Path to the JSON output file
767
+ */
768
+ getJsonOutputPath(uploadId, jsonFileName) {
769
+ if (jsonFileName) {
770
+ return jsonFileName;
771
+ }
772
+ return `${uploadId}_dcd.json`;
773
+ }
740
774
  }
741
775
  exports.default = Cloud;
@@ -1,30 +1,30 @@
1
1
  import { Command } from '@oclif/core';
2
2
  type StatusResponse = {
3
- status: 'FAILED' | 'PASSED' | 'CANCELLED' | 'PENDING' | 'RUNNING';
3
+ appBinaryId?: string;
4
+ attempts?: number;
5
+ consoleUrl?: string;
6
+ error?: string;
7
+ status: 'CANCELLED' | 'FAILED' | 'PASSED' | 'PENDING' | 'RUNNING';
4
8
  tests: {
5
- name: string;
6
- status: 'FAILED' | 'PASSED' | 'CANCELLED' | 'PENDING' | 'RUNNING';
7
9
  durationSeconds?: number;
8
10
  failReason?: string;
11
+ name: string;
12
+ status: 'CANCELLED' | 'FAILED' | 'PASSED' | 'PENDING' | 'RUNNING';
9
13
  }[];
10
14
  uploadId?: string;
11
- appBinaryId?: string;
12
- consoleUrl?: string;
13
- error?: string;
14
- attempts?: number;
15
15
  };
16
16
  export default class Status extends Command {
17
17
  static description: string;
18
- static examples: string[];
19
18
  static enableJsonFlag: boolean;
19
+ static examples: string[];
20
20
  static flags: {
21
- json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
22
- name: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
23
- 'upload-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
24
- apiKey: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
21
+ apiKey: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
25
22
  apiUrl: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
23
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
24
+ name: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
25
+ 'upload-id': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
26
26
  };
27
- private getStatusSymbol;
28
27
  run(): Promise<StatusResponse | void>;
28
+ private getStatusSymbol;
29
29
  }
30
30
  export {};