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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +35 -0
  2. package/dist/commands/artifacts.d.ts +28 -28
  3. package/dist/commands/artifacts.js +20 -23
  4. package/dist/commands/cloud.d.ts +57 -57
  5. package/dist/commands/cloud.js +173 -186
  6. package/dist/commands/list.d.ts +22 -22
  7. package/dist/commands/list.js +36 -38
  8. package/dist/commands/live.js +134 -127
  9. package/dist/commands/login.d.ts +11 -11
  10. package/dist/commands/login.js +46 -44
  11. package/dist/commands/logout.js +16 -18
  12. package/dist/commands/status.d.ts +11 -11
  13. package/dist/commands/status.js +45 -43
  14. package/dist/commands/switch-org.d.ts +7 -7
  15. package/dist/commands/switch-org.js +19 -21
  16. package/dist/commands/upgrade.js +29 -31
  17. package/dist/commands/upload.d.ts +10 -10
  18. package/dist/commands/upload.js +42 -43
  19. package/dist/commands/whoami.js +17 -20
  20. package/dist/config/environments.js +6 -12
  21. package/dist/config/flags/api.flags.js +1 -4
  22. package/dist/config/flags/binary.flags.js +1 -4
  23. package/dist/config/flags/device.flags.js +6 -9
  24. package/dist/config/flags/environment.flags.js +1 -4
  25. package/dist/config/flags/execution.flags.js +1 -4
  26. package/dist/config/flags/github.flags.js +1 -4
  27. package/dist/config/flags/output.flags.js +1 -4
  28. package/dist/constants.js +15 -18
  29. package/dist/gateways/api-gateway.d.ts +31 -6
  30. package/dist/gateways/api-gateway.js +70 -16
  31. package/dist/gateways/cli-auth-gateway.d.ts +1 -1
  32. package/dist/gateways/cli-auth-gateway.js +3 -6
  33. package/dist/gateways/realtime-gateway.d.ts +32 -0
  34. package/dist/gateways/realtime-gateway.js +103 -0
  35. package/dist/gateways/supabase-gateway.d.ts +1 -1
  36. package/dist/gateways/supabase-gateway.js +10 -14
  37. package/dist/index.js +41 -38
  38. package/dist/mcp/context.d.ts +33 -0
  39. package/dist/mcp/context.js +33 -0
  40. package/dist/mcp/helpers.d.ts +16 -0
  41. package/dist/mcp/helpers.js +34 -0
  42. package/dist/mcp/index.d.ts +2 -0
  43. package/dist/mcp/index.js +24 -0
  44. package/dist/mcp/server.d.ts +7 -0
  45. package/dist/mcp/server.js +27 -0
  46. package/dist/mcp/tools/download-artifacts.d.ts +11 -0
  47. package/dist/mcp/tools/download-artifacts.js +84 -0
  48. package/dist/mcp/tools/get-status.d.ts +7 -0
  49. package/dist/mcp/tools/get-status.js +39 -0
  50. package/dist/mcp/tools/list-devices.d.ts +7 -0
  51. package/dist/mcp/tools/list-devices.js +27 -0
  52. package/dist/mcp/tools/list-runs.d.ts +3 -0
  53. package/dist/mcp/tools/list-runs.js +60 -0
  54. package/dist/mcp/tools/run-cloud-test.d.ts +14 -0
  55. package/dist/mcp/tools/run-cloud-test.js +233 -0
  56. package/dist/methods.d.ts +32 -1
  57. package/dist/methods.js +125 -66
  58. package/dist/services/device-validation.service.d.ts +1 -1
  59. package/dist/services/device-validation.service.js +1 -5
  60. package/dist/services/execution-plan.service.js +14 -17
  61. package/dist/services/execution-plan.utils.js +15 -23
  62. package/dist/services/flow-paths.d.ts +17 -0
  63. package/dist/services/flow-paths.js +52 -0
  64. package/dist/services/metadata-extractor.service.js +22 -25
  65. package/dist/services/moropo.service.js +18 -20
  66. package/dist/services/report-download.service.d.ts +1 -1
  67. package/dist/services/report-download.service.js +5 -9
  68. package/dist/services/results-polling.service.d.ts +18 -3
  69. package/dist/services/results-polling.service.js +195 -108
  70. package/dist/services/telemetry.service.d.ts +10 -1
  71. package/dist/services/telemetry.service.js +40 -18
  72. package/dist/services/test-submission.service.d.ts +21 -4
  73. package/dist/services/test-submission.service.js +51 -34
  74. package/dist/services/version.service.d.ts +1 -1
  75. package/dist/services/version.service.js +1 -5
  76. package/dist/types/domain/auth.types.d.ts +8 -0
  77. package/dist/types/domain/auth.types.js +1 -2
  78. package/dist/types/domain/device.types.js +8 -11
  79. package/dist/types/domain/live.types.js +1 -2
  80. package/dist/types/generated/schema.types.js +1 -2
  81. package/dist/types/index.d.ts +2 -2
  82. package/dist/types/index.js +2 -18
  83. package/dist/types.js +1 -2
  84. package/dist/utils/auth.d.ts +1 -1
  85. package/dist/utils/auth.js +27 -28
  86. package/dist/utils/ci.d.ts +12 -0
  87. package/dist/utils/ci.js +39 -0
  88. package/dist/utils/cli.js +18 -27
  89. package/dist/utils/compatibility.d.ts +1 -1
  90. package/dist/utils/compatibility.js +5 -7
  91. package/dist/utils/config-store.js +33 -43
  92. package/dist/utils/connectivity.js +1 -4
  93. package/dist/utils/expo.js +15 -21
  94. package/dist/utils/orgs.js +8 -12
  95. package/dist/utils/paths.js +2 -5
  96. package/dist/utils/progress.js +2 -5
  97. package/dist/utils/styling.d.ts +35 -37
  98. package/dist/utils/styling.js +52 -86
  99. package/dist/utils/ui.d.ts +41 -0
  100. package/dist/utils/ui.js +95 -0
  101. package/package.json +27 -24
