@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.
@@ -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>;
@@ -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
- const mimeTypeLookupByExtension = {
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, 'legacy-upload': legacyUpload, 'maestro-version': maestroVersion, name, orientation, quiet, ...rest } = flags;
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 finalAppFile = appFile ?? firstFile;
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
- core_1.ux.action.start('Uploading binary', 'Initializing', { stdout: true });
139
- if (legacyUpload) {
140
- const binaryFormData = new FormData();
141
- if (finalAppFile?.endsWith('.app')) {
142
- const zippedAppBlob = await (0, methods_1.compressFolderToBlob)(finalAppFile);
143
- finalAppFile += '.zip';
144
- binaryFormData.set('file', zippedAppBlob, finalAppFile);
145
- }
146
- else {
147
- const binaryBlob = new Blob([await (0, promises_1.readFile)(finalAppFile)], {
148
- type: mimeTypeLookupByExtension[finalAppFile.split('.').pop()],
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);
@@ -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-12': ['15', '16', '17'],
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 extractAppMetadataIos: (appFilePath: string) => Promise<TAppMetadata>;
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 cli_ux_1 = require("@oclif/core/lib/cli-ux");
5
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
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
- // import { writeFile } from 'node:fs/promises';
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
- (0, cli_ux_1.warn)(`
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), { name: 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 extractAppMetadataIos = async (appFilePath) => new Promise((resolve, reject) => {
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
- let data;
151
- const bufferType = buffer[0];
152
- if (bufferType === 60 ||
153
- bufferType === '<' ||
154
- bufferType === 239) {
155
- data = (0, plist_1.parse)(buffer.toString());
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 provided directory: \n' +
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.split('/').pop() ?? 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 beneath entry point
32
- let unfilteredFlowFiles = await (0, planMethods_1.walk)(input, planMethods_1.isFlowFile);
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
- unsortedFlowFiles = matchedFiles.filter((file) => file !== 'config.yaml' &&
62
- (file.endsWith('.yaml') || file.endsWith('.yml')));
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 (unsortedFlowFiles.length === 0) {
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(`Top-level directory does not contain any Flows: ${path.resolve(input)}`);
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
- const { configPerFlowFile } = unsortedFlowFiles.reduce((acc, filePath) => {
72
- const { config } = (0, planMethods_1.readTestYamlFileAsJson)(cleanPath + filePath);
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[path.relative(cleanPath, filePath)],
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 = unsortedFlowFiles.filter((filePath) => {
101
+ const allFlows = unfilteredFlowFiles.filter((filePath) => {
113
102
  const config = configPerFlowFile[filePath];
114
103
  const tags = config?.tags || [];
115
104
  return ((allIncludeTags.length === 0 ||
@@ -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 walk(dir: string, filterFunction?: (filePath: string) => boolean): Promise<string[]>;
14
- export declare const checkIfFilesExistInWorkspace: (commandName: string, command: Record<string, string> | string | string[], filePath: string, cleanPath: 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, directory, input, testSteps, }: IProcessDependencies) => {
23
+ export declare const processDependencies: ({ config, input, testSteps, }: IProcessDependencies) => {
25
24
  allErrors: string[];
26
25
  allFiles: string[];
27
26
  };
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.processDependencies = exports.checkIfFilesExistInWorkspace = exports.walk = exports.readTestYamlFileAsJson = exports.readYamlFileAsJson = exports.isFlowFile = exports.getFlowsToRunInSequence = void 0;
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 walk(dir, filterFunction) {
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.walk = walk;
77
- const checkIfFilesExistInWorkspace = (commandName, command, filePath, cleanPath) => {
74
+ exports.readDirectory = readDirectory;
75
+ const checkIfFilesExistInWorkspace = (commandName, command, absoluteFilePath) => {
78
76
  const errors = [];
79
77
  const files = [];
80
- const directory = path.dirname(filePath);
81
- const buildError = (error) => `Flow file "${filePath.replace(cleanPath, './')}" has a command "${commandName}" that references a ${error} ${JSON.stringify(command)}`;
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, cleanPath);
82
+ const error = checkFile(absoluteFilePath);
85
83
  if (error)
86
84
  errors.push(buildError(error));
87
- files.push(absoluteFilePath.replace(cleanPath, './'));
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, cleanPath) => {
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, input, directory) => {
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(input), directory);
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, input, directory);
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, directory, input, testSteps, }) => {
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, directory);
141
+ const { errors, files } = checkStepsArray(steps, input);
146
142
  allErrors = [...allErrors, ...errors];
147
143
  allFiles = [...allFiles, ...files];
148
144
  }
@@ -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": "1.0.10"
294
+ "version": "2.0.0-rc.2"
279
295
  }
package/package.json CHANGED
@@ -80,7 +80,7 @@
80
80
  "test": "mocha --forbid-only \"test/**/*.test.ts\"",
81
81
  "version": "oclif readme && git add README.md"
82
82
  },
83
- "version": "1.0.10",
83
+ "version": "2.0.0-rc.2",
84
84
  "bugs": {
85
85
  "url": "https://discord.gg/gm3mJwcNw8"
86
86
  },