@devicecloud.dev/dcd 5.5.0 → 5.6.0-beta.4

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 (46) 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 +9 -4
  5. package/dist/commands/cloud.js +25 -8
  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 +5 -0
  13. package/dist/config/flags/execution.flags.js +5 -0
  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 +9 -4
  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 +20 -4
  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/execution-plan.service.d.ts +6 -0
  28. package/dist/services/execution-plan.service.js +98 -2
  29. package/dist/services/execution-plan.utils.js +69 -6
  30. package/dist/services/flow-paths.d.ts +9 -1
  31. package/dist/services/flow-paths.js +14 -2
  32. package/dist/services/notices.service.js +3 -0
  33. package/dist/services/report-download.service.js +9 -3
  34. package/dist/services/results-polling.service.d.ts +42 -1
  35. package/dist/services/results-polling.service.js +133 -7
  36. package/dist/services/test-submission.service.d.ts +18 -0
  37. package/dist/services/test-submission.service.js +30 -6
  38. package/dist/services/workspace-config.schema.d.ts +1 -0
  39. package/dist/services/workspace-config.schema.js +6 -0
  40. package/dist/types/domain/auth.types.d.ts +6 -0
  41. package/dist/types/domain/device.types.d.ts +18 -4
  42. package/dist/types/domain/device.types.js +17 -3
  43. package/dist/types/generated/schema.types.d.ts +127 -167
  44. package/dist/utils/auth.d.ts +33 -0
  45. package/dist/utils/auth.js +61 -2
  46. package/package.json +52 -12
@@ -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;
@@ -45,9 +53,29 @@ export declare function deviceFromResultRow(r: {
45
53
  config?: unknown;
46
54
  simulator_name?: string | null;
47
55
  }): TestDevice | undefined;
