@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.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +52 -0
  3. package/dist/commands/artifacts.d.ts +28 -28
  4. package/dist/commands/artifacts.js +20 -23
  5. package/dist/commands/cloud.d.ts +57 -57
  6. package/dist/commands/cloud.js +224 -192
  7. package/dist/commands/list.d.ts +22 -22
  8. package/dist/commands/list.js +43 -40
  9. package/dist/commands/live.js +134 -127
  10. package/dist/commands/login.d.ts +11 -11
  11. package/dist/commands/login.js +46 -44
  12. package/dist/commands/logout.js +16 -18
  13. package/dist/commands/status.d.ts +11 -11
  14. package/dist/commands/status.js +53 -44
  15. package/dist/commands/switch-org.d.ts +7 -7
  16. package/dist/commands/switch-org.js +19 -21
  17. package/dist/commands/upgrade.js +41 -33
  18. package/dist/commands/upload.d.ts +10 -10
  19. package/dist/commands/upload.js +42 -43
  20. package/dist/commands/whoami.js +17 -20
  21. package/dist/config/environments.js +6 -12
  22. package/dist/config/flags/api.flags.js +1 -4
  23. package/dist/config/flags/binary.flags.js +1 -4
  24. package/dist/config/flags/device.flags.js +6 -9
  25. package/dist/config/flags/environment.flags.js +1 -4
  26. package/dist/config/flags/execution.flags.js +1 -4
  27. package/dist/config/flags/github.flags.js +1 -4
  28. package/dist/config/flags/output.flags.js +1 -4
  29. package/dist/constants.js +15 -18
  30. package/dist/gateways/api-gateway.d.ts +31 -6
  31. package/dist/gateways/api-gateway.js +70 -16
  32. package/dist/gateways/cli-auth-gateway.d.ts +1 -1
  33. package/dist/gateways/cli-auth-gateway.js +3 -6
  34. package/dist/gateways/realtime-gateway.d.ts +32 -0
  35. package/dist/gateways/realtime-gateway.js +103 -0
  36. package/dist/gateways/supabase-gateway.d.ts +1 -1
  37. package/dist/gateways/supabase-gateway.js +10 -14
  38. package/dist/index.js +41 -38
  39. package/dist/mcp/context.d.ts +33 -0
  40. package/dist/mcp/context.js +33 -0
  41. package/dist/mcp/helpers.d.ts +16 -0
  42. package/dist/mcp/helpers.js +34 -0
  43. package/dist/mcp/index.d.ts +2 -0
  44. package/dist/mcp/index.js +24 -0
  45. package/dist/mcp/server.d.ts +7 -0
  46. package/dist/mcp/server.js +27 -0
  47. package/dist/mcp/tools/download-artifacts.d.ts +11 -0
  48. package/dist/mcp/tools/download-artifacts.js +84 -0
  49. package/dist/mcp/tools/get-status.d.ts +7 -0
  50. package/dist/mcp/tools/get-status.js +39 -0
  51. package/dist/mcp/tools/list-devices.d.ts +7 -0
  52. package/dist/mcp/tools/list-devices.js +27 -0
  53. package/dist/mcp/tools/list-runs.d.ts +3 -0
  54. package/dist/mcp/tools/list-runs.js +60 -0
  55. package/dist/mcp/tools/run-cloud-test.d.ts +14 -0
  56. package/dist/mcp/tools/run-cloud-test.js +233 -0
  57. package/dist/methods.d.ts +32 -1
  58. package/dist/methods.js +133 -79
  59. package/dist/services/device-validation.service.d.ts +1 -1
  60. package/dist/services/device-validation.service.js +1 -5
  61. package/dist/services/execution-plan.service.js +14 -17
  62. package/dist/services/execution-plan.utils.js +15 -23
  63. package/dist/services/flow-paths.d.ts +17 -0
  64. package/dist/services/flow-paths.js +52 -0
  65. package/dist/services/metadata-extractor.service.js +22 -25
  66. package/dist/services/moropo.service.js +18 -20
  67. package/dist/services/report-download.service.d.ts +1 -1
  68. package/dist/services/report-download.service.js +5 -9
  69. package/dist/services/results-polling.service.d.ts +18 -3
  70. package/dist/services/results-polling.service.js +211 -108
  71. package/dist/services/telemetry.service.d.ts +10 -1
  72. package/dist/services/telemetry.service.js +40 -18
  73. package/dist/services/test-submission.service.d.ts +21 -4
  74. package/dist/services/test-submission.service.js +51 -34
  75. package/dist/services/version.service.d.ts +30 -7
  76. package/dist/services/version.service.js +88 -32
  77. package/dist/types/domain/auth.types.d.ts +8 -0
  78. package/dist/types/domain/auth.types.js +1 -2
  79. package/dist/types/domain/device.types.js +8 -11
  80. package/dist/types/domain/live.types.js +1 -2
  81. package/dist/types/generated/schema.types.js +1 -2
  82. package/dist/types/index.d.ts +2 -2
  83. package/dist/types/index.js +2 -18
  84. package/dist/types.js +1 -2
  85. package/dist/utils/auth.d.ts +1 -1
  86. package/dist/utils/auth.js +27 -28
  87. package/dist/utils/ci.d.ts +12 -0
  88. package/dist/utils/ci.js +39 -0
  89. package/dist/utils/cli.d.ts +16 -2
  90. package/dist/utils/cli.js +57 -29
  91. package/dist/utils/compatibility.d.ts +1 -1
  92. package/dist/utils/compatibility.js +5 -7
  93. package/dist/utils/config-store.js +33 -43
  94. package/dist/utils/connectivity.js +1 -4
  95. package/dist/utils/expo.js +15 -21
  96. package/dist/utils/orgs.js +8 -12
  97. package/dist/utils/paths.js +2 -5
  98. package/dist/utils/progress.d.ts +3 -0
  99. package/dist/utils/progress.js +47 -8
  100. package/dist/utils/styling.d.ts +35 -37
  101. package/dist/utils/styling.js +52 -86
  102. package/dist/utils/ui.d.ts +41 -0
  103. package/dist/utils/ui.js +95 -0
  104. package/package.json +27 -24
