UNPKG

@vizzly-testing/cli

Version:

Visual review platform for UI developers and designers

198 lines (183 loc) 6.79 kB
/** * Status command implementation * Uses functional API operations directly */ import { createApiClient, getBuild } from '../api/index.js'; import { loadConfig } from '../utils/config-loader.js'; import { getApiUrl } from '../utils/environment-config.js'; import * as output from '../utils/output.js'; /** * Status command implementation * @param {string} buildId - Build ID to check status for * @param {Object} options - Command options * @param {Object} globalOptions - Global CLI options */ export async function statusCommand(buildId, options = {}, globalOptions = {}) { output.configure({ json: globalOptions.json, verbose: globalOptions.verbose, color: !globalOptions.noColor }); try { // Load configuration with CLI overrides let allOptions = { ...globalOptions, ...options }; let config = await loadConfig(globalOptions.config, allOptions); // Validate API token if (!config.apiKey) { output.error('API token required. Use --token or set VIZZLY_TOKEN environment variable'); process.exit(1); } // Get build details via functional API output.startSpinner('Fetching build status...'); let client = createApiClient({ baseUrl: config.apiUrl, token: config.apiKey, command: 'status' }); let buildStatus = await getBuild(client, buildId); output.stopSpinner(); // Extract build data from API response let build = buildStatus.build || buildStatus; // Output in JSON mode if (globalOptions.json) { let statusData = { buildId: build.id, status: build.status, name: build.name, createdAt: build.created_at, updatedAt: build.updated_at, completedAt: build.completed_at, environment: build.environment, branch: build.branch, commit: build.commit_sha, commitMessage: build.commit_message, screenshotsTotal: build.screenshot_count || 0, comparisonsTotal: build.total_comparisons || 0, newComparisons: build.new_comparisons || 0, changedComparisons: build.changed_comparisons || 0, identicalComparisons: build.identical_comparisons || 0, approvalStatus: build.approval_status, executionTime: build.execution_time_ms, isBaseline: build.is_baseline, userAgent: build.user_agent }; output.data(statusData); output.cleanup(); return; } // Human-readable output output.header('status', build.status); // Build info section let buildInfo = { Name: build.name || build.id, Status: build.status.toUpperCase(), Environment: build.environment }; if (build.branch) { buildInfo.Branch = build.branch; } if (build.commit_sha) { buildInfo.Commit = `${build.commit_sha.substring(0, 8)} - ${build.commit_message || 'No message'}`; } output.keyValue(buildInfo); output.blank(); // Comparison stats with visual indicators let colors = output.getColors(); let stats = []; let newCount = build.new_comparisons || 0; let changedCount = build.changed_comparisons || 0; let identicalCount = build.identical_comparisons || 0; let screenshotCount = build.screenshot_count || 0; output.labelValue('Screenshots', String(screenshotCount)); if (newCount > 0) { stats.push(`${colors.brand.info(newCount)} new`); } if (changedCount > 0) { stats.push(`${colors.brand.warning(changedCount)} changed`); } if (identicalCount > 0) { stats.push(`${colors.brand.success(identicalCount)} identical`); } if (stats.length > 0) { output.labelValue('Comparisons', stats.join(colors.brand.textMuted(' · '))); } if (build.approval_status) { output.labelValue('Approval', build.approval_status); } output.blank(); // Timing info if (build.created_at) { output.hint(`Created ${new Date(build.created_at).toLocaleString()}`); } if (build.completed_at) { output.hint(`Completed ${new Date(build.completed_at).toLocaleString()}`); } else if (build.status !== 'completed' && build.status !== 'failed') { output.hint(`Started ${new Date(build.started_at || build.created_at).toLocaleString()}`); } if (build.execution_time_ms) { output.hint(`Took ${Math.round(build.execution_time_ms / 1000)}s`); } // Show build URL if we can construct it let baseUrl = config.baseUrl || getApiUrl(); if (baseUrl && build.project_id) { let buildUrl = baseUrl.replace('/api', '') + `/projects/${build.project_id}/builds/${build.id}`; output.blank(); output.labelValue('View', output.link('Build', buildUrl)); } // Show additional info in verbose mode if (globalOptions.verbose) { output.blank(); output.divider(); output.blank(); let verboseInfo = {}; if (build.approved_screenshots > 0 || build.rejected_screenshots > 0 || build.pending_screenshots > 0) { verboseInfo.Approvals = `${build.approved_screenshots || 0} approved, ${build.rejected_screenshots || 0} rejected, ${build.pending_screenshots || 0} pending`; } if (build.avg_diff_percentage !== null) { verboseInfo['Avg Diff'] = `${(build.avg_diff_percentage * 100).toFixed(2)}%`; } if (build.github_pull_request_number) { verboseInfo['GitHub PR'] = `#${build.github_pull_request_number}`; } if (build.is_baseline) { verboseInfo.Baseline = 'Yes'; } verboseInfo['User Agent'] = build.user_agent || 'Unknown'; verboseInfo['Build ID'] = build.id; verboseInfo['Project ID'] = build.project_id; output.keyValue(verboseInfo); } // Show progress if build is still processing if (build.status === 'processing' || build.status === 'pending') { let totalJobs = build.completed_jobs + build.failed_jobs + build.processing_screenshots; if (totalJobs > 0) { let progress = (build.completed_jobs + build.failed_jobs) / totalJobs; output.blank(); output.print(` ${output.progressBar(progress * 100, 100)} ${Math.round(progress * 100)}%`); } } output.cleanup(); // Exit with appropriate code based on build status if (build.status === 'failed' || build.failed_jobs > 0) { process.exit(1); } } catch (error) { output.error('Failed to get build status', error); process.exit(1); } } /** * Validate status options * @param {string} buildId - Build ID to check * @param {Object} options - Command options */ export function validateStatusOptions(buildId) { let errors = []; if (!buildId || buildId.trim() === '') { errors.push('Build ID is required'); } return errors; }