@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.
- package/README.md +75 -2
- package/dist/commands/artifacts.d.ts +47 -18
- package/dist/commands/artifacts.js +69 -64
- package/dist/commands/cloud.d.ts +228 -88
- package/dist/commands/cloud.js +430 -342
- package/dist/commands/list.d.ts +39 -38
- package/dist/commands/list.js +124 -131
- package/dist/commands/live.d.ts +2 -0
- package/dist/commands/live.js +520 -0
- package/dist/commands/login.d.ts +17 -0
- package/dist/commands/login.js +252 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.js +30 -0
- package/dist/commands/status.d.ts +23 -42
- package/dist/commands/status.js +170 -179
- package/dist/commands/switch-org.d.ts +12 -0
- package/dist/commands/switch-org.js +76 -0
- package/dist/commands/upgrade.d.ts +2 -0
- package/dist/commands/upgrade.js +120 -0
- package/dist/commands/upload.d.ts +33 -18
- package/dist/commands/upload.js +72 -78
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.js +31 -0
- package/dist/config/environments.d.ts +31 -0
- package/dist/config/environments.js +52 -0
- package/dist/config/flags/api.flags.d.ts +10 -2
- package/dist/config/flags/api.flags.js +13 -14
- package/dist/config/flags/binary.flags.d.ts +17 -4
- package/dist/config/flags/binary.flags.js +14 -18
- package/dist/config/flags/device.flags.d.ts +49 -11
- package/dist/config/flags/device.flags.js +43 -38
- package/dist/config/flags/environment.flags.d.ts +27 -6
- package/dist/config/flags/environment.flags.js +24 -29
- package/dist/config/flags/execution.flags.d.ts +35 -8
- package/dist/config/flags/execution.flags.js +31 -41
- package/dist/config/flags/github.flags.d.ts +23 -5
- package/dist/config/flags/github.flags.js +19 -15
- package/dist/config/flags/output.flags.d.ts +57 -13
- package/dist/config/flags/output.flags.js +48 -47
- package/dist/constants.d.ts +218 -51
- package/dist/constants.js +17 -20
- package/dist/gateways/api-gateway.d.ts +72 -16
- package/dist/gateways/api-gateway.js +298 -104
- package/dist/gateways/cli-auth-gateway.d.ts +13 -0
- package/dist/gateways/cli-auth-gateway.js +54 -0
- package/dist/gateways/realtime-gateway.d.ts +32 -0
- package/dist/gateways/realtime-gateway.js +103 -0
- package/dist/gateways/supabase-gateway.d.ts +11 -11
- package/dist/gateways/supabase-gateway.js +20 -48
- package/dist/index.d.ts +2 -1
- package/dist/index.js +98 -4
- package/dist/mcp/context.d.ts +33 -0
- package/dist/mcp/context.js +33 -0
- package/dist/mcp/helpers.d.ts +16 -0
- package/dist/mcp/helpers.js +34 -0
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +24 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +27 -0
- package/dist/mcp/tools/download-artifacts.d.ts +11 -0
- package/dist/mcp/tools/download-artifacts.js +84 -0
- package/dist/mcp/tools/get-status.d.ts +7 -0
- package/dist/mcp/tools/get-status.js +39 -0
- package/dist/mcp/tools/list-devices.d.ts +7 -0
- package/dist/mcp/tools/list-devices.js +27 -0
- package/dist/mcp/tools/list-runs.d.ts +3 -0
- package/dist/mcp/tools/list-runs.js +60 -0
- package/dist/mcp/tools/run-cloud-test.d.ts +14 -0
- package/dist/mcp/tools/run-cloud-test.js +233 -0
- package/dist/methods.d.ts +34 -5
- package/dist/methods.js +266 -215
- package/dist/services/device-validation.service.d.ts +9 -1
- package/dist/services/device-validation.service.js +56 -40
- package/dist/services/execution-plan.service.js +40 -31
- package/dist/services/execution-plan.utils.d.ts +3 -0
- package/dist/services/execution-plan.utils.js +25 -55
- package/dist/services/flow-paths.d.ts +17 -0
- package/dist/services/flow-paths.js +52 -0
- package/dist/services/metadata-extractor.service.d.ts +0 -2
- package/dist/services/metadata-extractor.service.js +75 -78
- package/dist/services/moropo.service.js +33 -34
- package/dist/services/report-download.service.d.ts +12 -1
- package/dist/services/report-download.service.js +34 -27
- package/dist/services/results-polling.service.d.ts +23 -9
- package/dist/services/results-polling.service.js +257 -123
- package/dist/services/telemetry.service.d.ts +49 -0
- package/dist/services/telemetry.service.js +252 -0
- package/dist/services/test-submission.service.d.ts +21 -4
- package/dist/services/test-submission.service.js +51 -33
- package/dist/services/version.service.d.ts +4 -3
- package/dist/services/version.service.js +28 -16
- package/dist/types/domain/auth.types.d.ts +20 -0
- package/dist/types/domain/auth.types.js +1 -0
- package/dist/types/domain/device.types.js +8 -11
- package/dist/types/domain/live.types.d.ts +76 -0
- package/dist/types/domain/live.types.js +3 -0
- package/dist/types/generated/schema.types.js +1 -2
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.js +2 -18
- package/dist/types.js +1 -2
- package/dist/utils/auth.d.ts +13 -0
- package/dist/utils/auth.js +141 -0
- package/dist/utils/ci.d.ts +12 -0
- package/dist/utils/ci.js +39 -0
- package/dist/utils/cli.d.ts +35 -0
- package/dist/utils/cli.js +118 -0
- package/dist/utils/compatibility.d.ts +2 -1
- package/dist/utils/compatibility.js +6 -8
- package/dist/utils/config-store.d.ts +35 -0
- package/dist/utils/config-store.js +115 -0
- package/dist/utils/connectivity.js +8 -7
- package/dist/utils/expo.js +29 -24
- package/dist/utils/orgs.d.ts +11 -0
- package/dist/utils/orgs.js +36 -0
- package/dist/utils/paths.d.ts +11 -0
- package/dist/utils/paths.js +21 -0
- package/dist/utils/progress.d.ts +13 -0
- package/dist/utils/progress.js +47 -0
- package/dist/utils/styling.d.ts +42 -36
- package/dist/utils/styling.js +78 -82
- package/dist/utils/ui.d.ts +41 -0
- package/dist/utils/ui.js +95 -0
- package/package.json +36 -45
- package/bin/dev.cmd +0 -3
- package/bin/dev.js +0 -6
- package/bin/run.cmd +0 -3
- package/bin/run.js +0 -7
- package/dist/types/schema.types.d.ts +0 -2702
- package/dist/types/schema.types.js +0 -3
- 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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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 (!
|
|
71
|
+
if (!version && !device) {
|
|
55
72
|
return;
|
|
56
73
|
}
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
61
|
-
throw new Error(
|
|
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(
|
|
64
|
-
!
|
|
65
|
-
throw new Error(
|
|
80
|
+
if (Array.isArray(supportedVersions) &&
|
|
81
|
+
!supportedVersions.includes(requestedVersion)) {
|
|
82
|
+
throw new Error(unsupportedVersionMessage(deviceID, supportedVersions));
|
|
66
83
|
}
|
|
67
84
|
if (debug && logger) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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 } =
|
|
21
|
-
const { allErrors, allFiles } =
|
|
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 (!
|
|
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
|
-
?
|
|
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 } =
|
|
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 =
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
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 =
|
|
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 } =
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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: [${
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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(
|
|
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 } =
|
|
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
|