@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,123 +1,122 @@
|
|
|
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;
|
|
12
|
+
/**
|
|
13
|
+
* Parses an Info.plist buffer (XML, UTF-8 BOM'd XML, or binary bplist).
|
|
14
|
+
* Shared by the .app and .zip extractors.
|
|
15
|
+
*/
|
|
16
|
+
function parseInfoPlist(buffer) {
|
|
17
|
+
let data;
|
|
18
|
+
const bufferType = buffer[0];
|
|
19
|
+
// 60 = '<' (XML plist), 239 = UTF-8 BOM, 98 = 'b' (binary "bplist")
|
|
20
|
+
if (bufferType === 60 || bufferType === 239) {
|
|
21
|
+
data = parse(buffer.toString());
|
|
22
|
+
}
|
|
23
|
+
else if (bufferType === 98) {
|
|
24
|
+
data = parseBuffer(buffer)[0];
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
throw new Error('Unknown plist buffer type.');
|
|
28
|
+
}
|
|
29
|
+
return data;
|
|
30
|
+
}
|
|
10
31
|
/**
|
|
11
32
|
* Extracts metadata from Android APK files
|
|
12
33
|
*/
|
|
13
|
-
class AndroidMetadataExtractor {
|
|
34
|
+
export class AndroidMetadataExtractor {
|
|
14
35
|
canHandle(filePath) {
|
|
15
36
|
return filePath.endsWith('.apk');
|
|
16
37
|
}
|
|
17
38
|
async extract(filePath) {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
39
|
+
const apk = new Apk(filePath);
|
|
40
|
+
try {
|
|
41
|
+
const manifest = await apk.getManifestInfo();
|
|
42
|
+
return { appId: manifest.package, platform: 'android' };
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
apk.close();
|
|
46
|
+
}
|
|
21
47
|
}
|
|
22
48
|
}
|
|
23
|
-
exports.AndroidMetadataExtractor = AndroidMetadataExtractor;
|
|
24
49
|
/**
|
|
25
50
|
* Extracts metadata from iOS .app directories
|
|
26
51
|
*/
|
|
27
|
-
class IosAppMetadataExtractor {
|
|
52
|
+
export class IosAppMetadataExtractor {
|
|
28
53
|
canHandle(filePath) {
|
|
29
54
|
return filePath.endsWith('.app');
|
|
30
55
|
}
|
|
31
56
|
async extract(filePath) {
|
|
32
57
|
const infoPlistPath = path.normalize(path.join(filePath, 'Info.plist'));
|
|
33
|
-
const buffer = await
|
|
34
|
-
const data =
|
|
58
|
+
const buffer = await readFile(infoPlistPath);
|
|
59
|
+
const data = parseInfoPlist(buffer);
|
|
35
60
|
const appId = data.CFBundleIdentifier;
|
|
36
61
|
return { appId, platform: 'ios' };
|
|
37
62
|
}
|
|
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
63
|
}
|
|
55
|
-
exports.IosAppMetadataExtractor = IosAppMetadataExtractor;
|
|
56
64
|
/**
|
|
57
65
|
* Extracts metadata from iOS .zip files containing .app bundles
|
|
58
66
|
*/
|
|
59
|
-
class IosZipMetadataExtractor {
|
|
67
|
+
export class IosZipMetadataExtractor {
|
|
60
68
|
canHandle(filePath) {
|
|
61
69
|
return filePath.endsWith('.zip');
|
|
62
70
|
}
|
|
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
|
-
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,15 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
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';
|
|
9
8
|
/**
|
|
10
9
|
* Service for downloading and extracting Moropo tests from the Moropo API
|
|
11
10
|
*/
|
|
12
|
-
class MoropoService {
|
|
11
|
+
export class MoropoService {
|
|
13
12
|
MOROPO_API_URL = 'https://api.moropo.com/tests';
|
|
14
13
|
/**
|
|
15
14
|
* Download and extract Moropo tests from the API
|
|
@@ -20,9 +19,10 @@ class MoropoService {
|
|
|
20
19
|
const { apiKey, branchName = 'main', debug = false, quiet = false, json = false, logger } = options;
|
|
21
20
|
this.logDebug(debug, logger, '[DEBUG] Moropo v1 API key detected, downloading tests from Moropo API');
|
|
22
21
|
this.logDebug(debug, logger, `[DEBUG] Using branch name: ${branchName}`);
|
|
22
|
+
let moropoDir;
|
|
23
23
|
try {
|
|
24
24
|
if (!quiet && !json) {
|
|
25
|
-
|
|
25
|
+
ux.action.start('Downloading Moropo tests', 'Initializing', {
|
|
26
26
|
stdout: true,
|
|
27
27
|
});
|
|
28
28
|
}
|
|
@@ -36,7 +36,7 @@ class MoropoService {
|
|
|
36
36
|
if (!response.ok) {
|
|
37
37
|
throw new Error(`Failed to download Moropo tests: ${response.statusText}`);
|
|
38
38
|
}
|
|
39
|
-
|
|
39
|
+
moropoDir = path.join(os.tmpdir(), `moropo-tests-${Date.now()}`);
|
|
40
40
|
this.logDebug(debug, logger, `[DEBUG] Extracting Moropo tests to: ${moropoDir}`);
|
|
41
41
|
// Create moropo directory if it doesn't exist
|
|
42
42
|
if (!fs.existsSync(moropoDir)) {
|
|
@@ -49,7 +49,7 @@ class MoropoService {
|
|
|
49
49
|
// Extract zip file
|
|
50
50
|
await this.extractZipFile(zipPath, moropoDir);
|
|
51
51
|
if (!quiet && !json) {
|
|
52
|
-
|
|
52
|
+
ux.action.stop('completed');
|
|
53
53
|
}
|
|
54
54
|
this.logDebug(debug, logger, '[DEBUG] Successfully extracted Moropo tests');
|
|
55
55
|
// Create config.yaml file
|
|
@@ -59,10 +59,16 @@ class MoropoService {
|
|
|
59
59
|
}
|
|
60
60
|
catch (error) {
|
|
61
61
|
if (!quiet && !json) {
|
|
62
|
-
|
|
62
|
+
ux.action.stop('failed');
|
|
63
|
+
}
|
|
64
|
+
// Remove the temp directory (and any partially-written zip inside it)
|
|
65
|
+
if (moropoDir) {
|
|
66
|
+
fs.rmSync(moropoDir, { recursive: true, force: true });
|
|
63
67
|
}
|
|
64
68
|
this.logDebug(debug, logger, `[DEBUG] Error downloading/extracting Moropo tests: ${error}`);
|
|
65
|
-
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
|
+
});
|
|
66
72
|
}
|
|
67
73
|
}
|
|
68
74
|
createConfigFile(moropoDir) {
|
|
@@ -74,28 +80,22 @@ class MoropoService {
|
|
|
74
80
|
const contentLength = response.headers.get('content-length');
|
|
75
81
|
const totalSize = contentLength ? Number.parseInt(contentLength, 10) : 0;
|
|
76
82
|
let downloadedSize = 0;
|
|
77
|
-
|
|
78
|
-
const reader = response.body?.getReader();
|
|
79
|
-
if (!reader) {
|
|
83
|
+
if (!response.body) {
|
|
80
84
|
throw new Error('Failed to get response reader');
|
|
81
85
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
const source = Readable.fromWeb(response.body);
|
|
87
|
+
if (!quiet && !json && totalSize) {
|
|
88
|
+
// Progress tap — pipeline below still owns the flow/backpressure
|
|
89
|
+
source.on('data', (chunk) => {
|
|
90
|
+
downloadedSize += chunk.length;
|
|
87
91
|
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();
|
|
92
|
+
ux.action.status = `Downloading: ${progress}%`;
|
|
97
93
|
});
|
|
98
|
-
}
|
|
94
|
+
}
|
|
95
|
+
// pipeline (unlike a bare 'finish' wait) propagates errors from both
|
|
96
|
+
// streams, so disk-full or a stalled download rejects instead of
|
|
97
|
+
// crashing or hanging.
|
|
98
|
+
await pipeline(source, fs.createWriteStream(zipPath));
|
|
99
99
|
}
|
|
100
100
|
async extractZipFile(zipPath, extractPath) {
|
|
101
101
|
// eslint-disable-next-line new-cap
|
|
@@ -111,8 +111,7 @@ class MoropoService {
|
|
|
111
111
|
}
|
|
112
112
|
showProgress(quiet, json, message) {
|
|
113
113
|
if (!quiet && !json) {
|
|
114
|
-
|
|
114
|
+
ux.action.status = message;
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
-
exports.MoropoService = MoropoService;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { AuthContext } from '../types/domain/auth.types.js';
|
|
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
|
}
|
|
@@ -1,24 +1,21 @@
|
|
|
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
|
|
13
10
|
* @returns Promise that resolves when download is complete
|
|
14
11
|
*/
|
|
15
12
|
async downloadArtifacts(options) {
|
|
16
|
-
const { apiUrl,
|
|
13
|
+
const { apiUrl, auth, uploadId, downloadType, artifactsPath = './artifacts.zip', debug = false, logger, warnLogger, } = options;
|
|
17
14
|
try {
|
|
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}`);
|
|
@@ -28,9 +25,7 @@ class ReportDownloadService {
|
|
|
28
25
|
if (debug && logger) {
|
|
29
26
|
logger(`[DEBUG] Error downloading artifacts: ${error}`);
|
|
30
27
|
}
|
|
31
|
-
|
|
32
|
-
warnLogger('Failed to download artifacts');
|
|
33
|
-
}
|
|
28
|
+
this.warnDownloadFailure(warnLogger, 'artifacts', 'No artifacts found for this upload. Make sure your tests generated results.', error);
|
|
34
29
|
}
|
|
35
30
|
}
|
|
36
31
|
/**
|
|
@@ -81,12 +76,12 @@ class ReportDownloadService {
|
|
|
81
76
|
* @returns Promise that resolves when download is complete
|
|
82
77
|
*/
|
|
83
78
|
async downloadReport(type, filePath, options) {
|
|
84
|
-
const { apiUrl,
|
|
79
|
+
const { apiUrl, auth, uploadId, debug = false, logger, warnLogger } = options;
|
|
85
80
|
try {
|
|
86
81
|
if (debug && logger) {
|
|
87
82
|
logger(`[DEBUG] Downloading ${type.toUpperCase()} report`);
|
|
88
83
|
}
|
|
89
|
-
await
|
|
84
|
+
await ApiGateway.downloadReportGeneric(apiUrl, auth, uploadId, type, filePath);
|
|
90
85
|
if (logger) {
|
|
91
86
|
logger(`${type.toUpperCase()} test report has been downloaded to ${filePath}`);
|
|
92
87
|
}
|
|
@@ -95,20 +90,32 @@ class ReportDownloadService {
|
|
|
95
90
|
if (debug && logger) {
|
|
96
91
|
logger(`[DEBUG] Error downloading ${type.toUpperCase()} report: ${error}`);
|
|
97
92
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
93
|
+
this.warnDownloadFailure(warnLogger, `${type.toUpperCase()} report`, `No ${type.toUpperCase()} reports found for this upload. Make sure your tests generated results.`, error);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Warn about a failed download with the underlying cause plus hints for
|
|
98
|
+
* common error classes (missing results, permissions, bad paths)
|
|
99
|
+
* @param warnLogger Warning logger, if configured
|
|
100
|
+
* @param subject What was being downloaded, e.g. 'artifacts' or 'JUNIT report'
|
|
101
|
+
* @param notFoundHint Message to show when the error looks like a 404
|
|
102
|
+
* @param error The error that occurred
|
|
103
|
+
* @returns void
|
|
104
|
+
*/
|
|
105
|
+
warnDownloadFailure(warnLogger, subject, notFoundHint, error) {
|
|
106
|
+
if (!warnLogger) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
110
|
+
warnLogger(`Failed to download ${subject}: ${errorMessage}`);
|
|
111
|
+
if (errorMessage.includes('404')) {
|
|
112
|
+
warnLogger(notFoundHint);
|
|
113
|
+
}
|
|
114
|
+
else if (errorMessage.includes('EACCES') || errorMessage.includes('EPERM')) {
|
|
115
|
+
warnLogger('Permission denied. Check write permissions for the current directory.');
|
|
116
|
+
}
|
|
117
|
+
else if (errorMessage.includes('ENOENT')) {
|
|
118
|
+
warnLogger('Directory does not exist. Make sure you have write access to the current directory.');
|
|
111
119
|
}
|
|
112
120
|
}
|
|
113
121
|
}
|
|
114
|
-
exports.ReportDownloadService = ReportDownloadService;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type TestResult = paths['/results/{uploadId}']['get']['responses']['200']['content']['application/json']['results'][number];
|
|
1
|
+
import type { AuthContext } from '../types/domain/auth.types.js';
|
|
3
2
|
/**
|
|
4
3
|
* Custom error for run failures that includes the polling result
|
|
5
4
|
*/
|
|
@@ -8,7 +7,7 @@ export declare class RunFailedError extends Error {
|
|
|
8
7
|
constructor(result: PollingResult);
|
|
9
8
|
}
|
|
10
9
|
export interface PollingOptions {
|
|
11
|
-
|
|
10
|
+
auth: AuthContext;
|
|
12
11
|
apiUrl: string;
|
|
13
12
|
consoleUrl: string;
|
|
14
13
|
debug?: boolean;
|
|
@@ -47,22 +46,25 @@ export interface PollingResult {
|
|
|
47
46
|
*/
|
|
48
47
|
export declare class ResultsPollingService {
|
|
49
48
|
private readonly MAX_SEQUENTIAL_FAILURES;
|
|
50
|
-
private readonly
|
|
49
|
+
private readonly BEARER_POLL_INTERVAL_MS;
|
|
50
|
+
private readonly APIKEY_POLL_INTERVAL_MS;
|
|
51
|
+
private readonly ERROR_BACKOFF_BASE_MS;
|
|
52
|
+
private readonly MAX_ERROR_BACKOFF_MS;
|
|
51
53
|
/**
|
|
52
54
|
* Poll for test results until all tests complete
|
|
53
|
-
* @param results Initial test results from submission
|
|
54
55
|
* @param options Polling configuration
|
|
55
56
|
* @param testMetadata Optional metadata map for each test (flowName, tags)
|
|
56
57
|
* @returns Promise that resolves with final test results or rejects if tests fail
|
|
57
58
|
*/
|
|
58
|
-
pollUntilComplete(
|
|
59
|
+
pollUntilComplete(options: PollingOptions, testMetadata?: Record<string, TestMetadata>): Promise<PollingResult>;
|
|
60
|
+
private pollLoop;
|
|
59
61
|
private buildPollingResult;
|
|
60
62
|
private calculateStatusSummary;
|
|
61
63
|
private displayFinalResults;
|
|
62
64
|
/**
|
|
63
65
|
* Fetch results from API and log debug information
|
|
64
66
|
* @param apiUrl API base URL
|
|
65
|
-
* @param
|
|
67
|
+
* @param auth AuthContext carrying request headers
|
|
66
68
|
* @param uploadId Upload ID to fetch results for
|
|
67
69
|
* @param debug Whether debug logging is enabled
|
|
68
70
|
* @param logger Optional logger function
|
|
@@ -91,6 +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…".
|
|
108
|
+
*/
|
|
109
|
+
private buildStatusFooter;
|
|
95
110
|
}
|
|
96
|
-
export {};
|