@devicecloud.dev/dcd 4.4.9 → 5.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +75 -2
  2. package/dist/commands/artifacts.d.ts +47 -18
  3. package/dist/commands/artifacts.js +69 -64
  4. package/dist/commands/cloud.d.ts +228 -88
  5. package/dist/commands/cloud.js +430 -342
  6. package/dist/commands/list.d.ts +39 -38
  7. package/dist/commands/list.js +124 -131
  8. package/dist/commands/live.d.ts +2 -0
  9. package/dist/commands/live.js +520 -0
  10. package/dist/commands/login.d.ts +17 -0
  11. package/dist/commands/login.js +252 -0
  12. package/dist/commands/logout.d.ts +2 -0
  13. package/dist/commands/logout.js +30 -0
  14. package/dist/commands/status.d.ts +23 -42
  15. package/dist/commands/status.js +170 -179
  16. package/dist/commands/switch-org.d.ts +12 -0
  17. package/dist/commands/switch-org.js +76 -0
  18. package/dist/commands/upgrade.d.ts +2 -0
  19. package/dist/commands/upgrade.js +120 -0
  20. package/dist/commands/upload.d.ts +33 -18
  21. package/dist/commands/upload.js +72 -78
  22. package/dist/commands/whoami.d.ts +2 -0
  23. package/dist/commands/whoami.js +31 -0
  24. package/dist/config/environments.d.ts +31 -0
  25. package/dist/config/environments.js +52 -0
  26. package/dist/config/flags/api.flags.d.ts +10 -2
  27. package/dist/config/flags/api.flags.js +13 -14
  28. package/dist/config/flags/binary.flags.d.ts +17 -4
  29. package/dist/config/flags/binary.flags.js +14 -18
  30. package/dist/config/flags/device.flags.d.ts +49 -11
  31. package/dist/config/flags/device.flags.js +43 -38
  32. package/dist/config/flags/environment.flags.d.ts +27 -6
  33. package/dist/config/flags/environment.flags.js +24 -29
  34. package/dist/config/flags/execution.flags.d.ts +35 -8
  35. package/dist/config/flags/execution.flags.js +31 -41
  36. package/dist/config/flags/github.flags.d.ts +23 -5
  37. package/dist/config/flags/github.flags.js +19 -15
  38. package/dist/config/flags/output.flags.d.ts +57 -13
  39. package/dist/config/flags/output.flags.js +48 -47
  40. package/dist/constants.d.ts +218 -51
  41. package/dist/constants.js +17 -20
  42. package/dist/gateways/api-gateway.d.ts +72 -16
  43. package/dist/gateways/api-gateway.js +298 -104
  44. package/dist/gateways/cli-auth-gateway.d.ts +13 -0
  45. package/dist/gateways/cli-auth-gateway.js +54 -0
  46. package/dist/gateways/realtime-gateway.d.ts +32 -0
  47. package/dist/gateways/realtime-gateway.js +103 -0
  48. package/dist/gateways/supabase-gateway.d.ts +11 -11
  49. package/dist/gateways/supabase-gateway.js +20 -48
  50. package/dist/index.d.ts +2 -1
  51. package/dist/index.js +98 -4
  52. package/dist/mcp/context.d.ts +33 -0
  53. package/dist/mcp/context.js +33 -0
  54. package/dist/mcp/helpers.d.ts +16 -0
  55. package/dist/mcp/helpers.js +34 -0
  56. package/dist/mcp/index.d.ts +2 -0
  57. package/dist/mcp/index.js +24 -0
  58. package/dist/mcp/server.d.ts +7 -0
  59. package/dist/mcp/server.js +27 -0
  60. package/dist/mcp/tools/download-artifacts.d.ts +11 -0
  61. package/dist/mcp/tools/download-artifacts.js +84 -0
  62. package/dist/mcp/tools/get-status.d.ts +7 -0
  63. package/dist/mcp/tools/get-status.js +39 -0
  64. package/dist/mcp/tools/list-devices.d.ts +7 -0
  65. package/dist/mcp/tools/list-devices.js +27 -0
  66. package/dist/mcp/tools/list-runs.d.ts +3 -0
  67. package/dist/mcp/tools/list-runs.js +60 -0
  68. package/dist/mcp/tools/run-cloud-test.d.ts +14 -0
  69. package/dist/mcp/tools/run-cloud-test.js +233 -0
  70. package/dist/methods.d.ts +34 -5
  71. package/dist/methods.js +266 -215
  72. package/dist/services/device-validation.service.d.ts +9 -1
  73. package/dist/services/device-validation.service.js +56 -40
  74. package/dist/services/execution-plan.service.js +40 -31
  75. package/dist/services/execution-plan.utils.d.ts +3 -0
  76. package/dist/services/execution-plan.utils.js +25 -55
  77. package/dist/services/flow-paths.d.ts +17 -0
  78. package/dist/services/flow-paths.js +52 -0
  79. package/dist/services/metadata-extractor.service.d.ts +0 -2
  80. package/dist/services/metadata-extractor.service.js +75 -78
  81. package/dist/services/moropo.service.js +33 -34
  82. package/dist/services/report-download.service.d.ts +12 -1
  83. package/dist/services/report-download.service.js +34 -27
  84. package/dist/services/results-polling.service.d.ts +23 -9
  85. package/dist/services/results-polling.service.js +257 -123
  86. package/dist/services/telemetry.service.d.ts +49 -0
  87. package/dist/services/telemetry.service.js +252 -0
  88. package/dist/services/test-submission.service.d.ts +21 -4
  89. package/dist/services/test-submission.service.js +51 -33
  90. package/dist/services/version.service.d.ts +4 -3
  91. package/dist/services/version.service.js +28 -16
  92. package/dist/types/domain/auth.types.d.ts +20 -0
  93. package/dist/types/domain/auth.types.js +1 -0
  94. package/dist/types/domain/device.types.js +8 -11
  95. package/dist/types/domain/live.types.d.ts +76 -0
  96. package/dist/types/domain/live.types.js +3 -0
  97. package/dist/types/generated/schema.types.js +1 -2
  98. package/dist/types/index.d.ts +2 -2
  99. package/dist/types/index.js +2 -18
  100. package/dist/types.js +1 -2
  101. package/dist/utils/auth.d.ts +13 -0
  102. package/dist/utils/auth.js +141 -0
  103. package/dist/utils/ci.d.ts +12 -0
  104. package/dist/utils/ci.js +39 -0
  105. package/dist/utils/cli.d.ts +35 -0
  106. package/dist/utils/cli.js +118 -0
  107. package/dist/utils/compatibility.d.ts +2 -1
  108. package/dist/utils/compatibility.js +6 -8
  109. package/dist/utils/config-store.d.ts +35 -0
  110. package/dist/utils/config-store.js +115 -0
  111. package/dist/utils/connectivity.js +8 -7
  112. package/dist/utils/expo.js +29 -24
  113. package/dist/utils/orgs.d.ts +11 -0
  114. package/dist/utils/orgs.js +36 -0
  115. package/dist/utils/paths.d.ts +11 -0
  116. package/dist/utils/paths.js +21 -0
  117. package/dist/utils/progress.d.ts +13 -0
  118. package/dist/utils/progress.js +47 -0
  119. package/dist/utils/styling.d.ts +42 -36
  120. package/dist/utils/styling.js +78 -82
  121. package/dist/utils/ui.d.ts +41 -0
  122. package/dist/utils/ui.js +95 -0
  123. package/package.json +36 -45
  124. package/bin/dev.cmd +0 -3
  125. package/bin/dev.js +0 -6
  126. package/bin/run.cmd +0 -3
  127. package/bin/run.js +0 -7
  128. package/dist/types/schema.types.d.ts +0 -2702
  129. package/dist/types/schema.types.js +0 -3
  130. package/oclif.manifest.json +0 -884