@@ -1,20 +1,18 @@
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");
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 (0, auth_1.resolveAuth)({ apiKeyFlag: keyFlag });
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 api_gateway_1.ApiGateway.getLiveSession(apiUrl, auth, sessionName);
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 cli_1.CliError(`Session ${sessionName} is ${session.status}; cannot become ready.`);
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
- cli_1.logger.log(` ${styling_1.colors.dim(phase)}`);
39
+ logger.log(` ${colors.dim(phase)}`);
42
40
  }
43
41
  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` +
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 (0, node_fs_1.readFileSync)(filePath, 'utf8');
79
+ return readFileSync(filePath, 'utf8');
82
80
  }
83
81
  catch (err) {
84
- throw new cli_1.CliError(`Could not read flow file '${filePath}': ${err.message}`);
82
+ throw new CliError(`Could not read flow file '${filePath}': ${err.message}`);
85
83
  }
86
84
  }
87
85
  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`);
86
+ logger.log(result.success
87
+ ? ui.success('Command executed successfully')
88
+ : `${ui.statusSymbol('FAILED')} Command failed`);
91
89
  if (result.output) {
92
- cli_1.logger.log((0, styling_1.sectionHeader)('Output'));
93
- cli_1.logger.log(result.output);
90
+ logger.log(ui.section('Output'));
91
+ logger.log(result.output);
94
92
  }
95
93
  if (result.error) {
96
- cli_1.logger.log((0, styling_1.sectionHeader)('Error'));
97
- cli_1.logger.log(styling_1.colors.error(result.error));
94
+ logger.log(ui.section('Error'));
95
+ logger.log(colors.error(result.error));
98
96
  }
99
97
  }
