BMAD-METHOD/bmad-core/utils/bmad-lib.sh

339 lines
8.7 KiB
Bash

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