@devicecloud.dev/dcd 5.0.0-beta.0 → 5.0.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/README.md +35 -0
- package/dist/commands/artifacts.d.ts +28 -28
- package/dist/commands/artifacts.js +20 -23
- package/dist/commands/cloud.d.ts +57 -57
- package/dist/commands/cloud.js +173 -186
- package/dist/commands/list.d.ts +22 -22
- package/dist/commands/list.js +36 -38
- package/dist/commands/live.js +134 -127
- package/dist/commands/login.d.ts +11 -11
- package/dist/commands/login.js +46 -44
- package/dist/commands/logout.js +16 -18
- package/dist/commands/status.d.ts +11 -11
- package/dist/commands/status.js +45 -43
- package/dist/commands/switch-org.d.ts +7 -7
- package/dist/commands/switch-org.js +19 -21
- package/dist/commands/upgrade.js +29 -31
- package/dist/commands/upload.d.ts +10 -10
- package/dist/commands/upload.js +42 -43
- package/dist/commands/whoami.js +17 -20
- package/dist/config/environments.js +6 -12
- package/dist/config/flags/api.flags.js +1 -4
- package/dist/config/flags/binary.flags.js +1 -4
- package/dist/config/flags/device.flags.js +6 -9
- package/dist/config/flags/environment.flags.js +1 -4
- package/dist/config/flags/execution.flags.js +1 -4
- package/dist/config/flags/github.flags.js +1 -4
- package/dist/config/flags/output.flags.js +1 -4
- package/dist/constants.js +15 -18
- package/dist/gateways/api-gateway.d.ts +31 -6
- package/dist/gateways/api-gateway.js +70 -16
- package/dist/gateways/cli-auth-gateway.d.ts +1 -1
- package/dist/gateways/cli-auth-gateway.js +3 -6
- package/dist/gateways/realtime-gateway.d.ts +32 -0
- package/dist/gateways/realtime-gateway.js +103 -0
- package/dist/gateways/supabase-gateway.d.ts +1 -1
- package/dist/gateways/supabase-gateway.js +10 -14
- package/dist/index.js +41 -38
- package/dist/mcp/context.d.ts +33 -0
- package/dist/mcp/context.js +33 -0
- package/dist/mcp/helpers.d.ts +16 -0
- package/dist/mcp/helpers.js +34 -0
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +24 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +27 -0
- package/dist/mcp/tools/download-artifacts.d.ts +11 -0
- package/dist/mcp/tools/download-artifacts.js +84 -0
- package/dist/mcp/tools/get-status.d.ts +7 -0
- package/dist/mcp/tools/get-status.js +39 -0
- package/dist/mcp/tools/list-devices.d.ts +7 -0
- package/dist/mcp/tools/list-devices.js +27 -0
- package/dist/mcp/tools/list-runs.d.ts +3 -0
- package/dist/mcp/tools/list-runs.js +60 -0
- package/dist/mcp/tools/run-cloud-test.d.ts +14 -0
- package/dist/mcp/tools/run-cloud-test.js +233 -0
- package/dist/methods.d.ts +32 -1
- package/dist/methods.js +125 -66
- package/dist/services/device-validation.service.d.ts +1 -1
- package/dist/services/device-validation.service.js +1 -5
- package/dist/services/execution-plan.service.js +14 -17
- package/dist/services/execution-plan.utils.js +15 -23
- package/dist/services/flow-paths.d.ts +17 -0
- package/dist/services/flow-paths.js +52 -0
- package/dist/services/metadata-extractor.service.js +22 -25
- package/dist/services/moropo.service.js +18 -20
- package/dist/services/report-download.service.d.ts +1 -1
- package/dist/services/report-download.service.js +5 -9
- package/dist/services/results-polling.service.d.ts +18 -3
- package/dist/services/results-polling.service.js +195 -108
- package/dist/services/telemetry.service.d.ts +10 -1
- package/dist/services/telemetry.service.js +40 -18
- package/dist/services/test-submission.service.d.ts +21 -4
- package/dist/services/test-submission.service.js +51 -34
- package/dist/services/version.service.d.ts +1 -1
- package/dist/services/version.service.js +1 -5
- package/dist/types/domain/auth.types.d.ts +8 -0
- package/dist/types/domain/auth.types.js +1 -2
- package/dist/types/domain/device.types.js +8 -11
- package/dist/types/domain/live.types.js +1 -2
- package/dist/types/generated/schema.types.js +1 -2
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.js +2 -18
- package/dist/types.js +1 -2
- package/dist/utils/auth.d.ts +1 -1
- package/dist/utils/auth.js +27 -28
- package/dist/utils/ci.d.ts +12 -0
- package/dist/utils/ci.js +39 -0
- package/dist/utils/cli.js +18 -27
- package/dist/utils/compatibility.d.ts +1 -1
- package/dist/utils/compatibility.js +5 -7
- package/dist/utils/config-store.js +33 -43
- package/dist/utils/connectivity.js +1 -4
- package/dist/utils/expo.js +15 -21
- package/dist/utils/orgs.js +8 -12
- package/dist/utils/paths.js +2 -5
- package/dist/utils/progress.js +2 -5
- package/dist/utils/styling.d.ts +35 -37
- package/dist/utils/styling.js +52 -86
- package/dist/utils/ui.d.ts +41 -0
- package/dist/utils/ui.js +95 -0
- package/package.json +27 -24
package/dist/commands/login.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.loginCommand = void 0;
|
|
4
1
|
/**
|
|
5
2
|
* `dcd login` — delegates auth to the web frontend and persists the resulting
|
|
6
3
|
* Supabase session locally.
|
|
@@ -28,20 +25,21 @@ exports.loginCommand = void 0;
|
|
|
28
25
|
* rendezvous endpoint makes both cases trivial. A leaked URL is inert: it
|
|
29
26
|
* carries the challenge, not the verifier.
|
|
30
27
|
*/
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
28
|
+
import * as p from '@clack/prompts';
|
|
29
|
+
import { defineCommand } from 'citty';
|
|
30
|
+
import { spawn } from 'node:child_process';
|
|
31
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
32
|
+
import { ENVIRONMENTS, inferEnvFromApiUrl, resolveFrontendUrl } from '../config/environments.js';
|
|
33
|
+
import { CliError, logger } from '../utils/cli.js';
|
|
34
|
+
import { readConfig, writeConfig } from '../utils/config-store.js';
|
|
35
|
+
import { fetchOrgs, pickOrg } from '../utils/orgs.js';
|
|
36
|
+
import { colors } from '../utils/styling.js';
|
|
37
|
+
import { ui } from '../utils/ui.js';
|
|
40
38
|
const LOGIN_TIMEOUT_MS = 10 * 60 * 1000;
|
|
41
39
|
// 1s feels snappy after the user clicks "Authorize" in the browser. The api
|
|
42
40
|
// rate-limits /cli-login/claim at 60/min which is exactly this cadence.
|
|
43
41
|
const POLL_INTERVAL_MS = 1000;
|
|
44
|
-
|
|
42
|
+
export const loginCommand = defineCommand({
|
|
45
43
|
meta: {
|
|
46
44
|
name: 'login',
|
|
47
45
|
description: 'Authenticate with devicecloud.dev via your browser',
|
|
@@ -71,7 +69,7 @@ exports.loginCommand = (0, citty_1.defineCommand)({
|
|
|
71
69
|
// If there's an existing stored session, make the user confirm before we
|
|
72
70
|
// overwrite it. Silent clobber is fine for power users but surprising if
|
|
73
71
|
// someone runs `dcd login` by mistake while already authenticated.
|
|
74
|
-
const existing =
|
|
72
|
+
const existing = readConfig();
|
|
75
73
|
if (existing?.session) {
|
|
76
74
|
const currentOrg = existing.current_org_name ?? existing.current_org_id;
|
|
77
75
|
const ok = await p.confirm({
|
|
@@ -81,35 +79,37 @@ exports.loginCommand = (0, citty_1.defineCommand)({
|
|
|
81
79
|
initialValue: false,
|
|
82
80
|
});
|
|
83
81
|
if (p.isCancel(ok) || !ok) {
|
|
84
|
-
|
|
82
|
+
logger.log(ui.info('Keeping existing session.'));
|
|
85
83
|
return;
|
|
86
84
|
}
|
|
87
85
|
}
|
|
88
|
-
const env =
|
|
89
|
-
const SUPABASE_URL =
|
|
90
|
-
const frontendUrl = (frontendOverride ??
|
|
91
|
-
const state =
|
|
92
|
-
const codeVerifier = base64url(
|
|
93
|
-
const codeChallenge = base64url(
|
|
86
|
+
const env = inferEnvFromApiUrl(apiUrl);
|
|
87
|
+
const SUPABASE_URL = ENVIRONMENTS[env].supabase.url;
|
|
88
|
+
const frontendUrl = (frontendOverride ?? resolveFrontendUrl(apiUrl)).replace(/\/$/, '');
|
|
89
|
+
const state = randomBytes(32).toString('hex');
|
|
90
|
+
const codeVerifier = base64url(randomBytes(32));
|
|
91
|
+
const codeChallenge = base64url(createHash('sha256').update(codeVerifier, 'ascii').digest());
|
|
94
92
|
const loginUrl = `${frontendUrl}/cli-login?state=${state}&code_challenge=${codeChallenge}`;
|
|
95
|
-
|
|
93
|
+
logger.log(ui.section('Signing in to devicecloud.dev'));
|
|
94
|
+
const rows = [];
|
|
96
95
|
if (noBrowser) {
|
|
97
|
-
|
|
98
|
-
cli_1.logger.log(` ${styling_1.colors.highlight(loginUrl)}`);
|
|
96
|
+
rows.push(ui.note('Open this URL in a browser to finish login:'), colors.highlight(loginUrl));
|
|
99
97
|
}
|
|
100
98
|
else {
|
|
101
|
-
cli_1.logger.log(` ${styling_1.colors.dim('Opening your browser...')}`);
|
|
102
99
|
const opened = openBrowser(loginUrl);
|
|
100
|
+
rows.push(ui.note(opened
|
|
101
|
+
? 'Opening your browser…'
|
|
102
|
+
: 'Could not launch a browser. Open this URL manually:'));
|
|
103
103
|
if (!opened) {
|
|
104
|
-
|
|
105
|
-
cli_1.logger.log(` ${styling_1.colors.highlight(loginUrl)}`);
|
|
104
|
+
rows.push(colors.highlight(loginUrl));
|
|
106
105
|
}
|
|
107
106
|
}
|
|
108
|
-
|
|
107
|
+
rows.push(ui.note('Waiting for login to complete…'));
|
|
108
|
+
logger.log(ui.branch(rows));
|
|
109
109
|
try {
|
|
110
110
|
const payload = await pollForClaim(apiUrl, state, codeVerifier);
|
|
111
111
|
if (!payload.access_token || typeof payload.access_token !== 'string') {
|
|
112
|
-
throw new
|
|
112
|
+
throw new CliError('Claim response did not include an access_token.');
|
|
113
113
|
}
|
|
114
114
|
// Fetch orgs with the just-minted Bearer token. We don't write the
|
|
115
115
|
// config until the user has picked an org — half-completed state isn't
|
|
@@ -119,7 +119,7 @@ exports.loginCommand = (0, citty_1.defineCommand)({
|
|
|
119
119
|
};
|
|
120
120
|
let orgs;
|
|
121
121
|
try {
|
|
122
|
-
orgs = await
|
|
122
|
+
orgs = await fetchOrgs(apiUrl, bearerHeaders);
|
|
123
123
|
}
|
|
124
124
|
catch (err) {
|
|
125
125
|
// The api rejected the JWT. Before blaming the api, hit Supabase's
|
|
@@ -127,15 +127,15 @@ exports.loginCommand = (0, citty_1.defineCommand)({
|
|
|
127
127
|
// rejects it, the token itself is bad; if Supabase accepts, the
|
|
128
128
|
// api's JwtService is misconfigured.
|
|
129
129
|
const iss = decodeJwtIssuer(payload.access_token);
|
|
130
|
-
const supabaseUser = await probeSupabaseUser(iss,
|
|
130
|
+
const supabaseUser = await probeSupabaseUser(iss, ENVIRONMENTS[env].supabase.anonKey, payload.access_token);
|
|
131
131
|
const parts = [
|
|
132
132
|
iss ? `token iss: ${iss}` : null,
|
|
133
133
|
`supabase /auth/v1/user: ${supabaseUser}`,
|
|
134
134
|
].filter(Boolean);
|
|
135
|
-
throw new
|
|
135
|
+
throw new CliError(`${err.message} [${parts.join(' | ')}]`);
|
|
136
136
|
}
|
|
137
|
-
const chosen = await
|
|
138
|
-
|
|
137
|
+
const chosen = await pickOrg(orgs);
|
|
138
|
+
writeConfig({
|
|
139
139
|
version: 1,
|
|
140
140
|
env,
|
|
141
141
|
api_url: apiUrl,
|
|
@@ -150,14 +150,16 @@ exports.loginCommand = (0, citty_1.defineCommand)({
|
|
|
150
150
|
current_org_id: chosen.id,
|
|
151
151
|
current_org_name: chosen.name,
|
|
152
152
|
});
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
153
|
+
logger.log(ui.success(`Logged in as ${colors.highlight(payload.user_email)}`));
|
|
154
|
+
logger.log(ui.branch([
|
|
155
|
+
...ui.fields([['organization', colors.highlight(chosen.name)]]),
|
|
156
|
+
...(orgs.length > 1
|
|
157
|
+
? [ui.note(`Switch orgs later with ${colors.highlight('dcd switch-org')}`)]
|
|
158
|
+
: []),
|
|
159
|
+
]));
|
|
158
160
|
}
|
|
159
161
|
catch (error) {
|
|
160
|
-
|
|
162
|
+
logger.error(error, { exit: 1 });
|
|
161
163
|
}
|
|
162
164
|
},
|
|
163
165
|
});
|
|
@@ -168,7 +170,7 @@ function openBrowser(url) {
|
|
|
168
170
|
? { bin: 'cmd', args: ['/c', 'start', '""', url] }
|
|
169
171
|
: { bin: 'xdg-open', args: [url] };
|
|
170
172
|
try {
|
|
171
|
-
const child =
|
|
173
|
+
const child = spawn(cmd.bin, cmd.args, { detached: true, stdio: 'ignore' });
|
|
172
174
|
child.unref();
|
|
173
175
|
return true;
|
|
174
176
|
}
|
|
@@ -207,9 +209,9 @@ async function pollForClaim(apiUrl, state, codeVerifier) {
|
|
|
207
209
|
continue;
|
|
208
210
|
}
|
|
209
211
|
const body = await res.text().catch(() => '');
|
|
210
|
-
throw new
|
|
212
|
+
throw new CliError(`Login failed: ${res.status} ${res.statusText}${body ? ` — ${body}` : ''}`);
|
|
211
213
|
}
|
|
212
|
-
throw new
|
|
214
|
+
throw new CliError('Login timed out. Please run `dcd login` again.');
|
|
213
215
|
}
|
|
214
216
|
function base64url(buf) {
|
|
215
217
|
return buf.toString('base64').replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
|
|
@@ -247,4 +249,4 @@ function decodeJwtIssuer(token) {
|
|
|
247
249
|
function sleep(ms) {
|
|
248
250
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
249
251
|
}
|
|
250
|
-
|
|
252
|
+
export default loginCommand;
|
package/dist/commands/logout.js
CHANGED
|
@@ -1,32 +1,30 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.logoutCommand = void 0;
|
|
4
1
|
/**
|
|
5
2
|
* `dcd logout` — clears the stored Supabase session and best-effort revokes
|
|
6
3
|
* it on Supabase. Leaves DEVICE_CLOUD_API_KEY (env/flag) untouched.
|
|
7
4
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
5
|
+
import { defineCommand } from 'citty';
|
|
6
|
+
import { ENVIRONMENTS } from '../config/environments.js';
|
|
7
|
+
import { CliAuthGateway } from '../gateways/cli-auth-gateway.js';
|
|
8
|
+
import { logger } from '../utils/cli.js';
|
|
9
|
+
import { clearConfig, getConfigPath, readConfig } from '../utils/config-store.js';
|
|
10
|
+
import { colors } from '../utils/styling.js';
|
|
11
|
+
import { ui } from '../utils/ui.js';
|
|
12
|
+
export const logoutCommand = defineCommand({
|
|
15
13
|
meta: {
|
|
16
14
|
name: 'logout',
|
|
17
15
|
description: 'Clear the stored devicecloud.dev session',
|
|
18
16
|
},
|
|
19
17
|
async run() {
|
|
20
|
-
const config =
|
|
18
|
+
const config = readConfig();
|
|
21
19
|
if (!config?.session) {
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
logger.log(ui.info(`No active session found (${colors.dim(getConfigPath())}).`));
|
|
21
|
+
clearConfig();
|
|
24
22
|
return;
|
|
25
23
|
}
|
|
26
|
-
const { anonKey } =
|
|
27
|
-
await
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
const { anonKey } = ENVIRONMENTS[config.env].supabase;
|
|
25
|
+
await CliAuthGateway.signOut(config.supabase_url, anonKey, config.session);
|
|
26
|
+
clearConfig();
|
|
27
|
+
logger.log(ui.success(`Logged out ${colors.dim(`(${config.session.user_email})`)}`));
|
|
30
28
|
},
|
|
31
29
|
});
|
|
32
|
-
|
|
30
|
+
export default logoutCommand;
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
export declare const statusCommand: import("citty").CommandDef<{
|
|
2
|
-
json: {
|
|
3
|
-
type: "boolean";
|
|
4
|
-
description:
|
|
2
|
+
readonly json: {
|
|
3
|
+
readonly type: "boolean";
|
|
4
|
+
readonly description: "output in json format";
|
|
5
5
|
};
|
|
6
|
-
name: {
|
|
7
|
-
type: "string";
|
|
8
|
-
description:
|
|
6
|
+
readonly name: {
|
|
7
|
+
readonly type: "string";
|
|
8
|
+
readonly description: "Name of the upload to check status for";
|
|
9
9
|
};
|
|
10
|
-
'upload-id': {
|
|
11
|
-
type: "string";
|
|
12
|
-
description:
|
|
10
|
+
readonly 'upload-id': {
|
|
11
|
+
readonly type: "string";
|
|
12
|
+
readonly description: "UUID of the upload to check status for";
|
|
13
13
|
};
|
|
14
|
-
'api-key': {
|
|
14
|
+
readonly 'api-key': {
|
|
15
15
|
readonly type: "string";
|
|
16
16
|
readonly alias: ["apiKey"];
|
|
17
17
|
readonly description: "API key for devicecloud.dev (find this in the console UI). You can also set the DEVICE_CLOUD_API_KEY environment variable.";
|
|
18
18
|
};
|
|
19
|
-
'api-url': {
|
|
19
|
+
readonly 'api-url': {
|
|
20
20
|
readonly type: "string";
|
|
21
21
|
readonly alias: ["apiURL", "apiUrl"];
|
|
22
22
|
readonly description: "API base URL (defaults to the URL stored by `dcd login`, else prod)";
|
package/dist/commands/status.js
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const connectivity_1 = require("../utils/connectivity");
|
|
12
|
-
const styling_1 = require("../utils/styling");
|
|
1
|
+
import { defineCommand } from 'citty';
|
|
2
|
+
import { apiFlags } from '../config/flags/api.flags.js';
|
|
3
|
+
import { ApiGateway } from '../gateways/api-gateway.js';
|
|
4
|
+
import { formatDurationSeconds } from '../methods.js';
|
|
5
|
+
import { resolveAuth } from '../utils/auth.js';
|
|
6
|
+
import { CliError, logger } from '../utils/cli.js';
|
|
7
|
+
import { resolveApiUrl } from '../utils/config-store.js';
|
|
8
|
+
import { checkInternetConnectivity, } from '../utils/connectivity.js';
|
|
9
|
+
import { colors, formatId, formatUrl } from '../utils/styling.js';
|
|
10
|
+
import { ui } from '../utils/ui.js';
|
|
13
11
|
/** Errors the API gateway surfaces for 4xx-class failures — retrying is pointless. */
|
|
14
12
|
function isClientApiError(error) {
|
|
15
13
|
if (!error)
|
|
@@ -30,13 +28,13 @@ function formatDateTime(isoString) {
|
|
|
30
28
|
return isoString;
|
|
31
29
|
}
|
|
32
30
|
}
|
|
33
|
-
|
|
31
|
+
export const statusCommand = defineCommand({
|
|
34
32
|
meta: {
|
|
35
33
|
name: 'status',
|
|
36
34
|
description: 'Get the status of an upload by name or upload ID',
|
|
37
35
|
},
|
|
38
36
|
args: {
|
|
39
|
-
...
|
|
37
|
+
...apiFlags,
|
|
40
38
|
json: {
|
|
41
39
|
type: 'boolean',
|
|
42
40
|
description: 'output in json format',
|
|
@@ -53,7 +51,7 @@ exports.statusCommand = (0, citty_1.defineCommand)({
|
|
|
53
51
|
// eslint-disable-next-line complexity
|
|
54
52
|
async run({ args }) {
|
|
55
53
|
const apiKeyFlag = args['api-key'];
|
|
56
|
-
const apiUrl =
|
|
54
|
+
const apiUrl = resolveApiUrl(args['api-url']);
|
|
57
55
|
const json = Boolean(args.json);
|
|
58
56
|
const name = args.name;
|
|
59
57
|
const uploadId = args['upload-id'];
|
|
@@ -61,17 +59,17 @@ exports.statusCommand = (0, citty_1.defineCommand)({
|
|
|
61
59
|
await statusMain({ apiKeyFlag, apiUrl, json, name, uploadId });
|
|
62
60
|
}
|
|
63
61
|
catch (error) {
|
|
64
|
-
|
|
62
|
+
logger.error(error, { exit: 1, json });
|
|
65
63
|
}
|
|
66
64
|
},
|
|
67
65
|
});
|
|
68
66
|
async function statusMain({ apiKeyFlag, apiUrl, json, name, uploadId, }) {
|
|
69
|
-
const auth = await
|
|
67
|
+
const auth = await resolveAuth({ apiKeyFlag });
|
|
70
68
|
if (name && uploadId) {
|
|
71
|
-
throw new
|
|
69
|
+
throw new CliError('Cannot provide both --name and --upload-id. These options are mutually exclusive.');
|
|
72
70
|
}
|
|
73
71
|
if (!name && !uploadId) {
|
|
74
|
-
throw new
|
|
72
|
+
throw new CliError('Either --name or --upload-id must be provided');
|
|
75
73
|
}
|
|
76
74
|
let lastError = null;
|
|
77
75
|
let status = null;
|
|
@@ -79,7 +77,7 @@ async function statusMain({ apiKeyFlag, apiUrl, json, name, uploadId, }) {
|
|
|
79
77
|
for (let attempt = 1; attempt <= 5; attempt++) {
|
|
80
78
|
try {
|
|
81
79
|
attemptsMade = attempt;
|
|
82
|
-
status = (await
|
|
80
|
+
status = (await ApiGateway.getUploadStatus(apiUrl, auth, {
|
|
83
81
|
name,
|
|
84
82
|
uploadId,
|
|
85
83
|
}));
|
|
@@ -93,7 +91,7 @@ async function statusMain({ apiKeyFlag, apiUrl, json, name, uploadId, }) {
|
|
|
93
91
|
break;
|
|
94
92
|
}
|
|
95
93
|
if (attempt < 5) {
|
|
96
|
-
|
|
94
|
+
logger.log(isNetworkError
|
|
97
95
|
? `Network error on attempt ${attempt}/5. Retrying...`
|
|
98
96
|
: `Request failed on attempt ${attempt}/5. Retrying...`);
|
|
99
97
|
await new Promise((resolve) => {
|
|
@@ -115,9 +113,9 @@ async function statusMain({ apiKeyFlag, apiUrl, json, name, uploadId, }) {
|
|
|
115
113
|
}, null, 2));
|
|
116
114
|
return;
|
|
117
115
|
}
|
|
118
|
-
throw new
|
|
116
|
+
throw new CliError(errorMessage);
|
|
119
117
|
}
|
|
120
|
-
const connectivityCheck = await
|
|
118
|
+
const connectivityCheck = await checkInternetConnectivity();
|
|
121
119
|
let errorMessage;
|
|
122
120
|
if (connectivityCheck.connected) {
|
|
123
121
|
errorMessage = `Failed to get status after ${attemptsMade} attempt${attemptsMade > 1 ? 's' : ''}. Internet appears functional but unable to reach API. Last error: ${lastError?.message || 'Unknown error'}`;
|
|
@@ -143,7 +141,7 @@ async function statusMain({ apiKeyFlag, apiUrl, json, name, uploadId, }) {
|
|
|
143
141
|
}, null, 2));
|
|
144
142
|
return;
|
|
145
143
|
}
|
|
146
|
-
throw new
|
|
144
|
+
throw new CliError(errorMessage);
|
|
147
145
|
}
|
|
148
146
|
try {
|
|
149
147
|
if (json) {
|
|
@@ -152,42 +150,46 @@ async function statusMain({ apiKeyFlag, apiUrl, json, name, uploadId, }) {
|
|
|
152
150
|
console.log(JSON.stringify({ ...rest, tests }, null, 2));
|
|
153
151
|
return;
|
|
154
152
|
}
|
|
155
|
-
|
|
156
|
-
cli_1.logger.log(` ${(0, styling_1.formatStatus)(status.status)}`);
|
|
153
|
+
const fields = [];
|
|
157
154
|
if (status.name) {
|
|
158
|
-
|
|
155
|
+
fields.push(['name', colors.bold(status.name)]);
|
|
159
156
|
}
|
|
160
157
|
if (status.uploadId) {
|
|
161
|
-
|
|
158
|
+
fields.push(['upload id', formatId(status.uploadId)]);
|
|
162
159
|
}
|
|
163
160
|
if (status.appBinaryId) {
|
|
164
|
-
|
|
161
|
+
fields.push(['binary id', formatId(status.appBinaryId)]);
|
|
165
162
|
}
|
|
166
163
|
if (status.createdAt) {
|
|
167
|
-
|
|
164
|
+
fields.push(['created', formatDateTime(status.createdAt)]);
|
|
168
165
|
}
|
|
169
166
|
if (status.consoleUrl) {
|
|
170
|
-
|
|
167
|
+
fields.push(['console', formatUrl(status.consoleUrl)]);
|
|
171
168
|
}
|
|
169
|
+
logger.log(ui.section('Upload Status'));
|
|
170
|
+
logger.log(ui.branch([ui.status(status.status), ...ui.fields(fields)]));
|
|
172
171
|
if (status.tests.length > 0) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
cli_1.logger.log(` ${styling_1.colors.error('Fail reason:')} ${item.failReason}`);
|
|
178
|
-
}
|
|
172
|
+
logger.log(ui.section('Test Results'));
|
|
173
|
+
logger.log(ui.branch(status.tests.map((item) => {
|
|
174
|
+
const head = `${ui.statusSymbol(item.status)} ${item.name}`;
|
|
175
|
+
const meta = [];
|
|
179
176
|
if (item.durationSeconds) {
|
|
180
|
-
|
|
177
|
+
meta.push(formatDurationSeconds(item.durationSeconds));
|
|
181
178
|
}
|
|
182
179
|
if (item.createdAt) {
|
|
183
|
-
|
|
180
|
+
meta.push(colors.dim(formatDateTime(item.createdAt)));
|
|
184
181
|
}
|
|
185
|
-
|
|
186
|
-
|
|
182
|
+
if (item.status === 'FAILED' && item.failReason) {
|
|
183
|
+
meta.push(colors.error(item.failReason));
|
|
184
|
+
}
|
|
185
|
+
return meta.length > 0
|
|
186
|
+
? `${head} ${colors.dim('·')} ${meta.join(colors.dim(' · '))}`
|
|
187
|
+
: head;
|
|
188
|
+
})));
|
|
187
189
|
}
|
|
188
190
|
}
|
|
189
191
|
catch (error) {
|
|
190
|
-
throw new
|
|
192
|
+
throw new CliError(`Failed to get status: ${error.message}`);
|
|
191
193
|
}
|
|
192
194
|
}
|
|
193
|
-
|
|
195
|
+
export default statusCommand;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
export declare const switchOrgCommand: import("citty").CommandDef<{
|
|
2
|
-
'api-url': {
|
|
3
|
-
type: "string";
|
|
4
|
-
description:
|
|
2
|
+
readonly 'api-url': {
|
|
3
|
+
readonly type: "string";
|
|
4
|
+
readonly description: "API base URL (defaults to the URL stored by `dcd login`)";
|
|
5
5
|
};
|
|
6
|
-
org: {
|
|
7
|
-
type: "positional";
|
|
8
|
-
required: false;
|
|
9
|
-
description:
|
|
6
|
+
readonly org: {
|
|
7
|
+
readonly type: "positional";
|
|
8
|
+
readonly required: false;
|
|
9
|
+
readonly description: "Org name to switch to (omit for an interactive picker)";
|
|
10
10
|
};
|
|
11
11
|
}>;
|
|
12
12
|
export default switchOrgCommand;
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.switchOrgCommand = void 0;
|
|
4
1
|
/**
|
|
5
2
|
* `dcd switch-org` — changes the active org on the stored session.
|
|
6
3
|
*
|
|
@@ -8,13 +5,14 @@ exports.switchOrgCommand = void 0;
|
|
|
8
5
|
* confirming the user is a member via GET /me/orgs.
|
|
9
6
|
* Without an argument: fetch orgs and prompt the user to pick one.
|
|
10
7
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
8
|
+
import { defineCommand } from 'citty';
|
|
9
|
+
import { resolveAuth } from '../utils/auth.js';
|
|
10
|
+
import { CliError, logger } from '../utils/cli.js';
|
|
11
|
+
import { readConfig, resolveApiUrl, writeConfig } from '../utils/config-store.js';
|
|
12
|
+
import { fetchOrgs, pickOrg } from '../utils/orgs.js';
|
|
13
|
+
import { colors } from '../utils/styling.js';
|
|
14
|
+
import { ui } from '../utils/ui.js';
|
|
15
|
+
export const switchOrgCommand = defineCommand({
|
|
18
16
|
meta: {
|
|
19
17
|
name: 'switch-org',
|
|
20
18
|
description: 'Switch the active organization for the logged-in session',
|
|
@@ -31,31 +29,31 @@ exports.switchOrgCommand = (0, citty_1.defineCommand)({
|
|
|
31
29
|
},
|
|
32
30
|
},
|
|
33
31
|
async run({ args }) {
|
|
34
|
-
const config =
|
|
32
|
+
const config = readConfig();
|
|
35
33
|
if (!config?.session) {
|
|
36
|
-
throw new
|
|
34
|
+
throw new CliError('Not logged in. Run `dcd login` first.');
|
|
37
35
|
}
|
|
38
36
|
// Honor the env the user logged into — defaulting to prod here would send
|
|
39
37
|
// a dev Bearer token to the prod API.
|
|
40
|
-
const apiUrl =
|
|
38
|
+
const apiUrl = resolveApiUrl(args['api-url']);
|
|
41
39
|
const target = args.org;
|
|
42
40
|
// sessionOnly: an exported DEVICE_CLOUD_API_KEY must not shadow the
|
|
43
41
|
// browser session this command requires.
|
|
44
|
-
const auth = await
|
|
45
|
-
const orgs = await
|
|
42
|
+
const auth = await resolveAuth({ apiKeyFlag: undefined, sessionOnly: true });
|
|
43
|
+
const orgs = await fetchOrgs(apiUrl, auth.headers);
|
|
46
44
|
let chosen;
|
|
47
45
|
if (target) {
|
|
48
46
|
chosen = matchOrg(orgs, target);
|
|
49
47
|
}
|
|
50
48
|
else {
|
|
51
|
-
chosen = await
|
|
49
|
+
chosen = await pickOrg(orgs);
|
|
52
50
|
}
|
|
53
|
-
|
|
51
|
+
writeConfig({
|
|
54
52
|
...config,
|
|
55
53
|
current_org_id: chosen.id,
|
|
56
54
|
current_org_name: chosen.name,
|
|
57
55
|
});
|
|
58
|
-
|
|
56
|
+
logger.log(ui.success(`Switched to ${colors.highlight(chosen.name)}`));
|
|
59
57
|
},
|
|
60
58
|
});
|
|
61
59
|
/**
|
|
@@ -67,12 +65,12 @@ function matchOrg(orgs, target) {
|
|
|
67
65
|
const needle = target.toLowerCase();
|
|
68
66
|
const nameMatches = orgs.filter((o) => o.name.toLowerCase() === needle);
|
|
69
67
|
if (nameMatches.length > 1) {
|
|
70
|
-
throw new
|
|
68
|
+
throw new CliError(`Multiple orgs named "${target}". Run \`dcd switch-org\` without arguments to pick interactively.`);
|
|
71
69
|
}
|
|
72
70
|
const chosen = nameMatches[0] ?? orgs.find((o) => o.slug === target || o.id === target);
|
|
73
71
|
if (!chosen) {
|
|
74
|
-
throw new
|
|
72
|
+
throw new CliError(`No org named "${target}". Available: ${orgs.map((o) => o.name).join(', ')}`);
|
|
75
73
|
}
|
|
76
74
|
return chosen;
|
|
77
75
|
}
|
|
78
|
-
|
|
76
|
+
export default switchOrgCommand;
|