@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/commands/cloud.d.ts +30 -0
- package/dist/commands/cloud.js +78 -44
- package/dist/commands/status.d.ts +0 -1
- package/dist/commands/status.js +10 -33
- package/dist/commands/upload.d.ts +1 -0
- package/dist/commands/upload.js +11 -9
- package/dist/config/flags/execution.flags.js +2 -2
- package/dist/gateways/api-gateway.d.ts +2 -2
- package/dist/gateways/supabase-gateway.d.ts +1 -1
- package/dist/gateways/supabase-gateway.js +51 -9
- package/dist/methods.d.ts +1 -1
- package/dist/methods.js +183 -25
- package/dist/services/execution-plan.service.d.ts +23 -0
- package/dist/services/execution-plan.service.js +41 -0
- package/dist/services/results-polling.service.d.ts +30 -0
- package/dist/services/results-polling.service.js +167 -71
- package/dist/services/test-submission.service.js +11 -0
- package/dist/types/generated/schema.types.d.ts +1086 -348
- package/dist/types/schema.types.d.ts +1523 -0
- package/dist/types/schema.types.js +3 -0
- package/dist/utils/styling.d.ts +106 -0
- package/dist/utils/styling.js +166 -0
- package/oclif.manifest.json +10 -3
- package/package.json +16 -15
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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 (
|
|
134
|
-
|
|
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(
|
|
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(
|
|
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 {};
|