#!/usr/bin/env bash # bmad-lib.sh - Shared utilities for BMAD commands # Author: Quinn (Senior Developer QA) # Version: 1.0.0 # ============================================================================ # SHELL COMPATIBILITY LAYER # ============================================================================ # Detect shell and set compatibility options setup_shell_compatibility() { if [ -n "$ZSH_VERSION" ]; then # ZSH specific settings setopt LOCAL_OPTIONS NO_KSH_ARRAYS setopt PIPE_FAIL 2>/dev/null || true # Avoid reserved variable names in ZSH export BMAD_SHELL="zsh" elif [ -n "$BASH_VERSION" ]; then # Bash specific settings set -euo pipefail export BMAD_SHELL="bash" else # POSIX fallback export BMAD_SHELL="sh" fi } # ============================================================================ # ERROR HANDLING # ============================================================================ # Consistent error reporting bmad_error() { echo "❌ ERROR: $1" >&2 return "${2:-1}" } # Warning messages bmad_warn() { echo "⚠️ WARNING: $1" >&2 } # Success messages bmad_success() { echo "✅ $1" } # ============================================================================ # PROJECT VALIDATION # ============================================================================ # Check if in a BMAD project validate_bmad_project() { if [ ! -d ".bmad" ]; then bmad_error "No BMAD project found. Please initialize a project first." return 1 fi if [ ! -d ".bmad/stories" ]; then mkdir -p .bmad/stories bmad_warn "Created missing stories directory" fi if [ ! -d ".bmad/documents" ]; then mkdir -p .bmad/documents bmad_warn "Created missing documents directory" fi return 0 } # ============================================================================ # YAML VALIDATION # ============================================================================ # Validate story file structure validate_story_file() { local file="$1" local required_fields="id title status epic" local missing_fields="" if [ ! -f "$file" ]; then bmad_error "File not found: $file" return 1 fi for field in $required_fields; do if ! grep -q "^${field}:" "$file" 2>/dev/null; then missing_fields="$missing_fields $field" fi done if [ -n "$missing_fields" ]; then bmad_error "Story file $file missing required fields:$missing_fields" return 1 fi return 0 } # Validate YAML field value validate_yaml_value() { local file="$1" local field="$2" local pattern="$3" local value value=$(grep "^${field}:" "$file" 2>/dev/null | cut -d: -f2- | sed 's/^ //') if [ -z "$value" ]; then return 1 fi if ! echo "$value" | grep -qE "$pattern"; then bmad_error "Invalid value for $field: $value" return 1 fi return 0 } # ============================================================================ # SAFE YAML PARSING # ============================================================================ # Extract field value from YAML file get_yaml_field() { local file="$1" local field="$2" local default="${3:-}" if [ ! -f "$file" ]; then echo "$default" return fi local value value=$(grep "^${field}:" "$file" 2>/dev/null | cut -d: -f2- | sed 's/^ //' | tr -d '\r') # Handle null or empty values if [ -z "$value" ] || [ "$value" = "null" ]; then echo "$default" else echo "$value" fi } # Extract multi-line field from YAML get_yaml_multiline() { local file="$1" local field="$2" awk "/^${field}:/ {flag=1; next} /^[^ ]/ {flag=0} flag" "$file" 2>/dev/null } # ============================================================================ # PERFORMANCE OPTIMIZATION # ============================================================================ # Setup cache directory setup_cache() { local cache_dir=".bmad/.cache" if [ ! -d "$cache_dir" ]; then mkdir -p "$cache_dir" fi echo "$cache_dir" } # Check if cache is valid (newer than source files) is_cache_valid() { local cache_file="$1" local source_dir="$2" if [ ! -f "$cache_file" ]; then return 1 fi # Find any source file newer than cache if find "$source_dir" -name "*.yaml" -newer "$cache_file" 2>/dev/null | grep -q .; then return 1 fi return 0 } # Cache story statuses for performance cache_story_statuses() { local cache_dir cache_dir=$(setup_cache) local cache_file="$cache_dir/story_statuses.cache" if is_cache_valid "$cache_file" ".bmad/stories"; then cat "$cache_file" return 0 fi # Rebuild cache find .bmad/stories -name "*.yaml" -exec grep -H "^status:" {} \; 2>/dev/null | \ tee "$cache_file" } # ============================================================================ # FORMATTING HELPERS # ============================================================================ # Generate progress bar progress_bar() { local current="$1" local total="$2" local width="${3:-10}" if [ "$total" -eq 0 ]; then echo "[----------]" return fi local percent=$((current * 100 / total)) local filled=$((current * width / total)) local empty=$((width - filled)) printf "[" printf "█%.0s" $(seq 1 $filled 2>/dev/null || yes | head -n $filled | tr -d '\n') printf "░%.0s" $(seq 1 $empty 2>/dev/null || yes | head -n $empty | tr -d '\n') printf "] %d%%" "$percent" } # Map status to emoji status_to_emoji() { local status="$1" case "$status" in "draft") echo "📝" ;; "ready") echo "📋" ;; "in_progress") echo "🔄" ;; "code_review") echo "👀" ;; "qa_testing") echo "🧪" ;; "completed"|"done") echo "✅" ;; "blocked") echo "🚫" ;; *) echo "❓" ;; esac } # Priority to emoji priority_to_emoji() { local priority="$1" case "$priority" in "high") echo "🔴" ;; "medium") echo "🟡" ;; "low") echo "🔵" ;; *) echo "⚪" ;; esac } # ============================================================================ # DATA AGGREGATION # ============================================================================ # Count stories by status (optimized single pass) count_stories_by_status() { awk ' /^status: draft/ {draft++} /^status: ready/ {ready++} /^status: in_progress/ {in_progress++} /^status: code_review/ {code_review++} /^status: qa_testing/ {qa_testing++} /^status: completed|^status: done/ {completed++} END { print "draft=" draft+0 print "ready=" ready+0 print "in_progress=" in_progress+0 print "code_review=" code_review+0 print "qa_testing=" qa_testing+0 print "completed=" completed+0 } ' .bmad/stories/*.yaml 2>/dev/null } # Calculate story points (optimized) calculate_points() { local filter="${1:-}" if [ -z "$filter" ]; then awk '/^points:/ {sum+=$2} END {print sum+0}' .bmad/stories/*.yaml 2>/dev/null else grep -l "$filter" .bmad/stories/*.yaml 2>/dev/null | \ xargs awk '/^points:/ {sum+=$2} END {print sum+0}' 2>/dev/null fi } # ============================================================================ # COLOR OUTPUT SUPPORT # ============================================================================ # Check if terminal supports colors supports_color() { if [ -t 1 ] && [ -n "${TERM:-}" ] && [ "$TERM" != "dumb" ]; then return 0 fi return 1 } # Color codes (only if supported) setup_colors() { if supports_color; then export RED='\033[0;31m' export GREEN='\033[0;32m' export YELLOW='\033[1;33m' export BLUE='\033[0;34m' export PURPLE='\033[0;35m' export CYAN='\033[0;36m' export BOLD='\033[1m' export RESET='\033[0m' else export RED='' export GREEN='' export YELLOW='' export BLUE='' export PURPLE='' export CYAN='' export BOLD='' export RESET='' fi } # ============================================================================ # INITIALIZATION # ============================================================================ # Initialize library bmad_lib_init() { setup_shell_compatibility setup_colors } # Auto-initialize if sourced bmad_lib_init