@devicecloud.dev/dcd 5.0.0-beta.0 → 5.0.0-beta.2
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/LICENSE +21 -0
- package/README.md +52 -0
- package/dist/commands/artifacts.d.ts +28 -28
- package/dist/commands/artifacts.js +20 -23
- package/dist/commands/cloud.d.ts +57 -57
- package/dist/commands/cloud.js +224 -192
- package/dist/commands/list.d.ts +22 -22
- package/dist/commands/list.js +43 -40
- package/dist/commands/live.js +134 -127
- package/dist/commands/login.d.ts +11 -11
- package/dist/commands/login.js +46 -44
- package/dist/commands/logout.js +16 -18
- package/dist/commands/status.d.ts +11 -11
- package/dist/commands/status.js +53 -44
- package/dist/commands/switch-org.d.ts +7 -7
- package/dist/commands/switch-org.js +19 -21
- package/dist/commands/upgrade.js +41 -33
- package/dist/commands/upload.d.ts +10 -10
- package/dist/commands/upload.js +42 -43
- package/dist/commands/whoami.js +17 -20
- package/dist/config/environments.js +6 -12
- package/dist/config/flags/api.flags.js +1 -4
- package/dist/config/flags/binary.flags.js +1 -4
- package/dist/config/flags/device.flags.js +6 -9
- package/dist/config/flags/environment.flags.js +1 -4
- package/dist/config/flags/execution.flags.js +1 -4
- package/dist/config/flags/github.flags.js +1 -4
- package/dist/config/flags/output.flags.js +1 -4
- package/dist/constants.js +15 -18
- package/dist/gateways/api-gateway.d.ts +31 -6
- package/dist/gateways/api-gateway.js +70 -16
- package/dist/gateways/cli-auth-gateway.d.ts +1 -1
- package/dist/gateways/cli-auth-gateway.js +3 -6
- package/dist/gateways/realtime-gateway.d.ts +32 -0
- package/dist/gateways/realtime-gateway.js +103 -0
- package/dist/gateways/supabase-gateway.d.ts +1 -1
- package/dist/gateways/supabase-gateway.js +10 -14
- package/dist/index.js +41 -38
- 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 +32 -1
- package/dist/methods.js +133 -79
- package/dist/services/device-validation.service.d.ts +1 -1
- package/dist/services/device-validation.service.js +1 -5
- package/dist/services/execution-plan.service.js +14 -17
- package/dist/services/execution-plan.utils.js +15 -23
- package/dist/services/flow-paths.d.ts +17 -0
- package/dist/services/flow-paths.js +52 -0
- package/dist/services/metadata-extractor.service.js +22 -25
- package/dist/services/moropo.service.js +18 -20
- package/dist/services/report-download.service.d.ts +1 -1
- package/dist/services/report-download.service.js +5 -9
- package/dist/services/results-polling.service.d.ts +18 -3
- package/dist/services/results-polling.service.js +211 -108
- package/dist/services/telemetry.service.d.ts +10 -1
- package/dist/services/telemetry.service.js +40 -18
- package/dist/services/test-submission.service.d.ts +21 -4
- package/dist/services/test-submission.service.js +51 -34
- package/dist/services/version.service.d.ts +30 -7
- package/dist/services/version.service.js +88 -32
- package/dist/types/domain/auth.types.d.ts +8 -0
- package/dist/types/domain/auth.types.js +1 -2
- package/dist/types/domain/device.types.js +8 -11
- package/dist/types/domain/live.types.js +1 -2
- 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 +1 -1
- package/dist/utils/auth.js +27 -28
- package/dist/utils/ci.d.ts +12 -0
- package/dist/utils/ci.js +39 -0
- package/dist/utils/cli.d.ts +16 -2
- package/dist/utils/cli.js +57 -29
- package/dist/utils/compatibility.d.ts +1 -1
- package/dist/utils/compatibility.js +5 -7
- package/dist/utils/config-store.js +33 -43
- package/dist/utils/connectivity.js +1 -4
- package/dist/utils/expo.js +15 -21
- package/dist/utils/orgs.js +8 -12
- package/dist/utils/paths.js +2 -5
- package/dist/utils/progress.d.ts +3 -0
- package/dist/utils/progress.js +47 -8
- package/dist/utils/styling.d.ts +35 -37
- package/dist/utils/styling.js +52 -86
- package/dist/utils/ui.d.ts +41 -0
- package/dist/utils/ui.js +95 -0
- package/package.json +27 -24
|
@@ -1,14 +1,8 @@
|
|
|
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 []');
|
|
@@ -33,7 +27,7 @@ function getFlowsToRunInSequence(paths, flowOrder, debug = false) {
|
|
|
33
27
|
}
|
|
34
28
|
return namesInOrder.map((name) => paths[name]);
|
|
35
29
|
}
|
|
36
|
-
function isFlowFile(filePath) {
|
|
30
|
+
export function isFlowFile(filePath) {
|
|
37
31
|
// Exclude files inside .app bundles
|
|
38
32
|
// Check if any directory in the path ends with .app
|
|
39
33
|
const pathParts = filePath.split(path.sep);
|
|
@@ -44,7 +38,7 @@ function isFlowFile(filePath) {
|
|
|
44
38
|
}
|
|
45
39
|
return filePath.endsWith('.yaml') || filePath.endsWith('.yml');
|
|
46
40
|
}
|
|
47
|
-
const readYamlFileAsJson = (filePath) => {
|
|
41
|
+
export const readYamlFileAsJson = (filePath) => {
|
|
48
42
|
try {
|
|
49
43
|
const normalizedPath = path.normalize(filePath);
|
|
50
44
|
const yamlText = fs.readFileSync(normalizedPath, 'utf8');
|
|
@@ -61,11 +55,12 @@ const readYamlFileAsJson = (filePath) => {
|
|
|
61
55
|
return result;
|
|
62
56
|
}
|
|
63
57
|
catch (error) {
|
|
64
|
-
throw new Error(`Error parsing YAML file ${filePath}: ${error}
|
|
58
|
+
throw new Error(`Error parsing YAML file ${filePath}: ${error}`, {
|
|
59
|
+
cause: error,
|
|
60
|
+
});
|
|
65
61
|
}
|
|
66
62
|
};
|
|
67
|
-
|
|
68
|
-
const readTestYamlFileAsJson = (filePath) => {
|
|
63
|
+
export const readTestYamlFileAsJson = (filePath) => {
|
|
69
64
|
try {
|
|
70
65
|
const normalizedPath = path.normalize(filePath);
|
|
71
66
|
const yamlText = fs.readFileSync(normalizedPath, 'utf8');
|
|
@@ -88,11 +83,10 @@ const readTestYamlFileAsJson = (filePath) => {
|
|
|
88
83
|
catch (error) {
|
|
89
84
|
const message = `Error parsing YAML file ${filePath}: ${error}`;
|
|
90
85
|
console.error(message);
|
|
91
|
-
throw new Error(message);
|
|
86
|
+
throw new Error(message, { cause: error });
|
|
92
87
|
}
|
|
93
88
|
};
|
|
94
|
-
|
|
95
|
-
async function readDirectory(dir, filterFunction) {
|
|
89
|
+
export async function readDirectory(dir, filterFunction) {
|
|
96
90
|
const readDirResult = await fs.promises.readdir(dir);
|
|
97
91
|
const files = await Promise.all(readDirResult.map(async (file) => {
|
|
98
92
|
const filePath = path.join(dir, file);
|
|
@@ -108,7 +102,7 @@ async function readDirectory(dir, filterFunction) {
|
|
|
108
102
|
}));
|
|
109
103
|
return files.flat().filter(Boolean);
|
|
110
104
|
}
|
|
111
|
-
const checkIfFilesExistInWorkspace = (commandName, command, absoluteFilePath) => {
|
|
105
|
+
export const checkIfFilesExistInWorkspace = (commandName, command, absoluteFilePath) => {
|
|
112
106
|
const errors = [];
|
|
113
107
|
const files = [];
|
|
114
108
|
const directory = path.dirname(absoluteFilePath);
|
|
@@ -136,7 +130,6 @@ const checkIfFilesExistInWorkspace = (commandName, command, absoluteFilePath) =>
|
|
|
136
130
|
processFilePath(x.file);
|
|
137
131
|
return { errors, files };
|
|
138
132
|
};
|
|
139
|
-
exports.checkIfFilesExistInWorkspace = checkIfFilesExistInWorkspace;
|
|
140
133
|
const checkFile = (filePath) => {
|
|
141
134
|
if (!fs.existsSync(filePath))
|
|
142
135
|
return `non-existent file`;
|
|
@@ -149,7 +142,7 @@ const checkStepsArray = (steps, absoluteFilePath) => {
|
|
|
149
142
|
continue;
|
|
150
143
|
for (const [commandName, commandValue] of Object.entries(command)) {
|
|
151
144
|
if (commandsThatRequireFiles.has(commandName)) {
|
|
152
|
-
const { errors: newErrors, files: newFiles } =
|
|
145
|
+
const { errors: newErrors, files: newFiles } = checkIfFilesExistInWorkspace(commandName, commandValue, path.normalize(absoluteFilePath));
|
|
153
146
|
errors = [...errors, ...newErrors];
|
|
154
147
|
files = [...files, ...newFiles];
|
|
155
148
|
}
|
|
@@ -164,7 +157,7 @@ const checkStepsArray = (steps, absoluteFilePath) => {
|
|
|
164
157
|
}
|
|
165
158
|
return { errors, files };
|
|
166
159
|
};
|
|
167
|
-
const processDependencies = ({ config, input, testSteps, }) => {
|
|
160
|
+
export const processDependencies = ({ config, input, testSteps, }) => {
|
|
168
161
|
let allErrors = [];
|
|
169
162
|
let allFiles = [];
|
|
170
163
|
const { onFlowComplete, onFlowStart } = config ?? {};
|
|
@@ -186,4 +179,3 @@ const processDependencies = ({ config, input, testSteps, }) => {
|
|
|
186
179
|
}
|
|
187
180
|
return { allErrors, allFiles };
|
|
188
181
|
};
|
|
189
|
-
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
|
+
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import bplistParser from 'bplist-parser';
|
|
2
|
+
import nodeApk from 'node-apk';
|
|
3
|
+
import { readFile, rm } from 'node:fs/promises';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import StreamZip from 'node-stream-zip';
|
|
6
|
+
import { parse } from 'plist';
|
|
7
|
+
// node-apk and bplist-parser are CJS with no `exports` map; Node's named-export
|
|
8
|
+
// detection for CJS (cjs-module-lexer) is version-dependent, so destructure off
|
|
9
|
+
// the default import instead — that interop is guaranteed on every Node version.
|
|
10
|
+
const { Apk } = nodeApk;
|
|
11
|
+
const { parseBuffer } = bplistParser;
|
|
10
12
|
/**
|
|
11
13
|
* Parses an Info.plist buffer (XML, UTF-8 BOM'd XML, or binary bplist).
|
|
12
14
|
* Shared by the .app and .zip extractors.
|
|
@@ -16,10 +18,10 @@ function parseInfoPlist(buffer) {
|
|
|
16
18
|
const bufferType = buffer[0];
|
|
17
19
|
// 60 = '<' (XML plist), 239 = UTF-8 BOM, 98 = 'b' (binary "bplist")
|
|
18
20
|
if (bufferType === 60 || bufferType === 239) {
|
|
19
|
-
data =
|
|
21
|
+
data = parse(buffer.toString());
|
|
20
22
|
}
|
|
21
23
|
else if (bufferType === 98) {
|
|
22
|
-
data =
|
|
24
|
+
data = parseBuffer(buffer)[0];
|
|
23
25
|
}
|
|
24
26
|
else {
|
|
25
27
|
throw new Error('Unknown plist buffer type.');
|
|
@@ -29,12 +31,12 @@ function parseInfoPlist(buffer) {
|
|
|
29
31
|
/**
|
|
30
32
|
* Extracts metadata from Android APK files
|
|
31
33
|
*/
|
|
32
|
-
class AndroidMetadataExtractor {
|
|
34
|
+
export class AndroidMetadataExtractor {
|
|
33
35
|
canHandle(filePath) {
|
|
34
36
|
return filePath.endsWith('.apk');
|
|
35
37
|
}
|
|
36
38
|
async extract(filePath) {
|
|
37
|
-
const apk = new
|
|
39
|
+
const apk = new Apk(filePath);
|
|
38
40
|
try {
|
|
39
41
|
const manifest = await apk.getManifestInfo();
|
|
40
42
|
return { appId: manifest.package, platform: 'android' };
|
|
@@ -44,27 +46,25 @@ class AndroidMetadataExtractor {
|
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
}
|
|
47
|
-
exports.AndroidMetadataExtractor = AndroidMetadataExtractor;
|
|
48
49
|
/**
|
|
49
50
|
* Extracts metadata from iOS .app directories
|
|
50
51
|
*/
|
|
51
|
-
class IosAppMetadataExtractor {
|
|
52
|
+
export class IosAppMetadataExtractor {
|
|
52
53
|
canHandle(filePath) {
|
|
53
54
|
return filePath.endsWith('.app');
|
|
54
55
|
}
|
|
55
56
|
async extract(filePath) {
|
|
56
57
|
const infoPlistPath = path.normalize(path.join(filePath, 'Info.plist'));
|
|
57
|
-
const buffer = await
|
|
58
|
+
const buffer = await readFile(infoPlistPath);
|
|
58
59
|
const data = parseInfoPlist(buffer);
|
|
59
60
|
const appId = data.CFBundleIdentifier;
|
|
60
61
|
return { appId, platform: 'ios' };
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
|
-
exports.IosAppMetadataExtractor = IosAppMetadataExtractor;
|
|
64
64
|
/**
|
|
65
65
|
* Extracts metadata from iOS .zip files containing .app bundles
|
|
66
66
|
*/
|
|
67
|
-
class IosZipMetadataExtractor {
|
|
67
|
+
export class IosZipMetadataExtractor {
|
|
68
68
|
canHandle(filePath) {
|
|
69
69
|
return filePath.endsWith('.zip');
|
|
70
70
|
}
|
|
@@ -106,18 +106,17 @@ class IosZipMetadataExtractor {
|
|
|
106
106
|
});
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
-
exports.IosZipMetadataExtractor = IosZipMetadataExtractor;
|
|
110
109
|
/**
|
|
111
110
|
* Extracts metadata from Expo iOS .tar.gz archives by extracting the
|
|
112
111
|
* archive to a temp directory, finding the .app bundle inside, then
|
|
113
112
|
* delegating to IosAppMetadataExtractor.
|
|
114
113
|
*/
|
|
115
|
-
class ExpoTarGzMetadataExtractor {
|
|
114
|
+
export class ExpoTarGzMetadataExtractor {
|
|
116
115
|
canHandle(filePath) {
|
|
117
116
|
return filePath.endsWith('.tar.gz');
|
|
118
117
|
}
|
|
119
118
|
async extract(filePath) {
|
|
120
|
-
const { extractTarGz, findAppBundle } = await
|
|
119
|
+
const { extractTarGz, findAppBundle } = await import('../utils/expo.js');
|
|
121
120
|
const extractDir = await extractTarGz(filePath, false);
|
|
122
121
|
try {
|
|
123
122
|
const appPath = await findAppBundle(extractDir);
|
|
@@ -125,15 +124,14 @@ class ExpoTarGzMetadataExtractor {
|
|
|
125
124
|
return await iosExtractor.extract(appPath);
|
|
126
125
|
}
|
|
127
126
|
finally {
|
|
128
|
-
await
|
|
127
|
+
await rm(extractDir, { recursive: true, force: true }).catch(() => { });
|
|
129
128
|
}
|
|
130
129
|
}
|
|
131
130
|
}
|
|
132
|
-
exports.ExpoTarGzMetadataExtractor = ExpoTarGzMetadataExtractor;
|
|
133
131
|
/**
|
|
134
132
|
* Service for extracting app metadata from various file formats
|
|
135
133
|
*/
|
|
136
|
-
class MetadataExtractorService {
|
|
134
|
+
export class MetadataExtractorService {
|
|
137
135
|
extractors = [
|
|
138
136
|
new AndroidMetadataExtractor(),
|
|
139
137
|
new ExpoTarGzMetadataExtractor(),
|
|
@@ -159,4 +157,3 @@ class MetadataExtractorService {
|
|
|
159
157
|
}
|
|
160
158
|
}
|
|
161
159
|
}
|
|
162
|
-
exports.MetadataExtractorService = MetadataExtractorService;
|
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const node_stream_1 = require("node:stream");
|
|
9
|
-
const promises_1 = require("node:stream/promises");
|
|
10
|
-
const StreamZip = require("node-stream-zip");
|
|
1
|
+
import { ux } from '../utils/progress.js';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { Readable } from 'node:stream';
|
|
6
|
+
import { pipeline } from 'node:stream/promises';
|
|
7
|
+
import StreamZip from 'node-stream-zip';
|
|
11
8
|
/**
|
|
12
9
|
* Service for downloading and extracting Moropo tests from the Moropo API
|
|
13
10
|
*/
|
|
14
|
-
class MoropoService {
|
|
11
|
+
export class MoropoService {
|
|
15
12
|
MOROPO_API_URL = 'https://api.moropo.com/tests';
|
|
16
13
|
/**
|
|
17
14
|
* Download and extract Moropo tests from the API
|
|
@@ -25,7 +22,7 @@ class MoropoService {
|
|
|
25
22
|
let moropoDir;
|
|
26
23
|
try {
|
|
27
24
|
if (!quiet && !json) {
|
|
28
|
-
|
|
25
|
+
ux.action.start('Downloading Moropo tests', 'Initializing', {
|
|
29
26
|
stdout: true,
|
|
30
27
|
});
|
|
31
28
|
}
|
|
@@ -52,7 +49,7 @@ class MoropoService {
|
|
|
52
49
|
// Extract zip file
|
|
53
50
|
await this.extractZipFile(zipPath, moropoDir);
|
|
54
51
|
if (!quiet && !json) {
|
|
55
|
-
|
|
52
|
+
ux.action.stop('completed');
|
|
56
53
|
}
|
|
57
54
|
this.logDebug(debug, logger, '[DEBUG] Successfully extracted Moropo tests');
|
|
58
55
|
// Create config.yaml file
|
|
@@ -62,14 +59,16 @@ class MoropoService {
|
|
|
62
59
|
}
|
|
63
60
|
catch (error) {
|
|
64
61
|
if (!quiet && !json) {
|
|
65
|
-
|
|
62
|
+
ux.action.stop('failed');
|
|
66
63
|
}
|
|
67
64
|
// Remove the temp directory (and any partially-written zip inside it)
|
|
68
65
|
if (moropoDir) {
|
|
69
66
|
fs.rmSync(moropoDir, { recursive: true, force: true });
|
|
70
67
|
}
|
|
71
68
|
this.logDebug(debug, logger, `[DEBUG] Error downloading/extracting Moropo tests: ${error}`);
|
|
72
|
-
throw new Error(`Failed to download/extract Moropo tests: ${error}
|
|
69
|
+
throw new Error(`Failed to download/extract Moropo tests: ${error}`, {
|
|
70
|
+
cause: error,
|
|
71
|
+
});
|
|
73
72
|
}
|
|
74
73
|
}
|
|
75
74
|
createConfigFile(moropoDir) {
|
|
@@ -84,19 +83,19 @@ class MoropoService {
|
|
|
84
83
|
if (!response.body) {
|
|
85
84
|
throw new Error('Failed to get response reader');
|
|
86
85
|
}
|
|
87
|
-
const source =
|
|
86
|
+
const source = Readable.fromWeb(response.body);
|
|
88
87
|
if (!quiet && !json && totalSize) {
|
|
89
88
|
// Progress tap — pipeline below still owns the flow/backpressure
|
|
90
89
|
source.on('data', (chunk) => {
|
|
91
90
|
downloadedSize += chunk.length;
|
|
92
91
|
const progress = Math.round((downloadedSize / totalSize) * 100);
|
|
93
|
-
|
|
92
|
+
ux.action.status = `Downloading: ${progress}%`;
|
|
94
93
|
});
|
|
95
94
|
}
|
|
96
95
|
// pipeline (unlike a bare 'finish' wait) propagates errors from both
|
|
97
96
|
// streams, so disk-full or a stalled download rejects instead of
|
|
98
97
|
// crashing or hanging.
|
|
99
|
-
await
|
|
98
|
+
await pipeline(source, fs.createWriteStream(zipPath));
|
|
100
99
|
}
|
|
101
100
|
async extractZipFile(zipPath, extractPath) {
|
|
102
101
|
// eslint-disable-next-line new-cap
|
|
@@ -112,8 +111,7 @@ class MoropoService {
|
|
|
112
111
|
}
|
|
113
112
|
showProgress(quiet, json, message) {
|
|
114
113
|
if (!quiet && !json) {
|
|
115
|
-
|
|
114
|
+
ux.action.status = message;
|
|
116
115
|
}
|
|
117
116
|
}
|
|
118
117
|
}
|
|
119
|
-
exports.MoropoService = MoropoService;
|
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
exports.ReportDownloadService = void 0;
|
|
4
|
-
const path = require("node:path");
|
|
5
|
-
const api_gateway_1 = require("../gateways/api-gateway");
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import { ApiGateway } from '../gateways/api-gateway.js';
|
|
6
3
|
/**
|
|
7
4
|
* Service for downloading test artifacts and reports
|
|
8
5
|
*/
|
|
9
|
-
class ReportDownloadService {
|
|
6
|
+
export class ReportDownloadService {
|
|
10
7
|
/**
|
|
11
8
|
* Download test artifacts as a zip file
|
|
12
9
|
* @param options Download configuration
|
|
@@ -18,7 +15,7 @@ class ReportDownloadService {
|
|
|
18
15
|
if (debug && logger) {
|
|
19
16
|
logger(`[DEBUG] Downloading artifacts: ${downloadType}`);
|
|
20
17
|
}
|
|
21
|
-
await
|
|
18
|
+
await ApiGateway.downloadArtifactsZip(apiUrl, auth, uploadId, downloadType, artifactsPath);
|
|
22
19
|
if (logger) {
|
|
23
20
|
logger('\n');
|
|
24
21
|
logger(`Test artifacts have been downloaded to ${artifactsPath}`);
|
|
@@ -84,7 +81,7 @@ class ReportDownloadService {
|
|
|
84
81
|
if (debug && logger) {
|
|
85
82
|
logger(`[DEBUG] Downloading ${type.toUpperCase()} report`);
|
|
86
83
|
}
|
|
87
|
-
await
|
|
84
|
+
await ApiGateway.downloadReportGeneric(apiUrl, auth, uploadId, type, filePath);
|
|
88
85
|
if (logger) {
|
|
89
86
|
logger(`${type.toUpperCase()} test report has been downloaded to ${filePath}`);
|
|
90
87
|
}
|
|
@@ -122,4 +119,3 @@ class ReportDownloadService {
|
|
|
122
119
|
}
|
|
123
120
|
}
|
|
124
121
|
}
|
|
125
|
-
exports.ReportDownloadService = ReportDownloadService;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AuthContext } from '../types/domain/auth.types';
|
|
1
|
+
import type { AuthContext } from '../types/domain/auth.types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Custom error for run failures that includes the polling result
|
|
4
4
|
*/
|
|
@@ -46,7 +46,9 @@ export interface PollingResult {
|
|
|
46
46
|
*/
|
|
47
47
|
export declare class ResultsPollingService {
|
|
48
48
|
private readonly MAX_SEQUENTIAL_FAILURES;
|
|
49
|
-
private readonly
|
|
49
|
+
private readonly BEARER_POLL_INTERVAL_MS;
|
|
50
|
+
private readonly APIKEY_POLL_INTERVAL_MS;
|
|
51
|
+
private readonly ERROR_BACKOFF_BASE_MS;
|
|
50
52
|
private readonly MAX_ERROR_BACKOFF_MS;
|
|
51
53
|
/**
|
|
52
54
|
* Poll for test results until all tests complete
|
|
@@ -91,5 +93,18 @@ export declare class ResultsPollingService {
|
|
|
91
93
|
* @returns Promise that resolves after the delay
|
|
92
94
|
*/
|
|
93
95
|
private sleep;
|
|
94
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Build the body of the live status display (the per-test table, or just the
|
|
98
|
+
* one-line summary in quiet mode). The footer (countdown + realtime state) is
|
|
99
|
+
* appended separately by {@link buildStatusFooter} so it can re-render on a
|
|
100
|
+
* timer without re-fetching.
|
|
101
|
+
*/
|
|
102
|
+
private buildStatusBody;
|
|
103
|
+
/**
|
|
104
|
+
* Build the live footer shown under the status display: whether realtime
|
|
105
|
+
* updates are connected (for logged-in users) and how long until the next
|
|
106
|
+
* backstop poll. While a fetch is in flight (`nextPollAt` is null) the
|
|
107
|
+
* countdown reads "refreshing…". In quiet mode the countdown is omitted.
|
|
108
|
+
*/
|
|
109
|
+
private buildStatusFooter;
|
|
95
110
|
}
|