@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
|
@@ -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
|
|
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
|
|
44
|
+
* @returns The flow zip buffer, its SHA-256, and the string-encoded fields
|
|
41
45
|
*/
|
|
42
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
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
|
|
19
|
+
* @returns The flow zip buffer, its SHA-256, and the string-encoded fields
|
|
18
20
|
*/
|
|
19
|
-
async
|
|
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
|
|
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 =
|
|
56
|
+
const sha = createHash('sha256').update(buffer).digest('hex');
|
|
56
57
|
this.logDebug(debug, logger, `[DEBUG] Flow ZIP SHA-256: ${sha}`);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
94
|
+
fields.config = JSON.stringify(configPayload);
|
|
95
95
|
if (Object.keys(metadataObject).length > 0) {
|
|
96
96
|
const metadataPayload = { userMetadata: metadataObject };
|
|
97
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
158
|
+
setOptionalFields(target, fields) {
|
|
140
159
|
for (const [key, value] of Object.entries(fields)) {
|
|
141
160
|
if (value) {
|
|
142
|
-
|
|
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
|
-
*
|
|
8
|
-
*
|
|
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
|
-
|
|
2
|
-
|
|
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
|
-
*
|
|
11
|
-
*
|
|
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
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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 || (
|
|
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 || (
|
|
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 || (
|
|
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 || (
|
|
42
|
+
})(EAndroidApiLevels || (EAndroidApiLevels = {}));
|