@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.
- 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 +5 -5
- package/dist/commands/cloud.js +14 -6
- 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 +1 -1
- package/dist/config/flags/execution.flags.js +1 -1
- package/dist/config/flags/output.flags.d.ts +1 -1
- package/dist/config/flags/output.flags.js +1 -1
- package/dist/constants.d.ts +5 -5
- 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 +19 -3
- package/dist/services/device-validation.service.d.ts +17 -2
- package/dist/services/device-validation.service.js +93 -12
- 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 +21 -0
- package/dist/services/results-polling.service.js +55 -3
- package/dist/services/test-submission.service.d.ts +11 -0
- package/dist/services/test-submission.service.js +12 -5
- 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 +2 -2
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";
|
|
@@ -101,7 +101,7 @@ export declare const cloudCommand: import("citty").CommandDef<{
|
|
|
101
101
|
readonly 'cancel-previous': {
|
|
102
102
|
readonly type: "boolean";
|
|
103
103
|
readonly default: false;
|
|
104
|
-
readonly description: "Cancel the still-queued tests of the previous run from the same CI context
|
|
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
105
|
};
|
|
106
106
|
readonly config: {
|
|
107
107
|
readonly type: "string";
|
|
@@ -211,12 +211,12 @@ export declare const cloudCommand: import("citty").CommandDef<{
|
|
|
211
211
|
readonly 'android-no-snapshot': {
|
|
212
212
|
readonly type: "boolean";
|
|
213
213
|
readonly default: false;
|
|
214
|
-
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.";
|
|
215
215
|
};
|
|
216
216
|
readonly 'disable-animations': {
|
|
217
217
|
readonly type: "boolean";
|
|
218
|
-
readonly
|
|
219
|
-
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";
|
|
220
220
|
};
|
|
221
221
|
readonly 'app-binary-id': {
|
|
222
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';
|
|
@@ -140,10 +140,14 @@ export const cloudCommand = defineCommand({
|
|
|
140
140
|
// (#1152). Flag wins; otherwise DCD_ENCRYPT / DCD_ENCRYPT_BINARIES.
|
|
141
141
|
const encrypt = isEncryptionEnabled(args['encrypt'] ? true : undefined);
|
|
142
142
|
const includeTags = coerceArray(collectRepeatedFlag(rawArgs, ['--include-tags']));
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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;
|
|
147
151
|
const androidNoSnapshot = Boolean(args['android-no-snapshot']);
|
|
148
152
|
// Repeatable device-matrix flags: one validated cell each, no cross-product.
|
|
149
153
|
const iosMatrixFlags = collectRepeatedFlag(rawArgs, ['--ios-device-matrix']);
|
|
@@ -164,7 +168,9 @@ export const cloudCommand = defineCommand({
|
|
|
164
168
|
const renderEngine = validateEnum(args['render-engine'], RENDER_ENGINE_OPTIONS, 'render-engine');
|
|
165
169
|
const showCrosshairs = Boolean(args['show-crosshairs']);
|
|
166
170
|
const maestroChromeOnboarding = Boolean(args['maestro-chrome-onboarding']);
|
|
167
|
-
|
|
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'];
|
|
168
174
|
const ghBranch = args.branch;
|
|
169
175
|
const ghCommitSha = args['commit-sha'];
|
|
170
176
|
const ghRepoName = args['repo-name'];
|
|
@@ -557,6 +563,8 @@ export const cloudCommand = defineCommand({
|
|
|
557
563
|
androidNoSnapshot,
|
|
558
564
|
apiUrl,
|
|
559
565
|
appBinaryId: finalBinaryId,
|
|
566
|
+
// With --app-binary-id the binary's platform is unknown here.
|
|
567
|
+
appPlatform: appBinaryId ? undefined : platformFromAppFile(finalAppFile),
|
|
560
568
|
cancelPrevious,
|
|
561
569
|
cliVersion,
|
|
562
570
|
commonRoot,
|
|
@@ -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
|
};
|
|
@@ -5,7 +5,7 @@ export declare const executionFlags: {
|
|
|
5
5
|
readonly 'cancel-previous': {
|
|
6
6
|
readonly type: "boolean";
|
|
7
7
|
readonly default: false;
|
|
8
|
-
readonly description: "Cancel the still-queued tests of the previous run from the same CI context
|
|
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
9
|
};
|
|
10
10
|
readonly config: {
|
|
11
11
|
readonly type: "string";
|
|
@@ -5,7 +5,7 @@ export const executionFlags = {
|
|
|
5
5
|
'cancel-previous': {
|
|
6
6
|
type: 'boolean',
|
|
7
7
|
default: false,
|
|
8
|
-
description: 'Cancel the still-queued tests of the previous run from the same CI context
|
|
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
9
|
},
|
|
10
10
|
config: {
|
|
11
11
|
type: 'string',
|
|
@@ -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";
|
|
@@ -87,7 +87,7 @@ export declare const flags: {
|
|
|
87
87
|
readonly 'cancel-previous': {
|
|
88
88
|
readonly type: "boolean";
|
|
89
89
|
readonly default: false;
|
|
90
|
-
readonly description: "Cancel the still-queued tests of the previous run from the same CI context
|
|
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
91
|
};
|
|
92
92
|
readonly config: {
|
|
93
93
|
readonly type: "string";
|
|
@@ -197,12 +197,12 @@ export declare const flags: {
|
|
|
197
197
|
readonly 'android-no-snapshot': {
|
|
198
198
|
readonly type: "boolean";
|
|
199
199
|
readonly default: false;
|
|
200
|
-
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.";
|
|
201
201
|
};
|
|
202
202
|
readonly 'disable-animations': {
|
|
203
203
|
readonly type: "boolean";
|
|
204
|
-
readonly
|
|
205
|
-
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";
|
|
206
206
|
};
|
|
207
207
|
readonly 'app-binary-id': {
|
|
208
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 () => {
|
|
@@ -4,9 +4,11 @@ import { ApiError, ApiGateway } from '../../gateways/api-gateway.js';
|
|
|
4
4
|
import { plan } from '../../services/execution-plan.service.js';
|
|
5
5
|
import { computeCommonRoot, buildTestMetadataMap } from '../../services/flow-paths.js';
|
|
6
6
|
import { DeviceValidationService } from '../../services/device-validation.service.js';
|
|
7
|
+
import { platformFromAppFile } from '../../services/notices.service.js';
|
|
7
8
|
import { TestSubmissionService } from '../../services/test-submission.service.js';
|
|
8
9
|
import { VersionService } from '../../services/version.service.js';
|
|
9
10
|
import { uploadBinary, uploadFlowZip, verifyAppZip } from '../../methods.js';
|
|
11
|
+
import { refreshAuth } from '../../utils/auth.js';
|
|
10
12
|
import { getCliVersion } from '../../utils/cli.js';
|
|
11
13
|
import { fetchCompatibilityData } from '../../utils/compatibility.js';
|
|
12
14
|
import { isEncryptionEnabled } from '../../utils/envelope.js';
|
|
@@ -100,8 +102,15 @@ export function registerRunCloudTest(server) {
|
|
|
100
102
|
const cliVersion = getCliVersion();
|
|
101
103
|
const compatibilityData = await fetchCompatibilityData(apiUrl, auth);
|
|
102
104
|
const deviceValidation = new DeviceValidationService();
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
// Hints in validation errors name this tool's parameters, not CLI flags.
|
|
106
|
+
const argNames = {
|
|
107
|
+
androidApiLevel: 'androidApiLevel',
|
|
108
|
+
androidDevice: 'androidDevice',
|
|
109
|
+
iOSDevice: 'iosDevice',
|
|
110
|
+
iOSVersion: 'iosVersion',
|
|
111
|
+
};
|
|
112
|
+
deviceValidation.validateiOSDevice(args.iosVersion, args.iosDevice, compatibilityData, { argNames, logger: logStderr });
|
|
113
|
+
deviceValidation.validateAndroidDevice(args.androidApiLevel, args.androidDevice, Boolean(args.googlePlay), compatibilityData, { argNames, logger: logStderr });
|
|
105
114
|
const resolvedMaestroVersion = new VersionService().resolveMaestroVersion(args.maestroVersion, compatibilityData, { logger: logStderr });
|
|
106
115
|
// Directory inputs must end in a separator so the planner treats them
|
|
107
116
|
// as a workspace rather than a single file.
|
|
@@ -162,6 +171,8 @@ export function registerRunCloudTest(server) {
|
|
|
162
171
|
const { buffer, fields } = await testSubmissionService.buildTestPayload({
|
|
163
172
|
apiUrl,
|
|
164
173
|
appBinaryId,
|
|
174
|
+
// Picks the workspace config's per-platform disableAnimations.
|
|
175
|
+
appPlatform: args.appBinaryId ? undefined : platformFromAppFile(args.appFile),
|
|
165
176
|
cliVersion,
|
|
166
177
|
commonRoot,
|
|
167
178
|
continueOnFailure,
|
|
@@ -221,8 +232,13 @@ export function registerRunCloudTest(server) {
|
|
|
221
232
|
// Bounded poll. The run continues in the cloud regardless of timeout —
|
|
222
233
|
// the caller can resume with dcd_get_status using the returned uploadId.
|
|
223
234
|
const deadline = Date.now() + (args.waitTimeoutSeconds ?? 600) * 1000;
|
|
235
|
+
let pollAuth = auth;
|
|
224
236
|
for (;;) {
|
|
225
|
-
|
|
237
|
+
// A wait of up to an hour can outlast a `dcd login` access token.
|
|
238
|
+
pollAuth = await refreshAuth(pollAuth);
|
|
239
|
+
const status = await ApiGateway.getUploadStatus(apiUrl, pollAuth, {
|
|
240
|
+
uploadId,
|
|
241
|
+
});
|
|
226
242
|
if (TERMINAL_STATUSES.has(status.status)) {
|
|
227
243
|
return jsonResult({ uploadId, consoleUrl, status: status.status, tests: status.tests });
|
|
228
244
|
}
|
|
@@ -1,10 +1,25 @@
|
|
|
1
|
-
import { CompatibilityData } from '../utils/compatibility.js';
|
|
1
|
+
import type { CompatibilityData } from '../utils/compatibility.js';
|
|
2
|
+
/** The device / OS inputs a caller exposes, named for "pass it explicitly" hints. */
|
|
3
|
+
export interface DeviceArgNames {
|
|
4
|
+
androidApiLevel: string;
|
|
5
|
+
androidDevice: string;
|
|
6
|
+
iOSDevice: string;
|
|
7
|
+
iOSVersion: string;
|
|
8
|
+
}
|
|
2
9
|
export interface DeviceValidationOptions {
|
|
10
|
+
/** How the caller names its inputs in error hints. Defaults to the CLI flags. */
|
|
11
|
+
argNames?: DeviceArgNames;
|
|
3
12
|
debug?: boolean;
|
|
4
13
|
logger?: (message: string) => void;
|
|
5
14
|
}
|
|
6
15
|
/**
|
|
7
|
-
*
|
|
16
|
+
* Validates requested devices against the compatibility matrix the API serves
|
|
17
|
+
* from `GET /results/compatibility/data`, which is the only authority on what
|
|
18
|
+
* can run. Nothing here consults the CLI's device and OS enums
|
|
19
|
+
* (src/types/domain/device.types.ts) — those only feed `--help` — so a device
|
|
20
|
+
* or OS version the API adds is accepted without a CLI release, and one it
|
|
21
|
+
* withdraws is refused, before anything is uploaded, with the list the API
|
|
22
|
+
* offers today.
|
|
8
23
|
*/
|
|
9
24
|
export declare class DeviceValidationService {
|
|
10
25
|
/**
|