@devicecloud.dev/dcd 5.0.0-beta.0 → 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 (101) hide show
  1. package/README.md +35 -0
  2. package/dist/commands/artifacts.d.ts +28 -28
  3. package/dist/commands/artifacts.js +20 -23
  4. package/dist/commands/cloud.d.ts +57 -57
  5. package/dist/commands/cloud.js +173 -186
  6. package/dist/commands/list.d.ts +22 -22
  7. package/dist/commands/list.js +36 -38
  8. package/dist/commands/live.js +134 -127
  9. package/dist/commands/login.d.ts +11 -11
  10. package/dist/commands/login.js +46 -44
  11. package/dist/commands/logout.js +16 -18
  12. package/dist/commands/status.d.ts +11 -11
  13. package/dist/commands/status.js +45 -43
  14. package/dist/commands/switch-org.d.ts +7 -7
  15. package/dist/commands/switch-org.js +19 -21
  16. package/dist/commands/upgrade.js +29 -31
  17. package/dist/commands/upload.d.ts +10 -10
  18. package/dist/commands/upload.js +42 -43
  19. package/dist/commands/whoami.js +17 -20
  20. package/dist/config/environments.js +6 -12
  21. package/dist/config/flags/api.flags.js +1 -4
  22. package/dist/config/flags/binary.flags.js +1 -4
  23. package/dist/config/flags/device.flags.js +6 -9
  24. package/dist/config/flags/environment.flags.js +1 -4
  25. package/dist/config/flags/execution.flags.js +1 -4
  26. package/dist/config/flags/github.flags.js +1 -4
  27. package/dist/config/flags/output.flags.js +1 -4
  28. package/dist/constants.js +15 -18
  29. package/dist/gateways/api-gateway.d.ts +31 -6
  30. package/dist/gateways/api-gateway.js +70 -16
  31. package/dist/gateways/cli-auth-gateway.d.ts +1 -1
  32. package/dist/gateways/cli-auth-gateway.js +3 -6
  33. package/dist/gateways/realtime-gateway.d.ts +32 -0
  34. package/dist/gateways/realtime-gateway.js +103 -0
  35. package/dist/gateways/supabase-gateway.d.ts +1 -1
  36. package/dist/gateways/supabase-gateway.js +10 -14
  37. package/dist/index.js +41 -38
  38. package/dist/mcp/context.d.ts +33 -0
  39. package/dist/mcp/context.js +33 -0
  40. package/dist/mcp/helpers.d.ts +16 -0
  41. package/dist/mcp/helpers.js +34 -0
  42. package/dist/mcp/index.d.ts +2 -0
  43. package/dist/mcp/index.js +24 -0
  44. package/dist/mcp/server.d.ts +7 -0
  45. package/dist/mcp/server.js +27 -0
  46. package/dist/mcp/tools/download-artifacts.d.ts +11 -0
  47. package/dist/mcp/tools/download-artifacts.js +84 -0
  48. package/dist/mcp/tools/get-status.d.ts +7 -0
  49. package/dist/mcp/tools/get-status.js +39 -0
  50. package/dist/mcp/tools/list-devices.d.ts +7 -0
  51. package/dist/mcp/tools/list-devices.js +27 -0
  52. package/dist/mcp/tools/list-runs.d.ts +3 -0
  53. package/dist/mcp/tools/list-runs.js +60 -0
  54. package/dist/mcp/tools/run-cloud-test.d.ts +14 -0
  55. package/dist/mcp/tools/run-cloud-test.js +233 -0
  56. package/dist/methods.d.ts +32 -1
  57. package/dist/methods.js +125 -66
  58. package/dist/services/device-validation.service.d.ts +1 -1
  59. package/dist/services/device-validation.service.js +1 -5
  60. package/dist/services/execution-plan.service.js +14 -17
  61. package/dist/services/execution-plan.utils.js +15 -23
  62. package/dist/services/flow-paths.d.ts +17 -0
  63. package/dist/services/flow-paths.js +52 -0
  64. package/dist/services/metadata-extractor.service.js +22 -25
  65. package/dist/services/moropo.service.js +18 -20
  66. package/dist/services/report-download.service.d.ts +1 -1
  67. package/dist/services/report-download.service.js +5 -9
  68. package/dist/services/results-polling.service.d.ts +18 -3
  69. package/dist/services/results-polling.service.js +195 -108
  70. package/dist/services/telemetry.service.d.ts +10 -1
  71. package/dist/services/telemetry.service.js +40 -18
  72. package/dist/services/test-submission.service.d.ts +21 -4
  73. package/dist/services/test-submission.service.js +51 -34
  74. package/dist/services/version.service.d.ts +1 -1
  75. package/dist/services/version.service.js +1 -5
  76. package/dist/types/domain/auth.types.d.ts +8 -0
  77. package/dist/types/domain/auth.types.js +1 -2
  78. package/dist/types/domain/device.types.js +8 -11
  79. package/dist/types/domain/live.types.js +1 -2
  80. package/dist/types/generated/schema.types.js +1 -2
  81. package/dist/types/index.d.ts +2 -2
  82. package/dist/types/index.js +2 -18
  83. package/dist/types.js +1 -2
  84. package/dist/utils/auth.d.ts +1 -1
  85. package/dist/utils/auth.js +27 -28
  86. package/dist/utils/ci.d.ts +12 -0
  87. package/dist/utils/ci.js +39 -0
  88. package/dist/utils/cli.js +18 -27
  89. package/dist/utils/compatibility.d.ts +1 -1
  90. package/dist/utils/compatibility.js +5 -7
  91. package/dist/utils/config-store.js +33 -43
  92. package/dist/utils/connectivity.js +1 -4
  93. package/dist/utils/expo.js +15 -21
  94. package/dist/utils/orgs.js +8 -12
  95. package/dist/utils/paths.js +2 -5
  96. package/dist/utils/progress.js +2 -5
  97. package/dist/utils/styling.d.ts +35 -37
  98. package/dist/utils/styling.js +52 -86
  99. package/dist/utils/ui.d.ts +41 -0
  100. package/dist/utils/ui.js +95 -0
  101. package/package.json +27 -24
