@devicecloud.dev/dcd
Version:
Better cloud maestro testing
205 lines (204 loc) • 9.59 kB
JavaScript
;
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;