rdev/cookbooks/scripts/lib/stream-client.sh
jordan c59d348040 chore: prepare for composable monorepo template implementation
This commit captures the current state before implementing the composable
monorepo template system. Key changes included:

Infrastructure:
- Add CockroachDB provisioner adapter for database provisioning
- Add Redis provisioner adapter for cache provisioning
- Add build events system with PostgreSQL storage
- Add WebSocket endpoint for real-time build progress

Code agent improvements:
- Fix Claude Code adapter to use default allowed tools instead of dangerously-skip-permissions
- Add context-aware stream closing for cancellation support
- Improve parser tests for edge cases

Build system:
- Add build event constants and metrics
- Remove deprecated git_operations.go (replaced by pod_git_operations.go)
- Add rollback logic for multi-step provisioning operations

Documentation:
- Add composable-monorepo feature documentation
- Add DNS/Cloudflare service documentation
- Update deployment and troubleshooting guides

Cookbooks:
- Add fullstack-app cookbook
- Refactor landing-test with shared library

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 11:39:28 -07:00

284 lines
8.6 KiB
Bash
Executable File

#!/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
}