#!/bin/bash # SSE Stream Client Library for rdev API # Provides functions for consuming Server-Sent Events from build streams # # Usage: # source cookbooks/scripts/lib/stream-client.sh # stream_build_with_progress "$API_URL" "$API_KEY" "$PROJECT_ID" "$TASK_ID" # Colors for output STREAM_RED='\033[0;31m' STREAM_GREEN='\033[0;32m' STREAM_YELLOW='\033[1;33m' STREAM_BLUE='\033[0;34m' STREAM_CYAN='\033[0;36m' STREAM_NC='\033[0m' # Progress bar width PROGRESS_BAR_WIDTH=40 # Draw a progress bar # Arguments: percentage (0-100) draw_progress_bar() { local percent="${1:-0}" local filled=$((percent * PROGRESS_BAR_WIDTH / 100)) local empty=$((PROGRESS_BAR_WIDTH - filled)) printf "\r[" printf '%*s' "$filled" '' | tr ' ' '=' if [[ $filled -lt $PROGRESS_BAR_WIDTH ]]; then printf ">" printf '%*s' "$((empty - 1))" '' | tr ' ' ' ' fi printf "] %3d%%" "$percent" } # Parse SSE data line and extract JSON # Arguments: data line (after "data: " prefix) parse_sse_data() { local data="$1" echo "$data" } # Stream build events with progress bar # Arguments: api_url, api_key, project_id, task_id # Options: # --verbose Show all output (not just progress) # --last-id Last-Event-ID for reconnection stream_build_with_progress() { local api_url="$1" local api_key="$2" local project_id="$3" local task_id="$4" shift 4 local verbose=false local last_event_id="" # Parse options while [[ $# -gt 0 ]]; do case "$1" in --verbose) verbose=true shift ;; --last-id) last_event_id="$2" shift 2 ;; *) shift ;; esac done local stream_url="${api_url}/projects/${project_id}/events?stream_id=${task_id}" local curl_args=( -s -N -H "X-API-Key: ${api_key}" -H "Accept: text/event-stream" ) if [[ -n "$last_event_id" ]]; then curl_args+=(-H "Last-Event-ID: ${last_event_id}") fi echo -e "${STREAM_CYAN}Streaming build events...${STREAM_NC}" echo "" # Track state local current_phase="starting" local current_percent=0 local last_event_id_received="" # Stream events curl "${curl_args[@]}" "$stream_url" 2>/dev/null | while IFS= read -r line; do # Skip empty lines [[ -z "$line" ]] && continue # Parse event ID if [[ "$line" == "id:"* ]]; then last_event_id_received="${line#id: }" continue fi # Skip event type lines (we parse data directly) [[ "$line" == "event:"* ]] && continue # Parse data lines if [[ "$line" == "data:"* ]]; then local data="${line#data: }" local event_type event_type=$(echo "$data" | jq -r '.type // ""' 2>/dev/null) case "$event_type" in build.started) echo -e "${STREAM_GREEN}[BUILD STARTED]${STREAM_NC}" current_phase="starting" current_percent=0 draw_progress_bar 0 ;; build.progress) current_phase=$(echo "$data" | jq -r '.phase // "unknown"' 2>/dev/null) current_percent=$(echo "$data" | jq -r '.percentage // 0' 2>/dev/null | cut -d. -f1) draw_progress_bar "$current_percent" printf " [%s]" "$current_phase" ;; build.output) if [[ "$verbose" == "true" ]]; then local content content=$(echo "$data" | jq -r '.content // ""' 2>/dev/null) [[ -n "$content" ]] && printf "\n%s" "$content" fi ;; build.tool_use) local tool_name tool_name=$(echo "$data" | jq -r '.tool_name // "unknown"' 2>/dev/null) if [[ "$verbose" == "true" ]]; then printf "\n${STREAM_YELLOW}[TOOL: %s]${STREAM_NC}" "$tool_name" fi ;; build.error) local error_content error_content=$(echo "$data" | jq -r '.content // ""' 2>/dev/null) printf "\n${STREAM_RED}[ERROR] %s${STREAM_NC}" "$error_content" ;; build.completed) echo "" draw_progress_bar 100 printf " [complete]" echo "" echo -e "${STREAM_GREEN}[BUILD COMPLETED]${STREAM_NC}" local duration_ms duration_ms=$(echo "$data" | jq -r '.duration_ms // 0' 2>/dev/null) local duration_s=$((duration_ms / 1000)) echo "Duration: ${duration_s}s" return 0 ;; build.failed) echo "" local error error=$(echo "$data" | jq -r '.error // "unknown error"' 2>/dev/null) echo -e "${STREAM_RED}[BUILD FAILED]${STREAM_NC}" echo "Error: $error" return 1 ;; connected) local reconnecting reconnecting=$(echo "$data" | jq -r '.reconnecting // false' 2>/dev/null) if [[ "$reconnecting" == "true" ]]; then echo -e "${STREAM_YELLOW}[RECONNECTED]${STREAM_NC}" fi ;; heartbeat) # Silent heartbeat - just proves connection is alive ;; esac fi done # If we get here, the stream closed unexpectedly echo "" echo -e "${STREAM_YELLOW}[STREAM CLOSED]${STREAM_NC}" echo "Last event ID: $last_event_id_received" echo "To reconnect: stream_build_with_progress ... --last-id \"$last_event_id_received\"" return 2 } # Simple stream consumer that just prints events # Arguments: api_url, api_key, project_id, task_id stream_build_simple() { local api_url="$1" local api_key="$2" local project_id="$3" local task_id="$4" local stream_url="${api_url}/projects/${project_id}/events?stream_id=${task_id}" curl -s -N \ -H "X-API-Key: ${api_key}" \ -H "Accept: text/event-stream" \ "$stream_url" 2>/dev/null | while IFS= read -r line; do [[ -z "$line" ]] && continue if [[ "$line" == "data:"* ]]; then local data="${line#data: }" local event_type content event_type=$(echo "$data" | jq -r '.type // ""' 2>/dev/null) case "$event_type" in build.output|build.error) content=$(echo "$data" | jq -r '.content // ""' 2>/dev/null) [[ -n "$content" ]] && echo "$content" ;; build.completed) echo "[BUILD COMPLETED]" return 0 ;; build.failed) local error error=$(echo "$data" | jq -r '.error // ""' 2>/dev/null) echo "[BUILD FAILED] $error" return 1 ;; esac fi done } # Wait for build completion with polling fallback # Arguments: api_url, api_key, task_id, timeout_seconds # Returns: 0 on success, 1 on failure, 2 on timeout wait_for_build_completion() { local api_url="$1" local api_key="$2" local task_id="$3" local timeout="${4:-600}" local start_time=$(date +%s) while true; do local elapsed=$(($(date +%s) - start_time)) if [[ $elapsed -ge $timeout ]]; then return 2 # Timeout fi local response response=$(curl -s -X GET "${api_url}/builds/${task_id}" \ -H "X-API-Key: ${api_key}" 2>/dev/null) local status status=$(echo "$response" | jq -r '.data.status // "unknown"' 2>/dev/null) case "$status" in completed) local success success=$(echo "$response" | jq -r '.data.result.success // false' 2>/dev/null) if [[ "$success" == "true" ]]; then return 0 else return 1 fi ;; failed) return 1 ;; running|pending) sleep 5 ;; *) sleep 5 ;; esac done }