@devicecloud.dev/dcd 1.0.10 → 2.0.0-rc.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.d.ts +4 -6
- package/dist/commands/cloud.js +57 -102
- package/dist/constants.d.ts +3 -1
- package/dist/constants.js +20 -16
- package/dist/methods.d.ts +10 -2
- package/dist/methods.js +140 -24
- package/dist/plan.js +16 -27
- package/dist/planMethods.d.ts +3 -4
- package/dist/planMethods.js +14 -18
- package/oclif.manifest.json +28 -12
- package/package.json +1 -1
package/dist/commands/cloud.d.ts
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
+
export declare const mimeTypeLookupByExtension: Record<string, string>;
|
|
2
3
|
export declare enum EiOSDevices {
|
|
3
4
|
'ipad-pro-6th-gen' = "ipad-pro-6th-gen",
|
|
4
|
-
'iphone-12' = "iphone-12",
|
|
5
|
-
'iphone-12-mini' = "iphone-12-mini",
|
|
6
|
-
'iphone-12-pro-max' = "iphone-12-pro-max",
|
|
7
5
|
'iphone-13' = "iphone-13",
|
|
8
|
-
'iphone-13-mini' = "iphone-13-mini",
|
|
9
|
-
'iphone-13-pro-max' = "iphone-13-pro-max",
|
|
10
6
|
'iphone-14' = "iphone-14",
|
|
11
7
|
'iphone-14-plus' = "iphone-14-plus",
|
|
12
8
|
'iphone-14-pro' = "iphone-14-pro",
|
|
@@ -28,6 +24,8 @@ export default class Cloud extends Command {
|
|
|
28
24
|
static description: string;
|
|
29
25
|
static examples: string[];
|
|
30
26
|
static flags: {
|
|
27
|
+
'additional-app-binary-ids': import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
28
|
+
'additional-app-files': import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
31
29
|
'android-api-level': import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
32
30
|
'android-device': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
33
31
|
apiKey: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
@@ -37,6 +35,7 @@ export default class Cloud extends Command {
|
|
|
37
35
|
arm64: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
38
36
|
async: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
39
37
|
'device-locale': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
38
|
+
'download-artifacts': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
40
39
|
env: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
41
40
|
'exclude-flows': import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
42
41
|
'exclude-tags': import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
@@ -45,7 +44,6 @@ export default class Cloud extends Command {
|
|
|
45
44
|
'include-tags': import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
46
45
|
'ios-device': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
47
46
|
'ios-version': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
48
|
-
'legacy-upload': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
49
47
|
'maestro-version': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
50
48
|
name: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
51
49
|
orientation: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
package/dist/commands/cloud.js
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.EiOSDevices = void 0;
|
|
3
|
+
exports.EiOSDevices = exports.mimeTypeLookupByExtension = void 0;
|
|
4
4
|
/* eslint-disable complexity */
|
|
5
5
|
const core_1 = require("@oclif/core");
|
|
6
6
|
const cli_ux_1 = require("@oclif/core/lib/cli-ux");
|
|
7
7
|
const errors_1 = require("@oclif/core/lib/errors");
|
|
8
|
-
const supabase_js_1 = require("@supabase/supabase-js");
|
|
9
|
-
const file_1 = require("@web-std/file");
|
|
10
|
-
const promises_1 = require("node:fs/promises");
|
|
11
8
|
const path = require("node:path");
|
|
12
9
|
const constants_1 = require("../constants");
|
|
13
10
|
const methods_1 = require("../methods");
|
|
14
11
|
const plan_1 = require("../plan");
|
|
15
|
-
|
|
12
|
+
exports.mimeTypeLookupByExtension = {
|
|
16
13
|
apk: 'application/vnd.android.package-archive',
|
|
17
14
|
yaml: 'application/x-yaml',
|
|
18
15
|
zip: 'application/zip',
|
|
@@ -20,12 +17,7 @@ const mimeTypeLookupByExtension = {
|
|
|
20
17
|
var EiOSDevices;
|
|
21
18
|
(function (EiOSDevices) {
|
|
22
19
|
EiOSDevices["ipad-pro-6th-gen"] = "ipad-pro-6th-gen";
|
|
23
|
-
EiOSDevices["iphone-12"] = "iphone-12";
|
|
24
|
-
EiOSDevices["iphone-12-mini"] = "iphone-12-mini";
|
|
25
|
-
EiOSDevices["iphone-12-pro-max"] = "iphone-12-pro-max";
|
|
26
20
|
EiOSDevices["iphone-13"] = "iphone-13";
|
|
27
|
-
EiOSDevices["iphone-13-mini"] = "iphone-13-mini";
|
|
28
|
-
EiOSDevices["iphone-13-pro-max"] = "iphone-13-pro-max";
|
|
29
21
|
EiOSDevices["iphone-14"] = "iphone-14";
|
|
30
22
|
EiOSDevices["iphone-14-plus"] = "iphone-14-plus";
|
|
31
23
|
EiOSDevices["iphone-14-pro"] = "iphone-14-pro";
|
|
@@ -63,16 +55,20 @@ class Cloud extends core_1.Command {
|
|
|
63
55
|
}
|
|
64
56
|
await (0, methods_1.versionCheck)(this.config.version);
|
|
65
57
|
const { args, flags, raw } = await this.parse(Cloud);
|
|
66
|
-
const { 'android-api-level': androidApiLevel, 'android-device': androidDevice, apiKey, apiUrl, 'app-binary-id': appBinaryId, 'app-file': appFile, arm64, async, 'device-locale': deviceLocale, env, 'exclude-flows': excludeFlows, 'exclude-tags': excludeTags, flows, 'google-play': googlePlay, 'include-tags': includeTags, 'ios-device': iOSDevice, 'ios-version': iOSVersion, '
|
|
58
|
+
const { 'additional-app-binary-ids': nonFlatAdditionalAppBinaryIds, 'additional-app-files': nonFlatAdditionalAppFiles, 'android-api-level': androidApiLevel, 'android-device': androidDevice, apiKey, apiUrl, 'app-binary-id': appBinaryId, 'app-file': appFile, arm64, async, 'device-locale': deviceLocale, 'download-artifacts': downloadArtifacts, env, 'exclude-flows': excludeFlows, 'exclude-tags': excludeTags, flows, 'google-play': googlePlay, 'include-tags': includeTags, 'ios-device': iOSDevice, 'ios-version': iOSVersion, 'maestro-version': maestroVersion, name, orientation, quiet, ...rest } = flags;
|
|
67
59
|
if (arm64) {
|
|
68
60
|
(0, cli_ux_1.info)('Contact hello@devicecloud.dev to enquire about arm64 devices');
|
|
69
61
|
(0, cli_ux_1.exit)();
|
|
70
62
|
}
|
|
63
|
+
if (!apiKey)
|
|
64
|
+
throw new Error('You must provide an API key');
|
|
65
|
+
const additionalAppBinaryIds = nonFlatAdditionalAppBinaryIds?.flat();
|
|
66
|
+
const additionalAppFiles = nonFlatAdditionalAppFiles?.flat();
|
|
71
67
|
const { firstFile, secondFile } = args;
|
|
72
68
|
let finalBinaryId = appBinaryId;
|
|
73
|
-
let
|
|
69
|
+
let finalAdditionalBinaryIds = additionalAppBinaryIds;
|
|
70
|
+
const finalAppFile = appFile ?? firstFile;
|
|
74
71
|
let flowFile = flows ?? secondFile;
|
|
75
|
-
let metadata;
|
|
76
72
|
if (appBinaryId) {
|
|
77
73
|
if (secondFile) {
|
|
78
74
|
throw new Error('You cannot provide both an appBinaryId and a binary file');
|
|
@@ -105,6 +101,18 @@ class Cloud extends core_1.Command {
|
|
|
105
101
|
process.exit(2);
|
|
106
102
|
}
|
|
107
103
|
const { allExcludeTags, allIncludeTags, flowsToRun: testFileNames, referencedFiles, sequence, } = executionPlan;
|
|
104
|
+
const pathsShortestToLongest = [
|
|
105
|
+
...testFileNames,
|
|
106
|
+
...referencedFiles,
|
|
107
|
+
].sort((a, b) => a.split(path.sep).length - b.split(path.sep).length);
|
|
108
|
+
let commonRoot = path.parse(process.cwd()).root;
|
|
109
|
+
const folders = pathsShortestToLongest[0].split(path.sep);
|
|
110
|
+
for (const [index] of folders.entries()) {
|
|
111
|
+
const folderPath = folders.slice(0, index + 1).join(path.sep);
|
|
112
|
+
const isRoot = pathsShortestToLongest.every((file) => file.startsWith(folderPath));
|
|
113
|
+
if (isRoot)
|
|
114
|
+
commonRoot = folderPath;
|
|
115
|
+
}
|
|
108
116
|
const { continueOnFailure = true, flows: sequentialFlows = [] } = sequence ?? {};
|
|
109
117
|
if (!appBinaryId) {
|
|
110
118
|
if (!(flowFile && finalAppFile)) {
|
|
@@ -117,6 +125,7 @@ class Cloud extends core_1.Command {
|
|
|
117
125
|
await (0, methods_1.verifyAppZip)(finalAppFile);
|
|
118
126
|
}
|
|
119
127
|
}
|
|
128
|
+
await (0, methods_1.verifyAdditionalAppFiles)(additionalAppFiles);
|
|
120
129
|
const flagLogs = [];
|
|
121
130
|
for (const [k, v] of Object.entries(flags)) {
|
|
122
131
|
if (v && v.toString().length > 0) {
|
|
@@ -128,6 +137,8 @@ class Cloud extends core_1.Command {
|
|
|
128
137
|
Submitting new job
|
|
129
138
|
→ Flow(s): ${flowFile}
|
|
130
139
|
→ App: ${appBinaryId || finalAppFile}
|
|
140
|
+
${Boolean(additionalAppBinaryIds || additionalAppFiles) &&
|
|
141
|
+
`→ Additional app(s): ${additionalAppBinaryIds} ${additionalAppFiles}`}
|
|
131
142
|
|
|
132
143
|
With options
|
|
133
144
|
→ ${flagLogs.join(`
|
|
@@ -135,92 +146,18 @@ class Cloud extends core_1.Command {
|
|
|
135
146
|
|
|
136
147
|
`);
|
|
137
148
|
if (!finalBinaryId) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
binaryFormData.set('file', binaryBlob, finalAppFile);
|
|
151
|
-
}
|
|
152
|
-
const options = {
|
|
153
|
-
body: binaryFormData,
|
|
154
|
-
headers: { 'x-app-api-key': apiKey },
|
|
155
|
-
};
|
|
156
|
-
core_1.ux.action.status = `Uploading`;
|
|
157
|
-
const { binaryId, message } = await (0, methods_1.typeSafePost)(apiUrl, '/uploads/binary', options);
|
|
158
|
-
if (!binaryId)
|
|
159
|
-
throw new Error(message);
|
|
160
|
-
finalBinaryId = binaryId;
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
163
|
-
const { id, message, path, token } = await (0, methods_1.typeSafePost)(apiUrl, '/uploads/getBinaryUploadUrl', {
|
|
164
|
-
body: JSON.stringify({
|
|
165
|
-
platform: finalAppFile?.endsWith('.apk') ? 'android' : 'ios',
|
|
166
|
-
}),
|
|
167
|
-
headers: {
|
|
168
|
-
'content-type': 'application/json',
|
|
169
|
-
'x-app-api-key': apiKey,
|
|
170
|
-
},
|
|
171
|
-
});
|
|
172
|
-
finalBinaryId = id;
|
|
173
|
-
if (!path)
|
|
174
|
-
throw new Error(message);
|
|
175
|
-
let file;
|
|
176
|
-
if (finalAppFile?.endsWith('.app')) {
|
|
177
|
-
const zippedAppBlob = await (0, methods_1.compressFolderToBlob)(finalAppFile);
|
|
178
|
-
const filePath = finalAppFile + '.zip';
|
|
179
|
-
file = new file_1.File([zippedAppBlob], filePath);
|
|
180
|
-
}
|
|
181
|
-
else {
|
|
182
|
-
const binaryBlob = new Blob([await (0, promises_1.readFile)(finalAppFile)], {
|
|
183
|
-
type: mimeTypeLookupByExtension[finalAppFile.split('.').pop()],
|
|
184
|
-
});
|
|
185
|
-
file = new file_1.File([binaryBlob], finalAppFile);
|
|
186
|
-
}
|
|
187
|
-
try {
|
|
188
|
-
metadata = finalAppFile?.endsWith('.apk')
|
|
189
|
-
? await (0, methods_1.extractAppMetadataAndroid)(finalAppFile)
|
|
190
|
-
: await (0, methods_1.extractAppMetadataIos)(finalAppFile);
|
|
191
|
-
}
|
|
192
|
-
catch {
|
|
193
|
-
this.warn('Failed to extact app metadata, please share with support@devicecloud.dev so we can improve our parsing.');
|
|
194
|
-
}
|
|
195
|
-
// this needs to made nicer by using envs or maybe fetching the keys from the getSignedURL call
|
|
196
|
-
const SB = {
|
|
197
|
-
dev: {
|
|
198
|
-
SUPABASE_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxibXNvd2VodGp3bnFsdXJwZW1iIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDkyMTg0ODcsImV4cCI6MjAyNDc5NDQ4N30.zeLTMAuZ_WwYvGdeP0kdvL_Zrs-RQee5APPyxmWq7qQ',
|
|
199
|
-
SUPABASE_URL: 'https://lbmsowehtjwnqlurpemb.supabase.co',
|
|
200
|
-
},
|
|
201
|
-
prod: {
|
|
202
|
-
SUPABASE_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBneWRucGhiaW1ldGluc2dma2JvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDc1OTQzNDYsImV4cCI6MjAyMzE3MDM0Nn0.hAYOMFxxwX1exkQkY9xyQJGC_GhGnyogkj2N-kBkMI8',
|
|
203
|
-
SUPABASE_URL: 'https://pgydnphbimetinsgfkbo.supabase.co',
|
|
204
|
-
},
|
|
205
|
-
};
|
|
206
|
-
const { SUPABASE_KEY, SUPABASE_URL } = SB[apiUrl === 'https://api.devicecloud.dev' ? 'prod' : 'dev'];
|
|
207
|
-
const supabase = (0, supabase_js_1.createClient)(SUPABASE_URL, SUPABASE_KEY);
|
|
208
|
-
const uploadToUrl = await supabase.storage
|
|
209
|
-
.from('organizations')
|
|
210
|
-
.uploadToSignedUrl(path, token, file);
|
|
211
|
-
if (uploadToUrl.error)
|
|
212
|
-
throw new Error(uploadToUrl.error);
|
|
213
|
-
const { error } = await (0, methods_1.typeSafePost)(apiUrl, '/uploads/finaliseUpload', {
|
|
214
|
-
body: JSON.stringify({ id, metadata, path }),
|
|
215
|
-
headers: {
|
|
216
|
-
'content-type': 'application/json',
|
|
217
|
-
'x-app-api-key': apiKey,
|
|
218
|
-
},
|
|
219
|
-
});
|
|
220
|
-
if (error)
|
|
221
|
-
throw new Error(error);
|
|
222
|
-
}
|
|
223
|
-
core_1.ux.action.stop(`\nBinary uploaded with id: ${finalBinaryId}`);
|
|
149
|
+
if (!finalAppFile)
|
|
150
|
+
throw new Error('You must provide either an app binary id or an app file');
|
|
151
|
+
const binaryId = await (0, methods_1.uploadBinary)(finalAppFile, apiUrl, apiKey);
|
|
152
|
+
finalBinaryId = binaryId;
|
|
153
|
+
}
|
|
154
|
+
let uploadedBinaryIds = [];
|
|
155
|
+
if (additionalAppFiles?.length) {
|
|
156
|
+
uploadedBinaryIds = await (0, methods_1.uploadBinaries)(additionalAppFiles, apiUrl, apiKey);
|
|
157
|
+
finalAdditionalBinaryIds = [
|
|
158
|
+
...finalAdditionalBinaryIds,
|
|
159
|
+
...uploadedBinaryIds,
|
|
160
|
+
];
|
|
224
161
|
}
|
|
225
162
|
const testFormData = new FormData();
|
|
226
163
|
// eslint-disable-next-line unicorn/no-array-reduce
|
|
@@ -238,17 +175,18 @@ class Cloud extends core_1.Command {
|
|
|
238
175
|
...testFileNames,
|
|
239
176
|
...sequentialFlows,
|
|
240
177
|
]),
|
|
241
|
-
]);
|
|
178
|
+
], commonRoot);
|
|
242
179
|
const blob = new Blob([buffer], {
|
|
243
|
-
type: mimeTypeLookupByExtension.zip,
|
|
180
|
+
type: exports.mimeTypeLookupByExtension.zip,
|
|
244
181
|
});
|
|
245
182
|
testFormData.set('file', blob, 'flowFile.zip');
|
|
246
183
|
testFormData.set('appBinaryId', finalBinaryId);
|
|
247
|
-
testFormData.set('testFileNames', JSON.stringify(testFileNames));
|
|
184
|
+
testFormData.set('testFileNames', JSON.stringify(testFileNames.map((t) => t.replaceAll(commonRoot, '.'))));
|
|
248
185
|
testFormData.set('sequentialFlows', JSON.stringify(sequentialFlows));
|
|
249
186
|
testFormData.set('env', JSON.stringify(envObject));
|
|
250
187
|
testFormData.set('googlePlay', googlePlay ? 'true' : 'false');
|
|
251
188
|
testFormData.set('config', JSON.stringify({
|
|
189
|
+
additionalAppBinaryIds: finalAdditionalBinaryIds,
|
|
252
190
|
allExcludeTags,
|
|
253
191
|
allIncludeTags,
|
|
254
192
|
continueOnFailure,
|
|
@@ -256,6 +194,7 @@ class Cloud extends core_1.Command {
|
|
|
256
194
|
maestroVersion,
|
|
257
195
|
orientation,
|
|
258
196
|
raw,
|
|
197
|
+
uploadedBinaryIds,
|
|
259
198
|
version: this.config.version,
|
|
260
199
|
}));
|
|
261
200
|
if (androidApiLevel)
|
|
@@ -321,6 +260,22 @@ class Cloud extends core_1.Command {
|
|
|
321
260
|
core_1.ux.url(url, url);
|
|
322
261
|
(0, cli_ux_1.info)('\n');
|
|
323
262
|
clearInterval(intervalId);
|
|
263
|
+
if (downloadArtifacts) {
|
|
264
|
+
try {
|
|
265
|
+
await (0, methods_1.typeSafePostDownload)(apiUrl, `/results/${results[0].test_upload_id}/download`, {
|
|
266
|
+
body: JSON.stringify({ results: downloadArtifacts }),
|
|
267
|
+
headers: {
|
|
268
|
+
'content-type': 'application/json',
|
|
269
|
+
'x-app-api-key': apiKey,
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
(0, cli_ux_1.info)('\n');
|
|
273
|
+
(0, cli_ux_1.info)('Test artifacts have been downloaded to ./artifacts.zip');
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
this.warn('Failed to download artifacts');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
324
279
|
if (updatedResults.some((result) => result.status === 'FAILED')) {
|
|
325
280
|
// eslint-disable-next-line no-process-exit, unicorn/no-process-exit
|
|
326
281
|
process.exit(2);
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { EiOSDevices } from './commands/cloud';
|
|
2
2
|
export declare const flags: {
|
|
3
|
+
'additional-app-binary-ids': import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
4
|
+
'additional-app-files': import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
3
5
|
'android-api-level': import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
4
6
|
'android-device': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
5
7
|
apiKey: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
@@ -9,6 +11,7 @@ export declare const flags: {
|
|
|
9
11
|
arm64: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
10
12
|
async: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
11
13
|
'device-locale': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
14
|
+
'download-artifacts': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
12
15
|
env: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
13
16
|
'exclude-flows': import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
14
17
|
'exclude-tags': import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
@@ -17,7 +20,6 @@ export declare const flags: {
|
|
|
17
20
|
'include-tags': import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
18
21
|
'ios-device': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
19
22
|
'ios-version': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
20
|
-
'legacy-upload': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
21
23
|
'maestro-version': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
22
24
|
name: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
23
25
|
orientation: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
package/dist/constants.js
CHANGED
|
@@ -3,6 +3,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.iOSCompatibilityLookup = exports.flags = void 0;
|
|
4
4
|
const core_1 = require("@oclif/core");
|
|
5
5
|
exports.flags = {
|
|
6
|
+
'additional-app-binary-ids': core_1.Flags.string({
|
|
7
|
+
default: [],
|
|
8
|
+
description: 'The ID of the additional app binary(s) previously uploaded to devicecloud.dev to install before execution',
|
|
9
|
+
multiple: true,
|
|
10
|
+
multipleNonGreedy: true,
|
|
11
|
+
parse: (input) => input.split(','),
|
|
12
|
+
}),
|
|
13
|
+
'additional-app-files': core_1.Flags.file({
|
|
14
|
+
default: [],
|
|
15
|
+
description: 'Additional app binary(s) to install before execution',
|
|
16
|
+
multiple: true,
|
|
17
|
+
multipleNonGreedy: true,
|
|
18
|
+
parse: (input) => input.split(','),
|
|
19
|
+
}),
|
|
6
20
|
'android-api-level': core_1.Flags.integer({
|
|
7
21
|
description: '[Android only] Android API level to run your flow against',
|
|
8
22
|
options: ['33', '34'],
|
|
@@ -45,6 +59,10 @@ exports.flags = {
|
|
|
45
59
|
'device-locale': core_1.Flags.string({
|
|
46
60
|
description: 'Locale that will be set to a device, ISO-639-1 code and uppercase ISO-3166-1 code e.g. "de_DE" for Germany',
|
|
47
61
|
}),
|
|
62
|
+
'download-artifacts': core_1.Flags.string({
|
|
63
|
+
description: 'BETA (API may change) - download a zip containing the logs, screenshots and videos for each result in this run. You will debited a $0.01 egress fee for each result. Use --download-artifacts=FAILED for failures only or --download-artifacts=ALL for every result.',
|
|
64
|
+
options: ['ALL', 'FAILED'],
|
|
65
|
+
}),
|
|
48
66
|
env: core_1.Flags.file({
|
|
49
67
|
char: 'e',
|
|
50
68
|
description: 'One or more environment variables to inject into your flows',
|
|
@@ -85,12 +103,7 @@ exports.flags = {
|
|
|
85
103
|
'ios-device': core_1.Flags.string({
|
|
86
104
|
description: '[iOS only] iOS device to run your flow against',
|
|
87
105
|
options: [
|
|
88
|
-
'iphone-12',
|
|
89
|
-
'iphone-12-mini',
|
|
90
|
-
'iphone-12-pro-max',
|
|
91
106
|
'iphone-13',
|
|
92
|
-
'iphone-13-mini',
|
|
93
|
-
'iphone-13-pro-max',
|
|
94
107
|
'iphone-14',
|
|
95
108
|
'iphone-14-plus',
|
|
96
109
|
'iphone-14-pro',
|
|
@@ -110,10 +123,6 @@ exports.flags = {
|
|
|
110
123
|
description: '[iOS only] iOS version to run your flow against',
|
|
111
124
|
options: ['15', '16', '17', '18'],
|
|
112
125
|
}),
|
|
113
|
-
'legacy-upload': core_1.Flags.boolean({
|
|
114
|
-
default: false,
|
|
115
|
-
description: 'Use the legacy direct upload method',
|
|
116
|
-
}),
|
|
117
126
|
'maestro-version': core_1.Flags.string({
|
|
118
127
|
aliases: ['maestroVersion'],
|
|
119
128
|
description: '[ALPHA pre-release] - Maestro version to run your flow against',
|
|
@@ -146,13 +155,8 @@ exports.flags = {
|
|
|
146
155
|
}),
|
|
147
156
|
};
|
|
148
157
|
exports.iOSCompatibilityLookup = {
|
|
149
|
-
'ipad-pro-6th-gen': ['16', '17'],
|
|
150
|
-
'iphone-
|
|
151
|
-
'iphone-12-mini': ['15', '16', '17'],
|
|
152
|
-
'iphone-12-pro-max': ['15', '16', '17'],
|
|
153
|
-
'iphone-13': ['15', '16', '17'],
|
|
154
|
-
'iphone-13-mini': ['15', '16', '17'],
|
|
155
|
-
'iphone-13-pro-max': ['15', '16', '17'],
|
|
158
|
+
'ipad-pro-6th-gen': ['16', '17', '18'],
|
|
159
|
+
'iphone-13': ['15'],
|
|
156
160
|
'iphone-14': ['16', '17', '18'],
|
|
157
161
|
'iphone-14-plus': ['16', '17', '18'],
|
|
158
162
|
'iphone-14-pro': ['16', '17', '18'],
|
package/dist/methods.d.ts
CHANGED
|
@@ -7,6 +7,10 @@ export declare const typeSafePost: <T extends keyof paths>(baseUrl: string, path
|
|
|
7
7
|
body?: BodyInit;
|
|
8
8
|
headers?: HeadersInit;
|
|
9
9
|
}) => Promise<paths[T]["post"]["responses"]["201"]["content"]["application/json"]>;
|
|
10
|
+
export declare const typeSafePostDownload: (baseUrl: string, path: string, init?: {
|
|
11
|
+
body?: BodyInit;
|
|
12
|
+
headers?: HeadersInit;
|
|
13
|
+
}) => Promise<void>;
|
|
10
14
|
export declare const typeSafeGet: <T extends keyof paths>(baseUrl: string, path: string, init?: {
|
|
11
15
|
body?: FormData;
|
|
12
16
|
headers?: HeadersInit;
|
|
@@ -14,7 +18,11 @@ export declare const typeSafeGet: <T extends keyof paths>(baseUrl: string, path:
|
|
|
14
18
|
export declare const toBuffer: (archive: archiver.Archiver) => Promise<Buffer>;
|
|
15
19
|
export declare const compressDir: (sourceDir: string) => Promise<Buffer>;
|
|
16
20
|
export declare const compressFolderToBlob: (sourceDir: string) => Promise<Blob>;
|
|
17
|
-
export declare const compressFilesFromRelativePath: (path: string, files: string[]) => Promise<Buffer>;
|
|
21
|
+
export declare const compressFilesFromRelativePath: (path: string, files: string[], commonRoot: string) => Promise<Buffer>;
|
|
18
22
|
export declare const verifyAppZip: (zipPath: string) => Promise<void>;
|
|
19
23
|
export declare const extractAppMetadataAndroid: (appFilePath: string) => Promise<TAppMetadata>;
|
|
20
|
-
export declare const
|
|
24
|
+
export declare const extractAppMetadataIosZip: (appFilePath: string) => Promise<TAppMetadata>;
|
|
25
|
+
export declare const extractAppMetadataIos: (appFolderPath: string) => Promise<TAppMetadata>;
|
|
26
|
+
export declare const uploadBinary: (filePath: string, apiUrl: string, apiKey: string) => Promise<string>;
|
|
27
|
+
export declare const uploadBinaries: (finalAppFiles: string[], apiUrl: string, apiKey: string) => Promise<string[]>;
|
|
28
|
+
export declare const verifyAdditionalAppFiles: (appFiles: string[] | undefined) => Promise<void>;
|
package/dist/methods.js
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.extractAppMetadataIos = exports.extractAppMetadataAndroid = exports.verifyAppZip = exports.compressFilesFromRelativePath = exports.compressFolderToBlob = exports.compressDir = exports.toBuffer = exports.typeSafeGet = exports.typeSafePost = exports.versionCheck = void 0;
|
|
4
|
-
const
|
|
5
|
-
|
|
3
|
+
exports.verifyAdditionalAppFiles = exports.uploadBinaries = exports.uploadBinary = exports.extractAppMetadataIos = exports.extractAppMetadataIosZip = exports.extractAppMetadataAndroid = exports.verifyAppZip = exports.compressFilesFromRelativePath = exports.compressFolderToBlob = exports.compressDir = exports.toBuffer = exports.typeSafeGet = exports.typeSafePostDownload = exports.typeSafePost = exports.versionCheck = void 0;
|
|
4
|
+
const core_1 = require("@oclif/core");
|
|
5
|
+
const supabase_js_1 = require("@supabase/supabase-js");
|
|
6
|
+
// required polyfill for node 18
|
|
7
|
+
const file_1 = require("@web-std/file");
|
|
6
8
|
// @ts-ignore
|
|
7
9
|
const AppInfoParser = require("app-info-parser");
|
|
8
10
|
const archiver = require("archiver");
|
|
9
11
|
const bplist_parser_1 = require("bplist-parser");
|
|
10
|
-
|
|
12
|
+
const node_fs_1 = require("node:fs");
|
|
13
|
+
const promises_1 = require("node:fs/promises");
|
|
11
14
|
const nodePath = require("node:path");
|
|
12
15
|
const node_stream_1 = require("node:stream");
|
|
16
|
+
const promises_2 = require("node:stream/promises");
|
|
13
17
|
const StreamZip = require("node-stream-zip");
|
|
14
18
|
const plist_1 = require("plist");
|
|
19
|
+
const cloud_1 = require("./commands/cloud");
|
|
15
20
|
const PERMITTED_EXTENSIONS = new Set([
|
|
16
21
|
'yml',
|
|
17
22
|
'yaml',
|
|
@@ -27,7 +32,7 @@ const versionCheck = async (currentVersion) => {
|
|
|
27
32
|
const versionResponseJson = await versionResponse.json();
|
|
28
33
|
const latestVersion = versionResponseJson.version;
|
|
29
34
|
if (latestVersion !== currentVersion) {
|
|
30
|
-
|
|
35
|
+
core_1.ux.warn(`
|
|
31
36
|
-------------------
|
|
32
37
|
A new version of the devicecloud.dev CLI is available: ${latestVersion}
|
|
33
38
|
Run 'npm install -g @devicecloud.dev/dcd@latest' to update to the latest version
|
|
@@ -47,6 +52,19 @@ const typeSafePost = async (baseUrl, path, init) => {
|
|
|
47
52
|
return res.json();
|
|
48
53
|
};
|
|
49
54
|
exports.typeSafePost = typeSafePost;
|
|
55
|
+
const typeSafePostDownload = async (baseUrl, path, init) => {
|
|
56
|
+
const res = await fetch(baseUrl + path, {
|
|
57
|
+
...init,
|
|
58
|
+
method: 'POST',
|
|
59
|
+
});
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
throw new Error(await res.text());
|
|
62
|
+
}
|
|
63
|
+
const fileStream = (0, node_fs_1.createWriteStream)('./artifacts.zip', { flags: 'wx' });
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
65
|
+
await (0, promises_2.finished)(node_stream_1.Readable.fromWeb(res.body).pipe(fileStream));
|
|
66
|
+
};
|
|
67
|
+
exports.typeSafePostDownload = typeSafePostDownload;
|
|
50
68
|
const typeSafeGet = async (baseUrl, path, init) => {
|
|
51
69
|
const res = await fetch(baseUrl + path, init);
|
|
52
70
|
if (!res.ok) {
|
|
@@ -103,7 +121,7 @@ const compressFolderToBlob = async (sourceDir) => {
|
|
|
103
121
|
return new Blob([buffer], { type: 'application/zip' });
|
|
104
122
|
};
|
|
105
123
|
exports.compressFolderToBlob = compressFolderToBlob;
|
|
106
|
-
const compressFilesFromRelativePath = async (path, files) => {
|
|
124
|
+
const compressFilesFromRelativePath = async (path, files, commonRoot) => {
|
|
107
125
|
const archive = archiver('zip', {
|
|
108
126
|
zlib: { level: 9 },
|
|
109
127
|
});
|
|
@@ -111,7 +129,9 @@ const compressFilesFromRelativePath = async (path, files) => {
|
|
|
111
129
|
throw err;
|
|
112
130
|
});
|
|
113
131
|
for (const file of files) {
|
|
114
|
-
archive.file(nodePath.resolve(path, file), {
|
|
132
|
+
archive.file(nodePath.resolve(path, file), {
|
|
133
|
+
name: file.replace(commonRoot, ''),
|
|
134
|
+
});
|
|
115
135
|
}
|
|
116
136
|
const buffer = await (0, exports.toBuffer)(archive);
|
|
117
137
|
// await writeFile('./my-zip.zip', buffer);
|
|
@@ -139,7 +159,23 @@ const extractAppMetadataAndroid = async (appFilePath) => {
|
|
|
139
159
|
return { appId: result.package, platform: 'android' };
|
|
140
160
|
};
|
|
141
161
|
exports.extractAppMetadataAndroid = extractAppMetadataAndroid;
|
|
142
|
-
const
|
|
162
|
+
const parseInfoPlist = async (buffer) => {
|
|
163
|
+
let data;
|
|
164
|
+
const bufferType = buffer[0];
|
|
165
|
+
if (bufferType === 60 ||
|
|
166
|
+
bufferType === '<' ||
|
|
167
|
+
bufferType === 239) {
|
|
168
|
+
data = (0, plist_1.parse)(buffer.toString());
|
|
169
|
+
}
|
|
170
|
+
else if (bufferType === 98) {
|
|
171
|
+
data = (0, bplist_parser_1.parseBuffer)(buffer)[0];
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
throw new Error('Unknown plist buffer type.');
|
|
175
|
+
}
|
|
176
|
+
return data;
|
|
177
|
+
};
|
|
178
|
+
const extractAppMetadataIosZip = async (appFilePath) => new Promise((resolve, reject) => {
|
|
143
179
|
const zip = new StreamZip({ file: './' + appFilePath });
|
|
144
180
|
zip.on('ready', () => {
|
|
145
181
|
const infoPlist = Object.values(zip.entries()).find((e) => e.name.includes('Info.plist'));
|
|
@@ -147,26 +183,106 @@ const extractAppMetadataIos = async (appFilePath) => new Promise((resolve, rejec
|
|
|
147
183
|
reject(new Error('Failed to find info plist'));
|
|
148
184
|
}
|
|
149
185
|
const buffer = zip.entryDataSync(infoPlist.name);
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
else if (bufferType === 98) {
|
|
158
|
-
data = (0, bplist_parser_1.parseBuffer)(buffer)[0];
|
|
159
|
-
}
|
|
160
|
-
else {
|
|
161
|
-
reject(new Error('Unknown plist buffer type.'));
|
|
162
|
-
}
|
|
163
|
-
const appId = data.CFBundleIdentifier;
|
|
164
|
-
zip.close();
|
|
165
|
-
resolve({ appId, platform: 'ios' });
|
|
186
|
+
parseInfoPlist(buffer)
|
|
187
|
+
.then((data) => {
|
|
188
|
+
const appId = data.CFBundleIdentifier;
|
|
189
|
+
zip.close();
|
|
190
|
+
resolve({ appId, platform: 'ios' });
|
|
191
|
+
})
|
|
192
|
+
.catch(reject);
|
|
166
193
|
});
|
|
167
194
|
zip.on('error', (err) => {
|
|
168
195
|
console.error(err);
|
|
169
196
|
reject(err);
|
|
170
197
|
});
|
|
171
198
|
});
|
|
199
|
+
exports.extractAppMetadataIosZip = extractAppMetadataIosZip;
|
|
200
|
+
const extractAppMetadataIos = async (appFolderPath) => {
|
|
201
|
+
const infoPlistPath = nodePath.join(appFolderPath, 'Info.plist');
|
|
202
|
+
const buffer = await (0, promises_1.readFile)(infoPlistPath);
|
|
203
|
+
const data = await parseInfoPlist(buffer);
|
|
204
|
+
const appId = data.CFBundleIdentifier;
|
|
205
|
+
return { appId, platform: 'ios' };
|
|
206
|
+
};
|
|
172
207
|
exports.extractAppMetadataIos = extractAppMetadataIos;
|
|
208
|
+
const uploadBinary = async (filePath, apiUrl, apiKey) => {
|
|
209
|
+
core_1.ux.action.start('Uploading binary', 'Initializing', { stdout: true });
|
|
210
|
+
const { id, message, path, token } = await (0, exports.typeSafePost)(apiUrl, '/uploads/getBinaryUploadUrl', {
|
|
211
|
+
body: JSON.stringify({
|
|
212
|
+
platform: filePath?.endsWith('.apk') ? 'android' : 'ios',
|
|
213
|
+
}),
|
|
214
|
+
headers: {
|
|
215
|
+
'content-type': 'application/json',
|
|
216
|
+
'x-app-api-key': apiKey,
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
if (!path)
|
|
220
|
+
throw new Error(message);
|
|
221
|
+
let file;
|
|
222
|
+
if (filePath?.endsWith('.app')) {
|
|
223
|
+
const zippedAppBlob = await (0, exports.compressFolderToBlob)(filePath);
|
|
224
|
+
file = new file_1.File([zippedAppBlob], filePath + '.zip');
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
const binaryBlob = new Blob([await (0, promises_1.readFile)(filePath)], {
|
|
228
|
+
type: cloud_1.mimeTypeLookupByExtension[filePath.split('.').pop()],
|
|
229
|
+
});
|
|
230
|
+
file = new file_1.File([binaryBlob], filePath);
|
|
231
|
+
}
|
|
232
|
+
let metadata;
|
|
233
|
+
try {
|
|
234
|
+
metadata = filePath?.endsWith('.apk')
|
|
235
|
+
? await (0, exports.extractAppMetadataAndroid)(filePath)
|
|
236
|
+
: filePath?.endsWith('.zip')
|
|
237
|
+
? await (0, exports.extractAppMetadataIosZip)(filePath)
|
|
238
|
+
: await (0, exports.extractAppMetadataIos)(filePath);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
core_1.ux.warn('Failed to extract app metadata, please share with support@devicecloud.dev so we can improve our parsing.');
|
|
242
|
+
}
|
|
243
|
+
// this needs to made nicer by using envs or maybe fetching the keys from the getSignedURL call
|
|
244
|
+
const SB = {
|
|
245
|
+
dev: {
|
|
246
|
+
SUPABASE_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxibXNvd2VodGp3bnFsdXJwZW1iIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDkyMTg0ODcsImV4cCI6MjAyNDc5NDQ4N30.zeLTMAuZ_WwYvGdeP0kdvL_Zrs-RQee5APPyxmWq7qQ',
|
|
247
|
+
SUPABASE_URL: 'https://lbmsowehtjwnqlurpemb.supabase.co',
|
|
248
|
+
},
|
|
249
|
+
prod: {
|
|
250
|
+
SUPABASE_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBneWRucGhiaW1ldGluc2dma2JvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDc1OTQzNDYsImV4cCI6MjAyMzE3MDM0Nn0.hAYOMFxxwX1exkQkY9xyQJGC_GhGnyogkj2N-kBkMI8',
|
|
251
|
+
SUPABASE_URL: 'https://pgydnphbimetinsgfkbo.supabase.co',
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
const { SUPABASE_KEY, SUPABASE_URL } = SB[apiUrl === 'https://api.devicecloud.dev' ? 'prod' : 'dev'];
|
|
255
|
+
const supabase = (0, supabase_js_1.createClient)(SUPABASE_URL, SUPABASE_KEY);
|
|
256
|
+
const uploadToUrl = await supabase.storage
|
|
257
|
+
.from('organizations')
|
|
258
|
+
.uploadToSignedUrl(path, token, file);
|
|
259
|
+
if (uploadToUrl.error)
|
|
260
|
+
throw new Error(uploadToUrl.error);
|
|
261
|
+
const { error } = await (0, exports.typeSafePost)(apiUrl, '/uploads/finaliseUpload', {
|
|
262
|
+
body: JSON.stringify({ id, metadata, path }),
|
|
263
|
+
headers: {
|
|
264
|
+
'content-type': 'application/json',
|
|
265
|
+
'x-app-api-key': apiKey,
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
if (error)
|
|
269
|
+
throw new Error(error);
|
|
270
|
+
core_1.ux.action.stop(`\nBinary uploaded with id: ${id}`);
|
|
271
|
+
return id;
|
|
272
|
+
};
|
|
273
|
+
exports.uploadBinary = uploadBinary;
|
|
274
|
+
const uploadBinaries = async (finalAppFiles, apiUrl, apiKey) => Promise.all(finalAppFiles.map((f) => (0, exports.uploadBinary)(f, apiUrl, apiKey)));
|
|
275
|
+
exports.uploadBinaries = uploadBinaries;
|
|
276
|
+
const verifyAdditionalAppFiles = async (appFiles) => {
|
|
277
|
+
if (appFiles?.length) {
|
|
278
|
+
if (!appFiles.every((f) => ['apk', '.app', '.zip'].some((ext) => f.endsWith(ext)))) {
|
|
279
|
+
throw new Error('App file must be a .apk for android or .app/.zip file for iOS');
|
|
280
|
+
}
|
|
281
|
+
await Promise.all(appFiles.map(async (f) => {
|
|
282
|
+
if (f.endsWith('.zip')) {
|
|
283
|
+
await (0, exports.verifyAppZip)(f);
|
|
284
|
+
}
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
exports.verifyAdditionalAppFiles = verifyAdditionalAppFiles;
|
package/dist/plan.js
CHANGED
|
@@ -10,26 +10,24 @@ async function plan(input, includeTags, excludeTags, excludeFlows) {
|
|
|
10
10
|
throw new Error(`Flow path does not exist: ${path.resolve(input)}`);
|
|
11
11
|
}
|
|
12
12
|
if (fs.lstatSync(input).isFile()) {
|
|
13
|
-
const directory = path.dirname(input) + '/';
|
|
14
13
|
const { config, testSteps } = (0, planMethods_1.readTestYamlFileAsJson)(input);
|
|
15
14
|
const { allErrors, allFiles } = (0, planMethods_1.processDependencies)({
|
|
16
15
|
config,
|
|
17
|
-
directory,
|
|
18
16
|
input,
|
|
19
17
|
testSteps,
|
|
20
18
|
});
|
|
21
19
|
if (allErrors.length > 0) {
|
|
22
|
-
throw new Error('The following flow files are not present in the
|
|
20
|
+
throw new Error('The following flow files are not present in the filesystem: \n' +
|
|
23
21
|
allErrors.join('\n'));
|
|
24
22
|
}
|
|
25
23
|
return {
|
|
26
|
-
flowsToRun: [input
|
|
24
|
+
flowsToRun: [input],
|
|
27
25
|
referencedFiles: [...new Set(allFiles)],
|
|
28
26
|
totalFlowFiles: 1,
|
|
29
27
|
};
|
|
30
28
|
}
|
|
31
|
-
// raw list of all yaml files
|
|
32
|
-
let unfilteredFlowFiles = await (0, planMethods_1.
|
|
29
|
+
// raw list of all yaml files in workspace directory
|
|
30
|
+
let unfilteredFlowFiles = await (0, planMethods_1.readDirectory)(input, planMethods_1.isFlowFile);
|
|
33
31
|
if (unfilteredFlowFiles.length === 0) {
|
|
34
32
|
throw new Error(`Flow directory does not contain any Flow files: ${path.resolve(input)}`);
|
|
35
33
|
}
|
|
@@ -43,14 +41,9 @@ async function plan(input, includeTags, excludeTags, excludeFlows) {
|
|
|
43
41
|
const configFilePath = unfilteredFlowFiles.find((file) => file === input + 'config.yaml' || file === input + 'config.yml');
|
|
44
42
|
// remove all config files from the list to prevent processing them
|
|
45
43
|
unfilteredFlowFiles = unfilteredFlowFiles.filter((file) => !(file.endsWith('config.yaml') || file.endsWith('config.yml')));
|
|
46
|
-
const cleanPath = path.normalize(input);
|
|
47
|
-
const relativeFilePaths = unfilteredFlowFiles.map((file) => file.replace(cleanPath, ''));
|
|
48
|
-
const topLevelFlowFiles = relativeFilePaths.filter((file) => !file.includes('/'));
|
|
49
44
|
const workspaceConfig = configFilePath
|
|
50
45
|
? (0, planMethods_1.readYamlFileAsJson)(configFilePath)
|
|
51
46
|
: {};
|
|
52
|
-
// list of relative file paths from search
|
|
53
|
-
let unsortedFlowFiles = topLevelFlowFiles;
|
|
54
47
|
if (workspaceConfig.flows) {
|
|
55
48
|
const globs = workspaceConfig.flows.map((glob) => glob);
|
|
56
49
|
const matchedFiles = await (0, glob_1.glob)(globs, {
|
|
@@ -58,29 +51,26 @@ async function plan(input, includeTags, excludeTags, excludeFlows) {
|
|
|
58
51
|
nodir: true,
|
|
59
52
|
});
|
|
60
53
|
// overwrite the list of files with the globbed ones
|
|
61
|
-
|
|
62
|
-
(file
|
|
54
|
+
unfilteredFlowFiles = matchedFiles
|
|
55
|
+
.filter((file) => file !== 'config.yaml' &&
|
|
56
|
+
(file.endsWith('.yaml') || file.endsWith('.yml')))
|
|
57
|
+
.map((file) => path.resolve(input, file));
|
|
63
58
|
}
|
|
64
|
-
if (
|
|
59
|
+
if (unfilteredFlowFiles.length === 0) {
|
|
65
60
|
const error = workspaceConfig.flows
|
|
66
61
|
? new Error(`Flow inclusion pattern(s) did not match any Flow files:\n${workspaceConfig.flows.join('\n')}`)
|
|
67
|
-
: new Error(`
|
|
62
|
+
: new Error(`Workspace does not contain any Flows: ${path.resolve(input)}`);
|
|
68
63
|
throw error;
|
|
69
64
|
}
|
|
65
|
+
const { configPerFlowFile, testStepsPerFlowFile } =
|
|
70
66
|
// eslint-disable-next-line unicorn/no-array-reduce
|
|
71
|
-
|
|
72
|
-
const { config } = (0, planMethods_1.readTestYamlFileAsJson)(
|
|
67
|
+
unfilteredFlowFiles.reduce((acc, filePath) => {
|
|
68
|
+
const { config, testSteps } = (0, planMethods_1.readTestYamlFileAsJson)(filePath);
|
|
73
69
|
acc.configPerFlowFile[filePath] = config;
|
|
74
|
-
return acc;
|
|
75
|
-
}, {
|
|
76
|
-
configPerFlowFile: {},
|
|
77
|
-
});
|
|
78
|
-
// eslint-disable-next-line unicorn/no-array-reduce
|
|
79
|
-
const { testStepsPerFlowFile } = unfilteredFlowFiles.reduce((acc, filePath) => {
|
|
80
|
-
const { testSteps } = (0, planMethods_1.readTestYamlFileAsJson)(filePath);
|
|
81
70
|
acc.testStepsPerFlowFile[filePath] = testSteps;
|
|
82
71
|
return acc;
|
|
83
72
|
}, {
|
|
73
|
+
configPerFlowFile: {},
|
|
84
74
|
testStepsPerFlowFile: {},
|
|
85
75
|
});
|
|
86
76
|
let errors = [];
|
|
@@ -89,8 +79,7 @@ async function plan(input, includeTags, excludeTags, excludeFlows) {
|
|
|
89
79
|
if (!testSteps)
|
|
90
80
|
break;
|
|
91
81
|
const { allErrors: errs, allFiles: deps } = (0, planMethods_1.processDependencies)({
|
|
92
|
-
config: configPerFlowFile[
|
|
93
|
-
directory: cleanPath,
|
|
82
|
+
config: configPerFlowFile[filePath],
|
|
94
83
|
input: filePath,
|
|
95
84
|
testSteps,
|
|
96
85
|
});
|
|
@@ -109,7 +98,7 @@ async function plan(input, includeTags, excludeTags, excludeFlows) {
|
|
|
109
98
|
...excludeTags,
|
|
110
99
|
...(workspaceConfig.excludeTags || []),
|
|
111
100
|
];
|
|
112
|
-
const allFlows =
|
|
101
|
+
const allFlows = unfilteredFlowFiles.filter((filePath) => {
|
|
113
102
|
const config = configPerFlowFile[filePath];
|
|
114
103
|
const tags = config?.tags || [];
|
|
115
104
|
return ((allIncludeTags.length === 0 ||
|
package/dist/planMethods.d.ts
CHANGED
|
@@ -10,18 +10,17 @@ export declare const readTestYamlFileAsJson: (filePath: string) => {
|
|
|
10
10
|
config: null;
|
|
11
11
|
testSteps: Record<string, unknown>[];
|
|
12
12
|
};
|
|
13
|
-
export declare function
|
|
14
|
-
export declare const checkIfFilesExistInWorkspace: (commandName: string, command: Record<string, string> | string | string[],
|
|
13
|
+
export declare function readDirectory(dir: string, filterFunction?: (filePath: string) => boolean): Promise<string[]>;
|
|
14
|
+
export declare const checkIfFilesExistInWorkspace: (commandName: string, command: Record<string, string> | string | string[], absoluteFilePath: string) => {
|
|
15
15
|
errors: string[];
|
|
16
16
|
files: string[];
|
|
17
17
|
};
|
|
18
18
|
interface IProcessDependencies {
|
|
19
19
|
config?: Record<string, unknown> | null;
|
|
20
|
-
directory: string;
|
|
21
20
|
input: string;
|
|
22
21
|
testSteps: Record<string, unknown>[];
|
|
23
22
|
}
|
|
24
|
-
export declare const processDependencies: ({ config,
|
|
23
|
+
export declare const processDependencies: ({ config, input, testSteps, }: IProcessDependencies) => {
|
|
25
24
|
allErrors: string[];
|
|
26
25
|
allFiles: string[];
|
|
27
26
|
};
|
package/dist/planMethods.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.processDependencies = exports.checkIfFilesExistInWorkspace = exports.
|
|
3
|
+
exports.processDependencies = exports.checkIfFilesExistInWorkspace = exports.readDirectory = exports.readTestYamlFileAsJson = exports.readYamlFileAsJson = exports.isFlowFile = exports.getFlowsToRunInSequence = void 0;
|
|
4
4
|
/* eslint-disable unicorn/filename-case */
|
|
5
5
|
const yaml = require("js-yaml");
|
|
6
6
|
const fs = require("node:fs");
|
|
@@ -55,13 +55,11 @@ const readTestYamlFileAsJson = (filePath) => {
|
|
|
55
55
|
return { config: null, testSteps };
|
|
56
56
|
};
|
|
57
57
|
exports.readTestYamlFileAsJson = readTestYamlFileAsJson;
|
|
58
|
-
async function
|
|
58
|
+
async function readDirectory(dir, filterFunction) {
|
|
59
59
|
const readDirResult = await fs.promises.readdir(dir);
|
|
60
60
|
const files = await Promise.all(readDirResult.map(async (file) => {
|
|
61
61
|
const filePath = path.join(dir, file);
|
|
62
62
|
const stats = await fs.promises.stat(filePath);
|
|
63
|
-
if (stats.isDirectory())
|
|
64
|
-
return walk(filePath, filterFunction);
|
|
65
63
|
if (stats.isFile())
|
|
66
64
|
if (filterFunction) {
|
|
67
65
|
if (filterFunction(filePath))
|
|
@@ -73,18 +71,18 @@ async function walk(dir, filterFunction) {
|
|
|
73
71
|
}));
|
|
74
72
|
return files.flat().filter(Boolean);
|
|
75
73
|
}
|
|
76
|
-
exports.
|
|
77
|
-
const checkIfFilesExistInWorkspace = (commandName, command,
|
|
74
|
+
exports.readDirectory = readDirectory;
|
|
75
|
+
const checkIfFilesExistInWorkspace = (commandName, command, absoluteFilePath) => {
|
|
78
76
|
const errors = [];
|
|
79
77
|
const files = [];
|
|
80
|
-
const directory = path.dirname(
|
|
81
|
-
const buildError = (error) => `Flow file "${
|
|
78
|
+
const directory = path.dirname(absoluteFilePath);
|
|
79
|
+
const buildError = (error) => `Flow file "${absoluteFilePath}" has a command "${commandName}" that references a ${error} ${JSON.stringify(command)}`;
|
|
82
80
|
const processFilePath = (relativePath) => {
|
|
83
81
|
const absoluteFilePath = path.resolve(directory, relativePath);
|
|
84
|
-
const error = checkFile(absoluteFilePath
|
|
82
|
+
const error = checkFile(absoluteFilePath);
|
|
85
83
|
if (error)
|
|
86
84
|
errors.push(buildError(error));
|
|
87
|
-
files.push(absoluteFilePath
|
|
85
|
+
files.push(absoluteFilePath);
|
|
88
86
|
};
|
|
89
87
|
// simple command
|
|
90
88
|
if (typeof command === 'string')
|
|
@@ -102,13 +100,11 @@ const checkIfFilesExistInWorkspace = (commandName, command, filePath, cleanPath)
|
|
|
102
100
|
return { errors, files };
|
|
103
101
|
};
|
|
104
102
|
exports.checkIfFilesExistInWorkspace = checkIfFilesExistInWorkspace;
|
|
105
|
-
const checkFile = (filePath
|
|
103
|
+
const checkFile = (filePath) => {
|
|
106
104
|
if (!fs.existsSync(filePath))
|
|
107
105
|
return `non-existent file`;
|
|
108
|
-
if (!filePath.startsWith(cleanPath))
|
|
109
|
-
return `file outside the workspace`;
|
|
110
106
|
};
|
|
111
|
-
const checkStepsArray = (steps,
|
|
107
|
+
const checkStepsArray = (steps, absoluteFilePath) => {
|
|
112
108
|
let errors = [];
|
|
113
109
|
let files = [];
|
|
114
110
|
for (const command of steps) {
|
|
@@ -116,14 +112,14 @@ const checkStepsArray = (steps, input, directory) => {
|
|
|
116
112
|
continue;
|
|
117
113
|
for (const [commandName, commandValue] of Object.entries(command)) {
|
|
118
114
|
if (commandsThatRequireFiles.has(commandName)) {
|
|
119
|
-
const { errors: newErrors, files: newFiles } = (0, exports.checkIfFilesExistInWorkspace)(commandName, commandValue, path.normalize(
|
|
115
|
+
const { errors: newErrors, files: newFiles } = (0, exports.checkIfFilesExistInWorkspace)(commandName, commandValue, path.normalize(absoluteFilePath));
|
|
120
116
|
errors = [...errors, ...newErrors];
|
|
121
117
|
files = [...files, ...newFiles];
|
|
122
118
|
}
|
|
123
119
|
const nestedCommands = typeof commandValue === 'object' &&
|
|
124
120
|
commandValue.commands;
|
|
125
121
|
if (nestedCommands) {
|
|
126
|
-
const { errors: newErrors, files: newFiles } = checkStepsArray(nestedCommands,
|
|
122
|
+
const { errors: newErrors, files: newFiles } = checkStepsArray(nestedCommands, absoluteFilePath);
|
|
127
123
|
errors = [...errors, ...newErrors];
|
|
128
124
|
files = [...files, ...newFiles];
|
|
129
125
|
}
|
|
@@ -131,7 +127,7 @@ const checkStepsArray = (steps, input, directory) => {
|
|
|
131
127
|
}
|
|
132
128
|
return { errors, files };
|
|
133
129
|
};
|
|
134
|
-
const processDependencies = ({ config,
|
|
130
|
+
const processDependencies = ({ config, input, testSteps, }) => {
|
|
135
131
|
let allErrors = [];
|
|
136
132
|
let allFiles = [];
|
|
137
133
|
const { onFlowComplete, onFlowStart } = config ?? {};
|
|
@@ -142,7 +138,7 @@ const processDependencies = ({ config, directory, input, testSteps, }) => {
|
|
|
142
138
|
stepsArray.push(onFlowComplete);
|
|
143
139
|
for (const [index, steps] of stepsArray.entries()) {
|
|
144
140
|
try {
|
|
145
|
-
const { errors, files } = checkStepsArray(steps, input
|
|
141
|
+
const { errors, files } = checkStepsArray(steps, input);
|
|
146
142
|
allErrors = [...allErrors, ...errors];
|
|
147
143
|
allFiles = [...allFiles, ...files];
|
|
148
144
|
}
|
package/oclif.manifest.json
CHANGED
|
@@ -19,6 +19,22 @@
|
|
|
19
19
|
"<%= config.bin %> <%= command.id %>"
|
|
20
20
|
],
|
|
21
21
|
"flags": {
|
|
22
|
+
"additional-app-binary-ids": {
|
|
23
|
+
"description": "The ID of the additional app binary(s) previously uploaded to devicecloud.dev to install before execution",
|
|
24
|
+
"name": "additional-app-binary-ids",
|
|
25
|
+
"default": [],
|
|
26
|
+
"hasDynamicHelp": false,
|
|
27
|
+
"multiple": true,
|
|
28
|
+
"type": "option"
|
|
29
|
+
},
|
|
30
|
+
"additional-app-files": {
|
|
31
|
+
"description": "Additional app binary(s) to install before execution",
|
|
32
|
+
"name": "additional-app-files",
|
|
33
|
+
"default": [],
|
|
34
|
+
"hasDynamicHelp": false,
|
|
35
|
+
"multiple": true,
|
|
36
|
+
"type": "option"
|
|
37
|
+
},
|
|
22
38
|
"android-api-level": {
|
|
23
39
|
"description": "[Android only] Android API level to run your flow against",
|
|
24
40
|
"name": "android-api-level",
|
|
@@ -106,6 +122,17 @@
|
|
|
106
122
|
"multiple": false,
|
|
107
123
|
"type": "option"
|
|
108
124
|
},
|
|
125
|
+
"download-artifacts": {
|
|
126
|
+
"description": "BETA (API may change) - download a zip containing the logs, screenshots and videos for each result in this run. You will debited a $0.01 egress fee for each result. Use --download-artifacts=FAILED for failures only or --download-artifacts=ALL for every result.",
|
|
127
|
+
"name": "download-artifacts",
|
|
128
|
+
"hasDynamicHelp": false,
|
|
129
|
+
"multiple": false,
|
|
130
|
+
"options": [
|
|
131
|
+
"ALL",
|
|
132
|
+
"FAILED"
|
|
133
|
+
],
|
|
134
|
+
"type": "option"
|
|
135
|
+
},
|
|
109
136
|
"env": {
|
|
110
137
|
"char": "e",
|
|
111
138
|
"description": "One or more environment variables to inject into your flows",
|
|
@@ -166,12 +193,7 @@
|
|
|
166
193
|
"hasDynamicHelp": false,
|
|
167
194
|
"multiple": false,
|
|
168
195
|
"options": [
|
|
169
|
-
"iphone-12",
|
|
170
|
-
"iphone-12-mini",
|
|
171
|
-
"iphone-12-pro-max",
|
|
172
196
|
"iphone-13",
|
|
173
|
-
"iphone-13-mini",
|
|
174
|
-
"iphone-13-pro-max",
|
|
175
197
|
"iphone-14",
|
|
176
198
|
"iphone-14-plus",
|
|
177
199
|
"iphone-14-pro",
|
|
@@ -201,12 +223,6 @@
|
|
|
201
223
|
],
|
|
202
224
|
"type": "option"
|
|
203
225
|
},
|
|
204
|
-
"legacy-upload": {
|
|
205
|
-
"description": "Use the legacy direct upload method",
|
|
206
|
-
"name": "legacy-upload",
|
|
207
|
-
"allowNo": false,
|
|
208
|
-
"type": "boolean"
|
|
209
|
-
},
|
|
210
226
|
"maestro-version": {
|
|
211
227
|
"aliases": [
|
|
212
228
|
"maestroVersion"
|
|
@@ -275,5 +291,5 @@
|
|
|
275
291
|
]
|
|
276
292
|
}
|
|
277
293
|
},
|
|
278
|
-
"version": "
|
|
294
|
+
"version": "2.0.0-rc.2"
|
|
279
295
|
}
|