@devicecloud.dev/dcd 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,17 +1,19 @@
1
- devicecloud.dev CLI
2
- =================
1
+ # devicecloud.dev CLI
2
+
3
+ ---
3
4
 
4
5
  One line swap out for Maestro Cloud
5
6
 
6
7
  Install:
8
+
7
9
  ```sh-session
8
10
  $ npm install -g @devicecloud.dev/dcd
9
11
  ```
10
12
 
11
13
  Use:
14
+
12
15
  ```sh-session
13
16
  # maestro cloud --apiKey <apiKey> <appFile> .myFlows/
14
-
15
17
  $ dcd cloud --apiKey <apiKey> <appFile> .myFlows/
16
18
  ```
17
19
 
@@ -40,6 +40,7 @@ export default class Cloud extends Command {
40
40
  'include-tags': import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
41
41
  'ios-device': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
42
42
  'ios-version': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
43
+ 'legacy-upload': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
43
44
  name: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
44
45
  orientation: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
45
46
  };
@@ -5,6 +5,7 @@ exports.EiOSDevices = void 0;
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");
8
9
  const promises_1 = require("node:fs/promises");
9
10
  const path = require("node:path");
10
11
  const constants_1 = require("../constants");
@@ -53,15 +54,16 @@ class Cloud extends core_1.Command {
53
54
  try {
54
55
  await (0, methods_1.versionCheck)(this.config.version);
55
56
  const { args, flags, raw } = await this.parse(Cloud);
56
- const { 'android-api-level': androidApiLevel, 'android-device': androidDevice, apiKey, apiUrl, 'app-binary-id': appBinaryId, 'app-file': appFile, arm64, async, env, 'exclude-flows': excludeFlows, 'exclude-tags': excludeTags, flows, 'google-play': googlePlay, 'include-tags': includeTags, 'ios-device': iOSDevice, 'ios-version': iOSVersion, name, orientation, ...rest } = flags;
57
+ const { 'android-api-level': androidApiLevel, 'android-device': androidDevice, apiKey, apiUrl, 'app-binary-id': appBinaryId, 'app-file': appFile, arm64, async, env, 'exclude-flows': excludeFlows, 'exclude-tags': excludeTags, flows, 'google-play': googlePlay, 'include-tags': includeTags, 'ios-device': iOSDevice, 'ios-version': iOSVersion, 'legacy-upload': legacyUpload, name, orientation, ...rest } = flags;
57
58
  if (arm64) {
58
59
  (0, cli_ux_1.info)('Contact hello@devicecloud.dev to enquire about arm64 devices');
59
60
  (0, cli_ux_1.exit)();
60
61
  }
61
62
  const { firstFile, secondFile } = args;
62
63
  let finalBinaryId = appBinaryId;
63
- const finalAppFile = appFile ?? firstFile;
64
+ let finalAppFile = appFile ?? firstFile;
64
65
  let flowFile = flows ?? secondFile;
66
+ let metadata;
65
67
  if (appBinaryId) {
66
68
  if (secondFile) {
67
69
  throw new Error('You cannot provide both an appBinaryId and a binary file');
@@ -125,27 +127,91 @@ class Cloud extends core_1.Command {
125
127
  `);
126
128
  if (!finalBinaryId) {
127
129
  core_1.ux.action.start('Uploading binary', 'Initializing', { stdout: true });
128
- const binaryFormData = new FormData();
129
- if (finalAppFile?.endsWith('.app')) {
130
- const zippedAppBlob = await (0, methods_1.compressFolderToBlob)(finalAppFile);
131
- binaryFormData.set('file', zippedAppBlob, finalAppFile + '.zip');
130
+ if (legacyUpload) {
131
+ const binaryFormData = new FormData();
132
+ if (finalAppFile?.endsWith('.app')) {
133
+ const zippedAppBlob = await (0, methods_1.compressFolderToBlob)(finalAppFile);
134
+ finalAppFile += '.zip';
135
+ binaryFormData.set('file', zippedAppBlob, finalAppFile);
136
+ }
137
+ else {
138
+ const binaryBlob = new Blob([await (0, promises_1.readFile)(finalAppFile)], {
139
+ type: mimeTypeLookupByExtension[finalAppFile.split('.').pop()],
140
+ });
141
+ binaryFormData.set('file', binaryBlob, finalAppFile);
142
+ }
143
+ const options = {
144
+ body: binaryFormData,
145
+ headers: { 'x-app-api-key': apiKey },
146
+ };
147
+ core_1.ux.action.status = `Uploading`;
148
+ const { binaryId, message } = await (0, methods_1.typeSafePost)(apiUrl, '/uploads/binary', options);
149
+ if (!binaryId)
150
+ throw new Error(message);
151
+ finalBinaryId = binaryId;
132
152
  }
133
153
  else {
134
- const binaryBlob = new Blob([await (0, promises_1.readFile)(finalAppFile)], {
135
- type: mimeTypeLookupByExtension[finalAppFile.split('.').pop()],
154
+ const { id, message, path, token } = await (0, methods_1.typeSafePost)(apiUrl, '/uploads/getBinaryUploadUrl', {
155
+ body: JSON.stringify({
156
+ platform: finalAppFile?.endsWith('.apk') ? 'android' : 'ios',
157
+ }),
158
+ headers: {
159
+ 'content-type': 'application/json',
160
+ 'x-app-api-key': apiKey,
161
+ },
162
+ });
163
+ finalBinaryId = id;
164
+ if (!path)
165
+ throw new Error(message);
166
+ let file;
167
+ if (finalAppFile?.endsWith('.app')) {
168
+ const zippedAppBlob = await (0, methods_1.compressFolderToBlob)(finalAppFile);
169
+ const filePath = finalAppFile + '.zip';
170
+ file = new File([zippedAppBlob], filePath);
171
+ }
172
+ else {
173
+ const binaryBlob = new Blob([await (0, promises_1.readFile)(finalAppFile)], {
174
+ type: mimeTypeLookupByExtension[finalAppFile.split('.').pop()],
175
+ });
176
+ file = new File([binaryBlob], finalAppFile);
177
+ }
178
+ try {
179
+ metadata = finalAppFile?.endsWith('.apk')
180
+ ? await (0, methods_1.extractAppMetadataAndroid)(finalAppFile)
181
+ : await (0, methods_1.extractAppMetadataIos)(finalAppFile);
182
+ }
183
+ catch {
184
+ this.warn('Failed to extact app metadata, please share with support@devicecloud.dev so we can improve our parsing.');
185
+ }
186
+ // this needs to made nicer by using envs or maybe fetching the keys from the getSignedURL call
187
+ const SB = {
188
+ dev: {
189
+ SUPABASE_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxibXNvd2VodGp3bnFsdXJwZW1iIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDkyMTg0ODcsImV4cCI6MjAyNDc5NDQ4N30.zeLTMAuZ_WwYvGdeP0kdvL_Zrs-RQee5APPyxmWq7qQ',
190
+ SUPABASE_URL: 'https://lbmsowehtjwnqlurpemb.supabase.co',
191
+ },
192
+ prod: {
193
+ SUPABASE_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBneWRucGhiaW1ldGluc2dma2JvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDc1OTQzNDYsImV4cCI6MjAyMzE3MDM0Nn0.hAYOMFxxwX1exkQkY9xyQJGC_GhGnyogkj2N-kBkMI8',
194
+ SUPABASE_URL: 'https://pgydnphbimetinsgfkbo.supabase.co',
195
+ },
196
+ };
197
+ const { SUPABASE_KEY, SUPABASE_URL } = SB[apiUrl === 'https://api.devicecloud.dev' ? 'prod' : 'dev'];
198
+ const supabase = (0, supabase_js_1.createClient)(SUPABASE_URL, SUPABASE_KEY);
199
+ const uploadToUrl = await supabase.storage
200
+ .from('organizations')
201
+ .uploadToSignedUrl(path, token, file);
202
+ if (uploadToUrl.error)
203
+ throw new Error(uploadToUrl.error);
204
+ const { error } = await (0, methods_1.typeSafePost)(apiUrl, '/uploads/finaliseUpload', {
205
+ body: JSON.stringify({ id, metadata, path }),
206
+ headers: {
207
+ 'content-type': 'application/json',
208
+ 'x-app-api-key': apiKey,
209
+ },
136
210
  });
137
- binaryFormData.set('file', binaryBlob, finalAppFile);
211
+ if (error)
212
+ throw new Error(error);
138
213
  }
139
- const options = {
140
- body: binaryFormData,
141
- headers: { 'x-app-api-key': apiKey },
142
- };
143
- core_1.ux.action.status = `Uploading`;
144
- const { binaryId, message } = await (0, methods_1.typeSafePost)(apiUrl, '/uploads/binary', options);
145
- if (!binaryId)
146
- throw new Error(message);
147
- core_1.ux.action.stop(`\nBinary uploaded with id: ${binaryId}`);
148
- finalBinaryId = binaryId;
214
+ core_1.ux.action.stop(`\nBinary uploaded with id: ${finalBinaryId}`);
149
215
  }
150
216
  const testFormData = new FormData();
151
217
  // eslint-disable-next-line unicorn/no-array-reduce
@@ -190,6 +256,8 @@ class Cloud extends core_1.Command {
190
256
  testFormData.set('iOSDevice', iOSDevice.toString());
191
257
  if (name)
192
258
  testFormData.set('name', name.toString());
259
+ if (metadata)
260
+ testFormData.set('metadata', JSON.stringify(metadata));
193
261
  for (const [key, value] of Object.entries(rest)) {
194
262
  if (value) {
195
263
  testFormData.set(key, value);
@@ -16,6 +16,7 @@ export declare const flags: {
16
16
  'include-tags': import("@oclif/core/lib/interfaces").OptionFlag<string[], import("@oclif/core/lib/interfaces").CustomOptions>;
17
17
  'ios-device': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
18
18
  'ios-version': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
19
+ 'legacy-upload': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
19
20
  name: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
20
21
  orientation: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
21
22
  };
package/dist/constants.js CHANGED
@@ -103,6 +103,10 @@ exports.flags = {
103
103
  description: '[iOS only] iOS version to run your flow against',
104
104
  options: ['15', '16', '17'],
105
105
  }),
106
+ 'legacy-upload': core_1.Flags.boolean({
107
+ default: false,
108
+ description: 'Use the legacy direct upload method',
109
+ }),
106
110
  name: core_1.Flags.string({
107
111
  description: 'A custom name for your upload (useful for tagging commits etc)',
108
112
  }),
package/dist/methods.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  /// <reference types="node" />
2
2
  import * as archiver from 'archiver';
3
3
  import { paths } from '../../api/schema.types';
4
+ import { TAppMetadata } from './types';
4
5
  export declare const versionCheck: (currentVersion: string) => Promise<void>;
5
6
  export declare const typeSafePost: <T extends keyof paths>(baseUrl: string, path: string, init?: {
6
- body?: FormData;
7
+ body?: BodyInit;
7
8
  headers?: HeadersInit;
8
9
  }) => Promise<paths[T]["post"]["responses"]["201"]["content"]["application/json"]>;
9
10
  export declare const typeSafeGet: <T extends keyof paths>(baseUrl: string, path: string, init?: {
@@ -15,3 +16,5 @@ export declare const compressDir: (sourceDir: string) => Promise<Buffer>;
15
16
  export declare const compressFolderToBlob: (sourceDir: string) => Promise<Blob>;
16
17
  export declare const compressFilesFromRelativePath: (path: string, files: string[]) => Promise<Buffer>;
17
18
  export declare const verifyAppZip: (zipPath: string) => Promise<void>;
19
+ export declare const extractAppMetadataAndroid: (appFilePath: string) => Promise<TAppMetadata>;
20
+ export declare const extractAppMetadataIos: (appFilePath: string) => Promise<TAppMetadata>;
package/dist/methods.js CHANGED
@@ -1,12 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.verifyAppZip = exports.compressFilesFromRelativePath = exports.compressFolderToBlob = exports.compressDir = exports.toBuffer = exports.typeSafeGet = exports.typeSafePost = exports.versionCheck = void 0;
3
+ exports.extractAppMetadataIos = exports.extractAppMetadataAndroid = exports.verifyAppZip = exports.compressFilesFromRelativePath = exports.compressFolderToBlob = exports.compressDir = exports.toBuffer = exports.typeSafeGet = exports.typeSafePost = exports.versionCheck = void 0;
4
4
  const cli_ux_1 = require("@oclif/core/lib/cli-ux");
5
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
6
+ // @ts-ignore
7
+ const AppInfoParser = require("app-info-parser");
5
8
  const archiver = require("archiver");
9
+ const bplist_parser_1 = require("bplist-parser");
6
10
  // import { writeFile } from 'node:fs/promises';
7
11
  const nodePath = require("node:path");
8
12
  const node_stream_1 = require("node:stream");
9
13
  const StreamZip = require("node-stream-zip");
14
+ const plist_1 = require("plist");
10
15
  const PERMITTED_EXTENSIONS = new Set([
11
16
  'yml',
12
17
  'yaml',
@@ -35,7 +40,6 @@ const typeSafePost = async (baseUrl, path, init) => {
35
40
  const res = await fetch(baseUrl + path, {
36
41
  ...init,
37
42
  method: 'POST',
38
- signal: AbortSignal.timeout(3000000),
39
43
  });
40
44
  if (!res.ok) {
41
45
  throw new Error(await res.text());
@@ -129,3 +133,40 @@ const verifyAppZip = async (zipPath) => {
129
133
  zip.close();
130
134
  };
131
135
  exports.verifyAppZip = verifyAppZip;
136
+ const extractAppMetadataAndroid = async (appFilePath) => {
137
+ const parser = new AppInfoParser(appFilePath);
138
+ const result = await parser.parse();
139
+ return { appId: result.package, platform: 'android' };
140
+ };
141
+ exports.extractAppMetadataAndroid = extractAppMetadataAndroid;
142
+ const extractAppMetadataIos = async (appFilePath) => new Promise((resolve, reject) => {
143
+ const zip = new StreamZip({ file: './' + appFilePath });
144
+ zip.on('ready', () => {
145
+ const infoPlist = Object.values(zip.entries()).find((e) => e.name.includes('Info.plist'));
146
+ if (!infoPlist) {
147
+ reject(new Error('Failed to find info plist'));
148
+ }
149
+ 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' });
166
+ });
167
+ zip.on('error', (err) => {
168
+ console.error(err);
169
+ reject(err);
170
+ });
171
+ });
172
+ exports.extractAppMetadataIos = extractAppMetadataIos;
@@ -0,0 +1,4 @@
1
+ export type TAppMetadata = {
2
+ appId: string;
3
+ platform: 'android' | 'ios';
4
+ };
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -189,6 +189,12 @@
189
189
  ],
190
190
  "type": "option"
191
191
  },
192
+ "legacy-upload": {
193
+ "description": "Use the legacy direct upload method",
194
+ "name": "legacy-upload",
195
+ "allowNo": false,
196
+ "type": "boolean"
197
+ },
192
198
  "name": {
193
199
  "description": "A custom name for your upload (useful for tagging commits etc)",
194
200
  "name": "name",
@@ -226,5 +232,5 @@
226
232
  ]
227
233
  }
228
234
  },
229
- "version": "1.0.3"
235
+ "version": "1.0.5"
230
236
  }
package/package.json CHANGED
@@ -11,10 +11,14 @@
11
11
  "@oclif/plugin-plugins": "^4",
12
12
  "@oclif/plugin-update": "^4.1.11",
13
13
  "@oclif/plugin-warn-if-update-available": "^3.0.10",
14
+ "@supabase/supabase-js": "^2.45.1",
15
+ "app-info-parser": "^1.1.6",
14
16
  "archiver": "^6.0.1",
17
+ "bplist-parser": "^0.3.2",
15
18
  "glob": "^10.3.10",
16
19
  "js-yaml": "^4.1.0",
17
- "node-stream-zip": "^1.15.0"
20
+ "node-stream-zip": "^1.15.0",
21
+ "plist": "^3.1.0"
18
22
  },
19
23
  "description": "Better cloud maestro testing",
20
24
  "devDependencies": {
@@ -25,6 +29,7 @@
25
29
  "@types/glob-to-regexp": "^0.4.4",
26
30
  "@types/js-yaml": "^4.0.9",
27
31
  "@types/mocha": "^9.0.0",
32
+ "@types/plist": "^3.0.5",
28
33
  "chai": "^4",
29
34
  "eslint": "^8.56.0",
30
35
  "eslint-config-oclif": "^5",
@@ -37,7 +42,7 @@
37
42
  "typescript": "^5"
38
43
  },
39
44
  "engines": {
40
- "node": ">=21.0.0"
45
+ "node": ">=20.0.0"
41
46
  },
42
47
  "files": [
43
48
  "/bin",
@@ -73,7 +78,7 @@
73
78
  "test": "mocha --forbid-only \"test/**/*.test.ts\"",
74
79
  "version": "oclif readme && git add README.md"
75
80
  },
76
- "version": "1.0.3",
81
+ "version": "1.0.5",
77
82
  "bugs": {
78
83
  "url": "https://discord.gg/gm3mJwcNw8"
79
84
  },