@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
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Ships CLI lifecycle + error events to Axiom via the API's `/cli/logs`
3
+ * proxy (forwards to `cli-dev` / `cli-prod`). The proxy authenticates with
4
+ * the same `auth.headers` every other gateway uses (`x-app-api-key` or
5
+ * `Authorization: Bearer ...` + `x-dcd-org`), so we never need the Axiom
6
+ * token client-side.
7
+ *
8
+ * Two flush paths:
9
+ * - `flush()` — async, used after `runMain` returns naturally
10
+ * - `flushSync()` — sync via `curl`, used inside `logger.error` right before
11
+ * `process.exit` (no other way to send HTTP after exit is called)
12
+ *
13
+ * Opt out: `DCD_TELEMETRY_DISABLED=1` in the environment.
14
+ */
15
+ import { execFileSync } from 'node:child_process';
16
+ import { randomUUID } from 'node:crypto';
17
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
18
+ import { tmpdir } from 'node:os';
19
+ import { join } from 'node:path';
20
+ import { getCliVersion, getInstallMethod } from '../utils/cli.js';
21
+ const DEFAULT_API_URL = 'https://api.devicecloud.dev';
22
+ class Telemetry {
23
+ buffer = [];
24
+ config = null;
25
+ command = inferCommandFromArgv();
26
+ sessionId = randomUUID();
27
+ startedAt = Date.now();
28
+ disabled = !!process.env.DCD_TELEMETRY_DISABLED;
29
+ release = getCliVersion();
30
+ /**
31
+ * Called once per invocation by `resolveAuth` after the credential check
32
+ * succeeds. Before this is called, lifecycle events are buffered but cannot
33
+ * be sent. Commands that never reach `resolveAuth` (`--help`, `--version`,
34
+ * `dcd login` before sign-in completes) will skip telemetry entirely — by
35
+ * design, since those flows have no identity to attach.
36
+ */
37
+ configure(opts) {
38
+ if (this.disabled)
39
+ return;
40
+ this.config = {
41
+ apiUrl: opts.apiUrl ?? inferApiUrlFromArgv(),
42
+ auth: opts.auth,
43
+ command: this.command,
44
+ };
45
+ }
46
+ /**
47
+ * Override the command label attached to telemetry meta. The MCP server is
48
+ * long-lived and isn't a citty subcommand, so `inferCommandFromArgv` can't
49
+ * name it — `src/mcp/index.ts` calls this at boot.
50
+ */
51
+ setCommand(command) {
52
+ this.command = command;
53
+ }
54
+ recordCommandStart() {
55
+ this.startedAt = Date.now();
56
+ this.enqueue('info', 'cli.lifecycle', 'command started', {
57
+ argv: scrubArgv(process.argv.slice(2)),
58
+ });
59
+ }
60
+ recordMcpToolStart(tool) {
61
+ this.enqueue('info', 'cli.mcp', 'mcp tool invoked', { tool });
62
+ }
63
+ recordMcpToolSuccess(tool, durationMs) {
64
+ this.enqueue('info', 'cli.mcp', 'mcp tool completed', {
65
+ tool,
66
+ duration_ms: durationMs,
67
+ });
68
+ }
69
+ recordMcpToolFailure(tool, error, durationMs) {
70
+ this.enqueue('error', 'cli.mcp', 'mcp tool failed', {
71
+ tool,
72
+ duration_ms: durationMs,
73
+ error_message: error instanceof Error ? error.message : String(error),
74
+ error_name: error instanceof Error ? error.name : 'Error',
75
+ });
76
+ }
77
+ recordCommandSuccess() {
78
+ this.enqueue('info', 'cli.lifecycle', 'command completed', {
79
+ duration_ms: Date.now() - this.startedAt,
80
+ exit_code: 0,
81
+ });
82
+ }
83
+ recordCommandFailure(opts) {
84
+ const message = opts.error instanceof Error ? opts.error.message : String(opts.error);
85
+ this.enqueue('error', 'cli.lifecycle', 'command failed', {
86
+ duration_ms: Date.now() - this.startedAt,
87
+ exit_code: opts.exitCode,
88
+ error_message: message,
89
+ error_name: opts.error instanceof Error ? opts.error.name : 'CliError',
90
+ error_stack: opts.error instanceof Error ? opts.error.stack : undefined,
91
+ });
92
+ }
93
+ enqueue(level, context, message, extra) {
94
+ if (this.disabled)
95
+ return;
96
+ this.buffer.push({
97
+ timestamp: new Date().toISOString(),
98
+ level,
99
+ context,
100
+ message,
101
+ extra,
102
+ });
103
+ }
104
+ async flush() {
105
+ if (this.disabled || !this.config || this.buffer.length === 0)
106
+ return;
107
+ const events = this.buffer.splice(0, this.buffer.length);
108
+ const body = JSON.stringify({ events, meta: this.buildMeta() });
109
+ try {
110
+ await fetch(`${this.config.apiUrl}/cli/logs`, {
111
+ method: 'POST',
112
+ headers: {
113
+ 'content-type': 'application/json',
114
+ ...this.config.auth.headers,
115
+ },
116
+ body,
117
+ });
118
+ }
119
+ catch {
120
+ // Telemetry failures must never surface — silently drop.
121
+ }
122
+ }
123
+ /**
124
+ * Synchronous flush via `curl`. Used right before `process.exit` (which
125
+ * bypasses `beforeExit`, so async `fetch` would be killed mid-flight).
126
+ * Node has no built-in sync HTTP and `curl` ships with macOS, Linux, and
127
+ * Windows ≥ 1803 — that's the supported surface for the CLI.
128
+ */
129
+ flushSync() {
130
+ if (this.disabled || !this.config || this.buffer.length === 0)
131
+ return;
132
+ const events = this.buffer.splice(0, this.buffer.length);
133
+ const body = JSON.stringify({ events, meta: this.buildMeta() });
134
+ // Headers carry the API key / Bearer token, so they must not appear in
135
+ // curl's argv (world-readable via ps//proc while curl runs). They go in a
136
+ // 0600 config file inside a fresh 0700 temp dir instead; stdin carries the
137
+ // body, so it can't double as the config channel.
138
+ let configDir;
139
+ try {
140
+ configDir = mkdtempSync(join(tmpdir(), 'dcd-telemetry-'));
141
+ const configPath = join(configDir, 'curl.cfg');
142
+ const headerLines = ['header = "content-type: application/json"'];
143
+ for (const [k, v] of Object.entries(this.config.auth.headers)) {
144
+ headerLines.push(`header = "${k}: ${v}"`);
145
+ }
146
+ writeFileSync(configPath, headerLines.join('\n'), { mode: 0o600 });
147
+ execFileSync('curl', [
148
+ '-sS',
149
+ '-m',
150
+ '3',
151
+ '-X',
152
+ 'POST',
153
+ '-K',
154
+ configPath,
155
+ '--data-binary',
156
+ '@-',
157
+ `${this.config.apiUrl}/cli/logs`,
158
+ ], { input: body, stdio: ['pipe', 'ignore', 'ignore'] });
159
+ }
160
+ catch {
161
+ // Telemetry failures must never surface — silently drop.
162
+ }
163
+ finally {
164
+ if (configDir)
165
+ rmSync(configDir, { recursive: true, force: true });
166
+ }
167
+ }
168
+ buildMeta() {
169
+ if (!this.config) {
170
+ throw new Error('telemetry not configured');
171
+ }
172
+ return {
173
+ release: this.release,
174
+ command: this.command,
175
+ sessionId: this.sessionId,
176
+ authMode: this.config.auth.mode,
177
+ installMethod: getInstallMethod(),
178
+ nodeVersion: process.versions.node,
179
+ platform: process.platform,
180
+ arch: process.arch,
181
+ userEmail: this.config.auth.userEmail,
182
+ orgId: this.config.auth.orgId,
183
+ };
184
+ }
185
+ }
186
+ // Flags whose values are credential material (API keys, signed URLs) or
187
+ // user-provided env pairs that routinely carry test-account secrets. Their
188
+ // values must never reach the telemetry backend.
189
+ const SENSITIVE_FLAG_NAMES = new Set([
190
+ '--api-key',
191
+ '--apiKey',
192
+ '--moropo-v1-api-key',
193
+ '--app-url',
194
+ '--appUrl',
195
+ '-e',
196
+ '--env',
197
+ ]);
198
+ function scrubArgv(args) {
199
+ const scrubbed = [];
200
+ for (let i = 0; i < args.length; i++) {
201
+ const arg = args[i];
202
+ const eqIndex = arg.indexOf('=');
203
+ const flagName = eqIndex === -1 ? arg : arg.slice(0, eqIndex);
204
+ if (arg.startsWith('-') && SENSITIVE_FLAG_NAMES.has(flagName)) {
205
+ if (eqIndex === -1) {
206
+ scrubbed.push(arg);
207
+ if (i + 1 < args.length) {
208
+ scrubbed.push('<redacted>');
209
+ i++;
210
+ }
211
+ }
212
+ else {
213
+ scrubbed.push(`${flagName}=<redacted>`);
214
+ }
215
+ }
216
+ else {
217
+ scrubbed.push(arg);
218
+ }
219
+ }
220
+ return scrubbed;
221
+ }
222
+ // argv layout: node|tsx, script, command, ...flags. The first non-flag after
223
+ // the script is the subcommand. Falls back to 'help' for `--help`/`--version`
224
+ // invocations and to 'unknown' if we can't decide.
225
+ function inferCommandFromArgv() {
226
+ const args = process.argv.slice(2);
227
+ for (const arg of args) {
228
+ if (arg.startsWith('-'))
229
+ continue;
230
+ return arg;
231
+ }
232
+ if (args.some((a) => a === '--help' || a === '-h'))
233
+ return 'help';
234
+ if (args.some((a) => a === '--version' || a === '-v'))
235
+ return 'version';
236
+ return 'unknown';
237
+ }
238
+ const API_URL_FLAGS = ['--api-url', '--apiURL', '--apiUrl'];
239
+ function inferApiUrlFromArgv() {
240
+ const args = process.argv.slice(2);
241
+ for (let i = 0; i < args.length; i++) {
242
+ const arg = args[i];
243
+ for (const flag of API_URL_FLAGS) {
244
+ if (arg === flag && args[i + 1])
245
+ return args[i + 1];
246
+ if (arg.startsWith(`${flag}=`))
247
+ return arg.slice(flag.length + 1);
248
+ }
249
+ }
250
+ return DEFAULT_API_URL;
251
+ }
252
+ export const telemetry = new Telemetry();
@@ -1,4 +1,4 @@
1
- import { IExecutionPlan } from './execution-plan.service';
1
+ import { IExecutionPlan } from './execution-plan.service.js';
2
2
  export interface TestSubmissionConfig {
3
3
  androidApiLevel?: string;
4
4
  androidDevice?: string;
@@ -35,11 +35,28 @@ export interface TestSubmissionConfig {
35
35
  */
36
36
  export declare class TestSubmissionService {
37
37
  /**
38
- * Build FormData for test submission
38
+ * Build the test-submission payload: the compressed flow zip plus every
39
+ * non-`file` field, each encoded exactly as it is sent today. The same
40
+ * `fields` feed both the new JSON `submitFlowTest` body and the legacy
41
+ * multipart `buildFormData`, guaranteeing byte-identical field encoding
42
+ * across both paths.
39
43
  * @param config Test submission configuration
40
- * @returns FormData ready to be submitted to the API
44
+ * @returns The flow zip buffer, its SHA-256, and the string-encoded fields
41
45
  */
42
- buildTestFormData(config: TestSubmissionConfig): Promise<FormData>;
46
+ buildTestPayload(config: TestSubmissionConfig): Promise<{
47
+ buffer: Buffer;
48
+ fields: Record<string, string>;
49
+ sha: string;
50
+ }>;
51
+ /**
52
+ * Wraps the payload fields and flow zip into multipart FormData for the
53
+ * legacy `POST /uploads/flow` fallback. `file` is set first to preserve the
54
+ * exact part ordering the old code produced.
55
+ * @param fields String-encoded fields from {@link buildTestPayload}
56
+ * @param buffer The compressed flow zip
57
+ * @returns FormData ready to be submitted to the multipart API
58
+ */
59
+ buildFormData(fields: Record<string, string>, buffer: Buffer): FormData;
43
60
  private logDebug;
44
61
  private normalizeFilePath;
45
62
  private normalizePathMap;
@@ -1,26 +1,27 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.TestSubmissionService = void 0;
4
- const node_crypto_1 = require("node:crypto");
5
- const path = require("node:path");
6
- const methods_1 = require("../methods");
1
+ import { createHash } from 'node:crypto';
2
+ import * as path from 'node:path';
3
+ import { compressFilesFromRelativePath } from '../methods.js';
4
+ import { toPortableRelativePath } from '../utils/paths.js';
7
5
  const mimeTypeLookupByExtension = {
8
6
  zip: 'application/zip',
9
7
  };
10
8
  /**
11
9
  * Service for building test submission form data
12
10
  */
13
- class TestSubmissionService {
11
+ export class TestSubmissionService {
14
12
  /**
15
- * Build FormData for test submission
13
+ * Build the test-submission payload: the compressed flow zip plus every
14
+ * non-`file` field, each encoded exactly as it is sent today. The same
15
+ * `fields` feed both the new JSON `submitFlowTest` body and the legacy
16
+ * multipart `buildFormData`, guaranteeing byte-identical field encoding
17
+ * across both paths.
16
18
  * @param config Test submission configuration
17
- * @returns FormData ready to be submitted to the API
19
+ * @returns The flow zip buffer, its SHA-256, and the string-encoded fields
18
20
  */
19
- async buildTestFormData(config) {
21
+ async buildTestPayload(config) {
20
22
  const { appBinaryId, flowFile, executionPlan, commonRoot, cliVersion, env = [], metadata = [], googlePlay = false, androidApiLevel, androidDevice, androidNoSnapshot, iOSVersion, iOSDevice, name, runnerType, maestroVersion, deviceLocale, orientation, mitmHost, mitmPath, retry, continueOnFailure = true, report, showCrosshairs, maestroChromeOnboarding, raw, disableAnimations, debug = false, logger, } = config;
21
23
  const { allExcludeTags, allIncludeTags, flowMetadata, flowOverrides, flowsToRun: testFileNames, referencedFiles, sequence, workspaceConfig, } = executionPlan;
22
24
  const { flows: sequentialFlows = [] } = sequence ?? {};
23
- const testFormData = new FormData();
24
25
  const envObject = this.parseKeyValuePairs(env);
25
26
  const metadataObject = this.parseKeyValuePairs(metadata);
26
27
  if (Object.keys(envObject).length > 0) {
@@ -41,7 +42,7 @@ class TestSubmissionService {
41
42
  }
42
43
  }
43
44
  this.logDebug(debug, logger, `[DEBUG] Compressing files from path: ${flowFile}`);
44
- const buffer = await (0, methods_1.compressFilesFromRelativePath)(flowFile?.endsWith('.yaml') || flowFile?.endsWith('.yml')
45
+ const buffer = await compressFilesFromRelativePath(flowFile?.endsWith('.yaml') || flowFile?.endsWith('.yml')
45
46
  ? path.dirname(flowFile)
46
47
  : flowFile, [
47
48
  ...new Set([
@@ -52,19 +53,18 @@ class TestSubmissionService {
52
53
  ], commonRoot);
53
54
  this.logDebug(debug, logger, `[DEBUG] Compressed file size: ${buffer.length} bytes`);
54
55
  // Calculate SHA-256 hash of the flow ZIP
55
- const sha = (0, node_crypto_1.createHash)('sha256').update(buffer).digest('hex');
56
+ const sha = createHash('sha256').update(buffer).digest('hex');
56
57
  this.logDebug(debug, logger, `[DEBUG] Flow ZIP SHA-256: ${sha}`);
57
- const blob = new Blob([buffer], {
58
- type: mimeTypeLookupByExtension.zip,
59
- });
60
- testFormData.set('file', blob, 'flowFile.zip');
61
- testFormData.set('sha', sha);
62
- testFormData.set('appBinaryId', appBinaryId);
63
- testFormData.set('testFileNames', JSON.stringify(this.normalizePaths(testFileNames, commonRoot)));
64
- testFormData.set('flowMetadata', JSON.stringify(this.normalizePathMap(flowMetadata, commonRoot)));
65
- testFormData.set('testFileOverrides', JSON.stringify(this.normalizePathMap(flowOverrides, commonRoot)));
66
- testFormData.set('sequentialFlows', JSON.stringify(this.normalizePaths(sequentialFlows, commonRoot)));
67
- testFormData.set('env', JSON.stringify(envObject));
58
+ // String-encoded fields, in the same order and with the same encoding as
59
+ // the legacy multipart FormData. Reused verbatim by both submission paths.
60
+ const fields = {};
61
+ fields.sha = sha;
62
+ fields.appBinaryId = appBinaryId;
63
+ fields.testFileNames = JSON.stringify(this.normalizePaths(testFileNames, commonRoot));
64
+ fields.flowMetadata = JSON.stringify(this.normalizePathMap(flowMetadata, commonRoot));
65
+ fields.testFileOverrides = JSON.stringify(this.normalizePathMap(flowOverrides, commonRoot));
66
+ fields.sequentialFlows = JSON.stringify(this.normalizePaths(sequentialFlows, commonRoot));
67
+ fields.env = JSON.stringify(envObject);
68
68
  // Note: googlePlay is now included in configPayload below instead of as a separate field
69
69
  // to work around a FormData parsing issue in the API
70
70
  const targetPlatform = iOSDevice || iOSVersion ? 'ios' : 'android';
@@ -91,13 +91,13 @@ class TestSubmissionService {
91
91
  disableAnimations: effectiveDisableAnimations,
92
92
  version: cliVersion,
93
93
  };
94
- testFormData.set('config', JSON.stringify(configPayload));
94
+ fields.config = JSON.stringify(configPayload);
95
95
  if (Object.keys(metadataObject).length > 0) {
96
96
  const metadataPayload = { userMetadata: metadataObject };
97
- testFormData.set('metadata', JSON.stringify(metadataPayload));
97
+ fields.metadata = JSON.stringify(metadataPayload);
98
98
  this.logDebug(debug, logger, `[DEBUG] Sending metadata to API: ${JSON.stringify(metadataPayload)}`);
99
99
  }
100
- this.setOptionalFields(testFormData, {
100
+ this.setOptionalFields(fields, {
101
101
  androidApiLevel,
102
102
  androidDevice,
103
103
  iOSDevice,
@@ -106,9 +106,28 @@ class TestSubmissionService {
106
106
  runnerType,
107
107
  });
108
108
  if (workspaceConfig) {
109
- testFormData.set('workspaceConfig', JSON.stringify(workspaceConfig));
109
+ fields.workspaceConfig = JSON.stringify(workspaceConfig);
110
+ }
111
+ return { buffer, fields, sha };
112
+ }
113
+ /**
114
+ * Wraps the payload fields and flow zip into multipart FormData for the
115
+ * legacy `POST /uploads/flow` fallback. `file` is set first to preserve the
116
+ * exact part ordering the old code produced.
117
+ * @param fields String-encoded fields from {@link buildTestPayload}
118
+ * @param buffer The compressed flow zip
119
+ * @returns FormData ready to be submitted to the multipart API
120
+ */
121
+ buildFormData(fields, buffer) {
122
+ const formData = new FormData();
123
+ const blob = new Blob([buffer], {
124
+ type: mimeTypeLookupByExtension.zip,
125
+ });
126
+ formData.set('file', blob, 'flowFile.zip');
127
+ for (const [key, value] of Object.entries(fields)) {
128
+ formData.set(key, value);
110
129
  }
111
- return testFormData;
130
+ return formData;
112
131
  }
113
132
  logDebug(debug, logger, message) {
114
133
  if (debug && logger) {
@@ -116,7 +135,7 @@ class TestSubmissionService {
116
135
  }
117
136
  }
118
137
  normalizeFilePath(filePath, commonRoot) {
119
- return filePath.replaceAll(commonRoot, '.').split(path.sep).join('/');
138
+ return toPortableRelativePath(filePath, commonRoot);
120
139
  }
121
140
  normalizePathMap(map, commonRoot) {
122
141
  return Object.fromEntries(Object.entries(map).map(([key, value]) => [
@@ -136,12 +155,11 @@ class TestSubmissionService {
136
155
  return acc;
137
156
  }, {});
138
157
  }
139
- setOptionalFields(formData, fields) {
158
+ setOptionalFields(target, fields) {
140
159
  for (const [key, value] of Object.entries(fields)) {
141
160
  if (value) {
142
- formData.set(key, value.toString());
161
+ target[key] = value.toString();
143
162
  }
144
163
  }
145
164
  }
146
165
  }
147
- exports.TestSubmissionService = TestSubmissionService;
@@ -1,11 +1,12 @@
1
- import { CompatibilityData } from '../utils/compatibility';
1
+ import { CompatibilityData } from '../utils/compatibility.js';
2
2
  /**
3
3
  * Service for handling version validation and checking
4
4
  */
5
5
  export declare class VersionService {
6
6
  /**
7
- * Check npm registry for the latest published version of the CLI
8
- * @returns Latest version string or null if check fails
7
+ * Fetch the latest published CLI version from the release manifest.
8
+ * Works for both npm- and binary-installed users (no `npm` shell-out).
9
+ * Silently returns null on any failure — this check is informational only.
9
10
  */
10
11
  checkLatestCliVersion(): Promise<null | string>;
11
12
  /**
@@ -1,27 +1,31 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.VersionService = void 0;
4
- const node_child_process_1 = require("node:child_process");
1
+ const DEFAULT_MANIFEST_URL = 'https://get.devicecloud.dev/latest.json';
2
+ const MANIFEST_TIMEOUT_MS = 3000;
5
3
  /**
6
4
  * Service for handling version validation and checking
7
5
  */
8
- class VersionService {
6
+ export class VersionService {
9
7
  /**
10
- * Check npm registry for the latest published version of the CLI
11
- * @returns Latest version string or null if check fails
8
+ * Fetch the latest published CLI version from the release manifest.
9
+ * Works for both npm- and binary-installed users (no `npm` shell-out).
10
+ * Silently returns null on any failure — this check is informational only.
12
11
  */
13
12
  async checkLatestCliVersion() {
13
+ const url = process.env.DCD_MANIFEST_URL ?? DEFAULT_MANIFEST_URL;
14
+ const controller = new AbortController();
15
+ const timer = setTimeout(() => controller.abort(), MANIFEST_TIMEOUT_MS);
14
16
  try {
15
- const latestVersion = (0, node_child_process_1.execSync)('npm view @devicecloud.dev/dcd version', {
16
- encoding: 'utf8',
17
- stdio: ['ignore', 'pipe', 'ignore'],
18
- }).trim();
19
- return latestVersion;
17
+ const res = await fetch(url, { signal: controller.signal });
18
+ if (!res.ok)
19
+ return null;
20
+ const data = (await res.json());
21
+ return typeof data.version === 'string' ? data.version : null;
20
22
  }
21
23
  catch {
22
- // Silently fail - version check is informational only
23
24
  return null;
24
25
  }
26
+ finally {
27
+ clearTimeout(timer);
28
+ }
25
29
  }
26
30
  /**
27
31
  * Compare two semantic version strings
@@ -30,8 +34,14 @@ class VersionService {
30
34
  * @returns true if current is older than latest
31
35
  */
32
36
  isOutdated(current, latest) {
33
- const currentParts = current.split('.').map(Number);
34
- const latestParts = latest.split('.').map(Number);
37
+ // Strip any prerelease suffix ("1.2.3-beta.1" -> "1.2.3") and default
38
+ // missing segments to 0 so short/prerelease versions still compare.
39
+ const parts = (version) => {
40
+ const nums = version.split('-')[0].split('.').map(Number);
41
+ return [nums[0] || 0, nums[1] || 0, nums[2] || 0];
42
+ };
43
+ const currentParts = parts(current);
44
+ const latestParts = parts(latest);
35
45
  for (let i = 0; i < 3; i++) {
36
46
  if (currentParts[i] < latestParts[i])
37
47
  return true;
@@ -68,6 +78,9 @@ class VersionService {
68
78
  log(`[DEBUG] Using default Maestro version ${defaultVersion}`);
69
79
  }
70
80
  }
81
+ if (!resolvedVersion) {
82
+ throw new Error('Unable to resolve a Maestro version: compatibility data did not provide a default.');
83
+ }
71
84
  // Validate Maestro version
72
85
  if (!supportedVersions.includes(resolvedVersion)) {
73
86
  throw new Error(`Maestro version ${resolvedVersion} is not supported. Supported versions: ${supportedVersions.join(', ')}`);
@@ -79,4 +92,3 @@ class VersionService {
79
92
  return resolvedVersion;
80
93
  }
81
94
  }
82
- exports.VersionService = VersionService;
@@ -0,0 +1,20 @@
1
+ import type { DcdEnvName } from '../../config/environments.js';
2
+ /**
3
+ * Auth context threaded through gateways and services. Callers build this once
4
+ * (via resolveAuth) and the gateway spreads .headers into every fetch.
5
+ */
6
+ export interface AuthContext {
7
+ headers: Record<string, string>;
8
+ mode: 'apiKey' | 'bearer';
9
+ /**
10
+ * Supabase JWT — present when mode === 'bearer'. Lets realtime subscriptions
11
+ * authenticate the socket (RLS) without re-reading the session from disk.
12
+ */
13
+ accessToken?: string;
14
+ /** Environment the session belongs to — present when mode === 'bearer'. */
15
+ env?: DcdEnvName;
16
+ /** Present when mode === 'bearer'. */
17
+ orgId?: string;
18
+ /** Present when mode === 'bearer'. */
19
+ userEmail?: string;
20
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,11 +1,8 @@
1
- "use strict";
2
1
  /**
3
2
  * Device type definitions - should be kept in sync with API
4
3
  * @see /Users/riglar/repos/dcd/api/src/common/types/device.types.ts
5
4
  */
6
- Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.EAndroidApiLevels = exports.EiOSVersions = exports.EAndroidDevices = exports.EiOSDevices = void 0;
8
- var EiOSDevices;
5
+ export var EiOSDevices;
9
6
  (function (EiOSDevices) {
10
7
  EiOSDevices["ipad-pro-6th-gen"] = "ipad-pro-6th-gen";
11
8
  EiOSDevices["iphone-14"] = "iphone-14";
@@ -16,23 +13,23 @@ var EiOSDevices;
16
13
  EiOSDevices["iphone-16-plus"] = "iphone-16-plus";
17
14
  EiOSDevices["iphone-16-pro"] = "iphone-16-pro";
18
15
  EiOSDevices["iphone-16-pro-max"] = "iphone-16-pro-max";
19
- })(EiOSDevices || (exports.EiOSDevices = EiOSDevices = {}));
20
- var EAndroidDevices;
16
+ })(EiOSDevices || (EiOSDevices = {}));
17
+ export var EAndroidDevices;
21
18
  (function (EAndroidDevices) {
22
19
  EAndroidDevices["generic-tablet"] = "generic-tablet";
23
20
  EAndroidDevices["pixel-6"] = "pixel-6";
24
21
  EAndroidDevices["pixel-6-pro"] = "pixel-6-pro";
25
22
  EAndroidDevices["pixel-7"] = "pixel-7";
26
23
  EAndroidDevices["pixel-7-pro"] = "pixel-7-pro";
27
- })(EAndroidDevices || (exports.EAndroidDevices = EAndroidDevices = {}));
28
- var EiOSVersions;
24
+ })(EAndroidDevices || (EAndroidDevices = {}));
25
+ export var EiOSVersions;
29
26
  (function (EiOSVersions) {
30
27
  EiOSVersions["eighteen"] = "18";
31
28
  EiOSVersions["seventeen"] = "17";
32
29
  EiOSVersions["sixteen"] = "16";
33
30
  EiOSVersions["twentySix"] = "26";
34
- })(EiOSVersions || (exports.EiOSVersions = EiOSVersions = {}));
35
- var EAndroidApiLevels;
31
+ })(EiOSVersions || (EiOSVersions = {}));
32
+ export var EAndroidApiLevels;
36
33
  (function (EAndroidApiLevels) {
37
34
  EAndroidApiLevels["thirty"] = "30";
38
35
  EAndroidApiLevels["thirtyFive"] = "35";
@@ -42,4 +39,4 @@ var EAndroidApiLevels;
42
39
  EAndroidApiLevels["thirtyThree"] = "33";
43
40
  EAndroidApiLevels["thirtyTwo"] = "32";
44
41
  EAndroidApiLevels["twentyNine"] = "29";
45
- })(EAndroidApiLevels || (exports.EAndroidApiLevels = EAndroidApiLevels = {}));
42
+ })(EAndroidApiLevels || (EAndroidApiLevels = {}));