@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
|
@@ -1,8 +1,34 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { createWriteStream, mkdirSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { Readable } from 'node:stream';
|
|
5
|
+
import { pipeline } from 'node:stream/promises';
|
|
6
|
+
/**
|
|
7
|
+
* Error thrown for non-OK API responses, carrying the HTTP status so callers
|
|
8
|
+
* can branch on auth failures etc. without string matching.
|
|
9
|
+
*/
|
|
10
|
+
export class ApiError extends Error {
|
|
11
|
+
status;
|
|
12
|
+
constructor(message, status) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'ApiError';
|
|
15
|
+
this.status = status;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Parses a successful response body as JSON, wrapping parse failures in a
|
|
20
|
+
* descriptive error (a 200 with an HTML body otherwise surfaces as a bare
|
|
21
|
+
* SyntaxError with no context about which call failed).
|
|
22
|
+
*/
|
|
23
|
+
async function parseJsonResponse(res, operation) {
|
|
24
|
+
try {
|
|
25
|
+
return (await res.json());
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
throw new Error(`${operation}: API returned an invalid JSON response (${error instanceof Error ? error.message : String(error)})`, { cause: error });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export const ApiGateway = {
|
|
6
32
|
/**
|
|
7
33
|
* Enhances generic "fetch failed" errors with more specific diagnostic information
|
|
8
34
|
* @param error - The original TypeError from fetch
|
|
@@ -51,51 +77,82 @@ exports.ApiGateway = {
|
|
|
51
77
|
// Add context and improve readability
|
|
52
78
|
switch (res.status) {
|
|
53
79
|
case 400: {
|
|
54
|
-
throw new
|
|
80
|
+
throw new ApiError(`Invalid request: ${userMessage}`, 400);
|
|
55
81
|
}
|
|
56
82
|
case 401: {
|
|
57
|
-
|
|
83
|
+
// Auth-mode-neutral: the CLI accepts both API keys and Bearer sessions.
|
|
84
|
+
// status.ts matches on the "Authentication failed" / "API key" substrings.
|
|
85
|
+
let message = `Authentication failed — your credentials are invalid or expired. ` +
|
|
86
|
+
`Re-run \`dcd login\` or check your API key. (${operation})`;
|
|
87
|
+
if (userMessage) {
|
|
88
|
+
message += `\nServer response: ${userMessage}`;
|
|
89
|
+
}
|
|
90
|
+
throw new ApiError(message, 401);
|
|
58
91
|
}
|
|
59
92
|
case 403: {
|
|
60
93
|
// For 403, use the server's error message directly as it's now detailed
|
|
61
94
|
// If the message suggests an API key issue, provide additional guidance
|
|
62
95
|
if (userMessage.toLowerCase().includes('api key')) {
|
|
63
|
-
throw new
|
|
96
|
+
throw new ApiError(`${userMessage}\n\nTroubleshooting steps:\n` +
|
|
64
97
|
` 1. Verify DEVICE_CLOUD_API_KEY environment variable is set\n` +
|
|
65
98
|
` 2. Check you're using the correct API key for this environment\n` +
|
|
66
99
|
` 3. Ensure the API key hasn't been deleted or revoked\n` +
|
|
67
|
-
` 4. Confirm you're connecting to the correct API URL
|
|
100
|
+
` 4. Confirm you're connecting to the correct API URL`, 403);
|
|
68
101
|
}
|
|
69
|
-
throw new
|
|
102
|
+
throw new ApiError(`Access denied. ${userMessage}`, 403);
|
|
70
103
|
}
|
|
71
104
|
case 404: {
|
|
72
|
-
throw new
|
|
105
|
+
throw new ApiError(`Resource not found. ${userMessage}`, 404);
|
|
73
106
|
}
|
|
74
107
|
case 429: {
|
|
75
|
-
throw new
|
|
108
|
+
throw new ApiError(`Rate limit exceeded. Please try again later. (${operation})`, 429);
|
|
76
109
|
}
|
|
77
110
|
case 500: {
|
|
78
|
-
throw new
|
|
111
|
+
throw new ApiError(`Server error occurred. Please try again or contact support. (${operation})`, 500);
|
|
79
112
|
}
|
|
80
113
|
default: {
|
|
81
|
-
|
|
114
|
+
// `operation` is already phrased as "Failed to …", so don't append
|
|
115
|
+
// another "failed" here (avoids "Failed to execute test failed: …").
|
|
116
|
+
throw new ApiError(`${operation}: ${userMessage} (HTTP ${res.status})`, res.status);
|
|
82
117
|
}
|
|
83
118
|
}
|
|
84
119
|
},
|
|
85
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Streams a fetch response body to disk, expanding a leading tilde and
|
|
122
|
+
* creating the destination directory if needed. Internal helper shared by
|
|
123
|
+
* the download methods.
|
|
124
|
+
*/
|
|
125
|
+
async streamResponseToFile(res, destinationPath, operation) {
|
|
126
|
+
if (res.body === null) {
|
|
127
|
+
throw new Error(`${operation}: server response contained no body to download`);
|
|
128
|
+
}
|
|
129
|
+
// Handle tilde expansion for home directory
|
|
130
|
+
const expandedPath = destinationPath.replace(/^~(?=$|\/|\\)/, homedir());
|
|
131
|
+
// Create directory structure if it doesn't exist
|
|
132
|
+
const directory = path.dirname(expandedPath);
|
|
133
|
+
if (directory !== '.') {
|
|
134
|
+
mkdirSync(directory, { recursive: true });
|
|
135
|
+
}
|
|
136
|
+
// Use 'w' flag to overwrite existing files instead of failing.
|
|
137
|
+
// pipeline (unlike .pipe) propagates source-stream errors, so a
|
|
138
|
+
// mid-download network failure rejects instead of hanging forever.
|
|
139
|
+
const fileStream = createWriteStream(expandedPath, { flags: 'w' });
|
|
140
|
+
await pipeline(Readable.fromWeb(res.body), fileStream);
|
|
141
|
+
},
|
|
142
|
+
async checkForExistingUpload(baseUrl, auth, sha) {
|
|
86
143
|
try {
|
|
87
144
|
const res = await fetch(`${baseUrl}/uploads/checkForExistingUpload`, {
|
|
88
145
|
body: JSON.stringify({ sha }),
|
|
89
146
|
headers: {
|
|
90
147
|
'content-type': 'application/json',
|
|
91
|
-
|
|
148
|
+
...auth.headers,
|
|
92
149
|
},
|
|
93
150
|
method: 'POST',
|
|
94
151
|
});
|
|
95
152
|
if (!res.ok) {
|
|
96
153
|
await this.handleApiError(res, 'Failed to check for existing upload');
|
|
97
154
|
}
|
|
98
|
-
return res
|
|
155
|
+
return await parseJsonResponse(res, 'Failed to check for existing upload');
|
|
99
156
|
}
|
|
100
157
|
catch (error) {
|
|
101
158
|
// Handle network-level errors (DNS, connection refused, timeout, etc.)
|
|
@@ -105,48 +162,20 @@ exports.ApiGateway = {
|
|
|
105
162
|
throw error;
|
|
106
163
|
}
|
|
107
164
|
},
|
|
108
|
-
async downloadArtifactsZip(baseUrl,
|
|
165
|
+
async downloadArtifactsZip(baseUrl, auth, uploadId, results, artifactsPath = './artifacts.zip') {
|
|
109
166
|
try {
|
|
110
167
|
const res = await fetch(`${baseUrl}/results/${uploadId}/download`, {
|
|
111
168
|
body: JSON.stringify({ results }),
|
|
112
169
|
headers: {
|
|
113
170
|
'content-type': 'application/json',
|
|
114
|
-
|
|
171
|
+
...auth.headers,
|
|
115
172
|
},
|
|
116
173
|
method: 'POST',
|
|
117
174
|
});
|
|
118
175
|
if (!res.ok) {
|
|
119
176
|
await this.handleApiError(res, 'Failed to download artifacts');
|
|
120
177
|
}
|
|
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));
|
|
178
|
+
await this.streamResponseToFile(res, artifactsPath, 'Failed to download artifacts');
|
|
150
179
|
}
|
|
151
180
|
catch (error) {
|
|
152
181
|
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
@@ -156,7 +185,7 @@ exports.ApiGateway = {
|
|
|
156
185
|
}
|
|
157
186
|
},
|
|
158
187
|
async finaliseUpload(config) {
|
|
159
|
-
const { baseUrl,
|
|
188
|
+
const { baseUrl, auth, id, metadata, path, sha, supabaseSuccess, backblazeSuccess, bytes } = config;
|
|
160
189
|
try {
|
|
161
190
|
const res = await fetch(`${baseUrl}/uploads/finaliseUpload`, {
|
|
162
191
|
body: JSON.stringify({
|
|
@@ -165,19 +194,19 @@ exports.ApiGateway = {
|
|
|
165
194
|
id,
|
|
166
195
|
metadata,
|
|
167
196
|
path, // This is tempPath for TUS uploads
|
|
168
|
-
sha,
|
|
197
|
+
...(sha ? { sha } : {}),
|
|
169
198
|
supabaseSuccess,
|
|
170
199
|
}),
|
|
171
200
|
headers: {
|
|
172
201
|
'content-type': 'application/json',
|
|
173
|
-
|
|
202
|
+
...auth.headers,
|
|
174
203
|
},
|
|
175
204
|
method: 'POST',
|
|
176
205
|
});
|
|
177
206
|
if (!res.ok) {
|
|
178
207
|
await this.handleApiError(res, 'Failed to finalize upload');
|
|
179
208
|
}
|
|
180
|
-
return res
|
|
209
|
+
return await parseJsonResponse(res, 'Failed to finalize upload');
|
|
181
210
|
}
|
|
182
211
|
catch (error) {
|
|
183
212
|
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
@@ -186,20 +215,20 @@ exports.ApiGateway = {
|
|
|
186
215
|
throw error;
|
|
187
216
|
}
|
|
188
217
|
},
|
|
189
|
-
async getBinaryUploadUrl(baseUrl,
|
|
218
|
+
async getBinaryUploadUrl(baseUrl, auth, platform, fileSize) {
|
|
190
219
|
try {
|
|
191
220
|
const res = await fetch(`${baseUrl}/uploads/getBinaryUploadUrl`, {
|
|
192
221
|
body: JSON.stringify({ platform, fileSize }),
|
|
193
222
|
headers: {
|
|
194
223
|
'content-type': 'application/json',
|
|
195
|
-
|
|
224
|
+
...auth.headers,
|
|
196
225
|
},
|
|
197
226
|
method: 'POST',
|
|
198
227
|
});
|
|
199
228
|
if (!res.ok) {
|
|
200
229
|
await this.handleApiError(res, 'Failed to get upload URL');
|
|
201
230
|
}
|
|
202
|
-
return res
|
|
231
|
+
return await parseJsonResponse(res, 'Failed to get upload URL');
|
|
203
232
|
}
|
|
204
233
|
catch (error) {
|
|
205
234
|
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
@@ -208,20 +237,20 @@ exports.ApiGateway = {
|
|
|
208
237
|
throw error;
|
|
209
238
|
}
|
|
210
239
|
},
|
|
211
|
-
async finishLargeFile(baseUrl,
|
|
240
|
+
async finishLargeFile(baseUrl, auth, fileId, partSha1Array) {
|
|
212
241
|
try {
|
|
213
242
|
const res = await fetch(`${baseUrl}/uploads/finishLargeFile`, {
|
|
214
243
|
body: JSON.stringify({ fileId, partSha1Array }),
|
|
215
244
|
headers: {
|
|
216
245
|
'content-type': 'application/json',
|
|
217
|
-
|
|
246
|
+
...auth.headers,
|
|
218
247
|
},
|
|
219
248
|
method: 'POST',
|
|
220
249
|
});
|
|
221
250
|
if (!res.ok) {
|
|
222
251
|
await this.handleApiError(res, 'Failed to finish large file');
|
|
223
252
|
}
|
|
224
|
-
return res
|
|
253
|
+
return await parseJsonResponse(res, 'Failed to finish large file');
|
|
225
254
|
}
|
|
226
255
|
catch (error) {
|
|
227
256
|
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
@@ -230,16 +259,16 @@ exports.ApiGateway = {
|
|
|
230
259
|
throw error;
|
|
231
260
|
}
|
|
232
261
|
},
|
|
233
|
-
async getResultsForUpload(baseUrl,
|
|
262
|
+
async getResultsForUpload(baseUrl, auth, uploadId) {
|
|
234
263
|
// TODO: merge with getUploadStatus
|
|
235
264
|
try {
|
|
236
265
|
const res = await fetch(`${baseUrl}/results/${uploadId}`, {
|
|
237
|
-
headers: {
|
|
266
|
+
headers: { ...auth.headers },
|
|
238
267
|
});
|
|
239
268
|
if (!res.ok) {
|
|
240
269
|
await this.handleApiError(res, 'Failed to get results');
|
|
241
270
|
}
|
|
242
|
-
return res
|
|
271
|
+
return await parseJsonResponse(res, 'Failed to get results');
|
|
243
272
|
}
|
|
244
273
|
catch (error) {
|
|
245
274
|
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
@@ -248,7 +277,7 @@ exports.ApiGateway = {
|
|
|
248
277
|
throw error;
|
|
249
278
|
}
|
|
250
279
|
},
|
|
251
|
-
async getUploadStatus(baseUrl,
|
|
280
|
+
async getUploadStatus(baseUrl, auth, options) {
|
|
252
281
|
const queryParams = new URLSearchParams();
|
|
253
282
|
if (options.uploadId) {
|
|
254
283
|
queryParams.append('uploadId', options.uploadId);
|
|
@@ -259,13 +288,13 @@ exports.ApiGateway = {
|
|
|
259
288
|
try {
|
|
260
289
|
const response = await fetch(`${baseUrl}/uploads/status?${queryParams}`, {
|
|
261
290
|
headers: {
|
|
262
|
-
|
|
291
|
+
...auth.headers,
|
|
263
292
|
},
|
|
264
293
|
});
|
|
265
294
|
if (!response.ok) {
|
|
266
295
|
await this.handleApiError(response, 'Failed to get upload status');
|
|
267
296
|
}
|
|
268
|
-
return response
|
|
297
|
+
return await parseJsonResponse(response, 'Failed to get upload status');
|
|
269
298
|
}
|
|
270
299
|
catch (error) {
|
|
271
300
|
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
@@ -274,7 +303,7 @@ exports.ApiGateway = {
|
|
|
274
303
|
throw error;
|
|
275
304
|
}
|
|
276
305
|
},
|
|
277
|
-
async listUploads(baseUrl,
|
|
306
|
+
async listUploads(baseUrl, auth, options = {}) {
|
|
278
307
|
const queryParams = new URLSearchParams();
|
|
279
308
|
if (options.name) {
|
|
280
309
|
queryParams.append('name', options.name);
|
|
@@ -295,13 +324,13 @@ exports.ApiGateway = {
|
|
|
295
324
|
try {
|
|
296
325
|
const response = await fetch(url, {
|
|
297
326
|
headers: {
|
|
298
|
-
|
|
327
|
+
...auth.headers,
|
|
299
328
|
},
|
|
300
329
|
});
|
|
301
330
|
if (!response.ok) {
|
|
302
331
|
await this.handleApiError(response, 'Failed to list uploads');
|
|
303
332
|
}
|
|
304
|
-
return response
|
|
333
|
+
return await parseJsonResponse(response, 'Failed to list uploads');
|
|
305
334
|
}
|
|
306
335
|
catch (error) {
|
|
307
336
|
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
@@ -310,19 +339,19 @@ exports.ApiGateway = {
|
|
|
310
339
|
throw error;
|
|
311
340
|
}
|
|
312
341
|
},
|
|
313
|
-
async uploadFlow(baseUrl,
|
|
342
|
+
async uploadFlow(baseUrl, auth, testFormData) {
|
|
314
343
|
try {
|
|
315
344
|
const res = await fetch(`${baseUrl}/uploads/flow`, {
|
|
316
345
|
body: testFormData,
|
|
317
346
|
headers: {
|
|
318
|
-
|
|
347
|
+
...auth.headers,
|
|
319
348
|
},
|
|
320
349
|
method: 'POST',
|
|
321
350
|
});
|
|
322
351
|
if (!res.ok) {
|
|
323
352
|
await this.handleApiError(res, 'Failed to upload test flows');
|
|
324
353
|
}
|
|
325
|
-
return res
|
|
354
|
+
return await parseJsonResponse(res, 'Failed to upload test flows');
|
|
326
355
|
}
|
|
327
356
|
catch (error) {
|
|
328
357
|
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
@@ -331,16 +360,74 @@ exports.ApiGateway = {
|
|
|
331
360
|
throw error;
|
|
332
361
|
}
|
|
333
362
|
},
|
|
363
|
+
/**
|
|
364
|
+
* Requests a storage URL for a client-direct flow zip upload. Mirrors
|
|
365
|
+
* `getBinaryUploadUrl` (same response shape) but stages the zip under
|
|
366
|
+
* `<orgId>/tests/` instead of the app-binary path. A 404 here means the API
|
|
367
|
+
* predates the client-direct flow path — callers fall back to `uploadFlow`.
|
|
368
|
+
*/
|
|
369
|
+
async getFlowUploadUrl(baseUrl, auth, fileSize) {
|
|
370
|
+
try {
|
|
371
|
+
const res = await fetch(`${baseUrl}/uploads/getFlowUploadUrl`, {
|
|
372
|
+
body: JSON.stringify({ fileSize, useTus: true }),
|
|
373
|
+
headers: {
|
|
374
|
+
'content-type': 'application/json',
|
|
375
|
+
...auth.headers,
|
|
376
|
+
},
|
|
377
|
+
method: 'POST',
|
|
378
|
+
});
|
|
379
|
+
if (!res.ok) {
|
|
380
|
+
await this.handleApiError(res, 'Failed to get flow upload URL');
|
|
381
|
+
}
|
|
382
|
+
// Same response shape as getBinaryUploadUrl: { id, tempPath, finalPath, path, b2?, token? }.
|
|
383
|
+
return await parseJsonResponse(res, 'Failed to get flow upload URL');
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
387
|
+
throw this.enhanceFetchError(error, `${baseUrl}/uploads/getFlowUploadUrl`);
|
|
388
|
+
}
|
|
389
|
+
throw error;
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
/**
|
|
393
|
+
* Submits a flow test that references an already-uploaded zip (JSON body, no
|
|
394
|
+
* multipart). The response is identical to the legacy `POST /uploads/flow`.
|
|
395
|
+
* A 404 means the API predates this endpoint — callers fall back to
|
|
396
|
+
* `uploadFlow`.
|
|
397
|
+
*/
|
|
398
|
+
async submitFlowTest(baseUrl, auth, body) {
|
|
399
|
+
try {
|
|
400
|
+
const res = await fetch(`${baseUrl}/uploads/submitFlowTest`, {
|
|
401
|
+
body: JSON.stringify(body),
|
|
402
|
+
headers: {
|
|
403
|
+
'content-type': 'application/json',
|
|
404
|
+
...auth.headers,
|
|
405
|
+
},
|
|
406
|
+
method: 'POST',
|
|
407
|
+
});
|
|
408
|
+
if (!res.ok) {
|
|
409
|
+
await this.handleApiError(res, 'Failed to submit test flows');
|
|
410
|
+
}
|
|
411
|
+
// Identical response to the legacy multipart POST /uploads/flow.
|
|
412
|
+
return await parseJsonResponse(res, 'Failed to submit test flows');
|
|
413
|
+
}
|
|
414
|
+
catch (error) {
|
|
415
|
+
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
416
|
+
throw this.enhanceFetchError(error, `${baseUrl}/uploads/submitFlowTest`);
|
|
417
|
+
}
|
|
418
|
+
throw error;
|
|
419
|
+
}
|
|
420
|
+
},
|
|
334
421
|
/**
|
|
335
422
|
* Generic report download method that handles both junit and allure reports
|
|
336
423
|
* @param baseUrl - API base URL
|
|
337
|
-
* @param
|
|
424
|
+
* @param auth - AuthContext (API key or Bearer session) for authentication
|
|
338
425
|
* @param uploadId - Upload ID to download report for
|
|
339
426
|
* @param reportType - Type of report to download ('junit' or 'allure')
|
|
340
427
|
* @param reportPath - Optional custom path for the downloaded report
|
|
341
428
|
* @returns Promise that resolves when download is complete
|
|
342
429
|
*/
|
|
343
|
-
async downloadReportGeneric(baseUrl,
|
|
430
|
+
async downloadReportGeneric(baseUrl, auth, uploadId, reportType, reportPath) {
|
|
344
431
|
// Define endpoint and default filename mappings
|
|
345
432
|
const config = {
|
|
346
433
|
junit: {
|
|
@@ -369,7 +456,7 @@ exports.ApiGateway = {
|
|
|
369
456
|
// Make the download request
|
|
370
457
|
const res = await fetch(url, {
|
|
371
458
|
headers: {
|
|
372
|
-
|
|
459
|
+
...auth.headers,
|
|
373
460
|
},
|
|
374
461
|
method: 'GET',
|
|
375
462
|
});
|
|
@@ -380,38 +467,145 @@ exports.ApiGateway = {
|
|
|
380
467
|
}
|
|
381
468
|
throw new Error(`${errorPrefix}: ${res.status} ${errorText}`);
|
|
382
469
|
}
|
|
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
|
-
|
|
470
|
+
await this.streamResponseToFile(res, finalReportPath, errorPrefix);
|
|
471
|
+
}
|
|
472
|
+
catch (error) {
|
|
473
|
+
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
474
|
+
throw this.enhanceFetchError(error, url);
|
|
475
|
+
}
|
|
476
|
+
throw error;
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
async startLiveSession(baseUrl, auth, params) {
|
|
480
|
+
try {
|
|
481
|
+
const res = await fetch(`${baseUrl}/live`, {
|
|
482
|
+
body: JSON.stringify({
|
|
483
|
+
binaryUploadId: params.binaryUploadId,
|
|
484
|
+
deviceLocale: params.deviceLocale,
|
|
485
|
+
platform: params.platform,
|
|
486
|
+
androidDevice: params.androidDevice,
|
|
487
|
+
androidApiLevel: params.androidApiLevel,
|
|
488
|
+
}),
|
|
489
|
+
headers: {
|
|
490
|
+
'content-type': 'application/json',
|
|
491
|
+
...auth.headers,
|
|
492
|
+
},
|
|
493
|
+
method: 'POST',
|
|
494
|
+
});
|
|
495
|
+
if (!res.ok) {
|
|
496
|
+
await this.handleApiError(res, 'Failed to start live session');
|
|
497
|
+
}
|
|
498
|
+
return await parseJsonResponse(res, 'Failed to start live session');
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
502
|
+
throw this.enhanceFetchError(error, `${baseUrl}/live`);
|
|
503
|
+
}
|
|
504
|
+
throw error;
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
async installLiveBinary(baseUrl, auth, sessionName, binaryUploadId) {
|
|
508
|
+
const url = `${baseUrl}/live/${sessionName}/install`;
|
|
509
|
+
try {
|
|
510
|
+
const res = await fetch(url, {
|
|
511
|
+
body: JSON.stringify({ binaryUploadId }),
|
|
512
|
+
headers: {
|
|
513
|
+
'content-type': 'application/json',
|
|
514
|
+
...auth.headers,
|
|
515
|
+
},
|
|
516
|
+
method: 'POST',
|
|
517
|
+
});
|
|
518
|
+
if (!res.ok) {
|
|
519
|
+
await this.handleApiError(res, 'Failed to install binary');
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
524
|
+
throw this.enhanceFetchError(error, url);
|
|
525
|
+
}
|
|
526
|
+
throw error;
|
|
527
|
+
}
|
|
528
|
+
},
|
|
529
|
+
async execLiveYaml(baseUrl, auth, sessionName, yaml, opts = {}) {
|
|
530
|
+
const url = `${baseUrl}/live/${sessionName}/exec`;
|
|
531
|
+
try {
|
|
532
|
+
const res = await fetch(url, {
|
|
533
|
+
body: JSON.stringify({ yaml, async: opts.async }),
|
|
534
|
+
headers: {
|
|
535
|
+
'content-type': 'application/json',
|
|
536
|
+
...auth.headers,
|
|
537
|
+
},
|
|
538
|
+
method: 'POST',
|
|
539
|
+
});
|
|
540
|
+
if (!res.ok) {
|
|
541
|
+
await this.handleApiError(res, 'Failed to execute test');
|
|
542
|
+
}
|
|
543
|
+
return await parseJsonResponse(res, 'Failed to execute test');
|
|
544
|
+
}
|
|
545
|
+
catch (error) {
|
|
546
|
+
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
547
|
+
throw this.enhanceFetchError(error, url);
|
|
548
|
+
}
|
|
549
|
+
throw error;
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
async getLiveCommand(baseUrl, auth, sessionName, commandId) {
|
|
553
|
+
const url = `${baseUrl}/live/${sessionName}/commands/${commandId}`;
|
|
554
|
+
try {
|
|
555
|
+
const res = await fetch(url, { headers: { ...auth.headers }, method: 'GET' });
|
|
556
|
+
if (!res.ok) {
|
|
557
|
+
await this.handleApiError(res, 'Failed to get command status');
|
|
558
|
+
}
|
|
559
|
+
return await parseJsonResponse(res, 'Failed to get command status');
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
563
|
+
throw this.enhanceFetchError(error, url);
|
|
564
|
+
}
|
|
565
|
+
throw error;
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
async keepaliveLiveSession(baseUrl, auth, sessionName) {
|
|
569
|
+
const url = `${baseUrl}/live/${sessionName}/keepalive`;
|
|
570
|
+
try {
|
|
571
|
+
const res = await fetch(url, { headers: { ...auth.headers }, method: 'POST' });
|
|
572
|
+
// A keepalive failure shouldn't kill a long-running poll; swallow non-OK.
|
|
573
|
+
if (!res.ok)
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
// best effort
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
async stopLiveSession(baseUrl, auth, sessionName) {
|
|
581
|
+
const url = `${baseUrl}/live/${sessionName}`;
|
|
582
|
+
try {
|
|
583
|
+
const res = await fetch(url, {
|
|
584
|
+
headers: { ...auth.headers },
|
|
585
|
+
method: 'DELETE',
|
|
586
|
+
});
|
|
587
|
+
if (!res.ok) {
|
|
588
|
+
await this.handleApiError(res, 'Failed to stop session');
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
593
|
+
throw this.enhanceFetchError(error, url);
|
|
594
|
+
}
|
|
595
|
+
throw error;
|
|
596
|
+
}
|
|
597
|
+
},
|
|
598
|
+
async getLiveSession(baseUrl, auth, sessionName) {
|
|
599
|
+
const url = `${baseUrl}/live/${sessionName}`;
|
|
600
|
+
try {
|
|
601
|
+
const res = await fetch(url, {
|
|
602
|
+
headers: { ...auth.headers },
|
|
603
|
+
method: 'GET',
|
|
604
|
+
});
|
|
605
|
+
if (!res.ok) {
|
|
606
|
+
await this.handleApiError(res, 'Failed to get session status');
|
|
410
607
|
}
|
|
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));
|
|
608
|
+
return await parseJsonResponse(res, 'Failed to get session status');
|
|
415
609
|
}
|
|
416
610
|
catch (error) {
|
|
417
611
|
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { StoredSession } from '../utils/config-store.js';
|
|
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
|
+
};
|