@devicecloud.dev/dcd 5.6.0-beta.3 → 5.6.0

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 (39) hide show
  1. package/README.md +4 -2
  2. package/dist/commands/artifacts.d.ts +1 -1
  3. package/dist/commands/artifacts.js +1 -1
  4. package/dist/commands/cloud.d.ts +5 -5
  5. package/dist/commands/cloud.js +14 -6
  6. package/dist/commands/switch-org.js +5 -1
  7. package/dist/commands/upload.js +5 -1
  8. package/dist/commands/whoami.d.ts +7 -1
  9. package/dist/commands/whoami.js +26 -2
  10. package/dist/config/flags/device.flags.d.ts +3 -3
  11. package/dist/config/flags/device.flags.js +5 -3
  12. package/dist/config/flags/execution.flags.d.ts +1 -1
  13. package/dist/config/flags/execution.flags.js +1 -1
  14. package/dist/config/flags/output.flags.d.ts +1 -1
  15. package/dist/config/flags/output.flags.js +1 -1
  16. package/dist/constants.d.ts +5 -5
  17. package/dist/gateways/api-gateway.js +2 -1
  18. package/dist/gateways/realtime-gateway.d.ts +7 -0
  19. package/dist/gateways/realtime-gateway.js +12 -1
  20. package/dist/mcp/context.d.ts +14 -3
  21. package/dist/mcp/context.js +15 -3
  22. package/dist/mcp/tools/download-artifacts.js +9 -6
  23. package/dist/mcp/tools/list-devices.js +1 -1
  24. package/dist/mcp/tools/run-cloud-test.js +19 -3
  25. package/dist/services/device-validation.service.d.ts +17 -2
  26. package/dist/services/device-validation.service.js +93 -12
  27. package/dist/services/notices.service.js +3 -0
  28. package/dist/services/report-download.service.js +9 -3
  29. package/dist/services/results-polling.service.d.ts +21 -0
  30. package/dist/services/results-polling.service.js +55 -3
  31. package/dist/services/test-submission.service.d.ts +11 -0
  32. package/dist/services/test-submission.service.js +12 -5
  33. package/dist/types/domain/auth.types.d.ts +6 -0
  34. package/dist/types/domain/device.types.d.ts +18 -4
  35. package/dist/types/domain/device.types.js +17 -3
  36. package/dist/types/generated/schema.types.d.ts +127 -167
  37. package/dist/utils/auth.d.ts +33 -0
  38. package/dist/utils/auth.js +61 -2
  39. package/package.json +2 -2
@@ -1,5 +1,35 @@
1
+ const DEVICES_DOCS_URL = 'https://docs.devicecloud.dev/getting-started/devices-configuration';
2
+ /** `dcd cloud`'s spelling; the MCP tool passes its own parameter names. */
3
+ const CLI_ARG_NAMES = {
4
+ androidApiLevel: '--android-api-level',
5
+ androidDevice: '--android-device',
6
+ iOSDevice: '--ios-device',
7
+ iOSVersion: '--ios-version',
8
+ };
9
+ /** The versions a lookup offers for one device; empty when it offers none. */
10
+ function versionsFor(lookup, device) {
11
+ // hasOwn, so a slug like "constructor" can't resolve to a prototype member.
12
+ if (!lookup || !Object.hasOwn(lookup, device))
13
+ return [];
14
+ const versions = lookup[device];
15
+ return Array.isArray(versions) ? versions : [];
16
+ }
17
+ /**
18
+ * Devices the lookup can actually run, in the API's order. A rollout gate can
19
+ * leave a device with no versions (every non-Play device has an empty Google
20
+ * Play list), and those must not be offered as alternatives.
21
+ */
22
+ function offeredDevices(lookup) {
23
+ return Object.keys(lookup ?? {}).filter((device) => versionsFor(lookup, device).length > 0);
24
+ }
1
25
  /**
2
- * Service for validating device configurations against compatibility data
26
+ * Validates requested devices against the compatibility matrix the API serves
27
+ * from `GET /results/compatibility/data`, which is the only authority on what
28
+ * can run. Nothing here consults the CLI's device and OS enums
29
+ * (src/types/domain/device.types.ts) — those only feed `--help` — so a device
30
+ * or OS version the API adds is accepted without a CLI release, and one it
31
+ * withdraws is refused, before anything is uploaded, with the list the API
32
+ * offers today.
3
33
  */
