@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.
- 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 +69 -10
- package/dist/commands/logout.d.ts +6 -1
- package/dist/commands/logout.js +16 -2
- 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/flags/output.flags.d.ts +8 -0
- package/dist/config/flags/output.flags.js +8 -0
- 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/types/domain/device.types.d.ts +1 -4
- package/dist/types/domain/device.types.js +0 -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
package/dist/commands/login.js
CHANGED
|
@@ -18,9 +18,12 @@
|
|
|
18
18
|
* 5. Meanwhile, the CLI polls POST /cli-login/claim with {state, code_verifier}.
|
|
19
19
|
* Once the api has the row, it verifies sha256(verifier) === challenge,
|
|
20
20
|
* deletes the row, and returns the session.
|
|
21
|
-
* 6. CLI fetches /me/orgs with the fresh Bearer token and
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* 6. CLI fetches /me/orgs with the fresh Bearer token and picks the org:
|
|
22
|
+
* `--org` if given, else the only one, else a picker that starts on the
|
|
23
|
+
* last login's org. Without a TTY nobody can answer the picker, so the
|
|
24
|
+
* last login's org is used, or the command fails pointing at `--org`.
|
|
25
|
+
* Org selection lives here, not in the web UI — that way the CLI login
|
|
26
|
+
* and `dcd switch-org` share one picker.
|
|
24
27
|
*
|
|
25
28
|
* Why PKCE + server rendezvous (instead of a localhost loopback): Safari
|
|
26
29
|
* blocks HTTPS → HTTP fetches to 127.0.0.1 as mixed content, and a loopback
|
|
@@ -33,9 +36,11 @@ import { defineCommand } from 'citty';
|
|
|
33
36
|
import { spawn } from 'node:child_process';
|
|
34
37
|
import { createHash, randomBytes } from 'node:crypto';
|
|
35
38
|
import { ENVIRONMENTS, inferEnvFromApiUrl, resolveFrontendUrl } from '../config/environments.js';
|
|
39
|
+
import { jsonFlag } from '../config/flags/output.flags.js';
|
|
40
|
+
import { canPrompt } from '../utils/ci.js';
|
|
36
41
|
import { CliError, logger } from '../utils/cli.js';
|
|
37
42
|
import { readConfig, writeConfig } from '../utils/config-store.js';
|
|
38
|
-
import { fetchOrgs,
|
|
43
|
+
import { fetchOrgs, selectOrg } from '../utils/orgs.js';
|
|
39
44
|
import { colors } from '../utils/styling.js';
|
|
40
45
|
import { ui } from '../utils/ui.js';
|
|
41
46
|
const LOGIN_TIMEOUT_MS = 10 * 60 * 1000;
|
|
@@ -64,11 +69,35 @@ export const loginCommand = defineCommand({
|
|
|
64
69
|
default: true,
|
|
65
70
|
description: 'Open the login URL in a browser (pass --no-browser to just print it)',
|
|
66
71
|
},
|
|
72
|
+
org: {
|
|
73
|
+
type: 'string',
|
|
74
|
+
description: 'Organization to use, by name or id (skips the picker)',
|
|
75
|
+
},
|
|
76
|
+
json: jsonFlag,
|
|
67
77
|
},
|
|
68
78
|
async run({ args }) {
|
|
69
79
|
const apiUrl = args['api-url'].replace(/\/$/, '');
|
|
70
80
|
const frontendOverride = args['frontend-url'];
|
|
71
81
|
const noBrowser = args.browser === false;
|
|
82
|
+
const json = Boolean(args.json);
|
|
83
|
+
// citty gives '' for a bare `--org`. Refuse it, and an empty `--org "$ORG"`,
|
|
84
|
+
// rather than fall back to an org the caller didn't ask for.
|
|
85
|
+
const requestedOrg = args.org?.trim();
|
|
86
|
+
if (requestedOrg === '') {
|
|
87
|
+
throw new CliError('--org needs an organization name or id.');
|
|
88
|
+
}
|
|
89
|
+
// Login is interactive, so under --json the human rows and prompts move to
|
|
90
|
+
// stderr (the user still needs the URL) and stdout gets only the result.
|
|
91
|
+
const say = (m) => {
|
|
92
|
+
if (json)
|
|
93
|
+
process.stderr.write(`${m}\n`);
|
|
94
|
+
else
|
|
95
|
+
logger.log(m);
|
|
96
|
+
};
|
|
97
|
+
const promptOutput = json ? process.stderr : undefined;
|
|
98
|
+
// Without a TTY (a script, CI, a shell an agent runs) nobody can answer a
|
|
99
|
+
// prompt, so each one below falls back to the answer Enter would give.
|
|
100
|
+
const interactive = canPrompt(promptOutput);
|
|
72
101
|
// If there's an existing stored session, make the user confirm before we
|
|
73
102
|
// overwrite it. Silent clobber is fine for power users but surprising if
|
|
74
103
|
// someone runs `dcd login` by mistake while already authenticated. An
|
|
@@ -79,7 +108,12 @@ export const loginCommand = defineCommand({
|
|
|
79
108
|
if (existing?.session) {
|
|
80
109
|
const now = Math.floor(Date.now() / 1000);
|
|
81
110
|
if (existing.session.expires_at <= now) {
|
|
82
|
-
|
|
111
|
+
say(ui.info(`Your session for ${existing.session.user_email} has expired — signing in again.`));
|
|
112
|
+
}
|
|
113
|
+
else if (!interactive) {
|
|
114
|
+
// The confirm below defaults to yes. The old session stays until the
|
|
115
|
+
// new login succeeds, so this replaces nothing if it never completes.
|
|
116
|
+
say(ui.info(`Already logged in as ${existing.session.user_email} — signing in again.`));
|
|
83
117
|
}
|
|
84
118
|
else {
|
|
85
119
|
const currentOrg = existing.current_org_name ?? existing.current_org_id;
|
|
@@ -88,9 +122,15 @@ export const loginCommand = defineCommand({
|
|
|
88
122
|
(currentOrg ? ` (org ${currentOrg})` : '') +
|
|
89
123
|
`. Sign out and log in again?`,
|
|
90
124
|
initialValue: true,
|
|
125
|
+
output: promptOutput,
|
|
91
126
|
});
|
|
92
127
|
if (p.isCancel(ok) || !ok) {
|
|
93
|
-
|
|
128
|
+
if (json) {
|
|
129
|
+
logger.json({ status: 'UNCHANGED', user: existing.session.user_email });
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
logger.log(ui.info('Keeping existing session.'));
|
|
133
|
+
}
|
|
94
134
|
return;
|
|
95
135
|
}
|
|
96
136
|
}
|
|
@@ -102,7 +142,7 @@ export const loginCommand = defineCommand({
|
|
|
102
142
|
const codeVerifier = base64url(randomBytes(32));
|
|
103
143
|
const codeChallenge = base64url(createHash('sha256').update(codeVerifier, 'ascii').digest());
|
|
104
144
|
const loginUrl = `${frontendUrl}/cli-login?state=${state}&code_challenge=${codeChallenge}`;
|
|
105
|
-
|
|
145
|
+
say(ui.section('Signing in to devicecloud.dev'));
|
|
106
146
|
const rows = [];
|
|
107
147
|
if (noBrowser) {
|
|
108
148
|
rows.push(ui.note('Open this URL in a browser to finish login:'), colors.highlight(loginUrl));
|
|
@@ -117,7 +157,7 @@ export const loginCommand = defineCommand({
|
|
|
117
157
|
}
|
|
118
158
|
}
|
|
119
159
|
rows.push(ui.note('Waiting for login to complete…'));
|
|
120
|
-
|
|
160
|
+
say(ui.branch(rows));
|
|
121
161
|
try {
|
|
122
162
|
const payload = await pollForClaim(apiUrl, state, codeVerifier);
|
|
123
163
|
if (!payload.access_token || typeof payload.access_token !== 'string') {
|
|
@@ -146,7 +186,17 @@ export const loginCommand = defineCommand({
|
|
|
146
186
|
].filter(Boolean);
|
|
147
187
|
throw new CliError(`${err.message} [${parts.join(' | ')}]`);
|
|
148
188
|
}
|
|
149
|
-
const chosen = await
|
|
189
|
+
const { org: chosen, fromLastLogin } = await selectOrg(orgs, {
|
|
190
|
+
requested: requestedOrg,
|
|
191
|
+
// Org ids are per environment (dev org 1 isn't prod org 1), so the
|
|
192
|
+
// last login's org only counts if that login was to this same API.
|
|
193
|
+
lastOrgId: existing?.api_url === apiUrl ? existing.current_org_id : undefined,
|
|
194
|
+
interactive,
|
|
195
|
+
output: promptOutput,
|
|
196
|
+
});
|
|
197
|
+
if (fromLastLogin) {
|
|
198
|
+
say(ui.info(`No terminal to pick an organization in, so using ${chosen.name} from your last login.`));
|
|
199
|
+
}
|
|
150
200
|
writeConfig({
|
|
151
201
|
version: 1,
|
|
152
202
|
env,
|
|
@@ -162,6 +212,15 @@ export const loginCommand = defineCommand({
|
|
|
162
212
|
current_org_id: chosen.id,
|
|
163
213
|
current_org_name: chosen.name,
|
|
164
214
|
});
|
|
215
|
+
if (json) {
|
|
216
|
+
logger.json({
|
|
217
|
+
status: 'SUCCESS',
|
|
218
|
+
user: payload.user_email,
|
|
219
|
+
org: { id: chosen.id, name: chosen.name },
|
|
220
|
+
env,
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
165
224
|
logger.log(ui.success(`Logged in as ${colors.highlight(payload.user_email)}`));
|
|
166
225
|
logger.log(ui.branch([
|
|
167
226
|
...ui.fields([['organization', colors.highlight(chosen.name)]]),
|
|
@@ -171,7 +230,7 @@ export const loginCommand = defineCommand({
|
|
|
171
230
|
]));
|
|
172
231
|
}
|
|
173
232
|
catch (error) {
|
|
174
|
-
logger.error(error, { exit: 1 });
|
|
233
|
+
logger.error(error, { exit: 1, json });
|
|
175
234
|
}
|
|
176
235
|
},
|
|
177
236
|
});
|
|
@@ -1,2 +1,7 @@
|
|
|
1
|
-
export declare const logoutCommand: import("citty").CommandDef<
|
|
1
|
+
export declare const logoutCommand: import("citty").CommandDef<{
|
|
2
|
+
readonly json: {
|
|
3
|
+
readonly type: "boolean";
|
|
4
|
+
readonly description: "Output the result as a single JSON object on stdout";
|
|
5
|
+
};
|
|
6
|
+
}>;
|
|
2
7
|
export default logoutCommand;
|
package/dist/commands/logout.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { defineCommand } from 'citty';
|
|
6
6
|
import { ENVIRONMENTS } from '../config/environments.js';
|
|
7
|
+
import { jsonFlag } from '../config/flags/output.flags.js';
|
|
7
8
|
import { CliAuthGateway } from '../gateways/cli-auth-gateway.js';
|
|
8
9
|
import { logger } from '../utils/cli.js';
|
|
9
10
|
import { clearConfig, getConfigPath, readConfig } from '../utils/config-store.js';
|
|
@@ -14,16 +15,29 @@ export const logoutCommand = defineCommand({
|
|
|
14
15
|
name: 'logout',
|
|
15
16
|
description: 'Clear the stored devicecloud.dev session',
|
|
16
17
|
},
|
|
17
|
-
|
|
18
|
+
args: {
|
|
19
|
+
json: jsonFlag,
|
|
20
|
+
},
|
|
21
|
+
async run({ args }) {
|
|
22
|
+
const json = Boolean(args.json);
|
|
18
23
|
const config = readConfig();
|
|
19
24
|
if (!config?.session) {
|
|
20
|
-
logger.log(ui.info(`No active session found (${colors.dim(getConfigPath())}).`));
|
|
21
25
|
clearConfig();
|
|
26
|
+
if (json) {
|
|
27
|
+
logger.json({ status: 'SUCCESS', loggedOut: false });
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
logger.log(ui.info(`No active session found (${colors.dim(getConfigPath())}).`));
|
|
31
|
+
}
|
|
22
32
|
return;
|
|
23
33
|
}
|
|
24
34
|
const { anonKey } = ENVIRONMENTS[config.env].supabase;
|
|
25
35
|
await CliAuthGateway.signOut(config.supabase_url, anonKey, config.session);
|
|
26
36
|
clearConfig();
|
|
37
|
+
if (json) {
|
|
38
|
+
logger.json({ status: 'SUCCESS', loggedOut: true, user: config.session.user_email });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
27
41
|
logger.log(ui.success(`Logged out ${colors.dim(`(${config.session.user_email})`)}`));
|
|
28
42
|
},
|
|
29
43
|
});
|
package/dist/commands/status.js
CHANGED
|
@@ -105,13 +105,12 @@ async function statusMain({ apiKeyFlag, apiUrl, json, name, uploadId, }) {
|
|
|
105
105
|
if (isClientApiError(lastError)) {
|
|
106
106
|
const errorMessage = lastError?.message || 'Unknown error';
|
|
107
107
|
if (json) {
|
|
108
|
-
|
|
109
|
-
console.log(JSON.stringify({
|
|
108
|
+
logger.json({
|
|
110
109
|
status: 'FAILED',
|
|
111
110
|
error: errorMessage,
|
|
112
111
|
attempts: attemptsMade,
|
|
113
112
|
tests: [],
|
|
114
|
-
}
|
|
113
|
+
});
|
|
115
114
|
return;
|
|
116
115
|
}
|
|
117
116
|
throw new CliError(errorMessage);
|
|
@@ -128,8 +127,7 @@ async function statusMain({ apiKeyFlag, apiUrl, json, name, uploadId, }) {
|
|
|
128
127
|
errorMessage = `Failed to get status after ${attemptsMade} attempt${attemptsMade > 1 ? 's' : ''}.\n\nInternet connectivity check failed - all test endpoints unreachable:\n${endpointDetails}\n\nPlease verify your network connection and DNS resolution.\nLast API error: ${lastError?.message || 'Unknown error'}`;
|
|
129
128
|
}
|
|
130
129
|
if (json) {
|
|
131
|
-
|
|
132
|
-
console.log(JSON.stringify({
|
|
130
|
+
logger.json({
|
|
133
131
|
status: 'FAILED',
|
|
134
132
|
error: errorMessage,
|
|
135
133
|
attempts: attemptsMade,
|
|
@@ -139,7 +137,7 @@ async function statusMain({ apiKeyFlag, apiUrl, json, name, uploadId, }) {
|
|
|
139
137
|
message: connectivityCheck.message,
|
|
140
138
|
},
|
|
141
139
|
tests: [],
|
|
142
|
-
}
|
|
140
|
+
});
|
|
143
141
|
return;
|
|
144
142
|
}
|
|
145
143
|
throw new CliError(errorMessage);
|
|
@@ -147,8 +145,7 @@ async function statusMain({ apiKeyFlag, apiUrl, json, name, uploadId, }) {
|
|
|
147
145
|
try {
|
|
148
146
|
if (json) {
|
|
149
147
|
const { tests, ...rest } = status;
|
|
150
|
-
|
|
151
|
-
console.log(JSON.stringify({ ...rest, tests }, null, 2));
|
|
148
|
+
logger.json({ ...rest, tests });
|
|
152
149
|
return;
|
|
153
150
|
}
|
|
154
151
|
const fields = [];
|
|
@@ -8,5 +8,9 @@ export declare const switchOrgCommand: import("citty").CommandDef<{
|
|
|
8
8
|
readonly required: false;
|
|
9
9
|
readonly description: "Org name to switch to (omit for an interactive picker)";
|
|
10
10
|
};
|
|
11
|
+
readonly json: {
|
|
12
|
+
readonly type: "boolean";
|
|
13
|
+
readonly description: "Output the result as a single JSON object on stdout";
|
|
14
|
+
};
|
|
11
15
|
}>;
|
|
12
16
|
export default switchOrgCommand;
|
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* With an argument (`dcd switch-org <name>`): update config immediately after
|
|
5
5
|
* confirming the user is a member via GET /me/orgs.
|
|
6
|
-
* Without an argument: fetch orgs and prompt the user to pick one
|
|
6
|
+
* Without an argument: fetch orgs and prompt the user to pick one (an error
|
|
7
|
+
* instead when there's no TTY to answer the prompt).
|
|
7
8
|
*/
|
|
8
9
|
import { defineCommand } from 'citty';
|
|
10
|
+
import { jsonFlag } from '../config/flags/output.flags.js';
|
|
9
11
|
import { apiKeyOverride, resolveAuth } from '../utils/auth.js';
|
|
12
|
+
import { canPrompt } from '../utils/ci.js';
|
|
10
13
|
import { CliError, logger } from '../utils/cli.js';
|
|
11
14
|
import { readConfig, resolveApiUrl, writeConfig } from '../utils/config-store.js';
|
|
12
|
-
import { fetchOrgs, pickOrg } from '../utils/orgs.js';
|
|
15
|
+
import { fetchOrgs, matchOrg, pickOrg } from '../utils/orgs.js';
|
|
13
16
|
import { colors } from '../utils/styling.js';
|
|
14
17
|
import { ui } from '../utils/ui.js';
|
|
15
18
|
export const switchOrgCommand = defineCommand({
|
|
@@ -27,6 +30,7 @@ export const switchOrgCommand = defineCommand({
|
|
|
27
30
|
required: false,
|
|
28
31
|
description: 'Org name to switch to (omit for an interactive picker)',
|
|
29
32
|
},
|
|
33
|
+
json: jsonFlag,
|
|
30
34
|
},
|
|
31
35
|
async run({ args }) {
|
|
32
36
|
const config = readConfig();
|
|
@@ -37,44 +41,43 @@ export const switchOrgCommand = defineCommand({
|
|
|
37
41
|
// a dev Bearer token to the prod API.
|
|
38
42
|
const apiUrl = resolveApiUrl(args['api-url']);
|
|
39
43
|
const target = args.org;
|
|
44
|
+
const json = Boolean(args.json);
|
|
40
45
|
// sessionOnly: an exported DEVICE_CLOUD_API_KEY must not shadow the
|
|
41
46
|
// browser session this command requires.
|
|
42
47
|
const auth = await resolveAuth({ apiKeyFlag: undefined, sessionOnly: true });
|
|
43
48
|
const orgs = await fetchOrgs(apiUrl, auth.headers);
|
|
44
49
|
let chosen;
|
|
45
50
|
if (target) {
|
|
46
|
-
chosen = matchOrg(orgs, target);
|
|
51
|
+
chosen = matchOrg(orgs, target, '`dcd switch-org` without arguments');
|
|
47
52
|
}
|
|
48
53
|
else {
|
|
49
|
-
|
|
54
|
+
// Under --json the picker draws on stderr so stdout stays pure JSON.
|
|
55
|
+
const output = json ? process.stderr : undefined;
|
|
56
|
+
// Without a TTY the picker would wait forever for a keypress.
|
|
57
|
+
if (orgs.length > 1 && !canPrompt(output)) {
|
|
58
|
+
throw new CliError(`There's no terminal to pick an organization in. Run \`dcd switch-org <name>\` with one of: ${orgs.map((o) => o.name).join(', ')}`);
|
|
59
|
+
}
|
|
60
|
+
chosen = await pickOrg(orgs, { output });
|
|
50
61
|
}
|
|
51
62
|
writeConfig({
|
|
52
63
|
...config,
|
|
53
64
|
current_org_id: chosen.id,
|
|
54
65
|
current_org_name: chosen.name,
|
|
55
66
|
});
|
|
56
|
-
logger.log(ui.success(`Switched to ${colors.highlight(chosen.name)}`));
|
|
57
67
|
// The switch only changes the stored session; an exported key still wins.
|
|
58
|
-
|
|
68
|
+
const keyOverride = apiKeyOverride() === 'DEVICE_CLOUD_API_KEY';
|
|
69
|
+
if (json) {
|
|
70
|
+
logger.json({
|
|
71
|
+
status: 'SUCCESS',
|
|
72
|
+
org: { id: chosen.id, name: chosen.name, slug: chosen.slug ?? null },
|
|
73
|
+
apiKeyOverride: keyOverride,
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
logger.log(ui.success(`Switched to ${colors.highlight(chosen.name)}`));
|
|
78
|
+
if (keyOverride) {
|
|
59
79
|
logger.log(ui.warn(`${colors.highlight('DEVICE_CLOUD_API_KEY')} is set, so other dcd commands still authenticate with that API key and its org, not ${chosen.name}. Unset it to use the session.`));
|
|
60
80
|
}
|
|
61
81
|
},
|
|
62
82
|
});
|
|
63
|
-
/**
|
|
64
|
-
* Match a user-supplied string to an org. Prefers case-insensitive name match.
|
|
65
|
-
* Falls back to slug / id so existing scripts keep working, but error output
|
|
66
|
-
* only surfaces names.
|
|
67
|
-
*/
|
|
68
|
-
function matchOrg(orgs, target) {
|
|
69
|
-
const needle = target.toLowerCase();
|
|
70
|
-
const nameMatches = orgs.filter((o) => o.name.toLowerCase() === needle);
|
|
71
|
-
if (nameMatches.length > 1) {
|
|
72
|
-
throw new CliError(`Multiple orgs named "${target}". Run \`dcd switch-org\` without arguments to pick interactively.`);
|
|
73
|
-
}
|
|
74
|
-
const chosen = nameMatches[0] ?? orgs.find((o) => o.slug === target || o.id === target);
|
|
75
|
-
if (!chosen) {
|
|
76
|
-
throw new CliError(`No org named "${target}". Available: ${orgs.map((o) => o.name).join(', ')}`);
|
|
77
|
-
}
|
|
78
|
-
return chosen;
|
|
79
|
-
}
|
|
80
83
|
export default switchOrgCommand;
|
|
@@ -1,2 +1,7 @@
|
|
|
1
|
-
export declare const upgradeCommand: import("citty").CommandDef<
|
|
1
|
+
export declare const upgradeCommand: import("citty").CommandDef<{
|
|
2
|
+
readonly json: {
|
|
3
|
+
readonly type: "boolean";
|
|
4
|
+
readonly description: "Output the result as a single JSON object on stdout";
|
|
5
|
+
};
|
|
6
|
+
}>;
|
|
2
7
|
export default upgradeCommand;
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -12,6 +12,7 @@ import { chmodSync, createReadStream, createWriteStream, renameSync, unlinkSync,
|
|
|
12
12
|
import { Readable } from 'node:stream';
|
|
13
13
|
import { pipeline } from 'node:stream/promises';
|
|
14
14
|
import { defineCommand } from 'citty';
|
|
15
|
+
import { jsonFlag } from '../config/flags/output.flags.js';
|
|
15
16
|
import { VersionService } from '../services/version.service.js';
|
|
16
17
|
import { CliError, getCliVersion, getInstallMethod, logger, } from '../utils/cli.js';
|
|
17
18
|
import { colors } from '../utils/styling.js';
|
|
@@ -31,7 +32,15 @@ export const upgradeCommand = defineCommand({
|
|
|
31
32
|
name: 'upgrade',
|
|
32
33
|
description: 'Upgrade dcd in place to the latest released version',
|
|
33
34
|
},
|
|
34
|
-
|
|
35
|
+
args: {
|
|
36
|
+
json: jsonFlag,
|
|
37
|
+
},
|
|
38
|
+
async run({ args }) {
|
|
39
|
+
const json = Boolean(args.json);
|
|
40
|
+
const out = (m) => {
|
|
41
|
+
if (!json)
|
|
42
|
+
logger.log(m);
|
|
43
|
+
};
|
|
35
44
|
if (getInstallMethod() !== 'binary') {
|
|
36
45
|
throw new CliError('`dcd upgrade` only applies to the standalone binary install. ' +
|
|
37
46
|
'Run: npm install -g @devicecloud.dev/dcd@latest');
|
|
@@ -45,11 +54,19 @@ export const upgradeCommand = defineCommand({
|
|
|
45
54
|
// Reachable, but nothing published on this channel yet (e.g. a stable
|
|
46
55
|
// install while only betas exist). Not an error — just nothing to do.
|
|
47
56
|
if (result.version === null) {
|
|
57
|
+
if (json) {
|
|
58
|
+
logger.json({ status: 'NO_RELEASE', current, channel: result.channel });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
48
61
|
logger.log(ui.info(`No newer release available on the ${result.channel} channel.`));
|
|
49
62
|
return;
|
|
50
63
|
}
|
|
51
64
|
const latest = result.version;
|
|
52
65
|
if (!versionService.isOutdated(current, latest)) {
|
|
66
|
+
if (json) {
|
|
67
|
+
logger.json({ status: 'UP_TO_DATE', current, latest, channel: result.channel });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
53
70
|
logger.log(ui.success(`Already on the latest version (${colors.highlight(current)})`));
|
|
54
71
|
return;
|
|
55
72
|
}
|
|
@@ -69,8 +86,8 @@ export const upgradeCommand = defineCommand({
|
|
|
69
86
|
const base = process.env.DCD_DOWNLOAD_BASE ?? DEFAULT_DOWNLOAD_BASE;
|
|
70
87
|
const binaryUrl = `${base}/download/${latest}/${asset}`;
|
|
71
88
|
const sumsUrl = `${base}/download/${latest}/SHA256SUMS`;
|
|
72
|
-
|
|
73
|
-
|
|
89
|
+
out(ui.info(`Upgrading ${colors.highlight(current)} → ${colors.highlight(latest)}`));
|
|
90
|
+
out(ui.note(` ${binaryUrl}`));
|
|
74
91
|
const execPath = process.execPath;
|
|
75
92
|
const tmpPath = `${execPath}.new`;
|
|
76
93
|
try {
|
|
@@ -89,6 +106,10 @@ export const upgradeCommand = defineCommand({
|
|
|
89
106
|
safeUnlink(tmpPath);
|
|
90
107
|
throw new CliError(`Upgrade failed: ${e.message}. The existing binary at ${execPath} was not modified.`);
|
|
91
108
|
}
|
|
109
|
+
if (json) {
|
|
110
|
+
logger.json({ status: 'UPGRADED', current, latest, channel: result.channel });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
92
113
|
logger.log(ui.success(`Upgraded to ${colors.highlight(latest)}`));
|
|
93
114
|
},
|
|
94
115
|
});
|
package/dist/commands/upload.js
CHANGED
|
@@ -116,8 +116,7 @@ export const uploadCommand = defineCommand({
|
|
|
116
116
|
});
|
|
117
117
|
telemetry.recordEncryption({ binary: encryption.enabled });
|
|
118
118
|
if (json) {
|
|
119
|
-
|
|
120
|
-
console.log(JSON.stringify({ appBinaryId }, null, 2));
|
|
119
|
+
logger.json({ appBinaryId });
|
|
121
120
|
return;
|
|
122
121
|
}
|
|
123
122
|
logger.log(`\n${ui.success('Upload complete')}`);
|
|
@@ -4,5 +4,9 @@ export declare const whoamiCommand: import("citty").CommandDef<{
|
|
|
4
4
|
readonly type: "string";
|
|
5
5
|
readonly alias: ["apiKey"];
|
|
6
6
|
};
|
|
7
|
+
readonly json: {
|
|
8
|
+
readonly type: "boolean";
|
|
9
|
+
readonly description: "Output the result as a single JSON object on stdout";
|
|
10
|
+
};
|
|
7
11
|
}>;
|
|
8
12
|
export default whoamiCommand;
|
package/dist/commands/whoami.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { defineCommand } from 'citty';
|
|
9
9
|
import { apiFlags } from '../config/flags/api.flags.js';
|
|
10
|
+
import { jsonFlag } from '../config/flags/output.flags.js';
|
|
10
11
|
import { apiKeyOverride } from '../utils/auth.js';
|
|
11
12
|
import { logger } from '../utils/cli.js';
|
|
12
13
|
import { readConfig } from '../utils/config-store.js';
|
|
@@ -22,10 +23,26 @@ export const whoamiCommand = defineCommand({
|
|
|
22
23
|
...apiFlags['api-key'],
|
|
23
24
|
description: 'An API key as you would pass it to other commands; whoami says that it takes precedence over the stored session',
|
|
24
25
|
},
|
|
26
|
+
json: jsonFlag,
|
|
25
27
|
},
|
|
26
28
|
run({ args }) {
|
|
27
29
|
const config = readConfig();
|
|
28
30
|
const keySource = apiKeyOverride(args['api-key']);
|
|
31
|
+
if (args.json) {
|
|
32
|
+
logger.json({
|
|
33
|
+
status: 'SUCCESS',
|
|
34
|
+
loggedIn: Boolean(config?.session),
|
|
35
|
+
...(config?.session && {
|
|
36
|
+
user: config.session.user_email,
|
|
37
|
+
org: config.current_org_id
|
|
38
|
+
? { id: config.current_org_id, name: config.current_org_name ?? null }
|
|
39
|
+
: null,
|
|
40
|
+
env: config.env,
|
|
41
|
+
}),
|
|
42
|
+
apiKeySource: keySource ?? null,
|
|
43
|
+
});
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
29
46
|
const keyName = colors.highlight(keySource ?? '');
|
|
30
47
|
if (!config?.session) {
|
|
31
48
|
logger.log(ui.info(keySource === 'DEVICE_CLOUD_API_KEY'
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic `--json` flag for commands outside `cloud`. With it, stdout carries
|
|
3
|
+
* exactly one JSON object; failures print `{ status: 'FAILED', error }`.
|
|
4
|
+
*/
|
|
5
|
+
export declare const jsonFlag: {
|
|
6
|
+
readonly type: "boolean";
|
|
7
|
+
readonly description: "Output the result as a single JSON object on stdout";
|
|
8
|
+
};
|
|
1
9
|
/**
|
|
2
10
|
* Output format and artifact download flags
|
|
3
11
|
*/
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic `--json` flag for commands outside `cloud`. With it, stdout carries
|
|
3
|
+
* exactly one JSON object; failures print `{ status: 'FAILED', error }`.
|
|
4
|
+
*/
|
|
5
|
+
export const jsonFlag = {
|
|
6
|
+
type: 'boolean',
|
|
7
|
+
description: 'Output the result as a single JSON object on stdout',
|
|
8
|
+
};
|
|
1
9
|
/**
|
|
2
10
|
* Output format and artifact download flags
|
|
3
11
|
*/
|
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import { upgradeCommand } from './commands/upgrade.js';
|
|
|
13
13
|
import { uploadCommand } from './commands/upload.js';
|
|
14
14
|
import { whoamiCommand } from './commands/whoami.js';
|
|
15
15
|
import { telemetry } from './services/telemetry.service.js';
|
|
16
|
-
import { CliError, getCliVersion, logger } from './utils/cli.js';
|
|
16
|
+
import { CliError, getCliVersion, logger, wantsJson } from './utils/cli.js';
|
|
17
17
|
import { withoutHiddenArgs } from './utils/help.js';
|
|
18
18
|
// @clack/prompts ships the US spelling ("Canceled") for its built-in
|
|
19
19
|
// spinner/prompt cancellation message; align it with the British spelling
|
|
@@ -88,9 +88,12 @@ async function run() {
|
|
|
88
88
|
await telemetry.flush();
|
|
89
89
|
}
|
|
90
90
|
catch (error) {
|
|
91
|
+
// Under --json, stdout must stay a single JSON object: no help text, and
|
|
92
|
+
// the error itself is printed as `{ status: 'FAILED', error }`.
|
|
93
|
+
const json = wantsJson(rawArgs);
|
|
91
94
|
// citty throws CLIError (by name — the class isn't exported) for usage
|
|
92
95
|
// problems like unknown commands or missing required args.
|
|
93
|
-
if (error instanceof Error && error.name === 'CLIError') {
|
|
96
|
+
if (!json && error instanceof Error && error.name === 'CLIError') {
|
|
94
97
|
await showHelp(rawArgs);
|
|
95
98
|
}
|
|
96
99
|
const exitCode = error instanceof CliError ? error.exitCode : 1;
|
|
@@ -98,6 +101,7 @@ async function run() {
|
|
|
98
101
|
// synchronously, and exits with the given code.
|
|
99
102
|
logger.error(error instanceof Error ? error : String(error), {
|
|
100
103
|
exit: exitCode,
|
|
104
|
+
json,
|
|
101
105
|
});
|
|
102
106
|
}
|
|
103
107
|
}
|
|
@@ -17,6 +17,13 @@ export interface ReportDownloadOptions extends DownloadOptions {
|
|
|
17
17
|
junitPath?: string;
|
|
18
18
|
reportType: 'allure' | 'html' | 'html-detailed' | 'junit';
|
|
19
19
|
}
|
|
20
|
+
/** What one download attempt did; failures are warned about, not thrown. */
|
|
21
|
+
export interface DownloadResult {
|
|
22
|
+
error?: string;
|
|
23
|
+
ok: boolean;
|
|
24
|
+
path: string;
|
|
25
|
+
type: 'allure' | 'artifacts' | 'html' | 'junit';
|
|
26
|
+
}
|
|
20
27
|
/**
|
|
21
28
|
* Service for downloading test artifacts and reports
|
|
22
29
|
*/
|
|
@@ -24,21 +31,21 @@ export declare class ReportDownloadService {
|
|
|
24
31
|
/**
|
|
25
32
|
* Download test artifacts as a zip file
|
|
26
33
|
* @param options Download configuration
|
|
27
|
-
* @returns
|
|
34
|
+
* @returns What the download did
|
|
28
35
|
*/
|
|
29
|
-
downloadArtifacts(options: ArtifactsDownloadOptions): Promise<
|
|
36
|
+
downloadArtifacts(options: ArtifactsDownloadOptions): Promise<DownloadResult>;
|
|
30
37
|
/**
|
|
31
38
|
* Handle downloading reports based on the report type specified
|
|
32
39
|
* @param options Report download configuration
|
|
33
|
-
* @returns
|
|
40
|
+
* @returns What the download did, or undefined for an unknown report type
|
|
34
41
|
*/
|
|
35
|
-
downloadReports(options: ReportDownloadOptions): Promise<
|
|
42
|
+
downloadReports(options: ReportDownloadOptions): Promise<DownloadResult | undefined>;
|
|
36
43
|
/**
|
|
37
44
|
* Download a specific report type
|
|
38
45
|
* @param type Report type to download
|
|
39
46
|
* @param filePath Path where report should be saved
|
|
40
47
|
* @param options Download configuration
|
|
41
|
-
* @returns
|
|
48
|
+
* @returns What the download did
|
|
42
49
|
*/
|
|
43
50
|
private downloadReport;
|
|
44
51
|
/**
|