#!/usr/bin/env bash ############################################################################# # Registry Validator Script # Validates that all paths in registry.json point to actual files # Exit codes: # 0 = All paths valid # 1 = Missing files found # 2 = Registry parse error or missing dependencies ############################################################################# set -e # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' # Configuration REGISTRY_FILE="registry.json" REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" VERBOSE=false FIX_MODE=false # Counters TOTAL_PATHS=0 VALID_PATHS=0 MISSING_PATHS=0 ORPHANED_FILES=0 # Arrays to store results declare -a MISSING_FILES declare -a ORPHANED_COMPONENTS ############################################################################# # Utility Functions ############################################################################# print_header() { echo -e "${CYAN}${BOLD}" echo "╔════════════════════════════════════════════════════════════════╗" echo "║ ║" echo "║ Registry Validator v1.0.0 ║" echo "║ ║" echo "╚════════════════════════════════════════════════════════════════╝" echo -e "${NC}" } print_success() { echo -e "${GREEN}✓${NC} $1" } print_error() { echo -e "${RED}✗${NC} $1" } print_warning() { echo -e "${YELLOW}⚠${NC} $1" } print_info() { echo -e "${BLUE}ℹ${NC} $1" } usage() { echo "Usage: $0 [OPTIONS]" echo "" echo "Options:" echo " -v, --verbose Show detailed validation output" echo " -f, --fix Suggest fixes for missing files" echo " -h, --help Show this help message" echo "" echo "Exit codes:" echo " 0 = All paths valid" echo " 1 = Missing files found" echo " 2 = Registry parse error or missing dependencies" exit 0 } ############################################################################# # Dependency Checks ############################################################################# check_dependencies() { local missing_deps=() if ! command -v jq &> /dev/null; then missing_deps+=("jq") fi if [ ${#missing_deps[@]} -ne 0 ]; then print_error "Missing required dependencies: ${missing_deps[*]}" echo "" echo "Please install them:" echo " macOS: brew install ${missing_deps[*]}" echo " Ubuntu: sudo apt-get install ${missing_deps[*]}" echo " Fedora: sudo dnf install ${missing_deps[*]}" exit 2 fi } ############################################################################# # Registry Validation ############################################################################# validate_registry_file() { if [ ! -f "$REGISTRY_FILE" ]; then print_error "Registry file not found: $REGISTRY_FILE" exit 2 fi if ! jq empty "$REGISTRY_FILE" 2>/dev/null; then print_error "Registry file is not valid JSON" exit 2 fi print_success "Registry file is valid JSON" } validate_component_paths() { local category=$1 local category_display=$2 [ "$VERBOSE" = true ] && echo -e "\n${BOLD}Checking ${category_display}...${NC}" # Get all components in this category local components=$(jq -r ".components.${category}[]? | @json" "$REGISTRY_FILE" 2>/dev/null) if [ -z "$components" ]; then [ "$VERBOSE" = true ] && print_info "No ${category_display} found in registry" return fi while IFS= read -r component; do local id=$(echo "$component" | jq -r '.id') local path=$(echo "$component" | jq -r '.path') local name=$(echo "$component" | jq -r '.name') TOTAL_PATHS=$((TOTAL_PATHS + 1)) # Check if file exists if [ -f "$REPO_ROOT/$path" ]; then VALID_PATHS=$((VALID_PATHS + 1)) [ "$VERBOSE" = true ] && print_success "${category_display}: ${name} (${id})" else MISSING_PATHS=$((MISSING_PATHS + 1)) MISSING_FILES+=("${category}:${id}|${name}|${path}") print_error "${category_display}: ${name} (${id}) - File not found: ${path}" # Try to find similar files if in fix mode if [ "$FIX_MODE" = true ]; then suggest_fix "$path" "$id" fi fi done <<< "$components" } suggest_fix() { local missing_path=$1 local component_id=$2 # Extract directory and filename local dir=$(dirname "$missing_path") local filename=$(basename "$missing_path") local base_dir=$(echo "$dir" | cut -d'/' -f1-3) # e.g., .opencode/command # Look for similar files in the expected directory and subdirectories local similar_files=$(find "$REPO_ROOT/$base_dir" -type f -name "*.md" 2>/dev/null | grep -i "$component_id" || true) if [ -n "$similar_files" ]; then echo -e " ${YELLOW}→ Possible matches:${NC}" while IFS= read -r file; do local rel_path="${file#$REPO_ROOT/}" echo -e " ${CYAN}${rel_path}${NC}" done <<< "$similar_files" fi } scan_for_orphaned_files() { [ "$VERBOSE" = true ] && echo -e "\n${BOLD}Scanning for orphaned files...${NC}" # Get all paths from registry local registry_paths=$(jq -r '.components | to_entries[] | .value[] | .path' "$REGISTRY_FILE" 2>/dev/null | sort -u) # Scan .opencode directory for markdown files local categories=("agent" "command" "tool" "plugin" "context") for category in "${categories[@]}"; do local category_dir="$REPO_ROOT/.opencode/$category" if [ ! -d "$category_dir" ]; then continue fi # Find all .md and .ts files (excluding node_modules) while IFS= read -r file; do local rel_path="${file#$REPO_ROOT/}" # Skip node_modules if [[ "$rel_path" == *"/node_modules/"* ]]; then continue fi # Check if this path is in registry if ! echo "$registry_paths" | grep -q "^${rel_path}$"; then ORPHANED_FILES=$((ORPHANED_FILES + 1)) ORPHANED_COMPONENTS+=("$rel_path") [ "$VERBOSE" = true ] && print_warning "Orphaned file (not in registry): ${rel_path}" fi done < <(find "$category_dir" -type f \( -name "*.md" -o -name "*.ts" \) 2>/dev/null) done } ############################################################################# # Reporting ############################################################################# print_summary() { echo "" echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}" echo -e "${BOLD}Validation Summary${NC}" echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}" echo "" echo -e "Total paths checked: ${CYAN}${TOTAL_PATHS}${NC}" echo -e "Valid paths: ${GREEN}${VALID_PATHS}${NC}" echo -e "Missing paths: ${RED}${MISSING_PATHS}${NC}" if [ "$VERBOSE" = true ]; then echo -e "Orphaned files: ${YELLOW}${ORPHANED_FILES}${NC}" fi echo "" if [ $MISSING_PATHS -eq 0 ]; then print_success "All registry paths are valid!" if [ $ORPHANED_FILES -gt 0 ] && [ "$VERBOSE" = true ]; then echo "" print_warning "Found ${ORPHANED_FILES} orphaned file(s) not in registry" echo "" echo "Orphaned files:" for file in "${ORPHANED_COMPONENTS[@]}"; do echo " - $file" done echo "" echo "Consider adding these to registry.json or removing them." fi return 0 else print_error "Found ${MISSING_PATHS} missing file(s)" echo "" echo "Missing files:" for entry in "${MISSING_FILES[@]}"; do IFS='|' read -r cat_id name path <<< "$entry" echo " - ${path} (${cat_id})" done echo "" echo "Please fix these issues before proceeding." if [ "$FIX_MODE" = false ]; then echo "" print_info "Run with --fix flag to see suggested fixes" fi return 1 fi } ############################################################################# # Main ############################################################################# main() { # Parse arguments while [ $# -gt 0 ]; do case "$1" in -v|--verbose) VERBOSE=true shift ;; -f|--fix) FIX_MODE=true VERBOSE=true shift ;; -h|--help) usage ;; *) echo "Unknown option: $1" usage ;; esac done print_header # Check dependencies check_dependencies # Validate registry file validate_registry_file echo "" print_info "Validating component paths..." echo "" # Validate each category validate_component_paths "agents" "Agents" validate_component_paths "subagents" "Subagents" validate_component_paths "commands" "Commands" validate_component_paths "tools" "Tools" validate_component_paths "plugins" "Plugins" validate_component_paths "contexts" "Contexts" validate_component_paths "config" "Config" # Scan for orphaned files if verbose if [ "$VERBOSE" = true ]; then scan_for_orphaned_files fi # Print summary and exit with appropriate code if print_summary; then exit 0 else exit 1 fi } main "$@"