@devicecloud.dev/dcd 4.4.9 → 5.0.0-beta.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.
Files changed (130) hide show
  1. package/README.md +75 -2
  2. package/dist/commands/artifacts.d.ts +47 -18
  3. package/dist/commands/artifacts.js +69 -64
  4. package/dist/commands/cloud.d.ts +228 -88
  5. package/dist/commands/cloud.js +430 -342
  6. package/dist/commands/list.d.ts +39 -38
  7. package/dist/commands/list.js +124 -131
  8. package/dist/commands/live.d.ts +2 -0
  9. package/dist/commands/live.js +520 -0
  10. package/dist/commands/login.d.ts +17 -0
  11. package/dist/commands/login.js +252 -0
  12. package/dist/commands/logout.d.ts +2 -0
  13. package/dist/commands/logout.js +30 -0
  14. package/dist/commands/status.d.ts +23 -42
  15. package/dist/commands/status.js +170 -179
  16. package/dist/commands/switch-org.d.ts +12 -0
  17. package/dist/commands/switch-org.js +76 -0
  18. package/dist/commands/upgrade.d.ts +2 -0
  19. package/dist/commands/upgrade.js +120 -0
  20. package/dist/commands/upload.d.ts +33 -18
  21. package/dist/commands/upload.js +72 -78
  22. package/dist/commands/whoami.d.ts +2 -0
  23. package/dist/commands/whoami.js +31 -0
  24. package/dist/config/environments.d.ts +31 -0
  25. package/dist/config/environments.js +52 -0
  26. package/dist/config/flags/api.flags.d.ts +10 -2
  27. package/dist/config/flags/api.flags.js +13 -14
  28. package/dist/config/flags/binary.flags.d.ts +17 -4
  29. package/dist/config/flags/binary.flags.js +14 -18
  30. package/dist/config/flags/device.flags.d.ts +49 -11
  31. package/dist/config/flags/device.flags.js +43 -38
  32. package/dist/config/flags/environment.flags.d.ts +27 -6
  33. package/dist/config/flags/environment.flags.js +24 -29
  34. package/dist/config/flags/execution.flags.d.ts +35 -8
  35. package/dist/config/flags/execution.flags.js +31 -41
  36. package/dist/config/flags/github.flags.d.ts +23 -5
  37. package/dist/config/flags/github.flags.js +19 -15
  38. package/dist/config/flags/output.flags.d.ts +57 -13
  39. package/dist/config/flags/output.flags.js +48 -47
  40. package/dist/constants.d.ts +218 -51
  41. package/dist/constants.js +17 -20
  42. package/dist/gateways/api-gateway.d.ts +72 -16
  43. package/dist/gateways/api-gateway.js +298 -104
  44. package/dist/gateways/cli-auth-gateway.d.ts +13 -0
  45. package/dist/gateways/cli-auth-gateway.js +54 -0
  46. package/dist/gateways/realtime-gateway.d.ts +32 -0
  47. package/dist/gateways/realtime-gateway.js +103 -0
  48. package/dist/gateways/supabase-gateway.d.ts +11 -11
  49. package/dist/gateways/supabase-gateway.js +20 -48
  50. package/dist/index.d.ts +2 -1
  51. package/dist/index.js +98 -4
  52. package/dist/mcp/context.d.ts +33 -0
  53. package/dist/mcp/context.js +33 -0
  54. package/dist/mcp/helpers.d.ts +16 -0
  55. package/dist/mcp/helpers.js +34 -0
  56. package/dist/mcp/index.d.ts +2 -0
  57. package/dist/mcp/index.js +24 -0
  58. package/dist/mcp/server.d.ts +7 -0
  59. package/dist/mcp/server.js +27 -0
  60. package/dist/mcp/tools/download-artifacts.d.ts +11 -0
  61. package/dist/mcp/tools/download-artifacts.js +84 -0
  62. package/dist/mcp/tools/get-status.d.ts +7 -0
  63. package/dist/mcp/tools/get-status.js +39 -0
  64. package/dist/mcp/tools/list-devices.d.ts +7 -0
  65. package/dist/mcp/tools/list-devices.js +27 -0
  66. package/dist/mcp/tools/list-runs.d.ts +3 -0
  67. package/dist/mcp/tools/list-runs.js +60 -0
  68. package/dist/mcp/tools/run-cloud-test.d.ts +14 -0
  69. package/dist/mcp/tools/run-cloud-test.js +233 -0
  70. package/dist/methods.d.ts +34 -5
  71. package/dist/methods.js +266 -215
  72. package/dist/services/device-validation.service.d.ts +9 -1
  73. package/dist/services/device-validation.service.js +56 -40
  74. package/dist/services/execution-plan.service.js +40 -31
  75. package/dist/services/execution-plan.utils.d.ts +3 -0
  76. package/dist/services/execution-plan.utils.js +25 -55
  77. package/dist/services/flow-paths.d.ts +17 -0
  78. package/dist/services/flow-paths.js +52 -0
  79. package/dist/services/metadata-extractor.service.d.ts +0 -2
  80. package/dist/services/metadata-extractor.service.js +75 -78
  81. package/dist/services/moropo.service.js +33 -34
  82. package/dist/services/report-download.service.d.ts +12 -1
  83. package/dist/services/report-download.service.js +34 -27
  84. package/dist/services/results-polling.service.d.ts +23 -9
  85. package/dist/services/results-polling.service.js +257 -123
  86. package/dist/services/telemetry.service.d.ts +49 -0
  87. package/dist/services/telemetry.service.js +252 -0
  88. package/dist/services/test-submission.service.d.ts +21 -4
  89. package/dist/services/test-submission.service.js +51 -33
  90. package/dist/services/version.service.d.ts +4 -3
  91. package/dist/services/version.service.js +28 -16
  92. package/dist/types/domain/auth.types.d.ts +20 -0
  93. package/dist/types/domain/auth.types.js +1 -0
  94. package/dist/types/domain/device.types.js +8 -11
  95. package/dist/types/domain/live.types.d.ts +76 -0
  96. package/dist/types/domain/live.types.js +3 -0
  97. package/dist/types/generated/schema.types.js +1 -2
  98. package/dist/types/index.d.ts +2 -2
  99. package/dist/types/index.js +2 -18
  100. package/dist/types.js +1 -2
  101. package/dist/utils/auth.d.ts +13 -0
  102. package/dist/utils/auth.js +141 -0
  103. package/dist/utils/ci.d.ts +12 -0
  104. package/dist/utils/ci.js +39 -0
  105. package/dist/utils/cli.d.ts +35 -0
  106. package/dist/utils/cli.js +118 -0
  107. package/dist/utils/compatibility.d.ts +2 -1
  108. package/dist/utils/compatibility.js +6 -8
  109. package/dist/utils/config-store.d.ts +35 -0
  110. package/dist/utils/config-store.js +115 -0
  111. package/dist/utils/connectivity.js +8 -7
  112. package/dist/utils/expo.js +29 -24
  113. package/dist/utils/orgs.d.ts +11 -0
  114. package/dist/utils/orgs.js +36 -0
  115. package/dist/utils/paths.d.ts +11 -0
  116. package/dist/utils/paths.js +21 -0
  117. package/dist/utils/progress.d.ts +13 -0
  118. package/dist/utils/progress.js +47 -0
  119. package/dist/utils/styling.d.ts +42 -36
  120. package/dist/utils/styling.js +78 -82
  121. package/dist/utils/ui.d.ts +41 -0
  122. package/dist/utils/ui.js +95 -0
  123. package/package.json +36 -45
  124. package/bin/dev.cmd +0 -3
  125. package/bin/dev.js +0 -6
  126. package/bin/run.cmd +0 -3
  127. package/bin/run.js +0 -7
  128. package/dist/types/schema.types.d.ts +0 -2702
  129. package/dist/types/schema.types.js +0 -3
  130. package/oclif.manifest.json +0 -884
