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