@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.
- package/README.md +4 -2
- package/dist/commands/artifacts.d.ts +1 -1
- package/dist/commands/artifacts.js +1 -1
- package/dist/commands/cloud.d.ts +9 -4
- package/dist/commands/cloud.js +25 -8
- package/dist/commands/switch-org.js +5 -1
- package/dist/commands/upload.js +5 -1
- package/dist/commands/whoami.d.ts +7 -1
- package/dist/commands/whoami.js +26 -2
- package/dist/config/flags/device.flags.d.ts +3 -3
- package/dist/config/flags/device.flags.js +5 -3
- package/dist/config/flags/execution.flags.d.ts +5 -0
- package/dist/config/flags/execution.flags.js +5 -0
- package/dist/config/flags/output.flags.d.ts +1 -1
- package/dist/config/flags/output.flags.js +1 -1
- package/dist/constants.d.ts +9 -4
- package/dist/gateways/api-gateway.js +2 -1
- package/dist/gateways/realtime-gateway.d.ts +7 -0
- package/dist/gateways/realtime-gateway.js +12 -1
- package/dist/mcp/context.d.ts +14 -3
- package/dist/mcp/context.js +15 -3
- package/dist/mcp/tools/download-artifacts.js +9 -6
- package/dist/mcp/tools/list-devices.js +1 -1
- package/dist/mcp/tools/run-cloud-test.js +20 -4
- package/dist/services/device-validation.service.d.ts +17 -2
- package/dist/services/device-validation.service.js +93 -12
- package/dist/services/execution-plan.service.d.ts +6 -0
- package/dist/services/execution-plan.service.js +98 -2
- package/dist/services/execution-plan.utils.js +69 -6
- package/dist/services/flow-paths.d.ts +9 -1
- package/dist/services/flow-paths.js +14 -2
- package/dist/services/notices.service.js +3 -0
- package/dist/services/report-download.service.js +9 -3
- package/dist/services/results-polling.service.d.ts +42 -1
- package/dist/services/results-polling.service.js +133 -7
- package/dist/services/test-submission.service.d.ts +18 -0
- package/dist/services/test-submission.service.js +30 -6
- package/dist/services/workspace-config.schema.d.ts +1 -0
- package/dist/services/workspace-config.schema.js +6 -0
- package/dist/types/domain/auth.types.d.ts +6 -0
- package/dist/types/domain/device.types.d.ts +18 -4
- package/dist/types/domain/device.types.js +17 -3
- package/dist/types/generated/schema.types.d.ts +127 -167
- package/dist/utils/auth.d.ts +33 -0
- package/dist/utils/auth.js +61 -2
- package/package.json +52 -12
package/README.md
CHANGED
|
@@ -45,13 +45,15 @@ Add it to your MCP client config:
|
|
|
45
45
|
"mcpServers": {
|
|
46
46
|
"devicecloud": {
|
|
47
47
|
"command": "npx",
|
|
48
|
-
"args": ["-y", "
|
|
48
|
+
"args": ["-y", "--package=@devicecloud.dev/dcd", "dcd-mcp"],
|
|
49
49
|
"env": { "DEVICE_CLOUD_API_KEY": "<your-api-key>" }
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
`--package` matters: the package's default bin is `dcd`, so `npx @devicecloud.dev/dcd dcd-mcp` would run `dcd dcd-mcp` (an unknown command) instead of the server.
|
|
56
|
+
|
|
55
57
|
Auth is inherited from the CLI: set `DEVICE_CLOUD_API_KEY` as above, or run `dcd login` once and the server picks up the stored session. Point it at a non-prod environment with `DCD_API_URL`.
|
|
56
58
|
|
|
57
59
|
**Tools**
|
|
@@ -64,7 +66,7 @@ Auth is inherited from the CLI: set `DEVICE_CLOUD_API_KEY` as above, or run `dcd
|
|
|
64
66
|
| `dcd_download_artifacts` | Download a run's artifacts/report to disk |
|
|
65
67
|
| `dcd_run_cloud_test` | Submit a flow to run on the cloud (**billable**) |
|
|
66
68
|
|
|
67
|
-
**Read-only mode.** `dcd_run_cloud_test` consumes test minutes, so it is annotated as non-read-only/destructive (clients can prompt before calling it). To hide it entirely — recommended for autonomous or untrusted agents — pass `--read-only`
|
|
69
|
+
**Read-only mode.** `dcd_run_cloud_test` consumes test minutes, so it is annotated as non-read-only/destructive (clients can prompt before calling it). To hide it entirely — recommended for autonomous or untrusted agents — pass `--read-only` at the end of `args` (`["-y", "--package=@devicecloud.dev/dcd", "dcd-mcp", "--read-only"]`), or set `DCD_MCP_READONLY=1` in `env`.
|
|
68
70
|
|
|
69
71
|
By default `dcd_run_cloud_test` is async: it returns an `uploadId` immediately, which you poll with `dcd_get_status`. Pass `wait: true` (bounded by `waitTimeoutSeconds`) to block until completion, or `dryRun: true` to preview the flows without submitting.
|
|
70
72
|
|
|
@@ -27,7 +27,7 @@ export declare const artifactsCommand: import("citty").CommandDef<{
|
|
|
27
27
|
};
|
|
28
28
|
readonly 'html-path': {
|
|
29
29
|
readonly type: "string";
|
|
30
|
-
readonly description: "Custom file path for downloaded HTML report (default: ./report.
|
|
30
|
+
readonly description: "Custom file path for the downloaded HTML report, a ZIP of report.html and its assets (default: ./report.zip)";
|
|
31
31
|
};
|
|
32
32
|
readonly 'junit-path': {
|
|
33
33
|
readonly type: "string";
|
|
@@ -41,7 +41,7 @@ export const artifactsCommand = defineCommand({
|
|
|
41
41
|
},
|
|
42
42
|
'html-path': {
|
|
43
43
|
type: 'string',
|
|
44
|
-
description: 'Custom file path for downloaded HTML report (default: ./report.
|
|
44
|
+
description: 'Custom file path for the downloaded HTML report, a ZIP of report.html and its assets (default: ./report.zip)',
|
|
45
45
|
},
|
|
46
46
|
'junit-path': {
|
|
47
47
|
type: 'string',
|
package/dist/commands/cloud.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export declare const cloudCommand: import("citty").CommandDef<{
|
|
|
35
35
|
};
|
|
36
36
|
readonly 'html-path': {
|
|
37
37
|
readonly type: "string";
|
|
38
|
-
readonly description: "Custom file path for downloaded HTML report (requires --report html, default: ./report.
|
|
38
|
+
readonly description: "Custom file path for the downloaded HTML report, a ZIP of report.html and its assets (requires --report html or html-detailed, default: ./report.zip)";
|
|
39
39
|
};
|
|
40
40
|
readonly async: {
|
|
41
41
|
readonly type: "boolean";
|
|
@@ -98,6 +98,11 @@ export declare const cloudCommand: import("citty").CommandDef<{
|
|
|
98
98
|
readonly type: "string";
|
|
99
99
|
readonly description: "Pull request URL for this run (stored as gh_pr_url metadata)";
|
|
100
100
|
};
|
|
101
|
+
readonly 'cancel-previous': {
|
|
102
|
+
readonly type: "boolean";
|
|
103
|
+
readonly default: false;
|
|
104
|
+
readonly description: "Cancel the still-queued tests of the previous run from the same CI context: the same repo and branch or PR (and check name, if one is set), read from the run metadata. Needs repo plus branch/PR metadata (--repo-name with --branch or --pr-number, or --metadata gh_repo=… with gh_branch=… or gh_pr_number=…); without it nothing is superseded. Tests already running are left to finish; cancelled tests are refunded at 75%.";
|
|
105
|
+
};
|
|
101
106
|
readonly config: {
|
|
102
107
|
readonly type: "string";
|
|
103
108
|
readonly description: "Path to custom config.yaml file. If not provided, defaults to config.yaml in root flows folders.";
|
|
@@ -206,12 +211,12 @@ export declare const cloudCommand: import("citty").CommandDef<{
|
|
|
206
211
|
readonly 'android-no-snapshot': {
|
|
207
212
|
readonly type: "boolean";
|
|
208
213
|
readonly default: false;
|
|
209
|
-
readonly description: "[Android only] Force cold boot instead of using snapshot boot. This is automatically enabled for API
|
|
214
|
+
readonly description: "[Android only] Force cold boot instead of using snapshot boot. This is automatically enabled for API 34+ but can be used to force cold boot on older API levels.";
|
|
210
215
|
};
|
|
211
216
|
readonly 'disable-animations': {
|
|
212
217
|
readonly type: "boolean";
|
|
213
|
-
readonly
|
|
214
|
-
readonly
|
|
218
|
+
readonly description: "Disable device animations during test execution. On Android, disables system animation scales. On iOS, enables Reduce Motion. Reduces CPU load and may improve test reliability. Overrides platform.ios/android.disableAnimations in config.yaml.";
|
|
219
|
+
readonly negativeDescription: "Keep device animations on, even where config.yaml sets platform.ios/android.disableAnimations";
|
|
215
220
|
};
|
|
216
221
|
readonly 'app-binary-id': {
|
|
217
222
|
readonly type: "string";
|
package/dist/commands/cloud.js
CHANGED
|
@@ -14,7 +14,7 @@ import { deviceFromResultRow, ResultsPollingService, RunFailedError, } from '../
|
|
|
14
14
|
import { telemetry } from '../services/telemetry.service.js';
|
|
15
15
|
import { TestSubmissionService } from '../services/test-submission.service.js';
|
|
16
16
|
import { VersionService } from '../services/version.service.js';
|
|
17
|
-
import {
|
|
17
|
+
import { isIosMatrixConfig } from '../types/domain/device.types.js';
|
|
18
18
|
import { resolveAuth } from '../utils/auth.js';
|
|
19
19
|
import { assertMatrixSupported, matrixIsIos, parseDeviceMatrix, } from '../utils/device-matrix.js';
|
|
20
20
|
import { isEncryptionEnabled } from '../utils/envelope.js';
|
|
@@ -132,6 +132,7 @@ export const cloudCommand = defineCommand({
|
|
|
132
132
|
const excludeFlows = coerceArray(collectRepeatedFlag(rawArgs, ['--exclude-flows']));
|
|
133
133
|
const excludeTags = coerceArray(collectRepeatedFlag(rawArgs, ['--exclude-tags']));
|
|
134
134
|
let flows = args.flows;
|
|
135
|
+
const cancelPrevious = Boolean(args['cancel-previous']);
|
|
135
136
|
const googlePlay = Boolean(args['google-play']);
|
|
136
137
|
const ignoreShaCheck = Boolean(args['ignore-sha-check']);
|
|
137
138
|
// Single opt-in for client-side envelope encryption of every sensitive
|
|
@@ -139,10 +140,14 @@ export const cloudCommand = defineCommand({
|
|
|
139
140
|
// (#1152). Flag wins; otherwise DCD_ENCRYPT / DCD_ENCRYPT_BINARIES.
|
|
140
141
|
const encrypt = isEncryptionEnabled(args['encrypt'] ? true : undefined);
|
|
141
142
|
const includeTags = coerceArray(collectRepeatedFlag(rawArgs, ['--include-tags']));
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
// Devices and OS versions are checked against the API's live
|
|
144
|
+
// compatibility data further down (DeviceValidationService), not the
|
|
145
|
+
// CLI's enums, which only feed --help: a device the API adds must not
|
|
146
|
+
// need a CLI release, and one it drops must fail with the API's list.
|
|
147
|
+
const iOSDevice = args['ios-device'] || undefined;
|
|
148
|
+
const iOSVersion = args['ios-version'] || undefined;
|
|
149
|
+
const androidApiLevel = args['android-api-level'] || undefined;
|
|
150
|
+
const androidDevice = args['android-device'] || undefined;
|
|
146
151
|
const androidNoSnapshot = Boolean(args['android-no-snapshot']);
|
|
147
152
|
// Repeatable device-matrix flags: one validated cell each, no cross-product.
|
|
148
153
|
const iosMatrixFlags = collectRepeatedFlag(rawArgs, ['--ios-device-matrix']);
|
|
@@ -163,7 +168,9 @@ export const cloudCommand = defineCommand({
|
|
|
163
168
|
const renderEngine = validateEnum(args['render-engine'], RENDER_ENGINE_OPTIONS, 'render-engine');
|
|
164
169
|
const showCrosshairs = Boolean(args['show-crosshairs']);
|
|
165
170
|
const maestroChromeOnboarding = Boolean(args['maestro-chrome-onboarding']);
|
|
166
|
-
|
|
171
|
+
// Left undefined when not passed, so config.yaml's per-platform value
|
|
172
|
+
// applies; --no-disable-animations is an explicit false that beats it.
|
|
173
|
+
const disableAnimations = args['disable-animations'];
|
|
167
174
|
const ghBranch = args.branch;
|
|
168
175
|
const ghCommitSha = args['commit-sha'];
|
|
169
176
|
const ghRepoName = args['repo-name'];
|
|
@@ -401,15 +408,22 @@ export const cloudCommand = defineCommand({
|
|
|
401
408
|
}
|
|
402
409
|
throw error;
|
|
403
410
|
}
|
|
404
|
-
const { allExcludeTags, allIncludeTags, flowMetadata, flowOverrides, flowsToRun: testFileNames, referencedFiles, sequence, } = executionPlan;
|
|
411
|
+
const { allExcludeTags, allIncludeTags, flowMetadata, flowOverrides, flowsToRun: testFileNames, includedFiles, referencedFiles, sequence, } = executionPlan;
|
|
405
412
|
if (debug) {
|
|
406
413
|
out(`[DEBUG] All include tags: ${allIncludeTags?.join(', ') || 'none'}`);
|
|
407
414
|
out(`[DEBUG] All exclude tags: ${allExcludeTags?.join(', ') || 'none'}`);
|
|
408
415
|
out(`[DEBUG] Test file names: ${testFileNames.join(', ')}`);
|
|
409
416
|
}
|
|
410
|
-
const commonRoot = computeCommonRoot(testFileNames, referencedFiles);
|
|
417
|
+
const commonRoot = computeCommonRoot(testFileNames, referencedFiles, includedFiles);
|
|
411
418
|
if (debug) {
|
|
412
419
|
out(`[DEBUG] Common root directory: ${commonRoot}`);
|
|
420
|
+
// `includedPaths` files sitting beside the flows tree rather than
|
|
421
|
+
// inside it raise the common root, so every server-side flow key gains
|
|
422
|
+
// a leading segment. Harmless but visible in the console, so say it.
|
|
423
|
+
const rootWithoutIncludes = computeCommonRoot(testFileNames, referencedFiles);
|
|
424
|
+
if (includedFiles.length > 0 && rootWithoutIncludes !== commonRoot) {
|
|
425
|
+
out(`[DEBUG] \`includedPaths\` raised the common root from ${rootWithoutIncludes} to ${commonRoot} — flow paths gain a leading segment`);
|
|
426
|
+
}
|
|
413
427
|
}
|
|
414
428
|
const testMetadataMap = buildTestMetadataMap(flowMetadata, commonRoot);
|
|
415
429
|
if (debug) {
|
|
@@ -549,6 +563,9 @@ export const cloudCommand = defineCommand({
|
|
|
549
563
|
androidNoSnapshot,
|
|
550
564
|
apiUrl,
|
|
551
565
|
appBinaryId: finalBinaryId,
|
|
566
|
+
// With --app-binary-id the binary's platform is unknown here.
|
|
567
|
+
appPlatform: appBinaryId ? undefined : platformFromAppFile(finalAppFile),
|
|
568
|
+
cancelPrevious,
|
|
552
569
|
cliVersion,
|
|
553
570
|
commonRoot,
|
|
554
571
|
continueOnFailure,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Without an argument: fetch orgs and prompt the user to pick one.
|
|
7
7
|
*/
|
|
8
8
|
import { defineCommand } from 'citty';
|
|
9
|
-
import { resolveAuth } from '../utils/auth.js';
|
|
9
|
+
import { apiKeyOverride, resolveAuth } from '../utils/auth.js';
|
|
10
10
|
import { CliError, logger } from '../utils/cli.js';
|
|
11
11
|
import { readConfig, resolveApiUrl, writeConfig } from '../utils/config-store.js';
|
|
12
12
|
import { fetchOrgs, pickOrg } from '../utils/orgs.js';
|
|
@@ -54,6 +54,10 @@ export const switchOrgCommand = defineCommand({
|
|
|
54
54
|
current_org_name: chosen.name,
|
|
55
55
|
});
|
|
56
56
|
logger.log(ui.success(`Switched to ${colors.highlight(chosen.name)}`));
|
|
57
|
+
// The switch only changes the stored session; an exported key still wins.
|
|
58
|
+
if (apiKeyOverride() === 'DEVICE_CLOUD_API_KEY') {
|
|
59
|
+
logger.log(ui.warn(`${colors.highlight('DEVICE_CLOUD_API_KEY')} is set, so other dcd commands still authenticate with that API key and its org, not ${chosen.name}. Unset it to use the session.`));
|
|
60
|
+
}
|
|
57
61
|
},
|
|
58
62
|
});
|
|
59
63
|
/**
|
package/dist/commands/upload.js
CHANGED
|
@@ -7,6 +7,7 @@ import { uploadBinary, verifyAppZip } from '../methods.js';
|
|
|
7
7
|
import { resolveAuth } from '../utils/auth.js';
|
|
8
8
|
import { CliError, logger } from '../utils/cli.js';
|
|
9
9
|
import { resolveApiUrl } from '../utils/config-store.js';
|
|
10
|
+
import { isEncryptionEnabled } from '../utils/envelope.js';
|
|
10
11
|
import { downloadExpoUrl, extractTarGz, findAppBundle, isUrl } from '../utils/expo.js';
|
|
11
12
|
import { colors, formatId } from '../utils/styling.js';
|
|
12
13
|
import { ui } from '../utils/ui.js';
|
|
@@ -41,7 +42,10 @@ export const uploadCommand = defineCommand({
|
|
|
41
42
|
const apiUrl = resolveApiUrl(args['api-url']);
|
|
42
43
|
const appUrl = args['app-url'];
|
|
43
44
|
const ignoreShaCheck = Boolean(args['ignore-sha-check']);
|
|
44
|
-
|
|
45
|
+
// Same rule as `dcd cloud`: the flag turns encryption on, and without it
|
|
46
|
+
// DCD_ENCRYPT decides. Passing `false` here used to mean "explicitly
|
|
47
|
+
// off", which silently beat DCD_ENCRYPT=1 for every `dcd upload`.
|
|
48
|
+
const encryptBinary = isEncryptionEnabled(args['encrypt'] ? true : undefined);
|
|
45
49
|
const debug = Boolean(args.debug);
|
|
46
50
|
const positional = args.appFile;
|
|
47
51
|
const auth = await resolveAuth({ apiKeyFlag });
|
|
@@ -1,2 +1,8 @@
|
|
|
1
|
-
export declare const whoamiCommand: import("citty").CommandDef<
|
|
1
|
+
export declare const whoamiCommand: import("citty").CommandDef<{
|
|
2
|
+
readonly 'api-key': {
|
|
3
|
+
readonly description: "An API key as you would pass it to other commands; whoami says that it takes precedence over the stored session";
|
|
4
|
+
readonly type: "string";
|
|
5
|
+
readonly alias: ["apiKey"];
|
|
6
|
+
};
|
|
7
|
+
}>;
|
|
2
8
|
export default whoamiCommand;
|
package/dist/commands/whoami.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `dcd whoami` — prints the logged-in user + active org from stored config.
|
|
3
|
+
*
|
|
4
|
+
* It reads only the stored `dcd login` session, while every other command
|
|
5
|
+
* prefers an API key from --api-key or DEVICE_CLOUD_API_KEY — so when one is
|
|
6
|
+
* present it says so, rather than implying the session is what they use.
|
|
3
7
|
*/
|
|
4
8
|
import { defineCommand } from 'citty';
|
|
9
|
+
import { apiFlags } from '../config/flags/api.flags.js';
|
|
10
|
+
import { apiKeyOverride } from '../utils/auth.js';
|
|
5
11
|
import { logger } from '../utils/cli.js';
|
|
6
12
|
import { readConfig } from '../utils/config-store.js';
|
|
7
13
|
import { colors, formatId } from '../utils/styling.js';
|
|
@@ -11,10 +17,22 @@ export const whoamiCommand = defineCommand({
|
|
|
11
17
|
name: 'whoami',
|
|
12
18
|
description: 'Show the logged-in user and active organization',
|
|
13
19
|
},
|
|
14
|
-
|
|
20
|
+
args: {
|
|
21
|
+
'api-key': {
|
|
22
|
+
...apiFlags['api-key'],
|
|
23
|
+
description: 'An API key as you would pass it to other commands; whoami says that it takes precedence over the stored session',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
run({ args }) {
|
|
15
27
|
const config = readConfig();
|
|
28
|
+
const keySource = apiKeyOverride(args['api-key']);
|
|
29
|
+
const keyName = colors.highlight(keySource ?? '');
|
|
16
30
|
if (!config?.session) {
|
|
17
|
-
logger.log(ui.info(
|
|
31
|
+
logger.log(ui.info(keySource === 'DEVICE_CLOUD_API_KEY'
|
|
32
|
+
? `Not logged in. ${keyName} is set, so dcd commands authenticate with that API key.`
|
|
33
|
+
: keySource === '--api-key'
|
|
34
|
+
? `Not logged in. Commands given ${keyName} authenticate with that API key.`
|
|
35
|
+
: `Not logged in. Run ${colors.highlight('dcd login')} or set ${colors.highlight('DEVICE_CLOUD_API_KEY')}.`));
|
|
18
36
|
return;
|
|
19
37
|
}
|
|
20
38
|
// Fallback for sessions stored before we persisted org name alongside id.
|
|
@@ -26,6 +44,12 @@ export const whoamiCommand = defineCommand({
|
|
|
26
44
|
fields.push(['env', config.env]);
|
|
27
45
|
logger.log(ui.section('devicecloud.dev'));
|
|
28
46
|
logger.log(ui.branch(ui.fields(fields)));
|
|
47
|
+
if (keySource === 'DEVICE_CLOUD_API_KEY') {
|
|
48
|
+
logger.log(ui.warn(`${keyName} is set, so other dcd commands authenticate with that API key and its org, not this session. Unset it to use the session.`));
|
|
49
|
+
}
|
|
50
|
+
else if (keySource === '--api-key') {
|
|
51
|
+
logger.log(ui.warn(`Commands given ${keyName} authenticate with that API key and its org, not this session.`));
|
|
52
|
+
}
|
|
29
53
|
},
|
|
30
54
|
});
|
|
31
55
|
export default whoamiCommand;
|
|
@@ -56,11 +56,11 @@ export declare const deviceFlags: {
|
|
|
56
56
|
readonly 'android-no-snapshot': {
|
|
57
57
|
readonly type: "boolean";
|
|
58
58
|
readonly default: false;
|
|
59
|
-
readonly description: "[Android only] Force cold boot instead of using snapshot boot. This is automatically enabled for API
|
|
59
|
+
readonly description: "[Android only] Force cold boot instead of using snapshot boot. This is automatically enabled for API 34+ but can be used to force cold boot on older API levels.";
|
|
60
60
|
};
|
|
61
61
|
readonly 'disable-animations': {
|
|
62
62
|
readonly type: "boolean";
|
|
63
|
-
readonly
|
|
64
|
-
readonly
|
|
63
|
+
readonly description: "Disable device animations during test execution. On Android, disables system animation scales. On iOS, enables Reduce Motion. Reduces CPU load and may improve test reliability. Overrides platform.ios/android.disableAnimations in config.yaml.";
|
|
64
|
+
readonly negativeDescription: "Keep device animations on, even where config.yaml sets platform.ios/android.disableAnimations";
|
|
65
65
|
};
|
|
66
66
|
};
|
|
@@ -61,11 +61,13 @@ export const deviceFlags = {
|
|
|
61
61
|
'android-no-snapshot': {
|
|
62
62
|
type: 'boolean',
|
|
63
63
|
default: false,
|
|
64
|
-
description: '[Android only] Force cold boot instead of using snapshot boot. This is automatically enabled for API
|
|
64
|
+
description: '[Android only] Force cold boot instead of using snapshot boot. This is automatically enabled for API 34+ but can be used to force cold boot on older API levels.',
|
|
65
65
|
},
|
|
66
|
+
// No default: an unset flag must be distinguishable from --no-disable-animations
|
|
67
|
+
// so the platform's disableAnimations in config.yaml applies only when unset.
|
|
66
68
|
'disable-animations': {
|
|
67
69
|
type: 'boolean',
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
description: 'Disable device animations during test execution. On Android, disables system animation scales. On iOS, enables Reduce Motion. Reduces CPU load and may improve test reliability. Overrides platform.ios/android.disableAnimations in config.yaml.',
|
|
71
|
+
negativeDescription: 'Keep device animations on, even where config.yaml sets platform.ios/android.disableAnimations',
|
|
70
72
|
},
|
|
71
73
|
};
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
* Test execution and flow management flags
|
|
3
3
|
*/
|
|
4
4
|
export declare const executionFlags: {
|
|
5
|
+
readonly 'cancel-previous': {
|
|
6
|
+
readonly type: "boolean";
|
|
7
|
+
readonly default: false;
|
|
8
|
+
readonly description: "Cancel the still-queued tests of the previous run from the same CI context: the same repo and branch or PR (and check name, if one is set), read from the run metadata. Needs repo plus branch/PR metadata (--repo-name with --branch or --pr-number, or --metadata gh_repo=… with gh_branch=… or gh_pr_number=…); without it nothing is superseded. Tests already running are left to finish; cancelled tests are refunded at 75%.";
|
|
9
|
+
};
|
|
5
10
|
readonly config: {
|
|
6
11
|
readonly type: "string";
|
|
7
12
|
readonly description: "Path to custom config.yaml file. If not provided, defaults to config.yaml in root flows folders.";
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
* Test execution and flow management flags
|
|
3
3
|
*/
|
|
4
4
|
export const executionFlags = {
|
|
5
|
+
'cancel-previous': {
|
|
6
|
+
type: 'boolean',
|
|
7
|
+
default: false,
|
|
8
|
+
description: 'Cancel the still-queued tests of the previous run from the same CI context: the same repo and branch or PR (and check name, if one is set), read from the run metadata. Needs repo plus branch/PR metadata (--repo-name with --branch or --pr-number, or --metadata gh_repo=… with gh_branch=… or gh_pr_number=…); without it nothing is superseded. Tests already running are left to finish; cancelled tests are refunded at 75%.',
|
|
9
|
+
},
|
|
5
10
|
config: {
|
|
6
11
|
type: 'string',
|
|
7
12
|
description: 'Path to custom config.yaml file. If not provided, defaults to config.yaml in root flows folders.',
|
|
@@ -16,7 +16,7 @@ export declare const outputFlags: {
|
|
|
16
16
|
};
|
|
17
17
|
readonly 'html-path': {
|
|
18
18
|
readonly type: "string";
|
|
19
|
-
readonly description: "Custom file path for downloaded HTML report (requires --report html, default: ./report.
|
|
19
|
+
readonly description: "Custom file path for the downloaded HTML report, a ZIP of report.html and its assets (requires --report html or html-detailed, default: ./report.zip)";
|
|
20
20
|
};
|
|
21
21
|
readonly async: {
|
|
22
22
|
readonly type: "boolean";
|
|
@@ -16,7 +16,7 @@ export const outputFlags = {
|
|
|
16
16
|
},
|
|
17
17
|
'html-path': {
|
|
18
18
|
type: 'string',
|
|
19
|
-
description: 'Custom file path for downloaded HTML report (requires --report html, default: ./report.
|
|
19
|
+
description: 'Custom file path for the downloaded HTML report, a ZIP of report.html and its assets (requires --report html or html-detailed, default: ./report.zip)',
|
|
20
20
|
},
|
|
21
21
|
async: {
|
|
22
22
|
type: 'boolean',
|
package/dist/constants.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export declare const flags: {
|
|
|
21
21
|
};
|
|
22
22
|
readonly 'html-path': {
|
|
23
23
|
readonly type: "string";
|
|
24
|
-
readonly description: "Custom file path for downloaded HTML report (requires --report html, default: ./report.
|
|
24
|
+
readonly description: "Custom file path for the downloaded HTML report, a ZIP of report.html and its assets (requires --report html or html-detailed, default: ./report.zip)";
|
|
25
25
|
};
|
|
26
26
|
readonly async: {
|
|
27
27
|
readonly type: "boolean";
|
|
@@ -84,6 +84,11 @@ export declare const flags: {
|
|
|
84
84
|
readonly type: "string";
|
|
85
85
|
readonly description: "Pull request URL for this run (stored as gh_pr_url metadata)";
|
|
86
86
|
};
|
|
87
|
+
readonly 'cancel-previous': {
|
|
88
|
+
readonly type: "boolean";
|
|
89
|
+
readonly default: false;
|
|
90
|
+
readonly description: "Cancel the still-queued tests of the previous run from the same CI context: the same repo and branch or PR (and check name, if one is set), read from the run metadata. Needs repo plus branch/PR metadata (--repo-name with --branch or --pr-number, or --metadata gh_repo=… with gh_branch=… or gh_pr_number=…); without it nothing is superseded. Tests already running are left to finish; cancelled tests are refunded at 75%.";
|
|
91
|
+
};
|
|
87
92
|
readonly config: {
|
|
88
93
|
readonly type: "string";
|
|
89
94
|
readonly description: "Path to custom config.yaml file. If not provided, defaults to config.yaml in root flows folders.";
|
|
@@ -192,12 +197,12 @@ export declare const flags: {
|
|
|
192
197
|
readonly 'android-no-snapshot': {
|
|
193
198
|
readonly type: "boolean";
|
|
194
199
|
readonly default: false;
|
|
195
|
-
readonly description: "[Android only] Force cold boot instead of using snapshot boot. This is automatically enabled for API
|
|
200
|
+
readonly description: "[Android only] Force cold boot instead of using snapshot boot. This is automatically enabled for API 34+ but can be used to force cold boot on older API levels.";
|
|
196
201
|
};
|
|
197
202
|
readonly 'disable-animations': {
|
|
198
203
|
readonly type: "boolean";
|
|
199
|
-
readonly
|
|
200
|
-
readonly
|
|
204
|
+
readonly description: "Disable device animations during test execution. On Android, disables system animation scales. On iOS, enables Reduce Motion. Reduces CPU load and may improve test reliability. Overrides platform.ios/android.disableAnimations in config.yaml.";
|
|
205
|
+
readonly negativeDescription: "Keep device animations on, even where config.yaml sets platform.ios/android.disableAnimations";
|
|
201
206
|
};
|
|
202
207
|
readonly 'app-binary-id': {
|
|
203
208
|
readonly type: "string";
|
|
@@ -566,7 +566,8 @@ export const ApiGateway = {
|
|
|
566
566
|
},
|
|
567
567
|
html: {
|
|
568
568
|
endpoint: `/results/${uploadId}/html-report`,
|
|
569
|
-
|
|
569
|
+
// A ZIP of report.html plus its assets, not a bare HTML file.
|
|
570
|
+
defaultFilename: `report-${uploadId}.zip`,
|
|
570
571
|
notFoundMessage: `Upload ID '${uploadId}' not found or no HTML report available for this upload`,
|
|
571
572
|
errorPrefix: 'Failed to download HTML report',
|
|
572
573
|
},
|
|
@@ -8,6 +8,13 @@ export interface RealtimeResultsSubscription {
|
|
|
8
8
|
isConnected(): boolean;
|
|
9
9
|
/** Tear down the channel and close the socket. Best-effort, never throws. */
|
|
10
10
|
unsubscribe(): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Hand the socket a refreshed JWT. RLS authorises the channel with the token
|
|
13
|
+
* it joined with, and the server drops the channel once that expires — so a
|
|
14
|
+
* poll that outlives the token passes the new one on. Best-effort, never
|
|
15
|
+
* throws.
|
|
16
|
+
*/
|
|
17
|
+
updateAccessToken(accessToken: string): void;
|
|
11
18
|
}
|
|
12
19
|
export interface RealtimeSubscribeOptions {
|
|
13
20
|
/** Supabase JWT (AuthContext.accessToken) — authorises the socket for RLS. */
|
|
@@ -86,6 +86,13 @@ export class RealtimeResultsGateway {
|
|
|
86
86
|
/* best effort — process is exiting anyway */
|
|
87
87
|
}
|
|
88
88
|
},
|
|
89
|
+
updateAccessToken(nextToken) {
|
|
90
|
+
// setAuth updates the join payload and pushes the token to joined
|
|
91
|
+
// channels. A failure only costs realtime; the backstop poll stays.
|
|
92
|
+
activeClient.realtime.setAuth(nextToken).catch((error) => {
|
|
93
|
+
dbg(`failed to update the socket's token: ${error instanceof Error ? error.message : String(error)}`);
|
|
94
|
+
});
|
|
95
|
+
},
|
|
89
96
|
};
|
|
90
97
|
}
|
|
91
98
|
catch (error) {
|
|
@@ -97,7 +104,11 @@ export class RealtimeResultsGateway {
|
|
|
97
104
|
catch {
|
|
98
105
|
/* ignore */
|
|
99
106
|
}
|
|
100
|
-
return {
|
|
107
|
+
return {
|
|
108
|
+
isConnected: () => false,
|
|
109
|
+
async unsubscribe() { },
|
|
110
|
+
updateAccessToken() { },
|
|
111
|
+
};
|
|
101
112
|
}
|
|
102
113
|
}
|
|
103
114
|
}
|
package/dist/mcp/context.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Per-process MCP context:
|
|
3
|
-
* stderr logger every tool
|
|
2
|
+
* Per-process MCP context: the resolved AuthContext + API URL (re-resolved
|
|
3
|
+
* when a `dcd login` session nears expiry), plus the stderr logger every tool
|
|
4
|
+
* must use.
|
|
4
5
|
*
|
|
5
6
|
* stdio transport reserves **stdout** for the JSON-RPC frame stream — anything
|
|
6
7
|
* a tool prints there corrupts the protocol. Tools therefore call the services
|
|
@@ -15,17 +16,27 @@ export interface McpContext {
|
|
|
15
16
|
auth: AuthContext;
|
|
16
17
|
}
|
|
17
18
|
/**
|
|
18
|
-
* Resolve auth + API URL
|
|
19
|
+
* Resolve auth + API URL lazily on first tool invocation, and again whenever a
|
|
20
|
+
* `dcd login` session is near expiry.
|
|
19
21
|
*
|
|
20
22
|
* Lazy so `initialize` / `tools/list` succeed before the user has supplied a
|
|
21
23
|
* credential (clients enumerate tools on connect), and so an auth failure
|
|
22
24
|
* surfaces as a tool error rather than crashing the server at boot.
|
|
23
25
|
*
|
|
26
|
+
* Re-resolved near expiry because a session's access token lasts about an
|
|
27
|
+
* hour while an MCP server can run for days: resolving once left every tool
|
|
28
|
+
* call failing after the first hour. resolveAuth refreshes the stored session
|
|
29
|
+
* (or picks up one another `dcd` process already refreshed). A failed
|
|
30
|
+
* refresh leaves the cache alone, so the next call retries — including after
|
|
31
|
+
* the user runs `dcd login` again. API keys never expire and are resolved once.
|
|
32
|
+
*
|
|
24
33
|
* Precedence matches the CLI: `DEVICE_CLOUD_API_KEY` env > stored `dcd login`
|
|
25
34
|
* session. The API URL honors `DCD_API_URL` (handy for pointing the server at
|
|
26
35
|
* dev/staging), then the logged-in env, then the prod default.
|
|
27
36
|
*/
|
|
28
37
|
export declare function getContext(): Promise<McpContext>;
|
|
38
|
+
/** Forget the resolved context. For tests. */
|
|
39
|
+
export declare function clearContextCache(): void;
|
|
29
40
|
/**
|
|
30
41
|
* Read-only mode hides the only state-changing / billable tool
|
|
31
42
|
* (`dcd_run_cloud_test`). Recommended for autonomous or untrusted agents.
|
package/dist/mcp/context.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveAuth } from '../utils/auth.js';
|
|
1
|
+
import { isAuthExpiring, resolveAuth } from '../utils/auth.js';
|
|
2
2
|
import { resolveApiUrl } from '../utils/config-store.js';
|
|
3
3
|
/** Write a line to stderr. Safe under stdio transport; stdout is reserved. */
|
|
4
4
|
export function logStderr(message) {
|
|
@@ -6,24 +6,36 @@ export function logStderr(message) {
|
|
|
6
6
|
}
|
|
7
7
|
let cached = null;
|
|
8
8
|
/**
|
|
9
|
-
* Resolve auth + API URL
|
|
9
|
+
* Resolve auth + API URL lazily on first tool invocation, and again whenever a
|
|
10
|
+
* `dcd login` session is near expiry.
|
|
10
11
|
*
|
|
11
12
|
* Lazy so `initialize` / `tools/list` succeed before the user has supplied a
|
|
12
13
|
* credential (clients enumerate tools on connect), and so an auth failure
|
|
13
14
|
* surfaces as a tool error rather than crashing the server at boot.
|
|
14
15
|
*
|
|
16
|
+
* Re-resolved near expiry because a session's access token lasts about an
|
|
17
|
+
* hour while an MCP server can run for days: resolving once left every tool
|
|
18
|
+
* call failing after the first hour. resolveAuth refreshes the stored session
|
|
19
|
+
* (or picks up one another `dcd` process already refreshed). A failed
|
|
20
|
+
* refresh leaves the cache alone, so the next call retries — including after
|
|
21
|
+
* the user runs `dcd login` again. API keys never expire and are resolved once.
|
|
22
|
+
*
|
|
15
23
|
* Precedence matches the CLI: `DEVICE_CLOUD_API_KEY` env > stored `dcd login`
|
|
16
24
|
* session. The API URL honors `DCD_API_URL` (handy for pointing the server at
|
|
17
25
|
* dev/staging), then the logged-in env, then the prod default.
|
|
18
26
|
*/
|
|
19
27
|
export async function getContext() {
|
|
20
|
-
if (cached)
|
|
28
|
+
if (cached && !isAuthExpiring(cached.auth))
|
|
21
29
|
return cached;
|
|
22
30
|
const auth = await resolveAuth({ apiKeyFlag: undefined });
|
|
23
31
|
const apiUrl = resolveApiUrl(process.env.DCD_API_URL);
|
|
24
32
|
cached = { apiUrl, auth };
|
|
25
33
|
return cached;
|
|
26
34
|
}
|
|
35
|
+
/** Forget the resolved context. For tests. */
|
|
36
|
+
export function clearContextCache() {
|
|
37
|
+
cached = null;
|
|
38
|
+
}
|
|
27
39
|
/**
|
|
28
40
|
* Read-only mode hides the only state-changing / billable tool
|
|
29
41
|
* (`dcd_run_cloud_test`). Recommended for autonomous or untrusted agents.
|
|
@@ -34,7 +34,7 @@ export function registerDownloadArtifacts(server) {
|
|
|
34
34
|
reportPath: z
|
|
35
35
|
.string()
|
|
36
36
|
.optional()
|
|
37
|
-
.describe('Local path for the report file (
|
|
37
|
+
.describe('Local path for the report file (default ./report.xml for junit, ./report.html for allure, ./report.zip for html, which is a ZIP of report.html and its assets)'),
|
|
38
38
|
},
|
|
39
39
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
40
40
|
}, async (args) => runTool('dcd_download_artifacts', async () => {
|
|
@@ -57,11 +57,14 @@ export function registerDownloadArtifacts(server) {
|
|
|
57
57
|
});
|
|
58
58
|
let reportPath;
|
|
59
59
|
if (args.report) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
// The HTML report is a ZIP of report.html plus assets; Allure's is
|
|
61
|
+
// a single HTML file.
|
|
62
|
+
const defaultReportPath = {
|
|
63
|
+
allure: 'report.html',
|
|
64
|
+
html: 'report.zip',
|
|
65
|
+
junit: 'report.xml',
|
|
66
|
+
}[args.report];
|
|
67
|
+
reportPath = path.resolve(args.reportPath || defaultReportPath);
|
|
65
68
|
await service.downloadReports({
|
|
66
69
|
apiUrl,
|
|
67
70
|
auth,
|
|
@@ -11,7 +11,7 @@ export function registerListDevices(server) {
|
|
|
11
11
|
title: 'List available devices',
|
|
12
12
|
description: 'List the iOS and Android devices, OS versions, and Maestro versions available on devicecloud.dev. ' +
|
|
13
13
|
'Use this to discover valid device/version values before submitting a test run. ' +
|
|
14
|
-
'Returns: { ios, android, androidPlay, maestro }, where each platform maps
|
|
14
|
+
'Returns: { ios, android, androidPlay, maestro }, where each platform maps a device slug to the OS versions it runs.',
|
|
15
15
|
inputSchema: {},
|
|
16
16
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
17
17
|
}, async () => runTool('dcd_list_devices', async () => {
|