@devicecloud.dev/dcd 3.7.12-beta.1 → 4.0.1

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, flowOverrides, 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'}`);
@@ -371,6 +370,23 @@ class Cloud extends core_1.Command {
371
370
  flagLogs.push(`${k}: ${v}`);
372
371
  }
373
372
  }
373
+ // Format overrides information
374
+ const overridesEntries = Object.entries(flowOverrides);
375
+ const hasOverrides = overridesEntries.some(([, overrides]) => Object.keys(overrides).length > 0);
376
+ let overridesLog = '';
377
+ if (hasOverrides) {
378
+ overridesLog = '\n With overrides';
379
+ for (const [flowPath, overrides] of overridesEntries) {
380
+ if (Object.keys(overrides).length > 0) {
381
+ const relativePath = flowPath.replace(process.cwd(), '.');
382
+ overridesLog += `\n → ${relativePath}:`;
383
+ for (const [key, value] of Object.entries(overrides)) {
384
+ overridesLog += `\n ${key}: ${value}`;
385
+ }
386
+ }
387
+ }
388
+ overridesLog += '\n';
389
+ }
374
390
  this.log(`
375
391
 
376
392
  Submitting new job
@@ -382,7 +398,7 @@ class Cloud extends core_1.Command {
382
398
 
383
399
  With options
384
400
  → ${flagLogs.join(`
385
- → `)}
401
+ → `)}${overridesLog}
386
402
 
