@devicecloud.dev/dcd 4.4.9 → 5.0.0-beta.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.
Files changed (97) hide show
  1. package/README.md +40 -2
  2. package/dist/commands/artifacts.d.ts +47 -18
  3. package/dist/commands/artifacts.js +68 -60
  4. package/dist/commands/cloud.d.ts +228 -88
  5. package/dist/commands/cloud.js +389 -288
  6. package/dist/commands/list.d.ts +39 -38
  7. package/dist/commands/list.js +122 -127
  8. package/dist/commands/live.d.ts +2 -0
  9. package/dist/commands/live.js +513 -0
  10. package/dist/commands/login.d.ts +17 -0
  11. package/dist/commands/login.js +250 -0
  12. package/dist/commands/logout.d.ts +2 -0
  13. package/dist/commands/logout.js +32 -0
  14. package/dist/commands/status.d.ts +23 -42
  15. package/dist/commands/status.js +162 -173
  16. package/dist/commands/switch-org.d.ts +12 -0
  17. package/dist/commands/switch-org.js +78 -0
  18. package/dist/commands/upgrade.d.ts +2 -0
  19. package/dist/commands/upgrade.js +122 -0
  20. package/dist/commands/upload.d.ts +33 -18
  21. package/dist/commands/upload.js +62 -67
  22. package/dist/commands/whoami.d.ts +2 -0
  23. package/dist/commands/whoami.js +34 -0
  24. package/dist/config/environments.d.ts +31 -0
  25. package/dist/config/environments.js +58 -0
  26. package/dist/config/flags/api.flags.d.ts +10 -2
  27. package/dist/config/flags/api.flags.js +12 -10
  28. package/dist/config/flags/binary.flags.d.ts +17 -4
  29. package/dist/config/flags/binary.flags.js +13 -14
  30. package/dist/config/flags/device.flags.d.ts +49 -11
  31. package/dist/config/flags/device.flags.js +41 -33
  32. package/dist/config/flags/environment.flags.d.ts +27 -6
  33. package/dist/config/flags/environment.flags.js +23 -25
  34. package/dist/config/flags/execution.flags.d.ts +35 -8
  35. package/dist/config/flags/execution.flags.js +30 -37
  36. package/dist/config/flags/github.flags.d.ts +23 -5
  37. package/dist/config/flags/github.flags.js +18 -11
  38. package/dist/config/flags/output.flags.d.ts +57 -13
  39. package/dist/config/flags/output.flags.js +47 -43
  40. package/dist/constants.d.ts +218 -51
  41. package/dist/constants.js +2 -2
  42. package/dist/gateways/api-gateway.d.ts +43 -12
  43. package/dist/gateways/api-gateway.js +240 -100
  44. package/dist/gateways/cli-auth-gateway.d.ts +13 -0
  45. package/dist/gateways/cli-auth-gateway.js +57 -0
  46. package/dist/gateways/supabase-gateway.d.ts +11 -11
  47. package/dist/gateways/supabase-gateway.js +15 -39
  48. package/dist/index.d.ts +2 -1
  49. package/dist/index.js +93 -2
  50. package/dist/methods.d.ts +3 -5
  51. package/dist/methods.js +170 -178
  52. package/dist/services/device-validation.service.d.ts +8 -0
  53. package/dist/services/device-validation.service.js +55 -35
  54. package/dist/services/execution-plan.service.js +27 -15
  55. package/dist/services/execution-plan.utils.d.ts +3 -0
  56. package/dist/services/execution-plan.utils.js +10 -32
  57. package/dist/services/metadata-extractor.service.d.ts +0 -2
  58. package/dist/services/metadata-extractor.service.js +57 -57
  59. package/dist/services/moropo.service.js +25 -24
  60. package/dist/services/report-download.service.d.ts +12 -1
  61. package/dist/services/report-download.service.js +31 -20
  62. package/dist/services/results-polling.service.d.ts +6 -7
  63. package/dist/services/results-polling.service.js +80 -33
  64. package/dist/services/telemetry.service.d.ts +40 -0
  65. package/dist/services/telemetry.service.js +230 -0
  66. package/dist/services/test-submission.service.js +2 -1
  67. package/dist/services/version.service.d.ts +3 -2
  68. package/dist/services/version.service.js +27 -11
  69. package/dist/types/domain/auth.types.d.ts +12 -0
  70. package/dist/types/{schema.types.js → domain/auth.types.js} +0 -1
  71. package/dist/types/domain/live.types.d.ts +76 -0
  72. package/dist/types/domain/live.types.js +4 -0
  73. package/dist/utils/auth.d.ts +13 -0
  74. package/dist/utils/auth.js +142 -0
  75. package/dist/utils/cli.d.ts +35 -0
  76. package/dist/utils/cli.js +127 -0
  77. package/dist/utils/compatibility.d.ts +2 -1
  78. package/dist/utils/compatibility.js +2 -2
  79. package/dist/utils/config-store.d.ts +35 -0
  80. package/dist/utils/config-store.js +125 -0
  81. package/dist/utils/connectivity.js +7 -3
  82. package/dist/utils/expo.js +14 -3
  83. package/dist/utils/orgs.d.ts +11 -0
  84. package/dist/utils/orgs.js +40 -0
  85. package/dist/utils/paths.d.ts +11 -0
  86. package/dist/utils/paths.js +24 -0
  87. package/dist/utils/progress.d.ts +13 -0
  88. package/dist/utils/progress.js +50 -0
  89. package/dist/utils/styling.d.ts +13 -5
  90. package/dist/utils/styling.js +37 -7
  91. package/package.json +26 -38
  92. package/bin/dev.cmd +0 -3
  93. package/bin/dev.js +0 -6
  94. package/bin/run.cmd +0 -3
  95. package/bin/run.js +0 -7
  96. package/dist/types/schema.types.d.ts +0 -2702
  97. package/oclif.manifest.json +0 -884
