import type { DeployMessage } from '../../cdk/toolkit-lib'; import { GradientText } from './StepProgress'; import { Box, Text } from 'ink'; import React, { useMemo } from 'react'; interface DeployStatusProps { messages: DeployMessage[]; isComplete: boolean; hasError: boolean; } const PROGRESS_BAR_WIDTH = 20; // CDK message code for resource events const CDK_CODE_RESOURCE_EVENT = 'CDK_TOOLKIT_I5502'; /** * Extract resource progress from messages. * Progress is pre-extracted at the source (in createSwitchableIoHost). */ function extractProgress(messages: DeployMessage[]): { current: number; total: number } | null { // Search from end to find most recent message with progress for (let i = messages.length - 1; i >= 0; i--) { const msg = messages[i]; if (msg?.progress) { return { current: msg.progress.completed, total: msg.progress.total }; } } return null; } /** * Progress bar component. */ function ProgressBar({ current, total }: { current: number; total: number }) { const percent = total > 0 ? current / total : 0; const filled = Math.round(percent * PROGRESS_BAR_WIDTH); const empty = PROGRESS_BAR_WIDTH - filled; return ( [ {'█'.repeat(filled)} {'░'.repeat(empty)} ] {' '} {current}/{total} ); } type ResourceStatus = | 'CREATE_IN_PROGRESS' | 'CREATE_COMPLETE' | 'CREATE_FAILED' | 'UPDATE_IN_PROGRESS' | 'UPDATE_COMPLETE' | 'UPDATE_FAILED' | 'DELETE_IN_PROGRESS' | 'DELETE_COMPLETE' | 'DELETE_FAILED'; interface ParsedResource { resourceType: string; status: ResourceStatus; } /** * Get color for a resource status. */ function getStatusColor(status: ResourceStatus): string | undefined { if (status.endsWith('_COMPLETE')) return 'green'; if (status.endsWith('_FAILED')) return 'red'; if (status.endsWith('_IN_PROGRESS')) return 'cyan'; return undefined; } /** * Extract resource type and status from a CDK resource event message. * Only processes I5502 (resource event) messages. */ function parseResourceMessage(msg: DeployMessage): ParsedResource | null { // Only process resource event messages if (msg.code !== CDK_CODE_RESOURCE_EVENT) { return null; } const text = msg.message; // Skip CLEANUP messages - they're confusing if (text.includes('CLEANUP')) { return null; } // Format: "StackName | STATUS | AWS::Service::Resource | LogicalId" const resourceMatch = /(AWS::\S+)/.exec(text); const statusMatch = /(CREATE_IN_PROGRESS|CREATE_COMPLETE|CREATE_FAILED|UPDATE_IN_PROGRESS|UPDATE_COMPLETE|UPDATE_FAILED|DELETE_IN_PROGRESS|DELETE_COMPLETE|DELETE_FAILED)/.exec( text ); if (resourceMatch?.[1] && statusMatch) { const shortType = resourceMatch[1].replace(/^AWS::/, ''); return { resourceType: shortType, status: statusMatch[1] as ResourceStatus }; } return null; } /** * Render a resource line with color-coded status. */ function ResourceLine({ resource }: { resource: ParsedResource }) { const color = getStatusColor(resource.status); return ( {resource.resourceType} {resource.status} ); } /** * Deploy status component showing deployment progress in a contained box. * During deployment: shows last N resource events (type + status only) * After completion: shows success/failure state */ export function DeployStatus({ messages, isComplete, hasError }: DeployStatusProps) { // Parse and filter messages to only meaningful resource updates const parsedResources = messages .map(msg => ({ original: msg, parsed: parseResourceMessage(msg) })) .filter((m): m is { original: DeployMessage; parsed: ParsedResource } => m.parsed !== null) .slice(-8); // Extract progress for the bar const progress = useMemo(() => extractProgress(messages), [messages]); // When complete, show final status if (isComplete) { return ( {hasError ? '✗ Deploy to AWS Failed' : '✓ Deploy to AWS Complete'} {progress && ( )} {hasError && ( {parsedResources.slice(-3).map((m, i) => ( ))} )} ); } return ( {progress && ( )} {parsedResources.length > 0 && ( {parsedResources.map((m, i) => ( ))} )} ); }