@devicecloud.dev/dcd 4.1.2 → 4.1.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.
Files changed (46) hide show
  1. package/dist/commands/cloud.d.ts +26 -34
  2. package/dist/commands/cloud.js +117 -513
  3. package/dist/config/flags/api.flags.d.ts +7 -0
  4. package/dist/config/flags/api.flags.js +19 -0
  5. package/dist/config/flags/binary.flags.d.ts +8 -0
  6. package/dist/config/flags/binary.flags.js +20 -0
  7. package/dist/config/flags/device.flags.d.ts +14 -0
  8. package/dist/config/flags/device.flags.js +46 -0
  9. package/dist/config/flags/environment.flags.d.ts +11 -0
  10. package/dist/config/flags/environment.flags.js +37 -0
  11. package/dist/config/flags/execution.flags.d.ts +13 -0
  12. package/dist/config/flags/execution.flags.js +50 -0
  13. package/dist/config/flags/output.flags.d.ts +18 -0
  14. package/dist/config/flags/output.flags.js +61 -0
  15. package/dist/constants.d.ts +28 -24
  16. package/dist/constants.js +21 -206
  17. package/dist/gateways/api-gateway.d.ts +3 -3
  18. package/dist/methods.d.ts +0 -4
  19. package/dist/methods.js +15 -80
  20. package/dist/services/device-validation.service.d.ts +29 -0
  21. package/dist/services/device-validation.service.js +72 -0
  22. package/dist/{plan.d.ts → services/execution-plan.service.d.ts} +1 -1
  23. package/dist/{plan.js → services/execution-plan.service.js} +10 -10
  24. package/dist/{planMethods.js → services/execution-plan.utils.js} +0 -1
  25. package/dist/services/metadata-extractor.service.d.ts +46 -0
  26. package/dist/services/metadata-extractor.service.js +138 -0
  27. package/dist/services/moropo.service.d.ts +20 -0
  28. package/dist/services/moropo.service.js +113 -0
  29. package/dist/services/report-download.service.d.ts +40 -0
  30. package/dist/services/report-download.service.js +110 -0
  31. package/dist/services/results-polling.service.d.ts +45 -0
  32. package/dist/services/results-polling.service.js +210 -0
  33. package/dist/services/test-submission.service.d.ts +41 -0
  34. package/dist/services/test-submission.service.js +116 -0
  35. package/dist/services/version.service.d.ts +31 -0
  36. package/dist/services/version.service.js +81 -0
  37. package/dist/types/{schema.types.d.ts → generated/schema.types.d.ts} +349 -349
  38. package/dist/types/index.d.ts +6 -0
  39. package/dist/types/index.js +24 -0
  40. package/dist/utils/compatibility.d.ts +5 -0
  41. package/oclif.manifest.json +195 -209
  42. package/package.json +2 -9
  43. /package/dist/{planMethods.d.ts → services/execution-plan.utils.d.ts} +0 -0
  44. /package/dist/types/{device.types.d.ts → domain/device.types.d.ts} +0 -0
  45. /package/dist/types/{device.types.js → domain/device.types.js} +0 -0
  46. /package/dist/types/{schema.types.js → generated/schema.types.js} +0 -0