@@ -1,123 +1,122 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MetadataExtractorService = exports.ExpoTarGzMetadataExtractor = exports.IosZipMetadataExtractor = exports.IosAppMetadataExtractor = exports.AndroidMetadataExtractor = void 0;
4
- const AppInfoParser = require("app-info-parser");
5
- const bplist_parser_1 = require("bplist-parser");
6
- const promises_1 = require("node:fs/promises");
7
- const path = require("node:path");
8
- const StreamZip = require("node-stream-zip");
9
- const plist_1 = require("plist");
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 parser = new AppInfoParser(filePath);
19
- const result = await parser.parse();
20
- return { appId: result.package, platform: 'android' };
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 (0, promises_1.readFile)(infoPlistPath);
34
- const data = await this.parseInfoPlist(buffer);
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
- // Get all entries and sort them by path depth
68
- const entries = Object.values(zip.entries());
69
- const sortedEntries = entries.sort((a, b) => {
70
- const aDepth = a.name.split('/').length;
71
- const bDepth = b.name.split('/').length;
72
- return aDepth - bDepth;
73
- });
74
- // Find the first Info.plist in the shallowest directory
75
- const infoPlist = sortedEntries.find((e) => e.name.endsWith('.app/Info.plist'));
76
- if (!infoPlist) {
77
- reject(new Error('Failed to find info plist'));
78
- return;
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
- const buffer = zip.entryDataSync(infoPlist.name);
81
- this.parseInfoPlist(buffer)
82
- .then((data) => {
83
- const appId = data.CFBundleIdentifier;
95
+ catch (error) {
96
+ reject(error);
97
+ }
98
+ finally {
84
99
  zip.close();
85
- resolve({ appId, platform: 'ios' });
86
- })
87
- .catch(reject);
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 Promise.resolve().then(() => require('../utils/expo'));
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 (0, promises_1.rm)(extractDir, { recursive: true, force: true }).catch(() => { });
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
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MoropoService = void 0;
4
- const core_1 = require("@oclif/core");
5
- const fs = require("node:fs");
6
- const os = require("node:os");
7
- const path = require("node:path");
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
- core_1.ux.action.start('Downloading Moropo tests', 'Initializing', {
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
- const moropoDir = path.join(os.tmpdir(), `moropo-tests-${Date.now()}`);
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
- core_1.ux.action.stop('completed');
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
- core_1.ux.action.stop('failed');
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
- const fileStream = fs.createWriteStream(zipPath);
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
- let readerResult = await reader.read();
83
- while (!readerResult.done) {
84
- const { value } = readerResult;
85
- downloadedSize += value.length;
86
- if (!quiet && !json && totalSize) {
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
- core_1.ux.action.status = `Downloading: ${progress}%`;
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
- core_1.ux.action.status = message;
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
- apiKey: string;
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
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
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, apiKey, uploadId, downloadType, artifactsPath = './artifacts.zip', debug = false, logger, warnLogger, } = options;
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 api_gateway_1.ApiGateway.downloadArtifactsZip(apiUrl, apiKey, uploadId, downloadType, artifactsPath);
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
- if (warnLogger) {
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, apiKey, uploadId, debug = false, logger, warnLogger } = options;
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 api_gateway_1.ApiGateway.downloadReportGeneric(apiUrl, apiKey, uploadId, type, filePath);
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
- const errorMessage = error instanceof Error ? error.message : String(error);
99
- if (warnLogger) {
100
- warnLogger(`Failed to download ${type.toUpperCase()} report: ${errorMessage}`);
101
- if (errorMessage.includes('404')) {
102
- warnLogger(`No ${type.toUpperCase()} reports found for this upload. Make sure your tests generated results.`);
103
- }
104
- else if (errorMessage.includes('EACCES') || errorMessage.includes('EPERM')) {
105
- warnLogger('Permission denied. Check write permissions for the current directory.');
106
- }
107
- else if (errorMessage.includes('ENOENT')) {
108
- warnLogger('Directory does not exist. Make sure you have write access to the current directory.');
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 { paths } from '../types/generated/schema.types';
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
- apiKey: string;
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 POLL_INTERVAL_MS;
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(results: TestResult[], options: PollingOptions, testMetadata?: Record<string, TestMetadata>): Promise<PollingResult>;
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 apiKey API authentication key
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
- private updateDisplayStatus;
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 {};