@@ -0,0 +1,513 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.liveCommand = void 0;
4
+ const citty_1 = require("citty");
5
+ const node_fs_1 = require("node:fs");
6
+ const environments_1 = require("../config/environments");
7
+ const api_flags_1 = require("../config/flags/api.flags");
8
+ const api_gateway_1 = require("../gateways/api-gateway");
9
+ const auth_1 = require("../utils/auth");
10
+ const cli_1 = require("../utils/cli");
11
+ const config_store_1 = require("../utils/config-store");
12
+ const styling_1 = require("../utils/styling");
13
+ const PLATFORM_OPTIONS = ['android', 'ios'];
14
+ const READY_TIMEOUT_MS = 180_000;
15
+ const READY_POLL_MS = 2_000;
16
+ async function requireAuth(keyFlag) {
17
+ return (0, auth_1.resolveAuth)({ apiKeyFlag: keyFlag });
18
+ }
19
+ function sleep(ms) {
20
+ return new Promise((resolve) => setTimeout(resolve, ms));
21
+ }
22
+ /**
23
+ * Poll the session until it reports `ready` (RUNNING + device streaming),
24
+ * surfacing each new device phase so the user sees "Cloning device…",
25
+ * "Installing app…", etc. Removes the need to hand-roll retry loops around the
26
+ * 425/409 "not ready" window that `exec` returns right after start/install.
27
+ */
28
+ async function waitForReady(apiUrl, auth, sessionName, timeoutMs = READY_TIMEOUT_MS) {
29
+ const deadline = Date.now() + timeoutMs;
30
+ let lastPhase = '';
31
+ for (;;) {
32
+ const session = await api_gateway_1.ApiGateway.getLiveSession(apiUrl, auth, sessionName);
33
+ if (session.ready)
34
+ return session;
35
+ if (['CANCELLED', 'FAILED', 'STOPPED'].includes(session.status?.toUpperCase?.() ?? '')) {
36
+ throw new cli_1.CliError(`Session ${sessionName} is ${session.status}; cannot become ready.`);
37
+ }
38
+ const phase = session.device_state?.phase_label ?? session.device_state?.phase ?? session.status;
39
+ if (phase && phase !== lastPhase) {
40
+ lastPhase = phase;
41
+ cli_1.logger.log(` ${styling_1.colors.dim(phase)}`);
42
+ }
43
+ if (Date.now() >= deadline) {
44
+ throw new cli_1.CliError(`Timed out after ${Math.round(timeoutMs / 1000)}s waiting for the device to be ready` +
45
+ (phase ? ` (last phase: ${phase})` : '') +
46
+ '.');
47
+ }
48
+ await sleep(READY_POLL_MS);
49
+ }
50
+ }
51
+ /**
52
+ * Strip a Maestro flow header (`appId: ...` config block up to and including the
53
+ * first `---`) so a whole flow file can be fed to the exec endpoint, which wants
54
+ * just the command list. Returns the input unchanged if there's no `---`.
55
+ */
56
+ function extractFlowCommands(text) {
57
+ const lines = text.split(/\r?\n/);
58
+ const idx = lines.findIndex((l) => l.trim() === '---');
59
+ if (idx === -1)
60
+ return text.trim();
61
+ return lines.slice(idx + 1).join('\n').trim();
62
+ }
63
+ /**
64
+ * citty/mri silently drops the value after `--flag` when that value starts with
65
+ * `-` (e.g. `--yaml "- launchApp"`) for non-required string args. Recover it
66
+ * from the raw argv so the ergonomic inline form keeps working.
67
+ */
68
+ function recoverFlagValue(flag, parsed, rawArgs) {
69
+ if (parsed && parsed.trim())
70
+ return parsed;
71
+ const eq = rawArgs.find((a) => a.startsWith(`${flag}=`));
72
+ if (eq)
73
+ return eq.slice(flag.length + 1);
74
+ const idx = rawArgs.indexOf(flag);
75
+ if (idx !== -1 && idx + 1 < rawArgs.length)
76
+ return rawArgs[idx + 1];
77
+ return parsed;
78
+ }
79
+ function readFlowFile(filePath) {
80
+ try {
81
+ return (0, node_fs_1.readFileSync)(filePath, 'utf8');
82
+ }
83
+ catch (err) {
84
+ throw new cli_1.CliError(`Could not read flow file '${filePath}': ${err.message}`);
85
+ }
86
+ }
87
+ function printExecResult(result) {
88
+ cli_1.logger.log(result.success
89
+ ? `${styling_1.symbols.success} Command executed successfully`
90
+ : `${styling_1.symbols.error} Command failed`);
91
+ if (result.output) {
92
+ cli_1.logger.log((0, styling_1.sectionHeader)('Output'));
93
+ cli_1.logger.log(result.output);
94
+ }
95
+ if (result.error) {
96
+ cli_1.logger.log((0, styling_1.sectionHeader)('Error'));
97
+ cli_1.logger.log(styling_1.colors.error(result.error));
98
+ }
99
+ }
100
+ const startSub = (0, citty_1.defineCommand)({
101
+ meta: { name: 'start', description: 'Start a new live device session' },
102
+ args: {
103
+ ...api_flags_1.apiFlags,
104
+ platform: {
105
+ type: 'string',
106
+ default: 'android',
107
+ description: 'Device platform (options: android, ios)',
108
+ },
109
+ 'app-binary-id': {
110
+ type: 'string',
111
+ description: 'Binary upload ID to install on the device',
112
+ },
113
+ 'device-locale': {
114
+ type: 'string',
115
+ description: 'Device locale, ISO-639-1 + ISO-3166-1, e.g. "de_DE"',
116
+ },
117
+ 'android-device': {
118
+ type: 'string',
119
+ description: '[Android] Device profile to match a cloud run (options: pixel-6, pixel-6-pro, pixel-7, pixel-7-pro)',
120
+ },
121
+ 'android-api-level': {
122
+ type: 'string',
123
+ description: '[Android] API level for the device, e.g. 34 (requires --android-device)',
124
+ },
125
+ wait: {
126
+ type: 'boolean',
127
+ default: false,
128
+ description: 'Block until the device is ready to accept commands',
129
+ },
130
+ },
131
+ async run({ args }) {
132
+ const auth = await requireAuth(args['api-key']);
133
+ const apiUrl = (0, config_store_1.resolveApiUrl)(args['api-url']);
134
+ const platform = (0, cli_1.validateEnum)(args.platform, PLATFORM_OPTIONS, 'platform');
135
+ const binaryId = args['app-binary-id'];
136
+ const deviceLocale = args['device-locale'];
137
+ const androidDevice = args['android-device'];
138
+ const androidApiLevel = args['android-api-level'];
139
+ if ((androidDevice || androidApiLevel) && platform !== 'android') {
140
+ throw new cli_1.CliError('--android-device/--android-api-level are only valid with --platform android.');
141
+ }
142
+ if (Boolean(androidDevice) !== Boolean(androidApiLevel)) {
143
+ throw new cli_1.CliError('--android-device and --android-api-level must be provided together.');
144
+ }
145
+ cli_1.logger.log(`${styling_1.symbols.running} Starting ${platform} live session...`);
146
+ const session = await api_gateway_1.ApiGateway.startLiveSession(apiUrl, auth, {
147
+ binaryUploadId: binaryId,
148
+ deviceLocale,
149
+ platform,
150
+ androidDevice,
151
+ androidApiLevel,
152
+ });
153
+ const frontendUrl = (0, environments_1.resolveFrontendUrl)(apiUrl);
154
+ cli_1.logger.log(`${styling_1.symbols.success} Live session started`);
155
+ cli_1.logger.log(` ${styling_1.colors.dim('Session:')} ${styling_1.colors.highlight(session.session_name)}`);
156
+ cli_1.logger.log(` ${styling_1.colors.dim('Platform:')} ${session.platform}`);
157
+ cli_1.logger.log(` ${styling_1.colors.dim('Status:')} ${session.status}`);
158
+ cli_1.logger.log(` ${styling_1.colors.dim('Console:')} ${styling_1.colors.highlight(`${frontendUrl}/live?session=${session.session_name}`)}`);
159
+ if (args.wait) {
160
+ cli_1.logger.log('');
161
+ cli_1.logger.log(`${styling_1.symbols.running} Waiting for the device to be ready...`);
162
+ const ready = await waitForReady(apiUrl, auth, session.session_name);
163
+ cli_1.logger.log(`${styling_1.symbols.success} Device ready${ready.device_model ? ` (${ready.device_model})` : ''}`);
164
+ }
165
+ cli_1.logger.log('');
166
+ cli_1.logger.log(` ${styling_1.colors.dim('Install a binary:')} ${styling_1.colors.highlight(`dcd live install --session ${session.session_name} --app-binary-id <id>`)}`);
167
+ cli_1.logger.log(` ${styling_1.colors.dim('Run a flow:')} ${styling_1.colors.highlight(`dcd live run --session ${session.session_name} path/to/flow.yaml`)}`);
168
+ cli_1.logger.log(` ${styling_1.colors.dim('Inspect screen:')} ${styling_1.colors.highlight(`dcd live hierarchy --session ${session.session_name}`)}`);
169
+ cli_1.logger.log(` ${styling_1.colors.dim('Stop session:')} ${styling_1.colors.highlight(`dcd live stop --session ${session.session_name}`)}`);
170
+ },
171
+ });
172
+ const installSub = (0, citty_1.defineCommand)({
173
+ meta: { name: 'install', description: 'Install a binary on the device' },
174
+ args: {
175
+ ...api_flags_1.apiFlags,
176
+ session: { type: 'string', required: true, description: 'Live session name' },
177
+ 'app-binary-id': {
178
+ type: 'string',
179
+ required: true,
180
+ description: 'Binary upload ID to install on the device',
181
+ },
182
+ wait: {
183
+ type: 'boolean',
184
+ default: false,
185
+ description: 'Block until the device is ready again after installing',
186
+ },
187
+ },
188
+ async run({ args }) {
189
+ const auth = await requireAuth(args['api-key']);
190
+ const apiUrl = (0, config_store_1.resolveApiUrl)(args['api-url']);
191
+ const sessionName = args.session;
192
+ const binaryId = args['app-binary-id'];
193
+ cli_1.logger.log(`${styling_1.symbols.running} Installing binary ${styling_1.colors.highlight(binaryId)} on session ${styling_1.colors.highlight(sessionName)}...`);
194
+ await api_gateway_1.ApiGateway.installLiveBinary(apiUrl, auth, sessionName, binaryId);
195
+ cli_1.logger.log(`${styling_1.symbols.success} Binary installed successfully`);
196
+ if (args.wait) {
197
+ cli_1.logger.log(`${styling_1.symbols.running} Waiting for the device to be ready...`);
198
+ await waitForReady(apiUrl, auth, sessionName);
199
+ cli_1.logger.log(`${styling_1.symbols.success} Device ready`);
200
+ }
201
+ },
202
+ });
203
+ const execSub = (0, citty_1.defineCommand)({
204
+ meta: { name: 'exec', description: 'Execute Maestro YAML commands' },
205
+ args: {
206
+ ...api_flags_1.apiFlags,
207
+ session: { type: 'string', required: true, description: 'Live session name' },
208
+ yaml: { type: 'string', description: 'Maestro YAML commands to execute' },
209
+ file: {
210
+ type: 'string',
211
+ description: 'Path to a file containing the Maestro YAML to execute',
212
+ },
213
+ wait: {
214
+ type: 'boolean',
215
+ default: false,
216
+ description: 'Wait for the device to be ready before executing',
217
+ },
218
+ },
219
+ async run({ args, rawArgs }) {
220
+ const auth = await requireAuth(args['api-key']);
221
+ const apiUrl = (0, config_store_1.resolveApiUrl)(args['api-url']);
222
+ const sessionName = args.session;
223
+ const inlineYaml = recoverFlagValue('--yaml', args.yaml, rawArgs);
224
+ const file = args.file;
225
+ if (inlineYaml && file) {
226
+ throw new cli_1.CliError('Pass either --yaml or --file, not both.');
227
+ }
228
+ const yaml = file ? readFlowFile(file) : inlineYaml;
229
+ if (!yaml || !yaml.trim()) {
230
+ throw new cli_1.CliError('Provide commands to execute via --yaml or --file.');
231
+ }
232
+ if (args.wait) {
233
+ await waitForReady(apiUrl, auth, sessionName);
234
+ }
235
+ cli_1.logger.log(`${styling_1.symbols.running} Executing commands on session ${styling_1.colors.highlight(sessionName)}...`);
236
+ const result = await api_gateway_1.ApiGateway.execLiveYaml(apiUrl, auth, sessionName, yaml);
237
+ printExecResult(result);
238
+ },
239
+ });
240
+ const runSub = (0, citty_1.defineCommand)({
241
+ meta: {
242
+ name: 'run',
243
+ description: 'Run a whole Maestro flow file against the live session',
244
+ },
245
+ args: {
246
+ ...api_flags_1.apiFlags,
247
+ session: { type: 'string', required: true, description: 'Live session name' },
248
+ wait: {
249
+ type: 'boolean',
250
+ default: false,
251
+ description: 'Wait for the device to be ready before running',
252
+ },
253
+ timeout: {
254
+ type: 'string',
255
+ default: '600',
256
+ description: 'Max seconds to wait for the flow to finish (default 600)',
257
+ },
258
+ flowFile: {
259
+ type: 'positional',
260
+ required: true,
261
+ description: 'Path to the Maestro flow file to run (e.g. flow.yaml)',
262
+ },
263
+ },
264
+ async run({ args }) {
265
+ const auth = await requireAuth(args['api-key']);
266
+ const apiUrl = (0, config_store_1.resolveApiUrl)(args['api-url']);
267
+ const sessionName = args.session;
268
+ const flowFile = args.flowFile;
269
+ const timeoutMs = Math.max(1, Number(args.timeout) || 600) * 1000;
270
+ const commands = extractFlowCommands(readFlowFile(flowFile));
271
+ if (!commands) {
272
+ throw new cli_1.CliError(`Flow file '${flowFile}' has no commands to run.`);
273
+ }
274
+ if (args.wait) {
275
+ await waitForReady(apiUrl, auth, sessionName);
276
+ }
277
+ cli_1.logger.log(`${styling_1.symbols.running} Running ${styling_1.colors.highlight(flowFile)} on session ${styling_1.colors.highlight(sessionName)}...`);
278
+ // Submit asynchronously and poll, so a long flow isn't capped by the
279
+ // server's 120s synchronous exec limit. Older servers ignore `async` and
280
+ // return the full result inline — handle that by falling through.
281
+ const submitted = await api_gateway_1.ApiGateway.execLiveYaml(apiUrl, auth, sessionName, commands, {
282
+ async: true,
283
+ });
284
+ if (!submitted.commandId) {
285
+ printExecResult(submitted);
286
+ if (!submitted.success)
287
+ throw new cli_1.CliError(submitted.error || 'Flow failed.', 2);
288
+ return;
289
+ }
290
+ const commandId = submitted.commandId;
291
+ const deadline = Date.now() + timeoutMs;
292
+ let lastKeepalive = Date.now();
293
+ for (;;) {
294
+ const status = await api_gateway_1.ApiGateway.getLiveCommand(apiUrl, auth, sessionName, commandId);
295
+ if (status.done) {
296
+ printExecResult(status);
297
+ if (!status.success)
298
+ throw new cli_1.CliError(status.error || 'Flow failed.', 2);
299
+ return;
300
+ }
301
+ if (Date.now() >= deadline) {
302
+ throw new cli_1.CliError(`Flow still running after ${Math.round(timeoutMs / 1000)}s (command ${commandId}). ` +
303
+ 'Raise --timeout if the flow legitimately runs longer.', 2);
304
+ }
305
+ // Keep the session alive during long polls so the inactivity sweep
306
+ // doesn't cancel it mid-flow.
307
+ if (Date.now() - lastKeepalive > 60_000) {
308
+ await api_gateway_1.ApiGateway.keepaliveLiveSession(apiUrl, auth, sessionName);
309
+ lastKeepalive = Date.now();
310
+ }
311
+ await sleep(2_000);
312
+ }
313
+ },
314
+ });
315
+ const screenshotSub = (0, citty_1.defineCommand)({
316
+ meta: {
317
+ name: 'screenshot',
318
+ description: 'Save the current device screen to an image file (PNG/JPEG)',
319
+ },
320
+ args: {
321
+ ...api_flags_1.apiFlags,
322
+ session: { type: 'string', required: true, description: 'Live session name' },
323
+ output: {
324
+ type: 'string',
325
+ alias: ['o'],
326
+ description: 'File path to write the screenshot to (default: live-screenshot.<ext> matching the device format)',
327
+ },
328
+ },
329
+ async run({ args }) {
330
+ const auth = await requireAuth(args['api-key']);
331
+ const apiUrl = (0, config_store_1.resolveApiUrl)(args['api-url']);
332
+ const sessionName = args.session;
333
+ const session = await api_gateway_1.ApiGateway.getLiveSession(apiUrl, auth, sessionName);
334
+ const dataUrl = session.screenshot_data_url ?? session.hierarchy?.screenshot;
335
+ if (!dataUrl) {
336
+ throw new cli_1.CliError(`No screenshot available yet for ${sessionName}. The device may not be streaming — ` +
337
+ 'try again, or start/install with --wait.');
338
+ }
339
+ const match = /^data:image\/(\w+);base64,(.+)$/s.exec(dataUrl);
340
+ const ext = match ? match[1].replace('jpeg', 'jpg') : 'png';
341
+ const base64 = match ? match[2] : dataUrl;
342
+ const output = args.output ?? `live-screenshot.${ext}`;
343
+ (0, node_fs_1.writeFileSync)(output, Buffer.from(base64, 'base64'));
344
+ cli_1.logger.log(`${styling_1.symbols.success} Screenshot saved to ${styling_1.colors.highlight(output)}`);
345
+ if (session.hierarchy) {
346
+ cli_1.logger.log(` ${styling_1.colors.dim('Resolution:')} ${session.hierarchy.width}x${session.hierarchy.height}`);
347
+ }
348
+ },
349
+ });
350
+ const hierarchySub = (0, citty_1.defineCommand)({
351
+ meta: {
352
+ name: 'hierarchy',
353
+ description: 'Dump the current view hierarchy (the selectors you can tap/assert on)',
354
+ },
355
+ args: {
356
+ ...api_flags_1.apiFlags,
357
+ session: { type: 'string', required: true, description: 'Live session name' },
358
+ json: { type: 'boolean', description: 'Output the raw hierarchy as JSON' },
359
+ output: {
360
+ type: 'string',
361
+ alias: ['o'],
362
+ description: 'Write the output to a file instead of stdout',
363
+ },
364
+ },
365
+ async run({ args }) {
366
+ const auth = await requireAuth(args['api-key']);
367
+ const apiUrl = (0, config_store_1.resolveApiUrl)(args['api-url']);
368
+ const sessionName = args.session;
369
+ const json = Boolean(args.json);
370
+ const output = args.output;
371
+ const session = await api_gateway_1.ApiGateway.getLiveSession(apiUrl, auth, sessionName);
372
+ const hierarchy = session.hierarchy;
373
+ if (!hierarchy) {
374
+ throw new cli_1.CliError(`No hierarchy available yet for ${sessionName}. The device may not be streaming — ` +
375
+ 'try again, or start/install with --wait.');
376
+ }
377
+ if (json) {
378
+ const out = JSON.stringify(hierarchy, null, 2);
379
+ if (output) {
380
+ (0, node_fs_1.writeFileSync)(output, out);
381
+ cli_1.logger.log(`${styling_1.symbols.success} Hierarchy written to ${styling_1.colors.highlight(output)}`);
382
+ }
383
+ else {
384
+ // eslint-disable-next-line no-console
385
+ console.log(out);
386
+ }
387
+ return;
388
+ }
389
+ // Human-readable: only the nodes worth selecting on (have text / a11y / id).
390
+ const lines = [];
391
+ lines.push(`Hierarchy — ${hierarchy.width}x${hierarchy.height}, ${hierarchy.elements.length} elements`);
392
+ for (const el of hierarchy.elements) {
393
+ const parts = [];
394
+ if (el.text)
395
+ parts.push(`"${el.text}"`);
396
+ if (el.accessibilityText)
397
+ parts.push(`a11y:"${el.accessibilityText}"`);
398
+ if (el.resourceId)
399
+ parts.push(`id:${el.resourceId}`);
400
+ if (parts.length === 0)
401
+ continue;
402
+ const bounds = el.bounds
403
+ ? ` ${styling_1.colors.dim(`(${el.bounds.x},${el.bounds.y} ${el.bounds.width}x${el.bounds.height})`)}`
404
+ : '';
405
+ lines.push(` ${parts.join(' ')}${bounds}`);
406
+ }
407
+ const out = lines.join('\n');
408
+ if (output) {
409
+ (0, node_fs_1.writeFileSync)(output, `${out}\n`);
410
+ cli_1.logger.log(`${styling_1.symbols.success} Hierarchy written to ${styling_1.colors.highlight(output)}`);
411
+ }
412
+ else {
413
+ cli_1.logger.log(out);
414
+ }
415
+ },
416
+ });
417
+ const stopSub = (0, citty_1.defineCommand)({
418
+ meta: { name: 'stop', description: 'Stop a live session' },
419
+ args: {
420
+ ...api_flags_1.apiFlags,
421
+ session: { type: 'string', required: true, description: 'Live session name' },
422
+ },
423
+ async run({ args }) {
424
+ const auth = await requireAuth(args['api-key']);
425
+ const apiUrl = (0, config_store_1.resolveApiUrl)(args['api-url']);
426
+ const sessionName = args.session;
427
+ cli_1.logger.log(`${styling_1.symbols.running} Stopping session ${styling_1.colors.highlight(sessionName)}...`);
428
+ await api_gateway_1.ApiGateway.stopLiveSession(apiUrl, auth, sessionName);
429
+ cli_1.logger.log(`${styling_1.symbols.success} Session stopped`);
430
+ },
431
+ });
432
+ const statusSub = (0, citty_1.defineCommand)({
433
+ meta: { name: 'status', description: 'Get session status' },
434
+ args: {
435
+ ...api_flags_1.apiFlags,
436
+ session: { type: 'string', required: true, description: 'Live session name' },
437
+ },
438
+ async run({ args }) {
439
+ const auth = await requireAuth(args['api-key']);
440
+ const apiUrl = (0, config_store_1.resolveApiUrl)(args['api-url']);
441
+ const sessionName = args.session;
442
+ const session = await api_gateway_1.ApiGateway.getLiveSession(apiUrl, auth, sessionName);
443
+ cli_1.logger.log((0, styling_1.sectionHeader)('Live Session'));
444
+ cli_1.logger.log(` ${styling_1.colors.dim('Session:')} ${styling_1.colors.highlight(session.session_name)}`);
445
+ cli_1.logger.log(` ${styling_1.colors.dim('Platform:')} ${session.platform}`);
446
+ cli_1.logger.log(` ${styling_1.colors.dim('Status:')} ${session.status}`);
447
+ cli_1.logger.log(` ${styling_1.colors.dim('Ready:')} ${session.ready ? styling_1.colors.success('yes') : styling_1.colors.warning('no')}`);
448
+ const phase = session.device_state?.phase_label ?? session.device_state?.phase;
449
+ if (phase) {
450
+ cli_1.logger.log(` ${styling_1.colors.dim('Phase:')} ${phase}`);
451
+ }
452
+ if (session.device_model) {
453
+ cli_1.logger.log(` ${styling_1.colors.dim('Device:')} ${session.device_model}`);
454
+ }
455
+ if (session.device_locale) {
456
+ cli_1.logger.log(` ${styling_1.colors.dim('Locale:')} ${session.device_locale}`);
457
+ }
458
+ if (session.binary_upload_id) {
459
+ cli_1.logger.log(` ${styling_1.colors.dim('Binary:')} ${session.binary_upload_id}`);
460
+ }
461
+ if (typeof session.seconds_until_auto_cancel === 'number') {
462
+ cli_1.logger.log(` ${styling_1.colors.dim('Auto-cancel in:')} ${session.seconds_until_auto_cancel}s`);
463
+ }
464
+ cli_1.logger.log(` ${styling_1.colors.dim('Created:')} ${new Date(session.created_at).toLocaleString()}`);
465
+ },
466
+ });
467
+ exports.liveCommand = (0, citty_1.defineCommand)({
468
+ meta: {
469
+ name: 'live',
470
+ description: 'Start and interact with a live device session',
471
+ },
472
+ subCommands: {
473
+ start: startSub,
474
+ install: installSub,
475
+ exec: execSub,
476
+ run: runSub,
477
+ screenshot: screenshotSub,
478
+ hierarchy: hierarchySub,
479
+ stop: stopSub,
480
+ status: statusSub,
481
+ },
482
+ // citty's runCommand does not early-return after dispatching to a
483
+ // subcommand — it still invokes the parent `run` afterwards. So when a
484
+ // subcommand was matched, bail out here; otherwise the menu would print
485
+ // after every successful subcommand.
486
+ run({ rawArgs }) {
487
+ const subNames = new Set([
488
+ 'start',
489
+ 'install',
490
+ 'exec',
491
+ 'run',
492
+ 'screenshot',
493
+ 'hierarchy',
494
+ 'stop',
495
+ 'status',
496
+ ]);
497
+ const firstPositional = rawArgs.find((arg) => !arg.startsWith('-'));
498
+ if (firstPositional && subNames.has(firstPositional))
499
+ return;
500
+ cli_1.logger.log((0, styling_1.sectionHeader)('Live Session Commands'));
501
+ cli_1.logger.log(` ${styling_1.colors.bold('start')} Start a new live device session`);
502
+ cli_1.logger.log(` ${styling_1.colors.bold('install')} Install a binary on the device`);
503
+ cli_1.logger.log(` ${styling_1.colors.bold('exec')} Execute Maestro YAML commands`);
504
+ cli_1.logger.log(` ${styling_1.colors.bold('run')} Run a whole Maestro flow file`);
505
+ cli_1.logger.log(` ${styling_1.colors.bold('screenshot')} Save the current device screen to an image file`);
506
+ cli_1.logger.log(` ${styling_1.colors.bold('hierarchy')} Dump the current view hierarchy (selectors)`);
507
+ cli_1.logger.log(` ${styling_1.colors.bold('stop')} Stop a live session`);
508
+ cli_1.logger.log(` ${styling_1.colors.bold('status')} Get session status`);
509
+ cli_1.logger.log('');
510
+ cli_1.logger.log(` Run ${styling_1.colors.highlight('dcd live <command> --help')} for details`);
511
+ },
512
+ });
513
+ exports.default = exports.liveCommand;
@@ -0,0 +1,17 @@
1
+ export declare const loginCommand: import("citty").CommandDef<{
2
+ 'api-url': {
3
+ type: "string";
4
+ default: string;
5
+ description: string;
6
+ };
7
+ 'frontend-url': {
8
+ type: "string";
9
+ description: string;
10
+ };
11
+ browser: {
12
+ type: "boolean";
13
+ default: true;
14
+ description: string;
15
+ };
16
+ }>;
17
+ export default loginCommand;