@@ -14,14 +14,14 @@ export declare const ApiGateway: {
14
14
  downloadArtifactsZip(baseUrl: string, apiKey: string, uploadId: string, results: "ALL" | "FAILED", artifactsPath?: string): Promise<void>;
15
15
  finaliseUpload(baseUrl: string, apiKey: string, id: string, metadata: TAppMetadata, path: string, sha: string): Promise<Record<string, never>>;
16
16
  getBinaryUploadUrl(baseUrl: string, apiKey: string, platform: "android" | "ios"): Promise<{
17
+ id: string;
17
18
  message: string;
18
19
  path: string;
19
20
  token: string;
20
- id: string;
21
21
  }>;
22
22
  getResultsForUpload(baseUrl: string, apiKey: string, uploadId: string): Promise<{
23
+ results?: import("../types/generated/schema.types").components["schemas"]["TResultResponse"][];
23
24
  statusCode?: number;
24
- results?: import("../types/schema.types").components["schemas"]["TResultResponse"][];
25
25
  }>;
26
26
  getUploadStatus(baseUrl: string, apiKey: string, options: {
27
27
  name?: string;
@@ -37,7 +37,7 @@ export declare const ApiGateway: {
37
37
  }>;
38
38
  uploadFlow(baseUrl: string, apiKey: string, testFormData: FormData): Promise<{
39
39
  message?: string;
40
- results?: import("../types/schema.types").components["schemas"]["IDBResult"][];
40
+ results?: import("../types/generated/schema.types").components["schemas"]["IDBResult"][];
41
41
  }>;
42
42
  /**
43
43
  * Generic report download method that handles both junit and allure reports
package/dist/methods.d.ts CHANGED
@@ -1,12 +1,8 @@
1
1
  import * as archiver from 'archiver';
2
- import { TAppMetadata } from './types';
3
2
  export declare const toBuffer: (archive: archiver.Archiver) => Promise<Buffer<ArrayBuffer>>;
4
3
  export declare const compressFolderToBlob: (sourceDir: string) => Promise<Blob>;
5
4
  export declare const compressFilesFromRelativePath: (basePath: string, files: string[], commonRoot: string) => Promise<Buffer<ArrayBuffer>>;
6
5
  export declare const verifyAppZip: (zipPath: string) => Promise<void>;
7
- export declare const extractAppMetadataAndroid: (appFilePath: string) => Promise<TAppMetadata>;
8
- export declare const extractAppMetadataIosZip: (appFilePath: string) => Promise<TAppMetadata>;
9
- export declare const extractAppMetadataIos: (appFolderPath: string) => Promise<TAppMetadata>;
10
6
  export declare const uploadBinary: (filePath: string, apiUrl: string, apiKey: string, ignoreShaCheck?: boolean, log?: boolean) => Promise<string>;
11
7
  /**
12
8
  * Writes JSON data to a file with error handling
package/dist/methods.js CHANGED
@@ -1,22 +1,22 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.formatDurationSeconds = exports.writeJSONFile = exports.uploadBinary = exports.extractAppMetadataIos = exports.extractAppMetadataIosZip = exports.extractAppMetadataAndroid = exports.verifyAppZip = exports.compressFilesFromRelativePath = exports.compressFolderToBlob = exports.toBuffer = void 0;
3
+ exports.formatDurationSeconds = exports.writeJSONFile = exports.uploadBinary = exports.verifyAppZip = exports.compressFilesFromRelativePath = exports.compressFolderToBlob = exports.toBuffer = void 0;
4
4
  const core_1 = require("@oclif/core");
5
- // required polyfill for node 18
6
- const file_1 = require("@web-std/file");
7
- const AppInfoParser = require("app-info-parser");
8
5
  const archiver = require("archiver");
9
- const bplist_parser_1 = require("bplist-parser");
10
6
  const node_crypto_1 = require("node:crypto");
11
7
  const node_fs_1 = require("node:fs");
12
8
  const promises_1 = require("node:fs/promises");
13
9
  const path = require("node:path");
14
10
  const node_stream_1 = require("node:stream");
15
11
  const StreamZip = require("node-stream-zip");
16
- const plist_1 = require("plist");
17
- const cloud_1 = require("./commands/cloud");
18
12
  const api_gateway_1 = require("./gateways/api-gateway");
19
13
  const supabase_gateway_1 = require("./gateways/supabase-gateway");
14
+ const metadata_extractor_service_1 = require("./services/metadata-extractor.service");
15
+ const mimeTypeLookupByExtension = {
16
+ apk: 'application/vnd.android.package-archive',
17
+ yaml: 'application/x-yaml',
18
+ zip: 'application/zip',
19
+ };
20
20
  const toBuffer = async (archive) => {
21
21
  const chunks = [];
22
22
  const writable = new node_stream_1.Writable();
@@ -76,64 +76,6 @@ const verifyAppZip = async (zipPath) => {
76
76
  zip.close();
77
77
  };
78
78
  exports.verifyAppZip = verifyAppZip;
79
- const extractAppMetadataAndroid = async (appFilePath) => {
80
- const parser = new AppInfoParser(appFilePath);
81
- const result = await parser.parse();
82
- return { appId: result.package, platform: 'android' };
83
- };
84
- exports.extractAppMetadataAndroid = extractAppMetadataAndroid;
85
- const parseInfoPlist = async (buffer) => {
86
- let data;
87
- const bufferType = buffer[0];
88
- if (bufferType === 60 ||
89
- bufferType === '<' ||
90
- bufferType === 239) {
91
- data = (0, plist_1.parse)(buffer.toString());
92
- }
93
- else if (bufferType === 98) {
94
- data = (0, bplist_parser_1.parseBuffer)(buffer)[0];
95
- }
96
- else {
97
- throw new Error('Unknown plist buffer type.');
98
- }
99
- return data;
100
- };
101
- const extractAppMetadataIosZip = async (appFilePath) => new Promise((resolve, reject) => {
102
- const zip = new StreamZip({ file: appFilePath });
103
- zip.on('ready', () => {
104
- // Get all entries and sort them by path depth
105
- const entries = Object.values(zip.entries());
106
- const sortedEntries = entries.sort((a, b) => {
107
- const aDepth = a.name.split('/').length;
108
- const bDepth = b.name.split('/').length;
109
- return aDepth - bDepth;
110
- });
111
- // Find the first Info.plist in the shallowest directory
112
- const infoPlist = sortedEntries.find((e) => e.name.endsWith('.app/Info.plist'));
113
- if (!infoPlist) {
114
- reject(new Error('Failed to find info plist'));
115
- return;
116
- }
117
- const buffer = zip.entryDataSync(infoPlist.name);
118
- parseInfoPlist(buffer)
119
- .then((data) => {
120
- const appId = data.CFBundleIdentifier;
121
- zip.close();
122
- resolve({ appId, platform: 'ios' });
123
- })
124
- .catch(reject);
125
- });
126
- zip.on('error', reject);
127
- });
128
- exports.extractAppMetadataIosZip = extractAppMetadataIosZip;
129
- const extractAppMetadataIos = async (appFolderPath) => {
130
- const infoPlistPath = path.normalize(path.join(appFolderPath, 'Info.plist'));
131
- const buffer = await (0, promises_1.readFile)(infoPlistPath);
132
- const data = await parseInfoPlist(buffer);
133
- const appId = data.CFBundleIdentifier;
134
- return { appId, platform: 'ios' };
135
- };
136
- exports.extractAppMetadataIos = extractAppMetadataIos;
137
79
  const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false, log = true) => {
138
80
  if (log) {
139
81
  core_1.ux.action.start('Checking and uploading binary', 'Initializing', {
@@ -143,14 +85,14 @@ const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false, lo
143
85
  let file;
144
86
  if (filePath?.endsWith('.app')) {
145
87
  const zippedAppBlob = await (0, exports.compressFolderToBlob)(filePath);
146
- file = new file_1.File([zippedAppBlob], filePath + '.zip');
88
+ file = new File([zippedAppBlob], filePath + '.zip');
147
89
  }
148
90
  else {
149
91
  const fileBuffer = await (0, promises_1.readFile)(filePath);
150
92
  const binaryBlob = new Blob([new Uint8Array(fileBuffer)], {
151
- type: cloud_1.mimeTypeLookupByExtension[filePath.split('.').pop()],
93
+ type: mimeTypeLookupByExtension[filePath.split('.').pop()],
152
94
  });
153
- file = new file_1.File([binaryBlob], filePath);
95
+ file = new File([binaryBlob], filePath);
154
96
  }
155
97
  let sha;
156
98
  try {
@@ -179,18 +121,11 @@ const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false, lo
179
121
  const { id, message, path, token } = await api_gateway_1.ApiGateway.getBinaryUploadUrl(apiUrl, apiKey, filePath?.endsWith('.apk') ? 'android' : 'ios');
180
122
  if (!path)
181
123
  throw new Error(message);
182
- let metadata;
183
- try {
184
- metadata = filePath?.endsWith('.apk')
185
- ? await (0, exports.extractAppMetadataAndroid)(filePath)
186
- : filePath?.endsWith('.zip')
187
- ? await (0, exports.extractAppMetadataIosZip)(filePath)
188
- : await (0, exports.extractAppMetadataIos)(filePath);
189
- }
190
- catch {
191
- if (log) {
192
- core_1.ux.warn('Failed to extract app metadata, please share with support@devicecloud.dev so we can improve our parsing.');
193
- }
124
+ // Extract app metadata using the service
125
+ const metadataExtractor = new metadata_extractor_service_1.MetadataExtractorService();
126
+ const metadata = await metadataExtractor.extract(filePath);
127
+ if (!metadata) {
128
+ throw new Error(`Failed to extract metadata from ${filePath}. Supported formats: .apk, .app, .zip`);
194
129
  }
195
130
  const env = apiUrl === 'https://api.devicecloud.dev' ? 'prod' : 'dev';
196
131
  await supabase_gateway_1.SupabaseGateway.uploadToSignedUrl(env, path, token, file);
@@ -0,0 +1,29 @@
1
+ import { CompatibilityData } from '../utils/compatibility';
2
+ export interface DeviceValidationOptions {
3
+ debug?: boolean;
4
+ logger?: (message: string) => void;
5
+ }
6
+ /**
7
+ * Service for validating device configurations against compatibility data
8
+ */
9
+ export declare class DeviceValidationService {
10
+ /**
11
+ * Validate Android device configuration
12
+ * @param androidApiLevel Android API level to validate
13
+ * @param androidDevice Android device model to validate
14
+ * @param googlePlay Whether Google Play services are enabled
15
+ * @param compatibilityData Compatibility data from API
16
+ * @param options Validation options
17
+ * @throws Error if device/API level combination is not supported
18
+ */
19
+ validateAndroidDevice(androidApiLevel: string | undefined, androidDevice: string | undefined, googlePlay: boolean, compatibilityData: CompatibilityData, options?: DeviceValidationOptions): void;
20
+ /**
21
+ * Validate iOS device configuration
22
+ * @param iOSVersion iOS version to validate
23
+ * @param iOSDevice iOS device model to validate
24
+ * @param compatibilityData Compatibility data from API
25
+ * @param options Validation options
26
+ * @throws Error if device/version combination is not supported
27
+ */
28
+ validateiOSDevice(iOSVersion: string | undefined, iOSDevice: string | undefined, compatibilityData: CompatibilityData, options?: DeviceValidationOptions): void;
29
+ }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DeviceValidationService = void 0;
4
+ /**
5
+ * Service for validating device configurations against compatibility data
6
+ */
7
+ class DeviceValidationService {
8
+ /**
9
+ * Validate Android device configuration
10
+ * @param androidApiLevel Android API level to validate
11
+ * @param androidDevice Android device model to validate
12
+ * @param googlePlay Whether Google Play services are enabled
13
+ * @param compatibilityData Compatibility data from API
14
+ * @param options Validation options
15
+ * @throws Error if device/API level combination is not supported
16
+ */
17
+ validateAndroidDevice(androidApiLevel, androidDevice, googlePlay, compatibilityData, options = {}) {
18
+ const { debug = false, logger } = options;
19
+ if (!androidApiLevel && !androidDevice) {
20
+ return;
21
+ }
22
+ const androidDeviceID = androidDevice || 'pixel-7';
23
+ const lookup = googlePlay
24
+ ? compatibilityData.androidPlay
25
+ : compatibilityData.android;
26
+ const supportedAndroidVersions = lookup?.[androidDeviceID] || [];
27
+ const version = androidApiLevel || '34';
28
+ if (supportedAndroidVersions.length === 0) {
29
+ throw new Error(`We don't support that device configuration - please check the docs for supported devices: https://docs.devicecloud.dev/getting-started/devices-configuration`);
30
+ }
31
+ if (Array.isArray(supportedAndroidVersions) &&
32
+ !supportedAndroidVersions.includes(version)) {
33
+ throw new Error(`${androidDeviceID} ${googlePlay ? '(Play Store) ' : ''}only supports these Android API levels: ${supportedAndroidVersions.join(', ')}`);
34
+ }
35
+ if (debug && logger) {
36
+ logger(`DEBUG: Android device: ${androidDeviceID}`);
37
+ logger(`DEBUG: Android API level: ${version}`);
38
+ logger(`DEBUG: Google Play enabled: ${googlePlay}`);
39
+ logger(`DEBUG: Supported Android versions: ${supportedAndroidVersions.join(', ')}`);
40
+ }
41
+ }
42
+ /**
43
+ * Validate iOS device configuration
44
+ * @param iOSVersion iOS version to validate
45
+ * @param iOSDevice iOS device model to validate
46
+ * @param compatibilityData Compatibility data from API
47
+ * @param options Validation options
48
+ * @throws Error if device/version combination is not supported
49
+ */
50
+ validateiOSDevice(iOSVersion, iOSDevice, compatibilityData, options = {}) {
51
+ const { debug = false, logger } = options;
52
+ if (!iOSVersion && !iOSDevice) {
53
+ return;
54
+ }
55
+ const iOSDeviceID = iOSDevice || 'iphone-14';
56
+ const supportediOSVersions = compatibilityData?.ios?.[iOSDeviceID] || [];
57
+ const version = iOSVersion || '17';
58
+ if (supportediOSVersions.length === 0) {
59
+ throw new Error(`Device ${iOSDeviceID} is not supported. Please check the docs for supported devices: https://docs.devicecloud.dev/getting-started/devices-configuration`);
60
+ }
61
+ if (Array.isArray(supportediOSVersions) &&
62
+ !supportediOSVersions.includes(version)) {
63
+ throw new Error(`${iOSDeviceID} only supports these iOS versions: ${supportediOSVersions.join(', ')}`);
64
+ }
65
+ if (debug && logger) {
66
+ logger(`DEBUG: iOS device: ${iOSDeviceID}`);
67
+ logger(`DEBUG: iOS version: ${version}`);
68
+ logger(`DEBUG: Supported iOS versions: ${supportediOSVersions.join(', ')}`);
69
+ }
70
+ }
71
+ }
72
+ exports.DeviceValidationService = DeviceValidationService;
@@ -20,7 +20,7 @@ interface IExecutionOrder {
20
20
  continueOnFailure: boolean;
21
21
  flowsOrder: string[];
22
22
  }
23
- interface IExecutionPlan {
23
+ export interface IExecutionPlan {
24
24
  allExcludeTags?: null | string[];
25
25
  allIncludeTags?: null | string[];
26
26
  flowMetadata: Record<string, Record<string, unknown>>;
@@ -4,14 +4,14 @@ 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
- const planMethods_1 = require("./planMethods");
7
+ const execution_plan_utils_1 = require("./execution-plan.utils");
8
8
  async function checkDependencies(input) {
9
9
  const checkedDependencies = [];
10
10
  const uncheckedDependencies = [input];
11
11
  while (uncheckedDependencies.length > 0) {
12
12
  const fileToCheck = uncheckedDependencies.shift();
13
- const { config, testSteps } = (0, planMethods_1.readTestYamlFileAsJson)(fileToCheck);
14
- const { allErrors, allFiles } = (0, planMethods_1.processDependencies)({
13
+ const { config, testSteps } = (0, execution_plan_utils_1.readTestYamlFileAsJson)(fileToCheck);
14
+ const { allErrors, allFiles } = (0, execution_plan_utils_1.processDependencies)({
15
15
  config,
16
16
  input: fileToCheck,
17
17
  testSteps,
@@ -21,7 +21,7 @@ async function checkDependencies(input) {
21
21
  allErrors.join('\n'));
22
22
  }
23
23
  for (const file of allFiles) {
24
- if (!(0, planMethods_1.isFlowFile)(file)) {
24
+ if (!(0, execution_plan_utils_1.isFlowFile)(file)) {
25
25
  // js/media files don't have dependencies
26
26
  checkedDependencies.push(file);
27
27
  }
@@ -45,7 +45,7 @@ function getWorkspaceConfig(input, unfilteredFlowFiles) {
45
45
  const possibleConfigPaths = new Set([path.join(input, 'config.yaml'), path.join(input, 'config.yml')].map((p) => path.normalize(p)));
46
46
  const configFilePath = unfilteredFlowFiles.find((file) => possibleConfigPaths.has(path.normalize(file)));
47
47
  const config = configFilePath
48
- ? (0, planMethods_1.readYamlFileAsJson)(configFilePath)
48
+ ? (0, execution_plan_utils_1.readYamlFileAsJson)(configFilePath)
49
49
  : {};
50
50
  return config;
51
51
  }
@@ -75,7 +75,7 @@ async function plan(input, includeTags, excludeTags, excludeFlows, configFile, d
75
75
  normalizedInput.endsWith('config.yml')) {
76
76
  throw new Error('If using config.yaml, pass the workspace folder path, not the config file or a custom path via --config');
77
77
  }
78
- const { config } = (0, planMethods_1.readTestYamlFileAsJson)(normalizedInput);
78
+ const { config } = (0, execution_plan_utils_1.readTestYamlFileAsJson)(normalizedInput);
79
79
  const flowOverrides = {};
80
80
  if (config) {
81
81
  flowMetadata[normalizedInput] = config;
@@ -90,7 +90,7 @@ async function plan(input, includeTags, excludeTags, excludeFlows, configFile, d
90
90
  totalFlowFiles: 1,
91
91
  };
92
92
  }
93
- let unfilteredFlowFiles = await (0, planMethods_1.readDirectory)(normalizedInput, planMethods_1.isFlowFile);
93
+ let unfilteredFlowFiles = await (0, execution_plan_utils_1.readDirectory)(normalizedInput, execution_plan_utils_1.isFlowFile);
94
94
  if (unfilteredFlowFiles.length === 0) {
95
95
  throw new Error(`Flow directory does not contain any Flow files: ${path.resolve(normalizedInput)}`);
96
96
  }
@@ -101,7 +101,7 @@ async function plan(input, includeTags, excludeTags, excludeFlows, configFile, d
101
101
  if (!fs.existsSync(configFilePath)) {
102
102
  throw new Error(`Config file does not exist: ${configFilePath}`);
103
103
  }
104
- workspaceConfig = (0, planMethods_1.readYamlFileAsJson)(configFilePath);
104
+ workspaceConfig = (0, execution_plan_utils_1.readYamlFileAsJson)(configFilePath);
105
105
  }
106
106
  else {
107
107
  workspaceConfig = getWorkspaceConfig(normalizedInput, unfilteredFlowFiles);
@@ -154,7 +154,7 @@ async function plan(input, includeTags, excludeTags, excludeFlows, configFile, d
154
154
  const configPerFlowFile =
155
155
  // eslint-disable-next-line unicorn/no-array-reduce
156
156
  unfilteredFlowFiles.reduce((acc, filePath) => {
157
- const { config } = (0, planMethods_1.readTestYamlFileAsJson)(filePath);
157
+ const { config } = (0, execution_plan_utils_1.readTestYamlFileAsJson)(filePath);
158
158
  acc[filePath] = config;
159
159
  return acc;
160
160
  }, {});
@@ -207,7 +207,7 @@ async function plan(input, includeTags, excludeTags, excludeFlows, configFile, d
207
207
  if (debug && flowOrder !== normalizedFlowOrder) {
208
208
  console.log(`[DEBUG] Stripping trailing extension: "${flowOrder}" -> "${normalizedFlowOrder}"`);
209
209
  }
210
- return (0, planMethods_1.getFlowsToRunInSequence)(pathsByName, [normalizedFlowOrder], debug);
210
+ return (0, execution_plan_utils_1.getFlowsToRunInSequence)(pathsByName, [normalizedFlowOrder], debug);
211
211
  })
212
212
  .flat() || [];
213
213
  if (debug) {
@@ -4,7 +4,6 @@ exports.processDependencies = exports.checkIfFilesExistInWorkspace = exports.rea
4
4
  exports.getFlowsToRunInSequence = getFlowsToRunInSequence;
5
5
  exports.isFlowFile = isFlowFile;
6
6
  exports.readDirectory = readDirectory;
7
- /* eslint-disable unicorn/filename-case */
8
7
  const yaml = require("js-yaml");
9
8
  const fs = require("node:fs");
10
9
  const path = require("node:path");
@@ -0,0 +1,46 @@
1
+ export interface TAppMetadata {
2
+ appId: string;
3
+ platform: 'android' | 'ios';
4
+ }
5
+ /**
6
+ * Interface for platform-specific metadata extractors
7
+ */
8
+ export interface IMetadataExtractor {
9
+ canHandle(filePath: string): boolean;
10
+ extract(filePath: string): Promise<TAppMetadata>;
11
+ }
12
+ /**
13
+ * Extracts metadata from Android APK files
14
+ */
15
+ export declare class AndroidMetadataExtractor implements IMetadataExtractor {
16
+ canHandle(filePath: string): boolean;
17
+ extract(filePath: string): Promise<TAppMetadata>;
18
+ }
19
+ /**
20
+ * Extracts metadata from iOS .app directories
21
+ */
22
+ export declare class IosAppMetadataExtractor implements IMetadataExtractor {
23
+ canHandle(filePath: string): boolean;
24
+ extract(filePath: string): Promise<TAppMetadata>;
25
+ private parseInfoPlist;
26
+ }
27
+ /**
28
+ * Extracts metadata from iOS .zip files containing .app bundles
29
+ */
30
+ export declare class IosZipMetadataExtractor implements IMetadataExtractor {
31
+ canHandle(filePath: string): boolean;
32
+ extract(filePath: string): Promise<TAppMetadata>;
33
+ private parseInfoPlist;
34
+ }
35
+ /**
36
+ * Service for extracting app metadata from various file formats
37
+ */
38
+ export declare class MetadataExtractorService {
39
+ private extractors;
40
+ /**
41
+ * Extract app metadata from a file
42
+ * @param filePath - Path to the app file (.apk, .app, or .zip)
43
+ * @returns App metadata or undefined if extraction fails
44
+ */
45
+ extract(filePath: string): Promise<TAppMetadata | undefined>;
46
+ }
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MetadataExtractorService = exports.IosZipMetadataExtractor = exports.IosAppMetadataExtractor = exports.AndroidMetadataExtractor = void 0;
4
+ const AppInfoParser = require("app-info-parser");
5
+ const bplist_parser_1 = require("bplist-parser");
6
+ const promises_1 = require("node:fs/promises");
7
+ const path = require("node:path");
8
+ const StreamZip = require("node-stream-zip");
9
+ const plist_1 = require("plist");
10
+ /**
11
+ * Extracts metadata from Android APK files
12
+ */
13
+ class AndroidMetadataExtractor {
14
+ canHandle(filePath) {
15
+ return filePath.endsWith('.apk');
16
+ }
17
+ async extract(filePath) {
18
+ const parser = new AppInfoParser(filePath);
19
+ const result = await parser.parse();
20
+ return { appId: result.package, platform: 'android' };
21
+ }
22
+ }
23
+ exports.AndroidMetadataExtractor = AndroidMetadataExtractor;
24
+ /**
25
+ * Extracts metadata from iOS .app directories
26
+ */
27
+ class IosAppMetadataExtractor {
28
+ canHandle(filePath) {
29
+ return filePath.endsWith('.app');
30
+ }
31
+ async extract(filePath) {
32
+ const infoPlistPath = path.normalize(path.join(filePath, 'Info.plist'));
33
+ const buffer = await (0, promises_1.readFile)(infoPlistPath);
34
+ const data = await this.parseInfoPlist(buffer);
35
+ const appId = data.CFBundleIdentifier;
36
+ return { appId, platform: 'ios' };
37
+ }
38
+ async parseInfoPlist(buffer) {
39
+ let data;
40
+ const bufferType = buffer[0];
41
+ if (bufferType === 60 ||
42
+ bufferType === '<' ||
43
+ bufferType === 239) {
44
+ data = (0, plist_1.parse)(buffer.toString());
45
+ }
46
+ else if (bufferType === 98) {
47
+ data = (0, bplist_parser_1.parseBuffer)(buffer)[0];
48
+ }
49
+ else {
50
+ throw new Error('Unknown plist buffer type.');
51
+ }
52
+ return data;
53
+ }
54
+ }
55
+ exports.IosAppMetadataExtractor = IosAppMetadataExtractor;
56
+ /**
57
+ * Extracts metadata from iOS .zip files containing .app bundles
58
+ */
59
+ class IosZipMetadataExtractor {
60
+ canHandle(filePath) {
61
+ return filePath.endsWith('.zip');
62
+ }
63
+ async extract(filePath) {
64
+ return new Promise((resolve, reject) => {
65
+ const zip = new StreamZip({ file: filePath });
66
+ zip.on('ready', () => {
67
+ // Get all entries and sort them by path depth
68
+ const entries = Object.values(zip.entries());
69
+ const sortedEntries = entries.sort((a, b) => {
70
+ const aDepth = a.name.split('/').length;
71
+ const bDepth = b.name.split('/').length;
72
+ return aDepth - bDepth;
73
+ });
74
+ // Find the first Info.plist in the shallowest directory
75
+ const infoPlist = sortedEntries.find((e) => e.name.endsWith('.app/Info.plist'));
76
+ if (!infoPlist) {
77
+ reject(new Error('Failed to find info plist'));
78
+ return;
79
+ }
80
+ const buffer = zip.entryDataSync(infoPlist.name);
81
+ this.parseInfoPlist(buffer)
82
+ .then((data) => {
83
+ const appId = data.CFBundleIdentifier;
84
+ zip.close();
85
+ resolve({ appId, platform: 'ios' });
86
+ })
87
+ .catch(reject);
88
+ });
89
+ zip.on('error', reject);
90
+ });
91
+ }
92
+ async parseInfoPlist(buffer) {
93
+ let data;
94
+ const bufferType = buffer[0];
95
+ if (bufferType === 60 ||
96
+ bufferType === '<' ||
97
+ bufferType === 239) {
98
+ data = (0, plist_1.parse)(buffer.toString());
99
+ }
100
+ else if (bufferType === 98) {
101
+ data = (0, bplist_parser_1.parseBuffer)(buffer)[0];
102
+ }
103
+ else {
104
+ throw new Error('Unknown plist buffer type.');
105
+ }
106
+ return data;
107
+ }
108
+ }
109
+ exports.IosZipMetadataExtractor = IosZipMetadataExtractor;
110
+ /**
111
+ * Service for extracting app metadata from various file formats
112
+ */
113
+ class MetadataExtractorService {
114
+ extractors = [
115
+ new AndroidMetadataExtractor(),
116
+ new IosZipMetadataExtractor(),
117
+ new IosAppMetadataExtractor(),
118
+ ];
119
+ /**
120
+ * Extract app metadata from a file
121
+ * @param filePath - Path to the app file (.apk, .app, or .zip)
122
+ * @returns App metadata or undefined if extraction fails
123
+ */
124
+ async extract(filePath) {
125
+ const extractor = this.extractors.find((e) => e.canHandle(filePath));
126
+ if (!extractor) {
127
+ return undefined;
128
+ }
129
+ try {
130
+ return await extractor.extract(filePath);
131
+ }
132
+ catch {
133
+ console.warn('Failed to extract app metadata, please share with support@devicecloud.dev so we can improve our parsing.');
134
+ return undefined;
135
+ }
136
+ }
137
+ }
138
+ exports.MetadataExtractorService = MetadataExtractorService;
@@ -0,0 +1,20 @@
1
+ export interface MoropoDownloadOptions {
2
+ apiKey: string;
3
+ branchName?: string;
4
+ debug?: boolean;
5
+ json?: boolean;
6
+ logger?: (message: string) => void;
7
+ quiet?: boolean;
8
+ }
9
+ /**
10
+ * Service for downloading and extracting Moropo tests from the Moropo API
11
+ */
12
+ export declare class MoropoService {
13
+ private readonly MOROPO_API_URL;
14
+ /**
15
+ * Download and extract Moropo tests from the API
16
+ * @param options Download configuration options
17
+ * @returns Path to the extracted Moropo tests directory
18
+ */
19
+ downloadAndExtract(options: MoropoDownloadOptions): Promise<string>;
20
+ }