@devicecloud.dev/dcd 1.0.11 → 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,4 +1,5 @@
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
5
  'iphone-13' = "iphone-13",
@@ -23,6 +24,8 @@ export default class Cloud extends Command {
23
24
  static description: string;
24
25
  static examples: string[];
25
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>;
26
29
  'android-api-level': import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
27
30
  'android-device': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
28
31
  apiKey: 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',
@@ -58,16 +55,20 @@ class Cloud extends core_1.Command {
58
55
  }
59
56
  await (0, methods_1.versionCheck)(this.config.version);
60
57
  const { args, flags, raw } = await this.parse(Cloud);
61
- const { '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;
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;
62
59
  if (arm64) {
63
60
  (0, cli_ux_1.info)('Contact hello@devicecloud.dev to enquire about arm64 devices');
64
61
  (0, cli_ux_1.exit)();
65
62
  }
63
+ if (!apiKey)
64
+ throw new Error('You must provide an API key');
65
+ const additionalAppBinaryIds = nonFlatAdditionalAppBinaryIds?.flat();
66
+ const additionalAppFiles = nonFlatAdditionalAppFiles?.flat();
66
67
  const { firstFile, secondFile } = args;
67
68
  let finalBinaryId = appBinaryId;
69
+ let finalAdditionalBinaryIds = additionalAppBinaryIds;
68
70
  const finalAppFile = appFile ?? firstFile;
69
71
  let flowFile = flows ?? secondFile;
70
- let metadata;
71
72
  if (appBinaryId) {
72
73
  if (secondFile) {
73
74
  throw new Error('You cannot provide both an appBinaryId and a binary file');
@@ -100,6 +101,18 @@ class Cloud extends core_1.Command {
100
101
  process.exit(2);
101
102
  }
102
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
+ }
103
116
  const { continueOnFailure = true, flows: sequentialFlows = [] } = sequence ?? {};
104
117
  if (!appBinaryId) {
105
118
  if (!(flowFile && finalAppFile)) {
@@ -112,6 +125,7 @@ class Cloud extends core_1.Command {
112
125
  await (0, methods_1.verifyAppZip)(finalAppFile);
113
126
  }
114
127
  }
128
+ await (0, methods_1.verifyAdditionalAppFiles)(additionalAppFiles);
115
129
  const flagLogs = [];
116
130
  for (const [k, v] of Object.entries(flags)) {
117
131
  if (v && v.toString().length > 0) {
@@ -123,6 +137,8 @@ class Cloud extends core_1.Command {
123
137
  Submitting new job
124
138
  → Flow(s): ${flowFile}
125
139
  → App: ${appBinaryId || finalAppFile}
140
+ ${Boolean(additionalAppBinaryIds || additionalAppFiles) &&
141
+ `→ Additional app(s): ${additionalAppBinaryIds} ${additionalAppFiles}`}
126
142
 
127
143
  With options
128
144
  → ${flagLogs.join(`
@@ -130,69 +146,18 @@ class Cloud extends core_1.Command {
130
146
 
131
147
  `);
132
148
  if (!finalBinaryId) {
133
- core_1.ux.action.start('Uploading binary', 'Initializing', { stdout: true });
134
- const { id, message, path, token } = await (0, methods_1.typeSafePost)(apiUrl, '/uploads/getBinaryUploadUrl', {
135
- body: JSON.stringify({
136
- platform: finalAppFile?.endsWith('.apk') ? 'android' : 'ios',
137
- }),
138
- headers: {
139
- 'content-type': 'application/json',
140
- 'x-app-api-key': apiKey,
141
- },
142
- });
143
- finalBinaryId = id;
144
- if (!path)
145
- throw new Error(message);
146
- let file;
147
- if (finalAppFile?.endsWith('.app')) {
148
- const zippedAppBlob = await (0, methods_1.compressFolderToBlob)(finalAppFile);
149
- const filePath = finalAppFile + '.zip';
150
- file = new file_1.File([zippedAppBlob], filePath);
151
- }
152
- else {
153
- const binaryBlob = new Blob([await (0, promises_1.readFile)(finalAppFile)], {
154
- type: mimeTypeLookupByExtension[finalAppFile.split('.').pop()],
155
- });
156
- file = new file_1.File([binaryBlob], finalAppFile);
157
- }
158
- try {
159
- metadata = finalAppFile?.endsWith('.apk')
160
- ? await (0, methods_1.extractAppMetadataAndroid)(finalAppFile)
161
- : finalAppFile?.endsWith('.zip')
162
- ? await (0, methods_1.extractAppMetadataIosZip)(finalAppFile)
163
- : await (0, methods_1.extractAppMetadataIos)(finalAppFile);
164
- }
165
- catch {
166
- this.warn('Failed to extract app metadata, please share with support@devicecloud.dev so we can improve our parsing.');
167
- }
168
- // this needs to made nicer by using envs or maybe fetching the keys from the getSignedURL call
169
- const SB = {
170
- dev: {
171
- SUPABASE_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxibXNvd2VodGp3bnFsdXJwZW1iIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDkyMTg0ODcsImV4cCI6MjAyNDc5NDQ4N30.zeLTMAuZ_WwYvGdeP0kdvL_Zrs-RQee5APPyxmWq7qQ',
172
- SUPABASE_URL: 'https://lbmsowehtjwnqlurpemb.supabase.co',
173
- },
174
- prod: {
175
- SUPABASE_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBneWRucGhiaW1ldGluc2dma2JvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDc1OTQzNDYsImV4cCI6MjAyMzE3MDM0Nn0.hAYOMFxxwX1exkQkY9xyQJGC_GhGnyogkj2N-kBkMI8',
176
- SUPABASE_URL: 'https://pgydnphbimetinsgfkbo.supabase.co',
177
- },
178
- };
179
- const { SUPABASE_KEY, SUPABASE_URL } = SB[apiUrl === 'https://api.devicecloud.dev' ? 'prod' : 'dev'];
180
- const supabase = (0, supabase_js_1.createClient)(SUPABASE_URL, SUPABASE_KEY);
181
- const uploadToUrl = await supabase.storage
182
- .from('organizations')
183
- .uploadToSignedUrl(path, token, file);
184
- if (uploadToUrl.error)
185
- throw new Error(uploadToUrl.error);
186
- const { error } = await (0, methods_1.typeSafePost)(apiUrl, '/uploads/finaliseUpload', {
187
- body: JSON.stringify({ id, metadata, path }),
188
- headers: {
189
- 'content-type': 'application/json',
190
- 'x-app-api-key': apiKey,
191
- },
192
- });
193
- if (error)
194
- throw new Error(error);
195
- 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
+ ];
196
161
  }
197
162
  const testFormData = new FormData();
198
163
  // eslint-disable-next-line unicorn/no-array-reduce
@@ -210,17 +175,18 @@ class Cloud extends core_1.Command {
210
175
  ...testFileNames,
211
176
  ...sequentialFlows,
212
177
  ]),
213
- ]);
178
+ ], commonRoot);
214
179
  const blob = new Blob([buffer], {
215
- type: mimeTypeLookupByExtension.zip,
180
+ type: exports.mimeTypeLookupByExtension.zip,
216
181
  });
217
182
  testFormData.set('file', blob, 'flowFile.zip');
218
183
  testFormData.set('appBinaryId', finalBinaryId);
219
- testFormData.set('testFileNames', JSON.stringify(testFileNames));
184
+ testFormData.set('testFileNames', JSON.stringify(testFileNames.map((t) => t.replaceAll(commonRoot, '.'))));
220
185
  testFormData.set('sequentialFlows', JSON.stringify(sequentialFlows));
221
186
  testFormData.set('env', JSON.stringify(envObject));
222
187
  testFormData.set('googlePlay', googlePlay ? 'true' : 'false');
223
188
  testFormData.set('config', JSON.stringify({
189
+ additionalAppBinaryIds: finalAdditionalBinaryIds,
224
190
  allExcludeTags,
225
191
  allIncludeTags,
226
192
  continueOnFailure,
@@ -228,6 +194,7 @@ class Cloud extends core_1.Command {
228
194
  maestroVersion,
229
195
  orientation,
230
196
  raw,
197
+ uploadedBinaryIds,
231
198
  version: this.config.version,
232
199
  }));
233
200
  if (androidApiLevel)
@@ -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>;
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'],
package/dist/methods.d.ts CHANGED
@@ -18,8 +18,11 @@ export declare const typeSafeGet: <T extends keyof paths>(baseUrl: string, path:
18
18
  export declare const toBuffer: (archive: archiver.Archiver) => Promise<Buffer>;
19
19
  export declare const compressDir: (sourceDir: string) => Promise<Buffer>;
20
20
  export declare const compressFolderToBlob: (sourceDir: string) => Promise<Blob>;
21
- export declare const compressFilesFromRelativePath: (path: string, files: string[]) => Promise<Buffer>;
21
+ export declare const compressFilesFromRelativePath: (path: string, files: string[], commonRoot: string) => Promise<Buffer>;
22
22
  export declare const verifyAppZip: (zipPath: string) => Promise<void>;
23
23
  export declare const extractAppMetadataAndroid: (appFilePath: string) => Promise<TAppMetadata>;
24
24
  export declare const extractAppMetadataIosZip: (appFilePath: string) => Promise<TAppMetadata>;
25
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,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- 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 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");
@@ -14,6 +16,7 @@ const node_stream_1 = require("node:stream");
14
16
  const promises_2 = require("node:stream/promises");
15
17
  const StreamZip = require("node-stream-zip");
16
18
  const plist_1 = require("plist");
19
+ const cloud_1 = require("./commands/cloud");
17
20
  const PERMITTED_EXTENSIONS = new Set([
18
21
  'yml',
19
22
  'yaml',
@@ -29,7 +32,7 @@ const versionCheck = async (currentVersion) => {
29
32
  const versionResponseJson = await versionResponse.json();
30
33
  const latestVersion = versionResponseJson.version;
31
34
  if (latestVersion !== currentVersion) {
32
- (0, cli_ux_1.warn)(`
35
+ core_1.ux.warn(`
33
36
  -------------------
34
37
  A new version of the devicecloud.dev CLI is available: ${latestVersion}
35
38
  Run 'npm install -g @devicecloud.dev/dcd@latest' to update to the latest version
@@ -118,7 +121,7 @@ const compressFolderToBlob = async (sourceDir) => {
118
121
  return new Blob([buffer], { type: 'application/zip' });
119
122
  };
120
123
  exports.compressFolderToBlob = compressFolderToBlob;
121
- const compressFilesFromRelativePath = async (path, files) => {
124
+ const compressFilesFromRelativePath = async (path, files, commonRoot) => {
122
125
  const archive = archiver('zip', {
123
126
  zlib: { level: 9 },
124
127
  });
@@ -126,7 +129,9 @@ const compressFilesFromRelativePath = async (path, files) => {
126
129
  throw err;
127
130
  });
128
131
  for (const file of files) {
129
- archive.file(nodePath.resolve(path, file), { name: file });
132
+ archive.file(nodePath.resolve(path, file), {
133
+ name: file.replace(commonRoot, ''),
134
+ });
130
135
  }
131
136
  const buffer = await (0, exports.toBuffer)(archive);
132
137
  // await writeFile('./my-zip.zip', buffer);
@@ -200,3 +205,84 @@ const extractAppMetadataIos = async (appFolderPath) => {
200
205
  return { appId, platform: 'ios' };
201
206
  };
202
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",
@@ -275,5 +291,5 @@
275
291
  ]
276
292
  }
277
293
  },
278
- "version": "1.0.11"
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.11",
83
+ "version": "2.0.0-rc.2",
84
84
  "bugs": {
85
85
  "url": "https://discord.gg/gm3mJwcNw8"
86
86
  },