@devicecloud.dev/dcd 4.1.9-beta.0 → 4.2.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/dist/commands/cloud.js +2 -2
- package/dist/commands/list.d.ts +38 -0
- package/dist/commands/list.js +143 -0
- package/dist/commands/status.d.ts +9 -0
- package/dist/commands/status.js +79 -8
- package/dist/config/flags/output.flags.js +1 -1
- package/dist/gateways/api-gateway.d.ts +27 -0
- package/dist/gateways/api-gateway.js +290 -150
- package/dist/methods.js +146 -37
- package/dist/services/execution-plan.service.js +9 -0
- package/dist/services/execution-plan.utils.js +6 -10
- package/dist/services/test-submission.service.js +5 -0
- package/dist/types/domain/device.types.d.ts +1 -0
- package/dist/types/domain/device.types.js +1 -0
- package/dist/types/generated/schema.types.d.ts +1 -1
- package/dist/types/schema.types.d.ts +154 -101
- package/oclif.manifest.json +97 -2
- package/package.json +1 -2
package/dist/methods.js
CHANGED
|
@@ -115,7 +115,11 @@ const uploadBinary = async (config) => {
|
|
|
115
115
|
return uploadId;
|
|
116
116
|
}
|
|
117
117
|
catch (error) {
|
|
118
|
+
if (log) {
|
|
119
|
+
core_1.ux.action.stop(styling_1.colors.error('✗ Failed'));
|
|
120
|
+
}
|
|
118
121
|
if (debug) {
|
|
122
|
+
console.error('[DEBUG] === BINARY UPLOAD FAILED ===');
|
|
119
123
|
console.error('[DEBUG] Binary upload failed:', error);
|
|
120
124
|
console.error(`[DEBUG] Error type: ${error instanceof Error ? error.name : typeof error}`);
|
|
121
125
|
console.error(`[DEBUG] Error message: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -124,6 +128,14 @@ const uploadBinary = async (config) => {
|
|
|
124
128
|
}
|
|
125
129
|
console.error(`[DEBUG] Failed after ${Date.now() - startTime}ms`);
|
|
126
130
|
}
|
|
131
|
+
// Add helpful context for common errors
|
|
132
|
+
if (error instanceof Error) {
|
|
133
|
+
if (error.name === 'NetworkError') {
|
|
134
|
+
throw error; // NetworkError already has detailed troubleshooting info
|
|
135
|
+
}
|
|
136
|
+
// Re-throw with original message
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
127
139
|
throw error;
|
|
128
140
|
}
|
|
129
141
|
};
|
|
@@ -144,6 +156,31 @@ async function prepareFileForUpload(filePath, debug, startTime) {
|
|
|
144
156
|
if (debug) {
|
|
145
157
|
console.log('[DEBUG] Compressing .app folder to zip...');
|
|
146
158
|
}
|
|
159
|
+
// Validate that the .app directory exists before attempting to compress
|
|
160
|
+
// Without this check, archiver silently creates an empty 22-byte zip for non-existent paths
|
|
161
|
+
try {
|
|
162
|
+
await (0, promises_1.access)(filePath);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Provide helpful error message for common quoting issues
|
|
166
|
+
const hasQuotes = filePath.includes("'") || filePath.includes('"');
|
|
167
|
+
const errorMessage = [
|
|
168
|
+
`App folder not found: ${filePath}`,
|
|
169
|
+
'',
|
|
170
|
+
hasQuotes
|
|
171
|
+
? 'Note: Your path contains quote characters. If the folder name has spaces, ensure quotes wrap the entire path:'
|
|
172
|
+
: 'Note: If your folder name contains spaces, wrap the entire path in quotes:',
|
|
173
|
+
hasQuotes
|
|
174
|
+
? ` ❌ Wrong: --app-file=./path/'My App Name.app' (quotes become literal characters)`
|
|
175
|
+
: ` Example: --app-file="./path/My App Name.app"`,
|
|
176
|
+
hasQuotes
|
|
177
|
+
? ` ✅ Right: --app-file="./path/My App Name.app" (quotes processed by shell)`
|
|
178
|
+
: '',
|
|
179
|
+
]
|
|
180
|
+
.filter(Boolean)
|
|
181
|
+
.join('\n');
|
|
182
|
+
throw new Error(errorMessage);
|
|
183
|
+
}
|
|
147
184
|
const zippedAppBlob = await (0, exports.compressFolderToBlob)(filePath);
|
|
148
185
|
file = new File([zippedAppBlob], filePath + '.zip');
|
|
149
186
|
if (debug) {
|
|
@@ -225,9 +262,18 @@ async function checkExistingUpload(apiUrl, apiKey, sha, debug) {
|
|
|
225
262
|
}
|
|
226
263
|
catch (error) {
|
|
227
264
|
if (debug) {
|
|
228
|
-
console.error('[DEBUG] SHA
|
|
265
|
+
console.error('[DEBUG] === SHA CHECK FAILED ===');
|
|
266
|
+
console.error('[DEBUG] Continuing with upload despite SHA check failure');
|
|
229
267
|
console.error(`[DEBUG] Error type: ${error instanceof Error ? error.name : typeof error}`);
|
|
230
268
|
console.error(`[DEBUG] Error message: ${error instanceof Error ? error.message : String(error)}`);
|
|
269
|
+
if (error instanceof Error && error.stack) {
|
|
270
|
+
console.error(`[DEBUG] Stack trace:\n${error.stack}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
else if (error instanceof Error && error.name === 'NetworkError') {
|
|
274
|
+
// Even without debug, show a warning for network errors
|
|
275
|
+
console.warn('\nWarning: Failed to check for existing binary upload (network error).');
|
|
276
|
+
console.warn('Continuing with new upload...\n');
|
|
231
277
|
}
|
|
232
278
|
return { exists: false };
|
|
233
279
|
}
|
|
@@ -350,21 +396,37 @@ async function requestUploadPaths(apiUrl, apiKey, filePath, fileSize, debug) {
|
|
|
350
396
|
console.log(`[DEBUG] Target endpoint: ${apiUrl}/uploads/getBinaryUploadUrl`);
|
|
351
397
|
console.log(`[DEBUG] Platform: ${platform}`);
|
|
352
398
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
399
|
+
try {
|
|
400
|
+
const urlRequestStartTime = Date.now();
|
|
401
|
+
const { id, tempPath, finalPath, b2 } = await api_gateway_1.ApiGateway.getBinaryUploadUrl(apiUrl, apiKey, platform, fileSize);
|
|
402
|
+
if (debug) {
|
|
403
|
+
const hasStrategy = b2 && typeof b2 === 'object' && 'strategy' in b2;
|
|
404
|
+
console.log(`[DEBUG] Upload URL request completed in ${Date.now() - urlRequestStartTime}ms`);
|
|
405
|
+
console.log(`[DEBUG] Upload ID: ${id}`);
|
|
406
|
+
console.log(`[DEBUG] Temp path (TUS upload): ${tempPath}`);
|
|
407
|
+
console.log(`[DEBUG] Final path (after finalize): ${finalPath}`);
|
|
408
|
+
console.log(`[DEBUG] Backblaze upload URL provided: ${Boolean(b2)}`);
|
|
409
|
+
if (hasStrategy)
|
|
410
|
+
console.log(`[DEBUG] Backblaze strategy: ${b2.strategy}`);
|
|
411
|
+
}
|
|
412
|
+
if (!tempPath)
|
|
413
|
+
throw new Error('No upload path provided by API');
|
|
414
|
+
return { b2, finalPath, id, tempPath };
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
if (debug) {
|
|
418
|
+
console.error('[DEBUG] === FAILED TO GET UPLOAD URL ===');
|
|
419
|
+
console.error(`[DEBUG] Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
420
|
+
}
|
|
421
|
+
// Add context to the error
|
|
422
|
+
if (error instanceof Error) {
|
|
423
|
+
if (error.name === 'NetworkError') {
|
|
424
|
+
throw new Error(`Failed to request upload URL from API.\n\n${error.message}`);
|
|
425
|
+
}
|
|
426
|
+
throw new Error(`Failed to request upload URL: ${error.message}`);
|
|
427
|
+
}
|
|
428
|
+
throw error;
|
|
429
|
+
}
|
|
368
430
|
}
|
|
369
431
|
/**
|
|
370
432
|
* Extracts metadata from the binary file
|
|
@@ -448,8 +510,8 @@ async function performUpload(config) {
|
|
|
448
510
|
console.log(`[DEBUG] Upload summary - Supabase: ${supabaseResult.success ? '✓' : '✗'}, Backblaze: ${backblazeResult.success ? '✓' : '✗'}`);
|
|
449
511
|
console.log('[DEBUG] Finalizing upload...');
|
|
450
512
|
console.log(`[DEBUG] Target endpoint: ${apiUrl}/uploads/finaliseUpload`);
|
|
451
|
-
console.log(`[DEBUG]
|
|
452
|
-
console.log(`[DEBUG]
|
|
513
|
+
console.log(`[DEBUG] Uploaded to staging path: ${tempPath}`);
|
|
514
|
+
console.log(`[DEBUG] API will move to final path: ${finalPath}`);
|
|
453
515
|
console.log(`[DEBUG] Supabase upload status: ${supabaseResult.success ? 'SUCCESS' : 'FAILED'}`);
|
|
454
516
|
console.log(`[DEBUG] Backblaze upload status: ${backblazeResult.success ? 'SUCCESS' : 'FAILED'}`);
|
|
455
517
|
if (hasWarning)
|
|
@@ -529,10 +591,25 @@ async function uploadToBackblaze(uploadUrl, authorizationToken, fileName, file,
|
|
|
529
591
|
}
|
|
530
592
|
catch (error) {
|
|
531
593
|
if (debug) {
|
|
594
|
+
console.error('[DEBUG] === BACKBLAZE UPLOAD EXCEPTION ===');
|
|
532
595
|
console.error('[DEBUG] Backblaze upload exception:', error);
|
|
596
|
+
console.error(`[DEBUG] Error type: ${error instanceof Error ? error.name : typeof error}`);
|
|
597
|
+
console.error(`[DEBUG] Error message: ${error instanceof Error ? error.message : String(error)}`);
|
|
598
|
+
if (error instanceof Error && error.stack) {
|
|
599
|
+
console.error(`[DEBUG] Stack trace:\n${error.stack}`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
// Provide more specific error messages for common network errors
|
|
603
|
+
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
604
|
+
if (debug) {
|
|
605
|
+
console.error('[DEBUG] Network error detected - could be DNS, connection, or SSL issue');
|
|
606
|
+
}
|
|
607
|
+
console.warn('Warning: Backblaze upload failed due to network error');
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
// Don't throw - we don't want Backblaze failures to block the primary upload
|
|
611
|
+
console.warn(`Warning: Backblaze upload failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
533
612
|
}
|
|
534
|
-
// Don't throw - we don't want Backblaze failures to block the primary upload
|
|
535
|
-
console.warn(`Warning: Backblaze upload failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
536
613
|
return false;
|
|
537
614
|
}
|
|
538
615
|
}
|
|
@@ -608,30 +685,50 @@ async function uploadLargeFileToBackblaze(config) {
|
|
|
608
685
|
if (debug) {
|
|
609
686
|
console.log(`[DEBUG] Uploading part ${partNumber}/${uploadPartUrls.length} (${(partLength / 1024 / 1024).toFixed(2)} MB, SHA1: ${sha1Hex})`);
|
|
610
687
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
688
|
+
try {
|
|
689
|
+
const response = await fetch(uploadPartUrls[i].uploadUrl, {
|
|
690
|
+
body: new Uint8Array(partBuffer),
|
|
691
|
+
headers: {
|
|
692
|
+
Authorization: uploadPartUrls[i].authorizationToken,
|
|
693
|
+
'Content-Length': partLength.toString(),
|
|
694
|
+
'X-Bz-Content-Sha1': sha1Hex,
|
|
695
|
+
'X-Bz-Part-Number': partNumber.toString(),
|
|
696
|
+
},
|
|
697
|
+
method: 'POST',
|
|
698
|
+
});
|
|
699
|
+
if (!response.ok) {
|
|
700
|
+
const errorText = await response.text();
|
|
701
|
+
if (debug) {
|
|
702
|
+
console.error(`[DEBUG] Part ${partNumber} upload failed with status ${response.status}: ${errorText}`);
|
|
703
|
+
}
|
|
704
|
+
throw new Error(`Part ${partNumber} upload failed with status ${response.status}`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
catch (error) {
|
|
708
|
+
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
709
|
+
if (debug) {
|
|
710
|
+
console.error(`[DEBUG] Network error uploading part ${partNumber} - could be DNS, connection, or SSL issue`);
|
|
711
|
+
}
|
|
712
|
+
throw new Error(`Part ${partNumber} upload failed due to network error`);
|
|
625
713
|
}
|
|
626
|
-
throw
|
|
714
|
+
throw error;
|
|
627
715
|
}
|
|
628
716
|
if (debug) {
|
|
629
717
|
console.log(`[DEBUG] Part ${partNumber}/${uploadPartUrls.length} uploaded successfully`);
|
|
630
718
|
}
|
|
631
719
|
}
|
|
720
|
+
// Validate all parts were uploaded
|
|
721
|
+
if (partSha1Array.length !== uploadPartUrls.length) {
|
|
722
|
+
const errorMsg = `Part count mismatch: uploaded ${partSha1Array.length} parts but expected ${uploadPartUrls.length}`;
|
|
723
|
+
if (debug) {
|
|
724
|
+
console.error(`[DEBUG] ${errorMsg}`);
|
|
725
|
+
}
|
|
726
|
+
throw new Error(errorMsg);
|
|
727
|
+
}
|
|
632
728
|
// Finish the large file upload
|
|
633
729
|
if (debug) {
|
|
634
730
|
console.log('[DEBUG] Finishing large file upload...');
|
|
731
|
+
console.log(`[DEBUG] Finalizing ${partSha1Array.length} parts with fileId: ${fileId}`);
|
|
635
732
|
}
|
|
636
733
|
await api_gateway_1.ApiGateway.finishLargeFile(apiUrl, apiKey, fileId, partSha1Array);
|
|
637
734
|
if (debug) {
|
|
@@ -641,10 +738,22 @@ async function uploadLargeFileToBackblaze(config) {
|
|
|
641
738
|
}
|
|
642
739
|
catch (error) {
|
|
643
740
|
if (debug) {
|
|
741
|
+
console.error('[DEBUG] === BACKBLAZE LARGE FILE UPLOAD EXCEPTION ===');
|
|
644
742
|
console.error('[DEBUG] Large file upload exception:', error);
|
|
743
|
+
console.error(`[DEBUG] Error type: ${error instanceof Error ? error.name : typeof error}`);
|
|
744
|
+
console.error(`[DEBUG] Error message: ${error instanceof Error ? error.message : String(error)}`);
|
|
745
|
+
if (error instanceof Error && error.stack) {
|
|
746
|
+
console.error(`[DEBUG] Stack trace:\n${error.stack}`);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
// Provide more specific error messages
|
|
750
|
+
if (error instanceof Error && error.message.includes('network error')) {
|
|
751
|
+
console.warn('Warning: Backblaze large file upload failed due to network error');
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
// Don't throw - we don't want Backblaze failures to block the primary upload
|
|
755
|
+
console.warn(`Warning: Backblaze large file upload failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
645
756
|
}
|
|
646
|
-
// Don't throw - we don't want Backblaze failures to block the primary upload
|
|
647
|
-
console.warn(`Warning: Backblaze large file upload failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
648
757
|
return false;
|
|
649
758
|
}
|
|
650
759
|
}
|
|
@@ -255,6 +255,15 @@ async function plan(options) {
|
|
|
255
255
|
if (debug) {
|
|
256
256
|
console.log(`[DEBUG] Sequential flows resolved: ${flowsToRunInSequence.length} flow(s)`);
|
|
257
257
|
}
|
|
258
|
+
// Validate that all specified flows were found
|
|
259
|
+
if (workspaceConfig.executionOrder?.flowsOrder &&
|
|
260
|
+
workspaceConfig.executionOrder.flowsOrder.length > 0 &&
|
|
261
|
+
flowsToRunInSequence.length === 0) {
|
|
262
|
+
throw new Error(`executionOrder specified ${workspaceConfig.executionOrder.flowsOrder.length} flow(s) but none were found.\n\n` +
|
|
263
|
+
`Expected flows: ${workspaceConfig.executionOrder.flowsOrder.join(', ')}\n` +
|
|
264
|
+
`Available flow names: ${Object.keys(pathsByName).join(', ')}\n\n` +
|
|
265
|
+
`Hint: Flow names come from either the 'name' field in the flow config or the filename without extension.`);
|
|
266
|
+
}
|
|
258
267
|
const normalFlows = allFlows
|
|
259
268
|
.filter((flow) => !flowsToRunInSequence.includes(flow))
|
|
260
269
|
.sort((a, b) => a.localeCompare(b));
|
|
@@ -27,10 +27,8 @@ function getFlowsToRunInSequence(paths, flowOrder, debug = false) {
|
|
|
27
27
|
}
|
|
28
28
|
if (namesInOrder.length === 0) {
|
|
29
29
|
const notFound = [...orderSet].filter((item) => !availableNames.includes(item));
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
return [];
|
|
30
|
+
throw new Error(`Could not find flows specified in executionOrder.flowsOrder: ${notFound.join(', ')}\n\n` +
|
|
31
|
+
`Available flow names:\n${availableNames.join('\n')}`);
|
|
34
32
|
}
|
|
35
33
|
const result = [...orderSet].filter((item) => namesInOrder.includes(item));
|
|
36
34
|
if (result.length === 0) {
|
|
@@ -47,12 +45,10 @@ function getFlowsToRunInSequence(paths, flowOrder, debug = false) {
|
|
|
47
45
|
return resolvedPaths;
|
|
48
46
|
}
|
|
49
47
|
else {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
return [];
|
|
48
|
+
throw new Error(`Flow order mismatch in executionOrder.flowsOrder.\n\n` +
|
|
49
|
+
`Expected order: [${flowOrder.slice(0, result.length).join(', ')}]\n` +
|
|
50
|
+
`Actual order: [${result.join(', ')}]\n\n` +
|
|
51
|
+
`Please ensure flows are specified in the correct order.`);
|
|
56
52
|
}
|
|
57
53
|
}
|
|
58
54
|
function isFlowFile(filePath) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TestSubmissionService = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
4
5
|
const path = require("node:path");
|
|
5
6
|
const methods_1 = require("../methods");
|
|
6
7
|
const mimeTypeLookupByExtension = {
|
|
@@ -50,10 +51,14 @@ class TestSubmissionService {
|
|
|
50
51
|
]),
|
|
51
52
|
], commonRoot);
|
|
52
53
|
this.logDebug(debug, logger, `[DEBUG] Compressed file size: ${buffer.length} bytes`);
|
|
54
|
+
// Calculate SHA-256 hash of the flow ZIP
|
|
55
|
+
const sha = (0, node_crypto_1.createHash)('sha256').update(buffer).digest('hex');
|
|
56
|
+
this.logDebug(debug, logger, `[DEBUG] Flow ZIP SHA-256: ${sha}`);
|
|
53
57
|
const blob = new Blob([buffer], {
|
|
54
58
|
type: mimeTypeLookupByExtension.zip,
|
|
55
59
|
});
|
|
56
60
|
testFormData.set('file', blob, 'flowFile.zip');
|
|
61
|
+
testFormData.set('sha', sha);
|
|
57
62
|
testFormData.set('appBinaryId', appBinaryId);
|
|
58
63
|
testFormData.set('testFileNames', JSON.stringify(this.normalizePaths(testFileNames, commonRoot)));
|
|
59
64
|
testFormData.set('flowMetadata', JSON.stringify(this.normalizePathMap(flowMetadata, commonRoot)));
|
|
@@ -38,6 +38,7 @@ var EAndroidApiLevels;
|
|
|
38
38
|
EAndroidApiLevels["thirtyFive"] = "35";
|
|
39
39
|
EAndroidApiLevels["thirtyFour"] = "34";
|
|
40
40
|
EAndroidApiLevels["thirtyOne"] = "31";
|
|
41
|
+
EAndroidApiLevels["thirtySix"] = "36";
|
|
41
42
|
EAndroidApiLevels["thirtyThree"] = "33";
|
|
42
43
|
EAndroidApiLevels["thirtyTwo"] = "32";
|
|
43
44
|
EAndroidApiLevels["twentyNine"] = "29";
|
|
@@ -538,7 +538,7 @@ export interface components {
|
|
|
538
538
|
*/
|
|
539
539
|
file: string;
|
|
540
540
|
/** @enum {string} */
|
|
541
|
-
androidApiLevel?: "29" | "30" | "31" | "32" | "33" | "34" | "35";
|
|
541
|
+
androidApiLevel?: "29" | "30" | "31" | "32" | "33" | "34" | "35" | "36";
|
|
542
542
|
/** @enum {string} */
|
|
543
543
|
androidDevice?: "pixel-6" | "pixel-6-pro" | "pixel-7" | "pixel-7-pro" | "generic-tablet";
|
|
544
544
|
apiKey?: string;
|