validate-registry.sh 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. #!/usr/bin/env bash
  2. #############################################################################
  3. # Registry Validator Script
  4. # Validates that all paths in registry.json point to actual files
  5. # Exit codes:
  6. # 0 = All paths valid
  7. # 1 = Missing files found
  8. # 2 = Registry parse error or missing dependencies
  9. #############################################################################
  10. set -e
  11. # Colors
  12. RED='\033[0;31m'
  13. GREEN='\033[0;32m'
  14. YELLOW='\033[1;33m'
  15. BLUE='\033[0;34m'
  16. CYAN='\033[0;36m'
  17. BOLD='\033[1m'
  18. NC='\033[0m'
  19. # Configuration
  20. REGISTRY_FILE="registry.json"
  21. REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
  22. VERBOSE=false
  23. FIX_MODE=false
  24. # Counters
  25. TOTAL_PATHS=0
  26. VALID_PATHS=0
  27. MISSING_PATHS=0
  28. ORPHANED_FILES=0
  29. # Arrays to store results
  30. declare -a MISSING_FILES
  31. declare -a ORPHANED_COMPONENTS
  32. #############################################################################
  33. # Utility Functions
  34. #############################################################################
  35. print_header() {
  36. echo -e "${CYAN}${BOLD}"
  37. echo "╔════════════════════════════════════════════════════════════════╗"
  38. echo "║ ║"
  39. echo "║ Registry Validator v1.0.0 ║"
  40. echo "║ ║"
  41. echo "╚════════════════════════════════════════════════════════════════╝"
  42. echo -e "${NC}"
  43. }
  44. print_success() {
  45. echo -e "${GREEN}✓${NC} $1"
  46. }
  47. print_error() {
  48. echo -e "${RED}✗${NC} $1"
  49. }
  50. print_warning() {
  51. echo -e "${YELLOW}⚠${NC} $1"
  52. }
  53. print_info() {
  54. echo -e "${BLUE}ℹ${NC} $1"
  55. }
  56. usage() {
  57. echo "Usage: $0 [OPTIONS]"
  58. echo ""
  59. echo "Options:"
  60. echo " -v, --verbose Show detailed validation output"
  61. echo " -f, --fix Suggest fixes for missing files"
  62. echo " -h, --help Show this help message"
  63. echo ""
  64. echo "Exit codes:"
  65. echo " 0 = All paths valid"
  66. echo " 1 = Missing files found"
  67. echo " 2 = Registry parse error or missing dependencies"
  68. exit 0
  69. }
  70. #############################################################################
  71. # Dependency Checks
  72. #############################################################################
  73. check_dependencies() {
  74. local missing_deps=()
  75. if ! command -v jq &> /dev/null; then
  76. missing_deps+=("jq")
  77. fi
  78. if [ ${#missing_deps[@]} -ne 0 ]; then
  79. print_error "Missing required dependencies: ${missing_deps[*]}"
  80. echo ""
  81. echo "Please install them:"
  82. echo " macOS: brew install ${missing_deps[*]}"
  83. echo " Ubuntu: sudo apt-get install ${missing_deps[*]}"
  84. echo " Fedora: sudo dnf install ${missing_deps[*]}"
  85. exit 2
  86. fi
  87. }
  88. #############################################################################
  89. # Registry Validation
  90. #############################################################################
  91. validate_registry_file() {
  92. if [ ! -f "$REGISTRY_FILE" ]; then
  93. print_error "Registry file not found: $REGISTRY_FILE"
  94. exit 2
  95. fi
  96. if ! jq empty "$REGISTRY_FILE" 2>/dev/null; then
  97. print_error "Registry file is not valid JSON"
  98. exit 2
  99. fi
  100. print_success "Registry file is valid JSON"
  101. }
  102. validate_component_paths() {
  103. local category=$1
  104. local category_display=$2
  105. [ "$VERBOSE" = true ] && echo -e "\n${BOLD}Checking ${category_display}...${NC}"
  106. # Get all components in this category
  107. local components=$(jq -r ".components.${category}[]? | @json" "$REGISTRY_FILE" 2>/dev/null)
  108. if [ -z "$components" ]; then
  109. [ "$VERBOSE" = true ] && print_info "No ${category_display} found in registry"
  110. return
  111. fi
  112. while IFS= read -r component; do
  113. local id=$(echo "$component" | jq -r '.id')
  114. local path=$(echo "$component" | jq -r '.path')
  115. local name=$(echo "$component" | jq -r '.name')
  116. TOTAL_PATHS=$((TOTAL_PATHS + 1))
  117. # Check if file exists
  118. if [ -f "$REPO_ROOT/$path" ]; then
  119. VALID_PATHS=$((VALID_PATHS + 1))
  120. [ "$VERBOSE" = true ] && print_success "${category_display}: ${name} (${id})"
  121. else
  122. MISSING_PATHS=$((MISSING_PATHS + 1))
  123. MISSING_FILES+=("${category}:${id}|${name}|${path}")
  124. print_error "${category_display}: ${name} (${id}) - File not found: ${path}"
  125. # Try to find similar files if in fix mode
  126. if [ "$FIX_MODE" = true ]; then
  127. suggest_fix "$path" "$id"
  128. fi
  129. fi
  130. done <<< "$components"
  131. }
  132. suggest_fix() {
  133. local missing_path=$1
  134. local component_id=$2
  135. # Extract directory and filename
  136. local dir=$(dirname "$missing_path")
  137. local filename=$(basename "$missing_path")
  138. local base_dir=$(echo "$dir" | cut -d'/' -f1-3) # e.g., .opencode/command
  139. # Look for similar files in the expected directory and subdirectories
  140. local similar_files=$(find "$REPO_ROOT/$base_dir" -type f -name "*.md" 2>/dev/null | grep -i "$component_id" || true)
  141. if [ -n "$similar_files" ]; then
  142. echo -e " ${YELLOW}→ Possible matches:${NC}"
  143. while IFS= read -r file; do
  144. local rel_path="${file#$REPO_ROOT/}"
  145. echo -e " ${CYAN}${rel_path}${NC}"
  146. done <<< "$similar_files"
  147. fi
  148. }
  149. scan_for_orphaned_files() {
  150. [ "$VERBOSE" = true ] && echo -e "\n${BOLD}Scanning for orphaned files...${NC}"
  151. # Get all paths from registry
  152. local registry_paths=$(jq -r '.components | to_entries[] | .value[] | .path' "$REGISTRY_FILE" 2>/dev/null | sort -u)
  153. # Scan .opencode directory for markdown files
  154. local categories=("agent" "command" "tool" "plugin" "context")
  155. for category in "${categories[@]}"; do
  156. local category_dir="$REPO_ROOT/.opencode/$category"
  157. if [ ! -d "$category_dir" ]; then
  158. continue
  159. fi
  160. # Find all .md and .ts files (excluding node_modules)
  161. while IFS= read -r file; do
  162. local rel_path="${file#$REPO_ROOT/}"
  163. # Skip node_modules
  164. if [[ "$rel_path" == *"/node_modules/"* ]]; then
  165. continue
  166. fi
  167. # Check if this path is in registry
  168. if ! echo "$registry_paths" | grep -q "^${rel_path}$"; then
  169. ORPHANED_FILES=$((ORPHANED_FILES + 1))
  170. ORPHANED_COMPONENTS+=("$rel_path")
  171. [ "$VERBOSE" = true ] && print_warning "Orphaned file (not in registry): ${rel_path}"
  172. fi
  173. done < <(find "$category_dir" -type f \( -name "*.md" -o -name "*.ts" \) 2>/dev/null)
  174. done
  175. }
  176. #############################################################################
  177. # Reporting
  178. #############################################################################
  179. print_summary() {
  180. echo ""
  181. echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
  182. echo -e "${BOLD}Validation Summary${NC}"
  183. echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
  184. echo ""
  185. echo -e "Total paths checked: ${CYAN}${TOTAL_PATHS}${NC}"
  186. echo -e "Valid paths: ${GREEN}${VALID_PATHS}${NC}"
  187. echo -e "Missing paths: ${RED}${MISSING_PATHS}${NC}"
  188. if [ "$VERBOSE" = true ]; then
  189. echo -e "Orphaned files: ${YELLOW}${ORPHANED_FILES}${NC}"
  190. fi
  191. echo ""
  192. if [ $MISSING_PATHS -eq 0 ]; then
  193. print_success "All registry paths are valid!"
  194. if [ $ORPHANED_FILES -gt 0 ] && [ "$VERBOSE" = true ]; then
  195. echo ""
  196. print_warning "Found ${ORPHANED_FILES} orphaned file(s) not in registry"
  197. echo ""
  198. echo "Orphaned files:"
  199. for file in "${ORPHANED_COMPONENTS[@]}"; do
  200. echo " - $file"
  201. done
  202. echo ""
  203. echo "Consider adding these to registry.json or removing them."
  204. fi
  205. return 0
  206. else
  207. print_error "Found ${MISSING_PATHS} missing file(s)"
  208. echo ""
  209. echo "Missing files:"
  210. for entry in "${MISSING_FILES[@]}"; do
  211. IFS='|' read -r cat_id name path <<< "$entry"
  212. echo " - ${path} (${cat_id})"
  213. done
  214. echo ""
  215. echo "Please fix these issues before proceeding."
  216. if [ "$FIX_MODE" = false ]; then
  217. echo ""
  218. print_info "Run with --fix flag to see suggested fixes"
  219. fi
  220. return 1
  221. fi
  222. }
  223. #############################################################################
  224. # Main
  225. #############################################################################
  226. main() {
  227. # Parse arguments
  228. while [ $# -gt 0 ]; do
  229. case "$1" in
  230. -v|--verbose)
  231. VERBOSE=true
  232. shift
  233. ;;
  234. -f|--fix)
  235. FIX_MODE=true
  236. VERBOSE=true
  237. shift
  238. ;;
  239. -h|--help)
  240. usage
  241. ;;
  242. *)
  243. echo "Unknown option: $1"
  244. usage
  245. ;;
  246. esac
  247. done
  248. print_header
  249. # Check dependencies
  250. check_dependencies
  251. # Validate registry file
  252. validate_registry_file
  253. echo ""
  254. print_info "Validating component paths..."
  255. echo ""
  256. # Validate each category
  257. validate_component_paths "agents" "Agents"
  258. validate_component_paths "subagents" "Subagents"
  259. validate_component_paths "commands" "Commands"
  260. validate_component_paths "tools" "Tools"
  261. validate_component_paths "plugins" "Plugins"
  262. validate_component_paths "contexts" "Contexts"
  263. validate_component_paths "config" "Config"
  264. # Scan for orphaned files if verbose
  265. if [ "$VERBOSE" = true ]; then
  266. scan_for_orphaned_files
  267. fi
  268. # Print summary and exit with appropriate code
  269. if print_summary; then
  270. exit 0
  271. else
  272. exit 1
  273. fi
  274. }
  275. main "$@"