@devicecloud.dev/dcd 4.4.9 → 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 +75 -2
- package/dist/commands/artifacts.d.ts +47 -18
- package/dist/commands/artifacts.js +69 -64
- package/dist/commands/cloud.d.ts +228 -88
- package/dist/commands/cloud.js +430 -342
- package/dist/commands/list.d.ts +39 -38
- package/dist/commands/list.js +124 -131
- package/dist/commands/live.d.ts +2 -0
- package/dist/commands/live.js +520 -0
- package/dist/commands/login.d.ts +17 -0
- package/dist/commands/login.js +252 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.js +30 -0
- package/dist/commands/status.d.ts +23 -42
- package/dist/commands/status.js +170 -179
- package/dist/commands/switch-org.d.ts +12 -0
- package/dist/commands/switch-org.js +76 -0
- package/dist/commands/upgrade.d.ts +2 -0
- package/dist/commands/upgrade.js +120 -0
- package/dist/commands/upload.d.ts +33 -18
- package/dist/commands/upload.js +72 -78
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.js +31 -0
- package/dist/config/environments.d.ts +31 -0
- package/dist/config/environments.js +52 -0
- package/dist/config/flags/api.flags.d.ts +10 -2
- package/dist/config/flags/api.flags.js +13 -14
- package/dist/config/flags/binary.flags.d.ts +17 -4
- package/dist/config/flags/binary.flags.js +14 -18
- package/dist/config/flags/device.flags.d.ts +49 -11
- package/dist/config/flags/device.flags.js +43 -38
- package/dist/config/flags/environment.flags.d.ts +27 -6
- package/dist/config/flags/environment.flags.js +24 -29
- package/dist/config/flags/execution.flags.d.ts +35 -8
- package/dist/config/flags/execution.flags.js +31 -41
- package/dist/config/flags/github.flags.d.ts +23 -5
- package/dist/config/flags/github.flags.js +19 -15
- package/dist/config/flags/output.flags.d.ts +57 -13
- package/dist/config/flags/output.flags.js +48 -47
- package/dist/constants.d.ts +218 -51
- package/dist/constants.js +17 -20
- package/dist/gateways/api-gateway.d.ts +72 -16
- package/dist/gateways/api-gateway.js +298 -104
- package/dist/gateways/cli-auth-gateway.d.ts +13 -0
- package/dist/gateways/cli-auth-gateway.js +54 -0
- package/dist/gateways/realtime-gateway.d.ts +32 -0
- package/dist/gateways/realtime-gateway.js +103 -0
- package/dist/gateways/supabase-gateway.d.ts +11 -11
- package/dist/gateways/supabase-gateway.js +20 -48
- package/dist/index.d.ts +2 -1
- package/dist/index.js +98 -4
- 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 +34 -5
- package/dist/methods.js +266 -215
- package/dist/services/device-validation.service.d.ts +9 -1
- package/dist/services/device-validation.service.js +56 -40
- package/dist/services/execution-plan.service.js +40 -31
- package/dist/services/execution-plan.utils.d.ts +3 -0
- package/dist/services/execution-plan.utils.js +25 -55
- package/dist/services/flow-paths.d.ts +17 -0
- package/dist/services/flow-paths.js +52 -0
- package/dist/services/metadata-extractor.service.d.ts +0 -2
- package/dist/services/metadata-extractor.service.js +75 -78
- package/dist/services/moropo.service.js +33 -34
- package/dist/services/report-download.service.d.ts +12 -1
- package/dist/services/report-download.service.js +34 -27
- package/dist/services/results-polling.service.d.ts +23 -9
- package/dist/services/results-polling.service.js +257 -123
- package/dist/services/telemetry.service.d.ts +49 -0
- package/dist/services/telemetry.service.js +252 -0
- package/dist/services/test-submission.service.d.ts +21 -4
- package/dist/services/test-submission.service.js +51 -33
- package/dist/services/version.service.d.ts +4 -3
- package/dist/services/version.service.js +28 -16
- package/dist/types/domain/auth.types.d.ts +20 -0
- package/dist/types/domain/auth.types.js +1 -0
- package/dist/types/domain/device.types.js +8 -11
- package/dist/types/domain/live.types.d.ts +76 -0
- package/dist/types/domain/live.types.js +3 -0
- 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 +13 -0
- package/dist/utils/auth.js +141 -0
- package/dist/utils/ci.d.ts +12 -0
- package/dist/utils/ci.js +39 -0
- package/dist/utils/cli.d.ts +35 -0
- package/dist/utils/cli.js +118 -0
- package/dist/utils/compatibility.d.ts +2 -1
- package/dist/utils/compatibility.js +6 -8
- package/dist/utils/config-store.d.ts +35 -0
- package/dist/utils/config-store.js +115 -0
- package/dist/utils/connectivity.js +8 -7
- package/dist/utils/expo.js +29 -24
- package/dist/utils/orgs.d.ts +11 -0
- package/dist/utils/orgs.js +36 -0
- package/dist/utils/paths.d.ts +11 -0
- package/dist/utils/paths.js +21 -0
- package/dist/utils/progress.d.ts +13 -0
- package/dist/utils/progress.js +47 -0
- package/dist/utils/styling.d.ts +42 -36
- package/dist/utils/styling.js +78 -82
- package/dist/utils/ui.d.ts +41 -0
- package/dist/utils/ui.js +95 -0
- package/package.json +36 -45
- package/bin/dev.cmd +0 -3
- package/bin/dev.js +0 -6
- package/bin/run.cmd +0 -3
- package/bin/run.js +0 -7
- package/dist/types/schema.types.d.ts +0 -2702
- package/dist/types/schema.types.js +0 -3
- package/oclif.manifest.json +0 -884
package/dist/commands/status.js
CHANGED
|
@@ -1,204 +1,195 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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';
|
|
11
|
+
/** Errors the API gateway surfaces for 4xx-class failures — retrying is pointless. */
|
|
12
|
+
function isClientApiError(error) {
|
|
13
|
+
if (!error)
|
|
14
|
+
return false;
|
|
15
|
+
return (error.message.includes('Invalid request:') ||
|
|
16
|
+
error.message.includes('Resource not found') ||
|
|
17
|
+
error.message.includes('Authentication failed') ||
|
|
18
|
+
error.message.includes('Access denied') ||
|
|
19
|
+
error.message.includes('Invalid API key') ||
|
|
20
|
+
error.message.includes('Rate limit exceeded'));
|
|
21
|
+
}
|
|
22
|
+
function formatDateTime(isoString) {
|
|
23
|
+
try {
|
|
24
|
+
const date = new Date(isoString);
|
|
25
|
+
return date.toLocaleString();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return isoString;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export const statusCommand = defineCommand({
|
|
32
|
+
meta: {
|
|
33
|
+
name: 'status',
|
|
34
|
+
description: 'Get the status of an upload by name or upload ID',
|
|
35
|
+
},
|
|
36
|
+
args: {
|
|
37
|
+
...apiFlags,
|
|
38
|
+
json: {
|
|
39
|
+
type: 'boolean',
|
|
20
40
|
description: 'output in json format',
|
|
21
|
-
}
|
|
22
|
-
name:
|
|
41
|
+
},
|
|
42
|
+
name: {
|
|
43
|
+
type: 'string',
|
|
23
44
|
description: 'Name of the upload to check status for',
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
45
|
+
},
|
|
46
|
+
'upload-id': {
|
|
47
|
+
type: 'string',
|
|
27
48
|
description: 'UUID of the upload to check status for',
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
};
|
|
49
|
+
},
|
|
50
|
+
},
|
|
31
51
|
// eslint-disable-next-line complexity
|
|
32
|
-
async run() {
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
52
|
+
async run({ args }) {
|
|
53
|
+
const apiKeyFlag = args['api-key'];
|
|
54
|
+
const apiUrl = resolveApiUrl(args['api-url']);
|
|
55
|
+
const json = Boolean(args.json);
|
|
56
|
+
const name = args.name;
|
|
57
|
+
const uploadId = args['upload-id'];
|
|
58
|
+
try {
|
|
59
|
+
await statusMain({ apiKeyFlag, apiUrl, json, name, uploadId });
|
|
39
60
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return;
|
|
61
|
+
catch (error) {
|
|
62
|
+
logger.error(error, { exit: 1, json });
|
|
43
63
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
async function statusMain({ apiKeyFlag, apiUrl, json, name, uploadId, }) {
|
|
67
|
+
const auth = await resolveAuth({ apiKeyFlag });
|
|
68
|
+
if (name && uploadId) {
|
|
69
|
+
throw new CliError('Cannot provide both --name and --upload-id. These options are mutually exclusive.');
|
|
70
|
+
}
|
|
71
|
+
if (!name && !uploadId) {
|
|
72
|
+
throw new CliError('Either --name or --upload-id must be provided');
|
|
73
|
+
}
|
|
74
|
+
let lastError = null;
|
|
75
|
+
let status = null;
|
|
76
|
+
let attemptsMade = 0;
|
|
77
|
+
for (let attempt = 1; attempt <= 5; attempt++) {
|
|
78
|
+
try {
|
|
79
|
+
attemptsMade = attempt;
|
|
80
|
+
status = (await ApiGateway.getUploadStatus(apiUrl, auth, {
|
|
81
|
+
name,
|
|
82
|
+
uploadId,
|
|
83
|
+
}));
|
|
84
|
+
break;
|
|
47
85
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
attemptsMade = attempt;
|
|
54
|
-
status = (await api_gateway_1.ApiGateway.getUploadStatus(apiUrl, apiKey, {
|
|
55
|
-
name,
|
|
56
|
-
uploadId,
|
|
57
|
-
}));
|
|
86
|
+
catch (error) {
|
|
87
|
+
lastError = error;
|
|
88
|
+
const isNetworkError = lastError.name === 'NetworkError' ||
|
|
89
|
+
(error instanceof TypeError && lastError.message === 'fetch failed');
|
|
90
|
+
if (isClientApiError(lastError)) {
|
|
58
91
|
break;
|
|
59
92
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
(
|
|
66
|
-
|
|
67
|
-
lastError.message.includes('Resource not found') ||
|
|
68
|
-
lastError.message.includes('Authentication failed') ||
|
|
69
|
-
lastError.message.includes('Access denied') ||
|
|
70
|
-
lastError.message.includes('Invalid API key') ||
|
|
71
|
-
lastError.message.includes('Rate limit exceeded');
|
|
72
|
-
// Don't retry client errors - they won't succeed on retry
|
|
73
|
-
if (isClientError) {
|
|
74
|
-
break;
|
|
75
|
-
}
|
|
76
|
-
// Only retry network errors
|
|
77
|
-
if (attempt < 5 && isNetworkError) {
|
|
78
|
-
this.log(`Network error on attempt ${attempt}/5. Retrying...`);
|
|
79
|
-
await new Promise((resolve) => {
|
|
80
|
-
setTimeout(resolve, 1000 * attempt);
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
else if (attempt < 5) {
|
|
84
|
-
// For other errors (server errors), retry but with different message
|
|
85
|
-
this.log(`Request failed on attempt ${attempt}/5. Retrying...`);
|
|
86
|
-
await new Promise((resolve) => {
|
|
87
|
-
setTimeout(resolve, 1000 * attempt);
|
|
88
|
-
});
|
|
89
|
-
}
|
|
93
|
+
if (attempt < 5) {
|
|
94
|
+
logger.log(isNetworkError
|
|
95
|
+
? `Network error on attempt ${attempt}/5. Retrying...`
|
|
96
|
+
: `Request failed on attempt ${attempt}/5. Retrying...`);
|
|
97
|
+
await new Promise((resolve) => {
|
|
98
|
+
setTimeout(resolve, 1000 * attempt);
|
|
99
|
+
});
|
|
90
100
|
}
|
|
91
101
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
lastError.message.includes('Authentication failed') ||
|
|
97
|
-
lastError.message.includes('Access denied') ||
|
|
98
|
-
lastError.message.includes('Invalid API key') ||
|
|
99
|
-
lastError.message.includes('Rate limit exceeded'));
|
|
100
|
-
if (isClientError) {
|
|
101
|
-
// For client errors, show the error immediately without connectivity check
|
|
102
|
-
const errorMessage = lastError?.message || 'Unknown error';
|
|
103
|
-
if (json) {
|
|
104
|
-
return {
|
|
105
|
-
status: 'FAILED',
|
|
106
|
-
error: errorMessage,
|
|
107
|
-
attempts: attemptsMade,
|
|
108
|
-
tests: [],
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
this.error(errorMessage);
|
|
112
|
-
}
|
|
113
|
-
// Check if the failure is due to internet connectivity issues
|
|
114
|
-
const connectivityCheck = await (0, connectivity_1.checkInternetConnectivity)();
|
|
115
|
-
let errorMessage;
|
|
116
|
-
if (connectivityCheck.connected) {
|
|
117
|
-
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'}`;
|
|
118
|
-
}
|
|
119
|
-
else {
|
|
120
|
-
// Build detailed error message with endpoint diagnostics
|
|
121
|
-
const endpointDetails = connectivityCheck.endpointResults
|
|
122
|
-
.map((r) => ` - ${r.endpoint}: ${r.error} (${r.latencyMs}ms)`)
|
|
123
|
-
.join('\n');
|
|
124
|
-
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'}`;
|
|
125
|
-
}
|
|
102
|
+
}
|
|
103
|
+
if (!status) {
|
|
104
|
+
if (isClientApiError(lastError)) {
|
|
105
|
+
const errorMessage = lastError?.message || 'Unknown error';
|
|
126
106
|
if (json) {
|
|
127
|
-
|
|
107
|
+
// eslint-disable-next-line no-console
|
|
108
|
+
console.log(JSON.stringify({
|
|
128
109
|
status: 'FAILED',
|
|
129
110
|
error: errorMessage,
|
|
130
111
|
attempts: attemptsMade,
|
|
131
|
-
connectivityCheck: {
|
|
132
|
-
connected: connectivityCheck.connected,
|
|
133
|
-
endpointResults: connectivityCheck.endpointResults,
|
|
134
|
-
message: connectivityCheck.message,
|
|
135
|
-
},
|
|
136
112
|
tests: [],
|
|
137
|
-
};
|
|
113
|
+
}, null, 2));
|
|
114
|
+
return;
|
|
138
115
|
}
|
|
139
|
-
|
|
116
|
+
throw new CliError(errorMessage);
|
|
140
117
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return {
|
|
146
|
-
...rest,
|
|
147
|
-
tests,
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
this.log((0, styling_1.sectionHeader)('Upload Status'));
|
|
151
|
-
// Display overall status
|
|
152
|
-
this.log(` ${(0, styling_1.formatStatus)(status.status)}`);
|
|
153
|
-
if (status.name) {
|
|
154
|
-
this.log(` ${styling_1.colors.dim('Name:')} ${styling_1.colors.bold(status.name)}`);
|
|
155
|
-
}
|
|
156
|
-
if (status.uploadId) {
|
|
157
|
-
this.log(` ${styling_1.colors.dim('Upload ID:')} ${(0, styling_1.formatId)(status.uploadId)}`);
|
|
158
|
-
}
|
|
159
|
-
if (status.appBinaryId) {
|
|
160
|
-
this.log(` ${styling_1.colors.dim('Binary ID:')} ${(0, styling_1.formatId)(status.appBinaryId)}`);
|
|
161
|
-
}
|
|
162
|
-
if (status.createdAt) {
|
|
163
|
-
this.log(` ${styling_1.colors.dim('Created:')} ${this.formatDateTime(status.createdAt)}`);
|
|
164
|
-
}
|
|
165
|
-
if (status.consoleUrl) {
|
|
166
|
-
this.log(` ${styling_1.colors.dim('Console:')} ${(0, styling_1.formatUrl)(status.consoleUrl)}`);
|
|
167
|
-
}
|
|
168
|
-
if (status.tests.length > 0) {
|
|
169
|
-
this.log((0, styling_1.sectionHeader)('Test Results'));
|
|
170
|
-
for (const item of status.tests) {
|
|
171
|
-
this.log(` ${(0, styling_1.formatStatus)(item.status)} ${styling_1.colors.bold(item.name)}`);
|
|
172
|
-
if (item.status === 'FAILED' && item.failReason) {
|
|
173
|
-
this.log(` ${styling_1.colors.error('Fail reason:')} ${item.failReason}`);
|
|
174
|
-
}
|
|
175
|
-
if (item.durationSeconds) {
|
|
176
|
-
this.log(` ${styling_1.colors.dim('Duration:')} ${(0, methods_1.formatDurationSeconds)(item.durationSeconds)}`);
|
|
177
|
-
}
|
|
178
|
-
if (item.createdAt) {
|
|
179
|
-
this.log(` ${styling_1.colors.dim('Created:')} ${this.formatDateTime(item.createdAt)}`);
|
|
180
|
-
}
|
|
181
|
-
this.log('');
|
|
182
|
-
}
|
|
183
|
-
}
|
|
118
|
+
const connectivityCheck = await checkInternetConnectivity();
|
|
119
|
+
let errorMessage;
|
|
120
|
+
if (connectivityCheck.connected) {
|
|
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'}`;
|
|
184
122
|
}
|
|
185
|
-
|
|
186
|
-
|
|
123
|
+
else {
|
|
124
|
+
const endpointDetails = connectivityCheck.endpointResults
|
|
125
|
+
.map((r) => ` - ${r.endpoint}: ${r.error} (${r.latencyMs}ms)`)
|
|
126
|
+
.join('\n');
|
|
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'}`;
|
|
128
|
+
}
|
|
129
|
+
if (json) {
|
|
130
|
+
// eslint-disable-next-line no-console
|
|
131
|
+
console.log(JSON.stringify({
|
|
132
|
+
status: 'FAILED',
|
|
133
|
+
error: errorMessage,
|
|
134
|
+
attempts: attemptsMade,
|
|
135
|
+
connectivityCheck: {
|
|
136
|
+
connected: connectivityCheck.connected,
|
|
137
|
+
endpointResults: connectivityCheck.endpointResults,
|
|
138
|
+
message: connectivityCheck.message,
|
|
139
|
+
},
|
|
140
|
+
tests: [],
|
|
141
|
+
}, null, 2));
|
|
142
|
+
return;
|
|
187
143
|
}
|
|
144
|
+
throw new CliError(errorMessage);
|
|
188
145
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
146
|
+
try {
|
|
147
|
+
if (json) {
|
|
148
|
+
const { tests, ...rest } = status;
|
|
149
|
+
// eslint-disable-next-line no-console
|
|
150
|
+
console.log(JSON.stringify({ ...rest, tests }, null, 2));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const fields = [];
|
|
154
|
+
if (status.name) {
|
|
155
|
+
fields.push(['name', colors.bold(status.name)]);
|
|
156
|
+
}
|
|
157
|
+
if (status.uploadId) {
|
|
158
|
+
fields.push(['upload id', formatId(status.uploadId)]);
|
|
159
|
+
}
|
|
160
|
+
if (status.appBinaryId) {
|
|
161
|
+
fields.push(['binary id', formatId(status.appBinaryId)]);
|
|
162
|
+
}
|
|
163
|
+
if (status.createdAt) {
|
|
164
|
+
fields.push(['created', formatDateTime(status.createdAt)]);
|
|
198
165
|
}
|
|
199
|
-
|
|
200
|
-
|
|
166
|
+
if (status.consoleUrl) {
|
|
167
|
+
fields.push(['console', formatUrl(status.consoleUrl)]);
|
|
201
168
|
}
|
|
169
|
+
logger.log(ui.section('Upload Status'));
|
|
170
|
+
logger.log(ui.branch([ui.status(status.status), ...ui.fields(fields)]));
|
|
171
|
+
if (status.tests.length > 0) {
|
|
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 = [];
|
|
176
|
+
if (item.durationSeconds) {
|
|
177
|
+
meta.push(formatDurationSeconds(item.durationSeconds));
|
|
178
|
+
}
|
|
179
|
+
if (item.createdAt) {
|
|
180
|
+
meta.push(colors.dim(formatDateTime(item.createdAt)));
|
|
181
|
+
}
|
|
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
|
+
})));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
throw new CliError(`Failed to get status: ${error.message}`);
|
|
202
193
|
}
|
|
203
194
|
}
|
|
204
|
-
|
|
195
|
+
export default statusCommand;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const switchOrgCommand: import("citty").CommandDef<{
|
|
2
|
+
readonly 'api-url': {
|
|
3
|
+
readonly type: "string";
|
|
4
|
+
readonly description: "API base URL (defaults to the URL stored by `dcd login`)";
|
|
5
|
+
};
|
|
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
|
+
};
|
|
11
|
+
}>;
|
|
12
|
+
export default switchOrgCommand;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dcd switch-org` — changes the active org on the stored session.
|
|
3
|
+
*
|
|
4
|
+
* With an argument (`dcd switch-org <name>`): update config immediately after
|
|
5
|
+
* confirming the user is a member via GET /me/orgs.
|
|
6
|
+
* Without an argument: fetch orgs and prompt the user to pick one.
|
|
7
|
+
*/
|
|
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({
|
|
16
|
+
meta: {
|
|
17
|
+
name: 'switch-org',
|
|
18
|
+
description: 'Switch the active organization for the logged-in session',
|
|
19
|
+
},
|
|
20
|
+
args: {
|
|
21
|
+
'api-url': {
|
|
22
|
+
type: 'string',
|
|
23
|
+
description: 'API base URL (defaults to the URL stored by `dcd login`)',
|
|
24
|
+
},
|
|
25
|
+
org: {
|
|
26
|
+
type: 'positional',
|
|
27
|
+
required: false,
|
|
28
|
+
description: 'Org name to switch to (omit for an interactive picker)',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
async run({ args }) {
|
|
32
|
+
const config = readConfig();
|
|
33
|
+
if (!config?.session) {
|
|
34
|
+
throw new CliError('Not logged in. Run `dcd login` first.');
|
|
35
|
+
}
|
|
36
|
+
// Honor the env the user logged into — defaulting to prod here would send
|
|
37
|
+
// a dev Bearer token to the prod API.
|
|
38
|
+
const apiUrl = resolveApiUrl(args['api-url']);
|
|
39
|
+
const target = args.org;
|
|
40
|
+
// sessionOnly: an exported DEVICE_CLOUD_API_KEY must not shadow the
|
|
41
|
+
// browser session this command requires.
|
|
42
|
+
const auth = await resolveAuth({ apiKeyFlag: undefined, sessionOnly: true });
|
|
43
|
+
const orgs = await fetchOrgs(apiUrl, auth.headers);
|
|
44
|
+
let chosen;
|
|
45
|
+
if (target) {
|
|
46
|
+
chosen = matchOrg(orgs, target);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
chosen = await pickOrg(orgs);
|
|
50
|
+
}
|
|
51
|
+
writeConfig({
|
|
52
|
+
...config,
|
|
53
|
+
current_org_id: chosen.id,
|
|
54
|
+
current_org_name: chosen.name,
|
|
55
|
+
});
|
|
56
|
+
logger.log(ui.success(`Switched to ${colors.highlight(chosen.name)}`));
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
/**
|
|
60
|
+
* Match a user-supplied string to an org. Prefers case-insensitive name match.
|
|
61
|
+
* Falls back to slug / id so existing scripts keep working, but error output
|
|
62
|
+
* only surfaces names.
|
|
63
|
+
*/
|
|
64
|
+
function matchOrg(orgs, target) {
|
|
65
|
+
const needle = target.toLowerCase();
|
|
66
|
+
const nameMatches = orgs.filter((o) => o.name.toLowerCase() === needle);
|
|
67
|
+
if (nameMatches.length > 1) {
|
|
68
|
+
throw new CliError(`Multiple orgs named "${target}". Run \`dcd switch-org\` without arguments to pick interactively.`);
|
|
69
|
+
}
|
|
70
|
+
const chosen = nameMatches[0] ?? orgs.find((o) => o.slug === target || o.id === target);
|
|
71
|
+
if (!chosen) {
|
|
72
|
+
throw new CliError(`No org named "${target}". Available: ${orgs.map((o) => o.name).join(', ')}`);
|
|
73
|
+
}
|
|
74
|
+
return chosen;
|
|
75
|
+
}
|
|
76
|
+
export default switchOrgCommand;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dcd upgrade` — in-place replace of the standalone binary.
|
|
3
|
+
*
|
|
4
|
+
* Only meaningful for the bun-compiled binary install. npm-installed users
|
|
5
|
+
* are redirected to `npm install -g`. Fetches the latest version from the
|
|
6
|
+
* release manifest, downloads the matching binary, verifies its SHA256
|
|
7
|
+
* against the published SHA256SUMS, then atomically renames over the running
|
|
8
|
+
* executable (the old inode stays alive until the process exits).
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
import { chmodSync, createReadStream, createWriteStream, renameSync, unlinkSync, } from 'node:fs';
|
|
12
|
+
import { Readable } from 'node:stream';
|
|
13
|
+
import { pipeline } from 'node:stream/promises';
|
|
14
|
+
import { defineCommand } from 'citty';
|
|
15
|
+
import { VersionService } from '../services/version.service.js';
|
|
16
|
+
import { CliError, getCliVersion, getInstallMethod, logger, } from '../utils/cli.js';
|
|
17
|
+
import { colors } from '../utils/styling.js';
|
|
18
|
+
import { ui } from '../utils/ui.js';
|
|
19
|
+
const DEFAULT_DOWNLOAD_BASE = 'https://get.devicecloud.dev';
|
|
20
|
+
// Maps `${process.platform}-${process.arch}` to the GitHub Release asset filename
|
|
21
|
+
// produced by scripts/build-binaries.mjs. Keys must stay in sync with that script.
|
|
22
|
+
const ASSET_BY_PLATFORM = {
|
|
23
|
+
'darwin-arm64': 'dcd-darwin-arm64',
|
|
24
|
+
'darwin-x64': 'dcd-darwin-x64',
|
|
25
|
+
'linux-arm64': 'dcd-linux-arm64',
|
|
26
|
+
'linux-x64': 'dcd-linux-x64',
|
|
27
|
+
'win32-x64': 'dcd-windows-x64.exe',
|
|
28
|
+
};
|
|
29
|
+
export const upgradeCommand = defineCommand({
|
|
30
|
+
meta: {
|
|
31
|
+
name: 'upgrade',
|
|
32
|
+
description: 'Upgrade dcd in place to the latest released version',
|
|
33
|
+
},
|
|
34
|
+
async run() {
|
|
35
|
+
if (getInstallMethod() !== 'binary') {
|
|
36
|
+
throw new CliError('`dcd upgrade` only applies to the standalone binary install. ' +
|
|
37
|
+
'Run: npm install -g @devicecloud.dev/dcd@latest');
|
|
38
|
+
}
|
|
39
|
+
const current = getCliVersion();
|
|
40
|
+
const versionService = new VersionService();
|
|
41
|
+
const latest = await versionService.checkLatestCliVersion();
|
|
42
|
+
if (!latest) {
|
|
43
|
+
throw new CliError('Could not reach the update manifest. Check your network connection and try again.');
|
|
44
|
+
}
|
|
45
|
+
if (!versionService.isOutdated(current, latest)) {
|
|
46
|
+
logger.log(ui.success(`Already on the latest version (${colors.highlight(current)})`));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const platformKey = `${process.platform}-${process.arch}`;
|
|
50
|
+
const asset = ASSET_BY_PLATFORM[platformKey];
|
|
51
|
+
if (!asset) {
|
|
52
|
+
throw new CliError(`No binary published for ${platformKey}. Supported platforms: ${Object.keys(ASSET_BY_PLATFORM).join(', ')}`);
|
|
53
|
+
}
|
|
54
|
+
if (process.platform === 'win32') {
|
|
55
|
+
// Windows can't replace a running .exe; defer to a re-run of the installer.
|
|
56
|
+
const base = process.env.DCD_DOWNLOAD_BASE ?? DEFAULT_DOWNLOAD_BASE;
|
|
57
|
+
throw new CliError(`Automatic upgrade on Windows is not yet supported. Re-run the installer:\n irm ${base}/install.ps1 | iex`);
|
|
58
|
+
}
|
|
59
|
+
const base = process.env.DCD_DOWNLOAD_BASE ?? DEFAULT_DOWNLOAD_BASE;
|
|
60
|
+
const binaryUrl = `${base}/download/${latest}/${asset}`;
|
|
61
|
+
const sumsUrl = `${base}/download/${latest}/SHA256SUMS`;
|
|
62
|
+
logger.log(ui.info(`Upgrading ${colors.highlight(current)} → ${colors.highlight(latest)}`));
|
|
63
|
+
logger.log(ui.note(` ${binaryUrl}`));
|
|
64
|
+
const execPath = process.execPath;
|
|
65
|
+
const tmpPath = `${execPath}.new`;
|
|
66
|
+
try {
|
|
67
|
+
await downloadToFile(binaryUrl, tmpPath);
|
|
68
|
+
const expected = await fetchExpectedChecksum(sumsUrl, asset);
|
|
69
|
+
const actual = await sha256File(tmpPath);
|
|
70
|
+
if (expected !== actual) {
|
|
71
|
+
throw new Error(`Checksum mismatch for ${asset}: expected ${expected}, got ${actual}`);
|
|
72
|
+
}
|
|
73
|
+
chmodSync(tmpPath, 0o755);
|
|
74
|
+
// rename is atomic on the same filesystem; on POSIX the running process
|
|
75
|
+
// keeps the old inode open until exit, so this is safe to do mid-run.
|
|
76
|
+
renameSync(tmpPath, execPath);
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
safeUnlink(tmpPath);
|
|
80
|
+
throw new CliError(`Upgrade failed: ${e.message}. The existing binary at ${execPath} was not modified.`);
|
|
81
|
+
}
|
|
82
|
+
logger.log(ui.success(`Upgraded to ${colors.highlight(latest)}`));
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
async function downloadToFile(url, dest) {
|
|
86
|
+
const res = await fetch(url, { redirect: 'follow' });
|
|
87
|
+
if (!res.ok || !res.body) {
|
|
88
|
+
throw new Error(`HTTP ${res.status} fetching ${url}`);
|
|
89
|
+
}
|
|
90
|
+
// Node 22 exposes Readable.fromWeb for piping a WHATWG ReadableStream.
|
|
91
|
+
await pipeline(Readable.fromWeb(res.body), createWriteStream(dest));
|
|
92
|
+
}
|
|
93
|
+
async function fetchExpectedChecksum(sumsUrl, asset) {
|
|
94
|
+
const res = await fetch(sumsUrl, { redirect: 'follow' });
|
|
95
|
+
if (!res.ok)
|
|
96
|
+
throw new Error(`HTTP ${res.status} fetching ${sumsUrl}`);
|
|
97
|
+
const body = await res.text();
|
|
98
|
+
for (const line of body.split('\n')) {
|
|
99
|
+
const match = line.match(/^([a-f0-9]{64})\s+(.+)$/);
|
|
100
|
+
if (match && match[2].trim() === asset)
|
|
101
|
+
return match[1];
|
|
102
|
+
}
|
|
103
|
+
throw new Error(`SHA256SUMS has no entry for ${asset}`);
|
|
104
|
+
}
|
|
105
|
+
async function sha256File(path) {
|
|
106
|
+
const hash = createHash('sha256');
|
|
107
|
+
for await (const chunk of createReadStream(path)) {
|
|
108
|
+
hash.update(chunk);
|
|
109
|
+
}
|
|
110
|
+
return hash.digest('hex');
|
|
111
|
+
}
|
|
112
|
+
function safeUnlink(path) {
|
|
113
|
+
try {
|
|
114
|
+
unlinkSync(path);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Best-effort cleanup; failure here would only leave a .new file.
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export default upgradeCommand;
|
|
@@ -1,20 +1,35 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
export declare const uploadCommand: import("citty").CommandDef<{
|
|
2
|
+
readonly 'app-url': {
|
|
3
|
+
readonly type: "string";
|
|
4
|
+
readonly description: "Signed URL to an Expo iOS build (.tar.gz). The archive is downloaded and extracted automatically. Expo signed URLs expire after ~1 hour.";
|
|
5
5
|
};
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
static flags: {
|
|
10
|
-
apiKey: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
11
|
-
apiUrl: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
12
|
-
'app-url': import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
13
|
-
debug: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
14
|
-
'ignore-sha-check': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
6
|
+
readonly 'ignore-sha-check': {
|
|
7
|
+
readonly type: "boolean";
|
|
8
|
+
readonly description: "Ignore the sha hash check and upload the binary regardless of whether it already exists (not recommended)";
|
|
15
9
|
};
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
10
|
+
readonly debug: {
|
|
11
|
+
readonly type: "boolean";
|
|
12
|
+
readonly default: false;
|
|
13
|
+
readonly description: "Enable detailed debug logging for troubleshooting issues";
|
|
14
|
+
};
|
|
15
|
+
readonly json: {
|
|
16
|
+
readonly type: "boolean";
|
|
17
|
+
readonly description: "Output results in JSON format. Exit codes: 0 on success, 2 if the test run fails, 1 on CLI/infrastructure errors";
|
|
18
|
+
};
|
|
19
|
+
readonly appFile: {
|
|
20
|
+
readonly type: "positional";
|
|
21
|
+
readonly required: false;
|
|
22
|
+
readonly description: "The binary file or Expo signed URL to upload (e.g. test.apk, test.app, test.zip, build.tar.gz, or https://expo.dev/...)";
|
|
23
|
+
};
|
|
24
|
+
readonly 'api-key': {
|
|
25
|
+
readonly type: "string";
|
|
26
|
+
readonly alias: ["apiKey"];
|
|
27
|
+
readonly description: "API key for devicecloud.dev (find this in the console UI). You can also set the DEVICE_CLOUD_API_KEY environment variable.";
|
|
28
|
+
};
|
|
29
|
+
readonly 'api-url': {
|
|
30
|
+
readonly type: "string";
|
|
31
|
+
readonly alias: ["apiURL", "apiUrl"];
|
|
32
|
+
readonly description: "API base URL (defaults to the URL stored by `dcd login`, else prod)";
|
|
33
|
+
};
|
|
34
|
+
}>;
|
|
35
|
+
export default uploadCommand;
|