@devicecloud.dev/dcd 5.8.0-beta.3 → 5.9.0-beta.1

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.
@@ -33,6 +33,10 @@ export declare const artifactsCommand: import("citty").CommandDef<{
33
33
  readonly type: "string";
34
34
  readonly description: "Custom file path for downloaded JUnit report (default: ./report.xml)";
35
35
  };
36
+ readonly json: {
37
+ readonly type: "boolean";
38
+ readonly description: "Output the result as a single JSON object on stdout";
39
+ };
36
40
  readonly 'api-key': {
37
41
  readonly type: "string";
38
42
  readonly alias: ["apiKey"];
@@ -1,6 +1,7 @@
1
1
  import { defineCommand } from 'citty';
2
2
  import { apiFlags } from '../config/flags/api.flags.js';
3
- import { ReportDownloadService } from '../services/report-download.service.js';
3
+ import { jsonFlag } from '../config/flags/output.flags.js';
4
+ import { ReportDownloadService, } from '../services/report-download.service.js';
4
5
  import { resolveAuth } from '../utils/auth.js';
5
6
  import { CliError, logger, validateEnum } from '../utils/cli.js';
6
7
  import { resolveApiUrl } from '../utils/config-store.js';
@@ -47,6 +48,7 @@ export const artifactsCommand = defineCommand({
47
48
  type: 'string',
48
49
  description: 'Custom file path for downloaded JUnit report (default: ./report.xml)',
49
50
  },
51
+ json: jsonFlag,
50
52
  },
51
53
  async run({ args }) {
52
54
  const apiKeyFlag = args['api-key'];
@@ -59,6 +61,9 @@ export const artifactsCommand = defineCommand({
59
61
  const allurePath = args['allure-path'];
60
62
  const htmlPath = args['html-path'];
61
63
  const junitPath = args['junit-path'];
64
+ const json = Boolean(args.json);
65
+ // Success lines are dropped under --json; warnings stay on stderr.
66
+ const log = json ? undefined : (m) => logger.log(m);
62
67
  const auth = await resolveAuth({ apiKeyFlag });
63
68
  if (downloadArtifacts && report) {
64
69
  throw new CliError('--download-artifacts cannot also be provided when using --report (flags are mutually exclusive).');
@@ -67,31 +72,41 @@ export const artifactsCommand = defineCommand({
67
72
  throw new CliError('Either --download-artifacts or --report must be specified.');
68
73
  }
69
74
  const service = new ReportDownloadService();
75
+ const downloads = [];
70
76
  if (downloadArtifacts) {
71
- await service.downloadArtifacts({
77
+ downloads.push(await service.downloadArtifacts({
72
78
  auth,
73
79
  apiUrl,
74
80
  artifactsPath: artifactsPath ?? './artifacts.zip',
75
81
  debug,
76
82
  downloadType: downloadArtifacts,
77
- logger: (m) => logger.log(m),
83
+ logger: log,
78
84
  uploadId,
79
85
  warnLogger: (m) => logger.warn(m),
80
- });
86
+ }));
81
87
  }
82
88
  if (report) {
83
- await service.downloadReports({
89
+ const result = await service.downloadReports({
84
90
  allurePath,
85
91
  auth,
86
92
  apiUrl,
87
93
  debug,
88
94
  htmlPath,
89
95
  junitPath,
90
- logger: (m) => logger.log(m),
96
+ logger: log,
91
97
  reportType: report,
92
98
  uploadId,
93
99
  warnLogger: (m) => logger.warn(m),
94
100
  });
101
+ if (result)
102
+ downloads.push(result);
103
+ }
104
+ if (json) {
105
+ const ok = downloads.some((d) => d.ok);
106
+ logger.json({ status: ok ? 'SUCCESS' : 'FAILED', uploadId, downloads });
107
+ // Same exit code a thrown error would give, without a second JSON object.
108
+ if (!ok)
109
+ logger.fail('Every download failed', 1);
95
110
  }
96
111
  },
97
112
  });
@@ -751,8 +751,7 @@ export const cloudCommand = defineCommand({
751
751
  });
752
752
  }
753
753
  if (json) {
754
- // eslint-disable-next-line no-console
755
- console.log(JSON.stringify(jsonOutput, null, 2));
754
+ logger.json(jsonOutput);
756
755
  return;
757
756
  }
758
757
  out(ui.info('Not waiting for results as async flag is set to true'));
@@ -874,8 +873,7 @@ export const cloudCommand = defineCommand({
874
873
  await fsp.rm(p, { recursive: true, force: true }).catch(() => { });
875
874
  }
876
875
  if (output) {
877
- // eslint-disable-next-line no-console
878
- console.log(JSON.stringify(output, null, 2));
876
+ logger.json(output);
879
877
  }
880
878
  }
881
879
  if (caughtError) {
@@ -123,8 +123,7 @@ export const listCommand = defineCommand({
123
123
  to,
124
124
  }));
125
125
  if (json) {
126
- // eslint-disable-next-line no-console
127
- console.log(JSON.stringify(response, null, 2));
126
+ logger.json(response);
128
127
  return;
129
128
  }
130
129
  displayResults(response, apiUrl);
@@ -2,9 +2,10 @@ import { defineCommand } from 'citty';
2
2
  import { readFileSync, writeFileSync } from 'node:fs';
3
3
  import { resolveFrontendUrl } from '../config/environments.js';
4
4
  import { apiFlags } from '../config/flags/api.flags.js';
5
+ import { jsonFlag } from '../config/flags/output.flags.js';
5
6
  import { ApiGateway } from '../gateways/api-gateway.js';
6
7
  import { resolveAuth } from '../utils/auth.js';
7
- import { CliError, logger, validateEnum } from '../utils/cli.js';
8
+ import { CliError, logger, validateEnum, wantsJson } from '../utils/cli.js';
8
9
  import { resolveApiUrl } from '../utils/config-store.js';
9
10
  import { colors, formatUrl } from '../utils/styling.js';
10
11
  import { ui } from '../utils/ui.js';
@@ -14,6 +15,13 @@ const READY_POLL_MS = 2_000;
14
15
  async function requireAuth(keyFlag) {
15
16
  return resolveAuth({ apiKeyFlag: keyFlag });
16
17
  }
18
+ /** Human output, dropped under --json so stdout carries only the result. */
19
+ function humanLog(json) {
20
+ return (m) => {
21
+ if (!json)
22
+ logger.log(m);
23
+ };
24
+ }
17
25
  function sleep(ms) {
18
26
  return new Promise((resolve) => setTimeout(resolve, ms));
19
27
  }
@@ -23,7 +31,7 @@ function sleep(ms) {
23
31
  * "Installing app…", etc. Removes the need to hand-roll retry loops around the
24
32
  * 425/409 "not ready" window that `exec` returns right after start/install.
25
33
  */
26
- async function waitForReady(apiUrl, auth, sessionName, timeoutMs = READY_TIMEOUT_MS) {
34
+ async function waitForReady(apiUrl, auth, sessionName, log = (m) => logger.log(m), timeoutMs = READY_TIMEOUT_MS) {
27
35
  const deadline = Date.now() + timeoutMs;
28
36
  let lastPhase = '';
29
37
  for (;;) {
@@ -36,7 +44,7 @@ async function waitForReady(apiUrl, auth, sessionName, timeoutMs = READY_TIMEOUT
36
44
  const phase = session.device_state?.phase_label ?? session.device_state?.phase ?? session.status;
37
45
  if (phase && phase !== lastPhase) {
38
46
  lastPhase = phase;
39
- logger.log(` ${colors.dim(phase)}`);
47
+ log(` ${colors.dim(phase)}`);
40
48
  }
41
49
  if (Date.now() >= deadline) {
42
50
  throw new CliError(`Timed out after ${Math.round(timeoutMs / 1000)}s waiting for the device to be ready` +
@@ -82,7 +90,16 @@ function readFlowFile(filePath) {
82
90
  throw new CliError(`Could not read flow file '${filePath}': ${err.message}`);
83
91
  }
84
92
  }
85
- function printExecResult(result) {
93
+ function printExecResult(result, json, extra = {}) {
94
+ if (json) {
95
+ logger.json({
96
+ success: result.success,
97
+ ...(result.output && { output: result.output }),
98
+ ...(result.error && { error: result.error }),
99
+ ...extra,
100
+ });
101
+ return;
102
+ }
86
103
  logger.log(result.success
87
104
  ? ui.success('Command executed successfully')
88
105
  : `${ui.statusSymbol('FAILED')} Command failed`);
@@ -125,6 +142,7 @@ const startSub = defineCommand({
125
142
  default: false,
126
143
  description: 'Block until the device is ready to accept commands',
127
144
  },
145
+ json: jsonFlag,
128
146
  },
129
147
  async run({ args }) {
130
148
  const auth = await requireAuth(args['api-key']);
@@ -140,12 +158,14 @@ const startSub = defineCommand({
140
158
  if (Boolean(androidDevice) !== Boolean(androidApiLevel)) {
141
159
  throw new CliError('--android-device and --android-api-level must be provided together.');
142
160
  }
143
- logger.log(ui.warn(colors.bold('Live is in beta')));
144
- logger.log(ui.branch([
161
+ const json = Boolean(args.json);
162
+ const out = humanLog(json);
163
+ out(ui.warn(colors.bold('Live is in beta')));
164
+ out(ui.branch([
145
165
  'Live device sessions are a beta feature, billed at $0.03/min.',
146
166
  `${colors.dim('Not enrolled?')} Contact support to request access.`,
147
167
  ]));
148
- logger.log(ui.running(`Starting ${platform} live session…`));
168
+ out(ui.running(`Starting ${platform} live session…`));
149
169
  const session = await ApiGateway.startLiveSession(apiUrl, auth, {
150
170
  binaryUploadId: binaryId,
151
171
  deviceLocale,
@@ -154,17 +174,48 @@ const startSub = defineCommand({
154
174
  androidApiLevel,
155
175
  });
156
176
  const frontendUrl = resolveFrontendUrl(apiUrl);
157
- logger.log(ui.success('Live session started'));
158
- logger.log(ui.branch(ui.fields([
177
+ const consoleUrl = `${frontendUrl}/live?session=${session.session_name}`;
178
+ out(ui.success('Live session started'));
179
+ out(ui.branch(ui.fields([
159
180
  ['session', colors.highlight(session.session_name)],
160
181
  ['platform', session.platform],
161
182
  ['status', session.status],
162
- ['console', formatUrl(`${frontendUrl}/live?session=${session.session_name}`)],
183
+ ['console', formatUrl(consoleUrl)],
163
184
  ])));
185
+ let ready;
164
186
  if (args.wait) {
165
- logger.log(ui.running('Waiting for the device to be ready…'));
166
- const ready = await waitForReady(apiUrl, auth, session.session_name);
167
- logger.log(ui.success(`Device ready${ready.device_model ? ` (${ready.device_model})` : ''}`));
187
+ out(ui.running('Waiting for the device to be ready…'));
188
+ try {
189
+ ready = await waitForReady(apiUrl, auth, session.session_name, out);
190
+ }
191
+ catch (error) {
192
+ // The session exists and is billing. Under --json the name was never
193
+ // printed, so the top-level `{ status, error }` would leave a script
194
+ // with no way to `dcd live stop` it. Report the name with the error.
195
+ if (!json)
196
+ throw error;
197
+ logger.json({
198
+ status: 'FAILED',
199
+ error: error instanceof Error ? error.message : String(error),
200
+ session_name: session.session_name,
201
+ platform: session.platform,
202
+ ready: false,
203
+ consoleUrl,
204
+ });
205
+ logger.fail(error instanceof Error ? error : String(error), error instanceof CliError ? error.exitCode : 1);
206
+ }
207
+ out(ui.success(`Device ready${ready.device_model ? ` (${ready.device_model})` : ''}`));
208
+ }
209
+ if (json) {
210
+ logger.json({
211
+ session_name: session.session_name,
212
+ platform: session.platform,
213
+ status: ready?.status ?? session.status,
214
+ ready: Boolean(ready?.ready),
215
+ consoleUrl,
216
+ ...(ready?.device_model && { device_model: ready.device_model }),
217
+ });
218
+ return;
168
219
  }
169
220
  logger.log(ui.section('Next steps'));
170
221
  logger.log(ui.branch(ui.fields([
@@ -180,6 +231,7 @@ const installSub = defineCommand({
180
231
  args: {
181
232
  ...apiFlags,
182
233
  session: { type: 'string', required: true, description: 'Live session name' },
234
+ json: jsonFlag,
183
235
  'app-binary-id': {
184
236
  type: 'string',
185
237
  required: true,
@@ -196,13 +248,23 @@ const installSub = defineCommand({
196
248
  const apiUrl = resolveApiUrl(args['api-url']);
197
249
  const sessionName = args.session;
198
250
  const binaryId = args['app-binary-id'];
199
- logger.log(ui.running(`Installing binary ${colors.highlight(binaryId)} on session ${colors.highlight(sessionName)}…`));
251
+ const json = Boolean(args.json);
252
+ const out = humanLog(json);
253
+ out(ui.running(`Installing binary ${colors.highlight(binaryId)} on session ${colors.highlight(sessionName)}…`));
200
254
  await ApiGateway.installLiveBinary(apiUrl, auth, sessionName, binaryId);
201
- logger.log(ui.success('Binary installed successfully'));
255
+ out(ui.success('Binary installed successfully'));
202
256
  if (args.wait) {
203
- logger.log(ui.running('Waiting for the device to be ready…'));
204
- await waitForReady(apiUrl, auth, sessionName);
205
- logger.log(ui.success('Device ready'));
257
+ out(ui.running('Waiting for the device to be ready…'));
258
+ await waitForReady(apiUrl, auth, sessionName, out);
259
+ out(ui.success('Device ready'));
260
+ }
261
+ if (json) {
262
+ logger.json({
263
+ status: 'SUCCESS',
264
+ session: sessionName,
265
+ appBinaryId: binaryId,
266
+ ready: Boolean(args.wait),
267
+ });
206
268
  }
207
269
  },
208
270
  });
@@ -211,6 +273,7 @@ const execSub = defineCommand({
211
273
  args: {
212
274
  ...apiFlags,
213
275
  session: { type: 'string', required: true, description: 'Live session name' },
276
+ json: jsonFlag,
214
277
  yaml: { type: 'string', description: 'Maestro YAML commands to execute' },
215
278
  file: {
216
279
  type: 'string',
@@ -235,12 +298,14 @@ const execSub = defineCommand({
235
298
  if (!yaml || !yaml.trim()) {
236
299
  throw new CliError('Provide commands to execute via --yaml or --file.');
237
300
  }
301
+ const json = Boolean(args.json);
302
+ const out = humanLog(json);
238
303
  if (args.wait) {
239
- await waitForReady(apiUrl, auth, sessionName);
304
+ await waitForReady(apiUrl, auth, sessionName, out);
240
305
  }
241
- logger.log(ui.running(`Executing commands on session ${colors.highlight(sessionName)}…`));
306
+ out(ui.running(`Executing commands on session ${colors.highlight(sessionName)}…`));
242
307
  const result = await ApiGateway.execLiveYaml(apiUrl, auth, sessionName, yaml);
243
- printExecResult(result);
308
+ printExecResult(result, json);
244
309
  },
245
310
  });
246
311
  const runSub = defineCommand({
@@ -251,6 +316,7 @@ const runSub = defineCommand({
251
316
  args: {
252
317
  ...apiFlags,
253
318
  session: { type: 'string', required: true, description: 'Live session name' },
319
+ json: jsonFlag,
254
320
  wait: {
255
321
  type: 'boolean',
256
322
  default: false,
@@ -277,10 +343,22 @@ const runSub = defineCommand({
277
343
  if (!commands) {
278
344
  throw new CliError(`Flow file '${flowFile}' has no commands to run.`);
279
345
  }
346
+ const json = Boolean(args.json);
347
+ const out = humanLog(json);
348
+ // Print the result, then fail with exit 2 if the flow failed. Under --json
349
+ // the result object is the output, so exit without printing a second one.
350
+ const finish = (result, extra = {}) => {
351
+ printExecResult(result, json, extra);
352
+ if (result.success)
353
+ return;
354
+ if (json)
355
+ logger.fail(result.error || 'Flow failed.', 2);
356
+ throw new CliError(result.error || 'Flow failed.', 2);
357
+ };
280
358
  if (args.wait) {
281
- await waitForReady(apiUrl, auth, sessionName);
359
+ await waitForReady(apiUrl, auth, sessionName, out);
282
360
  }
283
- logger.log(ui.running(`Running ${colors.highlight(flowFile)} on session ${colors.highlight(sessionName)}…`));
361
+ out(ui.running(`Running ${colors.highlight(flowFile)} on session ${colors.highlight(sessionName)}…`));
284
362
  // Submit asynchronously and poll, so a long flow isn't capped by the
285
363
  // server's 120s synchronous exec limit. Older servers ignore `async` and
286
364
  // return the full result inline — handle that by falling through.
@@ -288,9 +366,7 @@ const runSub = defineCommand({
288
366
  async: true,
289
367
  });
290
368
  if (!submitted.commandId) {
291
- printExecResult(submitted);
292
- if (!submitted.success)
293
- throw new CliError(submitted.error || 'Flow failed.', 2);
369
+ finish(submitted);
294
370
  return;
295
371
  }
296
372
  const commandId = submitted.commandId;
@@ -299,9 +375,7 @@ const runSub = defineCommand({
299
375
  for (;;) {
300
376
  const status = await ApiGateway.getLiveCommand(apiUrl, auth, sessionName, commandId);
301
377
  if (status.done) {
302
- printExecResult(status);
303
- if (!status.success)
304
- throw new CliError(status.error || 'Flow failed.', 2);
378
+ finish(status, { commandId });
305
379
  return;
306
380
  }
307
381
  if (Date.now() >= deadline) {
@@ -326,6 +400,7 @@ const screenshotSub = defineCommand({
326
400
  args: {
327
401
  ...apiFlags,
328
402
  session: { type: 'string', required: true, description: 'Live session name' },
403
+ json: jsonFlag,
329
404
  output: {
330
405
  type: 'string',
331
406
  alias: ['o'],
@@ -347,6 +422,18 @@ const screenshotSub = defineCommand({
347
422
  const base64 = match ? match[2] : dataUrl;
348
423
  const output = args.output ?? `live-screenshot.${ext}`;
349
424
  writeFileSync(output, Buffer.from(base64, 'base64'));
425
+ if (args.json) {
426
+ logger.json({
427
+ status: 'SUCCESS',
428
+ path: output,
429
+ format: ext,
430
+ ...(session.hierarchy && {
431
+ width: session.hierarchy.width,
432
+ height: session.hierarchy.height,
433
+ }),
434
+ });
435
+ return;
436
+ }
350
437
  logger.log(ui.success(`Screenshot saved to ${colors.highlight(output)}`));
351
438
  if (session.hierarchy) {
352
439
  logger.log(ui.branch(ui.fields([
@@ -363,7 +450,10 @@ const hierarchySub = defineCommand({
363
450
  args: {
364
451
  ...apiFlags,
365
452
  session: { type: 'string', required: true, description: 'Live session name' },
366
- json: { type: 'boolean', description: 'Output the raw hierarchy as JSON' },
453
+ json: {
454
+ type: 'boolean',
455
+ description: 'Output the raw hierarchy as JSON (with --output, the file gets the hierarchy and stdout gets { status, path })',
456
+ },
367
457
  output: {
368
458
  type: 'string',
369
459
  alias: ['o'],
@@ -383,14 +473,12 @@ const hierarchySub = defineCommand({
383
473
  'try again, or start/install with --wait.');
384
474
  }
385
475
  if (json) {
386
- const out = JSON.stringify(hierarchy, null, 2);
387
476
  if (output) {
388
- writeFileSync(output, out);
389
- logger.log(ui.success(`Hierarchy written to ${colors.highlight(output)}`));
477
+ writeFileSync(output, JSON.stringify(hierarchy, null, 2));
478
+ logger.json({ status: 'SUCCESS', path: output });
390
479
  }
391
480
  else {
392
- // eslint-disable-next-line no-console
393
- console.log(out);
481
+ logger.json(hierarchy);
394
482
  }
395
483
  return;
396
484
  }
@@ -427,13 +515,19 @@ const stopSub = defineCommand({
427
515
  args: {
428
516
  ...apiFlags,
429
517
  session: { type: 'string', required: true, description: 'Live session name' },
518
+ json: jsonFlag,
430
519
  },
431
520
  async run({ args }) {
432
521
  const auth = await requireAuth(args['api-key']);
433
522
  const apiUrl = resolveApiUrl(args['api-url']);
434
523
  const sessionName = args.session;
435
- logger.log(ui.running(`Stopping session ${colors.highlight(sessionName)}…`));
524
+ const json = Boolean(args.json);
525
+ humanLog(json)(ui.running(`Stopping session ${colors.highlight(sessionName)}…`));
436
526
  await ApiGateway.stopLiveSession(apiUrl, auth, sessionName);
527
+ if (json) {
528
+ logger.json({ status: 'SUCCESS', session: sessionName });
529
+ return;
530
+ }
437
531
  logger.log(ui.success('Session stopped'));
438
532
  },
439
533
  });
@@ -442,12 +536,20 @@ const statusSub = defineCommand({
442
536
  args: {
443
537
  ...apiFlags,
444
538
  session: { type: 'string', required: true, description: 'Live session name' },
539
+ json: jsonFlag,
445
540
  },
446
541
  async run({ args }) {
447
542
  const auth = await requireAuth(args['api-key']);
448
543
  const apiUrl = resolveApiUrl(args['api-url']);
449
544
  const sessionName = args.session;
450
545
  const session = await ApiGateway.getLiveSession(apiUrl, auth, sessionName);
546
+ if (args.json) {
547
+ // The screenshot and hierarchy are large; `screenshot` / `hierarchy`
548
+ // are the commands for those.
549
+ const { hierarchy: _hierarchy, screenshot_data_url: _screenshot, ...rest } = session;
550
+ logger.json(rest);
551
+ return;
552
+ }
451
553
  const fields = [
452
554
  ['session', colors.highlight(session.session_name)],
453
555
  ['platform', session.platform],
@@ -475,39 +577,40 @@ const statusSub = defineCommand({
475
577
  logger.log(ui.branch(ui.fields(fields)));
476
578
  },
477
579
  });
580
+ const subCommands = {
581
+ start: startSub,
582
+ install: installSub,
583
+ exec: execSub,
584
+ run: runSub,
585
+ screenshot: screenshotSub,
586
+ hierarchy: hierarchySub,
587
+ stop: stopSub,
588
+ status: statusSub,
589
+ };
478
590
  export const liveCommand = defineCommand({
479
591
  meta: {
480
592
  name: 'live',
481
593
  description: 'Start and interact with a live device session',
482
594
  },
483
- subCommands: {
484
- start: startSub,
485
- install: installSub,
486
- exec: execSub,
487
- run: runSub,
488
- screenshot: screenshotSub,
489
- hierarchy: hierarchySub,
490
- stop: stopSub,
491
- status: statusSub,
492
- },
595
+ subCommands,
493
596
  // citty's runCommand does not early-return after dispatching to a
494
597
  // subcommand — it still invokes the parent `run` afterwards. So when a
495
598
  // subcommand was matched, bail out here; otherwise the menu would print
496
599
  // after every successful subcommand.
497
600
  run({ rawArgs }) {
498
- const subNames = new Set([
499
- 'start',
500
- 'install',
501
- 'exec',
502
- 'run',
503
- 'screenshot',
504
- 'hierarchy',
505
- 'stop',
506
- 'status',
507
- ]);
601
+ const subNames = new Set(Object.keys(subCommands));
508
602
  const firstPositional = rawArgs.find((arg) => !arg.startsWith('-'));
509
603
  if (firstPositional && subNames.has(firstPositional))
510
604
  return;
605
+ if (wantsJson(rawArgs)) {
606
+ logger.json({
607
+ commands: Object.entries(subCommands).map(([name, sub]) => ({
608
+ name,
609
+ description: sub.meta.description,
610
+ })),
611
+ });
612
+ return;
613
+ }
511
614
  logger.log(ui.section('Live Session Commands'));
512
615
  logger.log(ui.branch(ui.fields([
513
616
  [colors.bold('start'), 'Start a new live device session'],
@@ -13,5 +13,13 @@ export declare const loginCommand: import("citty").CommandDef<{
13
13
  readonly default: true;
14
14
  readonly description: "Open the login URL in a browser (pass --no-browser to just print it)";
15
15
  };
16
+ readonly org: {
17
+ readonly type: "string";
18
+ readonly description: "Organization to use, by name or id (skips the picker)";
19
+ };
20
+ readonly json: {
21
+ readonly type: "boolean";
22
+ readonly description: "Output the result as a single JSON object on stdout";
23
+ };
16
24
  }>;
17
25
  export default loginCommand;