@devicecloud.dev/dcd 5.8.0 → 5.9.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.
- package/dist/commands/artifacts.d.ts +4 -0
- package/dist/commands/artifacts.js +21 -6
- package/dist/commands/cloud.js +2 -4
- package/dist/commands/list.js +1 -2
- package/dist/commands/live.js +159 -56
- package/dist/commands/login.d.ts +8 -0
- package/dist/commands/login.js +74 -14
- package/dist/commands/logout.d.ts +6 -1
- package/dist/commands/logout.js +17 -5
- package/dist/commands/status.js +5 -8
- package/dist/commands/switch-org.d.ts +4 -0
- package/dist/commands/switch-org.js +26 -23
- package/dist/commands/upgrade.d.ts +6 -1
- package/dist/commands/upgrade.js +24 -3
- package/dist/commands/upload.js +1 -2
- package/dist/commands/whoami.d.ts +4 -0
- package/dist/commands/whoami.js +17 -0
- package/dist/config/environments.d.ts +6 -9
- package/dist/config/environments.js +4 -8
- package/dist/config/flags/output.flags.d.ts +8 -0
- package/dist/config/flags/output.flags.js +8 -0
- package/dist/config/supabase-key.d.ts +27 -0
- package/dist/config/supabase-key.js +54 -0
- package/dist/gateways/cli-auth-gateway.d.ts +3 -2
- package/dist/gateways/cli-auth-gateway.js +16 -6
- package/dist/gateways/realtime-gateway.js +48 -34
- package/dist/gateways/supabase-gateway.d.ts +1 -1
- package/dist/gateways/supabase-gateway.js +8 -6
- package/dist/index.js +6 -2
- package/dist/services/report-download.service.d.ts +12 -5
- package/dist/services/report-download.service.js +15 -10
- package/dist/utils/auth.js +1 -3
- package/dist/utils/ci.d.ts +9 -0
- package/dist/utils/ci.js +9 -0
- package/dist/utils/cli.d.ts +7 -0
- package/dist/utils/cli.js +23 -6
- package/dist/utils/orgs.d.ts +38 -1
- package/dist/utils/orgs.js +59 -3
- package/package.json +3 -3
|
@@ -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 {
|
|
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:
|
|
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:
|
|
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
|
});
|
package/dist/commands/cloud.js
CHANGED
|
@@ -751,8 +751,7 @@ export const cloudCommand = defineCommand({
|
|
|
751
751
|
});
|
|
752
752
|
}
|
|
753
753
|
if (json) {
|
|
754
|
-
|
|
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
|
-
|
|
878
|
-
console.log(JSON.stringify(output, null, 2));
|
|
876
|
+
logger.json(output);
|
|
879
877
|
}
|
|
880
878
|
}
|
|
881
879
|
if (caughtError) {
|
package/dist/commands/list.js
CHANGED
|
@@ -123,8 +123,7 @@ export const listCommand = defineCommand({
|
|
|
123
123
|
to,
|
|
124
124
|
}));
|
|
125
125
|
if (json) {
|
|
126
|
-
|
|
127
|
-
console.log(JSON.stringify(response, null, 2));
|
|
126
|
+
logger.json(response);
|
|
128
127
|
return;
|
|
129
128
|
}
|
|
130
129
|
displayResults(response, apiUrl);
|
package/dist/commands/live.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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(
|
|
183
|
+
['console', formatUrl(consoleUrl)],
|
|
163
184
|
])));
|
|
185
|
+
let ready;
|
|
164
186
|
if (args.wait) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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
|
-
|
|
255
|
+
out(ui.success('Binary installed successfully'));
|
|
202
256
|
if (args.wait) {
|
|
203
|
-
|
|
204
|
-
await waitForReady(apiUrl, auth, sessionName);
|
|
205
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
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,
|
|
389
|
-
logger.
|
|
477
|
+
writeFileSync(output, JSON.stringify(hierarchy, null, 2));
|
|
478
|
+
logger.json({ status: 'SUCCESS', path: output });
|
|
390
479
|
}
|
|
391
480
|
else {
|
|
392
|
-
|
|
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
|
-
|
|
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'],
|
package/dist/commands/login.d.ts
CHANGED
|
@@ -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;
|