387
403
  `);
388
404
  if (dryRun) {
@@ -437,9 +453,19 @@ class Cloud extends core_1.Command {
437
453
  acc[key] = value.join('=');
438
454
  return acc;
439
455
  }, {});
456
+ // eslint-disable-next-line unicorn/no-array-reduce
457
+ const metadataObject = (metadata ?? []).reduce((acc, cur) => {
458
+ const [key, ...value] = cur.split('=');
459
+ // handle case where value includes an equals sign
460
+ acc[key] = value.join('=');
461
+ return acc;
462
+ }, {});
440
463
  if (debug && Object.keys(envObject).length > 0) {
441
464
  this.log(`DEBUG: Environment variables: ${JSON.stringify(envObject)}`);
442
465
  }
466
+ if (debug && Object.keys(metadataObject).length > 0) {
467
+ this.log(`DEBUG: User metadata: ${JSON.stringify(metadataObject)}`);
468
+ }
443
469
  if (debug) {
444
470
  this.log(`DEBUG: Compressing files from path: ${flowFile}`);
445
471
  }
@@ -459,12 +485,20 @@ class Cloud extends core_1.Command {
459
485
  type: exports.mimeTypeLookupByExtension.zip,
460
486
  });
461
487
  testFormData.set('file', blob, 'flowFile.zip');
488
+ // finalBinaryId should always be defined after validation - fail fast if not
489
+ if (!finalBinaryId) {
490
+ throw new Error('Internal error: finalBinaryId should be defined after validation');
491
+ }
462
492
  testFormData.set('appBinaryId', finalBinaryId);
463
493
  testFormData.set('testFileNames', JSON.stringify(testFileNames.map((t) => t.replaceAll(commonRoot, '.').split(path.sep).join('/'))));
464
494
  testFormData.set('flowMetadata', JSON.stringify(Object.fromEntries(Object.entries(flowMetadata).map(([key, value]) => [
465
495
  key.replaceAll(commonRoot, '.').split(path.sep).join('/'),
466
496
  value,
467
497
  ]))));
498
+ testFormData.set('testFileOverrides', JSON.stringify(Object.fromEntries(Object.entries(flowOverrides).map(([key, value]) => [
499
+ key.replaceAll(commonRoot, '.').split(path.sep).join('/'),
500
+ value,
501
+ ]))));
468
502
  testFormData.set('sequentialFlows', JSON.stringify(sequentialFlows.map((t) => t.replaceAll(commonRoot, '.').split(path.sep).join('/'))));
469
503
  testFormData.set('env', JSON.stringify(envObject));
470
504
  testFormData.set('googlePlay', googlePlay ? 'true' : 'false');
@@ -474,17 +508,17 @@ class Cloud extends core_1.Command {
474
508
  autoRetriesRemaining: retry,
475
509
  continueOnFailure,
476
510
  deviceLocale,
477
- maestroVersion,
511
+ maestroVersion: resolvedMaestroVersion,
512
+ mitmHost,
513
+ mitmPath,
478
514
  orientation,
479
- x86Arch,
480
- report,
481
515
  raw: JSON.stringify(raw),
516
+ report,
517
+ showCrosshairs: flags['show-crosshairs'],
518
+ skipChromeOnboarding: flags['skip-chrome-onboarding'],
482
519
  uploadedBinaryIds,
483
520
  version: this.config.version,
484
- skipChromeOnboarding: flags['skip-chrome-onboarding'],
485
- showCrosshairs: flags['show-crosshairs'],
486
- mitmHost,
487
- mitmPath,
521
+ x86Arch,
488
522
  };
489
523
  if (finalAdditionalBinaryIds?.length > 0) {
490
524
  config.additionalAppBinaryIds = finalAdditionalBinaryIds;
@@ -493,6 +527,13 @@ class Cloud extends core_1.Command {
493
527
  config.uploadedBinaryIds = uploadedBinaryIds;
494
528
  }
495
529
  testFormData.set('config', JSON.stringify(config));
530
+ if (Object.keys(metadataObject).length > 0) {
531
+ const metadataPayload = { userMetadata: metadataObject };
532
+ testFormData.set('metadata', JSON.stringify(metadataPayload));
533
+ if (debug) {
534
+ this.log(`DEBUG: Sending metadata to API: ${JSON.stringify(metadataPayload)}`);
535
+ }
536
+ }
496
537
  if (androidApiLevel)
497
538
  testFormData.set('androidApiLevel', androidApiLevel.toString());
498
539
  if (androidDevice)
@@ -515,7 +556,7 @@ class Cloud extends core_1.Command {
515
556
  if (debug) {
516
557
  this.log(`DEBUG: Submitting flow upload request to ${apiUrl}/uploads/flow`);
517
558
  }
518
- const { message, results } = await ApiGateway_1.ApiGateway.uploadFlow(apiUrl, apiKey, testFormData);
559
+ const { message, results } = await api_gateway_1.ApiGateway.uploadFlow(apiUrl, apiKey, testFormData);
519
560
  if (debug) {
520
561
  this.log(`DEBUG: Flow upload response received`);
521
562
  this.log(`DEBUG: Message: ${message}`);
@@ -539,16 +580,16 @@ class Cloud extends core_1.Command {
539
580
  this.log(`DEBUG: Async flag is set, not waiting for results`);
540
581
  }
541
582
  const jsonOutput = {
542
- uploadId: results[0].test_upload_id,
543
583
  consoleUrl: url,
544
584
  status: 'PENDING',
545
585
  tests: results.map((r) => ({
546
586
  name: r.test_file_name,
547
587
  status: r.status,
548
588
  })),
589
+ uploadId: results[0].test_upload_id,
549
590
  };
550
591
  if (flags['json-file']) {
551
- const jsonFilePath = this.getJsonOutputPath(name, results[0].test_upload_id);
592
+ const jsonFilePath = this.getJsonOutputPath(results[0].test_upload_id, jsonFileName);
552
593
  (0, methods_1.writeJSONFile)(jsonFilePath, jsonOutput, this);
553
594
  }
554
595
  if (json) {
@@ -574,7 +615,7 @@ class Cloud extends core_1.Command {
574
615
  if (debug) {
575
616
  this.log(`DEBUG: Polling for results: ${results[0].test_upload_id}`);
576
617
  }
577
- const { results: updatedResults } = await ApiGateway_1.ApiGateway.getResultsForUpload(apiUrl, apiKey, results[0].test_upload_id);
618
+ const { results: updatedResults } = await api_gateway_1.ApiGateway.getResultsForUpload(apiUrl, apiKey, results[0].test_upload_id);
578
619
  if (!updatedResults) {
579
620
  throw new Error('no results');
580
621
  }
@@ -606,10 +647,11 @@ class Cloud extends core_1.Command {
606
647
  },
607
648
  duration: {
608
649
  get: (row) => row.duration_seconds
609
- ? (0, methods_1.formatDurationSeconds)(row.duration_seconds)
610
- : '',
650
+ ? (0, methods_1.formatDurationSeconds)(Number(row.duration_seconds))
651
+ : '-',
611
652
  },
612
653
  ...(hasFailedTests && {
654
+ // eslint-disable-next-line camelcase
613
655
  fail_reason: {
614
656
  get: (row) => row.status === 'FAILED' && row.fail_reason
615
657
  ? row.fail_reason
@@ -628,7 +670,7 @@ class Cloud extends core_1.Command {
628
670
  if (debug) {
629
671
  this.log(`DEBUG: Downloading artifacts: ${downloadArtifacts}`);
630
672
  }
631
- await ApiGateway_1.ApiGateway.downloadArtifactsZip(apiUrl, apiKey, results[0].test_upload_id, downloadArtifacts, artifactsPath);
673
+ await api_gateway_1.ApiGateway.downloadArtifactsZip(apiUrl, apiKey, results[0].test_upload_id, downloadArtifacts, artifactsPath);
632
674
  this.log('\n');
633
675
  this.log(`Test artifacts have been downloaded to ${artifactsPath || './artifacts.zip'}`);
634
676
  }
@@ -649,20 +691,20 @@ class Cloud extends core_1.Command {
649
691
  this.log(`DEBUG: Some tests failed, returning failed status`);
650
692
  }
651
693
  const jsonOutput = {
652
- uploadId: results[0].test_upload_id,
653
694
  consoleUrl: url,
654
695
  status: 'FAILED',
655
696
  tests: resultsWithoutEarlierTries.map((r) => ({
656
697
  name: r.test_file_name,
657
698
  status: r.status,
658
- duration_seconds: r.duration_seconds,
659
- fail_reason: r.status === 'FAILED'
699
+ durationSeconds: r.duration_seconds,
700
+ failReason: r.status === 'FAILED'
660
701
  ? r.fail_reason || 'No reason provided'
661
702
  : undefined,
662
703
  })),
704
+ uploadId: results[0].test_upload_id,
663
705
  };
664
706
  if (flags['json-file']) {
665
- const jsonFilePath = this.getJsonOutputPath(name, results[0].test_upload_id);
707
+ const jsonFilePath = this.getJsonOutputPath(results[0].test_upload_id, jsonFileName);
666
708
  (0, methods_1.writeJSONFile)(jsonFilePath, jsonOutput, this);
667
709
  }
668
710
  if (json) {
@@ -675,20 +717,20 @@ class Cloud extends core_1.Command {
675
717
  this.log(`DEBUG: All tests passed, returning success status`);
676
718
  }
677
719
  const jsonOutput = {
678
- uploadId: results[0].test_upload_id,
679
720
  consoleUrl: url,
680
721
  status: 'PASSED',
681
722
  tests: resultsWithoutEarlierTries.map((r) => ({
682
723
  name: r.test_file_name,
683
724
  status: r.status,
684
- duration_seconds: r.duration_seconds,
685
- fail_reason: r.status === 'FAILED'
725
+ durationSeconds: r.duration_seconds,
726
+ failReason: r.status === 'FAILED'
686
727
  ? r.fail_reason || 'No reason provided'
687
728
  : undefined,
688
729
  })),
730
+ uploadId: results[0].test_upload_id,
689
731
  };
690
732
  if (flags['json-file']) {
691
- const jsonFilePath = this.getJsonOutputPath(name, results[0].test_upload_id);
733
+ const jsonFilePath = this.getJsonOutputPath(results[0].test_upload_id, jsonFileName);
692
734
  (0, methods_1.writeJSONFile)(jsonFilePath, jsonOutput, this);
693
735
  }
694
736
  if (json) {
@@ -733,9 +775,22 @@ class Cloud extends core_1.Command {
733
775
  }
734
776
  finally {
735
777
  if (output) {
778
+ // eslint-disable-next-line no-unsafe-finally
736
779
  return output;
737
780
  }
738
781
  }
739
782
  }
783
+ /**
784
+ * Generate the JSON output file path based on upload ID or custom filename
785
+ * @param uploadId - Upload ID to use if custom filename is not provided
786
+ * @param jsonFileName - Optional custom filename (can include relative path)
787
+ * @returns Path to the JSON output file
788
+ */
789
+ getJsonOutputPath(uploadId, jsonFileName) {
790
+ if (jsonFileName) {
791
+ return jsonFileName;
792
+ }
793
+ return `${uploadId}_dcd.json`;
794
+ }
740
795
  }
741
796
  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 {};