@@ -1,4 +1,4 @@
1
- import { CompatibilityData } from '../utils/compatibility';
1
+ import { CompatibilityData } from '../utils/compatibility.js';
2
2
  export interface DeviceValidationOptions {
3
3
  debug?: boolean;
4
4
  logger?: (message: string) => void;
@@ -28,4 +28,12 @@ export declare class DeviceValidationService {
28
28
  * @throws Error if device/version combination is not supported
29
29
  */
30
30
  validateiOSDevice(iOSVersion: string | undefined, iOSDevice: string | undefined, compatibilityData: CompatibilityData, options?: DeviceValidationOptions): void;
31
+ /**
32
+ * Shared validation flow for both platforms: apply the default device,
33
+ * look up its supported versions, then check the requested version
34
+ * @param config Platform-specific lookup table, defaults, and messages
35
+ * @returns void
36
+ * @throws Error if device/version combination is not supported
37
+ */
38
+ private validateDevice;
31
39
  }
@@ -1,10 +1,7 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DeviceValidationService = void 0;
4
1
  /**
5
2
  * Service for validating device configurations against compatibility data
6
3
  */
7
- class DeviceValidationService {
4
+ export class DeviceValidationService {
8
5
  /**
9
6
  * Validate Android device configuration
10
7
  * @param androidApiLevel Android API level to validate
@@ -16,29 +13,24 @@ class DeviceValidationService {
16
13
  * @throws Error if device/API level combination is not supported
17
14
  */
18
15
  validateAndroidDevice(androidApiLevel, androidDevice, googlePlay, compatibilityData, options = {}) {
19
- const { debug = false, logger } = options;
20
- if (!androidApiLevel && !androidDevice) {
21
- return;
22
- }
23
- const androidDeviceID = androidDevice || 'pixel-7';
24
- const lookup = googlePlay
25
- ? compatibilityData.androidPlay
26
- : compatibilityData.android;
27
- const supportedAndroidVersions = lookup?.[androidDeviceID] || [];
28
- const version = androidApiLevel || '34';
29
- if (supportedAndroidVersions.length === 0) {
30
- 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`);
31
- }
32
- if (Array.isArray(supportedAndroidVersions) &&
33
- !supportedAndroidVersions.includes(version)) {
34
- throw new Error(`${androidDeviceID} ${googlePlay ? '(Play Store) ' : ''}only supports these Android API levels: ${supportedAndroidVersions.join(', ')}`);
35
- }
36
- if (debug && logger) {
37
- logger(`[DEBUG] Android device: ${androidDeviceID}`);
38
- logger(`[DEBUG] Android API level: ${version}`);
39
- logger(`[DEBUG] Google Play enabled: ${googlePlay}`);
40
- logger(`[DEBUG] Supported Android versions: ${supportedAndroidVersions.join(', ')}`);
41
- }
16
+ this.validateDevice({
17
+ debugLines: (deviceID, version, supportedVersions) => [
18
+ `[DEBUG] Android device: ${deviceID}`,
19
+ `[DEBUG] Android API level: ${version}`,
20
+ `[DEBUG] Google Play enabled: ${googlePlay}`,
21
+ `[DEBUG] Supported Android versions: ${supportedVersions.join(', ')}`,
22
+ ],
23
+ defaultDevice: 'pixel-7',
24
+ defaultVersion: '34',
25
+ device: androidDevice,
26
+ lookup: googlePlay
27
+ ? compatibilityData.androidPlay
28
+ : compatibilityData.android,
29
+ noSupportMessage: () => `We don't support that device configuration - please check the docs for supported devices: https://docs.devicecloud.dev/getting-started/devices-configuration`,
30
+ options,
31
+ unsupportedVersionMessage: (deviceID, supportedVersions) => `${deviceID} ${googlePlay ? '(Play Store) ' : ''}only supports these Android API levels: ${supportedVersions.join(', ')}`,
32
+ version: androidApiLevel,
33
+ });
42
34
  }
43
35
  /**
44
36
  * Validate iOS device configuration
@@ -50,25 +42,49 @@ class DeviceValidationService {
50
42
  * @throws Error if device/version combination is not supported
51
43
  */
52
44
  validateiOSDevice(iOSVersion, iOSDevice, compatibilityData, options = {}) {
45
+ this.validateDevice({
46
+ debugLines: (deviceID, version, supportedVersions) => [
47
+ `[DEBUG] iOS device: ${deviceID}`,
48
+ `[DEBUG] iOS version: ${version}`,
49
+ `[DEBUG] Supported iOS versions: ${supportedVersions.join(', ')}`,
50
+ ],
51
+ defaultDevice: 'iphone-14',
52
+ defaultVersion: '17',
53
+ device: iOSDevice,
54
+ lookup: compatibilityData?.ios,
55
+ noSupportMessage: (deviceID) => `Device ${deviceID} is not supported. Please check the docs for supported devices: https://docs.devicecloud.dev/getting-started/devices-configuration`,
56
+ options,
57
+ unsupportedVersionMessage: (deviceID, supportedVersions) => `${deviceID} only supports these iOS versions: ${supportedVersions.join(', ')}`,
58
+ version: iOSVersion,
59
+ });
60
+ }
61
+ /**
62
+ * Shared validation flow for both platforms: apply the default device,
63
+ * look up its supported versions, then check the requested version
64
+ * @param config Platform-specific lookup table, defaults, and messages
65
+ * @returns void
66
+ * @throws Error if device/version combination is not supported
67
+ */
68
+ validateDevice(config) {
69
+ const { debugLines, defaultDevice, defaultVersion, device, lookup, noSupportMessage, options, unsupportedVersionMessage, version, } = config;
53
70
  const { debug = false, logger } = options;
54
- if (!iOSVersion && !iOSDevice) {
71
+ if (!version && !device) {
55
72
  return;
56
73
  }
57
- const iOSDeviceID = iOSDevice || 'iphone-14';
58
- const supportediOSVersions = compatibilityData?.ios?.[iOSDeviceID] || [];
59
- const version = iOSVersion || '17';
60
- if (supportediOSVersions.length === 0) {
61
- throw new Error(`Device ${iOSDeviceID} is not supported. Please check the docs for supported devices: https://docs.devicecloud.dev/getting-started/devices-configuration`);
74
+ const deviceID = device || defaultDevice;
75
+ const supportedVersions = lookup?.[deviceID] || [];
76
+ const requestedVersion = version || defaultVersion;
77
+ if (supportedVersions.length === 0) {
78
+ throw new Error(noSupportMessage(deviceID));
62
79
  }
63
- if (Array.isArray(supportediOSVersions) &&
64
- !supportediOSVersions.includes(version)) {
65
- throw new Error(`${iOSDeviceID} only supports these iOS versions: ${supportediOSVersions.join(', ')}`);
80
+ if (Array.isArray(supportedVersions) &&
81
+ !supportedVersions.includes(requestedVersion)) {
82
+ throw new Error(unsupportedVersionMessage(deviceID, supportedVersions));
66
83
  }
67
84
  if (debug && logger) {
68
- logger(`[DEBUG] iOS device: ${iOSDeviceID}`);
69
- logger(`[DEBUG] iOS version: ${version}`);
70
- logger(`[DEBUG] Supported iOS versions: ${supportediOSVersions.join(', ')}`);
85
+ for (const line of debugLines(deviceID, requestedVersion, supportedVersions)) {
86
+ logger(line);
87
+ }
71
88
  }
72
89
  }
73
90
  }
74
- exports.DeviceValidationService = DeviceValidationService;
@@ -1,10 +1,6 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.plan = plan;
4
- const glob_1 = require("glob");
5
- const fs = require("node:fs");
6
- const path = require("node:path");
7
- const execution_plan_utils_1 = require("./execution-plan.utils");
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { getFlowsToRunInSequence, isFlowFile, processDependencies, readDirectory, readTestYamlFileAsJson, readYamlFileAsJson, } from './execution-plan.utils.js';
8
4
  /**
9
5
  * Recursively check and resolve all dependencies for a flow file
10
6
  * Includes runFlow references, JavaScript scripts, and media files
@@ -17,8 +13,8 @@ async function checkDependencies(input) {
17
13
  const uncheckedDependencies = [input];
18
14
  while (uncheckedDependencies.length > 0) {
19
15
  const fileToCheck = uncheckedDependencies.shift();
20
- const { config, testSteps } = (0, execution_plan_utils_1.readTestYamlFileAsJson)(fileToCheck);
21
- const { allErrors, allFiles } = (0, execution_plan_utils_1.processDependencies)({
16
+ const { config, testSteps } = readTestYamlFileAsJson(fileToCheck);
17
+ const { allErrors, allFiles } = processDependencies({
22
18
  config,
23
19
  input: fileToCheck,
24
20
  testSteps,
@@ -28,7 +24,7 @@ async function checkDependencies(input) {
28
24
  allErrors.join('\n'));
29
25
  }
30
26
  for (const file of allFiles) {
31
- if (!(0, execution_plan_utils_1.isFlowFile)(file)) {
27
+ if (!isFlowFile(file)) {
32
28
  // js/media files don't have dependencies
33
29
  checkedDependencies.push(file);
34
30
  }
@@ -64,7 +60,7 @@ function getWorkspaceConfig(input, unfilteredFlowFiles) {
64
60
  const possibleConfigPaths = new Set([path.join(input, 'config.yaml'), path.join(input, 'config.yml')].map((p) => path.normalize(p)));
65
61
  const configFilePath = unfilteredFlowFiles.find((file) => possibleConfigPaths.has(path.normalize(file)));
66
62
  const config = configFilePath
67
- ? (0, execution_plan_utils_1.readYamlFileAsJson)(configFilePath)
63
+ ? readYamlFileAsJson(configFilePath)
68
64
  : {};
69
65
  return config;
70
66
  }
@@ -100,7 +96,7 @@ async function planSingleFile(normalizedInput, configFile) {
100
96
  normalizedInput.endsWith('config.yml')) {
101
97
  throw new Error('If using config.yaml, pass the workspace folder path, not the config file or a custom path via --config');
102
98
  }
103
- const { config } = (0, execution_plan_utils_1.readTestYamlFileAsJson)(normalizedInput);
99
+ const { config } = readTestYamlFileAsJson(normalizedInput);
104
100
  const flowMetadata = {};
105
101
  const flowOverrides = {};
106
102
  if (config) {
@@ -113,7 +109,7 @@ async function planSingleFile(normalizedInput, configFile) {
113
109
  if (!fs.existsSync(configFilePath)) {
114
110
  throw new Error(`Config file does not exist: ${configFilePath}`);
115
111
  }
116
- workspaceConfig = (0, execution_plan_utils_1.readYamlFileAsJson)(configFilePath);
112
+ workspaceConfig = readYamlFileAsJson(configFilePath);
117
113
  }
118
114
  const checkedDependancies = await checkDependencies(normalizedInput);
119
115
  return {
@@ -131,16 +127,24 @@ async function planSingleFile(normalizedInput, configFile) {
131
127
  * @param normalizedInput - Normalized path to the workspace directory
132
128
  * @param unfilteredFlowFiles - List of all discovered flow files
133
129
  * @param configFile - Optional custom config file path
130
+ * @param excludeFlows - --exclude-flows patterns to re-apply to glob matches
134
131
  * @returns Filtered list of flow file paths matching the globs
135
132
  */
136
- async function applyFlowGlobs(workspaceConfig, normalizedInput, unfilteredFlowFiles, configFile) {
133
+ async function applyFlowGlobs(workspaceConfig, normalizedInput, unfilteredFlowFiles, configFile, excludeFlows) {
137
134
  if (workspaceConfig.flows) {
138
135
  const globs = workspaceConfig.flows.map((g) => g);
139
- const matchedFiles = await (0, glob_1.glob)(globs, {
140
- cwd: normalizedInput,
141
- nodir: true,
136
+ // fs.globSync lands in Node 22; the CLI's `engines.node` already requires it.
137
+ // No `nodir` option — we strip directories with a stat check below.
138
+ const allMatches = fs.globSync(globs, { cwd: normalizedInput });
139
+ const matchedFiles = allMatches.filter((file) => {
140
+ try {
141
+ return fs.statSync(path.resolve(normalizedInput, file)).isFile();
142
+ }
143
+ catch {
144
+ return false;
145
+ }
142
146
  });
143
- return matchedFiles
147
+ const globbedFlowFiles = matchedFiles
144
148
  .filter((file) => {
145
149
  if (file === 'config.yaml' || file === 'config.yml')
146
150
  return false;
@@ -156,6 +160,9 @@ async function applyFlowGlobs(workspaceConfig, normalizedInput, unfilteredFlowFi
156
160
  return true;
157
161
  })
158
162
  .map((file) => path.resolve(normalizedInput, file));
163
+ // Re-globbing from disk bypasses the earlier --exclude-flows filter, so
164
+ // re-apply it here or excluded flows sneak back in via `flows:` globs.
165
+ return filterFlowFiles(globbedFlowFiles, excludeFlows);
159
166
  }
160
167
  return unfilteredFlowFiles.filter((file) => !file.endsWith('config.yaml') &&
161
168
  !file.endsWith('config.yml') &&
@@ -176,14 +183,16 @@ function resolveSequentialFlows(workspaceConfig, pathsByName, debug) {
176
183
  console.log('[DEBUG] executionOrder.flowsOrder:', workspaceConfig.executionOrder.flowsOrder);
177
184
  console.log('[DEBUG] Available flow names:', Object.keys(pathsByName));
178
185
  }
179
- const flowsToRunInSequence = workspaceConfig.executionOrder.flowsOrder
180
- .flatMap((flowOrder) => {
181
- const normalizedFlowOrder = flowOrder.replace(/\.ya?ml$/i, '');
182
- if (debug && flowOrder !== normalizedFlowOrder) {
183
- console.log(`[DEBUG] Stripping trailing extension: "${flowOrder}" -> "${normalizedFlowOrder}"`);
184
- }
185
- return (0, execution_plan_utils_1.getFlowsToRunInSequence)(pathsByName, [normalizedFlowOrder], debug);
186
- });
186
+ // Dedupe so a flow listed twice in flowsOrder isn't run twice.
187
+ const flowsToRunInSequence = [
188
+ ...new Set(workspaceConfig.executionOrder.flowsOrder.flatMap((flowOrder) => {
189
+ const normalizedFlowOrder = flowOrder.replace(/\.ya?ml$/i, '');
190
+ if (debug && flowOrder !== normalizedFlowOrder) {
191
+ console.log(`[DEBUG] Stripping trailing extension: "${flowOrder}" -> "${normalizedFlowOrder}"`);
192
+ }
193
+ return getFlowsToRunInSequence(pathsByName, [normalizedFlowOrder], debug);
194
+ })),
195
+ ];
187
196
  if (debug) {
188
197
  console.log(`[DEBUG] Sequential flows resolved: ${flowsToRunInSequence.length} flow(s)`);
189
198
  }
@@ -213,7 +222,7 @@ function resolveSequentialFlows(workspaceConfig, pathsByName, debug) {
213
222
  * @returns Complete execution plan with flows, dependencies, and metadata
214
223
  * @throws Error if input path doesn't exist, no flows found, or dependencies missing
215
224
  */
216
- async function plan(options) {
225
+ export async function plan(options) {
217
226
  const { input, includeTags = [], excludeTags = [], excludeFlows, configFile, debug = false, } = options;
218
227
  const normalizedInput = path.normalize(input);
219
228
  const flowMetadata = {};
@@ -223,7 +232,7 @@ async function plan(options) {
223
232
  if (fs.lstatSync(normalizedInput).isFile()) {
224
233
  return planSingleFile(normalizedInput, configFile);
225
234
  }
226
- let unfilteredFlowFiles = await (0, execution_plan_utils_1.readDirectory)(normalizedInput, execution_plan_utils_1.isFlowFile);
235
+ let unfilteredFlowFiles = await readDirectory(normalizedInput, isFlowFile);
227
236
  if (unfilteredFlowFiles.length === 0) {
228
237
  throw new Error(`Flow directory does not contain any Flow files: ${path.resolve(normalizedInput)}`);
229
238
  }
@@ -234,12 +243,12 @@ async function plan(options) {
234
243
  if (!fs.existsSync(configFilePath)) {
235
244
  throw new Error(`Config file does not exist: ${configFilePath}`);
236
245
  }
237
- workspaceConfig = (0, execution_plan_utils_1.readYamlFileAsJson)(configFilePath);
246
+ workspaceConfig = readYamlFileAsJson(configFilePath);
238
247
  }
239
248
  else {
240
249
  workspaceConfig = getWorkspaceConfig(normalizedInput, unfilteredFlowFiles);
241
250
  }
242
- unfilteredFlowFiles = await applyFlowGlobs(workspaceConfig, normalizedInput, unfilteredFlowFiles, configFile);
251
+ unfilteredFlowFiles = await applyFlowGlobs(workspaceConfig, normalizedInput, unfilteredFlowFiles, configFile, excludeFlows);
243
252
  if (unfilteredFlowFiles.length === 0) {
244
253
  const error = workspaceConfig.flows
245
254
  ? new Error(`Flow inclusion pattern(s) did not match any Flow files:\n${workspaceConfig.flows.join('\n')}`)
@@ -248,7 +257,7 @@ async function plan(options) {
248
257
  }
249
258
  // eslint-disable-next-line unicorn/no-array-reduce
250
259
  const configPerFlowFile = unfilteredFlowFiles.reduce((acc, filePath) => {
251
- const { config } = (0, execution_plan_utils_1.readTestYamlFileAsJson)(filePath);
260
+ const { config } = readTestYamlFileAsJson(filePath);
252
261
  acc[filePath] = config;
253
262
  return acc;
254
263
  }, {});
@@ -6,6 +6,9 @@ export declare const readYamlFileAsJson: (filePath: string) => unknown;
6
6
  export declare const readTestYamlFileAsJson: (filePath: string) => {
7
7
  config: Record<string, unknown>;
8
8
  testSteps: Record<string, unknown>[];
9
+ } | {
10
+ config: null;
11
+ testSteps: Record<string, unknown>[];
9
12
  };
10
13
  export declare function readDirectory(dir: string, filterFunction?: (filePath: string) => boolean): Promise<string[]>;
11
14
  export declare const checkIfFilesExistInWorkspace: (commandName: string, command: Record<string, string> | string | string[], absoluteFilePath: string) => {
@@ -1,60 +1,33 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.processDependencies = exports.checkIfFilesExistInWorkspace = exports.readTestYamlFileAsJson = exports.readYamlFileAsJson = void 0;
4
- exports.getFlowsToRunInSequence = getFlowsToRunInSequence;
5
- exports.isFlowFile = isFlowFile;
6
- exports.readDirectory = readDirectory;
7
- const yaml = require("js-yaml");
8
- const fs = require("node:fs");
9
- const path = require("node:path");
1
+ import * as yaml from 'js-yaml';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
10
4
  const commandsThatRequireFiles = new Set(['addMedia', 'runFlow', 'runScript']);
11
- function getFlowsToRunInSequence(paths, flowOrder, debug = false) {
5
+ export function getFlowsToRunInSequence(paths, flowOrder, debug = false) {
12
6
  if (flowOrder.length === 0) {
13
7
  if (debug) {
14
8
  console.log('[DEBUG] getFlowsToRunInSequence: flowOrder is empty, returning []');
15
9
  }
16
10
  return [];
17
11
  }
18
- const orderSet = new Set(flowOrder);
19
12
  const availableNames = Object.keys(paths);
20
13
  if (debug) {
21
- console.log(`[DEBUG] getFlowsToRunInSequence: Looking for flows in order: [${[...orderSet].join(', ')}]`);
14
+ console.log(`[DEBUG] getFlowsToRunInSequence: Looking for flows in order: [${flowOrder.join(', ')}]`);
22
15
  console.log(`[DEBUG] getFlowsToRunInSequence: Available flow names: [${availableNames.join(', ')}]`);
23
16
  }
24
- const namesInOrder = availableNames.filter((key) => orderSet.has(key));
17
+ const namesInOrder = flowOrder.filter((name) => Object.hasOwn(paths, name));
25
18
  if (debug) {
26
19
  console.log(`[DEBUG] getFlowsToRunInSequence: Matched ${namesInOrder.length} flow(s): [${namesInOrder.join(', ')}]`);
27
20
  }
28
21
  if (namesInOrder.length === 0) {
29
- const notFound = [...orderSet].filter((item) => !availableNames.includes(item));
22
+ const notFound = flowOrder.filter((name) => !availableNames.includes(name));
30
23
  console.warn(`Warning: Could not find flows specified in executionOrder.flowsOrder: ${notFound.join(', ')}\n` +
31
24
  `This may be intentional if flows were excluded by tags.\n` +
32
25
  `Available flow names:\n${availableNames.join('\n')}`);
33
26
  return [];
34
27
  }
35
- const result = [...orderSet].filter((item) => namesInOrder.includes(item));
36
- if (result.length === 0) {
37
- const notFound = [...orderSet].filter((item) => !namesInOrder.includes(item));
38
- console.warn(`Warning: Could not find flows needed for execution in order: ${notFound.join(', ')}\n` +
39
- `This may be intentional if flows were excluded by tags.\n` +
40
- `Available flow names:\n${availableNames.join('\n')}`);
41
- return [];
42
- }
43
- if (flowOrder
44
- .slice(0, result.length)
45
- .every((value, index) => value === result[index])) {
46
- const resolvedPaths = result.map((item) => paths[item]);
47
- if (debug) {
48
- console.log(`[DEBUG] getFlowsToRunInSequence: Order matches, returning ${resolvedPaths.length} path(s)`);
49
- }
50
- return resolvedPaths;
51
- }
52
- throw new Error(`Flow order mismatch in executionOrder.flowsOrder.\n\n` +
53
- `Expected order: [${flowOrder.slice(0, result.length).join(', ')}]\n` +
54
- `Actual order: [${result.join(', ')}]\n\n` +
55
- `Please ensure flows are specified in the correct order.`);
28
+ return namesInOrder.map((name) => paths[name]);
56
29
  }
57
- function isFlowFile(filePath) {
30
+ export function isFlowFile(filePath) {
58
31
  // Exclude files inside .app bundles
59
32
  // Check if any directory in the path ends with .app
60
33
  const pathParts = filePath.split(path.sep);
@@ -65,7 +38,7 @@ function isFlowFile(filePath) {
65
38
  }
66
39
  return filePath.endsWith('.yaml') || filePath.endsWith('.yml');
67
40
  }
68
- const readYamlFileAsJson = (filePath) => {
41
+ export const readYamlFileAsJson = (filePath) => {
69
42
  try {
70
43
  const normalizedPath = path.normalize(filePath);
71
44
  const yamlText = fs.readFileSync(normalizedPath, 'utf8');
@@ -82,11 +55,12 @@ const readYamlFileAsJson = (filePath) => {
82
55
  return result;
83
56
  }
84
57
  catch (error) {
85
- throw new Error(`Error parsing YAML file ${filePath}: ${error}`);
58
+ throw new Error(`Error parsing YAML file ${filePath}: ${error}`, {
59
+ cause: error,
60
+ });
86
61
  }
87
62
  };
88
- exports.readYamlFileAsJson = readYamlFileAsJson;
89
- const readTestYamlFileAsJson = (filePath) => {
63
+ export const readTestYamlFileAsJson = (filePath) => {
90
64
  try {
91
65
  const normalizedPath = path.normalize(filePath);
92
66
  const yamlText = fs.readFileSync(normalizedPath, 'utf8');
@@ -96,25 +70,23 @@ const readTestYamlFileAsJson = (filePath) => {
96
70
  if (normalizedText.includes('\n---\n')) {
97
71
  const yamlTexts = normalizedText.split('\n---\n');
98
72
  const config = yaml.load(yamlTexts[0]);
99
- const testSteps = yaml.load(yamlTexts[1]);
100
- if (Object.keys(config ?? {}).length > 0) {
73
+ // Rejoin everything after the first separator so step documents beyond
74
+ // a second `---` aren't silently dropped.
75
+ const testSteps = yaml.load(yamlTexts.slice(1).join('\n'));
76
+ if (config && Object.keys(config).length > 0) {
101
77
  return { config, testSteps };
102
78
  }
103
79
  }
104
80
  const testSteps = yaml.load(yamlText);
105
- if (Object.keys(testSteps).length > 0) {
106
- return { config: null, testSteps };
107
- }
108
81
  return { config: null, testSteps };
109
82
  }
110
83
  catch (error) {
111
84
  const message = `Error parsing YAML file ${filePath}: ${error}`;
112
85
  console.error(message);
113
- throw new Error(message);
86
+ throw new Error(message, { cause: error });
114
87
  }
115
88
  };
116
- exports.readTestYamlFileAsJson = readTestYamlFileAsJson;
117
- async function readDirectory(dir, filterFunction) {
89
+ export async function readDirectory(dir, filterFunction) {
118
90
  const readDirResult = await fs.promises.readdir(dir);
119
91
  const files = await Promise.all(readDirResult.map(async (file) => {
120
92
  const filePath = path.join(dir, file);
@@ -130,7 +102,7 @@ async function readDirectory(dir, filterFunction) {
130
102
  }));
131
103
  return files.flat().filter(Boolean);
132
104
  }
133
- const checkIfFilesExistInWorkspace = (commandName, command, absoluteFilePath) => {
105
+ export const checkIfFilesExistInWorkspace = (commandName, command, absoluteFilePath) => {
134
106
  const errors = [];
135
107
  const files = [];
136
108
  const directory = path.dirname(absoluteFilePath);
@@ -142,9 +114,9 @@ const checkIfFilesExistInWorkspace = (commandName, command, absoluteFilePath) =>
142
114
  errors.push(buildError(error));
143
115
  files.push(absoluteFilePath);
144
116
  };
145
- // simple command
117
+ // simple command — processFilePath already resolves against `directory`
146
118
  if (typeof command === 'string') {
147
- processFilePath(path.normalize(path.join(directory, command)));
119
+ processFilePath(command);
148
120
  }
149
121
  // array command
150
122
  if (Array.isArray(command)) {
@@ -158,7 +130,6 @@ const checkIfFilesExistInWorkspace = (commandName, command, absoluteFilePath) =>
158
130
  processFilePath(x.file);
159
131
  return { errors, files };
160
132
  };
161
- exports.checkIfFilesExistInWorkspace = checkIfFilesExistInWorkspace;
162
133
  const checkFile = (filePath) => {
163
134
  if (!fs.existsSync(filePath))
164
135
  return `non-existent file`;
@@ -171,7 +142,7 @@ const checkStepsArray = (steps, absoluteFilePath) => {
171
142
  continue;
172
143
  for (const [commandName, commandValue] of Object.entries(command)) {
173
144
  if (commandsThatRequireFiles.has(commandName)) {
174
- const { errors: newErrors, files: newFiles } = (0, exports.checkIfFilesExistInWorkspace)(commandName, commandValue, path.normalize(absoluteFilePath));
145
+ const { errors: newErrors, files: newFiles } = checkIfFilesExistInWorkspace(commandName, commandValue, path.normalize(absoluteFilePath));
175
146
  errors = [...errors, ...newErrors];
176
147
  files = [...files, ...newFiles];
177
148
  }
@@ -186,7 +157,7 @@ const checkStepsArray = (steps, absoluteFilePath) => {
186
157
  }
187
158
  return { errors, files };
188
159
  };
189
- const processDependencies = ({ config, input, testSteps, }) => {
160
+ export const processDependencies = ({ config, input, testSteps, }) => {
190
161
  let allErrors = [];
191
162
  let allFiles = [];
192
163
  const { onFlowComplete, onFlowStart } = config ?? {};
@@ -208,4 +179,3 @@ const processDependencies = ({ config, input, testSteps, }) => {
208
179
  }
209
180
  return { allErrors, allFiles };
210
181
  };
211
- exports.processDependencies = processDependencies;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Longest whole-segment directory prefix shared by every flow + referenced
3
+ * file path. Segment comparison (not `startsWith`) so sibling dirs like
4
+ * `flows`/`flows-extra` can't merge, and the file segment itself is never
5
+ * consumed. Returns '' when the paths share no root at all (or none are given).
6
+ */
7
+ export declare function computeCommonRoot(testFileNames: string[], referencedFiles: string[]): string;
8
+ export interface FlowMetadataEntry {
9
+ flowName: string;
10
+ tags: string[];
11
+ }
12
+ /**
13
+ * Build the portable-relative-path → {flowName, tags} map that results are
14
+ * keyed by. Flow name comes from the YAML `name` field, falling back to the
15
+ * filename without extension; tags are normalized to a string array.
16
+ */
17
+ export declare function buildTestMetadataMap(flowMetadata: Record<string, Record<string, unknown> | null>, commonRoot: string): Record<string, FlowMetadataEntry>;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Pure helpers for turning absolute flow paths into the server-side relative
3
+ * keys that submission, the polling layer, and JSON output all share.
4
+ *
5
+ * Extracted from the cloud command so the MCP `dcd_run_cloud_test` tool builds
6
+ * byte-identical paths without duplicating the logic.
7
+ */
8
+ import * as path from 'node:path';
9
+ import { toPortableRelativePath } from '../utils/paths.js';
10
+ /**
11
+ * Longest whole-segment directory prefix shared by every flow + referenced
12
+ * file path. Segment comparison (not `startsWith`) so sibling dirs like
13
+ * `flows`/`flows-extra` can't merge, and the file segment itself is never
14
+ * consumed. Returns '' when the paths share no root at all (or none are given).
15
+ */
16
+ export function computeCommonRoot(testFileNames, referencedFiles) {
17
+ const pathsShortestToLongest = [...testFileNames, ...referencedFiles].sort((a, b) => a.split(path.sep).length - b.split(path.sep).length);
18
+ if (pathsShortestToLongest.length === 0)
19
+ return '';
20
+ const splitPaths = pathsShortestToLongest.map((p) => p.split(path.sep));
21
+ const shortestSegments = splitPaths[0];
22
+ let matchedSegments = 0;
23
+ for (let i = 0; i < shortestSegments.length - 1; i++) {
24
+ if (splitPaths.every((segments) => segments[i] === shortestSegments[i])) {
25
+ matchedSegments = i + 1;
26
+ }
27
+ else {
28
+ break;
29
+ }
30
+ }
31
+ return shortestSegments.slice(0, matchedSegments).join(path.sep);
32
+ }
33
+ /**
34
+ * Build the portable-relative-path → {flowName, tags} map that results are
35
+ * keyed by. Flow name comes from the YAML `name` field, falling back to the
36
+ * filename without extension; tags are normalized to a string array.
37
+ */
38
+ export function buildTestMetadataMap(flowMetadata, commonRoot) {
39
+ const map = {};
40
+ for (const [absolutePath, meta] of Object.entries(flowMetadata)) {
41
+ const normalizedPath = toPortableRelativePath(absolutePath, commonRoot);
42
+ const flowName = meta?.name || path.parse(absolutePath).name;
43
+ const rawTags = meta?.tags;
44
+ const tags = Array.isArray(rawTags)
45
+ ? rawTags.map(String)
46
+ : rawTags
47
+ ? [String(rawTags)]
48
+ : [];
49
+ map[normalizedPath] = { flowName, tags };
50
+ }
51
+ return map;
52
+ }
@@ -22,7 +22,6 @@ export declare class AndroidMetadataExtractor implements IMetadataExtractor {
22
22
  export declare class IosAppMetadataExtractor implements IMetadataExtractor {
23
23
  canHandle(filePath: string): boolean;
24
24
  extract(filePath: string): Promise<TAppMetadata>;
25
- private parseInfoPlist;
26
25
  }
27
26
  /**
28
27
  * Extracts metadata from iOS .zip files containing .app bundles
@@ -30,7 +29,6 @@ export declare class IosAppMetadataExtractor implements IMetadataExtractor {
30
29
  export declare class IosZipMetadataExtractor implements IMetadataExtractor {
31
30
  canHandle(filePath: string): boolean;
32
31
  extract(filePath: string): Promise<TAppMetadata>;
33
- private parseInfoPlist;
34
32
  }
35
33
  /**
36
34
  * Extracts metadata from Expo iOS .tar.gz archives by extracting the