@devicecloud.dev/dcd 5.0.0-beta.0 → 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 +35 -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 +173 -186
- package/dist/commands/list.d.ts +22 -22
- package/dist/commands/list.js +36 -38
- 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 +45 -43
- package/dist/commands/switch-org.d.ts +7 -7
- package/dist/commands/switch-org.js +19 -21
- package/dist/commands/upgrade.js +29 -31
- 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 +125 -66
- 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 +195 -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 +1 -1
- package/dist/services/version.service.js +1 -5
- 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.js +18 -27
- 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.js +2 -5
- 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)
|
|
@@ -660,7 +719,7 @@ async function uploadToBackblaze(uploadUrl, authorizationToken, fileName, source
|
|
|
660
719
|
async function readFileChunk(filePath, start, end) {
|
|
661
720
|
return new Promise((resolve, reject) => {
|
|
662
721
|
const chunks = [];
|
|
663
|
-
const stream =
|
|
722
|
+
const stream = createReadStream(filePath, { start, end: end - 1 }); // end is inclusive in createReadStream
|
|
664
723
|
stream.on('data', (chunk) => {
|
|
665
724
|
chunks.push(chunk);
|
|
666
725
|
});
|
|
@@ -678,7 +737,7 @@ async function readFileChunk(filePath, start, end) {
|
|
|
678
737
|
* @returns SHA1 hash as hex string
|
|
679
738
|
*/
|
|
680
739
|
function calculateSha1(buffer) {
|
|
681
|
-
const sha1 =
|
|
740
|
+
const sha1 = createHash('sha1');
|
|
682
741
|
sha1.update(buffer);
|
|
683
742
|
return sha1.digest('hex');
|
|
684
743
|
}
|
|
@@ -716,7 +775,9 @@ async function uploadPartToBackblaze(config) {
|
|
|
716
775
|
if (debug) {
|
|
717
776
|
console.error(`[DEBUG] Network error uploading part ${partNumber} - could be DNS, connection, or SSL issue`);
|
|
718
777
|
}
|
|
719
|
-
throw new Error(`Part ${partNumber} upload failed due to network error
|
|
778
|
+
throw new Error(`Part ${partNumber} upload failed due to network error`, {
|
|
779
|
+
cause: error,
|
|
780
|
+
});
|
|
720
781
|
}
|
|
721
782
|
throw error;
|
|
722
783
|
}
|
|
@@ -790,7 +851,7 @@ async function uploadLargeFileToBackblaze(config) {
|
|
|
790
851
|
console.log('[DEBUG] Finishing large file upload...');
|
|
791
852
|
console.log(`[DEBUG] Finalizing ${partSha1Array.length} parts with fileId: ${fileId}`);
|
|
792
853
|
}
|
|
793
|
-
await
|
|
854
|
+
await ApiGateway.finishLargeFile(apiUrl, auth, fileId, partSha1Array);
|
|
794
855
|
if (debug)
|
|
795
856
|
console.log('[DEBUG] Large file upload completed successfully');
|
|
796
857
|
return true;
|
|
@@ -801,8 +862,8 @@ async function uploadLargeFileToBackblaze(config) {
|
|
|
801
862
|
}
|
|
802
863
|
}
|
|
803
864
|
async function getFileHashFromPath(filePath) {
|
|
804
|
-
const hash =
|
|
805
|
-
for await (const chunk of
|
|
865
|
+
const hash = createHash('sha256');
|
|
866
|
+
for await (const chunk of createReadStream(filePath)) {
|
|
806
867
|
hash.update(chunk);
|
|
807
868
|
}
|
|
808
869
|
return hash.digest('hex');
|
|
@@ -814,37 +875,36 @@ async function getFileHashFromPath(filePath) {
|
|
|
814
875
|
* @param logger - Logger object with log and warn methods
|
|
815
876
|
* @returns true if successful, false if an error occurred
|
|
816
877
|
*/
|
|
817
|
-
const writeJSONFile = (filePath, data, logger) => {
|
|
878
|
+
export const writeJSONFile = (filePath, data, logger) => {
|
|
818
879
|
try {
|
|
819
880
|
const directory = path.dirname(filePath);
|
|
820
881
|
if (directory !== '.') {
|
|
821
|
-
|
|
882
|
+
mkdirSync(directory, { recursive: true });
|
|
822
883
|
}
|
|
823
|
-
|
|
824
|
-
logger.log(
|
|
884
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
885
|
+
logger.log(colors.dim('JSON output written to: ') + colors.highlight(path.resolve(filePath)));
|
|
825
886
|
}
|
|
826
887
|
catch (error) {
|
|
827
888
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
828
889
|
const isPermissionError = errorMessage.includes('EACCES') || errorMessage.includes('EPERM');
|
|
829
890
|
const isNoSuchFileError = errorMessage.includes('ENOENT');
|
|
830
|
-
logger.warn(
|
|
891
|
+
logger.warn(colors.error(`Failed to write JSON output to file: ${filePath}`));
|
|
831
892
|
if (isPermissionError) {
|
|
832
|
-
logger.warn(
|
|
833
|
-
logger.warn(
|
|
893
|
+
logger.warn(colors.dim(' Permission denied - check file/directory write permissions'));
|
|
894
|
+
logger.warn(colors.dim(' Try running with appropriate permissions or choose a different output location'));
|
|
834
895
|
}
|
|
835
896
|
else if (isNoSuchFileError) {
|
|
836
|
-
logger.warn(
|
|
897
|
+
logger.warn(colors.dim(' Directory does not exist - create the directory first or choose an existing path'));
|
|
837
898
|
}
|
|
838
|
-
logger.warn(
|
|
899
|
+
logger.warn(colors.dim(' Error details: ') + errorMessage);
|
|
839
900
|
}
|
|
840
901
|
};
|
|
841
|
-
exports.writeJSONFile = writeJSONFile;
|
|
842
902
|
/**
|
|
843
903
|
* Formats duration in seconds into a human readable string
|
|
844
904
|
* @param durationSeconds - Duration in seconds
|
|
845
905
|
* @returns Formatted duration string (e.g. "2m 30s" or "45s")
|
|
846
906
|
*/
|
|
847
|
-
const formatDurationSeconds = (durationSeconds) => {
|
|
907
|
+
export const formatDurationSeconds = (durationSeconds) => {
|
|
848
908
|
const minutes = Math.floor(durationSeconds / 60);
|
|
849
909
|
const seconds = durationSeconds % 60;
|
|
850
910
|
if (minutes > 0) {
|
|
@@ -852,4 +912,3 @@ const formatDurationSeconds = (durationSeconds) => {
|
|
|
852
912
|
}
|
|
853
913
|
return `${durationSeconds}s`;
|
|
854
914
|
};
|
|
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
|
}, {});
|
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
exports.getFlowsToRunInSequence = getFlowsToRunInSequence;
|
|
5
|
-
exports.isFlowFile = isFlowFile;
|
|
6
|
-
exports.readDirectory = readDirectory;
|
|
7
|
-
const yaml = require("js-yaml");
|
|
8
|
-
const fs = require("node:fs");
|
|
9
|
-
const path = require("node:path");
|
|
1
|
+
import * as yaml from 'js-yaml';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
10
4
|
const commandsThatRequireFiles = new Set(['addMedia', 'runFlow', 'runScript']);
|
|
11
|
-
function getFlowsToRunInSequence(paths, flowOrder, debug = false) {
|
|
5
|
+
export function getFlowsToRunInSequence(paths, flowOrder, debug = false) {
|
|
12
6
|
if (flowOrder.length === 0) {
|
|
13
7
|
if (debug) {
|
|
14
8
|
console.log('[DEBUG] getFlowsToRunInSequence: flowOrder is empty, returning []');
|
|
@@ -33,7 +27,7 @@ function getFlowsToRunInSequence(paths, flowOrder, debug = false) {
|
|
|
33
27
|
}
|
|
34
28
|
return namesInOrder.map((name) => paths[name]);
|
|
35
29
|
}
|
|
36
|
-
function isFlowFile(filePath) {
|
|
30
|
+
export function isFlowFile(filePath) {
|
|
37
31
|
// Exclude files inside .app bundles
|
|
38
32
|
// Check if any directory in the path ends with .app
|
|
39
33
|
const pathParts = filePath.split(path.sep);
|
|
@@ -44,7 +38,7 @@ function isFlowFile(filePath) {
|
|
|
44
38
|
}
|
|
45
39
|
return filePath.endsWith('.yaml') || filePath.endsWith('.yml');
|
|
46
40
|
}
|
|
47
|
-
const readYamlFileAsJson = (filePath) => {
|
|
41
|
+
export const readYamlFileAsJson = (filePath) => {
|
|
48
42
|
try {
|
|
49
43
|
const normalizedPath = path.normalize(filePath);
|
|
50
44
|
const yamlText = fs.readFileSync(normalizedPath, 'utf8');
|
|
@@ -61,11 +55,12 @@ const readYamlFileAsJson = (filePath) => {
|
|
|
61
55
|
return result;
|
|
62
56
|
}
|
|
63
57
|
catch (error) {
|
|
64
|
-
throw new Error(`Error parsing YAML file ${filePath}: ${error}
|
|
58
|
+
throw new Error(`Error parsing YAML file ${filePath}: ${error}`, {
|
|
59
|
+
cause: error,
|
|
60
|
+
});
|
|
65
61
|
}
|
|
66
62
|
};
|
|
67
|
-
|
|
68
|
-
const readTestYamlFileAsJson = (filePath) => {
|
|
63
|
+
export const readTestYamlFileAsJson = (filePath) => {
|
|
69
64
|
try {
|
|
70
65
|
const normalizedPath = path.normalize(filePath);
|
|
71
66
|
const yamlText = fs.readFileSync(normalizedPath, 'utf8');
|
|
@@ -88,11 +83,10 @@ const readTestYamlFileAsJson = (filePath) => {
|
|
|
88
83
|
catch (error) {
|
|
89
84
|
const message = `Error parsing YAML file ${filePath}: ${error}`;
|
|
90
85
|
console.error(message);
|
|
91
|
-
throw new Error(message);
|
|
86
|
+
throw new Error(message, { cause: error });
|
|
92
87
|
}
|
|
93
88
|
};
|
|
94
|
-
|
|
95
|
-
async function readDirectory(dir, filterFunction) {
|
|
89
|
+
export async function readDirectory(dir, filterFunction) {
|
|
96
90
|
const readDirResult = await fs.promises.readdir(dir);
|
|
97
91
|
const files = await Promise.all(readDirResult.map(async (file) => {
|
|
98
92
|
const filePath = path.join(dir, file);
|
|
@@ -108,7 +102,7 @@ async function readDirectory(dir, filterFunction) {
|
|
|
108
102
|
}));
|
|
109
103
|
return files.flat().filter(Boolean);
|
|
110
104
|
}
|
|
111
|
-
const checkIfFilesExistInWorkspace = (commandName, command, absoluteFilePath) => {
|
|
105
|
+
export const checkIfFilesExistInWorkspace = (commandName, command, absoluteFilePath) => {
|
|
112
106
|
const errors = [];
|
|
113
107
|
const files = [];
|
|
114
108
|
const directory = path.dirname(absoluteFilePath);
|
|
@@ -136,7 +130,6 @@ const checkIfFilesExistInWorkspace = (commandName, command, absoluteFilePath) =>
|
|
|
136
130
|
processFilePath(x.file);
|
|
137
131
|
return { errors, files };
|
|
138
132
|
};
|
|
139
|
-
exports.checkIfFilesExistInWorkspace = checkIfFilesExistInWorkspace;
|
|
140
133
|
const checkFile = (filePath) => {
|
|
141
134
|
if (!fs.existsSync(filePath))
|
|
142
135
|
return `non-existent file`;
|
|
@@ -149,7 +142,7 @@ const checkStepsArray = (steps, absoluteFilePath) => {
|
|
|
149
142
|
continue;
|
|
150
143
|
for (const [commandName, commandValue] of Object.entries(command)) {
|
|
151
144
|
if (commandsThatRequireFiles.has(commandName)) {
|
|
152
|
-
const { errors: newErrors, files: newFiles } =
|
|
145
|
+
const { errors: newErrors, files: newFiles } = checkIfFilesExistInWorkspace(commandName, commandValue, path.normalize(absoluteFilePath));
|
|
153
146
|
errors = [...errors, ...newErrors];
|
|
154
147
|
files = [...files, ...newFiles];
|
|
155
148
|
}
|
|
@@ -164,7 +157,7 @@ const checkStepsArray = (steps, absoluteFilePath) => {
|
|
|
164
157
|
}
|
|
165
158
|
return { errors, files };
|
|
166
159
|
};
|
|
167
|
-
const processDependencies = ({ config, input, testSteps, }) => {
|
|
160
|
+
export const processDependencies = ({ config, input, testSteps, }) => {
|
|
168
161
|
let allErrors = [];
|
|
169
162
|
let allFiles = [];
|
|
170
163
|
const { onFlowComplete, onFlowStart } = config ?? {};
|
|
@@ -186,4 +179,3 @@ const processDependencies = ({ config, input, testSteps, }) => {
|
|
|
186
179
|
}
|
|
187
180
|
return { allErrors, allFiles };
|
|
188
181
|
};
|
|
189
|
-
exports.processDependencies = processDependencies;
|