@devicecloud.dev/dcd 1.0.11 → 2.0.0-rc.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.run = void 0;
4
3
  var core_1 = require("@oclif/core");
5
4
  Object.defineProperty(exports, "run", { enumerable: true, get: function () { return core_1.run; } });
package/dist/methods.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- /// <reference types="node" />
2
1
  import * as archiver from 'archiver';
3
2
  import { paths } from '../../api/schema.types';
4
3
  import { TAppMetadata } from './types';
@@ -18,8 +17,11 @@ export declare const typeSafeGet: <T extends keyof paths>(baseUrl: string, path:
18
17
  export declare const toBuffer: (archive: archiver.Archiver) => Promise<Buffer>;
19
18
  export declare const compressDir: (sourceDir: string) => Promise<Buffer>;
20
19
  export declare const compressFolderToBlob: (sourceDir: string) => Promise<Blob>;
21
- export declare const compressFilesFromRelativePath: (path: string, files: string[]) => Promise<Buffer>;
20
+ export declare const compressFilesFromRelativePath: (path: string, files: string[], commonRoot: string) => Promise<Buffer>;
22
21
  export declare const verifyAppZip: (zipPath: string) => Promise<void>;
23
22
  export declare const extractAppMetadataAndroid: (appFilePath: string) => Promise<TAppMetadata>;
24
23
  export declare const extractAppMetadataIosZip: (appFilePath: string) => Promise<TAppMetadata>;
25
24
  export declare const extractAppMetadataIos: (appFolderPath: string) => Promise<TAppMetadata>;
25
+ export declare const uploadBinary: (filePath: string, apiUrl: string, apiKey: string) => Promise<string>;
26
+ export declare const uploadBinaries: (finalAppFiles: string[], apiUrl: string, apiKey: string) => Promise<string[]>;
27
+ 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
@@ -1,56 +1,70 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.plan = void 0;
3
+ exports.plan = plan;
4
4
  const glob_1 = require("glob");
5
5
  const fs = require("node:fs");
6
6
  const path = require("node:path");
7
7
  const planMethods_1 = require("./planMethods");
8
- async function plan(input, includeTags, excludeTags, excludeFlows) {
9
- if (!fs.existsSync(input)) {
10
- throw new Error(`Flow path does not exist: ${path.resolve(input)}`);
11
- }
12
- if (fs.lstatSync(input).isFile()) {
13
- const directory = path.dirname(input) + '/';
14
- const { config, testSteps } = (0, planMethods_1.readTestYamlFileAsJson)(input);
8
+ async function checkDependencies(input) {
9
+ const checkedDependencies = [];
10
+ const uncheckedDependencies = [input];
11
+ while (uncheckedDependencies.length > 0) {
12
+ const fileToCheck = uncheckedDependencies.shift();
13
+ const { config, testSteps } = (0, planMethods_1.readTestYamlFileAsJson)(fileToCheck);
15
14
  const { allErrors, allFiles } = (0, planMethods_1.processDependencies)({
16
15
  config,
17
- directory,
18
- input,
16
+ input: fileToCheck,
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
  }
23
+ for (const file of allFiles) {
24
+ if (!(0, planMethods_1.isFlowFile)(file)) {
25
+ // js/media files don't have dependencies
26
+ checkedDependencies.push(file);
27
+ }
28
+ if (!checkedDependencies.includes(file)) {
29
+ uncheckedDependencies.push(file);
30
+ }
31
+ }
32
+ if (!checkedDependencies.includes(fileToCheck)) {
33
+ checkedDependencies.push(fileToCheck);
34
+ }
35
+ }
36
+ return checkedDependencies;
37
+ }
38
+ function filterFlowFiles(unfilteredFlowFiles, excludeFlows) {
39
+ if (excludeFlows) {
40
+ return unfilteredFlowFiles.filter((file) => !excludeFlows.some((flow) => file.startsWith(path.resolve(flow))));
41
+ }
42
+ return unfilteredFlowFiles;
43
+ }
44
+ function getWorkspaceConfig(input, unfilteredFlowFiles) {
45
+ const configFilePath = unfilteredFlowFiles.find((file) => file === input + 'config.yaml' || file === input + 'config.yml');
46
+ return configFilePath
47
+ ? (0, planMethods_1.readYamlFileAsJson)(configFilePath)
48
+ : {};
49
+ }
50
+ async function plan(input, includeTags, excludeTags, excludeFlows) {
51
+ if (!fs.existsSync(input)) {
52
+ throw new Error(`Flow path does not exist: ${path.resolve(input)}`);
53
+ }
54
+ if (fs.lstatSync(input).isFile()) {
55
+ const checkedDependancies = await checkDependencies(input);
25
56
  return {
26
- flowsToRun: [input.split('/').pop() ?? input],
27
- referencedFiles: [...new Set(allFiles)],
57
+ flowsToRun: [input],
58
+ referencedFiles: [...new Set(checkedDependancies)],
28
59
  totalFlowFiles: 1,
29
60
  };
30
61
  }
31
- // raw list of all yaml files beneath entry point
32
- let unfilteredFlowFiles = await (0, planMethods_1.walk)(input, planMethods_1.isFlowFile);
62
+ let unfilteredFlowFiles = await (0, planMethods_1.readDirectory)(input, planMethods_1.isFlowFile);
33
63
  if (unfilteredFlowFiles.length === 0) {
34
64
  throw new Error(`Flow directory does not contain any Flow files: ${path.resolve(input)}`);
35
65
  }
36
- // filter out files that match the exclude list
37
- if (excludeFlows) {
38
- for (const flow of excludeFlows) {
39
- unfilteredFlowFiles = unfilteredFlowFiles.filter((file) => !file.startsWith(path.resolve(flow)));
40
- }
41
- }
42
- // if config file present at top level, use it
43
- const configFilePath = unfilteredFlowFiles.find((file) => file === input + 'config.yaml' || file === input + 'config.yml');
44
- // remove all config files from the list to prevent processing them
45
- 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
- const workspaceConfig = configFilePath
50
- ? (0, planMethods_1.readYamlFileAsJson)(configFilePath)
51
- : {};
52
- // list of relative file paths from search
53
- let unsortedFlowFiles = topLevelFlowFiles;
66
+ unfilteredFlowFiles = filterFlowFiles(unfilteredFlowFiles, excludeFlows);
67
+ const workspaceConfig = getWorkspaceConfig(input, unfilteredFlowFiles);
54
68
  if (workspaceConfig.flows) {
55
69
  const globs = workspaceConfig.flows.map((glob) => glob);
56
70
  const matchedFiles = await (0, glob_1.glob)(globs, {
@@ -58,49 +72,25 @@ async function plan(input, includeTags, excludeTags, excludeFlows) {
58
72
  nodir: true,
59
73
  });
60
74
  // overwrite the list of files with the globbed ones
61
- unsortedFlowFiles = matchedFiles.filter((file) => file !== 'config.yaml' &&
62
- (file.endsWith('.yaml') || file.endsWith('.yml')));
75
+ unfilteredFlowFiles = matchedFiles
76
+ .filter((file) => file !== 'config.yaml' &&
77
+ (file.endsWith('.yaml') || file.endsWith('.yml')))
78
+ .map((file) => path.resolve(input, file));
63
79
  }
64
- if (unsortedFlowFiles.length === 0) {
80
+ if (unfilteredFlowFiles.length === 0) {
65
81
  const error = workspaceConfig.flows
66
82
  ? 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)}`);
83
+ : new Error(`Workspace does not contain any Flows: ${path.resolve(input)}`);
68
84
  throw error;
69
85
  }
86
+ const configPerFlowFile =
70
87
  // eslint-disable-next-line unicorn/no-array-reduce
71
- const { configPerFlowFile } = unsortedFlowFiles.reduce((acc, filePath) => {
72
- const { config } = (0, planMethods_1.readTestYamlFileAsJson)(cleanPath + filePath);
73
- acc.configPerFlowFile[filePath] = config;
88
+ unfilteredFlowFiles.reduce((acc, filePath) => {
89
+ const { config } = (0, planMethods_1.readTestYamlFileAsJson)(filePath);
90
+ acc[filePath] = config;
74
91
  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
- acc.testStepsPerFlowFile[filePath] = testSteps;
82
- return acc;
83
- }, {
84
- testStepsPerFlowFile: {},
85
- });
86
- let errors = [];
87
- let allFiles = [];
88
- for (const [filePath, testSteps] of Object.entries(testStepsPerFlowFile)) {
89
- if (!testSteps)
90
- break;
91
- const { allErrors: errs, allFiles: deps } = (0, planMethods_1.processDependencies)({
92
- config: configPerFlowFile[path.relative(cleanPath, filePath)],
93
- directory: cleanPath,
94
- input: filePath,
95
- testSteps,
96
- });
97
- allFiles = [...allFiles, ...deps];
98
- errors = [...errors, ...errs];
99
- }
100
- if (errors.length > 0) {
101
- throw new Error('The following flow files are not present in the provided directory: \n' +
102
- errors.join('\n'));
103
- }
92
+ }, {});
93
+ const allFiles = await Promise.all(unfilteredFlowFiles.map((filePath) => checkDependencies(filePath))).then((results) => [...new Set(results.flat())]);
104
94
  const allIncludeTags = [
105
95
  ...includeTags,
106
96
  ...(workspaceConfig.includeTags || []),
@@ -109,7 +99,7 @@ async function plan(input, includeTags, excludeTags, excludeFlows) {
109
99
  ...excludeTags,
110
100
  ...(workspaceConfig.excludeTags || []),
111
101
  ];
112
- const allFlows = unsortedFlowFiles.filter((filePath) => {
102
+ const allFlows = unfilteredFlowFiles.filter((filePath) => {
113
103
  const config = configPerFlowFile[filePath];
114
104
  const tags = config?.tags || [];
115
105
  return ((allIncludeTags.length === 0 ||
@@ -142,4 +132,3 @@ async function plan(input, includeTags, excludeTags, excludeFlows) {
142
132
  totalFlowFiles: unfilteredFlowFiles.length,
143
133
  };
144
134
  }
145
- exports.plan = plan;
@@ -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,9 @@
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.readTestYamlFileAsJson = exports.readYamlFileAsJson = void 0;
4
+ exports.getFlowsToRunInSequence = getFlowsToRunInSequence;
5
+ exports.isFlowFile = isFlowFile;
6
+ exports.readDirectory = readDirectory;
4
7
  /* eslint-disable unicorn/filename-case */
5
8
  const yaml = require("js-yaml");
6
9
  const fs = require("node:fs");
@@ -28,11 +31,9 @@ function getFlowsToRunInSequence(paths, flowOrder) {
28
31
  return [];
29
32
  }
30
33
  }
31
- exports.getFlowsToRunInSequence = getFlowsToRunInSequence;
32
34
  function isFlowFile(filePath) {
33
35
  return filePath.endsWith('.yaml') || filePath.endsWith('.yml');
34
36
  }
35
- exports.isFlowFile = isFlowFile;
36
37
  const readYamlFileAsJson = (filePath) => {
37
38
  const yamlText = fs.readFileSync(filePath, 'utf8');
38
39
  return yaml.load(yamlText);
@@ -55,13 +56,11 @@ const readTestYamlFileAsJson = (filePath) => {
55
56
  return { config: null, testSteps };
56
57
  };
57
58
  exports.readTestYamlFileAsJson = readTestYamlFileAsJson;
58
- async function walk(dir, filterFunction) {
59
+ async function readDirectory(dir, filterFunction) {
59
60
  const readDirResult = await fs.promises.readdir(dir);
60
61
  const files = await Promise.all(readDirResult.map(async (file) => {
61
62
  const filePath = path.join(dir, file);
62
63
  const stats = await fs.promises.stat(filePath);
63
- if (stats.isDirectory())
64
- return walk(filePath, filterFunction);
65
64
  if (stats.isFile())
66
65
  if (filterFunction) {
67
66
  if (filterFunction(filePath))
@@ -73,22 +72,22 @@ async function walk(dir, filterFunction) {
73
72
  }));
74
73
  return files.flat().filter(Boolean);
75
74
  }
76
- exports.walk = walk;
77
- const checkIfFilesExistInWorkspace = (commandName, command, filePath, cleanPath) => {
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
- if (typeof command === 'string')
91
- processFilePath(command);
88
+ if (typeof command === 'string') {
89
+ processFilePath(path.normalize(directory + '/' + command));
90
+ }
92
91
  // array command
93
92
  if (Array.isArray(command)) {
94
93
  for (const file of command) {
@@ -102,13 +101,11 @@ const checkIfFilesExistInWorkspace = (commandName, command, filePath, cleanPath)
102
101
  return { errors, files };
103
102
  };
104
103
  exports.checkIfFilesExistInWorkspace = checkIfFilesExistInWorkspace;
105
- const checkFile = (filePath, cleanPath) => {
104
+ const checkFile = (filePath) => {
106
105
  if (!fs.existsSync(filePath))
107
106
  return `non-existent file`;
108
- if (!filePath.startsWith(cleanPath))
109
- return `file outside the workspace`;
110
107
  };
111
- const checkStepsArray = (steps, input, directory) => {
108
+ const checkStepsArray = (steps, absoluteFilePath) => {
112
109
  let errors = [];
113
110
  let files = [];
114
111
  for (const command of steps) {
@@ -116,14 +113,14 @@ const checkStepsArray = (steps, input, directory) => {
116
113
  continue;
117
114
  for (const [commandName, commandValue] of Object.entries(command)) {
118
115
  if (commandsThatRequireFiles.has(commandName)) {
119
- const { errors: newErrors, files: newFiles } = (0, exports.checkIfFilesExistInWorkspace)(commandName, commandValue, path.normalize(input), directory);
116
+ const { errors: newErrors, files: newFiles } = (0, exports.checkIfFilesExistInWorkspace)(commandName, commandValue, path.normalize(absoluteFilePath));
120
117
  errors = [...errors, ...newErrors];
121
118
  files = [...files, ...newFiles];
122
119
  }
123
120
  const nestedCommands = typeof commandValue === 'object' &&
124
121
  commandValue.commands;
125
122
  if (nestedCommands) {
126
- const { errors: newErrors, files: newFiles } = checkStepsArray(nestedCommands, input, directory);
123
+ const { errors: newErrors, files: newFiles } = checkStepsArray(nestedCommands, absoluteFilePath);
127
124
  errors = [...errors, ...newErrors];
128
125
  files = [...files, ...newFiles];
129
126
  }
@@ -131,7 +128,7 @@ const checkStepsArray = (steps, input, directory) => {
131
128
  }
132
129
  return { errors, files };
133
130
  };
134
- const processDependencies = ({ config, directory, input, testSteps, }) => {
131
+ const processDependencies = ({ config, input, testSteps, }) => {
135
132
  let allErrors = [];
136
133
  let allFiles = [];
137
134
  const { onFlowComplete, onFlowStart } = config ?? {};
@@ -142,7 +139,7 @@ const processDependencies = ({ config, directory, input, testSteps, }) => {
142
139
  stepsArray.push(onFlowComplete);
143
140
  for (const [index, steps] of stepsArray.entries()) {
144
141
  try {
145
- const { errors, files } = checkStepsArray(steps, input, directory);
142
+ const { errors, files } = checkStepsArray(steps, input);
146
143
  allErrors = [...allErrors, ...errors];
147
144
  allFiles = [...allFiles, ...files];
148
145
  }
@@ -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.3"
279
295
  }
package/package.json CHANGED
@@ -64,10 +64,6 @@
64
64
  ]
65
65
  },
66
66
  "private": false,
67
- "repository": {
68
- "type": "git",
69
- "url": "@devicecloud.dev/dcd"
70
- },
71
67
  "scripts": {
72
68
  "dcd": "./bin/dev.js",
73
69
  "prod": "./bin/run.js",
@@ -80,7 +76,7 @@
80
76
  "test": "mocha --forbid-only \"test/**/*.test.ts\"",
81
77
  "version": "oclif readme && git add README.md"
82
78
  },
83
- "version": "1.0.11",
79
+ "version": "2.0.0-rc.3",
84
80
  "bugs": {
85
81
  "url": "https://discord.gg/gm3mJwcNw8"
86
82
  },