@devicecloud.dev/dcd 4.4.9 → 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 -288
- 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
|
@@ -1,7 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ApiGateway = void 0;
|
|
3
|
+
exports.ApiGateway = exports.ApiError = void 0;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_os_1 = require("node:os");
|
|
4
6
|
const path = require("node:path");
|
|
7
|
+
const node_stream_1 = require("node:stream");
|
|
8
|
+
const promises_1 = require("node:stream/promises");
|
|
9
|
+
/**
|
|
10
|
+
* Error thrown for non-OK API responses, carrying the HTTP status so callers
|
|
11
|
+
* can branch on auth failures etc. without string matching.
|
|
12
|
+
*/
|
|
13
|
+
class ApiError extends Error {
|
|
14
|
+
status;
|
|
15
|
+
constructor(message, status) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'ApiError';
|
|
18
|
+
this.status = status;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.ApiError = ApiError;
|
|
22
|
+
/**
|
|
23
|
+
* Parses a successful response body as JSON, wrapping parse failures in a
|
|
24
|
+
* descriptive error (a 200 with an HTML body otherwise surfaces as a bare
|
|
25
|
+
* SyntaxError with no context about which call failed).
|
|
26
|
+
*/
|
|
27
|
+
async function parseJsonResponse(res, operation) {
|
|
28
|
+
try {
|
|
29
|
+
return (await res.json());
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
throw new Error(`${operation}: API returned an invalid JSON response (${error instanceof Error ? error.message : String(error)})`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
5
35
|
exports.ApiGateway = {
|
|
6
36
|
/**
|
|
7
37
|
* Enhances generic "fetch failed" errors with more specific diagnostic information
|
|
@@ -51,51 +81,82 @@ exports.ApiGateway = {
|
|
|
51
81
|
// Add context and improve readability
|
|
52
82
|
switch (res.status) {
|
|
53
83
|
case 400: {
|
|
54
|
-
throw new
|
|
84
|
+
throw new ApiError(`Invalid request: ${userMessage}`, 400);
|
|
55
85
|
}
|
|
56
86
|
case 401: {
|
|
57
|
-
|
|
87
|
+
// Auth-mode-neutral: the CLI accepts both API keys and Bearer sessions.
|
|
88
|
+
// status.ts matches on the "Authentication failed" / "API key" substrings.
|
|
89
|
+
let message = `Authentication failed — your credentials are invalid or expired. ` +
|
|
90
|
+
`Re-run \`dcd login\` or check your API key. (${operation})`;
|
|
91
|
+
if (userMessage) {
|
|
92
|
+
message += `\nServer response: ${userMessage}`;
|
|
93
|
+
}
|
|
94
|
+
throw new ApiError(message, 401);
|
|
58
95
|
}
|
|
59
96
|
case 403: {
|
|
60
97
|
// For 403, use the server's error message directly as it's now detailed
|
|
61
98
|
// If the message suggests an API key issue, provide additional guidance
|
|
62
99
|
if (userMessage.toLowerCase().includes('api key')) {
|
|
63
|
-
throw new
|
|
100
|
+
throw new ApiError(`${userMessage}\n\nTroubleshooting steps:\n` +
|
|
64
101
|
` 1. Verify DEVICE_CLOUD_API_KEY environment variable is set\n` +
|
|
65
102
|
` 2. Check you're using the correct API key for this environment\n` +
|
|
66
103
|
` 3. Ensure the API key hasn't been deleted or revoked\n` +
|
|
67
|
-
` 4. Confirm you're connecting to the correct API URL
|
|
104
|
+
` 4. Confirm you're connecting to the correct API URL`, 403);
|
|
68
105
|
}
|
|
69
|
-
throw new
|
|
106
|
+
throw new ApiError(`Access denied. ${userMessage}`, 403);
|
|
70
107
|
}
|
|
71
108
|
case 404: {
|
|
72
|
-
throw new
|
|
109
|
+
throw new ApiError(`Resource not found. ${userMessage}`, 404);
|
|
73
110
|
}
|
|
74
111
|
case 429: {
|
|
75
|
-
throw new
|
|
112
|
+
throw new ApiError(`Rate limit exceeded. Please try again later. (${operation})`, 429);
|
|
76
113
|
}
|
|
77
114
|
case 500: {
|
|
78
|
-
throw new
|
|
115
|
+
throw new ApiError(`Server error occurred. Please try again or contact support. (${operation})`, 500);
|
|
79
116
|
}
|
|
80
117
|
default: {
|
|
81
|
-
|
|
118
|
+
// `operation` is already phrased as "Failed to …", so don't append
|
|
119
|
+
// another "failed" here (avoids "Failed to execute test failed: …").
|
|
120
|
+
throw new ApiError(`${operation}: ${userMessage} (HTTP ${res.status})`, res.status);
|
|
82
121
|
}
|
|
83
122
|
}
|
|
84
123
|
},
|
|
85
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Streams a fetch response body to disk, expanding a leading tilde and
|
|
126
|
+
* creating the destination directory if needed. Internal helper shared by
|
|
127
|
+
* the download methods.
|
|
128
|
+
*/
|
|
129
|
+
async streamResponseToFile(res, destinationPath, operation) {
|
|
130
|
+
if (res.body === null) {
|
|
131
|
+
throw new Error(`${operation}: server response contained no body to download`);
|
|
132
|
+
}
|
|
133
|
+
// Handle tilde expansion for home directory
|
|
134
|
+
const expandedPath = destinationPath.replace(/^~(?=$|\/|\\)/, (0, node_os_1.homedir)());
|
|
135
|
+
// Create directory structure if it doesn't exist
|
|
136
|
+
const directory = path.dirname(expandedPath);
|
|
137
|
+
if (directory !== '.') {
|
|
138
|
+
(0, node_fs_1.mkdirSync)(directory, { recursive: true });
|
|
139
|
+
}
|
|
140
|
+
// Use 'w' flag to overwrite existing files instead of failing.
|
|
141
|
+
// pipeline (unlike .pipe) propagates source-stream errors, so a
|
|
142
|
+
// mid-download network failure rejects instead of hanging forever.
|
|
143
|
+
const fileStream = (0, node_fs_1.createWriteStream)(expandedPath, { flags: 'w' });
|
|
144
|
+
await (0, promises_1.pipeline)(node_stream_1.Readable.fromWeb(res.body), fileStream);
|
|
145
|
+
},
|
|
146
|
+
async checkForExistingUpload(baseUrl, auth, sha) {
|
|
86
147
|
try {
|
|
87
148
|
const res = await fetch(`${baseUrl}/uploads/checkForExistingUpload`, {
|
|
88
149
|
body: JSON.stringify({ sha }),
|
|
89
150
|
headers: {
|
|
90
151
|
'content-type': 'application/json',
|
|
91
|
-
|
|
152
|
+
...auth.headers,
|
|
92
153
|
},
|
|
93
154
|
method: 'POST',
|
|
94
155
|
});
|
|
95
156
|
if (!res.ok) {
|
|
96
157
|
await this.handleApiError(res, 'Failed to check for existing upload');
|
|
97
158
|
}
|
|
98
|
-
return res
|
|
159
|
+
return await parseJsonResponse(res, 'Failed to check for existing upload');
|
|
99
160
|
}
|
|
100
161
|
catch (error) {
|
|
101
162
|
// Handle network-level errors (DNS, connection refused, timeout, etc.)
|
|
@@ -105,48 +166,20 @@ exports.ApiGateway = {
|
|
|
105
166
|
throw error;
|
|
106
167
|
}
|
|
107
168
|
},
|
|
108
|
-
async downloadArtifactsZip(baseUrl,
|
|
169
|
+
async downloadArtifactsZip(baseUrl, auth, uploadId, results, artifactsPath = './artifacts.zip') {
|
|
109
170
|
try {
|
|
110
171
|
const res = await fetch(`${baseUrl}/results/${uploadId}/download`, {
|
|
111
172
|
body: JSON.stringify({ results }),
|
|
112
173
|
headers: {
|
|
113
174
|
'content-type': 'application/json',
|
|
114
|
-
|
|
175
|
+
...auth.headers,
|
|
115
176
|
},
|
|
116
177
|
method: 'POST',
|
|
117
178
|
});
|
|
118
179
|
if (!res.ok) {
|
|
119
180
|
await this.handleApiError(res, 'Failed to download artifacts');
|
|
120
181
|
}
|
|
121
|
-
|
|
122
|
-
if (artifactsPath.startsWith('~/') || artifactsPath === '~') {
|
|
123
|
-
artifactsPath = artifactsPath.replace(/^~(?=$|\/|\\)/,
|
|
124
|
-
// eslint-disable-next-line unicorn/prefer-module
|
|
125
|
-
require('node:os').homedir());
|
|
126
|
-
}
|
|
127
|
-
// Create directory structure if it doesn't exist
|
|
128
|
-
// eslint-disable-next-line unicorn/prefer-module
|
|
129
|
-
const { dirname } = require('node:path');
|
|
130
|
-
// eslint-disable-next-line unicorn/prefer-module
|
|
131
|
-
const { createWriteStream, mkdirSync } = require('node:fs');
|
|
132
|
-
// eslint-disable-next-line unicorn/prefer-module
|
|
133
|
-
const { finished } = require('node:stream/promises');
|
|
134
|
-
// eslint-disable-next-line unicorn/prefer-module
|
|
135
|
-
const { Readable } = require('node:stream');
|
|
136
|
-
const directory = dirname(artifactsPath);
|
|
137
|
-
if (directory !== '.') {
|
|
138
|
-
try {
|
|
139
|
-
mkdirSync(directory, { recursive: true });
|
|
140
|
-
}
|
|
141
|
-
catch (error) {
|
|
142
|
-
// Ignore if directory already exists
|
|
143
|
-
if (error.code !== 'EEXIST') {
|
|
144
|
-
throw error;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
const fileStream = createWriteStream(artifactsPath, { flags: 'w' });
|
|
149
|
-
await finished(Readable.fromWeb(res.body).pipe(fileStream));
|
|
182
|
+
await this.streamResponseToFile(res, artifactsPath, 'Failed to download artifacts');
|
|
150
183
|
}
|
|
151
184
|
catch (error) {
|
|
152
185
|
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
@@ -156,7 +189,7 @@ exports.ApiGateway = {
|
|
|
156
189
|
}
|
|
157
190
|
},
|
|
158
191
|
async finaliseUpload(config) {
|
|
159
|
-
const { baseUrl,
|
|
192
|
+
const { baseUrl, auth, id, metadata, path, sha, supabaseSuccess, backblazeSuccess, bytes } = config;
|
|
160
193
|
try {
|
|
161
194
|
const res = await fetch(`${baseUrl}/uploads/finaliseUpload`, {
|
|
162
195
|
body: JSON.stringify({
|
|
@@ -165,19 +198,19 @@ exports.ApiGateway = {
|
|
|
165
198
|
id,
|
|
166
199
|
metadata,
|
|
167
200
|
path, // This is tempPath for TUS uploads
|
|
168
|
-
sha,
|
|
201
|
+
...(sha ? { sha } : {}),
|
|
169
202
|
supabaseSuccess,
|
|
170
203
|
}),
|
|
171
204
|
headers: {
|
|
172
205
|
'content-type': 'application/json',
|
|
173
|
-
|
|
206
|
+
...auth.headers,
|
|
174
207
|
},
|
|
175
208
|
method: 'POST',
|
|
176
209
|
});
|
|
177
210
|
if (!res.ok) {
|
|
178
211
|
await this.handleApiError(res, 'Failed to finalize upload');
|
|
179
212
|
}
|
|
180
|
-
return res
|
|
213
|
+
return await parseJsonResponse(res, 'Failed to finalize upload');
|
|
181
214
|
}
|
|
182
215
|
catch (error) {
|
|
183
216
|
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
@@ -186,20 +219,20 @@ exports.ApiGateway = {
|
|
|
186
219
|
throw error;
|
|
187
220
|
}
|
|
188
221
|
},
|
|
189
|
-
async getBinaryUploadUrl(baseUrl,
|
|
222
|
+
async getBinaryUploadUrl(baseUrl, auth, platform, fileSize) {
|
|
190
223
|
try {
|
|
191
224
|
const res = await fetch(`${baseUrl}/uploads/getBinaryUploadUrl`, {
|
|
192
225
|
body: JSON.stringify({ platform, fileSize }),
|
|
193
226
|
headers: {
|
|
194
227
|
'content-type': 'application/json',
|
|
195
|
-
|
|
228
|
+
...auth.headers,
|
|
196
229
|
},
|
|
197
230
|
method: 'POST',
|
|
198
231
|
});
|
|
199
232
|
if (!res.ok) {
|
|
200
233
|
await this.handleApiError(res, 'Failed to get upload URL');
|
|
201
234
|
}
|
|
202
|
-
return res
|
|
235
|
+
return await parseJsonResponse(res, 'Failed to get upload URL');
|
|
203
236
|
}
|
|
204
237
|
catch (error) {
|
|
205
238
|
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
@@ -208,20 +241,20 @@ exports.ApiGateway = {
|
|
|
208
241
|
throw error;
|
|
209
242
|
}
|
|
210
243
|
},
|
|
211
|
-
async finishLargeFile(baseUrl,
|
|
244
|
+
async finishLargeFile(baseUrl, auth, fileId, partSha1Array) {
|
|
212
245
|
try {
|
|
213
246
|
const res = await fetch(`${baseUrl}/uploads/finishLargeFile`, {
|
|
214
247
|
body: JSON.stringify({ fileId, partSha1Array }),
|
|
215
248
|
headers: {
|
|
216
249
|
'content-type': 'application/json',
|
|
217
|
-
|
|
250
|
+
...auth.headers,
|
|
218
251
|
},
|
|
219
252
|
method: 'POST',
|
|
220
253
|
});
|
|
221
254
|
if (!res.ok) {
|
|
222
255
|
await this.handleApiError(res, 'Failed to finish large file');
|
|
223
256
|
}
|
|
224
|
-
return res
|
|
257
|
+
return await parseJsonResponse(res, 'Failed to finish large file');
|
|
225
258
|
}
|
|
226
259
|
catch (error) {
|
|
227
260
|
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
@@ -230,16 +263,16 @@ exports.ApiGateway = {
|
|
|
230
263
|
throw error;
|
|
231
264
|
}
|
|
232
265
|
},
|
|
233
|
-
async getResultsForUpload(baseUrl,
|
|
266
|
+
async getResultsForUpload(baseUrl, auth, uploadId) {
|
|
234
267
|
// TODO: merge with getUploadStatus
|
|
235
268
|
try {
|
|
236
269
|
const res = await fetch(`${baseUrl}/results/${uploadId}`, {
|
|
237
|
-
headers: {
|
|
270
|
+
headers: { ...auth.headers },
|
|
238
271
|
});
|
|
239
272
|
if (!res.ok) {
|
|
240
273
|
await this.handleApiError(res, 'Failed to get results');
|
|
241
274
|
}
|
|
242
|
-
return res
|
|
275
|
+
return await parseJsonResponse(res, 'Failed to get results');
|
|
243
276
|
}
|
|
244
277
|
catch (error) {
|
|
245
278
|
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
@@ -248,7 +281,7 @@ exports.ApiGateway = {
|
|
|
248
281
|
throw error;
|
|
249
282
|
}
|
|
250
283
|
},
|
|
251
|
-
async getUploadStatus(baseUrl,
|
|
284
|
+
async getUploadStatus(baseUrl, auth, options) {
|
|
252
285
|
const queryParams = new URLSearchParams();
|
|
253
286
|
if (options.uploadId) {
|
|
254
287
|
queryParams.append('uploadId', options.uploadId);
|
|
@@ -259,13 +292,13 @@ exports.ApiGateway = {
|
|
|
259
292
|
try {
|
|
260
293
|
const response = await fetch(`${baseUrl}/uploads/status?${queryParams}`, {
|
|
261
294
|
headers: {
|
|
262
|
-
|
|
295
|
+
...auth.headers,
|
|
263
296
|
},
|
|
264
297
|
});
|
|
265
298
|
if (!response.ok) {
|
|
266
299
|
await this.handleApiError(response, 'Failed to get upload status');
|
|
267
300
|
}
|
|
268
|
-
return response
|
|
301
|
+
return await parseJsonResponse(response, 'Failed to get upload status');
|
|
269
302
|
}
|
|
270
303
|
catch (error) {
|
|
271
304
|
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
@@ -274,7 +307,7 @@ exports.ApiGateway = {
|
|
|
274
307
|
throw error;
|
|
275
308
|
}
|
|
276
309
|
},
|
|
277
|
-
async listUploads(baseUrl,
|
|
310
|
+
async listUploads(baseUrl, auth, options = {}) {
|
|
278
311
|
const queryParams = new URLSearchParams();
|
|
279
312
|
if (options.name) {
|
|
280
313
|
queryParams.append('name', options.name);
|
|
@@ -295,13 +328,13 @@ exports.ApiGateway = {
|
|
|
295
328
|
try {
|
|
296
329
|
const response = await fetch(url, {
|
|
297
330
|
headers: {
|
|
298
|
-
|
|
331
|
+
...auth.headers,
|
|
299
332
|
},
|
|
300
333
|
});
|
|
301
334
|
if (!response.ok) {
|
|
302
335
|
await this.handleApiError(response, 'Failed to list uploads');
|
|
303
336
|
}
|
|
304
|
-
return response
|
|
337
|
+
return await parseJsonResponse(response, 'Failed to list uploads');
|
|
305
338
|
}
|
|
306
339
|
catch (error) {
|
|
307
340
|
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
@@ -310,19 +343,19 @@ exports.ApiGateway = {
|
|
|
310
343
|
throw error;
|
|
311
344
|
}
|
|
312
345
|
},
|
|
313
|
-
async uploadFlow(baseUrl,
|
|
346
|
+
async uploadFlow(baseUrl, auth, testFormData) {
|
|
314
347
|
try {
|
|
315
348
|
const res = await fetch(`${baseUrl}/uploads/flow`, {
|
|
316
349
|
body: testFormData,
|
|
317
350
|
headers: {
|
|
318
|
-
|
|
351
|
+
...auth.headers,
|
|
319
352
|
},
|
|
320
353
|
method: 'POST',
|
|
321
354
|
});
|
|
322
355
|
if (!res.ok) {
|
|
323
356
|
await this.handleApiError(res, 'Failed to upload test flows');
|
|
324
357
|
}
|
|
325
|
-
return res
|
|
358
|
+
return await parseJsonResponse(res, 'Failed to upload test flows');
|
|
326
359
|
}
|
|
327
360
|
catch (error) {
|
|
328
361
|
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
@@ -334,13 +367,13 @@ exports.ApiGateway = {
|
|
|
334
367
|
/**
|
|
335
368
|
* Generic report download method that handles both junit and allure reports
|
|
336
369
|
* @param baseUrl - API base URL
|
|
337
|
-
* @param
|
|
370
|
+
* @param auth - AuthContext (API key or Bearer session) for authentication
|
|
338
371
|
* @param uploadId - Upload ID to download report for
|
|
339
372
|
* @param reportType - Type of report to download ('junit' or 'allure')
|
|
340
373
|
* @param reportPath - Optional custom path for the downloaded report
|
|
341
374
|
* @returns Promise that resolves when download is complete
|
|
342
375
|
*/
|
|
343
|
-
async downloadReportGeneric(baseUrl,
|
|
376
|
+
async downloadReportGeneric(baseUrl, auth, uploadId, reportType, reportPath) {
|
|
344
377
|
// Define endpoint and default filename mappings
|
|
345
378
|
const config = {
|
|
346
379
|
junit: {
|
|
@@ -369,7 +402,7 @@ exports.ApiGateway = {
|
|
|
369
402
|
// Make the download request
|
|
370
403
|
const res = await fetch(url, {
|
|
371
404
|
headers: {
|
|
372
|
-
|
|
405
|
+
...auth.headers,
|
|
373
406
|
},
|
|
374
407
|
method: 'GET',
|
|
375
408
|
});
|
|
@@ -380,38 +413,145 @@ exports.ApiGateway = {
|
|
|
380
413
|
}
|
|
381
414
|
throw new Error(`${errorPrefix}: ${res.status} ${errorText}`);
|
|
382
415
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
416
|
+
await this.streamResponseToFile(res, finalReportPath, errorPrefix);
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
420
|
+
throw this.enhanceFetchError(error, url);
|
|
421
|
+
}
|
|
422
|
+
throw error;
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
async startLiveSession(baseUrl, auth, params) {
|
|
426
|
+
try {
|
|
427
|
+
const res = await fetch(`${baseUrl}/live`, {
|
|
428
|
+
body: JSON.stringify({
|
|
429
|
+
binaryUploadId: params.binaryUploadId,
|
|
430
|
+
deviceLocale: params.deviceLocale,
|
|
431
|
+
platform: params.platform,
|
|
432
|
+
androidDevice: params.androidDevice,
|
|
433
|
+
androidApiLevel: params.androidApiLevel,
|
|
434
|
+
}),
|
|
435
|
+
headers: {
|
|
436
|
+
'content-type': 'application/json',
|
|
437
|
+
...auth.headers,
|
|
438
|
+
},
|
|
439
|
+
method: 'POST',
|
|
440
|
+
});
|
|
441
|
+
if (!res.ok) {
|
|
442
|
+
await this.handleApiError(res, 'Failed to start live session');
|
|
443
|
+
}
|
|
444
|
+
return await parseJsonResponse(res, 'Failed to start live session');
|
|
445
|
+
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
448
|
+
throw this.enhanceFetchError(error, `${baseUrl}/live`);
|
|
449
|
+
}
|
|
450
|
+
throw error;
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
async installLiveBinary(baseUrl, auth, sessionName, binaryUploadId) {
|
|
454
|
+
const url = `${baseUrl}/live/${sessionName}/install`;
|
|
455
|
+
try {
|
|
456
|
+
const res = await fetch(url, {
|
|
457
|
+
body: JSON.stringify({ binaryUploadId }),
|
|
458
|
+
headers: {
|
|
459
|
+
'content-type': 'application/json',
|
|
460
|
+
...auth.headers,
|
|
461
|
+
},
|
|
462
|
+
method: 'POST',
|
|
463
|
+
});
|
|
464
|
+
if (!res.ok) {
|
|
465
|
+
await this.handleApiError(res, 'Failed to install binary');
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
470
|
+
throw this.enhanceFetchError(error, url);
|
|
471
|
+
}
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
async execLiveYaml(baseUrl, auth, sessionName, yaml, opts = {}) {
|
|
476
|
+
const url = `${baseUrl}/live/${sessionName}/exec`;
|
|
477
|
+
try {
|
|
478
|
+
const res = await fetch(url, {
|
|
479
|
+
body: JSON.stringify({ yaml, async: opts.async }),
|
|
480
|
+
headers: {
|
|
481
|
+
'content-type': 'application/json',
|
|
482
|
+
...auth.headers,
|
|
483
|
+
},
|
|
484
|
+
method: 'POST',
|
|
485
|
+
});
|
|
486
|
+
if (!res.ok) {
|
|
487
|
+
await this.handleApiError(res, 'Failed to execute test');
|
|
488
|
+
}
|
|
489
|
+
return await parseJsonResponse(res, 'Failed to execute test');
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
493
|
+
throw this.enhanceFetchError(error, url);
|
|
494
|
+
}
|
|
495
|
+
throw error;
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
async getLiveCommand(baseUrl, auth, sessionName, commandId) {
|
|
499
|
+
const url = `${baseUrl}/live/${sessionName}/commands/${commandId}`;
|
|
500
|
+
try {
|
|
501
|
+
const res = await fetch(url, { headers: { ...auth.headers }, method: 'GET' });
|
|
502
|
+
if (!res.ok) {
|
|
503
|
+
await this.handleApiError(res, 'Failed to get command status');
|
|
504
|
+
}
|
|
505
|
+
return await parseJsonResponse(res, 'Failed to get command status');
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
509
|
+
throw this.enhanceFetchError(error, url);
|
|
510
|
+
}
|
|
511
|
+
throw error;
|
|
512
|
+
}
|
|
513
|
+
},
|
|
514
|
+
async keepaliveLiveSession(baseUrl, auth, sessionName) {
|
|
515
|
+
const url = `${baseUrl}/live/${sessionName}/keepalive`;
|
|
516
|
+
try {
|
|
517
|
+
const res = await fetch(url, { headers: { ...auth.headers }, method: 'POST' });
|
|
518
|
+
// A keepalive failure shouldn't kill a long-running poll; swallow non-OK.
|
|
519
|
+
if (!res.ok)
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
// best effort
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
async stopLiveSession(baseUrl, auth, sessionName) {
|
|
527
|
+
const url = `${baseUrl}/live/${sessionName}`;
|
|
528
|
+
try {
|
|
529
|
+
const res = await fetch(url, {
|
|
530
|
+
headers: { ...auth.headers },
|
|
531
|
+
method: 'DELETE',
|
|
532
|
+
});
|
|
533
|
+
if (!res.ok) {
|
|
534
|
+
await this.handleApiError(res, 'Failed to stop session');
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
539
|
+
throw this.enhanceFetchError(error, url);
|
|
540
|
+
}
|
|
541
|
+
throw error;
|
|
542
|
+
}
|
|
543
|
+
},
|
|
544
|
+
async getLiveSession(baseUrl, auth, sessionName) {
|
|
545
|
+
const url = `${baseUrl}/live/${sessionName}`;
|
|
546
|
+
try {
|
|
547
|
+
const res = await fetch(url, {
|
|
548
|
+
headers: { ...auth.headers },
|
|
549
|
+
method: 'GET',
|
|
550
|
+
});
|
|
551
|
+
if (!res.ok) {
|
|
552
|
+
await this.handleApiError(res, 'Failed to get session status');
|
|
410
553
|
}
|
|
411
|
-
|
|
412
|
-
// Use 'w' flag to overwrite existing files instead of failing
|
|
413
|
-
const fileStream = createWriteStream(expandedPath, { flags: 'w' });
|
|
414
|
-
await finished(Readable.fromWeb(res.body).pipe(fileStream));
|
|
554
|
+
return await parseJsonResponse(res, 'Failed to get session status');
|
|
415
555
|
}
|
|
416
556
|
catch (error) {
|
|
417
557
|
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { StoredSession } from '../utils/config-store';
|
|
2
|
+
export interface RefreshedSession {
|
|
3
|
+
access_token: string;
|
|
4
|
+
refresh_token: string;
|
|
5
|
+
/** Unix epoch seconds. */
|
|
6
|
+
expires_at: number;
|
|
7
|
+
user_email: string;
|
|
8
|
+
user_id: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const CliAuthGateway: {
|
|
11
|
+
refresh(supabaseUrl: string, supabaseAnonKey: string, session: StoredSession): Promise<RefreshedSession>;
|
|
12
|
+
signOut(supabaseUrl: string, supabaseAnonKey: string, session: StoredSession): Promise<void>;
|
|
13
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CliAuthGateway = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Supabase session operations for the CLI: refresh an expired access token
|
|
6
|
+
* and sign-out (best-effort revocation on the Supabase side).
|
|
7
|
+
*
|
|
8
|
+
* Does not talk to the dcd API — the dcd-side session exchange lives in the
|
|
9
|
+
* login command's loopback flow, where the frontend POSTs ciphertext back.
|
|
10
|
+
*/
|
|
11
|
+
const supabase_js_1 = require("@supabase/supabase-js");
|
|
12
|
+
function client(supabaseUrl, supabaseAnonKey) {
|
|
13
|
+
return (0, supabase_js_1.createClient)(supabaseUrl, supabaseAnonKey, {
|
|
14
|
+
auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false },
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
exports.CliAuthGateway = {
|
|
18
|
+
async refresh(supabaseUrl, supabaseAnonKey, session) {
|
|
19
|
+
const sb = client(supabaseUrl, supabaseAnonKey);
|
|
20
|
+
const { data, error } = await sb.auth.refreshSession({
|
|
21
|
+
refresh_token: session.refresh_token,
|
|
22
|
+
});
|
|
23
|
+
if (error || !data.session || !data.user) {
|
|
24
|
+
throw new Error(`Failed to refresh session: ${error?.message ?? 'no session returned'}. ` +
|
|
25
|
+
`Run \`dcd login\` again.`);
|
|
26
|
+
}
|
|
27
|
+
const s = data.session;
|
|
28
|
+
const u = data.user;
|
|
29
|
+
if (!s.expires_at)
|
|
30
|
+
throw new Error('Refreshed session is missing expires_at');
|
|
31
|
+
return {
|
|
32
|
+
access_token: s.access_token,
|
|
33
|
+
refresh_token: s.refresh_token,
|
|
34
|
+
expires_at: s.expires_at,
|
|
35
|
+
user_email: u.email ?? session.user_email,
|
|
36
|
+
user_id: u.id,
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
async signOut(supabaseUrl, supabaseAnonKey, session) {
|
|
40
|
+
// `scope: 'local'` clears this client's SDK state only — no server call,
|
|
41
|
+
// no revocation. supabase-js defaults to `scope: 'global'`, which revokes
|
|
42
|
+
// EVERY session for the user (browser, mobile, other CLIs), so
|
|
43
|
+
// `dcd logout` would kick the user out of the web app. We don't want
|
|
44
|
+
// that — logging out locally should be local.
|
|
45
|
+
const sb = client(supabaseUrl, supabaseAnonKey);
|
|
46
|
+
try {
|
|
47
|
+
await sb.auth.setSession({
|
|
48
|
+
access_token: session.access_token,
|
|
49
|
+
refresh_token: session.refresh_token,
|
|
50
|
+
});
|
|
51
|
+
await sb.auth.signOut({ scope: 'local' });
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Best effort — the local config will be wiped regardless.
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
};
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
+
import { type DcdEnvName } from '../config/environments';
|
|
2
|
+
/** Disk-backed upload descriptor — see UploadSource in src/methods.ts. */
|
|
3
|
+
export interface ResumableUploadSource {
|
|
4
|
+
contentType: string;
|
|
5
|
+
diskPath: string;
|
|
6
|
+
name: string;
|
|
7
|
+
size: number;
|
|
8
|
+
}
|
|
1
9
|
export declare class SupabaseGateway {
|
|
2
|
-
private static SB;
|
|
3
|
-
static getSupabaseKeys(env: 'dev' | 'prod'): {
|
|
4
|
-
SUPABASE_PUBLIC_KEY: string;
|
|
5
|
-
SUPABASE_URL: string;
|
|
6
|
-
} | {
|
|
7
|
-
SUPABASE_PUBLIC_KEY: string;
|
|
8
|
-
SUPABASE_URL: string;
|
|
9
|
-
};
|
|
10
10
|
/**
|
|
11
11
|
* Upload to Supabase using resumable uploads (TUS protocol)
|
|
12
12
|
* Uploads to staging location (uploads/{id}/) using anon key
|
|
13
13
|
* File is later moved to final location by API after finalization
|
|
14
14
|
* @param env - Environment (dev or prod)
|
|
15
15
|
* @param path - Staging storage path (uploads/{id}/file.ext)
|
|
16
|
-
* @param
|
|
16
|
+
* @param source - Upload source descriptor; the file is streamed from disk
|
|
17
17
|
* @param debug - Enable debug logging
|
|
18
18
|
* @param onProgress - Optional callback for upload progress (bytesUploaded, bytesTotal)
|
|
19
19
|
* @returns Promise that resolves when upload completes
|
|
20
20
|
*/
|
|
21
|
-
static uploadResumable(env:
|
|
22
|
-
static uploadToSignedUrl(env:
|
|
21
|
+
static uploadResumable(env: DcdEnvName, path: string, source: ResumableUploadSource, debug?: boolean, onProgress?: (bytesUploaded: number, bytesTotal: number) => void): Promise<void>;
|
|
22
|
+
static uploadToSignedUrl(env: DcdEnvName, path: string, token: string, file: File, debug?: boolean): Promise<void>;
|
|
23
23
|
/**
|
|
24
24
|
* Logs network error details for debugging
|
|
25
25
|
* @param error - Error object to analyze for network-related issues
|