@devicecloud.dev/dcd 4.4.9 → 5.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -2
- package/dist/commands/artifacts.d.ts +47 -18
- package/dist/commands/artifacts.js +68 -60
- package/dist/commands/cloud.d.ts +228 -88
- package/dist/commands/cloud.js +389 -288
- package/dist/commands/list.d.ts +39 -38
- package/dist/commands/list.js +122 -127
- package/dist/commands/live.d.ts +2 -0
- package/dist/commands/live.js +513 -0
- package/dist/commands/login.d.ts +17 -0
- package/dist/commands/login.js +250 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.js +32 -0
- package/dist/commands/status.d.ts +23 -42
- package/dist/commands/status.js +162 -173
- package/dist/commands/switch-org.d.ts +12 -0
- package/dist/commands/switch-org.js +78 -0
- package/dist/commands/upgrade.d.ts +2 -0
- package/dist/commands/upgrade.js +122 -0
- package/dist/commands/upload.d.ts +33 -18
- package/dist/commands/upload.js +62 -67
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.js +34 -0
- package/dist/config/environments.d.ts +31 -0
- package/dist/config/environments.js +58 -0
- package/dist/config/flags/api.flags.d.ts +10 -2
- package/dist/config/flags/api.flags.js +12 -10
- package/dist/config/flags/binary.flags.d.ts +17 -4
- package/dist/config/flags/binary.flags.js +13 -14
- package/dist/config/flags/device.flags.d.ts +49 -11
- package/dist/config/flags/device.flags.js +41 -33
- package/dist/config/flags/environment.flags.d.ts +27 -6
- package/dist/config/flags/environment.flags.js +23 -25
- package/dist/config/flags/execution.flags.d.ts +35 -8
- package/dist/config/flags/execution.flags.js +30 -37
- package/dist/config/flags/github.flags.d.ts +23 -5
- package/dist/config/flags/github.flags.js +18 -11
- package/dist/config/flags/output.flags.d.ts +57 -13
- package/dist/config/flags/output.flags.js +47 -43
- package/dist/constants.d.ts +218 -51
- package/dist/constants.js +2 -2
- package/dist/gateways/api-gateway.d.ts +43 -12
- package/dist/gateways/api-gateway.js +240 -100
- package/dist/gateways/cli-auth-gateway.d.ts +13 -0
- package/dist/gateways/cli-auth-gateway.js +57 -0
- package/dist/gateways/supabase-gateway.d.ts +11 -11
- package/dist/gateways/supabase-gateway.js +15 -39
- package/dist/index.d.ts +2 -1
- package/dist/index.js +93 -2
- package/dist/methods.d.ts +3 -5
- package/dist/methods.js +170 -178
- package/dist/services/device-validation.service.d.ts +8 -0
- package/dist/services/device-validation.service.js +55 -35
- package/dist/services/execution-plan.service.js +27 -15
- package/dist/services/execution-plan.utils.d.ts +3 -0
- package/dist/services/execution-plan.utils.js +10 -32
- package/dist/services/metadata-extractor.service.d.ts +0 -2
- package/dist/services/metadata-extractor.service.js +57 -57
- package/dist/services/moropo.service.js +25 -24
- package/dist/services/report-download.service.d.ts +12 -1
- package/dist/services/report-download.service.js +31 -20
- package/dist/services/results-polling.service.d.ts +6 -7
- package/dist/services/results-polling.service.js +80 -33
- package/dist/services/telemetry.service.d.ts +40 -0
- package/dist/services/telemetry.service.js +230 -0
- package/dist/services/test-submission.service.js +2 -1
- package/dist/services/version.service.d.ts +3 -2
- package/dist/services/version.service.js +27 -11
- package/dist/types/domain/auth.types.d.ts +12 -0
- package/dist/types/{schema.types.js → domain/auth.types.js} +0 -1
- package/dist/types/domain/live.types.d.ts +76 -0
- package/dist/types/domain/live.types.js +4 -0
- package/dist/utils/auth.d.ts +13 -0
- package/dist/utils/auth.js +142 -0
- package/dist/utils/cli.d.ts +35 -0
- package/dist/utils/cli.js +127 -0
- package/dist/utils/compatibility.d.ts +2 -1
- package/dist/utils/compatibility.js +2 -2
- package/dist/utils/config-store.d.ts +35 -0
- package/dist/utils/config-store.js +125 -0
- package/dist/utils/connectivity.js +7 -3
- package/dist/utils/expo.js +14 -3
- package/dist/utils/orgs.d.ts +11 -0
- package/dist/utils/orgs.js +40 -0
- package/dist/utils/paths.d.ts +11 -0
- package/dist/utils/paths.js +24 -0
- package/dist/utils/progress.d.ts +13 -0
- package/dist/utils/progress.js +50 -0
- package/dist/utils/styling.d.ts +13 -5
- package/dist/utils/styling.js +37 -7
- package/package.json +26 -38
- 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/oclif.manifest.json +0 -884
|
@@ -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
|
}
|
|
@@ -16,29 +16,24 @@ class DeviceValidationService {
|
|
|
16
16
|
* @throws Error if device/API level combination is not supported
|
|
17
17
|
*/
|
|
18
18
|
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
|
-
}
|
|
19
|
+
this.validateDevice({
|
|
20
|
+
debugLines: (deviceID, version, supportedVersions) => [
|
|
21
|
+
`[DEBUG] Android device: ${deviceID}`,
|
|
22
|
+
`[DEBUG] Android API level: ${version}`,
|
|
23
|
+
`[DEBUG] Google Play enabled: ${googlePlay}`,
|
|
24
|
+
`[DEBUG] Supported Android versions: ${supportedVersions.join(', ')}`,
|
|
25
|
+
],
|
|
26
|
+
defaultDevice: 'pixel-7',
|
|
27
|
+
defaultVersion: '34',
|
|
28
|
+
device: androidDevice,
|
|
29
|
+
lookup: googlePlay
|
|
30
|
+
? compatibilityData.androidPlay
|
|
31
|
+
: compatibilityData.android,
|
|
32
|
+
noSupportMessage: () => `We don't support that device configuration - please check the docs for supported devices: https://docs.devicecloud.dev/getting-started/devices-configuration`,
|
|
33
|
+
options,
|
|
34
|
+
unsupportedVersionMessage: (deviceID, supportedVersions) => `${deviceID} ${googlePlay ? '(Play Store) ' : ''}only supports these Android API levels: ${supportedVersions.join(', ')}`,
|
|
35
|
+
version: androidApiLevel,
|
|
36
|
+
});
|
|
42
37
|
}
|
|
43
38
|
/**
|
|
44
39
|
* Validate iOS device configuration
|
|
@@ -50,24 +45,49 @@ class DeviceValidationService {
|
|
|
50
45
|
* @throws Error if device/version combination is not supported
|
|
51
46
|
*/
|
|
52
47
|
validateiOSDevice(iOSVersion, iOSDevice, compatibilityData, options = {}) {
|
|
48
|
+
this.validateDevice({
|
|
49
|
+
debugLines: (deviceID, version, supportedVersions) => [
|
|
50
|
+
`[DEBUG] iOS device: ${deviceID}`,
|
|
51
|
+
`[DEBUG] iOS version: ${version}`,
|
|
52
|
+
`[DEBUG] Supported iOS versions: ${supportedVersions.join(', ')}`,
|
|
53
|
+
],
|
|
54
|
+
defaultDevice: 'iphone-14',
|
|
55
|
+
defaultVersion: '17',
|
|
56
|
+
device: iOSDevice,
|
|
57
|
+
lookup: compatibilityData?.ios,
|
|
58
|
+
noSupportMessage: (deviceID) => `Device ${deviceID} is not supported. Please check the docs for supported devices: https://docs.devicecloud.dev/getting-started/devices-configuration`,
|
|
59
|
+
options,
|
|
60
|
+
unsupportedVersionMessage: (deviceID, supportedVersions) => `${deviceID} only supports these iOS versions: ${supportedVersions.join(', ')}`,
|
|
61
|
+
version: iOSVersion,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Shared validation flow for both platforms: apply the default device,
|
|
66
|
+
* look up its supported versions, then check the requested version
|
|
67
|
+
* @param config Platform-specific lookup table, defaults, and messages
|
|
68
|
+
* @returns void
|
|
69
|
+
* @throws Error if device/version combination is not supported
|
|
70
|
+
*/
|
|
71
|
+
validateDevice(config) {
|
|
72
|
+
const { debugLines, defaultDevice, defaultVersion, device, lookup, noSupportMessage, options, unsupportedVersionMessage, version, } = config;
|
|
53
73
|
const { debug = false, logger } = options;
|
|
54
|
-
if (!
|
|
74
|
+
if (!version && !device) {
|
|
55
75
|
return;
|
|
56
76
|
}
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
61
|
-
throw new Error(
|
|
77
|
+
const deviceID = device || defaultDevice;
|
|
78
|
+
const supportedVersions = lookup?.[deviceID] || [];
|
|
79
|
+
const requestedVersion = version || defaultVersion;
|
|
80
|
+
if (supportedVersions.length === 0) {
|
|
81
|
+
throw new Error(noSupportMessage(deviceID));
|
|
62
82
|
}
|
|
63
|
-
if (Array.isArray(
|
|
64
|
-
!
|
|
65
|
-
throw new Error(
|
|
83
|
+
if (Array.isArray(supportedVersions) &&
|
|
84
|
+
!supportedVersions.includes(requestedVersion)) {
|
|
85
|
+
throw new Error(unsupportedVersionMessage(deviceID, supportedVersions));
|
|
66
86
|
}
|
|
67
87
|
if (debug && logger) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
88
|
+
for (const line of debugLines(deviceID, requestedVersion, supportedVersions)) {
|
|
89
|
+
logger(line);
|
|
90
|
+
}
|
|
71
91
|
}
|
|
72
92
|
}
|
|
73
93
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.plan = plan;
|
|
4
|
-
const glob_1 = require("glob");
|
|
5
4
|
const fs = require("node:fs");
|
|
6
5
|
const path = require("node:path");
|
|
7
6
|
const execution_plan_utils_1 = require("./execution-plan.utils");
|
|
@@ -131,16 +130,24 @@ async function planSingleFile(normalizedInput, configFile) {
|
|
|
131
130
|
* @param normalizedInput - Normalized path to the workspace directory
|
|
132
131
|
* @param unfilteredFlowFiles - List of all discovered flow files
|
|
133
132
|
* @param configFile - Optional custom config file path
|
|
133
|
+
* @param excludeFlows - --exclude-flows patterns to re-apply to glob matches
|
|
134
134
|
* @returns Filtered list of flow file paths matching the globs
|
|
135
135
|
*/
|
|
136
|
-
async function applyFlowGlobs(workspaceConfig, normalizedInput, unfilteredFlowFiles, configFile) {
|
|
136
|
+
async function applyFlowGlobs(workspaceConfig, normalizedInput, unfilteredFlowFiles, configFile, excludeFlows) {
|
|
137
137
|
if (workspaceConfig.flows) {
|
|
138
138
|
const globs = workspaceConfig.flows.map((g) => g);
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
// fs.globSync lands in Node 22; the CLI's `engines.node` already requires it.
|
|
140
|
+
// No `nodir` option — we strip directories with a stat check below.
|
|
141
|
+
const allMatches = fs.globSync(globs, { cwd: normalizedInput });
|
|
142
|
+
const matchedFiles = allMatches.filter((file) => {
|
|
143
|
+
try {
|
|
144
|
+
return fs.statSync(path.resolve(normalizedInput, file)).isFile();
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
142
149
|
});
|
|
143
|
-
|
|
150
|
+
const globbedFlowFiles = matchedFiles
|
|
144
151
|
.filter((file) => {
|
|
145
152
|
if (file === 'config.yaml' || file === 'config.yml')
|
|
146
153
|
return false;
|
|
@@ -156,6 +163,9 @@ async function applyFlowGlobs(workspaceConfig, normalizedInput, unfilteredFlowFi
|
|
|
156
163
|
return true;
|
|
157
164
|
})
|
|
158
165
|
.map((file) => path.resolve(normalizedInput, file));
|
|
166
|
+
// Re-globbing from disk bypasses the earlier --exclude-flows filter, so
|
|
167
|
+
// re-apply it here or excluded flows sneak back in via `flows:` globs.
|
|
168
|
+
return filterFlowFiles(globbedFlowFiles, excludeFlows);
|
|
159
169
|
}
|
|
160
170
|
return unfilteredFlowFiles.filter((file) => !file.endsWith('config.yaml') &&
|
|
161
171
|
!file.endsWith('config.yml') &&
|
|
@@ -176,14 +186,16 @@ function resolveSequentialFlows(workspaceConfig, pathsByName, debug) {
|
|
|
176
186
|
console.log('[DEBUG] executionOrder.flowsOrder:', workspaceConfig.executionOrder.flowsOrder);
|
|
177
187
|
console.log('[DEBUG] Available flow names:', Object.keys(pathsByName));
|
|
178
188
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
189
|
+
// Dedupe so a flow listed twice in flowsOrder isn't run twice.
|
|
190
|
+
const flowsToRunInSequence = [
|
|
191
|
+
...new Set(workspaceConfig.executionOrder.flowsOrder.flatMap((flowOrder) => {
|
|
192
|
+
const normalizedFlowOrder = flowOrder.replace(/\.ya?ml$/i, '');
|
|
193
|
+
if (debug && flowOrder !== normalizedFlowOrder) {
|
|
194
|
+
console.log(`[DEBUG] Stripping trailing extension: "${flowOrder}" -> "${normalizedFlowOrder}"`);
|
|
195
|
+
}
|
|
196
|
+
return (0, execution_plan_utils_1.getFlowsToRunInSequence)(pathsByName, [normalizedFlowOrder], debug);
|
|
197
|
+
})),
|
|
198
|
+
];
|
|
187
199
|
if (debug) {
|
|
188
200
|
console.log(`[DEBUG] Sequential flows resolved: ${flowsToRunInSequence.length} flow(s)`);
|
|
189
201
|
}
|
|
@@ -239,7 +251,7 @@ async function plan(options) {
|
|
|
239
251
|
else {
|
|
240
252
|
workspaceConfig = getWorkspaceConfig(normalizedInput, unfilteredFlowFiles);
|
|
241
253
|
}
|
|
242
|
-
unfilteredFlowFiles = await applyFlowGlobs(workspaceConfig, normalizedInput, unfilteredFlowFiles, configFile);
|
|
254
|
+
unfilteredFlowFiles = await applyFlowGlobs(workspaceConfig, normalizedInput, unfilteredFlowFiles, configFile, excludeFlows);
|
|
243
255
|
if (unfilteredFlowFiles.length === 0) {
|
|
244
256
|
const error = workspaceConfig.flows
|
|
245
257
|
? new Error(`Flow inclusion pattern(s) did not match any Flow files:\n${workspaceConfig.flows.join('\n')}`)
|
|
@@ -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) => {
|
|
@@ -15,44 +15,23 @@ function getFlowsToRunInSequence(paths, flowOrder, debug = false) {
|
|
|
15
15
|
}
|
|
16
16
|
return [];
|
|
17
17
|
}
|
|
18
|
-
const orderSet = new Set(flowOrder);
|
|
19
18
|
const availableNames = Object.keys(paths);
|
|
20
19
|
if (debug) {
|
|
21
|
-
console.log(`[DEBUG] getFlowsToRunInSequence: Looking for flows in order: [${
|
|
20
|
+
console.log(`[DEBUG] getFlowsToRunInSequence: Looking for flows in order: [${flowOrder.join(', ')}]`);
|
|
22
21
|
console.log(`[DEBUG] getFlowsToRunInSequence: Available flow names: [${availableNames.join(', ')}]`);
|
|
23
22
|
}
|
|
24
|
-
const namesInOrder =
|
|
23
|
+
const namesInOrder = flowOrder.filter((name) => Object.hasOwn(paths, name));
|
|
25
24
|
if (debug) {
|
|
26
25
|
console.log(`[DEBUG] getFlowsToRunInSequence: Matched ${namesInOrder.length} flow(s): [${namesInOrder.join(', ')}]`);
|
|
27
26
|
}
|
|
28
27
|
if (namesInOrder.length === 0) {
|
|
29
|
-
const notFound =
|
|
28
|
+
const notFound = flowOrder.filter((name) => !availableNames.includes(name));
|
|
30
29
|
console.warn(`Warning: Could not find flows specified in executionOrder.flowsOrder: ${notFound.join(', ')}\n` +
|
|
31
30
|
`This may be intentional if flows were excluded by tags.\n` +
|
|
32
31
|
`Available flow names:\n${availableNames.join('\n')}`);
|
|
33
32
|
return [];
|
|
34
33
|
}
|
|
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.`);
|
|
34
|
+
return namesInOrder.map((name) => paths[name]);
|
|
56
35
|
}
|
|
57
36
|
function isFlowFile(filePath) {
|
|
58
37
|
// Exclude files inside .app bundles
|
|
@@ -96,15 +75,14 @@ const readTestYamlFileAsJson = (filePath) => {
|
|
|
96
75
|
if (normalizedText.includes('\n---\n')) {
|
|
97
76
|
const yamlTexts = normalizedText.split('\n---\n');
|
|
98
77
|
const config = yaml.load(yamlTexts[0]);
|
|
99
|
-
|
|
100
|
-
|
|
78
|
+
// Rejoin everything after the first separator so step documents beyond
|
|
79
|
+
// a second `---` aren't silently dropped.
|
|
80
|
+
const testSteps = yaml.load(yamlTexts.slice(1).join('\n'));
|
|
81
|
+
if (config && Object.keys(config).length > 0) {
|
|
101
82
|
return { config, testSteps };
|
|
102
83
|
}
|
|
103
84
|
}
|
|
104
85
|
const testSteps = yaml.load(yamlText);
|
|
105
|
-
if (Object.keys(testSteps).length > 0) {
|
|
106
|
-
return { config: null, testSteps };
|
|
107
|
-
}
|
|
108
86
|
return { config: null, testSteps };
|
|
109
87
|
}
|
|
110
88
|
catch (error) {
|
|
@@ -142,9 +120,9 @@ const checkIfFilesExistInWorkspace = (commandName, command, absoluteFilePath) =>
|
|
|
142
120
|
errors.push(buildError(error));
|
|
143
121
|
files.push(absoluteFilePath);
|
|
144
122
|
};
|
|
145
|
-
// simple command
|
|
123
|
+
// simple command — processFilePath already resolves against `directory`
|
|
146
124
|
if (typeof command === 'string') {
|
|
147
|
-
processFilePath(
|
|
125
|
+
processFilePath(command);
|
|
148
126
|
}
|
|
149
127
|
// array command
|
|
150
128
|
if (Array.isArray(command)) {
|
|
@@ -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
|
|
@@ -1,12 +1,31 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MetadataExtractorService = exports.ExpoTarGzMetadataExtractor = exports.IosZipMetadataExtractor = exports.IosAppMetadataExtractor = exports.AndroidMetadataExtractor = void 0;
|
|
4
|
-
const AppInfoParser = require("app-info-parser");
|
|
5
4
|
const bplist_parser_1 = require("bplist-parser");
|
|
5
|
+
const node_apk_1 = require("node-apk");
|
|
6
6
|
const promises_1 = require("node:fs/promises");
|
|
7
7
|
const path = require("node:path");
|
|
8
8
|
const StreamZip = require("node-stream-zip");
|
|
9
9
|
const plist_1 = require("plist");
|
|
10
|
+
/**
|
|
11
|
+
* Parses an Info.plist buffer (XML, UTF-8 BOM'd XML, or binary bplist).
|
|
12
|
+
* Shared by the .app and .zip extractors.
|
|
13
|
+
*/
|
|
14
|
+
function parseInfoPlist(buffer) {
|
|
15
|
+
let data;
|
|
16
|
+
const bufferType = buffer[0];
|
|
17
|
+
// 60 = '<' (XML plist), 239 = UTF-8 BOM, 98 = 'b' (binary "bplist")
|
|
18
|
+
if (bufferType === 60 || bufferType === 239) {
|
|
19
|
+
data = (0, plist_1.parse)(buffer.toString());
|
|
20
|
+
}
|
|
21
|
+
else if (bufferType === 98) {
|
|
22
|
+
data = (0, bplist_parser_1.parseBuffer)(buffer)[0];
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
throw new Error('Unknown plist buffer type.');
|
|
26
|
+
}
|
|
27
|
+
return data;
|
|
28
|
+
}
|
|
10
29
|
/**
|
|
11
30
|
* Extracts metadata from Android APK files
|
|
12
31
|
*/
|
|
@@ -15,9 +34,14 @@ class AndroidMetadataExtractor {
|
|
|
15
34
|
return filePath.endsWith('.apk');
|
|
16
35
|
}
|
|
17
36
|
async extract(filePath) {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
37
|
+
const apk = new node_apk_1.Apk(filePath);
|
|
38
|
+
try {
|
|
39
|
+
const manifest = await apk.getManifestInfo();
|
|
40
|
+
return { appId: manifest.package, platform: 'android' };
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
apk.close();
|
|
44
|
+
}
|
|
21
45
|
}
|
|
22
46
|
}
|
|
23
47
|
exports.AndroidMetadataExtractor = AndroidMetadataExtractor;
|
|
@@ -31,26 +55,10 @@ class IosAppMetadataExtractor {
|
|
|
31
55
|
async extract(filePath) {
|
|
32
56
|
const infoPlistPath = path.normalize(path.join(filePath, 'Info.plist'));
|
|
33
57
|
const buffer = await (0, promises_1.readFile)(infoPlistPath);
|
|
34
|
-
const data =
|
|
58
|
+
const data = parseInfoPlist(buffer);
|
|
35
59
|
const appId = data.CFBundleIdentifier;
|
|
36
60
|
return { appId, platform: 'ios' };
|
|
37
61
|
}
|
|
38
|
-
async parseInfoPlist(buffer) {
|
|
39
|
-
let data;
|
|
40
|
-
const bufferType = buffer[0];
|
|
41
|
-
if (bufferType === 60 ||
|
|
42
|
-
bufferType === '<' ||
|
|
43
|
-
bufferType === 239) {
|
|
44
|
-
data = (0, plist_1.parse)(buffer.toString());
|
|
45
|
-
}
|
|
46
|
-
else if (bufferType === 98) {
|
|
47
|
-
data = (0, bplist_parser_1.parseBuffer)(buffer)[0];
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
throw new Error('Unknown plist buffer type.');
|
|
51
|
-
}
|
|
52
|
-
return data;
|
|
53
|
-
}
|
|
54
62
|
}
|
|
55
63
|
exports.IosAppMetadataExtractor = IosAppMetadataExtractor;
|
|
56
64
|
/**
|
|
@@ -63,48 +71,40 @@ class IosZipMetadataExtractor {
|
|
|
63
71
|
async extract(filePath) {
|
|
64
72
|
return new Promise((resolve, reject) => {
|
|
65
73
|
const zip = new StreamZip({ file: filePath });
|
|
74
|
+
// A throw inside an emitter callback escapes the caller's try/catch and
|
|
75
|
+
// crashes the process, so route all failures through reject explicitly.
|
|
66
76
|
zip.on('ready', () => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
try {
|
|
78
|
+
// Get all entries and sort them by path depth
|
|
79
|
+
const entries = Object.values(zip.entries());
|
|
80
|
+
const sortedEntries = entries.sort((a, b) => {
|
|
81
|
+
const aDepth = a.name.split('/').length;
|
|
82
|
+
const bDepth = b.name.split('/').length;
|
|
83
|
+
return aDepth - bDepth;
|
|
84
|
+
});
|
|
85
|
+
// Find the first Info.plist in the shallowest directory
|
|
86
|
+
const infoPlist = sortedEntries.find((e) => e.name.endsWith('.app/Info.plist'));
|
|
87
|
+
if (!infoPlist) {
|
|
88
|
+
reject(new Error('Failed to find info plist'));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const buffer = zip.entryDataSync(infoPlist.name);
|
|
92
|
+
const data = parseInfoPlist(buffer);
|
|
93
|
+
resolve({ appId: data.CFBundleIdentifier, platform: 'ios' });
|
|
79
94
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
95
|
+
catch (error) {
|
|
96
|
+
reject(error);
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
84
99
|
zip.close();
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
zip.on('error', (error) => {
|
|
103
|
+
zip.close();
|
|
104
|
+
reject(error);
|
|
88
105
|
});
|
|
89
|
-
zip.on('error', reject);
|
|
90
106
|
});
|
|
91
107
|
}
|
|
92
|
-
async parseInfoPlist(buffer) {
|
|
93
|
-
let data;
|
|
94
|
-
const bufferType = buffer[0];
|
|
95
|
-
if (bufferType === 60 ||
|
|
96
|
-
bufferType === '<' ||
|
|
97
|
-
bufferType === 239) {
|
|
98
|
-
data = (0, plist_1.parse)(buffer.toString());
|
|
99
|
-
}
|
|
100
|
-
else if (bufferType === 98) {
|
|
101
|
-
data = (0, bplist_parser_1.parseBuffer)(buffer)[0];
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
104
|
-
throw new Error('Unknown plist buffer type.');
|
|
105
|
-
}
|
|
106
|
-
return data;
|
|
107
|
-
}
|
|
108
108
|
}
|
|
109
109
|
exports.IosZipMetadataExtractor = IosZipMetadataExtractor;
|
|
110
110
|
/**
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MoropoService = void 0;
|
|
4
|
-
const
|
|
4
|
+
const progress_1 = require("../utils/progress");
|
|
5
5
|
const fs = require("node:fs");
|
|
6
6
|
const os = require("node:os");
|
|
7
7
|
const path = require("node:path");
|
|
8
|
+
const node_stream_1 = require("node:stream");
|
|
9
|
+
const promises_1 = require("node:stream/promises");
|
|
8
10
|
const StreamZip = require("node-stream-zip");
|
|
9
11
|
/**
|
|
10
12
|
* Service for downloading and extracting Moropo tests from the Moropo API
|
|
@@ -20,9 +22,10 @@ class MoropoService {
|
|
|
20
22
|
const { apiKey, branchName = 'main', debug = false, quiet = false, json = false, logger } = options;
|
|
21
23
|
this.logDebug(debug, logger, '[DEBUG] Moropo v1 API key detected, downloading tests from Moropo API');
|
|
22
24
|
this.logDebug(debug, logger, `[DEBUG] Using branch name: ${branchName}`);
|
|
25
|
+
let moropoDir;
|
|
23
26
|
try {
|
|
24
27
|
if (!quiet && !json) {
|
|
25
|
-
|
|
28
|
+
progress_1.ux.action.start('Downloading Moropo tests', 'Initializing', {
|
|
26
29
|
stdout: true,
|
|
27
30
|
});
|
|
28
31
|
}
|
|
@@ -36,7 +39,7 @@ class MoropoService {
|
|
|
36
39
|
if (!response.ok) {
|
|
37
40
|
throw new Error(`Failed to download Moropo tests: ${response.statusText}`);
|
|
38
41
|
}
|
|
39
|
-
|
|
42
|
+
moropoDir = path.join(os.tmpdir(), `moropo-tests-${Date.now()}`);
|
|
40
43
|
this.logDebug(debug, logger, `[DEBUG] Extracting Moropo tests to: ${moropoDir}`);
|
|
41
44
|
// Create moropo directory if it doesn't exist
|
|
42
45
|
if (!fs.existsSync(moropoDir)) {
|
|
@@ -49,7 +52,7 @@ class MoropoService {
|
|
|
49
52
|
// Extract zip file
|
|
50
53
|
await this.extractZipFile(zipPath, moropoDir);
|
|
51
54
|
if (!quiet && !json) {
|
|
52
|
-
|
|
55
|
+
progress_1.ux.action.stop('completed');
|
|
53
56
|
}
|
|
54
57
|
this.logDebug(debug, logger, '[DEBUG] Successfully extracted Moropo tests');
|
|
55
58
|
// Create config.yaml file
|
|
@@ -59,7 +62,11 @@ class MoropoService {
|
|
|
59
62
|
}
|
|
60
63
|
catch (error) {
|
|
61
64
|
if (!quiet && !json) {
|
|
62
|
-
|
|
65
|
+
progress_1.ux.action.stop('failed');
|
|
66
|
+
}
|
|
67
|
+
// Remove the temp directory (and any partially-written zip inside it)
|
|
68
|
+
if (moropoDir) {
|
|
69
|
+
fs.rmSync(moropoDir, { recursive: true, force: true });
|
|
63
70
|
}
|
|
64
71
|
this.logDebug(debug, logger, `[DEBUG] Error downloading/extracting Moropo tests: ${error}`);
|
|
65
72
|
throw new Error(`Failed to download/extract Moropo tests: ${error}`);
|
|
@@ -74,28 +81,22 @@ class MoropoService {
|
|
|
74
81
|
const contentLength = response.headers.get('content-length');
|
|
75
82
|
const totalSize = contentLength ? Number.parseInt(contentLength, 10) : 0;
|
|
76
83
|
let downloadedSize = 0;
|
|
77
|
-
|
|
78
|
-
const reader = response.body?.getReader();
|
|
79
|
-
if (!reader) {
|
|
84
|
+
if (!response.body) {
|
|
80
85
|
throw new Error('Failed to get response reader');
|
|
81
86
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
const source = node_stream_1.Readable.fromWeb(response.body);
|
|
88
|
+
if (!quiet && !json && totalSize) {
|
|
89
|
+
// Progress tap — pipeline below still owns the flow/backpressure
|
|
90
|
+
source.on('data', (chunk) => {
|
|
91
|
+
downloadedSize += chunk.length;
|
|
87
92
|
const progress = Math.round((downloadedSize / totalSize) * 100);
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
fileStream.write(value);
|
|
91
|
-
readerResult = await reader.read();
|
|
92
|
-
}
|
|
93
|
-
fileStream.end();
|
|
94
|
-
await new Promise((resolve) => {
|
|
95
|
-
fileStream.on('finish', () => {
|
|
96
|
-
resolve();
|
|
93
|
+
progress_1.ux.action.status = `Downloading: ${progress}%`;
|
|
97
94
|
});
|
|
98
|
-
}
|
|
95
|
+
}
|
|
96
|
+
// pipeline (unlike a bare 'finish' wait) propagates errors from both
|
|
97
|
+
// streams, so disk-full or a stalled download rejects instead of
|
|
98
|
+
// crashing or hanging.
|
|
99
|
+
await (0, promises_1.pipeline)(source, fs.createWriteStream(zipPath));
|
|
99
100
|
}
|
|
100
101
|
async extractZipFile(zipPath, extractPath) {
|
|
101
102
|
// eslint-disable-next-line new-cap
|
|
@@ -111,7 +112,7 @@ class MoropoService {
|
|
|
111
112
|
}
|
|
112
113
|
showProgress(quiet, json, message) {
|
|
113
114
|
if (!quiet && !json) {
|
|
114
|
-
|
|
115
|
+
progress_1.ux.action.status = message;
|
|
115
116
|
}
|
|
116
117
|
}
|
|
117
118
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { AuthContext } from '../types/domain/auth.types';
|
|
1
2
|
export interface DownloadOptions {
|
|
2
|
-
|
|
3
|
+
auth: AuthContext;
|
|
3
4
|
apiUrl: string;
|
|
4
5
|
debug?: boolean;
|
|
5
6
|
logger?: (message: string) => void;
|
|
@@ -40,4 +41,14 @@ export declare class ReportDownloadService {
|
|
|
40
41
|
* @returns Promise that resolves when download is complete
|
|
41
42
|
*/
|
|
42
43
|
private downloadReport;
|
|
44
|
+
/**
|
|
45
|
+
* Warn about a failed download with the underlying cause plus hints for
|
|
46
|
+
* common error classes (missing results, permissions, bad paths)
|
|
47
|
+
* @param warnLogger Warning logger, if configured
|
|
48
|
+
* @param subject What was being downloaded, e.g. 'artifacts' or 'JUNIT report'
|
|
49
|
+
* @param notFoundHint Message to show when the error looks like a 404
|
|
50
|
+
* @param error The error that occurred
|
|
51
|
+
* @returns void
|
|
52
|
+
*/
|
|
53
|
+
private warnDownloadFailure;
|
|
43
54
|
}
|