@devicecloud.dev/dcd 4.2.4 → 4.2.6

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.
@@ -6,7 +6,7 @@ import { Command } from '@oclif/core';
6
6
  * - Flow file analysis and dependency resolution
7
7
  * - Device compatibility validation
8
8
  * - Test submission with parallel execution
9
- * - Real-time result polling with 5-second intervals
9
+ * - Real-time result polling with 10-second intervals
10
10
  * - Artifact download (reports, videos, logs)
11
11
  *
12
12
  * Replaces `maestro cloud` with DeviceCloud-specific functionality.
@@ -56,6 +56,7 @@ export default class Cloud extends Command {
56
56
  orientation: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
57
57
  'show-crosshairs': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
58
58
  'maestro-chrome-onboarding': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
59
+ 'android-no-snapshot': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
59
60
  'app-binary-id': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
60
61
  'app-file': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
61
62
  'ignore-sha-check': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
@@ -68,7 +69,7 @@ export default class Cloud extends Command {
68
69
  private moropoService;
69
70
  /** Service for downloading test reports and artifacts */
70
71
  private reportDownloadService;
71
- /** Service for polling test results with 5-second intervals */
72
+ /** Service for polling test results with 10-second intervals */
72
73
  private resultsPollingService;
73
74
  /** Service for submitting tests to the API */
74
75
  private testSubmissionService;
@@ -31,7 +31,7 @@ process.on('warning', (warning) => {
31
31
  * - Flow file analysis and dependency resolution
32
32
  * - Device compatibility validation
33
33
  * - Test submission with parallel execution
34
- * - Real-time result polling with 5-second intervals
34
+ * - Real-time result polling with 10-second intervals
35
35
  * - Artifact download (reports, videos, logs)
36
36
  *
37
37
  * Replaces `maestro cloud` with DeviceCloud-specific functionality.
@@ -59,7 +59,7 @@ class Cloud extends core_1.Command {
59
59
  moropoService = new moropo_service_1.MoropoService();
60
60
  /** Service for downloading test reports and artifacts */
61
61
  reportDownloadService = new report_download_service_1.ReportDownloadService();
62
- /** Service for polling test results with 5-second intervals */
62
+ /** Service for polling test results with 10-second intervals */
63
63
  resultsPollingService = new results_polling_service_1.ResultsPollingService();
64
64
  /** Service for submitting tests to the API */
65
65
  testSubmissionService = new test_submission_service_1.TestSubmissionService();
@@ -95,7 +95,7 @@ class Cloud extends core_1.Command {
95
95
  let jsonFile = false;
96
96
  try {
97
97
  const { args, flags, raw } = await this.parse(Cloud);
98
- let { '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, } = flags;
98
+ let { '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, 'android-no-snapshot': androidNoSnapshot, } = flags;
99
99
  // Store debug flag for use in catch block
100
100
  debugFlag = debug === true;
101
101
  jsonFile = flags['json-file'] === true;
@@ -242,7 +242,7 @@ class Cloud extends core_1.Command {
242
242
  }
243
243
  throw error;
244
244
  }
245
- const { allExcludeTags, allIncludeTags, flowOverrides, flowsToRun: testFileNames, referencedFiles, sequence, } = executionPlan;
245
+ const { allExcludeTags, allIncludeTags, flowMetadata, flowOverrides, flowsToRun: testFileNames, referencedFiles, sequence, } = executionPlan;
246
246
  if (debug) {
247
247
  this.log(`[DEBUG] All include tags: ${allIncludeTags?.join(', ') || 'none'}`);
248
248
  this.log(`[DEBUG] All exclude tags: ${allExcludeTags?.join(', ') || 'none'}`);
@@ -263,6 +263,21 @@ class Cloud extends core_1.Command {
263
263
  if (debug) {
264
264
  this.log(`[DEBUG] Common root directory: ${commonRoot}`);
265
265
  }
266
+ // Build testMetadataMap from flowMetadata (keyed by normalized test file name)
267
+ // This map provides flowName and tags for each test for JSON output
268
+ const testMetadataMap = {};
269
+ for (const [absolutePath, metadata] of Object.entries(flowMetadata)) {
270
+ // Normalize the path to match the format used in results (e.g., "./flows/test.yaml")
271
+ const normalizedPath = absolutePath.replaceAll(commonRoot, '.').split(path.sep).join('/');
272
+ const metadataRecord = metadata;
273
+ const flowName = metadataRecord?.name || path.parse(absolutePath).name;
274
+ const rawTags = metadataRecord?.tags;
275
+ const tags = Array.isArray(rawTags) ? rawTags.map(String) : (rawTags ? [String(rawTags)] : []);
276
+ testMetadataMap[normalizedPath] = { flowName, tags };
277
+ }
278
+ if (debug) {
279
+ this.log(`[DEBUG] Built testMetadataMap for ${Object.keys(testMetadataMap).length} flows`);
280
+ }
266
281
  const { continueOnFailure = true, flows: sequentialFlows = [] } = sequence ?? {};
267
282
  if (debug && sequentialFlows.length > 0) {
268
283
  this.log(`[DEBUG] Sequential flows: ${sequentialFlows.join(', ')}`);
@@ -363,6 +378,7 @@ class Cloud extends core_1.Command {
363
378
  const testFormData = await this.testSubmissionService.buildTestFormData({
364
379
  androidApiLevel,
365
380
  androidDevice,
381
+ androidNoSnapshot,
366
382
  appBinaryId: finalBinaryId,
367
383
  cliVersion: this.config.version,
368
384
  commonRoot,
@@ -420,8 +436,11 @@ class Cloud extends core_1.Command {
420
436
  consoleUrl: url,
421
437
  status: 'PENDING',
422
438
  tests: results.map((r) => ({
439
+ fileName: r.test_file_name,
440
+ flowName: testMetadataMap[r.test_file_name]?.flowName || path.parse(r.test_file_name).name,
423
441
  name: r.test_file_name,
424
442
  status: r.status,
443
+ tags: testMetadataMap[r.test_file_name]?.tags || [],
425
444
  })),
426
445
  uploadId: results[0].test_upload_id,
427
446
  };
@@ -446,7 +465,7 @@ class Cloud extends core_1.Command {
446
465
  logger: this.log.bind(this),
447
466
  quiet,
448
467
  uploadId: results[0].test_upload_id,
449
- })
468
+ }, testMetadataMap)
450
469
  .catch(async (error) => {
451
470
  if (error instanceof results_polling_service_1.RunFailedError) {
452
471
  // Handle failed test run
@@ -11,4 +11,5 @@ export declare const deviceFlags: {
11
11
  orientation: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
12
12
  'show-crosshairs': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
13
13
  'maestro-chrome-onboarding': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
14
+ 'android-no-snapshot': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
14
15
  };
@@ -43,4 +43,8 @@ exports.deviceFlags = {
43
43
  default: false,
44
44
  description: '[Android only] Force Maestro-based Chrome onboarding - note: this will slow your tests but can fix browser related crashes. See https://docs.devicecloud.dev/reference/chrome-onboarding for more information.',
45
45
  }),
46
+ 'android-no-snapshot': core_1.Flags.boolean({
47
+ default: false,
48
+ description: '[Android only] Force cold boot instead of using snapshot boot. This is automatically enabled for API 35+ but can be used to force cold boot on older API levels.',
49
+ }),
46
50
  };
@@ -43,6 +43,7 @@ export declare const flags: {
43
43
  orientation: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
44
44
  'show-crosshairs': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
45
45
  'maestro-chrome-onboarding': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
46
+ 'android-no-snapshot': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
46
47
  'app-binary-id': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
47
48
  'app-file': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
48
49
  'ignore-sha-check': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
package/dist/methods.js CHANGED
@@ -647,6 +647,94 @@ async function readFileObjectChunk(file, start, end) {
647
647
  const arrayBuffer = await slice.arrayBuffer();
648
648
  return Buffer.from(arrayBuffer);
649
649
  }
650
+ /**
651
+ * Reads a file chunk from either a File object or disk
652
+ * @param fileObject - Optional File object to read from
653
+ * @param filePath - Path to file on disk
654
+ * @param start - Start byte position
655
+ * @param end - End byte position
656
+ * @returns Promise resolving to Buffer containing the chunk
657
+ */
658
+ async function readChunk(fileObject, filePath, start, end) {
659
+ return fileObject
660
+ ? readFileObjectChunk(fileObject, start, end)
661
+ : readFileChunk(filePath, start, end);
662
+ }
663
+ /**
664
+ * Calculates SHA1 hash for a buffer
665
+ * @param buffer - Buffer to hash
666
+ * @returns SHA1 hash as hex string
667
+ */
668
+ function calculateSha1(buffer) {
669
+ const sha1 = (0, node_crypto_1.createHash)('sha1');
670
+ sha1.update(buffer);
671
+ return sha1.digest('hex');
672
+ }
673
+ /**
674
+ * Uploads a single part to Backblaze
675
+ * @param config - Configuration for the part upload
676
+ * @returns Promise that resolves when upload completes
677
+ */
678
+ async function uploadPartToBackblaze(config) {
679
+ const { uploadUrl, authorizationToken, partBuffer, partLength, sha1Hex, partNumber, totalParts, debug } = config;
680
+ if (debug) {
681
+ console.log(`[DEBUG] Uploading part ${partNumber}/${totalParts} (${(partLength / 1024 / 1024).toFixed(2)} MB, SHA1: ${sha1Hex})`);
682
+ }
683
+ try {
684
+ const response = await fetch(uploadUrl, {
685
+ body: new Uint8Array(partBuffer),
686
+ headers: {
687
+ Authorization: authorizationToken,
688
+ 'Content-Length': partLength.toString(),
689
+ 'X-Bz-Content-Sha1': sha1Hex,
690
+ 'X-Bz-Part-Number': partNumber.toString(),
691
+ },
692
+ method: 'POST',
693
+ });
694
+ if (!response.ok) {
695
+ const errorText = await response.text();
696
+ if (debug) {
697
+ console.error(`[DEBUG] Part ${partNumber} upload failed with status ${response.status}: ${errorText}`);
698
+ }
699
+ throw new Error(`Part ${partNumber} upload failed with status ${response.status}`);
700
+ }
701
+ }
702
+ catch (error) {
703
+ if (error instanceof TypeError && error.message === 'fetch failed') {
704
+ if (debug) {
705
+ console.error(`[DEBUG] Network error uploading part ${partNumber} - could be DNS, connection, or SSL issue`);
706
+ }
707
+ throw new Error(`Part ${partNumber} upload failed due to network error`);
708
+ }
709
+ throw error;
710
+ }
711
+ if (debug) {
712
+ console.log(`[DEBUG] Part ${partNumber}/${totalParts} uploaded successfully`);
713
+ }
714
+ }
715
+ /**
716
+ * Logs detailed error information for Backblaze upload failures
717
+ * @param error - The error that occurred
718
+ * @param debug - Whether debug logging is enabled
719
+ * @returns void
720
+ */
721
+ function logBackblazeUploadError(error, debug) {
722
+ if (debug) {
723
+ console.error('[DEBUG] === BACKBLAZE LARGE FILE UPLOAD EXCEPTION ===');
724
+ console.error('[DEBUG] Large file upload exception:', error);
725
+ console.error(`[DEBUG] Error type: ${error instanceof Error ? error.name : typeof error}`);
726
+ console.error(`[DEBUG] Error message: ${error instanceof Error ? error.message : String(error)}`);
727
+ if (error instanceof Error && error.stack) {
728
+ console.error(`[DEBUG] Stack trace:\n${error.stack}`);
729
+ }
730
+ }
731
+ if (error instanceof Error && error.message.includes('network error')) {
732
+ console.warn('Warning: Backblaze large file upload failed due to network error');
733
+ }
734
+ else {
735
+ console.warn(`Warning: Backblaze large file upload failed: ${error instanceof Error ? error.message : String(error)}`);
736
+ }
737
+ }
650
738
  /**
651
739
  * Upload large file to Backblaze using multi-part upload with streaming (for files >= 5MB)
652
740
  * Uses file streaming to avoid loading entire file into memory, preventing OOM errors on large files
@@ -657,7 +745,6 @@ async function uploadLargeFileToBackblaze(config) {
657
745
  const { apiUrl, apiKey, fileId, uploadPartUrls, filePath, fileSize, debug, fileObject } = config;
658
746
  try {
659
747
  const partSha1Array = [];
660
- // Calculate part size (divide file evenly across all parts)
661
748
  const partSize = Math.ceil(fileSize / uploadPartUrls.length);
662
749
  if (debug) {
663
750
  console.log(`[DEBUG] Uploading large file in ${uploadPartUrls.length} parts (streaming mode)`);
@@ -673,87 +760,38 @@ async function uploadLargeFileToBackblaze(config) {
673
760
  if (debug) {
674
761
  console.log(`[DEBUG] Reading part ${partNumber}/${uploadPartUrls.length} bytes ${start}-${end}`);
675
762
  }
676
- // Read part from File object (for .app bundles) or from disk
677
- const partBuffer = fileObject
678
- ? await readFileObjectChunk(fileObject, start, end)
679
- : await readFileChunk(filePath, start, end);
680
- // Calculate SHA1 for this part
681
- const sha1 = (0, node_crypto_1.createHash)('sha1');
682
- sha1.update(partBuffer);
683
- const sha1Hex = sha1.digest('hex');
763
+ const partBuffer = await readChunk(fileObject, filePath, start, end);
764
+ const sha1Hex = calculateSha1(partBuffer);
684
765
  partSha1Array.push(sha1Hex);
685
- if (debug) {
686
- console.log(`[DEBUG] Uploading part ${partNumber}/${uploadPartUrls.length} (${(partLength / 1024 / 1024).toFixed(2)} MB, SHA1: ${sha1Hex})`);
687
- }
688
- try {
689
- const response = await fetch(uploadPartUrls[i].uploadUrl, {
690
- body: new Uint8Array(partBuffer),
691
- headers: {
692
- Authorization: uploadPartUrls[i].authorizationToken,
693
- 'Content-Length': partLength.toString(),
694
- 'X-Bz-Content-Sha1': sha1Hex,
695
- 'X-Bz-Part-Number': partNumber.toString(),
696
- },
697
- method: 'POST',
698
- });
699
- if (!response.ok) {
700
- const errorText = await response.text();
701
- if (debug) {
702
- console.error(`[DEBUG] Part ${partNumber} upload failed with status ${response.status}: ${errorText}`);
703
- }
704
- throw new Error(`Part ${partNumber} upload failed with status ${response.status}`);
705
- }
706
- }
707
- catch (error) {
708
- if (error instanceof TypeError && error.message === 'fetch failed') {
709
- if (debug) {
710
- console.error(`[DEBUG] Network error uploading part ${partNumber} - could be DNS, connection, or SSL issue`);
711
- }
712
- throw new Error(`Part ${partNumber} upload failed due to network error`);
713
- }
714
- throw error;
715
- }
716
- if (debug) {
717
- console.log(`[DEBUG] Part ${partNumber}/${uploadPartUrls.length} uploaded successfully`);
718
- }
766
+ await uploadPartToBackblaze({
767
+ authorizationToken: uploadPartUrls[i].authorizationToken,
768
+ debug,
769
+ partBuffer,
770
+ partLength,
771
+ partNumber,
772
+ sha1Hex,
773
+ totalParts: uploadPartUrls.length,
774
+ uploadUrl: uploadPartUrls[i].uploadUrl,
775
+ });
719
776
  }
720
777
  // Validate all parts were uploaded
721
778
  if (partSha1Array.length !== uploadPartUrls.length) {
722
779
  const errorMsg = `Part count mismatch: uploaded ${partSha1Array.length} parts but expected ${uploadPartUrls.length}`;
723
- if (debug) {
780
+ if (debug)
724
781
  console.error(`[DEBUG] ${errorMsg}`);
725
- }
726
782
  throw new Error(errorMsg);
727
783
  }
728
- // Finish the large file upload
729
784
  if (debug) {
730
785
  console.log('[DEBUG] Finishing large file upload...');
731
786
  console.log(`[DEBUG] Finalizing ${partSha1Array.length} parts with fileId: ${fileId}`);
732
787
  }
733
788
  await api_gateway_1.ApiGateway.finishLargeFile(apiUrl, apiKey, fileId, partSha1Array);
734
- if (debug) {
789
+ if (debug)
735
790
  console.log('[DEBUG] Large file upload completed successfully');
736
- }
737
791
  return true;
738
792
  }
739
793
  catch (error) {
740
- if (debug) {
741
- console.error('[DEBUG] === BACKBLAZE LARGE FILE UPLOAD EXCEPTION ===');
742
- console.error('[DEBUG] Large file upload exception:', error);
743
- console.error(`[DEBUG] Error type: ${error instanceof Error ? error.name : typeof error}`);
744
- console.error(`[DEBUG] Error message: ${error instanceof Error ? error.message : String(error)}`);
745
- if (error instanceof Error && error.stack) {
746
- console.error(`[DEBUG] Stack trace:\n${error.stack}`);
747
- }
748
- }
749
- // Provide more specific error messages
750
- if (error instanceof Error && error.message.includes('network error')) {
751
- console.warn('Warning: Backblaze large file upload failed due to network error');
752
- }
753
- else {
754
- // Don't throw - we don't want Backblaze failures to block the primary upload
755
- console.warn(`Warning: Backblaze large file upload failed: ${error instanceof Error ? error.message : String(error)}`);
756
- }
794
+ logBackblazeUploadError(error, debug);
757
795
  return false;
758
796
  }
759
797
  }
@@ -89,6 +89,104 @@ function extractDeviceCloudOverrides(config) {
89
89
  }
90
90
  return overrides;
91
91
  }
92
+ /**
93
+ * Generate execution plan for a single flow file
94
+ * @param normalizedInput - Normalized path to the flow file
95
+ * @returns Execution plan for the single file with dependencies
96
+ */
97
+ async function planSingleFile(normalizedInput) {
98
+ if (normalizedInput.endsWith('config.yaml') ||
99
+ normalizedInput.endsWith('config.yml')) {
100
+ throw new Error('If using config.yaml, pass the workspace folder path, not the config file or a custom path via --config');
101
+ }
102
+ const { config } = (0, execution_plan_utils_1.readTestYamlFileAsJson)(normalizedInput);
103
+ const flowMetadata = {};
104
+ const flowOverrides = {};
105
+ if (config) {
106
+ flowMetadata[normalizedInput] = config;
107
+ flowOverrides[normalizedInput] = extractDeviceCloudOverrides(config);
108
+ }
109
+ const checkedDependancies = await checkDependencies(normalizedInput);
110
+ return {
111
+ flowMetadata,
112
+ flowOverrides,
113
+ flowsToRun: [normalizedInput],
114
+ referencedFiles: [...new Set(checkedDependancies)],
115
+ totalFlowFiles: 1,
116
+ };
117
+ }
118
+ /**
119
+ * Apply workspace.flows glob patterns to filter flow files
120
+ * @param workspaceConfig - Workspace configuration containing flow globs
121
+ * @param normalizedInput - Normalized path to the workspace directory
122
+ * @param unfilteredFlowFiles - List of all discovered flow files
123
+ * @param configFile - Optional custom config file path
124
+ * @returns Filtered list of flow file paths matching the globs
125
+ */
126
+ async function applyFlowGlobs(workspaceConfig, normalizedInput, unfilteredFlowFiles, configFile) {
127
+ if (workspaceConfig.flows) {
128
+ const globs = workspaceConfig.flows.map((g) => g);
129
+ const matchedFiles = await (0, glob_1.glob)(globs, {
130
+ cwd: normalizedInput,
131
+ nodir: true,
132
+ });
133
+ return matchedFiles
134
+ .filter((file) => {
135
+ if (file === 'config.yaml' || file === 'config.yml')
136
+ return false;
137
+ if (configFile && file === path.basename(configFile))
138
+ return false;
139
+ if (!file.endsWith('.yaml') && !file.endsWith('.yml'))
140
+ return false;
141
+ const pathParts = file.split(path.sep);
142
+ for (const part of pathParts) {
143
+ if (part.endsWith('.app'))
144
+ return false;
145
+ }
146
+ return true;
147
+ })
148
+ .map((file) => path.resolve(normalizedInput, file));
149
+ }
150
+ return unfilteredFlowFiles.filter((file) => !file.endsWith('config.yaml') &&
151
+ !file.endsWith('config.yml') &&
152
+ (!configFile || !file.endsWith(configFile)));
153
+ }
154
+ /**
155
+ * Resolve sequential execution order from workspace config
156
+ * @param workspaceConfig - Workspace configuration with executionOrder
157
+ * @param pathsByName - Map of flow names to their file paths
158
+ * @param debug - Whether to output debug logging
159
+ * @returns Array of flow paths in sequential execution order
160
+ */
161
+ function resolveSequentialFlows(workspaceConfig, pathsByName, debug) {
162
+ if (!workspaceConfig.executionOrder?.flowsOrder) {
163
+ return [];
164
+ }
165
+ if (debug) {
166
+ console.log('[DEBUG] executionOrder.flowsOrder:', workspaceConfig.executionOrder.flowsOrder);
167
+ console.log('[DEBUG] Available flow names:', Object.keys(pathsByName));
168
+ }
169
+ const flowsToRunInSequence = workspaceConfig.executionOrder.flowsOrder
170
+ .flatMap((flowOrder) => {
171
+ const normalizedFlowOrder = flowOrder.replace(/\.ya?ml$/i, '');
172
+ if (debug && flowOrder !== normalizedFlowOrder) {
173
+ console.log(`[DEBUG] Stripping trailing extension: "${flowOrder}" -> "${normalizedFlowOrder}"`);
174
+ }
175
+ return (0, execution_plan_utils_1.getFlowsToRunInSequence)(pathsByName, [normalizedFlowOrder], debug);
176
+ });
177
+ if (debug) {
178
+ console.log(`[DEBUG] Sequential flows resolved: ${flowsToRunInSequence.length} flow(s)`);
179
+ }
180
+ if (workspaceConfig.executionOrder.flowsOrder.length > 0 &&
181
+ flowsToRunInSequence.length === 0) {
182
+ console.warn(`Warning: executionOrder specified ${workspaceConfig.executionOrder.flowsOrder.length} flow(s) but none were found.\n` +
183
+ `This may be intentional if flows were excluded by tags.\n\n` +
184
+ `Expected flows: ${workspaceConfig.executionOrder.flowsOrder.join(', ')}\n` +
185
+ `Available flow names: ${Object.keys(pathsByName).join(', ')}\n\n` +
186
+ `Hint: Flow names come from either the 'name' field in the flow config or the filename without extension.`);
187
+ }
188
+ return flowsToRunInSequence;
189
+ }
92
190
  /**
93
191
  * Generate execution plan for test flows
94
192
  *
@@ -113,24 +211,7 @@ async function plan(options) {
113
211
  throw new Error(`Flow path does not exist: ${path.resolve(normalizedInput)}`);
114
212
  }
115
213
  if (fs.lstatSync(normalizedInput).isFile()) {
116
- if (normalizedInput.endsWith('config.yaml') ||
117
- normalizedInput.endsWith('config.yml')) {
118
- throw new Error('If using config.yaml, pass the workspace folder path, not the config file or a custom path via --config');
119
- }
120
- const { config } = (0, execution_plan_utils_1.readTestYamlFileAsJson)(normalizedInput);
121
- const flowOverrides = {};
122
- if (config) {
123
- flowMetadata[normalizedInput] = config;
124
- flowOverrides[normalizedInput] = extractDeviceCloudOverrides(config);
125
- }
126
- const checkedDependancies = await checkDependencies(normalizedInput);
127
- return {
128
- flowMetadata,
129
- flowOverrides,
130
- flowsToRun: [normalizedInput],
131
- referencedFiles: [...new Set(checkedDependancies)],
132
- totalFlowFiles: 1,
133
- };
214
+ return planSingleFile(normalizedInput);
134
215
  }
135
216
  let unfilteredFlowFiles = await (0, execution_plan_utils_1.readDirectory)(normalizedInput, execution_plan_utils_1.isFlowFile);
136
217
  if (unfilteredFlowFiles.length === 0) {
@@ -148,70 +229,26 @@ async function plan(options) {
148
229
  else {
149
230
  workspaceConfig = getWorkspaceConfig(normalizedInput, unfilteredFlowFiles);
150
231
  }
151
- if (workspaceConfig.flows) {
152
- const globs = workspaceConfig.flows.map((glob) => glob);
153
- const matchedFiles = await (0, glob_1.glob)(globs, {
154
- cwd: normalizedInput,
155
- nodir: true,
156
- });
157
- // overwrite the list of files with the globbed ones
158
- unfilteredFlowFiles = matchedFiles
159
- .filter((file) => {
160
- // Exclude config files
161
- if (file === 'config.yaml' || file === 'config.yml') {
162
- return false;
163
- }
164
- // Exclude config file if specified
165
- if (configFile && file === path.basename(configFile)) {
166
- return false;
167
- }
168
- // Check file extension
169
- if (!file.endsWith('.yaml') && !file.endsWith('.yml')) {
170
- return false;
171
- }
172
- // Exclude files inside .app bundles
173
- // Check if any directory in the path ends with .app
174
- const pathParts = file.split(path.sep);
175
- for (const part of pathParts) {
176
- if (part.endsWith('.app')) {
177
- return false;
178
- }
179
- }
180
- return true;
181
- })
182
- .map((file) => path.resolve(normalizedInput, file));
183
- }
184
- else {
185
- // workspace config has no flows, so we need to remove the config file from the test list
186
- unfilteredFlowFiles = unfilteredFlowFiles.filter((file) => !file.endsWith('config.yaml') &&
187
- !file.endsWith('config.yml') &&
188
- (!configFile || !file.endsWith(configFile)));
189
- }
232
+ unfilteredFlowFiles = await applyFlowGlobs(workspaceConfig, normalizedInput, unfilteredFlowFiles, configFile);
190
233
  if (unfilteredFlowFiles.length === 0) {
191
234
  const error = workspaceConfig.flows
192
235
  ? new Error(`Flow inclusion pattern(s) did not match any Flow files:\n${workspaceConfig.flows.join('\n')}`)
193
236
  : new Error(`Workspace does not contain any Flows: ${path.resolve(normalizedInput)}`);
194
237
  throw error;
195
238
  }
196
- const configPerFlowFile =
197
239
  // eslint-disable-next-line unicorn/no-array-reduce
198
- unfilteredFlowFiles.reduce((acc, filePath) => {
240
+ const configPerFlowFile = unfilteredFlowFiles.reduce((acc, filePath) => {
199
241
  const { config } = (0, execution_plan_utils_1.readTestYamlFileAsJson)(filePath);
200
242
  acc[filePath] = config;
201
243
  return acc;
202
244
  }, {});
203
- const allIncludeTags = [
204
- ...includeTags,
205
- ...(workspaceConfig.includeTags || []),
206
- ];
207
- const allExcludeTags = [
208
- ...excludeTags,
209
- ...(workspaceConfig.excludeTags || []),
210
- ];
245
+ const allIncludeTags = [...includeTags, ...(workspaceConfig.includeTags || [])];
246
+ const allExcludeTags = [...excludeTags, ...(workspaceConfig.excludeTags || [])];
211
247
  const flowOverrides = {};
212
248
  const allFlows = unfilteredFlowFiles.filter((filePath) => {
213
249
  const config = configPerFlowFile[filePath];
214
- const tags = config?.tags || [];
250
+ const rawTags = config?.tags;
251
+ const tags = Array.isArray(rawTags) ? rawTags : (rawTags ? [rawTags] : []);
215
252
  if (config) {
216
253
  flowMetadata[filePath] = config;
217
254
  flowOverrides[filePath] = extractDeviceCloudOverrides(config);
@@ -224,7 +261,6 @@ async function plan(options) {
224
261
  if (allFlows.length === 0) {
225
262
  throw new Error(`Include / Exclude tags did not match any Flows:\n\nInclude Tags:\n${allIncludeTags.join('\n')}\n\nExclude Tags:\n${allExcludeTags.join('\n')}`);
226
263
  }
227
- // Check dependencies only for the filtered flows
228
264
  const allFiles = await Promise.all(allFlows.map((filePath) => checkDependencies(filePath))).then((results) => [...new Set(results.flat())]);
229
265
  // eslint-disable-next-line unicorn/no-array-reduce
230
266
  const pathsByName = allFlows.reduce((acc, filePath) => {
@@ -236,35 +272,7 @@ async function plan(options) {
236
272
  }
237
273
  return acc;
238
274
  }, {});
239
- if (debug && workspaceConfig.executionOrder?.flowsOrder) {
240
- console.log('[DEBUG] executionOrder.flowsOrder:', workspaceConfig.executionOrder.flowsOrder);
241
- console.log('[DEBUG] Available flow names:', Object.keys(pathsByName));
242
- }
243
- const flowsToRunInSequence = workspaceConfig.executionOrder?.flowsOrder
244
- ?.map((flowOrder) => {
245
- // Strip .yaml/.yml extension only from the END of the string
246
- // This supports flowsOrder entries like "my_test.yml" matching "my_test"
247
- // while preserving extensions in the middle like "(file.yml) Name"
248
- const normalizedFlowOrder = flowOrder.replace(/\.ya?ml$/i, '');
249
- if (debug && flowOrder !== normalizedFlowOrder) {
250
- console.log(`[DEBUG] Stripping trailing extension: "${flowOrder}" -> "${normalizedFlowOrder}"`);
251
- }
252
- return (0, execution_plan_utils_1.getFlowsToRunInSequence)(pathsByName, [normalizedFlowOrder], debug);
253
- })
254
- .flat() || [];
255
- if (debug) {
256
- console.log(`[DEBUG] Sequential flows resolved: ${flowsToRunInSequence.length} flow(s)`);
257
- }
258
- // Validate that all specified flows were found
259
- if (workspaceConfig.executionOrder?.flowsOrder &&
260
- workspaceConfig.executionOrder.flowsOrder.length > 0 &&
261
- flowsToRunInSequence.length === 0) {
262
- console.warn(`Warning: executionOrder specified ${workspaceConfig.executionOrder.flowsOrder.length} flow(s) but none were found.\n` +
263
- `This may be intentional if flows were excluded by tags.\n\n` +
264
- `Expected flows: ${workspaceConfig.executionOrder.flowsOrder.join(', ')}\n` +
265
- `Available flow names: ${Object.keys(pathsByName).join(', ')}\n\n` +
266
- `Hint: Flow names come from either the 'name' field in the flow config or the filename without extension.`);
267
- }
275
+ const flowsToRunInSequence = resolveSequentialFlows(workspaceConfig, pathsByName, debug);
268
276
  const normalFlows = allFlows
269
277
  .filter((flow) => !flowsToRunInSequence.includes(flow))
270
278
  .sort((a, b) => a.localeCompare(b));
@@ -17,14 +17,28 @@ export interface PollingOptions {
17
17
  quiet?: boolean;
18
18
  uploadId: string;
19
19
  }
20
+ /** Metadata for a test flow extracted from YAML config */
21
+ export interface TestMetadata {
22
+ /** Flow name from YAML config 'name' field or filename without extension */
23
+ flowName: string;
24
+ /** Tags from YAML config 'tags' field */
25
+ tags: string[];
26
+ }
20
27
  export interface PollingResult {
21
28
  consoleUrl: string;
22
29
  status: 'FAILED' | 'PASSED';
23
30
  tests: Array<{
24
31
  durationSeconds: null | number;
25
32
  failReason?: string;
33
+ /** File path of the test (same as name, for clarity) */
34
+ fileName: string;
35
+ /** Flow name from YAML config or filename without extension */
36
+ flowName: string;
37
+ /** Test file name (unchanged for backwards compatibility) */
26
38
  name: string;
27
39
  status: string;
40
+ /** Tags from YAML config (empty array if none) */
41
+ tags: string[];
28
42
  }>;
29
43
  uploadId: string;
30
44
  }
@@ -38,9 +52,10 @@ export declare class ResultsPollingService {
38
52
  * Poll for test results until all tests complete
39
53
  * @param results Initial test results from submission
40
54
  * @param options Polling configuration
55
+ * @param testMetadata Optional metadata map for each test (flowName, tags)
41
56
  * @returns Promise that resolves with final test results or rejects if tests fail
42
57
  */
43
- pollUntilComplete(results: TestResult[], options: PollingOptions): Promise<PollingResult>;
58
+ pollUntilComplete(results: TestResult[], options: PollingOptions, testMetadata?: Record<string, TestMetadata>): Promise<PollingResult>;
44
59
  private buildPollingResult;
45
60
  private calculateStatusSummary;
46
61
  private displayFinalResults;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ResultsPollingService = exports.RunFailedError = void 0;
4
4
  const core_1 = require("@oclif/core");
5
5
  const cli_ux_1 = require("@oclif/core/lib/cli-ux");
6
+ const path = require("node:path");
6
7
  const api_gateway_1 = require("../gateways/api-gateway");
7
8
  const methods_1 = require("../methods");
8
9
  const connectivity_1 = require("../utils/connectivity");
@@ -24,14 +25,15 @@ exports.RunFailedError = RunFailedError;
24
25
  */
25
26
  class ResultsPollingService {
26
27
  MAX_SEQUENTIAL_FAILURES = 10;
27
- POLL_INTERVAL_MS = 5000;
28
+ POLL_INTERVAL_MS = 10_000;
28
29
  /**
29
30
  * Poll for test results until all tests complete
30
31
  * @param results Initial test results from submission
31
32
  * @param options Polling configuration
33
+ * @param testMetadata Optional metadata map for each test (flowName, tags)
32
34
  * @returns Promise that resolves with final test results or rejects if tests fail
33
35
  */
34
- async pollUntilComplete(results, options) {
36
+ async pollUntilComplete(results, options, testMetadata) {
35
37
  const { apiUrl, apiKey, uploadId, consoleUrl, quiet = false, json = false, debug = false, logger } = options;
36
38
  this.initializePollingDisplay(json, logger);
37
39
  let sequentialPollFailures = 0;
@@ -53,6 +55,7 @@ class ResultsPollingService {
53
55
  debug,
54
56
  json,
55
57
  logger,
58
+ testMetadata,
56
59
  uploadId,
57
60
  });
58
61
  }
@@ -74,7 +77,7 @@ class ResultsPollingService {
74
77
  }
75
78
  }
76
79
  }
77
- buildPollingResult(results, uploadId, consoleUrl) {
80
+ buildPollingResult(results, uploadId, consoleUrl, testMetadata) {
78
81
  const resultsWithoutEarlierTries = this.filterLatestResults(results);
79
82
  return {
80
83
  consoleUrl,
@@ -84,8 +87,11 @@ class ResultsPollingService {
84
87
  tests: resultsWithoutEarlierTries.map((r) => ({
85
88
  durationSeconds: r.duration_seconds,
86
89
  failReason: r.status === 'FAILED' ? r.fail_reason || 'No reason provided' : undefined,
90
+ fileName: r.test_file_name,
91
+ flowName: testMetadata?.[r.test_file_name]?.flowName || path.parse(r.test_file_name).name,
87
92
  name: r.test_file_name,
88
93
  status: r.status,
94
+ tags: testMetadata?.[r.test_file_name]?.tags || [],
89
95
  })),
90
96
  uploadId,
91
97
  };
@@ -219,12 +225,12 @@ class ResultsPollingService {
219
225
  * @returns Promise resolving to final polling result
220
226
  */
221
227
  async handleCompletedTests(updatedResults, options) {
222
- const { uploadId, consoleUrl, json, debug, logger } = options;
228
+ const { uploadId, consoleUrl, json, debug, logger, testMetadata } = options;
223
229
  if (debug && logger) {
224
230
  logger(`[DEBUG] All tests completed, stopping poll`);
225
231
  }
226
232
  this.displayFinalResults(updatedResults, consoleUrl, json, logger);
227
- const output = this.buildPollingResult(updatedResults, uploadId, consoleUrl);
233
+ const output = this.buildPollingResult(updatedResults, uploadId, consoleUrl, testMetadata);
228
234
  if (output.status === 'FAILED') {
229
235
  if (debug && logger) {
230
236
  logger(`[DEBUG] Some tests failed, returning failed status`);
@@ -2,6 +2,7 @@ import { IExecutionPlan } from './execution-plan.service';
2
2
  export interface TestSubmissionConfig {
3
3
  androidApiLevel?: string;
4
4
  androidDevice?: string;
5
+ androidNoSnapshot?: boolean;
5
6
  appBinaryId: string;
6
7
  cliVersion: string;
7
8
  commonRoot: string;
@@ -17,7 +17,7 @@ class TestSubmissionService {
17
17
  * @returns FormData ready to be submitted to the API
18
18
  */
19
19
  async buildTestFormData(config) {
20
- const { appBinaryId, flowFile, executionPlan, commonRoot, cliVersion, env = [], metadata = [], googlePlay = false, androidApiLevel, androidDevice, iOSVersion, iOSDevice, name, runnerType, maestroVersion, deviceLocale, orientation, mitmHost, mitmPath, retry, continueOnFailure = true, report, showCrosshairs, maestroChromeOnboarding, raw, debug = false, logger, } = config;
20
+ const { appBinaryId, flowFile, executionPlan, commonRoot, cliVersion, env = [], metadata = [], googlePlay = false, androidApiLevel, androidDevice, androidNoSnapshot, iOSVersion, iOSDevice, name, runnerType, maestroVersion, deviceLocale, orientation, mitmHost, mitmPath, retry, continueOnFailure = true, report, showCrosshairs, maestroChromeOnboarding, raw, debug = false, logger, } = config;
21
21
  const { allExcludeTags, allIncludeTags, flowMetadata, flowOverrides, flowsToRun: testFileNames, referencedFiles, sequence, workspaceConfig, } = executionPlan;
22
22
  const { flows: sequentialFlows = [] } = sequence ?? {};
23
23
  const testFormData = new FormData();
@@ -69,6 +69,7 @@ class TestSubmissionService {
69
69
  const configPayload = {
70
70
  allExcludeTags,
71
71
  allIncludeTags,
72
+ androidNoSnapshot,
72
73
  autoRetriesRemaining: retry,
73
74
  continueOnFailure,
74
75
  deviceLocale,
@@ -343,7 +343,7 @@ export interface paths {
343
343
  patch?: never;
344
344
  trace?: never;
345
345
  };
346
- "/org/increase_credits": {
346
+ "/org/paddle-webhook": {
347
347
  parameters: {
348
348
  query?: never;
349
349
  header?: never;
@@ -352,7 +352,7 @@ export interface paths {
352
352
  };
353
353
  get?: never;
354
354
  put?: never;
355
- post: operations["OrgController_paddleTransactionCompleted"];
355
+ post: operations["OrgController_handlePaddleWebhook"];
356
356
  delete?: never;
357
357
  options?: never;
358
358
  head?: never;
@@ -407,6 +407,54 @@ export interface paths {
407
407
  patch?: never;
408
408
  trace?: never;
409
409
  };
410
+ "/org/subscriptions": {
411
+ parameters: {
412
+ query?: never;
413
+ header?: never;
414
+ path?: never;
415
+ cookie?: never;
416
+ };
417
+ get?: never;
418
+ put?: never;
419
+ post: operations["OrgController_getAllSubscriptions"];
420
+ delete?: never;
421
+ options?: never;
422
+ head?: never;
423
+ patch?: never;
424
+ trace?: never;
425
+ };
426
+ "/org/update-overage-limit": {
427
+ parameters: {
428
+ query?: never;
429
+ header?: never;
430
+ path?: never;
431
+ cookie?: never;
432
+ };
433
+ get?: never;
434
+ put?: never;
435
+ post: operations["OrgController_updateOverageLimit"];
436
+ delete?: never;
437
+ options?: never;
438
+ head?: never;
439
+ patch?: never;
440
+ trace?: never;
441
+ };
442
+ "/org/usage-history": {
443
+ parameters: {
444
+ query?: never;
445
+ header?: never;
446
+ path?: never;
447
+ cookie?: never;
448
+ };
449
+ get?: never;
450
+ put?: never;
451
+ post: operations["OrgController_getUsageHistory"];
452
+ delete?: never;
453
+ options?: never;
454
+ head?: never;
455
+ patch?: never;
456
+ trace?: never;
457
+ };
410
458
  "/frontend/check-domain-saml": {
411
459
  parameters: {
412
460
  query?: never;
@@ -439,6 +487,22 @@ export interface paths {
439
487
  patch?: never;
440
488
  trace?: never;
441
489
  };
490
+ "/billing/create-subscription": {
491
+ parameters: {
492
+ query?: never;
493
+ header?: never;
494
+ path?: never;
495
+ cookie?: never;
496
+ };
497
+ get?: never;
498
+ put?: never;
499
+ post: operations["BillingController_createSubscription"];
500
+ delete?: never;
501
+ options?: never;
502
+ head?: never;
503
+ patch?: never;
504
+ trace?: never;
505
+ };
442
506
  }
443
507
  export type webhooks = Record<string, never>;
444
508
  export interface components {
@@ -457,11 +521,15 @@ export interface components {
457
521
  test_upload_id: string;
458
522
  };
459
523
  IGetBinaryUploadUrlArgs: {
524
+ /**
525
+ * @description Platform for the binary upload (ios or android)
526
+ * @enum {string}
527
+ */
528
+ platform: "ios" | "android";
460
529
  /** @description File size in bytes (optional, for Backblaze upload strategy) */
461
530
  fileSize?: number;
462
531
  /** @description Whether client uses TUS resumable uploads (true for new clients, undefined/false for legacy) */
463
532
  useTus?: boolean;
464
- platform: Record<string, never>;
465
533
  };
466
534
  B2SimpleUpload: {
467
535
  uploadUrl: string;
@@ -497,6 +565,7 @@ export interface components {
497
565
  token?: string;
498
566
  };
499
567
  ICheckForExistingUploadArgs: {
568
+ /** @description SHA-256 hash of the binary file */
500
569
  sha: string;
501
570
  };
502
571
  ICheckForExistingUploadResponse: {
@@ -504,6 +573,10 @@ export interface components {
504
573
  exists: boolean;
505
574
  };
506
575
  IFinaliseUploadArgs: {
576
+ /** @description Unique upload identifier */
577
+ id: string;
578
+ /** @description Storage path for the uploaded file */
579
+ path: string;
507
580
  /** @description File metadata (bundle ID, package name, platform) - required for new clients */
508
581
  metadata?: Record<string, never>;
509
582
  /** @description SHA-256 hash of the file - required for new clients */
@@ -520,8 +593,6 @@ export interface components {
520
593
  backblazeSuccess: boolean;
521
594
  /** @description Whether client uses TUS resumable uploads (true for new clients, undefined/false for legacy) */
522
595
  useTus?: boolean;
523
- id: string;
524
- path: string;
525
596
  };
526
597
  IFinaliseUploadResponse: Record<string, never>;
527
598
  IFinishLargeFileArgs: {
@@ -549,18 +620,25 @@ export interface components {
549
620
  * @description This file must be a zip file
550
621
  */
551
622
  file: string;
623
+ testFileNames?: string;
624
+ sequentialFlows?: string;
552
625
  /** @enum {string} */
553
626
  androidApiLevel?: "29" | "30" | "31" | "32" | "33" | "34" | "35" | "36";
554
627
  /** @enum {string} */
555
628
  androidDevice?: "pixel-6" | "pixel-6-pro" | "pixel-7" | "pixel-7-pro" | "generic-tablet";
556
629
  apiKey?: string;
557
630
  apiUrl?: string;
631
+ appBinaryId: string;
558
632
  appFile?: string;
633
+ env: string;
559
634
  /** @enum {string} */
560
635
  iOSVersion?: "16" | "17" | "18" | "26";
561
636
  /** @enum {string} */
562
637
  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";
563
638
  platform?: string;
639
+ googlePlay: boolean;
640
+ config: string;
641
+ name?: string;
564
642
  /** @enum {string} */
565
643
  runnerType?: "m4" | "m1" | "default" | "gpu1" | "cpu1";
566
644
  metadata?: string;
@@ -569,13 +647,6 @@ export interface components {
569
647
  testFileOverrides?: string;
570
648
  /** @description SHA-256 hash of the flow ZIP file */
571
649
  sha?: string;
572
- testFileNames?: string;
573
- sequentialFlows?: string;
574
- appBinaryId: string;
575
- env: string;
576
- googlePlay: boolean;
577
- config: string;
578
- name: string;
579
650
  };
580
651
  IRetryTestArgs: {
581
652
  resultId: number;
@@ -1302,10 +1373,11 @@ export interface operations {
1302
1373
  * "2.0.2",
1303
1374
  * "2.0.3",
1304
1375
  * "2.0.4",
1305
- * "2.0.9"
1376
+ * "2.0.9",
1377
+ * "2.1.0"
1306
1378
  * ],
1307
1379
  * "defaultVersion": "1.41.0",
1308
- * "latestVersion": "2.0.9"
1380
+ * "latestVersion": "2.1.0"
1309
1381
  * }
1310
1382
  * }
1311
1383
  * }
@@ -1499,7 +1571,7 @@ export interface operations {
1499
1571
  };
1500
1572
  };
1501
1573
  };
1502
- OrgController_paddleTransactionCompleted: {
1574
+ OrgController_handlePaddleWebhook: {
1503
1575
  parameters: {
1504
1576
  query?: never;
1505
1577
  header: {
@@ -1510,7 +1582,7 @@ export interface operations {
1510
1582
  };
1511
1583
  requestBody?: never;
1512
1584
  responses: {
1513
- /** @description Success. */
1585
+ /** @description Paddle webhook handler. */
1514
1586
  201: {
1515
1587
  headers: {
1516
1588
  [name: string]: unknown;
@@ -1605,6 +1677,97 @@ export interface operations {
1605
1677
  };
1606
1678
  };
1607
1679
  };
1680
+ OrgController_getAllSubscriptions: {
1681
+ parameters: {
1682
+ query?: never;
1683
+ header: {
1684
+ "x-app-api-key": string;
1685
+ };
1686
+ path?: never;
1687
+ cookie?: never;
1688
+ };
1689
+ requestBody: {
1690
+ content: {
1691
+ "application/json": {
1692
+ orgId: string;
1693
+ };
1694
+ };
1695
+ };
1696
+ responses: {
1697
+ /** @description All subscription data fetched successfully. */
1698
+ 201: {
1699
+ headers: {
1700
+ [name: string]: unknown;
1701
+ };
1702
+ content: {
1703
+ "application/json": Record<string, never>;
1704
+ };
1705
+ };
1706
+ };
1707
+ };
1708
+ OrgController_updateOverageLimit: {
1709
+ parameters: {
1710
+ query?: never;
1711
+ header: {
1712
+ "x-app-api-key": string;
1713
+ };
1714
+ path?: never;
1715
+ cookie?: never;
1716
+ };
1717
+ requestBody: {
1718
+ content: {
1719
+ "application/json": {
1720
+ orgId: string;
1721
+ overageLimit: number;
1722
+ };
1723
+ };
1724
+ };
1725
+ responses: {
1726
+ /** @description Overage limit updated successfully. */
1727
+ 201: {
1728
+ headers: {
1729
+ [name: string]: unknown;
1730
+ };
1731
+ content: {
1732
+ "application/json": Record<string, never>;
1733
+ };
1734
+ };
1735
+ };
1736
+ };
1737
+ OrgController_getUsageHistory: {
1738
+ parameters: {
1739
+ query?: never;
1740
+ header: {
1741
+ "x-app-api-key": string;
1742
+ };
1743
+ path?: never;
1744
+ cookie?: never;
1745
+ };
1746
+ requestBody: {
1747
+ content: {
1748
+ "application/json": {
1749
+ orgId: string;
1750
+ /** @enum {string} */
1751
+ format?: "json" | "csv";
1752
+ /** Format: date-time */
1753
+ startDate?: string;
1754
+ /** Format: date-time */
1755
+ endDate?: string;
1756
+ };
1757
+ };
1758
+ };
1759
+ responses: {
1760
+ /** @description Usage history fetched successfully. */
1761
+ 201: {
1762
+ headers: {
1763
+ [name: string]: unknown;
1764
+ };
1765
+ content: {
1766
+ "application/json": unknown[];
1767
+ };
1768
+ };
1769
+ };
1770
+ };
1608
1771
  FrontendController_checkDomainSaml: {
1609
1772
  parameters: {
1610
1773
  query?: never;
@@ -1680,4 +1843,41 @@ export interface operations {
1680
1843
  };
1681
1844
  };
1682
1845
  };
1846
+ BillingController_createSubscription: {
1847
+ parameters: {
1848
+ query?: never;
1849
+ header?: never;
1850
+ path?: never;
1851
+ cookie?: never;
1852
+ };
1853
+ requestBody: {
1854
+ content: {
1855
+ "application/json": {
1856
+ items: {
1857
+ price_id?: string;
1858
+ quantity?: number;
1859
+ }[];
1860
+ customer_email?: string;
1861
+ custom_data?: {
1862
+ orgId?: string;
1863
+ userId?: string;
1864
+ };
1865
+ billing_details?: {
1866
+ enable_checkout?: boolean;
1867
+ };
1868
+ };
1869
+ };
1870
+ };
1871
+ responses: {
1872
+ /** @description Subscription created successfully. */
1873
+ 201: {
1874
+ headers: {
1875
+ [name: string]: unknown;
1876
+ };
1877
+ content: {
1878
+ "application/json": Record<string, never>;
1879
+ };
1880
+ };
1881
+ };
1882
+ };
1683
1883
  }
@@ -175,6 +175,12 @@
175
175
  "allowNo": false,
176
176
  "type": "boolean"
177
177
  },
178
+ "android-no-snapshot": {
179
+ "description": "[Android only] Force cold boot instead of using snapshot boot. This is automatically enabled for API 35+ but can be used to force cold boot on older API levels.",
180
+ "name": "android-no-snapshot",
181
+ "allowNo": false,
182
+ "type": "boolean"
183
+ },
178
184
  "env": {
179
185
  "char": "e",
180
186
  "description": "One or more environment variables to inject into your flows",
@@ -667,5 +673,5 @@
667
673
  ]
668
674
  }
669
675
  },
670
- "version": "4.2.4"
676
+ "version": "4.2.6"
671
677
  }
package/package.json CHANGED
@@ -67,7 +67,7 @@
67
67
  "type": "git",
68
68
  "url": "https://devicecloud.dev"
69
69
  },
70
- "version": "4.2.4",
70
+ "version": "4.2.6",
71
71
  "bugs": {
72
72
  "url": "https://discord.gg/gm3mJwcNw8"
73
73
  },