4
34
  export class DeviceValidationService {
5
35
  /**
@@ -13,6 +43,10 @@ export class DeviceValidationService {
13
43
  * @throws Error if device/API level combination is not supported
14
44
  */
15
45
  validateAndroidDevice(androidApiLevel, androidDevice, googlePlay, compatibilityData, options = {}) {
46
+ const lookup = googlePlay
47
+ ? compatibilityData.androidPlay
48
+ : compatibilityData.android;
49
+ const argNames = options.argNames ?? CLI_ARG_NAMES;
16
50
  this.validateDevice({
17
51
  debugLines: (deviceID, version, supportedVersions) => [
18
52
  `[DEBUG] Android device: ${deviceID}`,
@@ -20,16 +54,27 @@ export class DeviceValidationService {
20
54
  `[DEBUG] Google Play enabled: ${googlePlay}`,
21
55
  `[DEBUG] Supported Android versions: ${supportedVersions.join(', ')}`,
22
56
  ],
57
+ // Mirrors the API, which applies these same global defaults to a
58
+ // request that omits either half (test-request-validator.service.ts)
59
+ // and then rejects an incompatible pair.
23
60
  defaultDevice: 'pixel-7',
24
61
  defaultVersion: '34',
25
62
  device: androidDevice,
26
- lookup: googlePlay
27
- ? compatibilityData.androidPlay
28
- : compatibilityData.android,
29
- noSupportMessage: () => `We don't support that device configuration - please check the docs for supported devices: https://docs.devicecloud.dev/getting-started/devices-configuration`,
63
+ deviceArg: argNames.androidDevice,
64
+ deviceKind: googlePlay ? 'Google Play' : 'Android',
65
+ formatVersion: (version) => `API level ${version}`,
66
+ lookup,
30
67
  options,
68
+ platform: 'Android',
69
+ // A real device that merely has no Google Play image deserves a sharper
70
+ // answer than "not supported".
71
+ unknownDeviceMessage: (deviceID) => googlePlay && versionsFor(compatibilityData.android, deviceID).length > 0
72
+ ? `Android device "${deviceID}" is not available with Google Play. Google Play devices: ${offeredDevices(lookup).join(', ')}. See ${DEVICES_DOCS_URL}`
73
+ : undefined,
31
74
  unsupportedVersionMessage: (deviceID, supportedVersions) => `${deviceID} ${googlePlay ? '(Play Store) ' : ''}only supports these Android API levels: ${supportedVersions.join(', ')}`,
32
75
  version: androidApiLevel,
76
+ versionArg: argNames.androidApiLevel,
77
+ versionName: 'Android API level',
33
78
  });
34
79
  }
35
80
  /**
@@ -42,20 +87,27 @@ export class DeviceValidationService {
42
87
  * @throws Error if device/version combination is not supported
43
88
  */
44
89
  validateiOSDevice(iOSVersion, iOSDevice, compatibilityData, options = {}) {
90
+ const argNames = options.argNames ?? CLI_ARG_NAMES;
45
91
  this.validateDevice({
46
92
  debugLines: (deviceID, version, supportedVersions) => [
47
93
  `[DEBUG] iOS device: ${deviceID}`,
48
94
  `[DEBUG] iOS version: ${version}`,
49
95
  `[DEBUG] Supported iOS versions: ${supportedVersions.join(', ')}`,
50
96
  ],
97
+ // Mirrors the API's global defaults, as for Android above.
51
98
  defaultDevice: 'iphone-14',
52
99
  defaultVersion: '17',
53
100
  device: iOSDevice,
101
+ deviceArg: argNames.iOSDevice,
102
+ deviceKind: 'iOS',
103
+ formatVersion: (version) => `iOS ${version}`,
54
104
  lookup: compatibilityData?.ios,
55
- noSupportMessage: (deviceID) => `Device ${deviceID} is not supported. Please check the docs for supported devices: https://docs.devicecloud.dev/getting-started/devices-configuration`,
56
105
  options,
106
+ platform: 'iOS',
57
107
  unsupportedVersionMessage: (deviceID, supportedVersions) => `${deviceID} only supports these iOS versions: ${supportedVersions.join(', ')}`,
58
108
  version: iOSVersion,
109
+ versionArg: argNames.iOSVersion,
110
+ versionName: 'iOS version',
59
111
  });
60
112
  }
61
113
  /**
@@ -66,20 +118,49 @@ export class DeviceValidationService {
66
118
  * @throws Error if device/version combination is not supported
67
119
  */
68
120
  validateDevice(config) {
69
- const { debugLines, defaultDevice, defaultVersion, device, lookup, noSupportMessage, options, unsupportedVersionMessage, version, } = config;
121
+ const { debugLines, defaultDevice, defaultVersion, device, deviceArg, deviceKind, formatVersion, lookup, options, platform, unknownDeviceMessage, unsupportedVersionMessage, version, versionArg, versionName, } = config;
70
122
  const { debug = false, logger } = options;
71
123
  if (!version && !device) {
72
124
  return;
73
125
  }
74
126
  const deviceID = device || defaultDevice;
75
- const supportedVersions = lookup?.[deviceID] || [];
127
+ const supportedVersions = versionsFor(lookup, deviceID);
76
128
  const requestedVersion = version || defaultVersion;
77
129
  if (supportedVersions.length === 0) {
78
- throw new Error(noSupportMessage(deviceID));
130
+ if (!device) {
131
+ // Only a version was requested, and the API no longer offers the
132
+ // device the CLI assumes it defaults to. That assumption is what's
133
+ // stale, not the request: the API resolves and validates its own
134
+ // default device when the run is submitted.
135
+ if (debug && logger) {
136
+ logger(`[DEBUG] Default ${platform} device ${deviceID} is not in the compatibility data; leaving the ${versionName} check to the API`);
137
+ }
138
+ return;
139
+ }
140
+ const offered = offeredDevices(lookup);
141
+ throw new Error(unknownDeviceMessage?.(deviceID) ??
142
+ (offered.length > 0
143
+ ? `${platform} device "${deviceID}" is not supported. Supported ${deviceKind} devices: ${offered.join(', ')}. See ${DEVICES_DOCS_URL}`
144
+ : `${platform} device "${deviceID}" is not supported. See ${DEVICES_DOCS_URL}`));
79
145
  }
80
- if (Array.isArray(supportedVersions) &&
81
- !supportedVersions.includes(requestedVersion)) {
82
- throw new Error(unsupportedVersionMessage(deviceID, supportedVersions));
146
+ if (!supportedVersions.includes(requestedVersion)) {
147
+ const unsupported = unsupportedVersionMessage(deviceID, supportedVersions);
148
+ if (!version) {
149
+ // Device-only request. The API fills in the same global default
150
+ // version rather than one this device runs, so the submission would
151
+ // be refused there too — say so, and how to avoid it.
152
+ throw new Error(`${unsupported}. No ${versionName} was given, so the default (${defaultVersion}) was checked: pass one of those explicitly with ${versionArg}`);
153
+ }
154
+ if (!device) {
155
+ // Version-only request: name the device that was assumed, and the
156
+ // devices that do offer the requested version.
157
+ const devicesWithVersion = offeredDevices(lookup).filter((d) => versionsFor(lookup, d).includes(requestedVersion));
158
+ throw new Error(`${unsupported}. No ${platform} device was given, so the default (${deviceID}) was checked: ` +
159
+ (devicesWithVersion.length > 0
160
+ ? `pass one that offers ${formatVersion(requestedVersion)} with ${deviceArg}: ${devicesWithVersion.join(', ')}`
161
+ : `no ${deviceKind} device offers ${formatVersion(requestedVersion)}`));
162
+ }
163
+ throw new Error(unsupported);
83
164
  }
84
165
  if (debug && logger) {
85
166
  for (const line of debugLines(deviceID, requestedVersion, supportedVersions)) {
@@ -80,6 +80,9 @@ function renderNotice(notice, opts) {
80
80
  export function platformFromAppFile(appFile) {
81
81
  if (!appFile)
82
82
  return undefined;
83
+ // Expo iOS simulator builds (`dcd cloud` extracts these to a .app first).
84
+ if (appFile.split('?')[0].toLowerCase().endsWith('.tar.gz'))
85
+ return 'ios';
83
86
  const ext = appFile.split('?')[0].toLowerCase().match(/.([a-z0-9]+)$/)?.[1];
84
87
  if (ext === 'apk' || ext === 'aab')
85
88
  return 'android';
@@ -1,5 +1,6 @@
1
1
  import * as path from 'node:path';
2
2
  import { ApiGateway } from '../gateways/api-gateway.js';
3
+ import { refreshAuth } from '../utils/auth.js';
3
4
  /**
4
5
  * Service for downloading test artifacts and reports
5
6
  */
@@ -15,7 +16,9 @@ export class ReportDownloadService {
15
16
  if (debug && logger) {
16
17
  logger(`[DEBUG] Downloading artifacts: ${downloadType}`);
17
18
  }
18
- await ApiGateway.downloadArtifactsZip(apiUrl, auth, uploadId, downloadType, artifactsPath);
19
+ // `dcd cloud` downloads after polling, which can outlast a `dcd login`
20
+ // token; a failed refresh lands in the warning below like any failure.
21
+ await ApiGateway.downloadArtifactsZip(apiUrl, await refreshAuth(auth), uploadId, downloadType, artifactsPath);
19
22
  if (logger) {
20
23
  logger('\n');
21
24
  logger(`Test artifacts have been downloaded to ${artifactsPath}`);
@@ -54,7 +57,9 @@ export class ReportDownloadService {
54
57
  }
55
58
  case 'html':
56
59
  case 'html-detailed': {
57
- const htmlReportPath = path.resolve(process.cwd(), htmlPath || 'report.html');
60
+ // The HTML report is a ZIP (report.html plus its screenshots and
61
+ // assets), so the default name says so. An explicit path is kept as-is.
62
+ const htmlReportPath = path.resolve(process.cwd(), htmlPath || 'report.zip');
58
63
  await this.downloadReport('html', htmlReportPath, {
59
64
  ...downloadOptions,
60
65
  warnLogger,
@@ -81,7 +86,8 @@ export class ReportDownloadService {
81
86
  if (debug && logger) {
82
87
  logger(`[DEBUG] Downloading ${type.toUpperCase()} report`);
83
88
  }
84
- await ApiGateway.downloadReportGeneric(apiUrl, auth, uploadId, type, filePath);
89
+ // As for artifacts: this can run after a poll that outlasted the token.
90
+ await ApiGateway.downloadReportGeneric(apiUrl, await refreshAuth(auth), uploadId, type, filePath);
85
91
  if (logger) {
86
92
  logger(`${type.toUpperCase()} test report has been downloaded to ${filePath}`);
87
93
  }
@@ -6,6 +6,14 @@ export declare class RunFailedError extends Error {
6
6
  result: PollingResult;
7
7
  constructor(result: PollingResult);
8
8
  }
9
+ /**
10
+ * The `dcd login` session behind a poll expired and only a new login can fix
11
+ * it, so the poll stops at once instead of spending its retry budget — meant
12
+ * for network blips — on a failure that can't clear.
13
+ */
14
+ export declare class PollingAuthError extends Error {
15
+ constructor(message: string);
16
+ }
9
17
  export interface PollingOptions {
10
18
  auth: AuthContext;
11
19
  apiUrl: string;
@@ -115,6 +123,19 @@ export declare class ResultsPollingService {
115
123
  * @returns Promise resolving to test results
116
124
  */
117
125
  private fetchAndLogResults;
126
+ /**
127
+ * Swap in a fresh `dcd login` token when the current one is about to
128
+ * expire, and hand it to the realtime socket too; API-key auth comes back
129
+ * as-is. A transient refresh failure is thrown as-is, to be retried like
130
+ * any failed poll; one only `dcd login` can fix becomes a PollingAuthError.
131
+ * @param auth The credential the previous poll used
132
+ * @param uploadId Upload being polled, for the reconnect hint
133
+ * @param subscription Realtime subscription to pass a new token to
134
+ * @param debug Whether debug logging is enabled
135
+ * @param logger Optional logger function
136
+ * @returns The credential for the next poll
137
+ */
138
+ private refreshPollingAuth;
118
139
  private filterLatestResults;
119
140
  /**
120
141
  * Handle completed tests and return final result
@@ -2,6 +2,7 @@ import * as path from 'node:path';
2
2
  import { ApiGateway } from '../gateways/api-gateway.js';
3
3
  import { RealtimeResultsGateway, } from '../gateways/realtime-gateway.js';
4
4
  import { formatDurationSeconds } from '../methods.js';
5
+ import { isDefinitiveAuthFailure, refreshAuth } from '../utils/auth.js';
5
6
  import { checkInternetConnectivity } from '../utils/connectivity.js';
6
7
  import { isCI } from '../utils/ci.js';
7
8
  import { ux } from '../utils/progress.js';
@@ -18,6 +19,17 @@ export class RunFailedError extends Error {
18
19
  this.name = 'RunFailedError';
19
20
  }
20
21
  }
22
+ /**
23
+ * The `dcd login` session behind a poll expired and only a new login can fix
24
+ * it, so the poll stops at once instead of spending its retry budget — meant
25
+ * for network blips — on a failure that can't clear.
26
+ */
27
+ export class PollingAuthError extends Error {
28
+ constructor(message) {
29
+ super(message);
30
+ this.name = 'PollingAuthError';
31
+ }
32
+ }
21
33
  /**
22
34
  * Structured device for a result row. Prefers the friendly deviceName/osVersion
23
35
  * the per-flow targeting / matrix fan-out stamps onto each result's config
@@ -115,7 +127,10 @@ export class ResultsPollingService {
115
127
  }
116
128
  }
117
129
  async pollLoop(options, testMetadata) {
118
- const { apiUrl, auth, uploadId, consoleUrl, quiet = false, json = false, debug = false, logger } = options;
130
+ const { apiUrl, uploadId, consoleUrl, quiet = false, json = false, debug = false, logger } = options;
131
+ // Re-checked before every poll: a run can outlast a `dcd login` access
132
+ // token (about an hour). API-key auth passes through unchanged.
133
+ let { auth } = options;
119
134
  this.initializePollingDisplay(json, logger);
120
135
  let sequentialPollFailures = 0;
121
136
  const pollIntervalMs = auth.mode === 'bearer'
@@ -213,6 +228,7 @@ export class ResultsPollingService {
213
228
  try {
214
229
  nextPollAt = null;
215
230
  renderStatus();
231
+ auth = await this.refreshPollingAuth(auth, uploadId, subscription, debug, logger);
216
232
  const updatedResults = await this.fetchAndLogResults(apiUrl, auth, uploadId, debug, logger);
217
233
  const { summary } = this.calculateStatusSummary(updatedResults);
218
234
  if (!json) {
@@ -238,8 +254,9 @@ export class ResultsPollingService {
238
254
  await waitForNextPoll(pollIntervalMs);
239
255
  }
240
256
  catch (error) {
241
- // Re-throw RunFailedError immediately (test failures, not polling errors)
242
- if (error instanceof RunFailedError) {
257
+ // Re-throw RunFailedError immediately (test failures, not polling
258
+ // errors), and an expired login no retry can fix.
259
+ if (error instanceof RunFailedError || error instanceof PollingAuthError) {
243
260
  throw error;
244
261
  }
245
262
  sequentialPollFailures++;
@@ -399,6 +416,41 @@ export class ResultsPollingService {
399
416
  }
400
417
  return updatedResults;
401
418
  }
419
+ /**
420
+ * Swap in a fresh `dcd login` token when the current one is about to
421
+ * expire, and hand it to the realtime socket too; API-key auth comes back
422
+ * as-is. A transient refresh failure is thrown as-is, to be retried like
423
+ * any failed poll; one only `dcd login` can fix becomes a PollingAuthError.
424
+ * @param auth The credential the previous poll used
425
+ * @param uploadId Upload being polled, for the reconnect hint
426
+ * @param subscription Realtime subscription to pass a new token to
427
+ * @param debug Whether debug logging is enabled
428
+ * @param logger Optional logger function
429
+ * @returns The credential for the next poll
430
+ */
431
+ async refreshPollingAuth(auth, uploadId, subscription, debug, logger) {
432
+ let next;
433
+ try {
434
+ next = await refreshAuth(auth);
435
+ }
436
+ catch (error) {
437
+ if (isDefinitiveAuthFailure(error)) {
438
+ const reason = error instanceof Error ? error.message : String(error);
439
+ throw new PollingAuthError(`Your dcd login session expired and could not be refreshed: ${reason}\n\n` +
440
+ `The test is still running in the cloud. Run \`dcd login\`, then reconnect with:\n dcd status --upload-id ${uploadId}`);
441
+ }
442
+ throw error;
443
+ }
444
+ if (next !== auth) {
445
+ if (debug && logger) {
446
+ logger('[DEBUG] Refreshed the dcd login session for polling');
447
+ }
448
+ if (next.accessToken && next.accessToken !== auth.accessToken) {
449
+ subscription?.updateAccessToken(next.accessToken);
450
+ }
451
+ }
452
+ return next;
453
+ }
402
454
  filterLatestResults(results) {
403
455
  // Resolve each row to the root of its retry chain (a retry's `retry_of`
404
456
  // may point at the previous retry rather than the original attempt), then
@@ -6,6 +6,12 @@ export interface TestSubmissionConfig {
6
6
  androidNoSnapshot?: boolean;
7
7
  apiUrl?: string;
8
8
  appBinaryId: string;
9
+ /**
10
+ * Platform of the app binary, when the CLI can tell from a local file
11
+ * (.apk → android; .app / .zip / Expo .tar.gz → ios). Unknown for
12
+ * --app-binary-id, which falls back to the device flags.
13
+ */
14
+ appPlatform?: 'android' | 'ios';
9
15
  /**
10
16
  * Ask the API to cancel the still-queued tests of the previous run from
11
17
  * the same CI context. Sent as its own field rather than inside `config`,
@@ -19,6 +25,11 @@ export interface TestSubmissionConfig {
19
25
  debug?: boolean;
20
26
  deviceLocale?: string;
21
27
  deviceMatrix?: DeviceMatrixConfig[];
28
+ /**
29
+ * --disable-animations as given on the command line: true / false when the
30
+ * flag (or --no-disable-animations) was passed, undefined when it wasn't —
31
+ * only then does the workspace config's per-platform value apply.
32
+ */
22
33
  disableAnimations?: boolean;
23
34
  /**
24
35
  * Encrypt the flow zip and env vars before upload (#1151/#1152), each with its
@@ -20,7 +20,7 @@ export class TestSubmissionService {
20
20
  * @returns The flow zip buffer, its SHA-256, and the string-encoded fields
21
21
  */
22
22
  async buildTestPayload(config) {
23
- const { apiUrl, appBinaryId, encrypt = false, flowFile, executionPlan, commonRoot, cliVersion, env = [], metadata = [], cancelPrevious = false, googlePlay = false, androidApiLevel, androidDevice, androidNoSnapshot, iOSVersion, iOSDevice, name, runnerType, maestroVersion, deviceLocale, orientation, retry, continueOnFailure = true, renderEngine, report, showCrosshairs, maestroChromeOnboarding, raw, disableAnimations, deviceMatrix, debug = false, logger, } = config;
23
+ const { apiUrl, appBinaryId, appPlatform, encrypt = false, flowFile, executionPlan, commonRoot, cliVersion, env = [], metadata = [], cancelPrevious = false, googlePlay = false, androidApiLevel, androidDevice, androidNoSnapshot, iOSVersion, iOSDevice, name, runnerType, maestroVersion, deviceLocale, orientation, retry, continueOnFailure = true, renderEngine, report, showCrosshairs, maestroChromeOnboarding, raw, disableAnimations, deviceMatrix, debug = false, logger, } = config;
24
24
  const { allExcludeTags, allIncludeTags, flowMetadata, flowOverrides, flowsToRun: testFileNames, includedFiles = [], referencedFiles, sequence, workspaceConfig, } = executionPlan;
25
25
  const { flows: sequentialFlows = [] } = sequence ?? {};
26
26
  const envObject = this.parseKeyValuePairs(env);
@@ -120,18 +120,25 @@ export class TestSubmissionService {
120
120
  fields.deviceMatrix = JSON.stringify(deviceMatrix);
121
121
  }
122
122
  // Platform used only to pick which workspace-config disableAnimations flag
123
- // applies. A device matrix is single-platform; its first cell decides. Fall
124
- // back to the scalar iOS flags for single-device submissions.
123
+ // applies. The binary decides when the CLI knows it — otherwise an iOS run
124
+ // without --ios-device/--ios-version read the android block. For an
125
+ // --app-binary-id, a device matrix (single-platform; its first cell
126
+ // decides) or the scalar iOS flags are the best evidence left.
125
127
  const matrixPlatform = deviceMatrix && deviceMatrix.length > 0
126
128
  ? 'iOSDevice' in deviceMatrix[0]
127
129
  ? 'ios'
128
130
  : 'android'
129
131
  : undefined;
130
- const targetPlatform = matrixPlatform ?? (iOSDevice || iOSVersion ? 'ios' : 'android');
132
+ const targetPlatform = appPlatform ??
133
+ matrixPlatform ??
134
+ (iOSDevice || iOSVersion ? 'ios' : 'android');
131
135
  const configYamlDisableAnimations = targetPlatform === 'ios'
132
136
  ? Boolean(workspaceConfig?.platform?.ios?.disableAnimations)
133
137
  : Boolean(workspaceConfig?.platform?.android?.disableAnimations);
134
- const effectiveDisableAnimations = disableAnimations || configYamlDisableAnimations;
138
+ // An explicit --disable-animations / --no-disable-animations wins over
139
+ // config.yaml; previously `flag || config` meant the flag could only ever
140
+ // turn animations off, never keep them on against the config.
141
+ const effectiveDisableAnimations = disableAnimations ?? configYamlDisableAnimations;
135
142
  const configPayload = {
136
143
  allExcludeTags,
137
144
  allIncludeTags,
@@ -13,6 +13,12 @@ export interface AuthContext {
13
13
  accessToken?: string;
14
14
  /** Environment the session belongs to — present when mode === 'bearer'. */
15
15
  env?: DcdEnvName;
16
+ /**
17
+ * Unix epoch seconds at which `accessToken` expires — present when
18
+ * mode === 'bearer'. Long-lived callers check it (utils/auth.ts
19
+ * isAuthExpiring / refreshAuth) because the headers below stop working then.
20
+ */
21
+ expiresAt?: number;
16
22
  /** Present when mode === 'bearer'. */
17
23
  orgId?: string;
18
24
  /** Present when mode === 'bearer'. */
@@ -1,17 +1,31 @@
1
1
  /**
2
2
  * Device type definitions - should be kept in sync with API
3
- * @see /Users/riglar/repos/dcd/api/src/common/types/device.types.ts
3
+ * @see dcd/api/src/common/types/device.types.ts
4
+ *
5
+ * These drive the `--ios-device` / `--ios-version` / `--android-device` /
6
+ * `--android-api-level` help text and nothing else: validation runs against
7
+ * the live compatibility payload (device-validation.service.ts). A value the
8
+ * API has dropped gets a "not supported" error listing what it offers now, and
9
+ * one it has added is accepted even if it is missing here — so keeping these
10
+ * lists current only improves the help text; it never gates a run.
11
+ *
12
+ * `iphone-14-pro` and `iphone-15-pro` were listed here but have never existed
13
+ * in the API enum; removed 2026-09-21 rather than carried forward.
4
14
  */
5
15
  export declare enum EiOSDevices {
6
16
  'ipad-pro-6th-gen' = "ipad-pro-6th-gen",
17
+ 'ipad-pro-m5-11' = "ipad-pro-m5-11",
18
+ 'ipad-pro-m5-13' = "ipad-pro-m5-13",
7
19
  'iphone-14' = "iphone-14",
8
- 'iphone-14-pro' = "iphone-14-pro",
9
20
  'iphone-15' = "iphone-15",
10
- 'iphone-15-pro' = "iphone-15-pro",
11
21
  'iphone-16' = "iphone-16",
12
22
  'iphone-16-plus' = "iphone-16-plus",
13
23
  'iphone-16-pro' = "iphone-16-pro",
14
- 'iphone-16-pro-max' = "iphone-16-pro-max"
24
+ 'iphone-16-pro-max' = "iphone-16-pro-max",
25
+ 'iphone-17' = "iphone-17",
26
+ 'iphone-18-pro' = "iphone-18-pro",
27
+ 'iphone-18-pro-max' = "iphone-18-pro-max",
28
+ 'iphone-air' = "iphone-air"
15
29
  }
16
30
  export declare enum EAndroidDevices {
17
31
  'generic-tablet' = "generic-tablet",
@@ -1,18 +1,32 @@
1
1
  /**
2
2
  * Device type definitions - should be kept in sync with API
3
- * @see /Users/riglar/repos/dcd/api/src/common/types/device.types.ts
3
+ * @see dcd/api/src/common/types/device.types.ts
4
+ *
5
+ * These drive the `--ios-device` / `--ios-version` / `--android-device` /
6
+ * `--android-api-level` help text and nothing else: validation runs against
7
+ * the live compatibility payload (device-validation.service.ts). A value the
8
+ * API has dropped gets a "not supported" error listing what it offers now, and
9
+ * one it has added is accepted even if it is missing here — so keeping these
10
+ * lists current only improves the help text; it never gates a run.
11
+ *
12
+ * `iphone-14-pro` and `iphone-15-pro` were listed here but have never existed
13
+ * in the API enum; removed 2026-09-21 rather than carried forward.
4
14
  */
5
15
  export var EiOSDevices;
6
16
  (function (EiOSDevices) {
7
17
  EiOSDevices["ipad-pro-6th-gen"] = "ipad-pro-6th-gen";
18
+ EiOSDevices["ipad-pro-m5-11"] = "ipad-pro-m5-11";
19
+ EiOSDevices["ipad-pro-m5-13"] = "ipad-pro-m5-13";
8
20
  EiOSDevices["iphone-14"] = "iphone-14";
9
- EiOSDevices["iphone-14-pro"] = "iphone-14-pro";
10
21
  EiOSDevices["iphone-15"] = "iphone-15";
11
- EiOSDevices["iphone-15-pro"] = "iphone-15-pro";
12
22
  EiOSDevices["iphone-16"] = "iphone-16";
13
23
  EiOSDevices["iphone-16-plus"] = "iphone-16-plus";
14
24
  EiOSDevices["iphone-16-pro"] = "iphone-16-pro";
15
25
  EiOSDevices["iphone-16-pro-max"] = "iphone-16-pro-max";
26
+ EiOSDevices["iphone-17"] = "iphone-17";
27
+ EiOSDevices["iphone-18-pro"] = "iphone-18-pro";
28
+ EiOSDevices["iphone-18-pro-max"] = "iphone-18-pro-max";
29
+ EiOSDevices["iphone-air"] = "iphone-air";
16
30
  })(EiOSDevices || (EiOSDevices = {}));
17
31
  export var EAndroidDevices;
18
32
  (function (EAndroidDevices) {