@devicecloud.dev/dcd 5.0.0-beta.0 → 5.0.0-beta.2
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/LICENSE +21 -0
- package/README.md +52 -0
- package/dist/commands/artifacts.d.ts +28 -28
- package/dist/commands/artifacts.js +20 -23
- package/dist/commands/cloud.d.ts +57 -57
- package/dist/commands/cloud.js +224 -192
- package/dist/commands/list.d.ts +22 -22
- package/dist/commands/list.js +43 -40
- package/dist/commands/live.js +134 -127
- package/dist/commands/login.d.ts +11 -11
- package/dist/commands/login.js +46 -44
- package/dist/commands/logout.js +16 -18
- package/dist/commands/status.d.ts +11 -11
- package/dist/commands/status.js +53 -44
- package/dist/commands/switch-org.d.ts +7 -7
- package/dist/commands/switch-org.js +19 -21
- package/dist/commands/upgrade.js +41 -33
- package/dist/commands/upload.d.ts +10 -10
- package/dist/commands/upload.js +42 -43
- package/dist/commands/whoami.js +17 -20
- package/dist/config/environments.js +6 -12
- package/dist/config/flags/api.flags.js +1 -4
- package/dist/config/flags/binary.flags.js +1 -4
- package/dist/config/flags/device.flags.js +6 -9
- package/dist/config/flags/environment.flags.js +1 -4
- package/dist/config/flags/execution.flags.js +1 -4
- package/dist/config/flags/github.flags.js +1 -4
- package/dist/config/flags/output.flags.js +1 -4
- package/dist/constants.js +15 -18
- package/dist/gateways/api-gateway.d.ts +31 -6
- package/dist/gateways/api-gateway.js +70 -16
- package/dist/gateways/cli-auth-gateway.d.ts +1 -1
- package/dist/gateways/cli-auth-gateway.js +3 -6
- package/dist/gateways/realtime-gateway.d.ts +32 -0
- package/dist/gateways/realtime-gateway.js +103 -0
- package/dist/gateways/supabase-gateway.d.ts +1 -1
- package/dist/gateways/supabase-gateway.js +10 -14
- package/dist/index.js +41 -38
- package/dist/mcp/context.d.ts +33 -0
- package/dist/mcp/context.js +33 -0
- package/dist/mcp/helpers.d.ts +16 -0
- package/dist/mcp/helpers.js +34 -0
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +24 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +27 -0
- package/dist/mcp/tools/download-artifacts.d.ts +11 -0
- package/dist/mcp/tools/download-artifacts.js +84 -0
- package/dist/mcp/tools/get-status.d.ts +7 -0
- package/dist/mcp/tools/get-status.js +39 -0
- package/dist/mcp/tools/list-devices.d.ts +7 -0
- package/dist/mcp/tools/list-devices.js +27 -0
- package/dist/mcp/tools/list-runs.d.ts +3 -0
- package/dist/mcp/tools/list-runs.js +60 -0
- package/dist/mcp/tools/run-cloud-test.d.ts +14 -0
- package/dist/mcp/tools/run-cloud-test.js +233 -0
- package/dist/methods.d.ts +32 -1
- package/dist/methods.js +133 -79
- package/dist/services/device-validation.service.d.ts +1 -1
- package/dist/services/device-validation.service.js +1 -5
- package/dist/services/execution-plan.service.js +14 -17
- package/dist/services/execution-plan.utils.js +15 -23
- package/dist/services/flow-paths.d.ts +17 -0
- package/dist/services/flow-paths.js +52 -0
- package/dist/services/metadata-extractor.service.js +22 -25
- package/dist/services/moropo.service.js +18 -20
- package/dist/services/report-download.service.d.ts +1 -1
- package/dist/services/report-download.service.js +5 -9
- package/dist/services/results-polling.service.d.ts +18 -3
- package/dist/services/results-polling.service.js +211 -108
- package/dist/services/telemetry.service.d.ts +10 -1
- package/dist/services/telemetry.service.js +40 -18
- package/dist/services/test-submission.service.d.ts +21 -4
- package/dist/services/test-submission.service.js +51 -34
- package/dist/services/version.service.d.ts +30 -7
- package/dist/services/version.service.js +88 -32
- package/dist/types/domain/auth.types.d.ts +8 -0
- package/dist/types/domain/auth.types.js +1 -2
- package/dist/types/domain/device.types.js +8 -11
- package/dist/types/domain/live.types.js +1 -2
- package/dist/types/generated/schema.types.js +1 -2
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.js +2 -18
- package/dist/types.js +1 -2
- package/dist/utils/auth.d.ts +1 -1
- package/dist/utils/auth.js +27 -28
- package/dist/utils/ci.d.ts +12 -0
- package/dist/utils/ci.js +39 -0
- package/dist/utils/cli.d.ts +16 -2
- package/dist/utils/cli.js +57 -29
- package/dist/utils/compatibility.d.ts +1 -1
- package/dist/utils/compatibility.js +5 -7
- package/dist/utils/config-store.js +33 -43
- package/dist/utils/connectivity.js +1 -4
- package/dist/utils/expo.js +15 -21
- package/dist/utils/orgs.js +8 -12
- package/dist/utils/paths.js +2 -5
- package/dist/utils/progress.d.ts +3 -0
- package/dist/utils/progress.js +47 -8
- package/dist/utils/styling.d.ts +35 -37
- package/dist/utils/styling.js +52 -86
- package/dist/utils/ui.d.ts +41 -0
- package/dist/utils/ui.js +95 -0
- package/package.json +27 -24
package/dist/methods.js
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const supabase_gateway_1 = require("./gateways/supabase-gateway");
|
|
16
|
-
const metadata_extractor_service_1 = require("./services/metadata-extractor.service");
|
|
17
|
-
const styling_1 = require("./utils/styling");
|
|
1
|
+
import { ux } from './utils/progress.js';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { createReadStream, createWriteStream, mkdirSync, readdirSync, writeFileSync, } from 'node:fs';
|
|
4
|
+
import { access, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
5
|
+
import * as os from 'node:os';
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
import { pipeline } from 'node:stream/promises';
|
|
8
|
+
import StreamZip from 'node-stream-zip';
|
|
9
|
+
import * as yazl from 'yazl';
|
|
10
|
+
import { inferEnvFromApiUrl } from './config/environments.js';
|
|
11
|
+
import { ApiError, ApiGateway } from './gateways/api-gateway.js';
|
|
12
|
+
import { SupabaseGateway } from './gateways/supabase-gateway.js';
|
|
13
|
+
import { MetadataExtractorService } from './services/metadata-extractor.service.js';
|
|
14
|
+
import { colors, formatId } from './utils/styling.js';
|
|
18
15
|
const mimeTypeLookupByExtension = {
|
|
19
16
|
apk: 'application/vnd.android.package-archive',
|
|
20
17
|
yaml: 'application/x-yaml',
|
|
@@ -44,7 +41,7 @@ function toZipEntryName(relativePath) {
|
|
|
44
41
|
async function compressFolderToTempZip(sourceDir) {
|
|
45
42
|
const zipfile = new yazl.ZipFile();
|
|
46
43
|
const rootName = path.basename(sourceDir);
|
|
47
|
-
const entries =
|
|
44
|
+
const entries = readdirSync(sourceDir, {
|
|
48
45
|
recursive: true,
|
|
49
46
|
withFileTypes: true,
|
|
50
47
|
});
|
|
@@ -55,13 +52,13 @@ async function compressFolderToTempZip(sourceDir) {
|
|
|
55
52
|
const relativePath = path.relative(sourceDir, absolutePath);
|
|
56
53
|
zipfile.addFile(absolutePath, toZipEntryName(path.join(rootName, relativePath)));
|
|
57
54
|
}
|
|
58
|
-
const tempDir = await
|
|
55
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'dcd-app-zip-'));
|
|
59
56
|
const zipPath = path.join(tempDir, `${rootName}.zip`);
|
|
60
57
|
zipfile.end();
|
|
61
|
-
await
|
|
58
|
+
await pipeline(zipfile.outputStream, createWriteStream(zipPath));
|
|
62
59
|
return { tempDir, zipPath };
|
|
63
60
|
}
|
|
64
|
-
const compressFilesFromRelativePath = async (basePath, files, commonRoot) => {
|
|
61
|
+
export const compressFilesFromRelativePath = async (basePath, files, commonRoot) => {
|
|
65
62
|
const zipfile = new yazl.ZipFile();
|
|
66
63
|
for (const file of files) {
|
|
67
64
|
// Anchored prefix strip — replace() would remove the first occurrence
|
|
@@ -70,8 +67,7 @@ const compressFilesFromRelativePath = async (basePath, files, commonRoot) => {
|
|
|
70
67
|
}
|
|
71
68
|
return zipToBuffer(zipfile);
|
|
72
69
|
};
|
|
73
|
-
|
|
74
|
-
const verifyAppZip = async (zipPath) => {
|
|
70
|
+
export const verifyAppZip = async (zipPath) => {
|
|
75
71
|
// eslint-disable-next-line import/namespace, new-cap
|
|
76
72
|
const zip = await new StreamZip.async({
|
|
77
73
|
file: zipPath,
|
|
@@ -96,11 +92,10 @@ const verifyAppZip = async (zipPath) => {
|
|
|
96
92
|
zip.close();
|
|
97
93
|
}
|
|
98
94
|
};
|
|
99
|
-
|
|
100
|
-
const uploadBinary = async (config) => {
|
|
95
|
+
export const uploadBinary = async (config) => {
|
|
101
96
|
const { filePath, apiUrl, auth, ignoreShaCheck = false, log = true, debug = false } = config;
|
|
102
97
|
if (log) {
|
|
103
|
-
|
|
98
|
+
ux.action.start(colors.bold('Checking and uploading binary'), colors.dim('Initializing'), {
|
|
104
99
|
stdout: true,
|
|
105
100
|
});
|
|
106
101
|
}
|
|
@@ -122,8 +117,8 @@ const uploadBinary = async (config) => {
|
|
|
122
117
|
const { exists, binaryId } = await checkExistingUpload(apiUrl, auth, sha, debug);
|
|
123
118
|
if (exists && binaryId) {
|
|
124
119
|
if (log) {
|
|
125
|
-
|
|
126
|
-
|
|
120
|
+
ux.info(colors.dim('SHA hash matches existing binary with ID: ') + formatId(binaryId) + colors.dim(', skipping upload. Force upload with --ignore-sha-check'));
|
|
121
|
+
ux.action.stop(colors.info('Skipping upload'));
|
|
127
122
|
}
|
|
128
123
|
return binaryId;
|
|
129
124
|
}
|
|
@@ -131,13 +126,13 @@ const uploadBinary = async (config) => {
|
|
|
131
126
|
// Perform the upload
|
|
132
127
|
const uploadId = await performUpload({ auth, apiUrl, debug, filePath, sha, source, startTime });
|
|
133
128
|
if (log) {
|
|
134
|
-
|
|
129
|
+
ux.action.stop(colors.success('\n✓ Binary uploaded with ID: ') + formatId(uploadId));
|
|
135
130
|
}
|
|
136
131
|
return uploadId;
|
|
137
132
|
}
|
|
138
133
|
catch (error) {
|
|
139
134
|
if (log) {
|
|
140
|
-
|
|
135
|
+
ux.action.stop(colors.error('✗ Failed'));
|
|
141
136
|
}
|
|
142
137
|
if (debug) {
|
|
143
138
|
console.error('[DEBUG] === BINARY UPLOAD FAILED ===');
|
|
@@ -153,11 +148,10 @@ const uploadBinary = async (config) => {
|
|
|
153
148
|
}
|
|
154
149
|
finally {
|
|
155
150
|
if (source?.cleanupDir) {
|
|
156
|
-
await
|
|
151
|
+
await rm(source.cleanupDir, { recursive: true, force: true }).catch(() => { });
|
|
157
152
|
}
|
|
158
153
|
}
|
|
159
154
|
};
|
|
160
|
-
exports.uploadBinary = uploadBinary;
|
|
161
155
|
/**
|
|
162
156
|
* Prepares a file for upload: .app directories are zipped to a temp file on
|
|
163
157
|
* disk; everything else is described in place. Nothing is read into memory.
|
|
@@ -178,7 +172,7 @@ async function prepareFileForUpload(filePath, debug, startTime) {
|
|
|
178
172
|
// Validate that the .app directory exists before attempting to compress —
|
|
179
173
|
// zipping a non-existent path silently produces an empty 22-byte zip.
|
|
180
174
|
try {
|
|
181
|
-
await
|
|
175
|
+
await access(filePath);
|
|
182
176
|
}
|
|
183
177
|
catch {
|
|
184
178
|
// Provide helpful error message for common quoting issues
|
|
@@ -201,7 +195,7 @@ async function prepareFileForUpload(filePath, debug, startTime) {
|
|
|
201
195
|
throw new Error(errorMessage);
|
|
202
196
|
}
|
|
203
197
|
const { tempDir, zipPath } = await compressFolderToTempZip(filePath);
|
|
204
|
-
const { size } = await
|
|
198
|
+
const { size } = await stat(zipPath);
|
|
205
199
|
source = {
|
|
206
200
|
contentType: 'application/zip',
|
|
207
201
|
cleanupDir: tempDir,
|
|
@@ -214,7 +208,7 @@ async function prepareFileForUpload(filePath, debug, startTime) {
|
|
|
214
208
|
}
|
|
215
209
|
}
|
|
216
210
|
else {
|
|
217
|
-
const { size } = await
|
|
211
|
+
const { size } = await stat(filePath);
|
|
218
212
|
if (debug) {
|
|
219
213
|
console.log(`[DEBUG] File size: ${(size / 1024 / 1024).toFixed(2)} MB`);
|
|
220
214
|
}
|
|
@@ -276,7 +270,7 @@ async function checkExistingUpload(apiUrl, auth, sha, debug) {
|
|
|
276
270
|
console.log(`[DEBUG] Target endpoint: ${apiUrl}/uploads/checkForExistingUpload`);
|
|
277
271
|
}
|
|
278
272
|
const shaCheckStartTime = Date.now();
|
|
279
|
-
const { appBinaryId, exists } = await
|
|
273
|
+
const { appBinaryId, exists } = await ApiGateway.checkForExistingUpload(apiUrl, auth, sha);
|
|
280
274
|
if (debug) {
|
|
281
275
|
console.log(`[DEBUG] SHA check completed in ${Date.now() - shaCheckStartTime}ms`);
|
|
282
276
|
console.log(`[DEBUG] Existing binary found: ${exists}`);
|
|
@@ -289,7 +283,7 @@ async function checkExistingUpload(apiUrl, auth, sha, debug) {
|
|
|
289
283
|
catch (error) {
|
|
290
284
|
// Invalid credentials will fail every subsequent request — surface now
|
|
291
285
|
// rather than after the user has waited through a potentially huge upload.
|
|
292
|
-
if (error instanceof
|
|
286
|
+
if (error instanceof ApiError && (error.status === 401 || error.status === 403)) {
|
|
293
287
|
throw error;
|
|
294
288
|
}
|
|
295
289
|
if (debug) {
|
|
@@ -325,7 +319,7 @@ async function uploadToSupabase(env, tempPath, source, debug) {
|
|
|
325
319
|
}
|
|
326
320
|
try {
|
|
327
321
|
const uploadStartTime = Date.now();
|
|
328
|
-
await
|
|
322
|
+
await SupabaseGateway.uploadResumable(env, tempPath, source, debug);
|
|
329
323
|
if (debug) {
|
|
330
324
|
const uploadDuration = Date.now() - uploadStartTime;
|
|
331
325
|
const uploadDurationSeconds = uploadDuration / 1000;
|
|
@@ -428,7 +422,7 @@ async function requestUploadPaths(apiUrl, auth, filePath, fileSize, debug) {
|
|
|
428
422
|
}
|
|
429
423
|
try {
|
|
430
424
|
const urlRequestStartTime = Date.now();
|
|
431
|
-
const { id, tempPath, finalPath, b2 } = await
|
|
425
|
+
const { id, tempPath, finalPath, b2 } = await ApiGateway.getBinaryUploadUrl(apiUrl, auth, platform, fileSize);
|
|
432
426
|
if (debug) {
|
|
433
427
|
const hasStrategy = b2 && typeof b2 === 'object' && 'strategy' in b2;
|
|
434
428
|
console.log(`[DEBUG] Upload URL request completed in ${Date.now() - urlRequestStartTime}ms`);
|
|
@@ -451,9 +445,11 @@ async function requestUploadPaths(apiUrl, auth, filePath, fileSize, debug) {
|
|
|
451
445
|
// Add context to the error
|
|
452
446
|
if (error instanceof Error) {
|
|
453
447
|
if (error.name === 'NetworkError') {
|
|
454
|
-
throw new Error(`Failed to request upload URL from API.\n\n${error.message}
|
|
448
|
+
throw new Error(`Failed to request upload URL from API.\n\n${error.message}`, { cause: error });
|
|
455
449
|
}
|
|
456
|
-
throw new Error(`Failed to request upload URL: ${error.message}
|
|
450
|
+
throw new Error(`Failed to request upload URL: ${error.message}`, {
|
|
451
|
+
cause: error,
|
|
452
|
+
});
|
|
457
453
|
}
|
|
458
454
|
throw error;
|
|
459
455
|
}
|
|
@@ -467,7 +463,7 @@ async function requestUploadPaths(apiUrl, auth, filePath, fileSize, debug) {
|
|
|
467
463
|
async function extractBinaryMetadata(filePath, debug) {
|
|
468
464
|
if (debug)
|
|
469
465
|
console.log('[DEBUG] Extracting app metadata...');
|
|
470
|
-
const metadataExtractor = new
|
|
466
|
+
const metadataExtractor = new MetadataExtractorService();
|
|
471
467
|
const metadata = await metadataExtractor.extract(filePath);
|
|
472
468
|
if (!metadata) {
|
|
473
469
|
throw new Error(`Failed to extract metadata from ${filePath}. Supported formats: .apk, .app, .zip`);
|
|
@@ -513,7 +509,7 @@ async function performUpload(config) {
|
|
|
513
509
|
const { id, tempPath, finalPath, b2 } = await requestUploadPaths(apiUrl, auth, filePath, source.size, debug);
|
|
514
510
|
// Extract app metadata
|
|
515
511
|
const metadata = await extractBinaryMetadata(filePath, debug);
|
|
516
|
-
const env =
|
|
512
|
+
const env = inferEnvFromApiUrl(apiUrl);
|
|
517
513
|
// Upload to Backblaze first (primary)
|
|
518
514
|
const backblazeResult = await handleBackblazeUpload({
|
|
519
515
|
auth,
|
|
@@ -525,11 +521,10 @@ async function performUpload(config) {
|
|
|
525
521
|
});
|
|
526
522
|
let lastError = backblazeResult.error;
|
|
527
523
|
// Always upload to Supabase (re-enabled as always-on alongside Backblaze)
|
|
528
|
-
let supabaseResult = { error: null, success: false };
|
|
529
524
|
if (debug) {
|
|
530
525
|
console.log('[DEBUG] Uploading to Supabase...');
|
|
531
526
|
}
|
|
532
|
-
supabaseResult = await uploadToSupabase(env, tempPath, source, debug);
|
|
527
|
+
const supabaseResult = await uploadToSupabase(env, tempPath, source, debug);
|
|
533
528
|
if (!supabaseResult.success && supabaseResult.error) {
|
|
534
529
|
lastError = supabaseResult.error;
|
|
535
530
|
}
|
|
@@ -549,7 +544,7 @@ async function performUpload(config) {
|
|
|
549
544
|
}
|
|
550
545
|
// Finalize upload
|
|
551
546
|
const finalizeStartTime = Date.now();
|
|
552
|
-
await
|
|
547
|
+
await ApiGateway.finaliseUpload({
|
|
553
548
|
auth,
|
|
554
549
|
backblazeSuccess: backblazeResult.success,
|
|
555
550
|
baseUrl: apiUrl,
|
|
@@ -567,6 +562,70 @@ async function performUpload(config) {
|
|
|
567
562
|
}
|
|
568
563
|
return id;
|
|
569
564
|
}
|
|
565
|
+
/**
|
|
566
|
+
* Uploads an already-built flow zip directly to storage, mirroring the binary
|
|
567
|
+
* client-direct path: getFlowUploadUrl → upload to storage (Backblaze if
|
|
568
|
+
* offered + TUS to Supabase) → return the storage reference for submitFlowTest.
|
|
569
|
+
*
|
|
570
|
+
* The zip arrives as an in-memory Buffer (from `compressFilesFromRelativePath`);
|
|
571
|
+
* it's written to a temp file so the exact same disk-streaming uploaders the
|
|
572
|
+
* binary path uses can be reused unchanged. `supabaseSuccess`/`backblazeSuccess`
|
|
573
|
+
* are reported honestly based on which uploads succeeded.
|
|
574
|
+
*
|
|
575
|
+
* Errors from `getFlowUploadUrl` propagate untouched so callers can detect a
|
|
576
|
+
* 404 from an older API and fall back to the legacy multipart endpoint.
|
|
577
|
+
*/
|
|
578
|
+
export const uploadFlowZip = async (config) => {
|
|
579
|
+
const { apiUrl, auth, buffer, debug = false } = config;
|
|
580
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'dcd-flow-zip-'));
|
|
581
|
+
const diskPath = path.join(tempDir, 'flowFile.zip');
|
|
582
|
+
try {
|
|
583
|
+
await writeFile(diskPath, buffer);
|
|
584
|
+
const source = {
|
|
585
|
+
contentType: 'application/zip',
|
|
586
|
+
diskPath,
|
|
587
|
+
name: 'flowFile.zip',
|
|
588
|
+
size: buffer.length,
|
|
589
|
+
};
|
|
590
|
+
// Same response shape as getBinaryUploadUrl. A 404 here means the API
|
|
591
|
+
// predates the client-direct flow path — let it propagate so the caller
|
|
592
|
+
// falls back to the multipart endpoint.
|
|
593
|
+
const { id, tempPath, finalPath, b2 } = await ApiGateway.getFlowUploadUrl(apiUrl, auth, buffer.length);
|
|
594
|
+
if (!tempPath)
|
|
595
|
+
throw new Error('No upload path provided by API');
|
|
596
|
+
const env = inferEnvFromApiUrl(apiUrl);
|
|
597
|
+
// Upload to Backblaze first (primary, if configured)
|
|
598
|
+
const backblazeResult = await handleBackblazeUpload({
|
|
599
|
+
auth,
|
|
600
|
+
apiUrl,
|
|
601
|
+
b2: b2,
|
|
602
|
+
debug,
|
|
603
|
+
finalPath,
|
|
604
|
+
source,
|
|
605
|
+
});
|
|
606
|
+
let lastError = backblazeResult.error;
|
|
607
|
+
// Always upload to Supabase (always-on alongside Backblaze)
|
|
608
|
+
const supabaseResult = await uploadToSupabase(env, tempPath, source, debug);
|
|
609
|
+
if (!supabaseResult.success && supabaseResult.error) {
|
|
610
|
+
lastError = supabaseResult.error;
|
|
611
|
+
}
|
|
612
|
+
validateUploadResults(supabaseResult.success, backblazeResult.success, lastError, b2, debug);
|
|
613
|
+
if (debug) {
|
|
614
|
+
console.log(`[DEBUG] Flow zip upload summary - Backblaze: ${backblazeResult.success ? '✓' : '✗'}, Supabase: ${supabaseResult.success ? '✓' : '✗'}`);
|
|
615
|
+
}
|
|
616
|
+
return {
|
|
617
|
+
backblazeSuccess: backblazeResult.success,
|
|
618
|
+
bytes: buffer.length,
|
|
619
|
+
id,
|
|
620
|
+
path: tempPath,
|
|
621
|
+
supabaseSuccess: supabaseResult.success,
|
|
622
|
+
useTus: true,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
finally {
|
|
626
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => { });
|
|
627
|
+
}
|
|
628
|
+
};
|
|
570
629
|
/**
|
|
571
630
|
* Upload file to Backblaze using signed URL (simple upload for files < 100MB)
|
|
572
631
|
* @param uploadUrl - Backblaze upload URL
|
|
@@ -582,9 +641,9 @@ async function uploadToBackblaze(uploadUrl, authorizationToken, fileName, source
|
|
|
582
641
|
// get the multi-part path), so one transient buffer here is bounded.
|
|
583
642
|
// S3 pre-signed PUTs reject chunked transfer encoding, which rules out a
|
|
584
643
|
// plain stream body.
|
|
585
|
-
const body = await
|
|
644
|
+
const body = await readFile(source.diskPath);
|
|
586
645
|
// Calculate SHA1 hash for Backblaze (B2 requires SHA1, not SHA256)
|
|
587
|
-
const sha1 =
|
|
646
|
+
const sha1 = createHash('sha1');
|
|
588
647
|
sha1.update(body);
|
|
589
648
|
const sha1Hex = sha1.digest('hex');
|
|
590
649
|
// Detect if this is an S3 pre-signed URL (authorization token is empty)
|
|
@@ -617,8 +676,9 @@ async function uploadToBackblaze(uploadUrl, authorizationToken, fileName, source
|
|
|
617
676
|
if (debug) {
|
|
618
677
|
console.error(`[DEBUG] Backblaze upload failed with status ${response.status}: ${errorText}`);
|
|
619
678
|
}
|
|
620
|
-
// Don't throw
|
|
621
|
-
|
|
679
|
+
// Don't throw and don't warn — Backblaze is the primary attempt and the
|
|
680
|
+
// Supabase fallback usually recovers. A user-facing error is raised only
|
|
681
|
+
// if every strategy fails (see validateUploadResults).
|
|
622
682
|
return false;
|
|
623
683
|
}
|
|
624
684
|
if (debug) {
|
|
@@ -641,12 +701,9 @@ async function uploadToBackblaze(uploadUrl, authorizationToken, fileName, source
|
|
|
641
701
|
if (debug) {
|
|
642
702
|
console.error('[DEBUG] Network error detected - could be DNS, connection, or SSL issue');
|
|
643
703
|
}
|
|
644
|
-
console.warn('Warning: Backblaze upload failed due to network error');
|
|
645
|
-
}
|
|
646
|
-
else {
|
|
647
|
-
// Don't throw - we don't want Backblaze failures to block the primary upload
|
|
648
|
-
console.warn(`Warning: Backblaze upload failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
649
704
|
}
|
|
705
|
+
// Don't throw and don't warn — the Supabase fallback usually recovers, and
|
|
706
|
+
// validateUploadResults raises a user-facing error only if all fail.
|
|
650
707
|
return false;
|
|
651
708
|
}
|
|
652
709
|
}
|
|
@@ -660,7 +717,7 @@ async function uploadToBackblaze(uploadUrl, authorizationToken, fileName, source
|
|
|
660
717
|
async function readFileChunk(filePath, start, end) {
|
|
661
718
|
return new Promise((resolve, reject) => {
|
|
662
719
|
const chunks = [];
|
|
663
|
-
const stream =
|
|
720
|
+
const stream = createReadStream(filePath, { start, end: end - 1 }); // end is inclusive in createReadStream
|
|
664
721
|
stream.on('data', (chunk) => {
|
|
665
722
|
chunks.push(chunk);
|
|
666
723
|
});
|
|
@@ -678,7 +735,7 @@ async function readFileChunk(filePath, start, end) {
|
|
|
678
735
|
* @returns SHA1 hash as hex string
|
|
679
736
|
*/
|
|
680
737
|
function calculateSha1(buffer) {
|
|
681
|
-
const sha1 =
|
|
738
|
+
const sha1 = createHash('sha1');
|
|
682
739
|
sha1.update(buffer);
|
|
683
740
|
return sha1.digest('hex');
|
|
684
741
|
}
|
|
@@ -716,7 +773,9 @@ async function uploadPartToBackblaze(config) {
|
|
|
716
773
|
if (debug) {
|
|
717
774
|
console.error(`[DEBUG] Network error uploading part ${partNumber} - could be DNS, connection, or SSL issue`);
|
|
718
775
|
}
|
|
719
|
-
throw new Error(`Part ${partNumber} upload failed due to network error
|
|
776
|
+
throw new Error(`Part ${partNumber} upload failed due to network error`, {
|
|
777
|
+
cause: error,
|
|
778
|
+
});
|
|
720
779
|
}
|
|
721
780
|
throw error;
|
|
722
781
|
}
|
|
@@ -740,12 +799,9 @@ function logBackblazeUploadError(error, debug) {
|
|
|
740
799
|
console.error(`[DEBUG] Stack trace:\n${error.stack}`);
|
|
741
800
|
}
|
|
742
801
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
else {
|
|
747
|
-
console.warn(`Warning: Backblaze large file upload failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
748
|
-
}
|
|
802
|
+
// No user-facing warning: Backblaze is the primary attempt and the Supabase
|
|
803
|
+
// fallback usually recovers; validateUploadResults raises the only
|
|
804
|
+
// user-facing error, and only when every strategy fails.
|
|
749
805
|
}
|
|
750
806
|
/**
|
|
751
807
|
* Upload large file to Backblaze using multi-part upload with streaming (for files >= 5MB)
|
|
@@ -790,7 +846,7 @@ async function uploadLargeFileToBackblaze(config) {
|
|
|
790
846
|
console.log('[DEBUG] Finishing large file upload...');
|
|
791
847
|
console.log(`[DEBUG] Finalizing ${partSha1Array.length} parts with fileId: ${fileId}`);
|
|
792
848
|
}
|
|
793
|
-
await
|
|
849
|
+
await ApiGateway.finishLargeFile(apiUrl, auth, fileId, partSha1Array);
|
|
794
850
|
if (debug)
|
|
795
851
|
console.log('[DEBUG] Large file upload completed successfully');
|
|
796
852
|
return true;
|
|
@@ -801,8 +857,8 @@ async function uploadLargeFileToBackblaze(config) {
|
|
|
801
857
|
}
|
|
802
858
|
}
|
|
803
859
|
async function getFileHashFromPath(filePath) {
|
|
804
|
-
const hash =
|
|
805
|
-
for await (const chunk of
|
|
860
|
+
const hash = createHash('sha256');
|
|
861
|
+
for await (const chunk of createReadStream(filePath)) {
|
|
806
862
|
hash.update(chunk);
|
|
807
863
|
}
|
|
808
864
|
return hash.digest('hex');
|
|
@@ -814,37 +870,36 @@ async function getFileHashFromPath(filePath) {
|
|
|
814
870
|
* @param logger - Logger object with log and warn methods
|
|
815
871
|
* @returns true if successful, false if an error occurred
|
|
816
872
|
*/
|
|
817
|
-
const writeJSONFile = (filePath, data, logger) => {
|
|
873
|
+
export const writeJSONFile = (filePath, data, logger) => {
|
|
818
874
|
try {
|
|
819
875
|
const directory = path.dirname(filePath);
|
|
820
876
|
if (directory !== '.') {
|
|
821
|
-
|
|
877
|
+
mkdirSync(directory, { recursive: true });
|
|
822
878
|
}
|
|
823
|
-
|
|
824
|
-
logger.log(
|
|
879
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
880
|
+
logger.log(colors.dim('JSON output written to: ') + colors.highlight(path.resolve(filePath)));
|
|
825
881
|
}
|
|
826
882
|
catch (error) {
|
|
827
883
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
828
884
|
const isPermissionError = errorMessage.includes('EACCES') || errorMessage.includes('EPERM');
|
|
829
885
|
const isNoSuchFileError = errorMessage.includes('ENOENT');
|
|
830
|
-
logger.warn(
|
|
886
|
+
logger.warn(colors.error(`Failed to write JSON output to file: ${filePath}`));
|
|
831
887
|
if (isPermissionError) {
|
|
832
|
-
logger.warn(
|
|
833
|
-
logger.warn(
|
|
888
|
+
logger.warn(colors.dim(' Permission denied - check file/directory write permissions'));
|
|
889
|
+
logger.warn(colors.dim(' Try running with appropriate permissions or choose a different output location'));
|
|
834
890
|
}
|
|
835
891
|
else if (isNoSuchFileError) {
|
|
836
|
-
logger.warn(
|
|
892
|
+
logger.warn(colors.dim(' Directory does not exist - create the directory first or choose an existing path'));
|
|
837
893
|
}
|
|
838
|
-
logger.warn(
|
|
894
|
+
logger.warn(colors.dim(' Error details: ') + errorMessage);
|
|
839
895
|
}
|
|
840
896
|
};
|
|
841
|
-
exports.writeJSONFile = writeJSONFile;
|
|
842
897
|
/**
|
|
843
898
|
* Formats duration in seconds into a human readable string
|
|
844
899
|
* @param durationSeconds - Duration in seconds
|
|
845
900
|
* @returns Formatted duration string (e.g. "2m 30s" or "45s")
|
|
846
901
|
*/
|
|
847
|
-
const formatDurationSeconds = (durationSeconds) => {
|
|
902
|
+
export const formatDurationSeconds = (durationSeconds) => {
|
|
848
903
|
const minutes = Math.floor(durationSeconds / 60);
|
|
849
904
|
const seconds = durationSeconds % 60;
|
|
850
905
|
if (minutes > 0) {
|
|
@@ -852,4 +907,3 @@ const formatDurationSeconds = (durationSeconds) => {
|
|
|
852
907
|
}
|
|
853
908
|
return `${durationSeconds}s`;
|
|
854
909
|
};
|
|
855
|
-
exports.formatDurationSeconds = formatDurationSeconds;
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DeviceValidationService = void 0;
|
|
4
1
|
/**
|
|
5
2
|
* Service for validating device configurations against compatibility data
|
|
6
3
|
*/
|
|
7
|
-
class DeviceValidationService {
|
|
4
|
+
export class DeviceValidationService {
|
|
8
5
|
/**
|
|
9
6
|
* Validate Android device configuration
|
|
10
7
|
* @param androidApiLevel Android API level to validate
|
|
@@ -91,4 +88,3 @@ class DeviceValidationService {
|
|
|
91
88
|
}
|
|
92
89
|
}
|
|
93
90
|
}
|
|
94
|
-
exports.DeviceValidationService = DeviceValidationService;
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const fs = require("node:fs");
|
|
5
|
-
const path = require("node:path");
|
|
6
|
-
const execution_plan_utils_1 = require("./execution-plan.utils");
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { getFlowsToRunInSequence, isFlowFile, processDependencies, readDirectory, readTestYamlFileAsJson, readYamlFileAsJson, } from './execution-plan.utils.js';
|
|
7
4
|
/**
|
|
8
5
|
* Recursively check and resolve all dependencies for a flow file
|
|
9
6
|
* Includes runFlow references, JavaScript scripts, and media files
|
|
@@ -16,8 +13,8 @@ async function checkDependencies(input) {
|
|
|
16
13
|
const uncheckedDependencies = [input];
|
|
17
14
|
while (uncheckedDependencies.length > 0) {
|
|
18
15
|
const fileToCheck = uncheckedDependencies.shift();
|
|
19
|
-
const { config, testSteps } =
|
|
20
|
-
const { allErrors, allFiles } =
|
|
16
|
+
const { config, testSteps } = readTestYamlFileAsJson(fileToCheck);
|
|
17
|
+
const { allErrors, allFiles } = processDependencies({
|
|
21
18
|
config,
|
|
22
19
|
input: fileToCheck,
|
|
23
20
|
testSteps,
|
|
@@ -27,7 +24,7 @@ async function checkDependencies(input) {
|
|
|
27
24
|
allErrors.join('\n'));
|
|
28
25
|
}
|
|
29
26
|
for (const file of allFiles) {
|
|
30
|
-
if (!
|
|
27
|
+
if (!isFlowFile(file)) {
|
|
31
28
|
// js/media files don't have dependencies
|
|
32
29
|
checkedDependencies.push(file);
|
|
33
30
|
}
|
|
@@ -63,7 +60,7 @@ function getWorkspaceConfig(input, unfilteredFlowFiles) {
|
|
|
63
60
|
const possibleConfigPaths = new Set([path.join(input, 'config.yaml'), path.join(input, 'config.yml')].map((p) => path.normalize(p)));
|
|
64
61
|
const configFilePath = unfilteredFlowFiles.find((file) => possibleConfigPaths.has(path.normalize(file)));
|
|
65
62
|
const config = configFilePath
|
|
66
|
-
?
|
|
63
|
+
? readYamlFileAsJson(configFilePath)
|
|
67
64
|
: {};
|
|
68
65
|
return config;
|
|
69
66
|
}
|
|
@@ -99,7 +96,7 @@ async function planSingleFile(normalizedInput, configFile) {
|
|
|
99
96
|
normalizedInput.endsWith('config.yml')) {
|
|
100
97
|
throw new Error('If using config.yaml, pass the workspace folder path, not the config file or a custom path via --config');
|
|
101
98
|
}
|
|
102
|
-
const { config } =
|
|
99
|
+
const { config } = readTestYamlFileAsJson(normalizedInput);
|
|
103
100
|
const flowMetadata = {};
|
|
104
101
|
const flowOverrides = {};
|
|
105
102
|
if (config) {
|
|
@@ -112,7 +109,7 @@ async function planSingleFile(normalizedInput, configFile) {
|
|
|
112
109
|
if (!fs.existsSync(configFilePath)) {
|
|
113
110
|
throw new Error(`Config file does not exist: ${configFilePath}`);
|
|
114
111
|
}
|
|
115
|
-
workspaceConfig =
|
|
112
|
+
workspaceConfig = readYamlFileAsJson(configFilePath);
|
|
116
113
|
}
|
|
117
114
|
const checkedDependancies = await checkDependencies(normalizedInput);
|
|
118
115
|
return {
|
|
@@ -193,7 +190,7 @@ function resolveSequentialFlows(workspaceConfig, pathsByName, debug) {
|
|
|
193
190
|
if (debug && flowOrder !== normalizedFlowOrder) {
|
|
194
191
|
console.log(`[DEBUG] Stripping trailing extension: "${flowOrder}" -> "${normalizedFlowOrder}"`);
|
|
195
192
|
}
|
|
196
|
-
return
|
|
193
|
+
return getFlowsToRunInSequence(pathsByName, [normalizedFlowOrder], debug);
|
|
197
194
|
})),
|
|
198
195
|
];
|
|
199
196
|
if (debug) {
|
|
@@ -225,7 +222,7 @@ function resolveSequentialFlows(workspaceConfig, pathsByName, debug) {
|
|
|
225
222
|
* @returns Complete execution plan with flows, dependencies, and metadata
|
|
226
223
|
* @throws Error if input path doesn't exist, no flows found, or dependencies missing
|
|
227
224
|
*/
|
|
228
|
-
async function plan(options) {
|
|
225
|
+
export async function plan(options) {
|
|
229
226
|
const { input, includeTags = [], excludeTags = [], excludeFlows, configFile, debug = false, } = options;
|
|
230
227
|
const normalizedInput = path.normalize(input);
|
|
231
228
|
const flowMetadata = {};
|
|
@@ -235,7 +232,7 @@ async function plan(options) {
|
|
|
235
232
|
if (fs.lstatSync(normalizedInput).isFile()) {
|
|
236
233
|
return planSingleFile(normalizedInput, configFile);
|
|
237
234
|
}
|
|
238
|
-
let unfilteredFlowFiles = await
|
|
235
|
+
let unfilteredFlowFiles = await readDirectory(normalizedInput, isFlowFile);
|
|
239
236
|
if (unfilteredFlowFiles.length === 0) {
|
|
240
237
|
throw new Error(`Flow directory does not contain any Flow files: ${path.resolve(normalizedInput)}`);
|
|
241
238
|
}
|
|
@@ -246,7 +243,7 @@ async function plan(options) {
|
|
|
246
243
|
if (!fs.existsSync(configFilePath)) {
|
|
247
244
|
throw new Error(`Config file does not exist: ${configFilePath}`);
|
|
248
245
|
}
|
|
249
|
-
workspaceConfig =
|
|
246
|
+
workspaceConfig = readYamlFileAsJson(configFilePath);
|
|
250
247
|
}
|
|
251
248
|
else {
|
|
252
249
|
workspaceConfig = getWorkspaceConfig(normalizedInput, unfilteredFlowFiles);
|
|
@@ -260,7 +257,7 @@ async function plan(options) {
|
|
|
260
257
|
}
|
|
261
258
|
// eslint-disable-next-line unicorn/no-array-reduce
|
|
262
259
|
const configPerFlowFile = unfilteredFlowFiles.reduce((acc, filePath) => {
|
|
263
|
-
const { config } =
|
|
260
|
+
const { config } = readTestYamlFileAsJson(filePath);
|
|
264
261
|
acc[filePath] = config;
|
|
265
262
|
return acc;
|
|
266
263
|
}, {});
|