100
- const startSub = (0, citty_1.defineCommand)({
98
+ const startSub = defineCommand({
101
99
  meta: { name: 'start', description: 'Start a new live device session' },
102
100
  args: {
103
- ...api_flags_1.apiFlags,
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 = (0, config_store_1.resolveApiUrl)(args['api-url']);
134
- const platform = (0, cli_1.validateEnum)(args.platform, PLATFORM_OPTIONS, '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 cli_1.CliError('--android-device/--android-api-level are only valid with --platform android.');
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 cli_1.CliError('--android-device and --android-api-level must be provided together.');
141
+ throw new CliError('--android-device and --android-api-level must be provided together.');
144
142
  }
145
- cli_1.logger.log(`${styling_1.symbols.running} Starting ${platform} live session...`);
146
- const session = await api_gateway_1.ApiGateway.startLiveSession(apiUrl, auth, {
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 = (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}`)}`);
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
- cli_1.logger.log('');
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
- cli_1.logger.log(`${styling_1.symbols.success} Device ready${ready.device_model ? ` (${ready.device_model})` : ''}`);
162
+ logger.log(ui.success(`Device ready${ready.device_model ? ` (${ready.device_model})` : ''}`));
164
163
  }
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}`)}`);
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 = (0, citty_1.defineCommand)({
173
+ const installSub = defineCommand({
173
174
  meta: { name: 'install', description: 'Install a binary on the device' },
174
175
  args: {
175
- ...api_flags_1.apiFlags,
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 = (0, config_store_1.resolveApiUrl)(args['api-url']);
191
+ const apiUrl = resolveApiUrl(args['api-url']);
191
192
  const sessionName = args.session;
192
193
  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`);
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
- cli_1.logger.log(`${styling_1.symbols.running} Waiting for the device to be ready...`);
198
+ logger.log(ui.running('Waiting for the device to be ready…'));
198
199
  await waitForReady(apiUrl, auth, sessionName);
199
- cli_1.logger.log(`${styling_1.symbols.success} Device ready`);
200
+ logger.log(ui.success('Device ready'));
200
201
  }
201
202
  },
202
203
  });
203
- const execSub = (0, citty_1.defineCommand)({
204
+ const execSub = defineCommand({
204
205
  meta: { name: 'exec', description: 'Execute Maestro YAML commands' },
205
206
  args: {
206
- ...api_flags_1.apiFlags,
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 = (0, config_store_1.resolveApiUrl)(args['api-url']);
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 cli_1.CliError('Pass either --yaml or --file, not both.');
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 cli_1.CliError('Provide commands to execute via --yaml or --file.');
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
- 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);
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 = (0, citty_1.defineCommand)({
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
- ...api_flags_1.apiFlags,
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 = (0, config_store_1.resolveApiUrl)(args['api-url']);
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 cli_1.CliError(`Flow file '${flowFile}' has no commands to run.`);
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
- cli_1.logger.log(`${styling_1.symbols.running} Running ${styling_1.colors.highlight(flowFile)} on session ${styling_1.colors.highlight(sessionName)}...`);
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 api_gateway_1.ApiGateway.execLiveYaml(apiUrl, auth, sessionName, commands, {
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 cli_1.CliError(submitted.error || 'Flow failed.', 2);
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 api_gateway_1.ApiGateway.getLiveCommand(apiUrl, auth, sessionName, commandId);
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 cli_1.CliError(status.error || 'Flow failed.', 2);
299
+ throw new CliError(status.error || 'Flow failed.', 2);
299
300
  return;
300
301
  }
301
302
  if (Date.now() >= deadline) {
302
- throw new cli_1.CliError(`Flow still running after ${Math.round(timeoutMs / 1000)}s (command ${commandId}). ` +
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 api_gateway_1.ApiGateway.keepaliveLiveSession(apiUrl, auth, sessionName);
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 = (0, citty_1.defineCommand)({
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
- ...api_flags_1.apiFlags,
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 = (0, config_store_1.resolveApiUrl)(args['api-url']);
332
+ const apiUrl = resolveApiUrl(args['api-url']);
332
333
  const sessionName = args.session;
333
- const session = await api_gateway_1.ApiGateway.getLiveSession(apiUrl, auth, sessionName);
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 cli_1.CliError(`No screenshot available yet for ${sessionName}. The device may not be streaming — ` +
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
- (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)}`);
344
+ writeFileSync(output, Buffer.from(base64, 'base64'));
345
+ logger.log(ui.success(`Screenshot saved to ${colors.highlight(output)}`));
345
346
  if (session.hierarchy) {
346
- cli_1.logger.log(` ${styling_1.colors.dim('Resolution:')} ${session.hierarchy.width}x${session.hierarchy.height}`);
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 = (0, citty_1.defineCommand)({
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
- ...api_flags_1.apiFlags,
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 = (0, config_store_1.resolveApiUrl)(args['api-url']);
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 api_gateway_1.ApiGateway.getLiveSession(apiUrl, auth, sessionName);
374
+ const session = await ApiGateway.getLiveSession(apiUrl, auth, sessionName);
372
375
  const hierarchy = session.hierarchy;
373
376
  if (!hierarchy) {
374
- throw new cli_1.CliError(`No hierarchy available yet for ${sessionName}. The device may not be streaming — ` +
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
- (0, node_fs_1.writeFileSync)(output, out);
381
- cli_1.logger.log(`${styling_1.symbols.success} Hierarchy written to ${styling_1.colors.highlight(output)}`);
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
- ? ` ${styling_1.colors.dim(`(${el.bounds.x},${el.bounds.y} ${el.bounds.width}x${el.bounds.height})`)}`
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
- (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)}`);
412
+ writeFileSync(output, `${out}\n`);
413
+ logger.log(ui.success(`Hierarchy written to ${colors.highlight(output)}`));
411
414
  }
412
415
  else {
413
- cli_1.logger.log(out);
416
+ logger.log(out);
414
417
  }
415
418
  },
416
419
  });
417
- const stopSub = (0, citty_1.defineCommand)({
420
+ const stopSub = defineCommand({
418
421
  meta: { name: 'stop', description: 'Stop a live session' },
419
422
  args: {
420
- ...api_flags_1.apiFlags,
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 = (0, config_store_1.resolveApiUrl)(args['api-url']);
428
+ const apiUrl = resolveApiUrl(args['api-url']);
426
429
  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
+ 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 = (0, citty_1.defineCommand)({
435
+ const statusSub = defineCommand({
433
436
  meta: { name: 'status', description: 'Get session status' },
434
437
  args: {
435
- ...api_flags_1.apiFlags,
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 = (0, config_store_1.resolveApiUrl)(args['api-url']);
443
+ const apiUrl = resolveApiUrl(args['api-url']);
441
444
  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')}`);
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
- cli_1.logger.log(` ${styling_1.colors.dim('Phase:')} ${phase}`);
454
+ fields.push(['phase', phase]);
451
455
  }
452
456
  if (session.device_model) {
453
- cli_1.logger.log(` ${styling_1.colors.dim('Device:')} ${session.device_model}`);
457
+ fields.push(['device', session.device_model]);
454
458
  }
455
459
  if (session.device_locale) {
456
- cli_1.logger.log(` ${styling_1.colors.dim('Locale:')} ${session.device_locale}`);
460
+ fields.push(['locale', session.device_locale]);
457
461
  }
458
462
  if (session.binary_upload_id) {
459
- cli_1.logger.log(` ${styling_1.colors.dim('Binary:')} ${session.binary_upload_id}`);
463
+ fields.push(['binary', session.binary_upload_id]);
460
464
  }
461
465
  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`);
466
+ fields.push(['auto-cancel in', `${session.seconds_until_auto_cancel}s`]);
463
467
  }
464
- cli_1.logger.log(` ${styling_1.colors.dim('Created:')} ${new Date(session.created_at).toLocaleString()}`);
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
- exports.liveCommand = (0, citty_1.defineCommand)({
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
- 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`);
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
- exports.default = exports.liveCommand;
520
+ export default liveCommand;
@@ -1,17 +1,17 @@
1
1
  export declare const loginCommand: import("citty").CommandDef<{
2
- 'api-url': {
3
- type: "string";
4
- default: string;
5
- description: string;
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: string;
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: string;
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;