UNPKG

@devicecloud.dev/dcd

Version:

Better cloud maestro testing

205 lines (204 loc) 9.59 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const core_1 = require("@oclif/core"); const constants_1 = require("../constants"); const api_gateway_1 = require("../gateways/api-gateway"); const methods_1 = require("../methods"); const connectivity_1 = require("../utils/connectivity"); const styling_1 = require("../utils/styling"); class Status extends core_1.Command { static description = 'Get the status of an upload by name or upload ID'; static enableJsonFlag = true; static examples = [ '<%= config.bin %> <%= command.id %> --name my-upload-name', '<%= config.bin %> <%= command.id %> --upload-id 123e4567-e89b-12d3-a456-426614174000 --json', ]; static flags = { apiKey: constants_1.flags.apiKey, apiUrl: constants_1.flags.apiUrl, json: core_1.Flags.boolean({ description: 'output in json format', }), name: core_1.Flags.string({ description: 'Name of the upload to check status for', exclusive: ['upload-id'], }), 'upload-id': core_1.Flags.string({ description: 'UUID of the upload to check status for', exclusive: ['name'], }), }; // eslint-disable-next-line complexity async run() { const { flags } = await this.parse(Status); const { apiKey: apiKeyFlag, apiUrl, json, name, 'upload-id': uploadId, } = flags; const apiKey = apiKeyFlag || process.env.DEVICE_CLOUD_API_KEY; if (!apiKey) { this.error('API key is required. Please provide it via --api-key flag or DEVICE_CLOUD_API_KEY environment variable.'); return; } if (name && uploadId) { this.error('Cannot provide both --name and --upload-id. These options are mutually exclusive.'); return; } if (!name && !uploadId) { this.error('Either --name or --upload-id must be provided'); return; } let lastError = null; let status = null; let attemptsMade = 0; for (let attempt = 1; attempt <= 5; attempt++) { try { attemptsMade = attempt; status = (await api_gateway_1.ApiGateway.getUploadStatus(apiUrl, apiKey, { name, uploadId, })); break; } catch (error) { lastError = error; // Check if this is a retryable error (network/timeout issues) // Non-retryable errors: 4xx client errors (bad request, not found, unauthorized, forbidden) const isNetworkError = lastError.name === 'NetworkError' || (error instanceof TypeError && lastError.message === 'fetch failed'); const isClientError = lastError.message.includes('Invalid request:') || lastError.message.includes('Resource not found') || lastError.message.includes('Authentication failed') || lastError.message.includes('Access denied') || lastError.message.includes('Invalid API key') || lastError.message.includes('Rate limit exceeded'); // Don't retry client errors - they won't succeed on retry if (isClientError) { break; } // Only retry network errors if (attempt < 5 && isNetworkError) { this.log(`Network error on attempt ${attempt}/5. Retrying...`); await new Promise((resolve) => { setTimeout(resolve, 1000 * attempt); }); } else if (attempt < 5) { // For other errors (server errors), retry but with different message this.log(`Request failed on attempt ${attempt}/5. Retrying...`); await new Promise((resolve) => { setTimeout(resolve, 1000 * attempt); }); } } } if (!status) { // Check if this was a client error (non-retryable) const isClientError = lastError && (lastError.message.includes('Invalid request:') || lastError.message.includes('Resource not found') || lastError.message.includes('Authentication failed') || lastError.message.includes('Access denied') || lastError.message.includes('Invalid API key') || lastError.message.includes('Rate limit exceeded')); if (isClientError) { // For client errors, show the error immediately without connectivity check const errorMessage = lastError?.message || 'Unknown error'; if (json) { return { status: 'FAILED', error: errorMessage, attempts: attemptsMade, tests: [], }; } this.error(errorMessage); } // Check if the failure is due to internet connectivity issues const connectivityCheck = await (0, connectivity_1.checkInternetConnectivity)(); let errorMessage; if (connectivityCheck.connected) { errorMessage = `Failed to get status after ${attemptsMade} attempt${attemptsMade > 1 ? 's' : ''}. Internet appears functional but unable to reach API. Last error: ${lastError?.message || 'Unknown error'}`; } else { // Build detailed error message with endpoint diagnostics const endpointDetails = connectivityCheck.endpointResults .map((r) => ` - ${r.endpoint}: ${r.error} (${r.latencyMs}ms)`) .join('\n'); errorMessage = `Failed to get status after ${attemptsMade} attempt${attemptsMade > 1 ? 's' : ''}.\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'}`; } if (json) { return { status: 'FAILED', error: errorMessage, attempts: attemptsMade, connectivityCheck: { connected: connectivityCheck.connected, endpointResults: connectivityCheck.endpointResults, message: connectivityCheck.message, }, tests: [], }; } this.error(errorMessage); } try { if (json) { // Reconstruct object to ensure tests appears at the bottom const { tests, ...rest } = status; return { ...rest, tests, }; } this.log((0, styling_1.sectionHeader)('Upload Status')); // Display overall status this.log(` ${(0, styling_1.formatStatus)(status.status)}`); if (status.name) { this.log(` ${styling_1.colors.dim('Name:')} ${styling_1.colors.bold(status.name)}`); } if (status.uploadId) { this.log(` ${styling_1.colors.dim('Upload ID:')} ${(0, styling_1.formatId)(status.uploadId)}`); } if (status.appBinaryId) { this.log(` ${styling_1.colors.dim('Binary ID:')} ${(0, styling_1.formatId)(status.appBinaryId)}`); } if (status.createdAt) { this.log(` ${styling_1.colors.dim('Created:')} ${this.formatDateTime(status.createdAt)}`); } if (status.consoleUrl) { this.log(` ${styling_1.colors.dim('Console:')} ${(0, styling_1.formatUrl)(status.consoleUrl)}`); } if (status.tests.length > 0) { this.log((0, styling_1.sectionHeader)('Test Results')); for (const item of status.tests) { this.log(` ${(0, styling_1.formatStatus)(item.status)} ${styling_1.colors.bold(item.name)}`); if (item.status === 'FAILED' && item.failReason) { this.log(` ${styling_1.colors.error('Fail reason:')} ${item.failReason}`); } if (item.durationSeconds) { this.log(` ${styling_1.colors.dim('Duration:')} ${(0, methods_1.formatDurationSeconds)(item.durationSeconds)}`); } if (item.createdAt) { this.log(` ${styling_1.colors.dim('Created:')} ${this.formatDateTime(item.createdAt)}`); } this.log(''); } } } catch (error) { this.error(`Failed to get status: ${error.message}`); } } /** * Format an ISO date string to a human-readable local date/time * @param isoString - ISO 8601 date string * @returns Formatted local date/time string */ formatDateTime(isoString) { try { const date = new Date(isoString); return date.toLocaleString(); } catch { return isoString; } } } exports.default = Status;