56
+ /**
57
+ * Was this row cancelled because a newer run from the same CI context
58
+ * replaced it (`dcd cloud --cancel-previous`)?
59
+ *
60
+ * Read through a structural cast for the same reason deviceFromResultRow
61
+ * does: the committed generated types are regenerated wholesale from dev's
62
+ * swagger and lag the API, and this must work against an API that already
63
+ * sends the field.
64
+ */
65
+ export declare function isSupersededRow(r: unknown): boolean;
66
+ /** The upload that superseded this run, for the console link. */
67
+ export declare function supersedingUploadId(results: unknown[]): string | undefined;
68
+ /**
69
+ * The superseding run's console link, derived from this run's.
70
+ *
71
+ * The `result` parameter deep-links a single test, and that id belongs to
72
+ * THIS upload — carrying it over would point at a result the newer upload
73
+ * does not contain. Swap the upload id and drop it.
74
+ */
75
+ export declare function supersedingConsoleUrl(consoleUrl: string, uploadId: string, supersededBy: string): string;
48
76
  export interface PollingResult {
49
77
  consoleUrl: string;
50
- status: 'FAILED' | 'PASSED';
78
+ status: 'FAILED' | 'PASSED' | 'SUPERSEDED';
51
79
  tests: Array<{
52
80
  /** Device this result ran on (present when the API reports it). */
53
81
  device?: TestDevice;
@@ -95,6 +123,19 @@ export declare class ResultsPollingService {
95
123
  * @returns Promise resolving to test results
96
124
  */
97
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;
98
139
  private filterLatestResults;
99
140
  /**
100
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
@@ -38,6 +50,45 @@ export function deviceFromResultRow(r) {
38
50
  googlePlay: sim ? /(_PLAY|-play)$/.test(sim) : undefined,
39
51
  };
40
52
  }
53
+ /**
54
+ * Was this row cancelled because a newer run from the same CI context
55
+ * replaced it (`dcd cloud --cancel-previous`)?
56
+ *
57
+ * Read through a structural cast for the same reason deviceFromResultRow
58
+ * does: the committed generated types are regenerated wholesale from dev's
59
+ * swagger and lag the API, and this must work against an API that already
60
+ * sends the field.
61
+ */
62
+ export function isSupersededRow(r) {
63
+ const reason = r
64
+ ?.cancellation_reason;
65
+ return typeof reason === 'string' && reason.startsWith('superseded_by:');
66
+ }
67
+ /** The upload that superseded this run, for the console link. */
68
+ export function supersedingUploadId(results) {
69
+ for (const r of results) {
70
+ const reason = r
71
+ ?.cancellation_reason;
72
+ if (typeof reason === 'string' && reason.startsWith('superseded_by:')) {
73
+ return reason.slice('superseded_by:'.length) || undefined;
74
+ }
75
+ }
76
+ return undefined;
77
+ }
78
+ /**
79
+ * The superseding run's console link, derived from this run's.
80
+ *
81
+ * The `result` parameter deep-links a single test, and that id belongs to
82
+ * THIS upload — carrying it over would point at a result the newer upload
83
+ * does not contain. Swap the upload id and drop it.
84
+ */
85
+ export function supersedingConsoleUrl(consoleUrl, uploadId, supersededBy) {
86
+ return consoleUrl
87
+ .replace(uploadId, supersededBy)
88
+ .replace(/&result=[^&]*/, '')
89
+ .replace(/\?result=[^&]*&/, '?')
90
+ .replace(/\?result=[^&]*$/, '');
91
+ }
41
92
  /**
42
93
  * Service for polling test results from the API
43
94
  */
@@ -76,7 +127,10 @@ export class ResultsPollingService {
76
127
  }
77
128
  }
78
129
  async pollLoop(options, testMetadata) {
79
- 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;
80
134
  this.initializePollingDisplay(json, logger);
81
135
  let sequentialPollFailures = 0;
82
136
  const pollIntervalMs = auth.mode === 'bearer'
@@ -174,6 +228,7 @@ export class ResultsPollingService {
174
228
  try {
175
229
  nextPollAt = null;
176
230
  renderStatus();
231
+ auth = await this.refreshPollingAuth(auth, uploadId, subscription, debug, logger);
177
232
  const updatedResults = await this.fetchAndLogResults(apiUrl, auth, uploadId, debug, logger);
178
233
  const { summary } = this.calculateStatusSummary(updatedResults);
179
234
  if (!json) {
@@ -199,8 +254,9 @@ export class ResultsPollingService {
199
254
  await waitForNextPoll(pollIntervalMs);
200
255
  }
201
256
  catch (error) {
202
- // Re-throw RunFailedError immediately (test failures, not polling errors)
203
- 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) {
204
260
  throw error;
205
261
  }
206
262
  sequentialPollFailures++;
@@ -226,13 +282,23 @@ export class ResultsPollingService {
226
282
  }
227
283
  buildPollingResult(results, uploadId, consoleUrl, testMetadata) {
228
284
  const resultsWithoutEarlierTries = this.filterLatestResults(results);
285
+ // ANY superseded row settles the whole verdict, even alongside a test
286
+ // that had genuinely failed before the newer run replaced this one: this
287
+ // run no longer speaks for the commit, so failing the build on its behalf
288
+ // is wrong. Actions reaches the same conclusion — a cancelled run's
289
+ // conclusion is `cancelled`, whatever had already failed inside it. The
290
+ // failure is still printed and still in `tests[]`; only the exit code
291
+ // changes.
292
+ const superseded = resultsWithoutEarlierTries.some(isSupersededRow);
229
293
  return {
230
294
  consoleUrl,
231
295
  // Anything other than an explicit pass (CANCELLED, ERROR, a status we
232
296
  // don't know about yet) must fail the run — this gates CI exit codes.
233
- status: resultsWithoutEarlierTries.every((result) => result.status === 'PASSED')
234
- ? 'PASSED'
235
- : 'FAILED',
297
+ status: superseded
298
+ ? 'SUPERSEDED'
299
+ : resultsWithoutEarlierTries.every((result) => result.status === 'PASSED')
300
+ ? 'PASSED'
301
+ : 'FAILED',
236
302
  tests: resultsWithoutEarlierTries.map((r) => ({
237
303
  // r carries config/simulator_name at runtime; the committed generated
238
304
  // types lag the API (regenerated wholesale from dev's swagger), so read
@@ -259,8 +325,12 @@ export class ResultsPollingService {
259
325
  const pending = statusCounts.PENDING || 0;
260
326
  const queued = statusCounts.QUEUED || 0;
261
327
  const running = statusCounts.RUNNING || 0;
328
+ // CANCELLED is terminal, so it counts as completed. Without it a
329
+ // cancelled or superseded run's footer sticks at "8/12 completed"
330
+ // forever, having already stopped polling.
331
+ const cancelled = statusCounts.CANCELLED || 0;
262
332
  const total = results.length;
263
- const completed = passed + failed;
333
+ const completed = passed + failed + cancelled;
264
334
  const summary = formatTestSummary({
265
335
  completed,
266
336
  failed,
@@ -346,6 +416,41 @@ export class ResultsPollingService {
346
416
  }
347
417
  return updatedResults;
348
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
+ }
349
454
  filterLatestResults(results) {
350
455
  // Resolve each row to the root of its retry chain (a retry's `retry_of`
351
456
  // may point at the previous retry rather than the original attempt), then
@@ -386,6 +491,27 @@ export class ResultsPollingService {
386
491
  }
387
492
  this.displayFinalResults(updatedResults, consoleUrl, json, logger);
388
493
  const output = this.buildPollingResult(updatedResults, uploadId, consoleUrl, testMetadata);
494
+ if (output.status === 'SUPERSEDED') {
495
+ // Exit 0: a newer run of the same CI context replaced this one, so
496
+ // failing the build here would fail it for work nobody is waiting on.
497
+ // Falls through to the success return below — RunFailedError, and with
498
+ // it the exit code 2 in `dcd cloud`, is never reached.
499
+ if (logger && !json) {
500
+ const newer = supersedingUploadId(updatedResults);
501
+ logger('\n');
502
+ logger(ui.warn('Run superseded by a newer run from the same CI context — exiting 0'));
503
+ if (newer) {
504
+ logger(ui.branch(ui.fields([
505
+ [
506
+ 'superseded by',
507
+ colors.url(supersedingConsoleUrl(consoleUrl, uploadId, newer)),
508
+ ],
509
+ ])));
510
+ }
511
+ logger('\n');
512
+ }
513
+ return output;
514
+ }
389
515
  if (output.status === 'FAILED') {
390
516
  if (debug && logger) {
391
517
  logger(`[DEBUG] Some tests failed, returning failed status`);
@@ -6,12 +6,30 @@ 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';
15
+ /**
16
+ * Ask the API to cancel the still-queued tests of the previous run from
17
+ * the same CI context. Sent as its own field rather than inside `config`,
18
+ * which is stamped onto every result row and shipped to the runner — this
19
+ * is a one-off submission directive, not run configuration.
20
+ */
21
+ cancelPrevious?: boolean;
9
22
  cliVersion: string;
10
23
  commonRoot: string;
11
24
  continueOnFailure?: boolean;
12
25
  debug?: boolean;
13
26
  deviceLocale?: string;
14
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
+ */
15
33
  disableAnimations?: boolean;
16
34
  /**
17
35
  * Encrypt the flow zip and env vars before upload (#1151/#1152), each with its
@@ -20,8 +20,8 @@ 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 = [], 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
- const { allExcludeTags, allIncludeTags, flowMetadata, flowOverrides, flowsToRun: testFileNames, referencedFiles, sequence, workspaceConfig, } = executionPlan;
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
+ 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);
27
27
  const metadataObject = this.parseKeyValuePairs(metadata);
@@ -42,6 +42,17 @@ export class TestSubmissionService {
42
42
  }
43
43
  }
44
44
  }
45
+ // Logged separately from referencedFiles: these come from config.yaml's
46
+ // `includedPaths` rather than from a flow command, and they can raise the
47
+ // common root (see computeCommonRoot) — which shows up here as every flow
48
+ // key gaining a leading directory segment.
49
+ if (includedFiles.length > 0) {
50
+ this.logDebug(debug, logger, `[DEBUG] Uploading ${includedFiles.length} file(s) from \`includedPaths\`:`);
51
+ for (const file of includedFiles) {
52
+ const normalizedPath = this.normalizeFilePath(file, commonRoot);
53
+ this.logDebug(debug, logger, `[DEBUG] - ${normalizedPath}`);
54
+ }
55
+ }
45
56
  this.logDebug(debug, logger, `[DEBUG] Compressing files from path: ${flowFile}`);
46
57
  const plaintextZip = await compressFilesFromRelativePath(flowFile?.endsWith('.yaml') || flowFile?.endsWith('.yml')
47
58
  ? path.dirname(flowFile)
@@ -50,6 +61,7 @@ export class TestSubmissionService {
50
61
  ...referencedFiles,
51
62
  ...testFileNames,
52
63
  ...sequentialFlows,
64
+ ...includedFiles,
53
65
  ]),
54
66
  ], commonRoot);
55
67
  this.logDebug(debug, logger, `[DEBUG] Compressed file size: ${plaintextZip.length} bytes`);
@@ -108,18 +120,25 @@ export class TestSubmissionService {
108
120
  fields.deviceMatrix = JSON.stringify(deviceMatrix);
109
121
  }
110
122
  // Platform used only to pick which workspace-config disableAnimations flag
111
- // applies. A device matrix is single-platform; its first cell decides. Fall
112
- // 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.
113
127
  const matrixPlatform = deviceMatrix && deviceMatrix.length > 0
114
128
  ? 'iOSDevice' in deviceMatrix[0]
115
129
  ? 'ios'
116
130
  : 'android'
117
131
  : undefined;
118
- const targetPlatform = matrixPlatform ?? (iOSDevice || iOSVersion ? 'ios' : 'android');
132
+ const targetPlatform = appPlatform ??
133
+ matrixPlatform ??
134
+ (iOSDevice || iOSVersion ? 'ios' : 'android');
119
135
  const configYamlDisableAnimations = targetPlatform === 'ios'
120
136
  ? Boolean(workspaceConfig?.platform?.ios?.disableAnimations)
121
137
  : Boolean(workspaceConfig?.platform?.android?.disableAnimations);
122
- 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;
123
142
  const configPayload = {
124
143
  allExcludeTags,
125
144
  allIncludeTags,
@@ -144,6 +163,11 @@ export class TestSubmissionService {
144
163
  fields.metadata = JSON.stringify(metadataPayload);
145
164
  this.logDebug(debug, logger, `[DEBUG] Sending metadata to API: ${JSON.stringify(metadataPayload)}`);
146
165
  }
166
+ // Only sent when asked for, so every other submission's wire shape is
167
+ // unchanged and the flag is simply ignored by an older API.
168
+ if (cancelPrevious) {
169
+ fields.cancelPrevious = 'true';
170
+ }
147
171
  this.setOptionalFields(fields, {
148
172
  androidApiLevel,
149
173
  androidDevice,
@@ -12,6 +12,7 @@ export declare const WorkspaceConfigSchema: z.ZodObject<{
12
12
  flowsOrder: z.ZodArray<z.ZodString>;
13
13
  }, z.core.$strip>>>;
14
14
  flows: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
15
+ includedPaths: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
15
16
  includeTags: z.ZodOptional<z.ZodNullable<z.ZodPreprocess<z.ZodArray<z.ZodString>, unknown>>>;
16
17
  local: z.ZodOptional<z.ZodNullable<z.ZodObject<{
17
18
  deterministicOrder: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
@@ -42,6 +42,7 @@ export const WorkspaceConfigSchema = z.looseObject({
42
42
  excludeTags: tagList.nullish(),
43
43
  executionOrder: ExecutionOrderSchema.nullish(),
44
44
  flows: z.array(z.string()).nullish(),
45
+ includedPaths: z.array(z.string()).nullish(),
45
46
  includeTags: tagList.nullish(),
46
47
  local: z
47
48
  .looseObject({ deterministicOrder: z.boolean().nullish() })
@@ -80,10 +81,15 @@ export const WORKSPACE_CONFIG_KEYS = new Set(Object.keys(WorkspaceConfigSchema.s
80
81
  * Near-misses that aren't just a casing slip on a real key. Keyed lowercase.
81
82
  */
82
83
  const KEY_ALIASES = {
84
+ assets: 'includedPaths',
83
85
  continueonfailure: 'executionOrder.continueOnFailure',
84
86
  excludetag: 'excludeTags',
87
+ files: 'includedPaths',
85
88
  floworder: 'executionOrder.flowsOrder',
86
89
  flowsorder: 'executionOrder.flowsOrder',
90
+ includedpath: 'includedPaths',
91
+ includefiles: 'includedPaths',
92
+ includepaths: 'includedPaths',
87
93
  includetag: 'includeTags',
88
94
  tags: 'includeTags / excludeTags',
89
95
  };
@@ -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) {