@devicecloud.dev/dcd 3.7.0 → 3.7.3
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 +30 -17
- package/dist/commands/status.js +3 -9
- package/dist/gateways/ApiGateway.d.ts +35 -0
- package/dist/gateways/ApiGateway.js +123 -0
- package/dist/gateways/SupabaseGateway.d.ts +11 -0
- package/dist/gateways/SupabaseGateway.js +29 -0
- package/dist/methods.d.ts +6 -25
- package/dist/methods.js +22 -117
- package/oclif.manifest.json +1 -1
- package/package.json +2 -2
package/dist/commands/cloud.js
CHANGED
|
@@ -12,6 +12,7 @@ const StreamZip = require("node-stream-zip");
|
|
|
12
12
|
const constants_1 = require("../constants");
|
|
13
13
|
const methods_1 = require("../methods");
|
|
14
14
|
const plan_1 = require("../plan");
|
|
15
|
+
const ApiGateway_1 = require("../gateways/ApiGateway");
|
|
15
16
|
exports.mimeTypeLookupByExtension = {
|
|
16
17
|
apk: 'application/vnd.android.package-archive',
|
|
17
18
|
yaml: 'application/x-yaml',
|
|
@@ -124,8 +125,11 @@ class Cloud extends core_1.Command {
|
|
|
124
125
|
};
|
|
125
126
|
}
|
|
126
127
|
const [major] = process.versions.node.split('.').map(Number);
|
|
127
|
-
if (major <
|
|
128
|
-
|
|
128
|
+
if (major < 20) {
|
|
129
|
+
this.warn(`WARNING: You are using node version ${major}. DeviceCloud requires node version 20 or later`);
|
|
130
|
+
if (major < 18) {
|
|
131
|
+
throw new Error('Invalid node version');
|
|
132
|
+
}
|
|
129
133
|
}
|
|
130
134
|
await this.versionCheck();
|
|
131
135
|
// Download and expand Moropo zip if API key is present
|
|
@@ -518,11 +522,7 @@ class Cloud extends core_1.Command {
|
|
|
518
522
|
if (debug) {
|
|
519
523
|
this.log(`DEBUG: Submitting flow upload request to ${apiUrl}/uploads/flow`);
|
|
520
524
|
}
|
|
521
|
-
const
|
|
522
|
-
body: testFormData,
|
|
523
|
-
headers: { 'x-app-api-key': apiKey },
|
|
524
|
-
};
|
|
525
|
-
const { message, results } = await (0, methods_1.typeSafePost)(apiUrl, '/uploads/flow', options);
|
|
525
|
+
const { message, results } = await ApiGateway_1.ApiGateway.uploadFlow(apiUrl, apiKey, testFormData);
|
|
526
526
|
if (debug) {
|
|
527
527
|
this.log(`DEBUG: Flow upload response received`);
|
|
528
528
|
this.log(`DEBUG: Message: ${message}`);
|
|
@@ -581,9 +581,7 @@ class Cloud extends core_1.Command {
|
|
|
581
581
|
if (debug) {
|
|
582
582
|
this.log(`DEBUG: Polling for results: ${results[0].test_upload_id}`);
|
|
583
583
|
}
|
|
584
|
-
const { results: updatedResults } = await
|
|
585
|
-
headers: { 'x-app-api-key': apiKey },
|
|
586
|
-
});
|
|
584
|
+
const { results: updatedResults } = await ApiGateway_1.ApiGateway.getResultsForUpload(apiUrl, apiKey, results[0].test_upload_id);
|
|
587
585
|
if (!updatedResults) {
|
|
588
586
|
throw new Error('no results');
|
|
589
587
|
}
|
|
@@ -607,11 +605,24 @@ class Cloud extends core_1.Command {
|
|
|
607
605
|
if (!json) {
|
|
608
606
|
core_1.ux.action.stop('completed');
|
|
609
607
|
this.log('\n');
|
|
608
|
+
const hasFailedTests = updatedResults.some((result) => result.status === 'FAILED');
|
|
610
609
|
(0, cli_ux_1.table)(updatedResults, {
|
|
611
610
|
status: { get: (row) => row.status },
|
|
612
611
|
test: {
|
|
613
612
|
get: (row) => `${row.test_file_name} ${row.retry_of ? '(retry)' : ''}`,
|
|
614
613
|
},
|
|
614
|
+
duration: {
|
|
615
|
+
get: (row) => row.duration_seconds
|
|
616
|
+
? (0, methods_1.formatDurationSeconds)(row.duration_seconds)
|
|
617
|
+
: '',
|
|
618
|
+
},
|
|
619
|
+
...(hasFailedTests && {
|
|
620
|
+
fail_reason: {
|
|
621
|
+
get: (row) => row.status === 'FAILED' && row.fail_reason
|
|
622
|
+
? row.fail_reason
|
|
623
|
+
: '',
|
|
624
|
+
},
|
|
625
|
+
}),
|
|
615
626
|
}, { printLine: this.log.bind(this) });
|
|
616
627
|
this.log('\n');
|
|
617
628
|
this.log('Run completed, you can access the results at:');
|
|
@@ -624,13 +635,7 @@ class Cloud extends core_1.Command {
|
|
|
624
635
|
if (debug) {
|
|
625
636
|
this.log(`DEBUG: Downloading artifacts: ${downloadArtifacts}`);
|
|
626
637
|
}
|
|
627
|
-
await
|
|
628
|
-
body: JSON.stringify({ results: downloadArtifacts }),
|
|
629
|
-
headers: {
|
|
630
|
-
'content-type': 'application/json',
|
|
631
|
-
'x-app-api-key': apiKey,
|
|
632
|
-
},
|
|
633
|
-
}, artifactsPath);
|
|
638
|
+
await ApiGateway_1.ApiGateway.downloadArtifactsZip(apiUrl, apiKey, results[0].test_upload_id, downloadArtifacts, artifactsPath);
|
|
634
639
|
this.log('\n');
|
|
635
640
|
this.log(`Test artifacts have been downloaded to ${artifactsPath || './artifacts.zip'}`);
|
|
636
641
|
}
|
|
@@ -657,6 +662,10 @@ class Cloud extends core_1.Command {
|
|
|
657
662
|
tests: resultsWithoutEarlierTries.map((r) => ({
|
|
658
663
|
name: r.test_file_name,
|
|
659
664
|
status: r.status,
|
|
665
|
+
duration_seconds: r.duration_seconds,
|
|
666
|
+
fail_reason: r.status === 'FAILED'
|
|
667
|
+
? r.fail_reason || 'No reason provided'
|
|
668
|
+
: undefined,
|
|
660
669
|
})),
|
|
661
670
|
};
|
|
662
671
|
if (flags['json-file']) {
|
|
@@ -679,6 +688,10 @@ class Cloud extends core_1.Command {
|
|
|
679
688
|
tests: resultsWithoutEarlierTries.map((r) => ({
|
|
680
689
|
name: r.test_file_name,
|
|
681
690
|
status: r.status,
|
|
691
|
+
duration_seconds: r.duration_seconds,
|
|
692
|
+
fail_reason: r.status === 'FAILED'
|
|
693
|
+
? r.fail_reason || 'No reason provided'
|
|
694
|
+
: undefined,
|
|
682
695
|
})),
|
|
683
696
|
};
|
|
684
697
|
if (flags['json-file']) {
|
package/dist/commands/status.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const core_1 = require("@oclif/core");
|
|
4
4
|
const constants_1 = require("../constants");
|
|
5
|
+
const ApiGateway_1 = require("../gateways/ApiGateway");
|
|
5
6
|
const methods_1 = require("../methods");
|
|
6
7
|
class Status extends core_1.Command {
|
|
7
8
|
static description = 'Get the status of an upload by name or upload ID';
|
|
@@ -52,7 +53,7 @@ class Status extends core_1.Command {
|
|
|
52
53
|
return;
|
|
53
54
|
}
|
|
54
55
|
try {
|
|
55
|
-
const status = (await
|
|
56
|
+
const status = (await ApiGateway_1.ApiGateway.getUploadStatus(apiUrl, apiKey, {
|
|
56
57
|
name,
|
|
57
58
|
uploadId,
|
|
58
59
|
}));
|
|
@@ -85,14 +86,7 @@ class Status extends core_1.Command {
|
|
|
85
86
|
this.log(` Fail reason: ${item.failReason}`);
|
|
86
87
|
}
|
|
87
88
|
if (item.durationSeconds) {
|
|
88
|
-
|
|
89
|
-
const seconds = item.durationSeconds % 60;
|
|
90
|
-
if (minutes > 0) {
|
|
91
|
-
this.log(` Duration: ${minutes}m ${seconds}s`);
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
this.log(` Duration: ${item.durationSeconds}s`);
|
|
95
|
-
}
|
|
89
|
+
this.log(` Duration: ${(0, methods_1.formatDurationSeconds)(item.durationSeconds)}`);
|
|
96
90
|
}
|
|
97
91
|
this.log('');
|
|
98
92
|
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { TAppMetadata } from '../types';
|
|
2
|
+
export declare class ApiGateway {
|
|
3
|
+
static getResultsForUpload: (baseUrl: string, apiKey: string, uploadId: string) => Promise<{
|
|
4
|
+
statusCode?: number;
|
|
5
|
+
results?: import("../../../api/schema.types").components["schemas"]["TResultResponse"][];
|
|
6
|
+
}>;
|
|
7
|
+
static getUploadStatus: (baseUrl: string, apiKey: string, options: {
|
|
8
|
+
uploadId?: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
}) => Promise<{
|
|
11
|
+
status: "PASSED" | "FAILED" | "CANCELLED" | "PENDING";
|
|
12
|
+
tests: Array<{
|
|
13
|
+
name: string;
|
|
14
|
+
status: "PASSED" | "FAILED" | "CANCELLED" | "PENDING";
|
|
15
|
+
durationSeconds?: number;
|
|
16
|
+
failReason?: string;
|
|
17
|
+
}>;
|
|
18
|
+
}>;
|
|
19
|
+
static downloadArtifactsZip: (baseUrl: string, apiKey: string, uploadId: string, results: "ALL" | "FAILED", artifactsPath?: string) => Promise<void>;
|
|
20
|
+
static uploadFlow: (baseUrl: string, apiKey: string, testFormData: FormData) => Promise<{
|
|
21
|
+
message?: string;
|
|
22
|
+
results?: import("../../../api/schema.types").components["schemas"]["IDBResult"][];
|
|
23
|
+
}>;
|
|
24
|
+
static finaliseUpload: (baseUrl: string, apiKey: string, id: string, metadata: TAppMetadata, path: string, sha: string) => Promise<Record<string, never>>;
|
|
25
|
+
static getBinaryUploadUrl: (baseUrl: string, apiKey: string, platform: "android" | "ios") => Promise<{
|
|
26
|
+
message: string;
|
|
27
|
+
path: string;
|
|
28
|
+
token: string;
|
|
29
|
+
id: string;
|
|
30
|
+
}>;
|
|
31
|
+
static checkForExistingUpload: (baseUrl: string, apiKey: string, sha: string) => Promise<{
|
|
32
|
+
appBinaryId: string;
|
|
33
|
+
exists: boolean;
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ApiGateway = void 0;
|
|
4
|
+
class ApiGateway {
|
|
5
|
+
static getResultsForUpload = async (baseUrl, apiKey, uploadId) => {
|
|
6
|
+
// todo: merge with getUploadStatus
|
|
7
|
+
const res = await fetch(`${baseUrl}/results/${uploadId}`, {
|
|
8
|
+
headers: { 'x-app-api-key': apiKey },
|
|
9
|
+
});
|
|
10
|
+
if (!res.ok) {
|
|
11
|
+
throw new Error(await res.text());
|
|
12
|
+
}
|
|
13
|
+
return res.json();
|
|
14
|
+
};
|
|
15
|
+
static getUploadStatus = async (baseUrl, apiKey, options) => {
|
|
16
|
+
const queryParams = new URLSearchParams();
|
|
17
|
+
if (options.uploadId) {
|
|
18
|
+
queryParams.append('uploadId', options.uploadId);
|
|
19
|
+
}
|
|
20
|
+
if (options.name) {
|
|
21
|
+
queryParams.append('name', options.name);
|
|
22
|
+
}
|
|
23
|
+
const response = await fetch(`${baseUrl}/uploads/status?${queryParams}`, {
|
|
24
|
+
headers: {
|
|
25
|
+
'x-app-api-key': apiKey,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
throw new Error(`Failed to fetch status: ${response.statusText}`);
|
|
30
|
+
}
|
|
31
|
+
return response.json();
|
|
32
|
+
};
|
|
33
|
+
static downloadArtifactsZip = async (baseUrl, apiKey, uploadId, results, artifactsPath = './artifacts.zip') => {
|
|
34
|
+
const res = await fetch(`${baseUrl}/results/${uploadId}/download`, {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: {
|
|
37
|
+
'x-app-api-key': apiKey,
|
|
38
|
+
'content-type': 'application/json',
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify({ results }),
|
|
41
|
+
});
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
throw new Error(await res.text());
|
|
44
|
+
}
|
|
45
|
+
// Handle tilde expansion for home directory
|
|
46
|
+
if (artifactsPath.startsWith('~/') || artifactsPath === '~') {
|
|
47
|
+
artifactsPath = artifactsPath.replace(/^~(?=$|\/|\\)/, require('os').homedir());
|
|
48
|
+
}
|
|
49
|
+
// Create directory structure if it doesn't exist
|
|
50
|
+
const { dirname } = require('node:path');
|
|
51
|
+
const { mkdirSync, createWriteStream } = require('node:fs');
|
|
52
|
+
const { finished } = require('node:stream/promises');
|
|
53
|
+
const { Readable } = require('node:stream');
|
|
54
|
+
const directory = dirname(artifactsPath);
|
|
55
|
+
if (directory !== '.') {
|
|
56
|
+
try {
|
|
57
|
+
mkdirSync(directory, { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
// Ignore if directory already exists
|
|
61
|
+
if (error.code !== 'EEXIST') {
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const fileStream = createWriteStream(artifactsPath, { flags: 'wx' });
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
68
|
+
await finished(Readable.fromWeb(res.body).pipe(fileStream));
|
|
69
|
+
};
|
|
70
|
+
static uploadFlow = async (baseUrl, apiKey, testFormData) => {
|
|
71
|
+
const res = await fetch(`${baseUrl}/uploads/flow`, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
body: testFormData,
|
|
74
|
+
headers: {
|
|
75
|
+
'x-app-api-key': apiKey,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
if (!res.ok) {
|
|
79
|
+
throw new Error(await res.text());
|
|
80
|
+
}
|
|
81
|
+
return res.json();
|
|
82
|
+
};
|
|
83
|
+
static finaliseUpload = async (baseUrl, apiKey, id, metadata, path, sha) => {
|
|
84
|
+
const res = await fetch(`${baseUrl}/uploads/finaliseUpload`, {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
body: JSON.stringify({ id, metadata, path, sha }),
|
|
87
|
+
headers: {
|
|
88
|
+
'content-type': 'application/json',
|
|
89
|
+
'x-app-api-key': apiKey,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
if (!res.ok) {
|
|
93
|
+
throw new Error(await res.text());
|
|
94
|
+
}
|
|
95
|
+
return res.json();
|
|
96
|
+
};
|
|
97
|
+
static getBinaryUploadUrl = async (baseUrl, apiKey, platform) => {
|
|
98
|
+
const res = await fetch(`${baseUrl}/uploads/getBinaryUploadUrl`, {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
body: JSON.stringify({ platform }),
|
|
101
|
+
headers: {
|
|
102
|
+
'content-type': 'application/json',
|
|
103
|
+
'x-app-api-key': apiKey,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
if (!res.ok) {
|
|
107
|
+
throw new Error(await res.text());
|
|
108
|
+
}
|
|
109
|
+
return res.json();
|
|
110
|
+
};
|
|
111
|
+
static checkForExistingUpload = async (baseUrl, apiKey, sha) => {
|
|
112
|
+
const res = await fetch(`${baseUrl}/uploads/checkForExistingUpload`, {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
body: JSON.stringify({ sha }),
|
|
115
|
+
headers: {
|
|
116
|
+
'content-type': 'application/json',
|
|
117
|
+
'x-app-api-key': apiKey,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
return res.json();
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
exports.ApiGateway = ApiGateway;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare class SupabaseGateway {
|
|
2
|
+
private static SB;
|
|
3
|
+
static getSupabaseKeys(env: 'prod' | 'dev'): {
|
|
4
|
+
SUPABASE_PUBLIC_KEY: string;
|
|
5
|
+
SUPABASE_URL: string;
|
|
6
|
+
} | {
|
|
7
|
+
SUPABASE_PUBLIC_KEY: string;
|
|
8
|
+
SUPABASE_URL: string;
|
|
9
|
+
};
|
|
10
|
+
static uploadToSignedUrl(env: 'prod' | 'dev', path: string, token: string, file: File): Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SupabaseGateway = void 0;
|
|
4
|
+
const supabase_js_1 = require("@supabase/supabase-js");
|
|
5
|
+
class SupabaseGateway {
|
|
6
|
+
static SB = {
|
|
7
|
+
dev: {
|
|
8
|
+
SUPABASE_PUBLIC_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxibXNvd2VodGp3bnFsdXJwZW1iIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDkyMTg0ODcsImV4cCI6MjAyNDc5NDQ4N30.zeLTMAuZ_WwYvGdeP0kdvL_Zrs-RQee5APPyxmWq7qQ',
|
|
9
|
+
SUPABASE_URL: 'https://lbmsowehtjwnqlurpemb.supabase.co',
|
|
10
|
+
},
|
|
11
|
+
prod: {
|
|
12
|
+
SUPABASE_PUBLIC_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBneWRucGhiaW1ldGluc2dma2JvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDc1OTQzNDYsImV4cCI6MjAyMzE3MDM0Nn0.hAYOMFxxwX1exkQkY9xyQJGC_GhGnyogkj2N-kBkMI8',
|
|
13
|
+
SUPABASE_URL: 'https://pgydnphbimetinsgfkbo.supabase.co',
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
static getSupabaseKeys(env) {
|
|
17
|
+
return this.SB[env];
|
|
18
|
+
}
|
|
19
|
+
static async uploadToSignedUrl(env, path, token, file) {
|
|
20
|
+
const { SUPABASE_URL, SUPABASE_PUBLIC_KEY } = this.getSupabaseKeys(env);
|
|
21
|
+
const supabase = (0, supabase_js_1.createClient)(SUPABASE_URL, SUPABASE_PUBLIC_KEY);
|
|
22
|
+
const uploadToUrl = await supabase.storage
|
|
23
|
+
.from('organizations')
|
|
24
|
+
.uploadToSignedUrl(path, token, file);
|
|
25
|
+
if (uploadToUrl.error)
|
|
26
|
+
throw new Error(uploadToUrl.error);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.SupabaseGateway = SupabaseGateway;
|
package/dist/methods.d.ts
CHANGED
|
@@ -1,18 +1,5 @@
|
|
|
1
1
|
import * as archiver from 'archiver';
|
|
2
|
-
import { paths } from '../../api/schema.types';
|
|
3
2
|
import { TAppMetadata } from './types';
|
|
4
|
-
export declare const typeSafePost: <T extends keyof paths>(baseUrl: string, path: string, init?: {
|
|
5
|
-
body?: BodyInit;
|
|
6
|
-
headers?: HeadersInit;
|
|
7
|
-
}) => Promise<paths[T]["post"]["responses"]["201"]["content"]["application/json"]>;
|
|
8
|
-
export declare const typeSafePostDownload: (baseUrl: string, path: string, init?: {
|
|
9
|
-
body?: BodyInit;
|
|
10
|
-
headers?: HeadersInit;
|
|
11
|
-
}, artifactsPath?: string) => Promise<void>;
|
|
12
|
-
export declare const typeSafeGet: <T extends keyof paths>(baseUrl: string, path: string, init?: {
|
|
13
|
-
body?: FormData;
|
|
14
|
-
headers?: HeadersInit;
|
|
15
|
-
}) => Promise<paths[T]["get"]["responses"]["200"]["content"]["application/json"]>;
|
|
16
3
|
export declare const toBuffer: (archive: archiver.Archiver) => Promise<Buffer<ArrayBuffer>>;
|
|
17
4
|
export declare const compressFolderToBlob: (sourceDir: string) => Promise<Blob>;
|
|
18
5
|
export declare const compressFilesFromRelativePath: (path: string, files: string[], commonRoot: string) => Promise<Buffer<ArrayBuffer>>;
|
|
@@ -23,18 +10,6 @@ export declare const extractAppMetadataIos: (appFolderPath: string) => Promise<T
|
|
|
23
10
|
export declare const uploadBinary: (filePath: string, apiUrl: string, apiKey: string, ignoreShaCheck?: boolean, log?: boolean) => Promise<string>;
|
|
24
11
|
export declare const uploadBinaries: (finalAppFiles: string[], apiUrl: string, apiKey: string, ignoreShaCheck?: boolean, log?: boolean) => Promise<string[]>;
|
|
25
12
|
export declare const verifyAdditionalAppFiles: (appFiles: string[] | undefined) => Promise<void>;
|
|
26
|
-
export declare const getUploadStatus: (apiUrl: string, apiKey: string, options: {
|
|
27
|
-
uploadId?: string;
|
|
28
|
-
name?: string;
|
|
29
|
-
}) => Promise<{
|
|
30
|
-
status: "PASSED" | "FAILED" | "CANCELLED" | "PENDING";
|
|
31
|
-
tests: Array<{
|
|
32
|
-
name: string;
|
|
33
|
-
status: "PASSED" | "FAILED" | "CANCELLED" | "PENDING";
|
|
34
|
-
durationSeconds?: number;
|
|
35
|
-
failReason?: string;
|
|
36
|
-
}>;
|
|
37
|
-
}>;
|
|
38
13
|
/**
|
|
39
14
|
* Writes JSON data to a file with error handling
|
|
40
15
|
* @param filePath - Path to the output JSON file
|
|
@@ -46,3 +21,9 @@ export declare const writeJSONFile: (filePath: string, data: any, logger: {
|
|
|
46
21
|
log: (message: string) => void;
|
|
47
22
|
warn: (message: string) => void;
|
|
48
23
|
}) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Formats duration in seconds into a human readable string
|
|
26
|
+
* @param durationSeconds - Duration in seconds
|
|
27
|
+
* @returns Formatted duration string (e.g. "2m 30s" or "45s")
|
|
28
|
+
*/
|
|
29
|
+
export declare const formatDurationSeconds: (durationSeconds: number) => string;
|
package/dist/methods.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.formatDurationSeconds = exports.writeJSONFile = exports.verifyAdditionalAppFiles = exports.uploadBinaries = exports.uploadBinary = exports.extractAppMetadataIos = exports.extractAppMetadataIosZip = exports.extractAppMetadataAndroid = exports.verifyAppZip = exports.compressFilesFromRelativePath = exports.compressFolderToBlob = exports.toBuffer = void 0;
|
|
4
4
|
const core_1 = require("@oclif/core");
|
|
5
|
-
const supabase_js_1 = require("@supabase/supabase-js");
|
|
6
5
|
// required polyfill for node 18
|
|
7
6
|
const file_1 = require("@web-std/file");
|
|
8
7
|
// @ts-ignore
|
|
@@ -13,64 +12,13 @@ const node_fs_1 = require("node:fs");
|
|
|
13
12
|
const promises_1 = require("node:fs/promises");
|
|
14
13
|
const nodePath = require("node:path");
|
|
15
14
|
const node_stream_1 = require("node:stream");
|
|
16
|
-
const promises_2 = require("node:stream/promises");
|
|
17
15
|
const StreamZip = require("node-stream-zip");
|
|
18
16
|
const plist_1 = require("plist");
|
|
19
17
|
const node_crypto_1 = require("node:crypto");
|
|
20
18
|
const path = require("path");
|
|
21
|
-
const node_path_1 = require("node:path");
|
|
22
|
-
const os = require("node:os");
|
|
23
19
|
const cloud_1 = require("./commands/cloud");
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
...init,
|
|
27
|
-
method: 'POST',
|
|
28
|
-
});
|
|
29
|
-
if (!res.ok) {
|
|
30
|
-
throw new Error(await res.text());
|
|
31
|
-
}
|
|
32
|
-
return res.json();
|
|
33
|
-
};
|
|
34
|
-
exports.typeSafePost = typeSafePost;
|
|
35
|
-
const typeSafePostDownload = async (baseUrl, path, init, artifactsPath) => {
|
|
36
|
-
const res = await fetch(baseUrl + path, {
|
|
37
|
-
...init,
|
|
38
|
-
method: 'POST',
|
|
39
|
-
});
|
|
40
|
-
if (!res.ok) {
|
|
41
|
-
throw new Error(await res.text());
|
|
42
|
-
}
|
|
43
|
-
let outputPath = artifactsPath || './artifacts.zip';
|
|
44
|
-
// Handle tilde expansion for home directory
|
|
45
|
-
if (outputPath.startsWith('~/') || outputPath === '~') {
|
|
46
|
-
outputPath = outputPath.replace(/^~(?=$|\/|\\)/, os.homedir());
|
|
47
|
-
}
|
|
48
|
-
// Create directory structure if it doesn't exist
|
|
49
|
-
const directory = (0, node_path_1.dirname)(outputPath);
|
|
50
|
-
if (directory !== '.') {
|
|
51
|
-
try {
|
|
52
|
-
(0, node_fs_1.mkdirSync)(directory, { recursive: true });
|
|
53
|
-
}
|
|
54
|
-
catch (error) {
|
|
55
|
-
// Ignore if directory already exists
|
|
56
|
-
if (error.code !== 'EEXIST') {
|
|
57
|
-
throw error;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
const fileStream = (0, node_fs_1.createWriteStream)(outputPath, { flags: 'wx' });
|
|
62
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
63
|
-
await (0, promises_2.finished)(node_stream_1.Readable.fromWeb(res.body).pipe(fileStream));
|
|
64
|
-
};
|
|
65
|
-
exports.typeSafePostDownload = typeSafePostDownload;
|
|
66
|
-
const typeSafeGet = async (baseUrl, path, init) => {
|
|
67
|
-
const res = await fetch(baseUrl + path, init);
|
|
68
|
-
if (!res.ok) {
|
|
69
|
-
throw new Error(await res.text());
|
|
70
|
-
}
|
|
71
|
-
return res.json();
|
|
72
|
-
};
|
|
73
|
-
exports.typeSafeGet = typeSafeGet;
|
|
20
|
+
const SupabaseGateway_1 = require("./gateways/SupabaseGateway");
|
|
21
|
+
const ApiGateway_1 = require("./gateways/ApiGateway");
|
|
74
22
|
const toBuffer = async (archive) => {
|
|
75
23
|
const chunks = [];
|
|
76
24
|
const writable = new node_stream_1.Writable();
|
|
@@ -216,13 +164,7 @@ const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false, lo
|
|
|
216
164
|
}
|
|
217
165
|
if (!ignoreShaCheck && sha) {
|
|
218
166
|
try {
|
|
219
|
-
const { appBinaryId, exists } = await
|
|
220
|
-
body: JSON.stringify({ sha }),
|
|
221
|
-
headers: {
|
|
222
|
-
'content-type': 'application/json',
|
|
223
|
-
'x-app-api-key': apiKey,
|
|
224
|
-
},
|
|
225
|
-
});
|
|
167
|
+
const { appBinaryId, exists } = await ApiGateway_1.ApiGateway.checkForExistingUpload(apiUrl, apiKey, sha);
|
|
226
168
|
if (exists) {
|
|
227
169
|
if (log) {
|
|
228
170
|
core_1.ux.info(`sha hash matches existing binary with id: ${appBinaryId}, skipping upload. Force upload with --ignore-sha-check`);
|
|
@@ -235,15 +177,7 @@ const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false, lo
|
|
|
235
177
|
// ignore error
|
|
236
178
|
}
|
|
237
179
|
}
|
|
238
|
-
const { id, message, path, token } = await (
|
|
239
|
-
body: JSON.stringify({
|
|
240
|
-
platform: filePath?.endsWith('.apk') ? 'android' : 'ios',
|
|
241
|
-
}),
|
|
242
|
-
headers: {
|
|
243
|
-
'content-type': 'application/json',
|
|
244
|
-
'x-app-api-key': apiKey,
|
|
245
|
-
},
|
|
246
|
-
});
|
|
180
|
+
const { id, message, path, token } = await ApiGateway_1.ApiGateway.getBinaryUploadUrl(apiUrl, apiKey, filePath?.endsWith('.apk') ? 'android' : 'ios');
|
|
247
181
|
if (!path)
|
|
248
182
|
throw new Error(message);
|
|
249
183
|
let metadata;
|
|
@@ -259,33 +193,9 @@ const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false, lo
|
|
|
259
193
|
core_1.ux.warn('Failed to extract app metadata, please share with support@devicecloud.dev so we can improve our parsing.');
|
|
260
194
|
}
|
|
261
195
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
SUPABASE_PUBLIC_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxibXNvd2VodGp3bnFsdXJwZW1iIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDkyMTg0ODcsImV4cCI6MjAyNDc5NDQ4N30.zeLTMAuZ_WwYvGdeP0kdvL_Zrs-RQee5APPyxmWq7qQ',
|
|
266
|
-
SUPABASE_URL: 'https://lbmsowehtjwnqlurpemb.supabase.co',
|
|
267
|
-
},
|
|
268
|
-
prod: {
|
|
269
|
-
SUPABASE_PUBLIC_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBneWRucGhiaW1ldGluc2dma2JvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDc1OTQzNDYsImV4cCI6MjAyMzE3MDM0Nn0.hAYOMFxxwX1exkQkY9xyQJGC_GhGnyogkj2N-kBkMI8',
|
|
270
|
-
SUPABASE_URL: 'https://pgydnphbimetinsgfkbo.supabase.co',
|
|
271
|
-
},
|
|
272
|
-
};
|
|
273
|
-
const { SUPABASE_PUBLIC_KEY, SUPABASE_URL } = SB[apiUrl === 'https://api.devicecloud.dev' ? 'prod' : 'dev'];
|
|
274
|
-
const supabase = (0, supabase_js_1.createClient)(SUPABASE_URL, SUPABASE_PUBLIC_KEY);
|
|
275
|
-
const uploadToUrl = await supabase.storage
|
|
276
|
-
.from('organizations')
|
|
277
|
-
.uploadToSignedUrl(path, token, file);
|
|
278
|
-
if (uploadToUrl.error)
|
|
279
|
-
throw new Error(uploadToUrl.error);
|
|
280
|
-
const { error } = await (0, exports.typeSafePost)(apiUrl, '/uploads/finaliseUpload', {
|
|
281
|
-
body: JSON.stringify({ id, metadata, path, sha }),
|
|
282
|
-
headers: {
|
|
283
|
-
'content-type': 'application/json',
|
|
284
|
-
'x-app-api-key': apiKey,
|
|
285
|
-
},
|
|
286
|
-
});
|
|
287
|
-
if (error)
|
|
288
|
-
throw new Error(error);
|
|
196
|
+
const env = apiUrl === 'https://api.devicecloud.dev' ? 'prod' : 'dev';
|
|
197
|
+
await SupabaseGateway_1.SupabaseGateway.uploadToSignedUrl(env, path, token, file);
|
|
198
|
+
await ApiGateway_1.ApiGateway.finaliseUpload(apiUrl, apiKey, id, metadata, path, sha);
|
|
289
199
|
if (log) {
|
|
290
200
|
core_1.ux.action.stop(`\nBinary uploaded with id: ${id}`);
|
|
291
201
|
}
|
|
@@ -329,25 +239,6 @@ async function getFileHashFromFile(file) {
|
|
|
329
239
|
processChunks();
|
|
330
240
|
});
|
|
331
241
|
}
|
|
332
|
-
const getUploadStatus = async (apiUrl, apiKey, options) => {
|
|
333
|
-
const queryParams = new URLSearchParams();
|
|
334
|
-
if (options.uploadId) {
|
|
335
|
-
queryParams.append('uploadId', options.uploadId);
|
|
336
|
-
}
|
|
337
|
-
if (options.name) {
|
|
338
|
-
queryParams.append('name', options.name);
|
|
339
|
-
}
|
|
340
|
-
const response = await fetch(`${apiUrl}/uploads/status?${queryParams}`, {
|
|
341
|
-
headers: {
|
|
342
|
-
'x-app-api-key': apiKey,
|
|
343
|
-
},
|
|
344
|
-
});
|
|
345
|
-
if (!response.ok) {
|
|
346
|
-
throw new Error(`Failed to fetch status: ${response.statusText}`);
|
|
347
|
-
}
|
|
348
|
-
return response.json();
|
|
349
|
-
};
|
|
350
|
-
exports.getUploadStatus = getUploadStatus;
|
|
351
242
|
/**
|
|
352
243
|
* Writes JSON data to a file with error handling
|
|
353
244
|
* @param filePath - Path to the output JSON file
|
|
@@ -367,3 +258,17 @@ const writeJSONFile = (filePath, data, logger) => {
|
|
|
367
258
|
}
|
|
368
259
|
};
|
|
369
260
|
exports.writeJSONFile = writeJSONFile;
|
|
261
|
+
/**
|
|
262
|
+
* Formats duration in seconds into a human readable string
|
|
263
|
+
* @param durationSeconds - Duration in seconds
|
|
264
|
+
* @returns Formatted duration string (e.g. "2m 30s" or "45s")
|
|
265
|
+
*/
|
|
266
|
+
const formatDurationSeconds = (durationSeconds) => {
|
|
267
|
+
const minutes = Math.floor(durationSeconds / 60);
|
|
268
|
+
const seconds = durationSeconds % 60;
|
|
269
|
+
if (minutes > 0) {
|
|
270
|
+
return `${minutes}m ${seconds}s`;
|
|
271
|
+
}
|
|
272
|
+
return `${durationSeconds}s`;
|
|
273
|
+
};
|
|
274
|
+
exports.formatDurationSeconds = formatDurationSeconds;
|
package/oclif.manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"typescript": "^5"
|
|
44
44
|
},
|
|
45
45
|
"engines": {
|
|
46
|
-
"node": ">=
|
|
46
|
+
"node": ">=20.0.0"
|
|
47
47
|
},
|
|
48
48
|
"files": [
|
|
49
49
|
"/bin",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"prepare": "yarn build",
|
|
80
80
|
"version": "oclif readme && git add README.md"
|
|
81
81
|
},
|
|
82
|
-
"version": "3.7.
|
|
82
|
+
"version": "3.7.3",
|
|
83
83
|
"bugs": {
|
|
84
84
|
"url": "https://discord.gg/gm3mJwcNw8"
|
|
85
85
|
},
|