@devicecloud.dev/dcd 5.0.0-beta.0 → 5.0.0-beta.2
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/LICENSE +21 -0
- package/README.md +52 -0
- package/dist/commands/artifacts.d.ts +28 -28
- package/dist/commands/artifacts.js +20 -23
- package/dist/commands/cloud.d.ts +57 -57
- package/dist/commands/cloud.js +224 -192
- package/dist/commands/list.d.ts +22 -22
- package/dist/commands/list.js +43 -40
- package/dist/commands/live.js +134 -127
- package/dist/commands/login.d.ts +11 -11
- package/dist/commands/login.js +46 -44
- package/dist/commands/logout.js +16 -18
- package/dist/commands/status.d.ts +11 -11
- package/dist/commands/status.js +53 -44
- package/dist/commands/switch-org.d.ts +7 -7
- package/dist/commands/switch-org.js +19 -21
- package/dist/commands/upgrade.js +41 -33
- package/dist/commands/upload.d.ts +10 -10
- package/dist/commands/upload.js +42 -43
- package/dist/commands/whoami.js +17 -20
- package/dist/config/environments.js +6 -12
- package/dist/config/flags/api.flags.js +1 -4
- package/dist/config/flags/binary.flags.js +1 -4
- package/dist/config/flags/device.flags.js +6 -9
- package/dist/config/flags/environment.flags.js +1 -4
- package/dist/config/flags/execution.flags.js +1 -4
- package/dist/config/flags/github.flags.js +1 -4
- package/dist/config/flags/output.flags.js +1 -4
- package/dist/constants.js +15 -18
- package/dist/gateways/api-gateway.d.ts +31 -6
- package/dist/gateways/api-gateway.js +70 -16
- package/dist/gateways/cli-auth-gateway.d.ts +1 -1
- package/dist/gateways/cli-auth-gateway.js +3 -6
- package/dist/gateways/realtime-gateway.d.ts +32 -0
- package/dist/gateways/realtime-gateway.js +103 -0
- package/dist/gateways/supabase-gateway.d.ts +1 -1
- package/dist/gateways/supabase-gateway.js +10 -14
- package/dist/index.js +41 -38
- package/dist/mcp/context.d.ts +33 -0
- package/dist/mcp/context.js +33 -0
- package/dist/mcp/helpers.d.ts +16 -0
- package/dist/mcp/helpers.js +34 -0
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +24 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +27 -0
- package/dist/mcp/tools/download-artifacts.d.ts +11 -0
- package/dist/mcp/tools/download-artifacts.js +84 -0
- package/dist/mcp/tools/get-status.d.ts +7 -0
- package/dist/mcp/tools/get-status.js +39 -0
- package/dist/mcp/tools/list-devices.d.ts +7 -0
- package/dist/mcp/tools/list-devices.js +27 -0
- package/dist/mcp/tools/list-runs.d.ts +3 -0
- package/dist/mcp/tools/list-runs.js +60 -0
- package/dist/mcp/tools/run-cloud-test.d.ts +14 -0
- package/dist/mcp/tools/run-cloud-test.js +233 -0
- package/dist/methods.d.ts +32 -1
- package/dist/methods.js +133 -79
- package/dist/services/device-validation.service.d.ts +1 -1
- package/dist/services/device-validation.service.js +1 -5
- package/dist/services/execution-plan.service.js +14 -17
- package/dist/services/execution-plan.utils.js +15 -23
- package/dist/services/flow-paths.d.ts +17 -0
- package/dist/services/flow-paths.js +52 -0
- package/dist/services/metadata-extractor.service.js +22 -25
- package/dist/services/moropo.service.js +18 -20
- package/dist/services/report-download.service.d.ts +1 -1
- package/dist/services/report-download.service.js +5 -9
- package/dist/services/results-polling.service.d.ts +18 -3
- package/dist/services/results-polling.service.js +211 -108
- package/dist/services/telemetry.service.d.ts +10 -1
- package/dist/services/telemetry.service.js +40 -18
- package/dist/services/test-submission.service.d.ts +21 -4
- package/dist/services/test-submission.service.js +51 -34
- package/dist/services/version.service.d.ts +30 -7
- package/dist/services/version.service.js +88 -32
- package/dist/types/domain/auth.types.d.ts +8 -0
- package/dist/types/domain/auth.types.js +1 -2
- package/dist/types/domain/device.types.js +8 -11
- package/dist/types/domain/live.types.js +1 -2
- package/dist/types/generated/schema.types.js +1 -2
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.js +2 -18
- package/dist/types.js +1 -2
- package/dist/utils/auth.d.ts +1 -1
- package/dist/utils/auth.js +27 -28
- package/dist/utils/ci.d.ts +12 -0
- package/dist/utils/ci.js +39 -0
- package/dist/utils/cli.d.ts +16 -2
- package/dist/utils/cli.js +57 -29
- package/dist/utils/compatibility.d.ts +1 -1
- package/dist/utils/compatibility.js +5 -7
- package/dist/utils/config-store.js +33 -43
- package/dist/utils/connectivity.js +1 -4
- package/dist/utils/expo.js +15 -21
- package/dist/utils/orgs.js +8 -12
- package/dist/utils/paths.js +2 -5
- package/dist/utils/progress.d.ts +3 -0
- package/dist/utils/progress.js +47 -8
- package/dist/utils/styling.d.ts +35 -37
- package/dist/utils/styling.js +52 -86
- package/dist/utils/ui.d.ts +41 -0
- package/dist/utils/ui.js +95 -0
- package/package.json +27 -24
package/dist/commands/live.js
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const config_store_1 = require("../utils/config-store");
|
|
12
|
-
const styling_1 = require("../utils/styling");
|
|
1
|
+
import { defineCommand } from 'citty';
|
|
2
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { resolveFrontendUrl } from '../config/environments.js';
|
|
4
|
+
import { apiFlags } from '../config/flags/api.flags.js';
|
|
5
|
+
import { ApiGateway } from '../gateways/api-gateway.js';
|
|
6
|
+
import { resolveAuth } from '../utils/auth.js';
|
|
7
|
+
import { CliError, logger, validateEnum } from '../utils/cli.js';
|
|
8
|
+
import { resolveApiUrl } from '../utils/config-store.js';
|
|
9
|
+
import { colors, formatUrl } from '../utils/styling.js';
|
|
10
|
+
import { ui } from '../utils/ui.js';
|
|
13
11
|
const PLATFORM_OPTIONS = ['android', 'ios'];
|
|
14
12
|
const READY_TIMEOUT_MS = 180_000;
|
|
15
13
|
const READY_POLL_MS = 2_000;
|
|
16
14
|
async function requireAuth(keyFlag) {
|
|
17
|
-
return
|
|
15
|
+
return resolveAuth({ apiKeyFlag: keyFlag });
|
|
18
16
|
}
|
|
19
17
|
function sleep(ms) {
|
|
20
18
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -29,19 +27,19 @@ async function waitForReady(apiUrl, auth, sessionName, timeoutMs = READY_TIMEOUT
|
|
|
29
27
|
const deadline = Date.now() + timeoutMs;
|
|
30
28
|
let lastPhase = '';
|
|
31
29
|
for (;;) {
|
|
32
|
-
const session = await
|
|
30
|
+
const session = await ApiGateway.getLiveSession(apiUrl, auth, sessionName);
|
|
33
31
|
if (session.ready)
|
|
34
32
|
return session;
|
|
35
33
|
if (['CANCELLED', 'FAILED', 'STOPPED'].includes(session.status?.toUpperCase?.() ?? '')) {
|
|
36
|
-
throw new
|
|
34
|
+
throw new CliError(`Session ${sessionName} is ${session.status}; cannot become ready.`);
|
|
37
35
|
}
|
|
38
36
|
const phase = session.device_state?.phase_label ?? session.device_state?.phase ?? session.status;
|
|
39
37
|
if (phase && phase !== lastPhase) {
|
|
40
38
|
lastPhase = phase;
|
|
41
|
-
|
|
39
|
+
logger.log(` ${colors.dim(phase)}`);
|
|
42
40
|
}
|
|
43
41
|
if (Date.now() >= deadline) {
|
|
44
|
-
throw new
|
|
42
|
+
throw new CliError(`Timed out after ${Math.round(timeoutMs / 1000)}s waiting for the device to be ready` +
|
|
45
43
|
(phase ? ` (last phase: ${phase})` : '') +
|
|
46
44
|
'.');
|
|
47
45
|
}
|
|
@@ -78,29 +76,29 @@ function recoverFlagValue(flag, parsed, rawArgs) {
|
|
|
78
76
|
}
|
|
79
77
|
function readFlowFile(filePath) {
|
|
80
78
|
try {
|
|
81
|
-
return
|
|
79
|
+
return readFileSync(filePath, 'utf8');
|
|
82
80
|
}
|
|
83
81
|
catch (err) {
|
|
84
|
-
throw new
|
|
82
|
+
throw new CliError(`Could not read flow file '${filePath}': ${err.message}`);
|
|
85
83
|
}
|
|
86
84
|
}
|
|
87
85
|
function printExecResult(result) {
|
|
88
|
-
|
|
89
|
-
?
|
|
90
|
-
: `${
|
|
86
|
+
logger.log(result.success
|
|
87
|
+
? ui.success('Command executed successfully')
|
|
88
|
+
: `${ui.statusSymbol('FAILED')} Command failed`);
|
|
91
89
|
if (result.output) {
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
logger.log(ui.section('Output'));
|
|
91
|
+
logger.log(result.output);
|
|
94
92
|
}
|
|
95
93
|
if (result.error) {
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
logger.log(ui.section('Error'));
|
|
95
|
+
logger.log(colors.error(result.error));
|
|
98
96
|
}
|
|
99
97
|
}
|
|
100
|
-
const startSub =
|
|
98
|
+
const startSub = defineCommand({
|
|
101
99
|
meta: { name: 'start', description: 'Start a new live device session' },
|
|
102
100
|
args: {
|
|
103
|
-
...
|
|
101
|
+
...apiFlags,
|
|
104
102
|
platform: {
|
|
105
103
|
type: 'string',
|
|
106
104
|
default: 'android',
|
|
@@ -130,49 +128,52 @@ const startSub = (0, citty_1.defineCommand)({
|
|
|
130
128
|
},
|
|
131
129
|
async run({ args }) {
|
|
132
130
|
const auth = await requireAuth(args['api-key']);
|
|
133
|
-
const apiUrl =
|
|
134
|
-
const platform =
|
|
131
|
+
const apiUrl = resolveApiUrl(args['api-url']);
|
|
132
|
+
const platform = validateEnum(args.platform, PLATFORM_OPTIONS, 'platform');
|
|
135
133
|
const binaryId = args['app-binary-id'];
|
|
136
134
|
const deviceLocale = args['device-locale'];
|
|
137
135
|
const androidDevice = args['android-device'];
|
|
138
136
|
const androidApiLevel = args['android-api-level'];
|
|
139
137
|
if ((androidDevice || androidApiLevel) && platform !== 'android') {
|
|
140
|
-
throw new
|
|
138
|
+
throw new CliError('--android-device/--android-api-level are only valid with --platform android.');
|
|
141
139
|
}
|
|
142
140
|
if (Boolean(androidDevice) !== Boolean(androidApiLevel)) {
|
|
143
|
-
throw new
|
|
141
|
+
throw new CliError('--android-device and --android-api-level must be provided together.');
|
|
144
142
|
}
|
|
145
|
-
|
|
146
|
-
const session = await
|
|
143
|
+
logger.log(ui.running(`Starting ${platform} live session…`));
|
|
144
|
+
const session = await ApiGateway.startLiveSession(apiUrl, auth, {
|
|
147
145
|
binaryUploadId: binaryId,
|
|
148
146
|
deviceLocale,
|
|
149
147
|
platform,
|
|
150
148
|
androidDevice,
|
|
151
149
|
androidApiLevel,
|
|
152
150
|
});
|
|
153
|
-
const frontendUrl =
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
151
|
+
const frontendUrl = resolveFrontendUrl(apiUrl);
|
|
152
|
+
logger.log(ui.success('Live session started'));
|
|
153
|
+
logger.log(ui.branch(ui.fields([
|
|
154
|
+
['session', colors.highlight(session.session_name)],
|
|
155
|
+
['platform', session.platform],
|
|
156
|
+
['status', session.status],
|
|
157
|
+
['console', formatUrl(`${frontendUrl}/live?session=${session.session_name}`)],
|
|
158
|
+
])));
|
|
159
159
|
if (args.wait) {
|
|
160
|
-
|
|
161
|
-
cli_1.logger.log(`${styling_1.symbols.running} Waiting for the device to be ready...`);
|
|
160
|
+
logger.log(ui.running('Waiting for the device to be ready…'));
|
|
162
161
|
const ready = await waitForReady(apiUrl, auth, session.session_name);
|
|
163
|
-
|
|
162
|
+
logger.log(ui.success(`Device ready${ready.device_model ? ` (${ready.device_model})` : ''}`));
|
|
164
163
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
164
|
+
logger.log(ui.section('Next steps'));
|
|
165
|
+
logger.log(ui.branch(ui.fields([
|
|
166
|
+
['install a binary', colors.highlight(`dcd live install --session ${session.session_name} --app-binary-id <id>`)],
|
|
167
|
+
['run a flow', colors.highlight(`dcd live run --session ${session.session_name} path/to/flow.yaml`)],
|
|
168
|
+
['inspect screen', colors.highlight(`dcd live hierarchy --session ${session.session_name}`)],
|
|
169
|
+
['stop session', colors.highlight(`dcd live stop --session ${session.session_name}`)],
|
|
170
|
+
])));
|
|
170
171
|
},
|
|
171
172
|
});
|
|
172
|
-
const installSub =
|
|
173
|
+
const installSub = defineCommand({
|
|
173
174
|
meta: { name: 'install', description: 'Install a binary on the device' },
|
|
174
175
|
args: {
|
|
175
|
-
...
|
|
176
|
+
...apiFlags,
|
|
176
177
|
session: { type: 'string', required: true, description: 'Live session name' },
|
|
177
178
|
'app-binary-id': {
|
|
178
179
|
type: 'string',
|
|
@@ -187,23 +188,23 @@ const installSub = (0, citty_1.defineCommand)({
|
|
|
187
188
|
},
|
|
188
189
|
async run({ args }) {
|
|
189
190
|
const auth = await requireAuth(args['api-key']);
|
|
190
|
-
const apiUrl =
|
|
191
|
+
const apiUrl = resolveApiUrl(args['api-url']);
|
|
191
192
|
const sessionName = args.session;
|
|
192
193
|
const binaryId = args['app-binary-id'];
|
|
193
|
-
|
|
194
|
-
await
|
|
195
|
-
|
|
194
|
+
logger.log(ui.running(`Installing binary ${colors.highlight(binaryId)} on session ${colors.highlight(sessionName)}…`));
|
|
195
|
+
await ApiGateway.installLiveBinary(apiUrl, auth, sessionName, binaryId);
|
|
196
|
+
logger.log(ui.success('Binary installed successfully'));
|
|
196
197
|
if (args.wait) {
|
|
197
|
-
|
|
198
|
+
logger.log(ui.running('Waiting for the device to be ready…'));
|
|
198
199
|
await waitForReady(apiUrl, auth, sessionName);
|
|
199
|
-
|
|
200
|
+
logger.log(ui.success('Device ready'));
|
|
200
201
|
}
|
|
201
202
|
},
|
|
202
203
|
});
|
|
203
|
-
const execSub =
|
|
204
|
+
const execSub = defineCommand({
|
|
204
205
|
meta: { name: 'exec', description: 'Execute Maestro YAML commands' },
|
|
205
206
|
args: {
|
|
206
|
-
...
|
|
207
|
+
...apiFlags,
|
|
207
208
|
session: { type: 'string', required: true, description: 'Live session name' },
|
|
208
209
|
yaml: { type: 'string', description: 'Maestro YAML commands to execute' },
|
|
209
210
|
file: {
|
|
@@ -218,32 +219,32 @@ const execSub = (0, citty_1.defineCommand)({
|
|
|
218
219
|
},
|
|
219
220
|
async run({ args, rawArgs }) {
|
|
220
221
|
const auth = await requireAuth(args['api-key']);
|
|
221
|
-
const apiUrl =
|
|
222
|
+
const apiUrl = resolveApiUrl(args['api-url']);
|
|
222
223
|
const sessionName = args.session;
|
|
223
224
|
const inlineYaml = recoverFlagValue('--yaml', args.yaml, rawArgs);
|
|
224
225
|
const file = args.file;
|
|
225
226
|
if (inlineYaml && file) {
|
|
226
|
-
throw new
|
|
227
|
+
throw new CliError('Pass either --yaml or --file, not both.');
|
|
227
228
|
}
|
|
228
229
|
const yaml = file ? readFlowFile(file) : inlineYaml;
|
|
229
230
|
if (!yaml || !yaml.trim()) {
|
|
230
|
-
throw new
|
|
231
|
+
throw new CliError('Provide commands to execute via --yaml or --file.');
|
|
231
232
|
}
|
|
232
233
|
if (args.wait) {
|
|
233
234
|
await waitForReady(apiUrl, auth, sessionName);
|
|
234
235
|
}
|
|
235
|
-
|
|
236
|
-
const result = await
|
|
236
|
+
logger.log(ui.running(`Executing commands on session ${colors.highlight(sessionName)}…`));
|
|
237
|
+
const result = await ApiGateway.execLiveYaml(apiUrl, auth, sessionName, yaml);
|
|
237
238
|
printExecResult(result);
|
|
238
239
|
},
|
|
239
240
|
});
|
|
240
|
-
const runSub =
|
|
241
|
+
const runSub = defineCommand({
|
|
241
242
|
meta: {
|
|
242
243
|
name: 'run',
|
|
243
244
|
description: 'Run a whole Maestro flow file against the live session',
|
|
244
245
|
},
|
|
245
246
|
args: {
|
|
246
|
-
...
|
|
247
|
+
...apiFlags,
|
|
247
248
|
session: { type: 'string', required: true, description: 'Live session name' },
|
|
248
249
|
wait: {
|
|
249
250
|
type: 'boolean',
|
|
@@ -263,62 +264,62 @@ const runSub = (0, citty_1.defineCommand)({
|
|
|
263
264
|
},
|
|
264
265
|
async run({ args }) {
|
|
265
266
|
const auth = await requireAuth(args['api-key']);
|
|
266
|
-
const apiUrl =
|
|
267
|
+
const apiUrl = resolveApiUrl(args['api-url']);
|
|
267
268
|
const sessionName = args.session;
|
|
268
269
|
const flowFile = args.flowFile;
|
|
269
270
|
const timeoutMs = Math.max(1, Number(args.timeout) || 600) * 1000;
|
|
270
271
|
const commands = extractFlowCommands(readFlowFile(flowFile));
|
|
271
272
|
if (!commands) {
|
|
272
|
-
throw new
|
|
273
|
+
throw new CliError(`Flow file '${flowFile}' has no commands to run.`);
|
|
273
274
|
}
|
|
274
275
|
if (args.wait) {
|
|
275
276
|
await waitForReady(apiUrl, auth, sessionName);
|
|
276
277
|
}
|
|
277
|
-
|
|
278
|
+
logger.log(ui.running(`Running ${colors.highlight(flowFile)} on session ${colors.highlight(sessionName)}…`));
|
|
278
279
|
// Submit asynchronously and poll, so a long flow isn't capped by the
|
|
279
280
|
// server's 120s synchronous exec limit. Older servers ignore `async` and
|
|
280
281
|
// return the full result inline — handle that by falling through.
|
|
281
|
-
const submitted = await
|
|
282
|
+
const submitted = await ApiGateway.execLiveYaml(apiUrl, auth, sessionName, commands, {
|
|
282
283
|
async: true,
|
|
283
284
|
});
|
|
284
285
|
if (!submitted.commandId) {
|
|
285
286
|
printExecResult(submitted);
|
|
286
287
|
if (!submitted.success)
|
|
287
|
-
throw new
|
|
288
|
+
throw new CliError(submitted.error || 'Flow failed.', 2);
|
|
288
289
|
return;
|
|
289
290
|
}
|
|
290
291
|
const commandId = submitted.commandId;
|
|
291
292
|
const deadline = Date.now() + timeoutMs;
|
|
292
293
|
let lastKeepalive = Date.now();
|
|
293
294
|
for (;;) {
|
|
294
|
-
const status = await
|
|
295
|
+
const status = await ApiGateway.getLiveCommand(apiUrl, auth, sessionName, commandId);
|
|
295
296
|
if (status.done) {
|
|
296
297
|
printExecResult(status);
|
|
297
298
|
if (!status.success)
|
|
298
|
-
throw new
|
|
299
|
+
throw new CliError(status.error || 'Flow failed.', 2);
|
|
299
300
|
return;
|
|
300
301
|
}
|
|
301
302
|
if (Date.now() >= deadline) {
|
|
302
|
-
throw new
|
|
303
|
+
throw new CliError(`Flow still running after ${Math.round(timeoutMs / 1000)}s (command ${commandId}). ` +
|
|
303
304
|
'Raise --timeout if the flow legitimately runs longer.', 2);
|
|
304
305
|
}
|
|
305
306
|
// Keep the session alive during long polls so the inactivity sweep
|
|
306
307
|
// doesn't cancel it mid-flow.
|
|
307
308
|
if (Date.now() - lastKeepalive > 60_000) {
|
|
308
|
-
await
|
|
309
|
+
await ApiGateway.keepaliveLiveSession(apiUrl, auth, sessionName);
|
|
309
310
|
lastKeepalive = Date.now();
|
|
310
311
|
}
|
|
311
312
|
await sleep(2_000);
|
|
312
313
|
}
|
|
313
314
|
},
|
|
314
315
|
});
|
|
315
|
-
const screenshotSub =
|
|
316
|
+
const screenshotSub = defineCommand({
|
|
316
317
|
meta: {
|
|
317
318
|
name: 'screenshot',
|
|
318
319
|
description: 'Save the current device screen to an image file (PNG/JPEG)',
|
|
319
320
|
},
|
|
320
321
|
args: {
|
|
321
|
-
...
|
|
322
|
+
...apiFlags,
|
|
322
323
|
session: { type: 'string', required: true, description: 'Live session name' },
|
|
323
324
|
output: {
|
|
324
325
|
type: 'string',
|
|
@@ -328,32 +329,34 @@ const screenshotSub = (0, citty_1.defineCommand)({
|
|
|
328
329
|
},
|
|
329
330
|
async run({ args }) {
|
|
330
331
|
const auth = await requireAuth(args['api-key']);
|
|
331
|
-
const apiUrl =
|
|
332
|
+
const apiUrl = resolveApiUrl(args['api-url']);
|
|
332
333
|
const sessionName = args.session;
|
|
333
|
-
const session = await
|
|
334
|
+
const session = await ApiGateway.getLiveSession(apiUrl, auth, sessionName);
|
|
334
335
|
const dataUrl = session.screenshot_data_url ?? session.hierarchy?.screenshot;
|
|
335
336
|
if (!dataUrl) {
|
|
336
|
-
throw new
|
|
337
|
+
throw new CliError(`No screenshot available yet for ${sessionName}. The device may not be streaming — ` +
|
|
337
338
|
'try again, or start/install with --wait.');
|
|
338
339
|
}
|
|
339
340
|
const match = /^data:image\/(\w+);base64,(.+)$/s.exec(dataUrl);
|
|
340
341
|
const ext = match ? match[1].replace('jpeg', 'jpg') : 'png';
|
|
341
342
|
const base64 = match ? match[2] : dataUrl;
|
|
342
343
|
const output = args.output ?? `live-screenshot.${ext}`;
|
|
343
|
-
|
|
344
|
-
|
|
344
|
+
writeFileSync(output, Buffer.from(base64, 'base64'));
|
|
345
|
+
logger.log(ui.success(`Screenshot saved to ${colors.highlight(output)}`));
|
|
345
346
|
if (session.hierarchy) {
|
|
346
|
-
|
|
347
|
+
logger.log(ui.branch(ui.fields([
|
|
348
|
+
['resolution', `${session.hierarchy.width}x${session.hierarchy.height}`],
|
|
349
|
+
])));
|
|
347
350
|
}
|
|
348
351
|
},
|
|
349
352
|
});
|
|
350
|
-
const hierarchySub =
|
|
353
|
+
const hierarchySub = defineCommand({
|
|
351
354
|
meta: {
|
|
352
355
|
name: 'hierarchy',
|
|
353
356
|
description: 'Dump the current view hierarchy (the selectors you can tap/assert on)',
|
|
354
357
|
},
|
|
355
358
|
args: {
|
|
356
|
-
...
|
|
359
|
+
...apiFlags,
|
|
357
360
|
session: { type: 'string', required: true, description: 'Live session name' },
|
|
358
361
|
json: { type: 'boolean', description: 'Output the raw hierarchy as JSON' },
|
|
359
362
|
output: {
|
|
@@ -364,21 +367,21 @@ const hierarchySub = (0, citty_1.defineCommand)({
|
|
|
364
367
|
},
|
|
365
368
|
async run({ args }) {
|
|
366
369
|
const auth = await requireAuth(args['api-key']);
|
|
367
|
-
const apiUrl =
|
|
370
|
+
const apiUrl = resolveApiUrl(args['api-url']);
|
|
368
371
|
const sessionName = args.session;
|
|
369
372
|
const json = Boolean(args.json);
|
|
370
373
|
const output = args.output;
|
|
371
|
-
const session = await
|
|
374
|
+
const session = await ApiGateway.getLiveSession(apiUrl, auth, sessionName);
|
|
372
375
|
const hierarchy = session.hierarchy;
|
|
373
376
|
if (!hierarchy) {
|
|
374
|
-
throw new
|
|
377
|
+
throw new CliError(`No hierarchy available yet for ${sessionName}. The device may not be streaming — ` +
|
|
375
378
|
'try again, or start/install with --wait.');
|
|
376
379
|
}
|
|
377
380
|
if (json) {
|
|
378
381
|
const out = JSON.stringify(hierarchy, null, 2);
|
|
379
382
|
if (output) {
|
|
380
|
-
|
|
381
|
-
|
|
383
|
+
writeFileSync(output, out);
|
|
384
|
+
logger.log(ui.success(`Hierarchy written to ${colors.highlight(output)}`));
|
|
382
385
|
}
|
|
383
386
|
else {
|
|
384
387
|
// eslint-disable-next-line no-console
|
|
@@ -400,71 +403,74 @@ const hierarchySub = (0, citty_1.defineCommand)({
|
|
|
400
403
|
if (parts.length === 0)
|
|
401
404
|
continue;
|
|
402
405
|
const bounds = el.bounds
|
|
403
|
-
? ` ${
|
|
406
|
+
? ` ${colors.dim(`(${el.bounds.x},${el.bounds.y} ${el.bounds.width}x${el.bounds.height})`)}`
|
|
404
407
|
: '';
|
|
405
408
|
lines.push(` ${parts.join(' ')}${bounds}`);
|
|
406
409
|
}
|
|
407
410
|
const out = lines.join('\n');
|
|
408
411
|
if (output) {
|
|
409
|
-
|
|
410
|
-
|
|
412
|
+
writeFileSync(output, `${out}\n`);
|
|
413
|
+
logger.log(ui.success(`Hierarchy written to ${colors.highlight(output)}`));
|
|
411
414
|
}
|
|
412
415
|
else {
|
|
413
|
-
|
|
416
|
+
logger.log(out);
|
|
414
417
|
}
|
|
415
418
|
},
|
|
416
419
|
});
|
|
417
|
-
const stopSub =
|
|
420
|
+
const stopSub = defineCommand({
|
|
418
421
|
meta: { name: 'stop', description: 'Stop a live session' },
|
|
419
422
|
args: {
|
|
420
|
-
...
|
|
423
|
+
...apiFlags,
|
|
421
424
|
session: { type: 'string', required: true, description: 'Live session name' },
|
|
422
425
|
},
|
|
423
426
|
async run({ args }) {
|
|
424
427
|
const auth = await requireAuth(args['api-key']);
|
|
425
|
-
const apiUrl =
|
|
428
|
+
const apiUrl = resolveApiUrl(args['api-url']);
|
|
426
429
|
const sessionName = args.session;
|
|
427
|
-
|
|
428
|
-
await
|
|
429
|
-
|
|
430
|
+
logger.log(ui.running(`Stopping session ${colors.highlight(sessionName)}…`));
|
|
431
|
+
await ApiGateway.stopLiveSession(apiUrl, auth, sessionName);
|
|
432
|
+
logger.log(ui.success('Session stopped'));
|
|
430
433
|
},
|
|
431
434
|
});
|
|
432
|
-
const statusSub =
|
|
435
|
+
const statusSub = defineCommand({
|
|
433
436
|
meta: { name: 'status', description: 'Get session status' },
|
|
434
437
|
args: {
|
|
435
|
-
...
|
|
438
|
+
...apiFlags,
|
|
436
439
|
session: { type: 'string', required: true, description: 'Live session name' },
|
|
437
440
|
},
|
|
438
441
|
async run({ args }) {
|
|
439
442
|
const auth = await requireAuth(args['api-key']);
|
|
440
|
-
const apiUrl =
|
|
443
|
+
const apiUrl = resolveApiUrl(args['api-url']);
|
|
441
444
|
const sessionName = args.session;
|
|
442
|
-
const session = await
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
445
|
+
const session = await ApiGateway.getLiveSession(apiUrl, auth, sessionName);
|
|
446
|
+
const fields = [
|
|
447
|
+
['session', colors.highlight(session.session_name)],
|
|
448
|
+
['platform', session.platform],
|
|
449
|
+
['status', session.status],
|
|
450
|
+
['ready', session.ready ? colors.success('yes') : colors.warning('no')],
|
|
451
|
+
];
|
|
448
452
|
const phase = session.device_state?.phase_label ?? session.device_state?.phase;
|
|
449
453
|
if (phase) {
|
|
450
|
-
|
|
454
|
+
fields.push(['phase', phase]);
|
|
451
455
|
}
|
|
452
456
|
if (session.device_model) {
|
|
453
|
-
|
|
457
|
+
fields.push(['device', session.device_model]);
|
|
454
458
|
}
|
|
455
459
|
if (session.device_locale) {
|
|
456
|
-
|
|
460
|
+
fields.push(['locale', session.device_locale]);
|
|
457
461
|
}
|
|
458
462
|
if (session.binary_upload_id) {
|
|
459
|
-
|
|
463
|
+
fields.push(['binary', session.binary_upload_id]);
|
|
460
464
|
}
|
|
461
465
|
if (typeof session.seconds_until_auto_cancel === 'number') {
|
|
462
|
-
|
|
466
|
+
fields.push(['auto-cancel in', `${session.seconds_until_auto_cancel}s`]);
|
|
463
467
|
}
|
|
464
|
-
|
|
468
|
+
fields.push(['created', new Date(session.created_at).toLocaleString()]);
|
|
469
|
+
logger.log(ui.section('Live Session'));
|
|
470
|
+
logger.log(ui.branch(ui.fields(fields)));
|
|
465
471
|
},
|
|
466
472
|
});
|
|
467
|
-
|
|
473
|
+
export const liveCommand = defineCommand({
|
|
468
474
|
meta: {
|
|
469
475
|
name: 'live',
|
|
470
476
|
description: 'Start and interact with a live device session',
|
|
@@ -497,17 +503,18 @@ exports.liveCommand = (0, citty_1.defineCommand)({
|
|
|
497
503
|
const firstPositional = rawArgs.find((arg) => !arg.startsWith('-'));
|
|
498
504
|
if (firstPositional && subNames.has(firstPositional))
|
|
499
505
|
return;
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
506
|
+
logger.log(ui.section('Live Session Commands'));
|
|
507
|
+
logger.log(ui.branch(ui.fields([
|
|
508
|
+
[colors.bold('start'), 'Start a new live device session'],
|
|
509
|
+
[colors.bold('install'), 'Install a binary on the device'],
|
|
510
|
+
[colors.bold('exec'), 'Execute Maestro YAML commands'],
|
|
511
|
+
[colors.bold('run'), 'Run a whole Maestro flow file'],
|
|
512
|
+
[colors.bold('screenshot'), 'Save the current device screen to an image file'],
|
|
513
|
+
[colors.bold('hierarchy'), 'Dump the current view hierarchy (selectors)'],
|
|
514
|
+
[colors.bold('stop'), 'Stop a live session'],
|
|
515
|
+
[colors.bold('status'), 'Get session status'],
|
|
516
|
+
])));
|
|
517
|
+
logger.log(ui.note(`\nRun ${colors.highlight('dcd live <command> --help')} for details`));
|
|
511
518
|
},
|
|
512
519
|
});
|
|
513
|
-
|
|
520
|
+
export default liveCommand;
|
package/dist/commands/login.d.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
export declare const loginCommand: import("citty").CommandDef<{
|
|
2
|
-
'api-url': {
|
|
3
|
-
type: "string";
|
|
4
|
-
default:
|
|
5
|
-
description:
|
|
2
|
+
readonly 'api-url': {
|
|
3
|
+
readonly type: "string";
|
|
4
|
+
readonly default: "https://api.devicecloud.dev";
|
|
5
|
+
readonly description: "API base URL";
|
|
6
6
|
};
|
|
7
|
-
'frontend-url': {
|
|
8
|
-
type: "string";
|
|
9
|
-
description:
|
|
7
|
+
readonly 'frontend-url': {
|
|
8
|
+
readonly type: "string";
|
|
9
|
+
readonly description: "Override the frontend URL used to complete login (defaults per env)";
|
|
10
10
|
};
|
|
11
|
-
browser: {
|
|
12
|
-
type: "boolean";
|
|
13
|
-
default: true;
|
|
14
|
-
description:
|
|
11
|
+
readonly browser: {
|
|
12
|
+
readonly type: "boolean";
|
|
13
|
+
readonly default: true;
|
|
14
|
+
readonly description: "Open the login URL in a browser (pass --no-browser to just print it)";
|
|
15
15
|
};
|
|
16
16
|
}>;
|
|
17
17
|
export default loginCommand;
|