@devicecloud.dev/dcd 4.4.3 → 4.4.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.
@@ -0,0 +1,22 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Live extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ apiKey: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
7
+ apiUrl: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
8
+ 'app-binary-id': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
9
+ platform: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
10
+ 'session-id': import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces").CustomOptions>;
11
+ yaml: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
12
+ };
13
+ static hidden: boolean;
14
+ static strict: boolean;
15
+ run(): Promise<void>;
16
+ private execTest;
17
+ private getFrontendUrl;
18
+ private getStatus;
19
+ private installBinary;
20
+ private startSession;
21
+ private stopSession;
22
+ }
@@ -0,0 +1,207 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const constants_1 = require("../constants");
5
+ const api_gateway_1 = require("../gateways/api-gateway");
6
+ const styling_1 = require("../utils/styling");
7
+ class Live extends core_1.Command {
8
+ static description = 'Start and interact with a live device session';
9
+ static examples = [
10
+ '<%= config.bin %> <%= command.id %> start',
11
+ '<%= config.bin %> <%= command.id %> start --platform android --app-binary-id abc-123',
12
+ '<%= config.bin %> <%= command.id %> install --session-id 42 --app-binary-id abc-123',
13
+ '<%= config.bin %> <%= command.id %> exec --session-id 42 --yaml "- launchApp"',
14
+ '<%= config.bin %> <%= command.id %> stop --session-id 42',
15
+ '<%= config.bin %> <%= command.id %> status --session-id 42',
16
+ ];
17
+ static flags = {
18
+ apiKey: constants_1.flags.apiKey,
19
+ apiUrl: constants_1.flags.apiUrl,
20
+ 'app-binary-id': core_1.Flags.string({
21
+ description: 'Binary upload ID to install on the device',
22
+ }),
23
+ platform: core_1.Flags.string({
24
+ default: 'android',
25
+ description: 'Device platform',
26
+ options: ['android', 'ios'],
27
+ }),
28
+ 'session-id': core_1.Flags.integer({
29
+ description: 'Live session ID (required for install, exec, stop, status)',
30
+ }),
31
+ yaml: core_1.Flags.string({
32
+ description: 'Maestro YAML commands to execute (for exec subcommand)',
33
+ }),
34
+ };
35
+ static hidden = true;
36
+ static strict = false;
37
+ async run() {
38
+ const { argv, flags } = await this.parse(Live);
39
+ const { apiKey: apiKeyFlag, apiUrl, 'app-binary-id': binaryId, platform, 'session-id': sessionId, yaml, } = flags;
40
+ const subcommand = argv[0];
41
+ if (!subcommand || !['exec', 'install', 'start', 'status', 'stop'].includes(subcommand)) {
42
+ this.log((0, styling_1.sectionHeader)('Live Session Commands'));
43
+ this.log(` ${styling_1.colors.bold('start')} Start a new live device session`);
44
+ this.log(` ${styling_1.colors.bold('install')} Install a binary on the device`);
45
+ this.log(` ${styling_1.colors.bold('exec')} Execute Maestro YAML commands`);
46
+ this.log(` ${styling_1.colors.bold('stop')} Stop a live session`);
47
+ this.log(` ${styling_1.colors.bold('status')} Get session status`);
48
+ this.log('');
49
+ this.log(` Run ${styling_1.colors.highlight('dcd live <command> --help')} for details`);
50
+ return;
51
+ }
52
+ const apiKey = apiKeyFlag || process.env.DEVICE_CLOUD_API_KEY;
53
+ if (!apiKey) {
54
+ this.error('API key is required. Provide via --api-key flag or DEVICE_CLOUD_API_KEY environment variable.');
55
+ }
56
+ switch (subcommand) {
57
+ case 'start': {
58
+ await this.startSession(apiUrl, apiKey, platform, binaryId);
59
+ break;
60
+ }
61
+ case 'install': {
62
+ if (!sessionId)
63
+ this.error('--session-id is required for install');
64
+ if (!binaryId)
65
+ this.error('--app-binary-id is required for install');
66
+ await this.installBinary(apiUrl, apiKey, sessionId, binaryId);
67
+ break;
68
+ }
69
+ case 'exec': {
70
+ if (!sessionId)
71
+ this.error('--session-id is required for exec');
72
+ if (!yaml)
73
+ this.error('--yaml is required for exec');
74
+ await this.execTest(apiUrl, apiKey, sessionId, yaml);
75
+ break;
76
+ }
77
+ case 'stop': {
78
+ if (!sessionId)
79
+ this.error('--session-id is required for stop');
80
+ await this.stopSession(apiUrl, apiKey, sessionId);
81
+ break;
82
+ }
83
+ case 'status': {
84
+ if (!sessionId)
85
+ this.error('--session-id is required for status');
86
+ await this.getStatus(apiUrl, apiKey, sessionId);
87
+ break;
88
+ }
89
+ default: {
90
+ break;
91
+ }
92
+ }
93
+ }
94
+ async execTest(apiUrl, apiKey, sessionId, yaml) {
95
+ this.log(`${styling_1.symbols.running} Executing commands on session ${sessionId}...`);
96
+ const res = await fetch(`${apiUrl}/live/${sessionId}/exec`, {
97
+ body: JSON.stringify({ yaml }),
98
+ headers: {
99
+ 'content-type': 'application/json',
100
+ 'x-app-api-key': apiKey,
101
+ },
102
+ method: 'POST',
103
+ });
104
+ if (!res.ok) {
105
+ await api_gateway_1.ApiGateway.handleApiError(res, 'Failed to execute test');
106
+ }
107
+ const result = (await res.json());
108
+ if (result.success) {
109
+ this.log(`${styling_1.symbols.success} Command executed successfully`);
110
+ }
111
+ else {
112
+ this.log(`${styling_1.symbols.error} Command failed`);
113
+ }
114
+ if (result.output) {
115
+ this.log((0, styling_1.sectionHeader)('Output'));
116
+ this.log(result.output);
117
+ }
118
+ if (result.error) {
119
+ this.log((0, styling_1.sectionHeader)('Error'));
120
+ this.log(styling_1.colors.error(result.error));
121
+ }
122
+ }
123
+ getFrontendUrl(apiUrl) {
124
+ if (apiUrl.includes('localhost:8000'))
125
+ return 'http://localhost:5173';
126
+ if (apiUrl.includes('api.dev.'))
127
+ return 'https://dev.console.devicecloud.dev';
128
+ return 'https://console.devicecloud.dev';
129
+ }
130
+ async getStatus(apiUrl, apiKey, sessionId) {
131
+ const res = await fetch(`${apiUrl}/live/${sessionId}`, {
132
+ headers: {
133
+ 'x-app-api-key': apiKey,
134
+ },
135
+ method: 'GET',
136
+ });
137
+ if (!res.ok) {
138
+ await api_gateway_1.ApiGateway.handleApiError(res, 'Failed to get session status');
139
+ }
140
+ const session = (await res.json());
141
+ this.log((0, styling_1.sectionHeader)('Live Session'));
142
+ this.log(` ${styling_1.colors.dim('Session ID:')} ${styling_1.colors.highlight(String(session.id))}`);
143
+ this.log(` ${styling_1.colors.dim('Platform:')} ${session.platform}`);
144
+ this.log(` ${styling_1.colors.dim('Status:')} ${session.status}`);
145
+ if (session.binary_upload_id) {
146
+ this.log(` ${styling_1.colors.dim('Binary:')} ${session.binary_upload_id}`);
147
+ }
148
+ this.log(` ${styling_1.colors.dim('Created:')} ${new Date(session.created_at).toLocaleString()}`);
149
+ }
150
+ async installBinary(apiUrl, apiKey, sessionId, binaryId) {
151
+ this.log(`${styling_1.symbols.running} Installing binary ${styling_1.colors.highlight(binaryId)} on session ${sessionId}...`);
152
+ const res = await fetch(`${apiUrl}/live/${sessionId}/install`, {
153
+ body: JSON.stringify({ binaryUploadId: binaryId }),
154
+ headers: {
155
+ 'content-type': 'application/json',
156
+ 'x-app-api-key': apiKey,
157
+ },
158
+ method: 'POST',
159
+ });
160
+ if (!res.ok) {
161
+ await api_gateway_1.ApiGateway.handleApiError(res, 'Failed to install binary');
162
+ }
163
+ this.log(`${styling_1.symbols.success} Binary installed successfully`);
164
+ }
165
+ async startSession(apiUrl, apiKey, platform, binaryId) {
166
+ this.log(`${styling_1.symbols.running} Starting ${platform} live session...`);
167
+ const res = await fetch(`${apiUrl}/live`, {
168
+ body: JSON.stringify({
169
+ binaryUploadId: binaryId,
170
+ platform,
171
+ }),
172
+ headers: {
173
+ 'content-type': 'application/json',
174
+ 'x-app-api-key': apiKey,
175
+ },
176
+ method: 'POST',
177
+ });
178
+ if (!res.ok) {
179
+ await api_gateway_1.ApiGateway.handleApiError(res, 'Failed to start live session');
180
+ }
181
+ const session = (await res.json());
182
+ const frontendUrl = this.getFrontendUrl(apiUrl);
183
+ this.log(`${styling_1.symbols.success} Live session started`);
184
+ this.log(` ${styling_1.colors.dim('Session ID:')} ${styling_1.colors.highlight(String(session.id))}`);
185
+ this.log(` ${styling_1.colors.dim('Platform:')} ${session.platform}`);
186
+ this.log(` ${styling_1.colors.dim('Status:')} ${session.status}`);
187
+ this.log(` ${styling_1.colors.dim('Console:')} ${styling_1.colors.highlight(`${frontendUrl}/live?session=${session.id}`)}`);
188
+ this.log('');
189
+ this.log(` ${styling_1.colors.dim('Install a binary:')} ${styling_1.colors.highlight(`dcd live install --session-id ${session.id} --app-binary-id <id>`)}`);
190
+ this.log(` ${styling_1.colors.dim('Run a command:')} ${styling_1.colors.highlight(`dcd live exec --session-id ${session.id} --yaml "- launchApp"`)}`);
191
+ this.log(` ${styling_1.colors.dim('Stop session:')} ${styling_1.colors.highlight(`dcd live stop --session-id ${session.id}`)}`);
192
+ }
193
+ async stopSession(apiUrl, apiKey, sessionId) {
194
+ this.log(`${styling_1.symbols.running} Stopping session ${sessionId}...`);
195
+ const res = await fetch(`${apiUrl}/live/${sessionId}`, {
196
+ headers: {
197
+ 'x-app-api-key': apiKey,
198
+ },
199
+ method: 'DELETE',
200
+ });
201
+ if (!res.ok) {
202
+ await api_gateway_1.ApiGateway.handleApiError(res, 'Failed to stop session');
203
+ }
204
+ this.log(`${styling_1.symbols.success} Session stopped`);
205
+ }
206
+ }
207
+ exports.default = Live;
@@ -49,6 +49,6 @@ exports.deviceFlags = {
49
49
  }),
