@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.
- package/README.md +35 -0
- package/dist/commands/artifacts.d.ts +28 -28
- package/dist/commands/artifacts.js +20 -23
- package/dist/commands/cloud.d.ts +57 -57
- package/dist/commands/cloud.js +173 -186
- package/dist/commands/list.d.ts +22 -22
- package/dist/commands/list.js +36 -38
- package/dist/commands/live.js +134 -127
- package/dist/commands/login.d.ts +11 -11
- package/dist/commands/login.js +46 -44
- package/dist/commands/logout.js +16 -18
- package/dist/commands/status.d.ts +11 -11
- package/dist/commands/status.js +45 -43
- package/dist/commands/switch-org.d.ts +7 -7
- package/dist/commands/switch-org.js +19 -21
- package/dist/commands/upgrade.js +29 -31
- package/dist/commands/upload.d.ts +10 -10
- package/dist/commands/upload.js +42 -43
- package/dist/commands/whoami.js +17 -20
- package/dist/config/environments.js +6 -12
- package/dist/config/flags/api.flags.js +1 -4
- package/dist/config/flags/binary.flags.js +1 -4
- package/dist/config/flags/device.flags.js +6 -9
- package/dist/config/flags/environment.flags.js +1 -4
- package/dist/config/flags/execution.flags.js +1 -4
- package/dist/config/flags/github.flags.js +1 -4
- package/dist/config/flags/output.flags.js +1 -4
- package/dist/constants.js +15 -18
- package/dist/gateways/api-gateway.d.ts +31 -6
- package/dist/gateways/api-gateway.js +70 -16
- package/dist/gateways/cli-auth-gateway.d.ts +1 -1
- package/dist/gateways/cli-auth-gateway.js +3 -6
- package/dist/gateways/realtime-gateway.d.ts +32 -0
- package/dist/gateways/realtime-gateway.js +103 -0
- package/dist/gateways/supabase-gateway.d.ts +1 -1
- package/dist/gateways/supabase-gateway.js +10 -14
- package/dist/index.js +41 -38
- 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 +32 -1
- package/dist/methods.js +125 -66
- package/dist/services/device-validation.service.d.ts +1 -1
- package/dist/services/device-validation.service.js +1 -5
- package/dist/services/execution-plan.service.js +14 -17
- package/dist/services/execution-plan.utils.js +15 -23
- package/dist/services/flow-paths.d.ts +17 -0
- package/dist/services/flow-paths.js +52 -0
- package/dist/services/metadata-extractor.service.js +22 -25
- package/dist/services/moropo.service.js +18 -20
- package/dist/services/report-download.service.d.ts +1 -1
- package/dist/services/report-download.service.js +5 -9
- package/dist/services/results-polling.service.d.ts +18 -3
- package/dist/services/results-polling.service.js +195 -108
- package/dist/services/telemetry.service.d.ts +10 -1
- package/dist/services/telemetry.service.js +40 -18
- package/dist/services/test-submission.service.d.ts +21 -4
- package/dist/services/test-submission.service.js +51 -34
- package/dist/services/version.service.d.ts +1 -1
- package/dist/services/version.service.js +1 -5
- package/dist/types/domain/auth.types.d.ts +8 -0
- package/dist/types/domain/auth.types.js +1 -2
- package/dist/types/domain/device.types.js +8 -11
- package/dist/types/domain/live.types.js +1 -2
- 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 +1 -1
- package/dist/utils/auth.js +27 -28
- package/dist/utils/ci.d.ts +12 -0
- package/dist/utils/ci.js +39 -0
- package/dist/utils/cli.js +18 -27
- package/dist/utils/compatibility.d.ts +1 -1
- package/dist/utils/compatibility.js +5 -7
- package/dist/utils/config-store.js +33 -43
- package/dist/utils/connectivity.js +1 -4
- package/dist/utils/expo.js +15 -21
- package/dist/utils/orgs.js +8 -12
- package/dist/utils/paths.js +2 -5
- package/dist/utils/progress.js +2 -5
- package/dist/utils/styling.d.ts +35 -37
- package/dist/utils/styling.js +52 -86
- package/dist/utils/ui.d.ts +41 -0
- package/dist/utils/ui.js +95 -0
- package/package.json +27 -24
package/dist/commands/cloud.js
CHANGED
|
@@ -1,28 +1,27 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.cloudCommand = void 0;
|
|
4
1
|
/* eslint-disable complexity */
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
2
|
+
import { defineCommand } from 'citty';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { flags as allFlags } from '../constants.js';
|
|
5
|
+
import { ApiError, ApiGateway } from '../gateways/api-gateway.js';
|
|
6
|
+
import { uploadBinary, uploadFlowZip, verifyAppZip, writeJSONFile, } from '../methods.js';
|
|
7
|
+
import { DeviceValidationService } from '../services/device-validation.service.js';
|
|
8
|
+
import { plan } from '../services/execution-plan.service.js';
|
|
9
|
+
import { buildTestMetadataMap, computeCommonRoot, } from '../services/flow-paths.js';
|
|
10
|
+
import { MoropoService } from '../services/moropo.service.js';
|
|
11
|
+
import { ReportDownloadService } from '../services/report-download.service.js';
|
|
12
|
+
import { ResultsPollingService, RunFailedError, } from '../services/results-polling.service.js';
|
|
13
|
+
import { telemetry } from '../services/telemetry.service.js';
|
|
14
|
+
import { TestSubmissionService } from '../services/test-submission.service.js';
|
|
15
|
+
import { VersionService } from '../services/version.service.js';
|
|
16
|
+
import { EAndroidApiLevels, EAndroidDevices, EiOSDevices, EiOSVersions, } from '../types/domain/device.types.js';
|
|
17
|
+
import { resolveAuth } from '../utils/auth.js';
|
|
18
|
+
import { isCI } from '../utils/ci.js';
|
|
19
|
+
import { CliError, coerceArray, getCliVersion, getUpgradeCommand, logger, parseIntFlag, validateEnum, } from '../utils/cli.js';
|
|
20
|
+
import { fetchCompatibilityData, } from '../utils/compatibility.js';
|
|
21
|
+
import { resolveApiUrl } from '../utils/config-store.js';
|
|
22
|
+
import { downloadExpoUrl, extractTarGz, findAppBundle, isUrl } from '../utils/expo.js';
|
|
23
|
+
import { colors, formatId, formatUrl, getConsoleUrl, } from '../utils/styling.js';
|
|
24
|
+
import { ui } from '../utils/ui.js';
|
|
26
25
|
// Suppress punycode deprecation warning (caused by whatwg, supabase dependency).
|
|
27
26
|
// Every other warning must still reach the user — removeAllListeners drops
|
|
28
27
|
// Node's default printer, so re-emit manually.
|
|
@@ -51,13 +50,13 @@ const RUNNER_TYPE_OPTIONS = ['default', 'm4', 'm1', 'gpu1', 'cpu1'];
|
|
|
51
50
|
*
|
|
52
51
|
* Replaces `maestro cloud` with DeviceCloud-specific functionality.
|
|
53
52
|
*/
|
|
54
|
-
|
|
53
|
+
export const cloudCommand = defineCommand({
|
|
55
54
|
meta: {
|
|
56
55
|
name: 'cloud',
|
|
57
56
|
description: 'Test a Flow or set of Flows on devicecloud.dev (https://devicecloud.dev). Provide your application file and a folder with Maestro flows to run them in parallel on multiple devices. The command will block until all analyses have completed.',
|
|
58
57
|
},
|
|
59
58
|
args: {
|
|
60
|
-
...
|
|
59
|
+
...allFlags,
|
|
61
60
|
firstFile: {
|
|
62
61
|
type: 'positional',
|
|
63
62
|
required: false,
|
|
@@ -71,23 +70,21 @@ exports.cloudCommand = (0, citty_1.defineCommand)({
|
|
|
71
70
|
},
|
|
72
71
|
// eslint-disable-next-line complexity
|
|
73
72
|
async run({ args }) {
|
|
74
|
-
const cliVersion =
|
|
75
|
-
const deviceValidationService = new
|
|
76
|
-
const moropoService = new
|
|
77
|
-
const reportDownloadService = new
|
|
78
|
-
const resultsPollingService = new
|
|
79
|
-
const testSubmissionService = new
|
|
80
|
-
const versionService = new
|
|
73
|
+
const cliVersion = getCliVersion();
|
|
74
|
+
const deviceValidationService = new DeviceValidationService();
|
|
75
|
+
const moropoService = new MoropoService();
|
|
76
|
+
const reportDownloadService = new ReportDownloadService();
|
|
77
|
+
const resultsPollingService = new ResultsPollingService();
|
|
78
|
+
const testSubmissionService = new TestSubmissionService();
|
|
79
|
+
const versionService = new VersionService();
|
|
81
80
|
const versionCheck = async () => {
|
|
82
81
|
const latestVersion = await versionService.checkLatestCliVersion();
|
|
83
82
|
if (latestVersion && versionService.isOutdated(cliVersion, latestVersion)) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
'
|
|
88
|
-
|
|
89
|
-
styling_1.colors.info((0, cli_1.getUpgradeCommand)());
|
|
90
|
-
out(`\n${(0, styling_1.box)(body)}\n`);
|
|
83
|
+
out(ui.warn(colors.bold('Update available')));
|
|
84
|
+
out(ui.branch([
|
|
85
|
+
`A new version of the DeviceCloud CLI is available: ${colors.highlight(latestVersion)}`,
|
|
86
|
+
`${colors.dim('Run:')} ${colors.info(getUpgradeCommand())}`,
|
|
87
|
+
]));
|
|
91
88
|
}
|
|
92
89
|
};
|
|
93
90
|
let output = null;
|
|
@@ -100,15 +97,15 @@ exports.cloudCommand = (0, citty_1.defineCommand)({
|
|
|
100
97
|
// Chatty progress logs are suppressed when --json is active so stdout stays parseable.
|
|
101
98
|
const out = (m) => {
|
|
102
99
|
if (!jsonFlag)
|
|
103
|
-
|
|
100
|
+
logger.log(m);
|
|
104
101
|
};
|
|
105
102
|
const warnOut = (m) => {
|
|
106
103
|
if (!jsonFlag)
|
|
107
|
-
|
|
104
|
+
logger.warn(m);
|
|
108
105
|
};
|
|
109
106
|
try {
|
|
110
107
|
const apiKeyFlag = args['api-key'];
|
|
111
|
-
const apiUrl =
|
|
108
|
+
const apiUrl = resolveApiUrl(args['api-url']);
|
|
112
109
|
const appBinaryId = args['app-binary-id'];
|
|
113
110
|
const appFile = args['app-file'];
|
|
114
111
|
const appUrl = args['app-url'];
|
|
@@ -120,34 +117,34 @@ exports.cloudCommand = (0, citty_1.defineCommand)({
|
|
|
120
117
|
const configFile = args.config;
|
|
121
118
|
const debug = Boolean(args.debug);
|
|
122
119
|
const deviceLocale = args['device-locale'];
|
|
123
|
-
const downloadArtifacts =
|
|
120
|
+
const downloadArtifacts = validateEnum(args['download-artifacts'], DOWNLOAD_OPTIONS, 'download-artifacts');
|
|
124
121
|
const dryRun = Boolean(args['dry-run']);
|
|
125
|
-
const env =
|
|
126
|
-
const excludeFlows =
|
|
127
|
-
const excludeTags =
|
|
122
|
+
const env = coerceArray(args.env, false);
|
|
123
|
+
const excludeFlows = coerceArray(args['exclude-flows']);
|
|
124
|
+
const excludeTags = coerceArray(args['exclude-tags']);
|
|
128
125
|
let flows = args.flows;
|
|
129
126
|
const googlePlay = Boolean(args['google-play']);
|
|
130
127
|
const ignoreShaCheck = Boolean(args['ignore-sha-check']);
|
|
131
|
-
const includeTags =
|
|
132
|
-
const iOSDevice =
|
|
133
|
-
const iOSVersion =
|
|
134
|
-
const androidApiLevel =
|
|
135
|
-
const androidDevice =
|
|
128
|
+
const includeTags = coerceArray(args['include-tags']);
|
|
129
|
+
const iOSDevice = validateEnum(args['ios-device'], Object.values(EiOSDevices), 'ios-device');
|
|
130
|
+
const iOSVersion = validateEnum(args['ios-version'], Object.values(EiOSVersions), 'ios-version');
|
|
131
|
+
const androidApiLevel = validateEnum(args['android-api-level'], Object.values(EAndroidApiLevels), 'android-api-level');
|
|
132
|
+
const androidDevice = validateEnum(args['android-device'], Object.values(EAndroidDevices), 'android-device');
|
|
136
133
|
const androidNoSnapshot = Boolean(args['android-no-snapshot']);
|
|
137
134
|
const json = Boolean(args.json);
|
|
138
135
|
const jsonFileFlag = Boolean(args['json-file']);
|
|
139
136
|
const jsonFileName = args['json-file-name'];
|
|
140
137
|
const maestroVersion = args['maestro-version'];
|
|
141
|
-
const metadata =
|
|
138
|
+
const metadata = coerceArray(args.metadata, false);
|
|
142
139
|
const mitmHost = args.mitmHost;
|
|
143
140
|
const mitmPath = args.mitmPath;
|
|
144
141
|
const moropoApiKey = args['moropo-v1-api-key'];
|
|
145
142
|
const name = args.name;
|
|
146
|
-
const orientation =
|
|
143
|
+
const orientation = validateEnum(args.orientation, ORIENTATION_OPTIONS, 'orientation');
|
|
147
144
|
let quiet = Boolean(args.quiet);
|
|
148
|
-
const report =
|
|
149
|
-
let retry =
|
|
150
|
-
const runnerType =
|
|
145
|
+
const report = validateEnum(args.report, REPORT_OPTIONS, 'report');
|
|
146
|
+
let retry = parseIntFlag(args.retry, 'retry');
|
|
147
|
+
const runnerType = validateEnum(args['runner-type'], RUNNER_TYPE_OPTIONS, 'runner-type') ?? 'default';
|
|
151
148
|
const showCrosshairs = Boolean(args['show-crosshairs']);
|
|
152
149
|
const maestroChromeOnboarding = Boolean(args['maestro-chrome-onboarding']);
|
|
153
150
|
const disableAnimations = Boolean(args['disable-animations']);
|
|
@@ -158,7 +155,7 @@ exports.cloudCommand = (0, citty_1.defineCommand)({
|
|
|
158
155
|
const ghPrUrl = args['pr-url'];
|
|
159
156
|
debugFlag = debug;
|
|
160
157
|
jsonFile = jsonFileFlag;
|
|
161
|
-
out(
|
|
158
|
+
out(colors.dim(`dcd v${cliVersion}`));
|
|
162
159
|
if (debug) {
|
|
163
160
|
out('[DEBUG] Starting command execution with debug logging enabled');
|
|
164
161
|
out(`[DEBUG] Node version: ${process.versions.node}`);
|
|
@@ -169,19 +166,19 @@ exports.cloudCommand = (0, citty_1.defineCommand)({
|
|
|
169
166
|
out('--json-file is true: JSON output will be written to file, forcing --quiet flag for better CI output');
|
|
170
167
|
}
|
|
171
168
|
if (mitmPath && !mitmHost) {
|
|
172
|
-
throw new
|
|
169
|
+
throw new CliError('--mitmPath requires --mitmHost to be set');
|
|
173
170
|
}
|
|
174
171
|
if (jsonFileName && !jsonFileFlag) {
|
|
175
|
-
throw new
|
|
172
|
+
throw new CliError('--json-file-name requires --json-file');
|
|
176
173
|
}
|
|
177
174
|
if (artifactsPath && !downloadArtifacts) {
|
|
178
|
-
throw new
|
|
175
|
+
throw new CliError('--artifacts-path requires --download-artifacts');
|
|
179
176
|
}
|
|
180
177
|
if ((junitPath || allurePath || htmlPath) && !report) {
|
|
181
|
-
throw new
|
|
178
|
+
throw new CliError('Report path flags (--junit-path/--allure-path/--html-path) require --report');
|
|
182
179
|
}
|
|
183
180
|
if (appFile && appUrl) {
|
|
184
|
-
throw new
|
|
181
|
+
throw new CliError('--app-file and --app-url are mutually exclusive');
|
|
185
182
|
}
|
|
186
183
|
if (json) {
|
|
187
184
|
const originalStdoutWrite = process.stdout.write;
|
|
@@ -197,7 +194,7 @@ exports.cloudCommand = (0, citty_1.defineCommand)({
|
|
|
197
194
|
if (major < 20) {
|
|
198
195
|
warnOut(`WARNING: You are using node version ${major}. DeviceCloud requires node version 20 or later`);
|
|
199
196
|
if (major < 18) {
|
|
200
|
-
throw new
|
|
197
|
+
throw new CliError('Invalid node version');
|
|
201
198
|
}
|
|
202
199
|
}
|
|
203
200
|
await versionCheck();
|
|
@@ -212,10 +209,15 @@ exports.cloudCommand = (0, citty_1.defineCommand)({
|
|
|
212
209
|
});
|
|
213
210
|
tempFiles.push(flows);
|
|
214
211
|
}
|
|
215
|
-
const auth = await
|
|
212
|
+
const auth = await resolveAuth({ apiKeyFlag });
|
|
213
|
+
// Nudge interactive api-key users toward `dcd login`, which unlocks live
|
|
214
|
+
// (realtime) status updates. Suppressed in CI and non-interactive output.
|
|
215
|
+
if (auth.mode === 'apiKey' && !json && !quiet && !isCI()) {
|
|
216
|
+
out(colors.dim('Tip: run `dcd login` for live test updates and a smoother experience than passing --api-key.'));
|
|
217
|
+
}
|
|
216
218
|
let compatibilityData;
|
|
217
219
|
try {
|
|
218
|
-
compatibilityData = await
|
|
220
|
+
compatibilityData = await fetchCompatibilityData(apiUrl, auth);
|
|
219
221
|
if (debug) {
|
|
220
222
|
out('[DEBUG] Successfully fetched compatibility data from API');
|
|
221
223
|
}
|
|
@@ -225,7 +227,7 @@ exports.cloudCommand = (0, citty_1.defineCommand)({
|
|
|
225
227
|
if (debug) {
|
|
226
228
|
out(`[DEBUG] Failed to fetch compatibility data from API: ${errorMessage}`);
|
|
227
229
|
}
|
|
228
|
-
throw new
|
|
230
|
+
throw new CliError(`Failed to fetch device compatibility data: ${errorMessage}. Please check your API key and connection.`);
|
|
229
231
|
}
|
|
230
232
|
if (debug) {
|
|
231
233
|
out(`[DEBUG] API URL: ${apiUrl}`);
|
|
@@ -236,25 +238,21 @@ exports.cloudCommand = (0, citty_1.defineCommand)({
|
|
|
236
238
|
});
|
|
237
239
|
const REMOVED_MAESTRO_VERSIONS = ['1.39.1', '1.39.2', '1.39.7', '2.0.3', '2.4.0'];
|
|
238
240
|
if (REMOVED_MAESTRO_VERSIONS.includes(resolvedMaestroVersion)) {
|
|
239
|
-
throw new
|
|
241
|
+
throw new CliError(`Maestro version ${resolvedMaestroVersion} is no longer supported. ` +
|
|
240
242
|
`Please upgrade to a newer version. See: https://docs.devicecloud.dev/configuration/maestro-versions`);
|
|
241
243
|
}
|
|
242
244
|
if (retry !== undefined && retry > 2) {
|
|
243
|
-
out(
|
|
244
|
-
styling_1.colors.dim('Retries are now free of charge but limited to 2. If your test is still failing after 2 retries, please ask for help on Discord.'));
|
|
245
|
+
out(ui.warn('Retries are now free of charge but limited to 2. If your test is still failing after 2 retries, please ask for help on Discord.'));
|
|
245
246
|
retry = 2;
|
|
246
247
|
}
|
|
247
248
|
if (runnerType === 'm4') {
|
|
248
|
-
out(
|
|
249
|
-
styling_1.colors.dim('Note: runnerType m4 is experimental and currently supports iOS only, Android will revert to default.'));
|
|
249
|
+
out(ui.info('runnerType m4 is experimental and currently supports iOS only, Android will revert to default.'));
|
|
250
250
|
}
|
|
251
251
|
if (runnerType === 'm1') {
|
|
252
|
-
out(
|
|
253
|
-
styling_1.colors.dim('Note: runnerType m1 is experimental and currently supports Android (Pixel 7, API Level 34) only.'));
|
|
252
|
+
out(ui.info('runnerType m1 is experimental and currently supports Android (Pixel 7, API Level 34) only.'));
|
|
254
253
|
}
|
|
255
254
|
if (runnerType === 'gpu1') {
|
|
256
|
-
out(
|
|
257
|
-
styling_1.colors.dim('Note: runnerType gpu1 is Android-only (all devices, API Level 34 or 35), available to all users.'));
|
|
255
|
+
out(ui.info('runnerType gpu1 is Android-only (all devices, API Level 34 or 35), available to all users.'));
|
|
258
256
|
}
|
|
259
257
|
const firstFile = args.firstFile;
|
|
260
258
|
const secondFile = args.secondFile;
|
|
@@ -262,17 +260,17 @@ exports.cloudCommand = (0, citty_1.defineCommand)({
|
|
|
262
260
|
let finalAppFile = appFile ?? (appUrl ?? firstFile);
|
|
263
261
|
let flowFile = flows ?? secondFile;
|
|
264
262
|
if (finalAppFile && !appBinaryId) {
|
|
265
|
-
if (
|
|
266
|
-
out(` ${
|
|
267
|
-
const tarPath = await
|
|
263
|
+
if (isUrl(finalAppFile)) {
|
|
264
|
+
out(` ${colors.dim('→ Downloading Expo build from URL...')}`);
|
|
265
|
+
const tarPath = await downloadExpoUrl(finalAppFile, debug);
|
|
268
266
|
tempFiles.push(tarPath);
|
|
269
267
|
finalAppFile = tarPath;
|
|
270
268
|
}
|
|
271
269
|
if (finalAppFile.endsWith('.tar.gz')) {
|
|
272
|
-
out(` ${
|
|
273
|
-
const extractDir = await
|
|
270
|
+
out(` ${colors.dim('→ Extracting Expo archive...')}`);
|
|
271
|
+
const extractDir = await extractTarGz(finalAppFile, debug);
|
|
274
272
|
tempFiles.push(extractDir);
|
|
275
|
-
finalAppFile = await
|
|
273
|
+
finalAppFile = await findAppBundle(extractDir);
|
|
276
274
|
if (debug) {
|
|
277
275
|
out(`[DEBUG] Found .app bundle at: ${finalAppFile}`);
|
|
278
276
|
}
|
|
@@ -287,7 +285,7 @@ exports.cloudCommand = (0, citty_1.defineCommand)({
|
|
|
287
285
|
}
|
|
288
286
|
if (appBinaryId) {
|
|
289
287
|
if (secondFile) {
|
|
290
|
-
throw new
|
|
288
|
+
throw new CliError('You cannot provide both an appBinaryId and a binary file');
|
|
291
289
|
}
|
|
292
290
|
flowFile = flows ?? firstFile;
|
|
293
291
|
}
|
|
@@ -297,7 +295,7 @@ exports.cloudCommand = (0, citty_1.defineCommand)({
|
|
|
297
295
|
flowFile = firstFile;
|
|
298
296
|
}
|
|
299
297
|
if (!flowFile) {
|
|
300
|
-
throw new
|
|
298
|
+
throw new CliError('You must provide a flow file');
|
|
301
299
|
}
|
|
302
300
|
deviceValidationService.validateiOSDevice(iOSVersion, iOSDevice, compatibilityData, {
|
|
303
301
|
debug,
|
|
@@ -321,7 +319,7 @@ exports.cloudCommand = (0, citty_1.defineCommand)({
|
|
|
321
319
|
if (debug) {
|
|
322
320
|
out('[DEBUG] Generating execution plan...');
|
|
323
321
|
}
|
|
324
|
-
executionPlan = await
|
|
322
|
+
executionPlan = await plan({
|
|
325
323
|
input: flowFile,
|
|
326
324
|
includeTags,
|
|
327
325
|
excludeTags,
|
|
@@ -349,42 +347,11 @@ exports.cloudCommand = (0, citty_1.defineCommand)({
|
|
|
349
347
|
out(`[DEBUG] All exclude tags: ${allExcludeTags?.join(', ') || 'none'}`);
|
|
350
348
|
out(`[DEBUG] Test file names: ${testFileNames.join(', ')}`);
|
|
351
349
|
}
|
|
352
|
-
const
|
|
353
|
-
...testFileNames,
|
|
354
|
-
...referencedFiles,
|
|
355
|
-
].sort((a, b) => a.split(path.sep).length - b.split(path.sep).length);
|
|
356
|
-
// Longest whole-segment directory prefix shared by every path. Segment
|
|
357
|
-
// comparison (not startsWith) so sibling dirs like `flows`/`flows-extra`
|
|
358
|
-
// can't merge, and the file segment itself is never consumed. '' when
|
|
359
|
-
// the paths share no root at all.
|
|
360
|
-
const splitPaths = pathsShortestToLongest.map((p) => p.split(path.sep));
|
|
361
|
-
const shortestSegments = splitPaths[0];
|
|
362
|
-
let matchedSegments = 0;
|
|
363
|
-
for (let i = 0; i < shortestSegments.length - 1; i++) {
|
|
364
|
-
if (splitPaths.every((segments) => segments[i] === shortestSegments[i])) {
|
|
365
|
-
matchedSegments = i + 1;
|
|
366
|
-
}
|
|
367
|
-
else {
|
|
368
|
-
break;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
const commonRoot = shortestSegments.slice(0, matchedSegments).join(path.sep);
|
|
350
|
+
const commonRoot = computeCommonRoot(testFileNames, referencedFiles);
|
|
372
351
|
if (debug) {
|
|
373
352
|
out(`[DEBUG] Common root directory: ${commonRoot}`);
|
|
374
353
|
}
|
|
375
|
-
const testMetadataMap =
|
|
376
|
-
for (const [absolutePath, meta] of Object.entries(flowMetadata)) {
|
|
377
|
-
const normalizedPath = (0, paths_1.toPortableRelativePath)(absolutePath, commonRoot);
|
|
378
|
-
const metadataRecord = meta;
|
|
379
|
-
const flowName = metadataRecord?.name || path.parse(absolutePath).name;
|
|
380
|
-
const rawTags = metadataRecord?.tags;
|
|
381
|
-
const tags = Array.isArray(rawTags)
|
|
382
|
-
? rawTags.map(String)
|
|
383
|
-
: rawTags
|
|
384
|
-
? [String(rawTags)]
|
|
385
|
-
: [];
|
|
386
|
-
testMetadataMap[normalizedPath] = { flowName, tags };
|
|
387
|
-
}
|
|
354
|
+
const testMetadataMap = buildTestMetadataMap(flowMetadata, commonRoot);
|
|
388
355
|
if (debug) {
|
|
389
356
|
out(`[DEBUG] Built testMetadataMap for ${Object.keys(testMetadataMap).length} flows`);
|
|
390
357
|
}
|
|
@@ -395,16 +362,16 @@ exports.cloudCommand = (0, citty_1.defineCommand)({
|
|
|
395
362
|
}
|
|
396
363
|
if (!appBinaryId) {
|
|
397
364
|
if (!(flowFile && finalAppFile)) {
|
|
398
|
-
throw new
|
|
365
|
+
throw new CliError('You must provide a flow file and an app binary id');
|
|
399
366
|
}
|
|
400
367
|
if (!['.apk', '.app', '.zip', '.tar.gz'].some((ext) => finalAppFile.endsWith(ext))) {
|
|
401
|
-
throw new
|
|
368
|
+
throw new CliError('App file must be a .apk for Android, .app/.zip for iOS, or .tar.gz (Expo iOS build)');
|
|
402
369
|
}
|
|
403
370
|
if (finalAppFile.endsWith('.zip')) {
|
|
404
371
|
if (debug) {
|
|
405
372
|
out(`[DEBUG] Verifying iOS app zip file: ${finalAppFile}`);
|
|
406
373
|
}
|
|
407
|
-
await
|
|
374
|
+
await verifyAppZip(finalAppFile);
|
|
408
375
|
}
|
|
409
376
|
}
|
|
410
377
|
const flagLogs = [];
|
|
@@ -417,7 +384,7 @@ exports.cloudCommand = (0, citty_1.defineCommand)({
|
|
|
417
384
|
'appUrl',
|
|
418
385
|
]);
|
|
419
386
|
// Only log canonical flag keys (skip citty-populated alias duplicates like apiURL/apiUrl).
|
|
420
|
-
const canonicalFlagKeys = new Set(Object.keys(
|
|
387
|
+
const canonicalFlagKeys = new Set(Object.keys(allFlags));
|
|
421
388
|
for (const [k, v] of Object.entries(args)) {
|
|
422
389
|
if (!canonicalFlagKeys.has(k))
|
|
423
390
|
continue;
|
|
@@ -430,59 +397,47 @@ exports.cloudCommand = (0, citty_1.defineCommand)({
|
|
|
430
397
|
}
|
|
431
398
|
const overridesEntries = Object.entries(flowOverrides);
|
|
432
399
|
const hasOverrides = overridesEntries.some(([, overrides]) => Object.keys(overrides).length > 0);
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
if (Object.keys(overrides).length > 0) {
|
|
438
|
-
const relativePath = flowPath.replace(process.cwd(), '.');
|
|
439
|
-
overridesLog += `\n ${styling_1.colors.dim('→')} ${relativePath}:`;
|
|
440
|
-
for (const [key, value] of Object.entries(overrides)) {
|
|
441
|
-
overridesLog += `\n ${styling_1.colors.dim(key + ':')} ${styling_1.colors.highlight(String(value))}`;
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
out((0, styling_1.sectionHeader)('Submitting new job'));
|
|
447
|
-
out(` ${styling_1.colors.dim('→ Flow(s):')} ${styling_1.colors.highlight(flowFile)}`);
|
|
448
|
-
out(` ${styling_1.colors.dim('→ App:')} ${styling_1.colors.highlight(appBinaryId || finalAppFile || '')}`);
|
|
400
|
+
const submitRows = ui.fields([
|
|
401
|
+
['flow(s)', colors.highlight(flowFile)],
|
|
402
|
+
['app', colors.highlight(appBinaryId || finalAppFile || '')],
|
|
403
|
+
]);
|
|
449
404
|
if (flagLogs.length > 0) {
|
|
450
|
-
|
|
451
|
-
|
|
405
|
+
submitRows.push('', colors.bold('Options'));
|
|
406
|
+
submitRows.push(...ui.fields(flagLogs.map((flagLog) => {
|
|
452
407
|
const [key, ...valueParts] = flagLog.split(': ');
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
}
|
|
408
|
+
return [key, colors.highlight(valueParts.join(': '))];
|
|
409
|
+
})));
|
|
456
410
|
}
|
|
457
411
|
if (hasOverrides) {
|
|
458
|
-
|
|
412
|
+
submitRows.push('', colors.bold('Overrides'));
|
|
413
|
+
for (const [flowPath, overrides] of overridesEntries) {
|
|
414
|
+
if (Object.keys(overrides).length === 0) {
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
submitRows.push(colors.dim(`${flowPath.replace(process.cwd(), '.')}:`));
|
|
418
|
+
submitRows.push(...ui.fields(Object.entries(overrides).map(([key, value]) => [key, colors.highlight(String(value))])));
|
|
419
|
+
}
|
|
459
420
|
}
|
|
460
|
-
out('');
|
|
421
|
+
out(ui.section('Submitting new job'));
|
|
422
|
+
out(ui.branch(submitRows));
|
|
461
423
|
if (dryRun) {
|
|
462
|
-
out(
|
|
463
|
-
out(
|
|
464
|
-
out(
|
|
465
|
-
for (const test of testFileNames) {
|
|
466
|
-
out((0, styling_1.listItem)(test));
|
|
467
|
-
}
|
|
424
|
+
out(ui.warn(`${colors.bold('Dry run mode')} ${colors.dim('— no tests were actually triggered')}`));
|
|
425
|
+
out(ui.section('The following tests would have been run'));
|
|
426
|
+
out(ui.branch(testFileNames));
|
|
468
427
|
if (sequentialFlows.length > 0) {
|
|
469
|
-
out(
|
|
470
|
-
out(
|
|
471
|
-
for (const flow of sequentialFlows) {
|
|
472
|
-
out((0, styling_1.listItem)(flow));
|
|
473
|
-
}
|
|
428
|
+
out(ui.section('Sequential flows'));
|
|
429
|
+
out(ui.branch(sequentialFlows));
|
|
474
430
|
}
|
|
475
|
-
out('\n');
|
|
476
431
|
return;
|
|
477
432
|
}
|
|
478
433
|
if (!finalBinaryId) {
|
|
479
434
|
if (!finalAppFile) {
|
|
480
|
-
throw new
|
|
435
|
+
throw new CliError('You must provide either an app binary id or an app file');
|
|
481
436
|
}
|
|
482
437
|
if (debug) {
|
|
483
438
|
out(`[DEBUG] Uploading binary file: ${finalAppFile}`);
|
|
484
439
|
}
|
|
485
|
-
const binaryId = await
|
|
440
|
+
const binaryId = await uploadBinary({
|
|
486
441
|
auth,
|
|
487
442
|
apiUrl,
|
|
488
443
|
debug,
|
|
@@ -496,7 +451,7 @@ exports.cloudCommand = (0, citty_1.defineCommand)({
|
|
|
496
451
|
}
|
|
497
452
|
}
|
|
498
453
|
if (!finalBinaryId) {
|
|
499
|
-
throw new
|
|
454
|
+
throw new CliError('Internal error: finalBinaryId should be defined after validation');
|
|
500
455
|
}
|
|
501
456
|
const ghMetadataOverrides = [];
|
|
502
457
|
if (ghBranch)
|
|
@@ -510,7 +465,7 @@ exports.cloudCommand = (0, citty_1.defineCommand)({
|
|
|
510
465
|
if (ghPrUrl)
|
|
511
466
|
ghMetadataOverrides.push(`gh_pr_url=${ghPrUrl}`);
|
|
512
467
|
const mergedMetadata = [...ghMetadataOverrides, ...metadata];
|
|
513
|
-
const
|
|
468
|
+
const { buffer, fields } = await testSubmissionService.buildTestPayload({
|
|
514
469
|
androidApiLevel,
|
|
515
470
|
androidDevice,
|
|
516
471
|
androidNoSnapshot,
|
|
@@ -541,31 +496,63 @@ exports.cloudCommand = (0, citty_1.defineCommand)({
|
|
|
541
496
|
maestroChromeOnboarding,
|
|
542
497
|
disableAnimations,
|
|
543
498
|
});
|
|
544
|
-
|
|
545
|
-
|
|
499
|
+
// New path: upload the zip directly to storage, then submit a JSON test
|
|
500
|
+
// referencing it. Older API deployments lack these endpoints — a real API
|
|
501
|
+
// 404s (route undefined), some proxies 405 (path/method not allowed); in
|
|
502
|
+
// either case fall back to the legacy multipart POST /uploads/flow, which
|
|
503
|
+
// is byte-identical bar the storage reference.
|
|
504
|
+
let response;
|
|
505
|
+
try {
|
|
506
|
+
const storageRef = await uploadFlowZip({ apiUrl, auth, buffer, debug });
|
|
507
|
+
if (debug) {
|
|
508
|
+
out(`[DEBUG] Flow zip uploaded (id=${storageRef.id}, supabase=${storageRef.supabaseSuccess}, backblaze=${storageRef.backblazeSuccess})`);
|
|
509
|
+
out(`[DEBUG] Submitting flow test to ${apiUrl}/uploads/submitFlowTest`);
|
|
510
|
+
}
|
|
511
|
+
response = await ApiGateway.submitFlowTest(apiUrl, auth, {
|
|
512
|
+
...fields,
|
|
513
|
+
...storageRef,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
if (error instanceof ApiError &&
|
|
518
|
+
(error.status === 404 || error.status === 405)) {
|
|
519
|
+
if (debug) {
|
|
520
|
+
out(`[DEBUG] Client-direct flow upload unavailable (HTTP ${error.status}); falling back to multipart ${apiUrl}/uploads/flow`);
|
|
521
|
+
}
|
|
522
|
+
const testFormData = testSubmissionService.buildFormData(fields, buffer);
|
|
523
|
+
response = await ApiGateway.uploadFlow(apiUrl, auth, testFormData);
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
throw error;
|
|
527
|
+
}
|
|
546
528
|
}
|
|
547
|
-
const { message, results } =
|
|
529
|
+
const { message, results } = response;
|
|
548
530
|
if (debug) {
|
|
549
|
-
out(`[DEBUG] Flow
|
|
531
|
+
out(`[DEBUG] Flow submission response received`);
|
|
550
532
|
out(`[DEBUG] Message: ${message}`);
|
|
551
533
|
out(`[DEBUG] Results count: ${results?.length || 0}`);
|
|
552
534
|
}
|
|
553
535
|
if (!results?.length) {
|
|
554
|
-
throw new
|
|
536
|
+
throw new CliError('No tests created: ' + message);
|
|
555
537
|
}
|
|
556
|
-
out(`${
|
|
538
|
+
out(`${ui.success('Submitted')} ${colors.dim(message)}`);
|
|
557
539
|
const testNames = results
|
|
558
540
|
.map((r) => r.test_file_name)
|
|
559
541
|
.sort((a, b) => a.localeCompare(b))
|
|
560
|
-
.join(
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
out(
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
542
|
+
.join(colors.dim(', '));
|
|
543
|
+
const url = getConsoleUrl(apiUrl, results[0].test_upload_id, results[0].id);
|
|
544
|
+
out(ui.section(`Created ${results.length} test${results.length === 1 ? '' : 's'}`));
|
|
545
|
+
out(ui.branch([
|
|
546
|
+
testNames,
|
|
547
|
+
...ui.fields([
|
|
548
|
+
['results', formatUrl(url)],
|
|
549
|
+
['upload id', formatId(results[0].test_upload_id)],
|
|
550
|
+
[
|
|
551
|
+
'poll status',
|
|
552
|
+
colors.info(`dcd status --upload-id ${results[0].test_upload_id}`),
|
|
553
|
+
],
|
|
554
|
+
]),
|
|
555
|
+
]));
|
|
569
556
|
if (async) {
|
|
570
557
|
if (debug) {
|
|
571
558
|
out(`[DEBUG] Async flag is set, not waiting for results`);
|
|
@@ -585,7 +572,7 @@ exports.cloudCommand = (0, citty_1.defineCommand)({
|
|
|
585
572
|
};
|
|
586
573
|
if (jsonFileFlag) {
|
|
587
574
|
const jsonFilePath = jsonFileName || `${results[0].test_upload_id}_dcd.json`;
|
|
588
|
-
|
|
575
|
+
writeJSONFile(jsonFilePath, jsonOutput, {
|
|
589
576
|
log: (m) => out(m),
|
|
590
577
|
warn: (m) => warnOut(m),
|
|
591
578
|
});
|
|
@@ -595,7 +582,7 @@ exports.cloudCommand = (0, citty_1.defineCommand)({
|
|
|
595
582
|
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
596
583
|
return;
|
|
597
584
|
}
|
|
598
|
-
out(
|
|
585
|
+
out(ui.info('Not waiting for results as async flag is set to true'));
|
|
599
586
|
return;
|
|
600
587
|
}
|
|
601
588
|
const pollingResult = await resultsPollingService
|
|
@@ -610,11 +597,11 @@ exports.cloudCommand = (0, citty_1.defineCommand)({
|
|
|
610
597
|
uploadId: results[0].test_upload_id,
|
|
611
598
|
}, testMetadataMap)
|
|
612
599
|
.catch(async (error) => {
|
|
613
|
-
if (error instanceof
|
|
600
|
+
if (error instanceof RunFailedError) {
|
|
614
601
|
const jsonOutput = error.result;
|
|
615
602
|
if (jsonFileFlag) {
|
|
616
603
|
const jsonFilePath = jsonFileName || `${results[0].test_upload_id}_dcd.json`;
|
|
617
|
-
|
|
604
|
+
writeJSONFile(jsonFilePath, jsonOutput, {
|
|
618
605
|
log: (m) => out(m),
|
|
619
606
|
warn: (m) => warnOut(m),
|
|
620
607
|
});
|
|
@@ -690,7 +677,7 @@ exports.cloudCommand = (0, citty_1.defineCommand)({
|
|
|
690
677
|
const jsonOutput = pollingResult;
|
|
691
678
|
if (jsonFileFlag) {
|
|
692
679
|
const jsonFilePath = jsonFileName || `${results[0].test_upload_id}_dcd.json`;
|
|
693
|
-
|
|
680
|
+
writeJSONFile(jsonFilePath, jsonOutput, {
|
|
694
681
|
log: (m) => out(m),
|
|
695
682
|
warn: (m) => warnOut(m),
|
|
696
683
|
});
|
|
@@ -709,7 +696,7 @@ exports.cloudCommand = (0, citty_1.defineCommand)({
|
|
|
709
696
|
caughtError = error;
|
|
710
697
|
}
|
|
711
698
|
finally {
|
|
712
|
-
const fsp = await
|
|
699
|
+
const fsp = await import('node:fs/promises');
|
|
713
700
|
for (const p of tempFiles) {
|
|
714
701
|
await fsp.rm(p, { recursive: true, force: true }).catch(() => { });
|
|
715
702
|
}
|
|
@@ -723,12 +710,12 @@ exports.cloudCommand = (0, citty_1.defineCommand)({
|
|
|
723
710
|
// --json-file keeps exit 0 on a failed run (documented contract);
|
|
724
711
|
// otherwise 2 distinguishes test failure from infra errors (1).
|
|
725
712
|
const exitCode = jsonFile ? 0 : 2;
|
|
726
|
-
|
|
727
|
-
|
|
713
|
+
telemetry.recordCommandFailure({ error: 'RUN_FAILED', exitCode });
|
|
714
|
+
telemetry.flushSync();
|
|
728
715
|
process.exit(exitCode);
|
|
729
716
|
}
|
|
730
|
-
|
|
717
|
+
logger.error(caughtError, { exit: 1, json: jsonFlag });
|
|
731
718
|
}
|
|
732
719
|
},
|
|
733
720
|
});
|
|
734
|
-
|
|
721
|
+
export default cloudCommand;
|