@@ -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
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MetadataExtractorService = exports.ExpoTarGzMetadataExtractor = exports.IosZipMetadataExtractor = exports.IosAppMetadataExtractor = exports.AndroidMetadataExtractor = void 0;
4
- const bplist_parser_1 = require("bplist-parser");
5
- const node_apk_1 = require("node-apk");
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;
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 = (0, plist_1.parse)(buffer.toString());
21
+ data = parse(buffer.toString());
20
22
  }
21
23
  else if (bufferType === 98) {
22
- data = (0, bplist_parser_1.parseBuffer)(buffer)[0];
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 node_apk_1.Apk(filePath);
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 (0, promises_1.readFile)(infoPlistPath);
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 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,17 +1,14 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MoropoService = void 0;
4
- const progress_1 = require("../utils/progress");
5
- const fs = require("node:fs");
6
- const os = require("node:os");
7
- const path = require("node:path");
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
- progress_1.ux.action.start('Downloading Moropo tests', 'Initializing', {
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
- progress_1.ux.action.stop('completed');
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
- progress_1.ux.action.stop('failed');
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 = node_stream_1.Readable.fromWeb(response.body);
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
- progress_1.ux.action.status = `Downloading: ${progress}%`;
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 (0, promises_1.pipeline)(source, fs.createWriteStream(zipPath));
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
- progress_1.ux.action.status = message;
114
+ ux.action.status = message;
116
115
  }
117
116
  }
118
117
  }
119
- exports.MoropoService = MoropoService;
@@ -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
  export interface DownloadOptions {
3
3
  auth: AuthContext;
4
4
  apiUrl: string;
@@ -1,12 +1,9 @@
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
@@ -18,7 +15,7 @@ class ReportDownloadService {
18
15
  if (debug && logger) {
19
16
  logger(`[DEBUG] Downloading artifacts: ${downloadType}`);
20
17
  }
21
- await api_gateway_1.ApiGateway.downloadArtifactsZip(apiUrl, auth, 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}`);
@@ -84,7 +81,7 @@ class ReportDownloadService {
84
81
  if (debug && logger) {
85
82
  logger(`[DEBUG] Downloading ${type.toUpperCase()} report`);
86
83
  }
87
- await api_gateway_1.ApiGateway.downloadReportGeneric(apiUrl, auth, uploadId, type, filePath);
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 POLL_INTERVAL_MS;
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
- 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
  }