50
50
  'disable-animations': core_1.Flags.boolean({
51
51
  default: false,
52
- description: '[Android only] Disable device animations during test execution. This reduces CPU load and may improve test reliability on resource-constrained runners.',
52
+ 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.',
53
53
  }),
54
54
  };
@@ -14,6 +14,14 @@ interface IWorkspaceConfig {
14
14
  includeTags?: null | string[];
15
15
  local?: ILocal | null;
16
16
  notifications?: INotificationsConfig;
17
+ platform?: {
18
+ android?: {
19
+ disableAnimations?: boolean;
20
+ };
21
+ ios?: {
22
+ disableAnimations?: boolean;
23
+ };
24
+ };
17
25
  }
18
26
  /** Local execution configuration */
19
27
  interface ILocal {
@@ -92,9 +92,10 @@ function extractDeviceCloudOverrides(config) {
92
92
  /**
93
93
  * Generate execution plan for a single flow file
94
94
  * @param normalizedInput - Normalized path to the flow file
95
+ * @param configFile - Optional custom config file path
95
96
  * @returns Execution plan for the single file with dependencies
96
97
  */
97
- async function planSingleFile(normalizedInput) {
98
+ async function planSingleFile(normalizedInput, configFile) {
98
99
  if (normalizedInput.endsWith('config.yaml') ||
99
100
  normalizedInput.endsWith('config.yml')) {
100
101
  throw new Error('If using config.yaml, pass the workspace folder path, not the config file or a custom path via --config');
@@ -106,6 +107,14 @@ async function planSingleFile(normalizedInput) {
106
107
  flowMetadata[normalizedInput] = config;
107
108
  flowOverrides[normalizedInput] = extractDeviceCloudOverrides(config);
108
109
  }
110
+ let workspaceConfig;
111
+ if (configFile) {
112
+ const configFilePath = path.resolve(process.cwd(), configFile);
113
+ if (!fs.existsSync(configFilePath)) {
114
+ throw new Error(`Config file does not exist: ${configFilePath}`);
115
+ }
116
+ workspaceConfig = (0, execution_plan_utils_1.readYamlFileAsJson)(configFilePath);
117
+ }
109
118
  const checkedDependancies = await checkDependencies(normalizedInput);
110
119
  return {
111
120
  flowMetadata,
@@ -113,6 +122,7 @@ async function planSingleFile(normalizedInput) {
113
122
  flowsToRun: [normalizedInput],
114
123
  referencedFiles: [...new Set(checkedDependancies)],
115
124
  totalFlowFiles: 1,
125
+ workspaceConfig,
116
126
  };
117
127
  }
118
128
  /**
@@ -211,7 +221,7 @@ async function plan(options) {
211
221
  throw new Error(`Flow path does not exist: ${path.resolve(normalizedInput)}`);
212
222
  }
213
223
  if (fs.lstatSync(normalizedInput).isFile()) {
214
- return planSingleFile(normalizedInput);
224
+ return planSingleFile(normalizedInput, configFile);
215
225
  }
216
226
  let unfilteredFlowFiles = await (0, execution_plan_utils_1.readDirectory)(normalizedInput, execution_plan_utils_1.isFlowFile);
217
227
  if (unfilteredFlowFiles.length === 0) {
@@ -67,6 +67,11 @@ class TestSubmissionService {
67
67
  testFormData.set('env', JSON.stringify(envObject));
68
68
  // Note: googlePlay is now included in configPayload below instead of as a separate field
69
69
  // to work around a FormData parsing issue in the API
70
+ const targetPlatform = iOSDevice || iOSVersion ? 'ios' : 'android';
71
+ const configYamlDisableAnimations = targetPlatform === 'ios'
72
+ ? Boolean(workspaceConfig?.platform?.ios?.disableAnimations)
73
+ : Boolean(workspaceConfig?.platform?.android?.disableAnimations);
74
+ const effectiveDisableAnimations = disableAnimations || configYamlDisableAnimations;
70
75
  const configPayload = {
71
76
  allExcludeTags,
72
77
  allIncludeTags,
@@ -83,7 +88,7 @@ class TestSubmissionService {
83
88
  report,
84
89
  showCrosshairs,
85
90
  maestroChromeOnboarding,
86
- disableAnimations,
91
+ disableAnimations: effectiveDisableAnimations,
87
92
  version: cliVersion,
88
93
  };
89
94
  testFormData.set('config', JSON.stringify(configPayload));
@@ -332,7 +332,7 @@
332
332
  "type": "boolean"
333
333
  },
334
334
  "disable-animations": {
335
- "description": "[Android only] Disable device animations during test execution. This reduces CPU load and may improve test reliability on resource-constrained runners.",
335
+ "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.",
336
336
  "name": "disable-animations",
337
337
  "allowNo": false,
338
338
  "type": "boolean"
@@ -717,6 +717,92 @@
717
717
  "list.js"
718
718
  ]
719
719
  },
720
+ "live": {
721
+ "aliases": [],
722
+ "args": {},
723
+ "description": "Start and interact with a live device session",
724
+ "examples": [
725
+ "<%= config.bin %> <%= command.id %> start",
726
+ "<%= config.bin %> <%= command.id %> start --platform android --app-binary-id abc-123",
727
+ "<%= config.bin %> <%= command.id %> install --session-id 42 --app-binary-id abc-123",
728
+ "<%= config.bin %> <%= command.id %> exec --session-id 42 --yaml \"- launchApp\"",
729
+ "<%= config.bin %> <%= command.id %> stop --session-id 42",
730
+ "<%= config.bin %> <%= command.id %> status --session-id 42"
731
+ ],
732
+ "flags": {
733
+ "apiKey": {
734
+ "aliases": [
735
+ "api-key"
736
+ ],
737
+ "description": "API key for devicecloud.dev (find this in the console UI). You can also set the DEVICE_CLOUD_API_KEY environment variable.",
738
+ "name": "apiKey",
739
+ "hasDynamicHelp": false,
740
+ "multiple": false,
741
+ "type": "option"
742
+ },
743
+ "apiUrl": {
744
+ "aliases": [
745
+ "api-url",
746
+ "apiURL"
747
+ ],
748
+ "description": "API base URL",
749
+ "hidden": true,
750
+ "name": "apiUrl",
751
+ "default": "https://api.devicecloud.dev",
752
+ "hasDynamicHelp": false,
753
+ "multiple": false,
754
+ "type": "option"
755
+ },
756
+ "app-binary-id": {
757
+ "description": "Binary upload ID to install on the device",
758
+ "name": "app-binary-id",
759
+ "hasDynamicHelp": false,
760
+ "multiple": false,
761
+ "type": "option"
762
+ },
763
+ "platform": {
764
+ "description": "Device platform",
765
+ "name": "platform",
766
+ "default": "android",
767
+ "hasDynamicHelp": false,
768
+ "multiple": false,
769
+ "options": [
770
+ "android",
771
+ "ios"
772
+ ],
773
+ "type": "option"
774
+ },
775
+ "session-id": {
776
+ "description": "Live session ID (required for install, exec, stop, status)",
777
+ "name": "session-id",
778
+ "hasDynamicHelp": false,
779
+ "multiple": false,
780
+ "type": "option"
781
+ },
782
+ "yaml": {
783
+ "description": "Maestro YAML commands to execute (for exec subcommand)",
784
+ "name": "yaml",
785
+ "hasDynamicHelp": false,
786
+ "multiple": false,
787
+ "type": "option"
788
+ }
789
+ },
790
+ "hasDynamicHelp": false,
791
+ "hidden": true,
792
+ "hiddenAliases": [],
793
+ "id": "live",
794
+ "pluginAlias": "@devicecloud.dev/dcd",
795
+ "pluginName": "@devicecloud.dev/dcd",
796
+ "pluginType": "core",
797
+ "strict": false,
798
+ "enableJsonFlag": false,
799
+ "isESM": false,
800
+ "relativePath": [
801
+ "dist",
802
+ "commands",
803
+ "live.js"
804
+ ]
805
+ },
720
806
  "status": {
721
807
  "aliases": [],
722
808
  "args": {},
@@ -880,5 +966,5 @@
880
966
  ]
881
967
  }
882
968
  },
883
- "version": "4.4.3"
969
+ "version": "4.4.4"
884
970
  }
package/package.json CHANGED
@@ -36,7 +36,7 @@
36
36
  "eslint-config-oclif-typescript": "^3.1.14",
37
37
  "eslint-config-prettier": "^10.1.8",
38
38
  "mocha": "^11.7.5",
39
- "oclif": "^4.22.87",
39
+ "oclif": "^4.22.96",
40
40
  "shx": "^0.4.0",
41
41
  "ts-node": "^10.9.2",
42
42
  "typescript": "^5.9.3"
@@ -69,7 +69,7 @@
69
69
  "type": "git",
70
70
  "url": "https://devicecloud.dev"
71
71
  },
72
- "version": "4.4.3",
72
+ "version": "4.4.4",
73
73
  "bugs": {
74
74
  "url": "https://discord.gg/gm3mJwcNw8"
75
75
  },