@@ -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
- const citty_1 = require("citty");
6
- const path = require("node:path");
7
- const constants_1 = require("../constants");
8
- const api_gateway_1 = require("../gateways/api-gateway");
9
- const methods_1 = require("../methods");
10
- const device_validation_service_1 = require("../services/device-validation.service");
11
- const execution_plan_service_1 = require("../services/execution-plan.service");
12
- const moropo_service_1 = require("../services/moropo.service");
13
- const report_download_service_1 = require("../services/report-download.service");
14
- const results_polling_service_1 = require("../services/results-polling.service");
15
- const telemetry_service_1 = require("../services/telemetry.service");
16
- const test_submission_service_1 = require("../services/test-submission.service");
17
- const version_service_1 = require("../services/version.service");
18
- const device_types_1 = require("../types/domain/device.types");
19
- const auth_1 = require("../utils/auth");
20
- const cli_1 = require("../utils/cli");
21
- const compatibility_1 = require("../utils/compatibility");
22
- const config_store_1 = require("../utils/config-store");
23
- const expo_1 = require("../utils/expo");
24
- const paths_1 = require("../utils/paths");
25
- const styling_1 = require("../utils/styling");
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
- exports.cloudCommand = (0, citty_1.defineCommand)({
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
- ...constants_1.flags,
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 = (0, cli_1.getCliVersion)();
75
- const deviceValidationService = new device_validation_service_1.DeviceValidationService();
76
- const moropoService = new moropo_service_1.MoropoService();
77
- const reportDownloadService = new report_download_service_1.ReportDownloadService();
78
- const resultsPollingService = new results_polling_service_1.ResultsPollingService();
79
- const testSubmissionService = new test_submission_service_1.TestSubmissionService();
80
- const versionService = new version_service_1.VersionService();
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
- const body = `${styling_1.symbols.warning} ${styling_1.colors.bold('Update Available')}\n` +
85
- styling_1.colors.dim('A new version of the DeviceCloud CLI is available: ') +
86
- styling_1.colors.highlight(latestVersion) +
87
- '\n' +
88
- styling_1.colors.dim('Run: ') +
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
- cli_1.logger.log(m);
100
+ logger.log(m);
104
101
  };
105
102
  const warnOut = (m) => {
106
103
  if (!jsonFlag)
107
- cli_1.logger.warn(m);
104
+ logger.warn(m);
108
105
  };
109
106
  try {
110
107
  const apiKeyFlag = args['api-key'];
111
- const apiUrl = (0, config_store_1.resolveApiUrl)(args['api-url']);
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 = (0, cli_1.validateEnum)(args['download-artifacts'], DOWNLOAD_OPTIONS, 'download-artifacts');
120
+ const downloadArtifacts = validateEnum(args['download-artifacts'], DOWNLOAD_OPTIONS, 'download-artifacts');
124
121
  const dryRun = Boolean(args['dry-run']);
125
- const env = (0, cli_1.coerceArray)(args.env, false);
126
- const excludeFlows = (0, cli_1.coerceArray)(args['exclude-flows']);
127
- const excludeTags = (0, cli_1.coerceArray)(args['exclude-tags']);
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 = (0, cli_1.coerceArray)(args['include-tags']);
132
- const iOSDevice = (0, cli_1.validateEnum)(args['ios-device'], Object.values(device_types_1.EiOSDevices), 'ios-device');
133
- const iOSVersion = (0, cli_1.validateEnum)(args['ios-version'], Object.values(device_types_1.EiOSVersions), 'ios-version');
134
- const androidApiLevel = (0, cli_1.validateEnum)(args['android-api-level'], Object.values(device_types_1.EAndroidApiLevels), 'android-api-level');
135
- const androidDevice = (0, cli_1.validateEnum)(args['android-device'], Object.values(device_types_1.EAndroidDevices), 'android-device');
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 = (0, cli_1.coerceArray)(args.metadata, false);
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 = (0, cli_1.validateEnum)(args.orientation, ORIENTATION_OPTIONS, 'orientation');
143
+ const orientation = validateEnum(args.orientation, ORIENTATION_OPTIONS, 'orientation');
147
144
  let quiet = Boolean(args.quiet);
148
- const report = (0, cli_1.validateEnum)(args.report, REPORT_OPTIONS, 'report');
149
- let retry = (0, cli_1.parseIntFlag)(args.retry, 'retry');
150
- const runnerType = (0, cli_1.validateEnum)(args['runner-type'], RUNNER_TYPE_OPTIONS, 'runner-type') ?? 'default';
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(styling_1.colors.dim(`dcd v${cliVersion}`));
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 cli_1.CliError('--mitmPath requires --mitmHost to be set');
169
+ throw new CliError('--mitmPath requires --mitmHost to be set');
173
170
  }
174
171
  if (jsonFileName && !jsonFileFlag) {
175
- throw new cli_1.CliError('--json-file-name requires --json-file');
172
+ throw new CliError('--json-file-name requires --json-file');
176
173
  }
177
174
  if (artifactsPath && !downloadArtifacts) {
178
- throw new cli_1.CliError('--artifacts-path requires --download-artifacts');
175
+ throw new CliError('--artifacts-path requires --download-artifacts');
179
176
  }
180
177
  if ((junitPath || allurePath || htmlPath) && !report) {
181
- throw new cli_1.CliError('Report path flags (--junit-path/--allure-path/--html-path) require --report');
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 cli_1.CliError('--app-file and --app-url are mutually exclusive');
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 cli_1.CliError('Invalid node version');
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 (0, auth_1.resolveAuth)({ apiKeyFlag });
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 (0, compatibility_1.fetchCompatibilityData)(apiUrl, auth);
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 cli_1.CliError(`Failed to fetch device compatibility data: ${errorMessage}. Please check your API key and connection.`);
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 cli_1.CliError(`Maestro version ${resolvedMaestroVersion} is no longer supported. ` +
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(`${styling_1.symbols.warning} ` +
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(`${styling_1.symbols.info} ` +
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(`${styling_1.symbols.info} ` +
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(`${styling_1.symbols.info} ` +
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 ((0, expo_1.isUrl)(finalAppFile)) {
266
- out(` ${styling_1.colors.dim('→ Downloading Expo build from URL...')}`);
267
- const tarPath = await (0, expo_1.downloadExpoUrl)(finalAppFile, debug);
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(` ${styling_1.colors.dim('→ Extracting Expo archive...')}`);
273
- const extractDir = await (0, expo_1.extractTarGz)(finalAppFile, debug);
270
+ out(` ${colors.dim('→ Extracting Expo archive...')}`);
271
+ const extractDir = await extractTarGz(finalAppFile, debug);
274
272
  tempFiles.push(extractDir);
275
- finalAppFile = await (0, expo_1.findAppBundle)(extractDir);
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 cli_1.CliError('You cannot provide both an appBinaryId and a binary file');
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 cli_1.CliError('You must provide a flow file');
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 (0, execution_plan_service_1.plan)({
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 pathsShortestToLongest = [
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 cli_1.CliError('You must provide a flow file and an app binary id');
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 cli_1.CliError('App file must be a .apk for Android, .app/.zip for iOS, or .tar.gz (Expo iOS build)');
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 (0, methods_1.verifyAppZip)(finalAppFile);
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(constants_1.flags));
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
- let overridesLog = '';
434
- if (hasOverrides) {
435
- overridesLog = '\n\n ' + styling_1.colors.bold('With overrides');
436
- for (const [flowPath, overrides] of overridesEntries) {
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
- out(`\n ${styling_1.colors.bold('With options')}`);
451
- for (const flagLog of flagLogs) {
405
+ submitRows.push('', colors.bold('Options'));
406
+ submitRows.push(...ui.fields(flagLogs.map((flagLog) => {
452
407
  const [key, ...valueParts] = flagLog.split(': ');
453
- const value = valueParts.join(': ');
454
- out(` ${styling_1.colors.dim('→ ' + key + ':')} ${styling_1.colors.highlight(value)}`);
455
- }
408
+ return [key, colors.highlight(valueParts.join(': '))];
409
+ })));
456
410
  }
457
411
  if (hasOverrides) {
458
- out(overridesLog);
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(`\n${styling_1.symbols.warning} ${styling_1.colors.bold('Dry run mode')} ${styling_1.colors.dim('- no tests were actually triggered')}\n`);
463
- out(styling_1.colors.bold('The following tests would have been run:'));
464
- out(styling_1.dividers.light);
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(`\n${styling_1.colors.bold('Sequential flows:')}`);
470
- out(styling_1.dividers.short);
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 cli_1.CliError('You must provide either an app binary id or an app file');
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 (0, methods_1.uploadBinary)({
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 cli_1.CliError('Internal error: finalBinaryId should be defined after validation');
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 testFormData = await testSubmissionService.buildTestFormData({
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
- if (debug) {
545
- out(`[DEBUG] Submitting flow upload request to ${apiUrl}/uploads/flow`);
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 } = await api_gateway_1.ApiGateway.uploadFlow(apiUrl, auth, testFormData);
529
+ const { message, results } = response;
548
530
  if (debug) {
549
- out(`[DEBUG] Flow upload response received`);
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 cli_1.CliError('No tests created: ' + message);
536
+ throw new CliError('No tests created: ' + message);
555
537
  }
556
- out(`${styling_1.symbols.success} ${styling_1.colors.bold('Submitted')} ${styling_1.colors.dim(message)}`);
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(styling_1.colors.dim(', '));
561
- out(`\n${styling_1.colors.bold(`Created ${results.length} test${results.length === 1 ? '' : 's'}:`)} ${testNames}\n`);
562
- const url = (0, styling_1.getConsoleUrl)(apiUrl, results[0].test_upload_id, results[0].id);
563
- out(styling_1.colors.bold('Run triggered') + styling_1.colors.dim(', you can access the results at:'));
564
- out((0, styling_1.formatUrl)(url));
565
- out('');
566
- out(styling_1.colors.dim('Your upload ID is: ') + (0, styling_1.formatId)(results[0].test_upload_id));
567
- out(styling_1.colors.dim('Poll upload status using: ') +
568
- styling_1.colors.info(`dcd status --api-key ... --upload-id ${results[0].test_upload_id}`));
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
- (0, methods_1.writeJSONFile)(jsonFilePath, jsonOutput, {
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(`\n${styling_1.symbols.info} ${styling_1.colors.dim('Not waiting for results as async flag is set to true')}\n`);
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 results_polling_service_1.RunFailedError) {
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
- (0, methods_1.writeJSONFile)(jsonFilePath, jsonOutput, {
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
- (0, methods_1.writeJSONFile)(jsonFilePath, jsonOutput, {
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 Promise.resolve().then(() => require('node:fs/promises'));
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
- telemetry_service_1.telemetry.recordCommandFailure({ error: 'RUN_FAILED', exitCode });
727
- telemetry_service_1.telemetry.flushSync();
713
+ telemetry.recordCommandFailure({ error: 'RUN_FAILED', exitCode });
714
+ telemetry.flushSync();
728
715
  process.exit(exitCode);
729
716
  }
730
- cli_1.logger.error(caughtError, { exit: 1, json: jsonFlag });
717
+ logger.error(caughtError, { exit: 1, json: jsonFlag });
731
718
  }
732
719
  },
733
720
  });
734
- exports.default = exports.cloudCommand;
721
+ export default cloudCommand;