@devicecloud.dev/dcd 4.1.4 → 4.1.6

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/dist/methods.js CHANGED
@@ -12,6 +12,7 @@ const StreamZip = require("node-stream-zip");
12
12
  const api_gateway_1 = require("./gateways/api-gateway");
13
13
  const supabase_gateway_1 = require("./gateways/supabase-gateway");
14
14
  const metadata_extractor_service_1 = require("./services/metadata-extractor.service");
15
+ const styling_1 = require("./utils/styling");
15
16
  const mimeTypeLookupByExtension = {
16
17
  apk: 'application/vnd.android.package-archive',
17
18
  yaml: 'application/x-yaml',
@@ -76,66 +77,223 @@ const verifyAppZip = async (zipPath) => {
76
77
  zip.close();
77
78
  };
78
79
  exports.verifyAppZip = verifyAppZip;
79
- const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false, log = true) => {
80
+ const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false, log = true, debug = false) => {
80
81
  if (log) {
81
- core_1.ux.action.start('Checking and uploading binary', 'Initializing', {
82
+ core_1.ux.action.start(styling_1.colors.bold('Checking and uploading binary'), styling_1.colors.dim('Initializing'), {
82
83
  stdout: true,
83
84
  });
84
85
  }
86
+ if (debug) {
87
+ console.log('[DEBUG] Binary upload started');
88
+ console.log(`[DEBUG] File path: ${filePath}`);
89
+ console.log(`[DEBUG] API URL: ${apiUrl}`);
90
+ console.log(`[DEBUG] Ignore SHA check: ${ignoreShaCheck}`);
91
+ }
92
+ const startTime = Date.now();
93
+ try {
94
+ // Prepare file for upload
95
+ const file = await prepareFileForUpload(filePath, debug, startTime);
96
+ // Calculate SHA hash
97
+ const sha = await calculateFileHash(file, debug, log);
98
+ // Check for existing upload with same SHA
99
+ if (!ignoreShaCheck && sha) {
100
+ const { exists, binaryId } = await checkExistingUpload(apiUrl, apiKey, sha, debug);
101
+ if (exists && binaryId) {
102
+ if (log) {
103
+ core_1.ux.info(styling_1.colors.dim('SHA hash matches existing binary with ID: ') + (0, styling_1.formatId)(binaryId) + styling_1.colors.dim(', skipping upload. Force upload with --ignore-sha-check'));
104
+ core_1.ux.action.stop(styling_1.colors.info('Skipping upload'));
105
+ }
106
+ return binaryId;
107
+ }
108
+ }
109
+ // Perform the upload
110
+ const uploadId = await performUpload(filePath, apiUrl, apiKey, file, sha, debug, startTime);
111
+ if (log) {
112
+ core_1.ux.action.stop(styling_1.colors.success('\n✓ Binary uploaded with ID: ') + (0, styling_1.formatId)(uploadId));
113
+ }
114
+ return uploadId;
115
+ }
116
+ catch (error) {
117
+ if (debug) {
118
+ console.error('[DEBUG] Binary upload failed:', error);
119
+ console.error(`[DEBUG] Error type: ${error instanceof Error ? error.name : typeof error}`);
120
+ console.error(`[DEBUG] Error message: ${error instanceof Error ? error.message : String(error)}`);
121
+ if (error instanceof Error && error.stack) {
122
+ console.error(`[DEBUG] Stack trace: ${error.stack}`);
123
+ }
124
+ console.error(`[DEBUG] Failed after ${Date.now() - startTime}ms`);
125
+ }
126
+ throw error;
127
+ }
128
+ };
129
+ exports.uploadBinary = uploadBinary;
130
+ /**
131
+ * Prepares a file for upload by reading or compressing it
132
+ * @param filePath Path to the file to upload
133
+ * @param debug Whether debug logging is enabled
134
+ * @param startTime Timestamp when upload started
135
+ * @returns Promise resolving to prepared File object
136
+ */
137
+ async function prepareFileForUpload(filePath, debug, startTime) {
138
+ if (debug) {
139
+ console.log('[DEBUG] Preparing file for upload...');
140
+ }
85
141
  let file;
86
142
  if (filePath?.endsWith('.app')) {
143
+ if (debug) {
144
+ console.log('[DEBUG] Compressing .app folder to zip...');
145
+ }
87
146
  const zippedAppBlob = await (0, exports.compressFolderToBlob)(filePath);
88
147
  file = new File([zippedAppBlob], filePath + '.zip');
148
+ if (debug) {
149
+ console.log(`[DEBUG] Compressed file size: ${(zippedAppBlob.size / 1024 / 1024).toFixed(2)} MB`);
150
+ }
89
151
  }
90
152
  else {
153
+ if (debug) {
154
+ console.log('[DEBUG] Reading binary file...');
155
+ }
91
156
  const fileBuffer = await (0, promises_1.readFile)(filePath);
157
+ if (debug) {
158
+ console.log(`[DEBUG] File size: ${(fileBuffer.length / 1024 / 1024).toFixed(2)} MB`);
159
+ }
92
160
  const binaryBlob = new Blob([new Uint8Array(fileBuffer)], {
93
161
  type: mimeTypeLookupByExtension[filePath.split('.').pop()],
94
162
  });
95
163
  file = new File([binaryBlob], filePath);
96
164
  }
97
- let sha;
165
+ if (debug) {
166
+ console.log(`[DEBUG] File preparation completed in ${Date.now() - startTime}ms`);
167
+ }
168
+ return file;
169
+ }
170
+ /**
171
+ * Calculates SHA-256 hash for a file
172
+ * @param file File to calculate hash for
173
+ * @param debug Whether debug logging is enabled
174
+ * @param log Whether to log warnings
175
+ * @returns Promise resolving to SHA-256 hash or undefined if failed
176
+ */
177
+ async function calculateFileHash(file, debug, log) {
98
178
  try {
99
- sha = await getFileHashFromFile(file);
179
+ if (debug) {
180
+ console.log('[DEBUG] Calculating SHA-256 hash...');
181
+ }
182
+ const hashStartTime = Date.now();
183
+ const sha = await getFileHashFromFile(file);
184
+ if (debug) {
185
+ console.log(`[DEBUG] SHA-256 hash: ${sha}`);
186
+ console.log(`[DEBUG] Hash calculation completed in ${Date.now() - hashStartTime}ms`);
187
+ }
188
+ return sha;
100
189
  }
101
190
  catch (error) {
102
191
  if (log) {
103
192
  console.warn('Warning: Failed to get file hash', error);
104
193
  }
194
+ if (debug) {
195
+ console.error('[DEBUG] Hash calculation failed:', error);
196
+ }
197
+ return undefined;
105
198
  }
106
- if (!ignoreShaCheck && sha) {
107
- try {
108
- const { appBinaryId, exists } = await api_gateway_1.ApiGateway.checkForExistingUpload(apiUrl, apiKey, sha);
199
+ }
200
+ /**
201
+ * Checks if an upload with the same SHA already exists
202
+ * @param apiUrl API base URL
203
+ * @param apiKey API authentication key
204
+ * @param sha SHA-256 hash to check
205
+ * @param debug Whether debug logging is enabled
206
+ * @returns Promise resolving to object with exists flag and optional binaryId
207
+ */
208
+ async function checkExistingUpload(apiUrl, apiKey, sha, debug) {
209
+ try {
210
+ if (debug) {
211
+ console.log('[DEBUG] Checking for existing upload with matching SHA...');
212
+ console.log(`[DEBUG] Target endpoint: ${apiUrl}/uploads/checkForExistingUpload`);
213
+ }
214
+ const shaCheckStartTime = Date.now();
215
+ const { appBinaryId, exists } = await api_gateway_1.ApiGateway.checkForExistingUpload(apiUrl, apiKey, sha);
216
+ if (debug) {
217
+ console.log(`[DEBUG] SHA check completed in ${Date.now() - shaCheckStartTime}ms`);
218
+ console.log(`[DEBUG] Existing binary found: ${exists}`);
109
219
  if (exists) {
110
- if (log) {
111
- core_1.ux.info(`sha hash matches existing binary with id: ${appBinaryId}, skipping upload. Force upload with --ignore-sha-check`);
112
- core_1.ux.action.stop(`Skipping upload.`);
113
- }
114
- return appBinaryId;
220
+ console.log(`[DEBUG] Existing binary ID: ${appBinaryId}`);
115
221
  }
116
222
  }
117
- catch {
118
- // ignore error
223
+ return { binaryId: appBinaryId, exists };
224
+ }
225
+ catch (error) {
226
+ if (debug) {
227
+ console.error('[DEBUG] SHA check failed (continuing with upload):', error);
228
+ console.error(`[DEBUG] Error type: ${error instanceof Error ? error.name : typeof error}`);
229
+ console.error(`[DEBUG] Error message: ${error instanceof Error ? error.message : String(error)}`);
119
230
  }
231
+ return { exists: false };
232
+ }
233
+ }
234
+ /**
235
+ * Performs the actual file upload
236
+ * @param filePath Path to the file being uploaded
237
+ * @param apiUrl API base URL
238
+ * @param apiKey API authentication key
239
+ * @param file Prepared file to upload
240
+ * @param sha SHA-256 hash of the file
241
+ * @param debug Whether debug logging is enabled
242
+ * @param startTime Timestamp when upload started
243
+ * @returns Promise resolving to upload ID
244
+ */
245
+ async function performUpload(filePath, apiUrl, apiKey, file, sha, debug, startTime) {
246
+ if (debug) {
247
+ console.log('[DEBUG] Requesting upload URL...');
248
+ console.log(`[DEBUG] Target endpoint: ${apiUrl}/uploads/getBinaryUploadUrl`);
249
+ console.log(`[DEBUG] Platform: ${filePath?.endsWith('.apk') ? 'android' : 'ios'}`);
120
250
  }
251
+ const urlRequestStartTime = Date.now();
121
252
  const { id, message, path, token } = await api_gateway_1.ApiGateway.getBinaryUploadUrl(apiUrl, apiKey, filePath?.endsWith('.apk') ? 'android' : 'ios');
253
+ if (debug) {
254
+ console.log(`[DEBUG] Upload URL request completed in ${Date.now() - urlRequestStartTime}ms`);
255
+ console.log(`[DEBUG] Upload ID: ${id}`);
256
+ console.log(`[DEBUG] Upload path: ${path}`);
257
+ }
122
258
  if (!path)
123
259
  throw new Error(message);
124
260
  // Extract app metadata using the service
261
+ if (debug) {
262
+ console.log('[DEBUG] Extracting app metadata...');
263
+ }
125
264
  const metadataExtractor = new metadata_extractor_service_1.MetadataExtractorService();
126
265
  const metadata = await metadataExtractor.extract(filePath);
127
266
  if (!metadata) {
128
267
  throw new Error(`Failed to extract metadata from ${filePath}. Supported formats: .apk, .app, .zip`);
129
268
  }
269
+ if (debug) {
270
+ console.log(`[DEBUG] Metadata extracted: ${JSON.stringify(metadata)}`);
271
+ }
130
272
  const env = apiUrl === 'https://api.devicecloud.dev' ? 'prod' : 'dev';
131
- await supabase_gateway_1.SupabaseGateway.uploadToSignedUrl(env, path, token, file);
273
+ if (debug) {
274
+ console.log(`[DEBUG] Uploading to Supabase storage (${env})...`);
275
+ console.log(`[DEBUG] File size: ${(file.size / 1024 / 1024).toFixed(2)} MB`);
276
+ }
277
+ const uploadStartTime = Date.now();
278
+ await supabase_gateway_1.SupabaseGateway.uploadToSignedUrl(env, path, token, file, debug);
279
+ if (debug) {
280
+ const uploadDuration = Date.now() - uploadStartTime;
281
+ const uploadSpeed = (file.size / 1024 / 1024) / (uploadDuration / 1000);
282
+ console.log(`[DEBUG] File upload completed in ${uploadDuration}ms`);
283
+ console.log(`[DEBUG] Average upload speed: ${uploadSpeed.toFixed(2)} MB/s`);
284
+ }
285
+ if (debug) {
286
+ console.log('[DEBUG] Finalizing upload...');
287
+ console.log(`[DEBUG] Target endpoint: ${apiUrl}/uploads/finaliseUpload`);
288
+ }
289
+ const finalizeStartTime = Date.now();
132
290
  await api_gateway_1.ApiGateway.finaliseUpload(apiUrl, apiKey, id, metadata, path, sha);
133
- if (log) {
134
- core_1.ux.action.stop(`\nBinary uploaded with id: ${id}`);
291
+ if (debug) {
292
+ console.log(`[DEBUG] Upload finalization completed in ${Date.now() - finalizeStartTime}ms`);
293
+ console.log(`[DEBUG] Total upload time: ${Date.now() - startTime}ms`);
135
294
  }
136
295
  return id;
137
- };
138
- exports.uploadBinary = uploadBinary;
296
+ }
139
297
  async function getFileHashFromFile(file) {
140
298
  return new Promise((resolve, reject) => {
141
299
  const hash = (0, node_crypto_1.createHash)('sha256');
@@ -168,21 +326,21 @@ async function getFileHashFromFile(file) {
168
326
  const writeJSONFile = (filePath, data, logger) => {
169
327
  try {
170
328
  (0, node_fs_1.writeFileSync)(filePath, JSON.stringify(data, null, 2));
171
- logger.log(`JSON output written to: ${path.resolve(filePath)}`);
329
+ logger.log(styling_1.colors.dim('JSON output written to: ') + styling_1.colors.highlight(path.resolve(filePath)));
172
330
  }
173
331
  catch (error) {
174
332
  const errorMessage = error instanceof Error ? error.message : String(error);
175
333
  const isPermissionError = errorMessage.includes('EACCES') || errorMessage.includes('EPERM');
176
334
  const isNoSuchFileError = errorMessage.includes('ENOENT');
177
- logger.warn(`Failed to write JSON output to file: ${filePath}`);
335
+ logger.warn(styling_1.colors.warning('⚠') + ' ' + styling_1.colors.error(`Failed to write JSON output to file: ${filePath}`));
178
336
  if (isPermissionError) {
179
- logger.warn('Permission denied - check file/directory write permissions');
180
- logger.warn('Try running with appropriate permissions or choose a different output location');
337
+ logger.warn(styling_1.colors.dim(' Permission denied - check file/directory write permissions'));
338
+ logger.warn(styling_1.colors.dim(' Try running with appropriate permissions or choose a different output location'));
181
339
  }
182
340
  else if (isNoSuchFileError) {
183
- logger.warn('Directory does not exist - create the directory first or choose an existing path');
341
+ logger.warn(styling_1.colors.dim(' Directory does not exist - create the directory first or choose an existing path'));
184
342
  }
185
- logger.warn(`Error details: ${errorMessage}`);
343
+ logger.warn(styling_1.colors.dim(' Error details: ') + errorMessage);
186
344
  }
187
345
  };
188
346
  exports.writeJSONFile = writeJSONFile;
@@ -1,3 +1,4 @@
1
+ /** Email notification configuration */
1
2
  interface INotificationsConfig {
2
3
  email?: {
3
4
  enabled?: boolean;
@@ -5,6 +6,7 @@ interface INotificationsConfig {
5
6
  recipients?: string[];
6
7
  };
7
8
  }
9
+ /** Workspace configuration from config.yaml */
8
10
  interface IWorkspaceConfig {
9
11
  excludeTags?: null | string[];
10
12
  executionOrder?: IExecutionOrder | null;
@@ -13,13 +15,16 @@ interface IWorkspaceConfig {
13
15
  local?: ILocal | null;
14
16
  notifications?: INotificationsConfig;
15
17
  }
18
+ /** Local execution configuration */
16
19
  interface ILocal {
17
20
  deterministicOrder: boolean | null;
18
21
  }
22
+ /** Sequential execution configuration */
19
23
  interface IExecutionOrder {
20
24
  continueOnFailure: boolean;
21
25
  flowsOrder: string[];
22
26
  }
27
+ /** Options for execution plan generation */
23
28
  export interface PlanOptions {
24
29
  configFile?: string;
25
30
  debug?: boolean;
@@ -28,6 +33,7 @@ export interface PlanOptions {
28
33
  includeTags?: string[];
29
34
  input: string;
30
35
  }
36
+ /** Execution plan containing all flows to run with metadata and dependencies */
31
37
  export interface IExecutionPlan {
32
38
  allExcludeTags?: null | string[];
33
39
  allIncludeTags?: null | string[];
@@ -39,9 +45,26 @@ export interface IExecutionPlan {
39
45
  totalFlowFiles: number;
40
46
  workspaceConfig?: IWorkspaceConfig;
41
47
  }
48
+ /** Flow sequence configuration for ordered execution */
42
49
  interface IFlowSequence {
43
50
  continueOnFailure?: boolean;
44
51
  flows: string[];
45
52
  }
53
+ /**
54
+ * Generate execution plan for test flows
55
+ *
56
+ * Handles:
57
+ * - Single file or directory input
58
+ * - Workspace configuration (config.yaml)
59
+ * - Flow inclusion/exclusion patterns
60
+ * - Tag-based filtering (include/exclude)
61
+ * - Dependency resolution (runFlow, scripts, media)
62
+ * - Sequential execution ordering
63
+ * - DeviceCloud-specific overrides
64
+ *
65
+ * @param options - Plan generation options
66
+ * @returns Complete execution plan with flows, dependencies, and metadata
67
+ * @throws Error if input path doesn't exist, no flows found, or dependencies missing
68
+ */
46
69
  export declare function plan(options: PlanOptions): Promise<IExecutionPlan>;
47
70
  export {};
@@ -5,6 +5,13 @@ const glob_1 = require("glob");
5
5
  const fs = require("node:fs");
6
6
  const path = require("node:path");
7
7
  const execution_plan_utils_1 = require("./execution-plan.utils");
8
+ /**
9
+ * Recursively check and resolve all dependencies for a flow file
10
+ * Includes runFlow references, JavaScript scripts, and media files
11
+ * @param input - Path to flow file to check
12
+ * @returns Array of all dependency file paths (deduplicated)
13
+ * @throws Error if any referenced files are missing
14
+ */
8
15
  async function checkDependencies(input) {
9
16
  const checkedDependencies = [];
10
17
  const uncheckedDependencies = [input];
@@ -35,12 +42,24 @@ async function checkDependencies(input) {
35
42
  }
36
43
  return checkedDependencies;
37
44
  }
45
+ /**
46
+ * Filter flow files based on exclude patterns
47
+ * @param unfilteredFlowFiles - All discovered flow files
48
+ * @param excludeFlows - Patterns to exclude
49
+ * @returns Filtered array of flow file paths
50
+ */
38
51
  function filterFlowFiles(unfilteredFlowFiles, excludeFlows) {
39
52
  if (excludeFlows) {
40
53
  return unfilteredFlowFiles.filter((file) => !excludeFlows.some((flow) => path.normalize(file).startsWith(path.normalize(path.resolve(flow)))));
41
54
  }
42
55
  return unfilteredFlowFiles;
43
56
  }
57
+ /**
58
+ * Load workspace configuration from config.yaml/yml if present
59
+ * @param input - Input directory path
60
+ * @param unfilteredFlowFiles - List of discovered flow files
61
+ * @returns Workspace configuration object (empty if no config file found)
62
+ */
44
63
  function getWorkspaceConfig(input, unfilteredFlowFiles) {
45
64
  const possibleConfigPaths = new Set([path.join(input, 'config.yaml'), path.join(input, 'config.yml')].map((p) => path.normalize(p)));
46
65
  const configFilePath = unfilteredFlowFiles.find((file) => possibleConfigPaths.has(path.normalize(file)));
@@ -49,6 +68,12 @@ function getWorkspaceConfig(input, unfilteredFlowFiles) {
49
68
  : {};
50
69
  return config;
51
70
  }
71
+ /**
72
+ * Extract DeviceCloud-specific override environment variables
73
+ * Looks for env vars prefixed with DEVICECLOUD_OVERRIDE_
74
+ * @param config - Flow configuration object
75
+ * @returns Object containing override key-value pairs
76
+ */
52
77
  function extractDeviceCloudOverrides(config) {
53
78
  if (!config || !config.env || typeof config.env !== 'object') {
54
79
  return {};
@@ -64,6 +89,22 @@ function extractDeviceCloudOverrides(config) {
64
89
  }
65
90
  return overrides;
66
91
  }
92
+ /**
93
+ * Generate execution plan for test flows
94
+ *
95
+ * Handles:
96
+ * - Single file or directory input
97
+ * - Workspace configuration (config.yaml)
98
+ * - Flow inclusion/exclusion patterns
99
+ * - Tag-based filtering (include/exclude)
100
+ * - Dependency resolution (runFlow, scripts, media)
101
+ * - Sequential execution ordering
102
+ * - DeviceCloud-specific overrides
103
+ *
104
+ * @param options - Plan generation options
105
+ * @returns Complete execution plan with flows, dependencies, and metadata
106
+ * @throws Error if input path doesn't exist, no flows found, or dependencies missing
107
+ */
67
108
  async function plan(options) {
68
109
  const { input, includeTags = [], excludeTags = [], excludeFlows, configFile, debug = false, } = options;
69
110
  const normalizedInput = path.normalize(input);
@@ -44,8 +44,38 @@ export declare class ResultsPollingService {
44
44
  private buildPollingResult;
45
45
  private calculateStatusSummary;
46
46
  private displayFinalResults;
47
+ /**
48
+ * Fetch results from API and log debug information
49
+ * @param apiUrl API base URL
50
+ * @param apiKey API authentication key
51
+ * @param uploadId Upload ID to fetch results for
52
+ * @param debug Whether debug logging is enabled
53
+ * @param logger Optional logger function
54
+ * @returns Promise resolving to test results
55
+ */
56
+ private fetchAndLogResults;
47
57
  private filterLatestResults;
58
+ /**
59
+ * Handle completed tests and return final result
60
+ * @param updatedResults Test results from API
61
+ * @param options Completion handling options
62
+ * @returns Promise resolving to final polling result
63
+ */
64
+ private handleCompletedTests;
48
65
  private handlePollingError;
66
+ /**
67
+ * Initialize the polling display UI
68
+ * @param json Whether to output in JSON format
69
+ * @param logger Optional logger function for output
70
+ * @returns void
71
+ */
72
+ private initializePollingDisplay;
73
+ /**
74
+ * Sleep for the specified number of milliseconds
75
+ * @param ms Number of milliseconds to sleep
76
+ * @returns Promise that resolves after the delay
77
+ */
78
+ private sleep;
49
79
  private updateDisplayStatus;
50
80
  }
51
81
  export {};