validate-registry.sh 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  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
  108. components=$(jq -r ".components.${category}[]? | @json" "$REGISTRY_FILE" 2>/dev/null)
  109. if [ -z "$components" ]; then
  110. [ "$VERBOSE" = true ] && print_info "No ${category_display} found in registry"
  111. return
  112. fi
  113. while IFS= read -r component; do
  114. local id
  115. local path
  116. local name
  117. id=$(echo "$component" | jq -r '.id')
  118. path=$(echo "$component" | jq -r '.path')
  119. name=$(echo "$component" | jq -r '.name')
  120. TOTAL_PATHS=$((TOTAL_PATHS + 1))
  121. # Check if file exists
  122. if [ -f "$REPO_ROOT/$path" ]; then
  123. VALID_PATHS=$((VALID_PATHS + 1))
  124. [ "$VERBOSE" = true ] && print_success "${category_display}: ${name} (${id})"
  125. else
  126. MISSING_PATHS=$((MISSING_PATHS + 1))
  127. MISSING_FILES+=("${category}:${id}|${name}|${path}")
  128. print_error "${category_display}: ${name} (${id}) - File not found: ${path}"
  129. # Try to find similar files if in fix mode
  130. if [ "$FIX_MODE" = true ]; then
  131. suggest_fix "$path" "$id"
  132. fi
  133. fi
  134. done <<< "$components"
  135. }
  136. suggest_fix() {
  137. local missing_path=$1
  138. local component_id=$2
  139. # Extract directory and filename
  140. local dir
  141. local base_dir
  142. dir=$(dirname "$missing_path")
  143. base_dir=$(echo "$dir" | cut -d'/' -f1-3) # e.g., .opencode/command
  144. # Look for similar files in the expected directory and subdirectories
  145. local similar_files
  146. similar_files=$(find "$REPO_ROOT/$base_dir" -type f -name "*.md" 2>/dev/null | grep -i "$component_id" || true)
  147. if [ -n "$similar_files" ]; then
  148. echo -e " ${YELLOW}→ Possible matches:${NC}"
  149. while IFS= read -r file; do
  150. local rel_path="${file#"$REPO_ROOT"/}"
  151. echo -e " ${CYAN}${rel_path}${NC}"
  152. done <<< "$similar_files"
  153. fi
  154. }
  155. scan_for_orphaned_files() {
  156. [ "$VERBOSE" = true ] && echo -e "\n${BOLD}Scanning for orphaned files...${NC}"
  157. # Get all paths from registry
  158. local registry_paths
  159. registry_paths=$(jq -r '.components | to_entries[] | .value[] | .path' "$REGISTRY_FILE" 2>/dev/null | sort -u)
  160. # Scan .opencode directory for markdown files
  161. local categories=("agent" "command" "tool" "plugin" "context")
  162. for category in "${categories[@]}"; do
  163. local category_dir="$REPO_ROOT/.opencode/$category"
  164. if [ ! -d "$category_dir" ]; then
  165. continue
  166. fi
  167. # Find all .md and .ts files (excluding node_modules)
  168. while IFS= read -r file; do
  169. local rel_path="${file#"$REPO_ROOT"/}"
  170. # Skip node_modules
  171. if [[ "$rel_path" == *"/node_modules/"* ]]; then
  172. continue
  173. fi
  174. # Check if this path is in registry
  175. # shellcheck disable=SC2143
  176. if ! echo "$registry_paths" | grep -q "^${rel_path}$"; then
  177. ORPHANED_FILES=$((ORPHANED_FILES + 1))
  178. ORPHANED_COMPONENTS+=("$rel_path")
  179. [ "$VERBOSE" = true ] && print_warning "Orphaned file (not in registry): ${rel_path}"
  180. fi
  181. done < <(find "$category_dir" -type f \( -name "*.md" -o -name "*.ts" \) 2>/dev/null)
  182. done
  183. }
  184. #############################################################################
  185. # Reporting
  186. #############################################################################
  187. print_summary() {
  188. echo ""
  189. echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
  190. echo -e "${BOLD}Validation Summary${NC}"
  191. echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
  192. echo ""
  193. echo -e "Total paths checked: ${CYAN}${TOTAL_PATHS}${NC}"
  194. echo -e "Valid paths: ${GREEN}${VALID_PATHS}${NC}"
  195. echo -e "Missing paths: ${RED}${MISSING_PATHS}${NC}"
  196. if [ "$VERBOSE" = true ]; then
  197. echo -e "Orphaned files: ${YELLOW}${ORPHANED_FILES}${NC}"
  198. fi
  199. echo ""
  200. if [ $MISSING_PATHS -eq 0 ]; then
  201. print_success "All registry paths are valid!"
  202. if [ $ORPHANED_FILES -gt 0 ] && [ "$VERBOSE" = true ]; then
  203. echo ""
  204. print_warning "Found ${ORPHANED_FILES} orphaned file(s) not in registry"
  205. echo ""
  206. echo "Orphaned files:"
  207. for file in "${ORPHANED_COMPONENTS[@]}"; do
  208. echo " - $file"
  209. done
  210. echo ""
  211. echo "Consider adding these to registry.json or removing them."
  212. fi
  213. return 0
  214. else
  215. print_error "Found ${MISSING_PATHS} missing file(s)"
  216. echo ""
  217. echo "Missing files:"
  218. for entry in "${MISSING_FILES[@]}"; do
  219. IFS='|' read -r cat_id name path <<< "$entry"
  220. echo " - ${path} (${cat_id})"
  221. done
  222. echo ""
  223. echo "Please fix these issues before proceeding."
  224. if [ "$FIX_MODE" = false ]; then
  225. echo ""
  226. print_info "Run with --fix flag to see suggested fixes"
  227. fi
  228. return 1
  229. fi
  230. }
  231. #############################################################################
  232. # Main
  233. #############################################################################
  234. main() {
  235. # Parse arguments
  236. while [ $# -gt 0 ]; do
  237. case "$1" in
  238. -v|--verbose)
  239. VERBOSE=true
  240. shift
  241. ;;
  242. -f|--fix)
  243. FIX_MODE=true
  244. VERBOSE=true
  245. shift
  246. ;;
  247. -h|--help)
  248. usage
  249. ;;
  250. *)
  251. echo "Unknown option: $1"
  252. usage
  253. ;;
  254. esac
  255. done
  256. print_header
  257. # Check dependencies
  258. check_dependencies
  259. # Validate registry file
  260. validate_registry_file
  261. echo ""
  262. print_info "Validating component paths..."
  263. echo ""
  264. # Validate each category
  265. validate_component_paths "agents" "Agents"
  266. validate_component_paths "subagents" "Subagents"
  267. validate_component_paths "commands" "Commands"
  268. validate_component_paths "tools" "Tools"
  269. validate_component_paths "plugins" "Plugins"
  270. validate_component_paths "contexts" "Contexts"
  271. validate_component_paths "config" "Config"
  272. # Scan for orphaned files if verbose
  273. if [ "$VERBOSE" = true ]; then
  274. scan_for_orphaned_files
  275. fi
  276. # Print summary and exit with appropriate code
  277. if print_summary; then
  278. exit 0
  279. else
  280. exit 1
  281. fi
  282. }
  283. main "$@"