@devicecloud.dev/dcd 4.1.1 → 4.1.2

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.
@@ -14,6 +14,7 @@ const api_gateway_1 = require("../gateways/api-gateway");
14
14
  const methods_1 = require("../methods");
15
15
  const plan_1 = require("../plan");
16
16
  const compatibility_1 = require("../utils/compatibility");
17
+ const connectivity_1 = require("../utils/connectivity");
17
18
  const StreamZip = require("node-stream-zip");
18
19
  exports.mimeTypeLookupByExtension = {
19
20
  apk: 'application/vnd.android.package-archive',
@@ -308,7 +309,7 @@ class Cloud extends core_1.Command {
308
309
  if (debug) {
309
310
  this.log('DEBUG: Generating execution plan...');
310
311
  }
311
- executionPlan = await (0, plan_1.plan)(flowFile, includeTags.flat(), excludeTags.flat(), excludeFlows.flat(), configFile);
312
+ executionPlan = await (0, plan_1.plan)(flowFile, includeTags.flat(), excludeTags.flat(), excludeFlows.flat(), configFile, debug);
312
313
  if (debug) {
313
314
  this.log(`DEBUG: Execution plan generated`);
314
315
  this.log(`DEBUG: Total flow files: ${executionPlan.totalFlowFiles}`);
@@ -580,6 +581,7 @@ class Cloud extends core_1.Command {
580
581
  this.log('\nYou can safely close this terminal and the tests will continue\n');
581
582
  }
582
583
  let sequentialPollFaillures = 0;
584
+ let previousSummary = '';
583
585
  if (debug) {
584
586
  this.log(`DEBUG: Starting polling loop for results`);
585
587
  }
@@ -599,11 +601,34 @@ class Cloud extends core_1.Command {
599
601
  this.log(`DEBUG: Result status: ${result.test_file_name} - ${result.status}`);
600
602
  }
601
603
  }
602
- if (!quiet && !json) {
603
- core_1.ux.action.status =
604
- '\nStatus Test\n─────────── ───────────────';
605
- for (const { retry_of: isRetry, status, test_file_name: test, } of updatedResults) {
606
- core_1.ux.action.status += `\n${status.padEnd(10, ' ')} ${test} ${isRetry ? '(retry)' : ''}`;
604
+ // Calculate summary statistics
605
+ const statusCounts = {};
606
+ for (const result of updatedResults) {
607
+ statusCounts[result.status] = (statusCounts[result.status] || 0) + 1;
608
+ }
609
+ const passed = statusCounts.PASSED || 0;
610
+ const failed = statusCounts.FAILED || 0;
611
+ const pending = statusCounts.PENDING || 0;
612
+ const running = statusCounts.RUNNING || 0;
613
+ const total = updatedResults.length;
614
+ const completed = passed + failed;
615
+ // Display quantitative summary in quiet mode only
616
+ const summary = `${completed}/${total} | ✓ ${passed} | ✗ ${failed} | ▶ ${running} | ⏸ ${pending}`;
617
+ if (!json) {
618
+ if (quiet) {
619
+ // Only update status when the summary changes in quiet mode
620
+ if (summary !== previousSummary) {
621
+ core_1.ux.action.status = summary;
622
+ previousSummary = summary;
623
+ }
624
+ }
625
+ else {
626
+ // Show detailed table in non-quiet mode (no summary)
627
+ core_1.ux.action.status =
628
+ '\nStatus Test\n─────────── ───────────────';
629
+ for (const { retry_of: isRetry, status, test_file_name: test, } of updatedResults) {
630
+ core_1.ux.action.status += `\n${status.padEnd(10, ' ')} ${test} ${isRetry ? '(retry)' : ''}`;
631
+ }
607
632
  }
608
633
  }
609
634
  if (updatedResults.every((result) => !['PENDING', 'RUNNING'].includes(result.status))) {
@@ -728,6 +753,29 @@ class Cloud extends core_1.Command {
728
753
  if (sequentialPollFaillures > 10) {
729
754
  // dropped poll requests shouldn't err user CI
730
755
  clearInterval(intervalId);
756
+ // Check if the failure is due to internet connectivity issues
757
+ if (debug) {
758
+ this.log('DEBUG: Checking internet connectivity...');
759
+ }
760
+ const connectivityCheck = await (0, connectivity_1.checkInternetConnectivity)();
761
+ if (debug) {
762
+ this.log(`DEBUG: ${connectivityCheck.message}`);
763
+ for (const result of connectivityCheck.endpointResults) {
764
+ if (result.success) {
765
+ this.log(`DEBUG: ✓ ${result.endpoint} - ${result.statusCode} (${result.latencyMs}ms)`);
766
+ }
767
+ else {
768
+ this.log(`DEBUG: ✗ ${result.endpoint} - ${result.error} (${result.latencyMs}ms)`);
769
+ }
770
+ }
771
+ }
772
+ if (!connectivityCheck.connected) {
773
+ // Build detailed error message with endpoint diagnostics
774
+ const endpointDetails = connectivityCheck.endpointResults
775
+ .map((r) => ` - ${r.endpoint}: ${r.error} (${r.latencyMs}ms)`)
776
+ .join('\n');
777
+ throw new Error(`Unable to fetch results after 10 attempts.\n\nInternet connectivity check failed - all test endpoints unreachable:\n${endpointDetails}\n\nPlease verify your network connection and DNS resolution.`);
778
+ }
731
779
  throw new Error('unable to fetch results after 10 attempts');
732
780
  }
733
781
  this.log('unable to fetch results, trying again...');
@@ -1,7 +1,13 @@
1
1
  import { Command } from '@oclif/core';
2
+ import { ConnectivityCheckResult } from '../utils/connectivity';
2
3
  type StatusResponse = {
3
4
  appBinaryId?: string;
4
5
  attempts?: number;
6
+ connectivityCheck?: {
7
+ connected: boolean;
8
+ endpointResults: ConnectivityCheckResult['endpointResults'];
9
+ message: string;
10
+ };
5
11
  consoleUrl?: string;
6
12
  error?: string;
7
13
  status: 'CANCELLED' | 'FAILED' | 'PASSED' | 'PENDING' | 'RUNNING';
@@ -4,6 +4,7 @@ const core_1 = require("@oclif/core");
4
4
  const constants_1 = require("../constants");
5
5
  const api_gateway_1 = require("../gateways/api-gateway");
6
6
  const methods_1 = require("../methods");
7
+ const connectivity_1 = require("../utils/connectivity");
7
8
  class Status extends core_1.Command {
8
9
  static description = 'Get the status of an upload by name or upload ID';
9
10
  static enableJsonFlag = true;
@@ -64,10 +65,27 @@ class Status extends core_1.Command {
64
65
  }
65
66
  }
66
67
  if (!status) {
67
- const errorMessage = `Failed to get status after 5 attempts. Check your network. Last error: ${lastError?.message || 'Unknown error'}`;
68
+ // Check if the failure is due to internet connectivity issues
69
+ const connectivityCheck = await (0, connectivity_1.checkInternetConnectivity)();
70
+ let errorMessage;
71
+ if (connectivityCheck.connected) {
72
+ errorMessage = `Failed to get status after 5 attempts. Internet appears functional but unable to reach API. Last error: ${lastError?.message || 'Unknown error'}`;
73
+ }
74
+ else {
75
+ // Build detailed error message with endpoint diagnostics
76
+ const endpointDetails = connectivityCheck.endpointResults
77
+ .map((r) => ` - ${r.endpoint}: ${r.error} (${r.latencyMs}ms)`)
78
+ .join('\n');
79
+ errorMessage = `Failed to get status after 5 attempts.\n\nInternet connectivity check failed - all test endpoints unreachable:\n${endpointDetails}\n\nPlease verify your network connection and DNS resolution.\nLast API error: ${lastError?.message || 'Unknown error'}`;
80
+ }
68
81
  if (json) {
69
82
  return {
70
83
  attempts: 5,
84
+ connectivityCheck: {
85
+ connected: connectivityCheck.connected,
86
+ endpointResults: connectivityCheck.endpointResults,
87
+ message: connectivityCheck.message,
88
+ },
71
89
  error: errorMessage,
72
90
  status: 'FAILED',
73
91
  tests: [],
package/dist/plan.d.ts CHANGED
@@ -35,5 +35,5 @@ interface IFlowSequence {
35
35
  continueOnFailure?: boolean;
36
36
  flows: string[];
37
37
  }
38
- export declare function plan(input: string, includeTags: string[], excludeTags: string[], excludeFlows?: string[], configFile?: string): Promise<IExecutionPlan>;
38
+ export declare function plan(input: string, includeTags: string[], excludeTags: string[], excludeFlows?: string[], configFile?: string, debug?: boolean): Promise<IExecutionPlan>;
39
39
  export {};
package/dist/plan.js CHANGED
@@ -64,7 +64,7 @@ function extractDeviceCloudOverrides(config) {
64
64
  }
65
65
  return overrides;
66
66
  }
67
- async function plan(input, includeTags, excludeTags, excludeFlows, configFile) {
67
+ async function plan(input, includeTags, excludeTags, excludeFlows, configFile, debug = false) {
68
68
  const normalizedInput = path.normalize(input);
69
69
  const flowMetadata = {};
70
70
  if (!fs.existsSync(normalizedInput)) {
@@ -189,12 +189,30 @@ async function plan(input, includeTags, excludeTags, excludeFlows, configFile) {
189
189
  const config = configPerFlowFile[filePath];
190
190
  const name = config?.name || path.parse(filePath).name;
191
191
  acc[name] = filePath;
192
+ if (debug) {
193
+ console.log(`[DEBUG] Flow name mapping: "${name}" -> ${filePath}`);
194
+ }
192
195
  return acc;
193
196
  }, {});
197
+ if (debug && workspaceConfig.executionOrder?.flowsOrder) {
198
+ console.log('[DEBUG] executionOrder.flowsOrder:', workspaceConfig.executionOrder.flowsOrder);
199
+ console.log('[DEBUG] Available flow names:', Object.keys(pathsByName));
200
+ }
194
201
  const flowsToRunInSequence = workspaceConfig.executionOrder?.flowsOrder
195
- ?.map((flowOrder) => flowOrder.replace('.yaml', '').replace('.yml', '')) // support case where ext is left on
196
- ?.map((flowOrder) => (0, planMethods_1.getFlowsToRunInSequence)(pathsByName, [flowOrder]))
202
+ ?.map((flowOrder) => {
203
+ // Strip .yaml/.yml extension only from the END of the string
204
+ // This supports flowsOrder entries like "my_test.yml" matching "my_test"
205
+ // while preserving extensions in the middle like "(file.yml) Name"
206
+ const normalizedFlowOrder = flowOrder.replace(/\.ya?ml$/i, '');
207
+ if (debug && flowOrder !== normalizedFlowOrder) {
208
+ console.log(`[DEBUG] Stripping trailing extension: "${flowOrder}" -> "${normalizedFlowOrder}"`);
209
+ }
210
+ return (0, planMethods_1.getFlowsToRunInSequence)(pathsByName, [normalizedFlowOrder], debug);
211
+ })
197
212
  .flat() || [];
213
+ if (debug) {
214
+ console.log(`[DEBUG] Sequential flows resolved: ${flowsToRunInSequence.length} flow(s)`);
215
+ }
198
216
  const normalFlows = allFlows
199
217
  .filter((flow) => !flowsToRunInSequence.includes(flow))
200
218
  .sort((a, b) => a.localeCompare(b));
@@ -1,6 +1,6 @@
1
1
  export declare function getFlowsToRunInSequence(paths: {
2
2
  [key: string]: string;
3
- }, flowOrder: string[]): string[];
3
+ }, flowOrder: string[], debug?: boolean): string[];
4
4
  export declare function isFlowFile(filePath: string): boolean;
5
5
  export declare const readYamlFileAsJson: (filePath: string) => unknown;
6
6
  export declare const readTestYamlFileAsJson: (filePath: string) => {
@@ -9,25 +9,50 @@ const yaml = require("js-yaml");
9
9
  const fs = require("node:fs");
10
10
  const path = require("node:path");
11
11
  const commandsThatRequireFiles = new Set(['addMedia', 'runFlow', 'runScript']);
12
- function getFlowsToRunInSequence(paths, flowOrder) {
13
- if (flowOrder.length === 0)
12
+ function getFlowsToRunInSequence(paths, flowOrder, debug = false) {
13
+ if (flowOrder.length === 0) {
14
+ if (debug) {
15
+ console.log('[DEBUG] getFlowsToRunInSequence: flowOrder is empty, returning []');
16
+ }
14
17
  return [];
18
+ }
15
19
  const orderSet = new Set(flowOrder);
16
- const namesInOrder = Object.keys(paths).filter((key) => orderSet.has(key));
17
- if (namesInOrder.length === 0)
20
+ const availableNames = Object.keys(paths);
21
+ if (debug) {
22
+ console.log(`[DEBUG] getFlowsToRunInSequence: Looking for flows in order: [${[...orderSet].join(', ')}]`);
23
+ console.log(`[DEBUG] getFlowsToRunInSequence: Available flow names: [${availableNames.join(', ')}]`);
24
+ }
25
+ const namesInOrder = availableNames.filter((key) => orderSet.has(key));
26
+ if (debug) {
27
+ console.log(`[DEBUG] getFlowsToRunInSequence: Matched ${namesInOrder.length} flow(s): [${namesInOrder.join(', ')}]`);
28
+ }
29
+ if (namesInOrder.length === 0) {
30
+ const notFound = [...orderSet].filter((item) => !availableNames.includes(item));
31
+ if (debug) {
32
+ console.log(`[DEBUG] getFlowsToRunInSequence: No flows matched, not found: [${notFound.join(', ')}]`);
33
+ }
18
34
  return [];
35
+ }
19
36
  const result = [...orderSet].filter((item) => namesInOrder.includes(item));
20
37
  if (result.length === 0) {
21
- throw new Error(`Could not find flows needed for execution in order: ${[...orderSet]
22
- .filter((item) => !namesInOrder.includes(item))
23
- .join(', ')}`);
38
+ const notFound = [...orderSet].filter((item) => !namesInOrder.includes(item));
39
+ throw new Error(`Could not find flows needed for execution in order: ${notFound.join(', ')}\n\nAvailable flow names:\n${availableNames.join('\n')}`);
24
40
  }
25
41
  else if (flowOrder
26
42
  .slice(0, result.length)
27
43
  .every((value, index) => value === result[index])) {
28
- return result.map((item) => paths[item]);
44
+ const resolvedPaths = result.map((item) => paths[item]);
45
+ if (debug) {
46
+ console.log(`[DEBUG] getFlowsToRunInSequence: Order matches, returning ${resolvedPaths.length} path(s)`);
47
+ }
48
+ return resolvedPaths;
29
49
  }
30
50
  else {
51
+ if (debug) {
52
+ console.log('[DEBUG] getFlowsToRunInSequence: Order does not match, returning []');
53
+ console.log(`[DEBUG] Expected order: [${flowOrder.slice(0, result.length).join(', ')}]`);
54
+ console.log(`[DEBUG] Actual result: [${result.join(', ')}]`);
55
+ }
31
56
  return [];
32
57
  }
33
58
  }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Utility for checking internet connectivity using third-party endpoints
3
+ */
4
+ export type ConnectivityCheckResult = {
5
+ /** Whether internet connectivity was detected */
6
+ connected: boolean;
7
+ /** Detailed results for each endpoint tested */
8
+ endpointResults: Array<{
9
+ /** The endpoint URL that was tested */
10
+ endpoint: string;
11
+ /** Error message if request failed */
12
+ error?: string;
13
+ /** Time taken for the request in milliseconds */
14
+ latencyMs?: number;
15
+ /** HTTP status code if request succeeded */
16
+ statusCode?: number;
17
+ /** Whether this endpoint was reachable */
18
+ success: boolean;
19
+ }>;
20
+ /** Summary message for developers */
21
+ message: string;
22
+ };
23
+ /**
24
+ * Check if the system has internet connectivity by testing against
25
+ * multiple reliable third-party endpoints with detailed diagnostics.
26
+ *
27
+ * @returns Promise<ConnectivityCheckResult> - Detailed connectivity check results
28
+ */
29
+ export declare function checkInternetConnectivity(): Promise<ConnectivityCheckResult>;
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ /**
3
+ * Utility for checking internet connectivity using third-party endpoints
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.checkInternetConnectivity = checkInternetConnectivity;
7
+ /**
8
+ * Check if the system has internet connectivity by testing against
9
+ * multiple reliable third-party endpoints with detailed diagnostics.
10
+ *
11
+ * @returns Promise<ConnectivityCheckResult> - Detailed connectivity check results
12
+ */
13
+ async function checkInternetConnectivity() {
14
+ // Use multiple reliable endpoints to test connectivity
15
+ const testEndpoints = [
16
+ { url: 'https://www.google.com/generate_204', description: 'Google' },
17
+ { url: 'https://www.cloudflare.com/cdn-cgi/trace', description: 'Cloudflare' },
18
+ { url: 'https://1.1.1.1/', description: 'Cloudflare DNS' },
19
+ ];
20
+ const endpointResults = [];
21
+ let anySuccess = false;
22
+ // Try each endpoint with a short timeout
23
+ for (const { url, description } of testEndpoints) {
24
+ const startTime = Date.now();
25
+ try {
26
+ const controller = new AbortController();
27
+ const timeoutId = setTimeout(() => controller.abort(), 3000); // 3 second timeout
28
+ const response = await fetch(url, {
29
+ method: 'HEAD', // Use HEAD to minimize data transfer
30
+ signal: controller.signal,
31
+ // Disable redirects to get faster response
32
+ redirect: 'manual',
33
+ });
34
+ clearTimeout(timeoutId);
35
+ const latencyMs = Date.now() - startTime;
36
+ // Any response (including 3xx redirects) indicates connectivity
37
+ if (response) {
38
+ anySuccess = true;
39
+ endpointResults.push({
40
+ endpoint: `${description} (${url})`,
41
+ success: true,
42
+ statusCode: response.status,
43
+ latencyMs,
44
+ });
45
+ // Found working connection, no need to test more
46
+ break;
47
+ }
48
+ }
49
+ catch (error) {
50
+ const latencyMs = Date.now() - startTime;
51
+ let errorMessage = 'Unknown error';
52
+ if (error instanceof Error) {
53
+ if (error.name === 'AbortError') {
54
+ errorMessage = 'Request timeout (>3s)';
55
+ }
56
+ else if (error.message.includes('fetch failed')) {
57
+ errorMessage = 'Network request failed (DNS/connection error)';
58
+ }
59
+ else if (error.message.includes('ENOTFOUND')) {
60
+ errorMessage = 'DNS resolution failed';
61
+ }
62
+ else if (error.message.includes('ECONNREFUSED')) {
63
+ errorMessage = 'Connection refused';
64
+ }
65
+ else if (error.message.includes('ETIMEDOUT')) {
66
+ errorMessage = 'Connection timeout';
67
+ }
68
+ else if (error.message.includes('ENETUNREACH')) {
69
+ errorMessage = 'Network unreachable';
70
+ }
71
+ else {
72
+ errorMessage = error.message;
73
+ }
74
+ }
75
+ endpointResults.push({
76
+ endpoint: `${description} (${url})`,
77
+ success: false,
78
+ error: errorMessage,
79
+ latencyMs,
80
+ });
81
+ // Continue to next endpoint if this one fails
82
+ continue;
83
+ }
84
+ }
85
+ // Generate developer-friendly message
86
+ let message;
87
+ if (anySuccess) {
88
+ const successfulEndpoint = endpointResults.find((r) => r.success);
89
+ message = `Internet connectivity verified via ${successfulEndpoint?.endpoint} (${successfulEndpoint?.latencyMs}ms)`;
90
+ }
91
+ else {
92
+ const testedEndpoints = endpointResults.map((r) => r.endpoint).join(', ');
93
+ message = `No internet connectivity detected. Tested endpoints: ${testedEndpoints}`;
94
+ }
95
+ return {
96
+ connected: anySuccess,
97
+ endpointResults,
98
+ message,
99
+ };
100
+ }
@@ -579,5 +579,5 @@
579
579
  ]
580
580
  }
581
581
  },
582
- "version": "4.1.1"
582
+ "version": "4.1.2"
583
583
  }
package/package.json CHANGED
@@ -72,7 +72,7 @@
72
72
  "type": "git",
73
73
  "url": "https://devicecloud.dev"
74
74
  },
75
- "version": "4.1.1",
75
+ "version": "4.1.2",
76
76
  "bugs": {
77
77
  "url": "https://discord.gg/gm3mJwcNw8"
78
78
  },