validate-registry.sh 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  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. MISSING_DEPENDENCIES=0
  30. # Arrays to store results
  31. declare -a MISSING_FILES
  32. declare -a ORPHANED_COMPONENTS
  33. declare -a MISSING_DEPS
  34. #############################################################################
  35. # Utility Functions
  36. #############################################################################
  37. print_header() {
  38. echo -e "${CYAN}${BOLD}"
  39. echo "╔════════════════════════════════════════════════════════════════╗"
  40. echo "║ ║"
  41. echo "║ Registry Validator v1.0.0 ║"
  42. echo "║ ║"
  43. echo "╚════════════════════════════════════════════════════════════════╝"
  44. echo -e "${NC}"
  45. }
  46. print_success() {
  47. echo -e "${GREEN}✓${NC} $1"
  48. }
  49. print_error() {
  50. echo -e "${RED}✗${NC} $1"
  51. }
  52. print_warning() {
  53. echo -e "${YELLOW}⚠${NC} $1"
  54. }
  55. print_info() {
  56. echo -e "${BLUE}ℹ${NC} $1"
  57. }
  58. usage() {
  59. echo "Usage: $0 [OPTIONS]"
  60. echo ""
  61. echo "Options:"
  62. echo " -v, --verbose Show detailed validation output"
  63. echo " -f, --fix Suggest fixes for missing files"
  64. echo " -h, --help Show this help message"
  65. echo ""
  66. echo "Exit codes:"
  67. echo " 0 = All paths valid"
  68. echo " 1 = Missing files found"
  69. echo " 2 = Registry parse error or missing dependencies"
  70. exit 0
  71. }
  72. #############################################################################
  73. # Dependency Checks
  74. #############################################################################
  75. check_dependencies() {
  76. local missing_deps=()
  77. if ! command -v jq &> /dev/null; then
  78. missing_deps+=("jq")
  79. fi
  80. if [ ${#missing_deps[@]} -ne 0 ]; then
  81. print_error "Missing required dependencies: ${missing_deps[*]}"
  82. echo ""
  83. echo "Please install them:"
  84. echo " macOS: brew install ${missing_deps[*]}"
  85. echo " Ubuntu: sudo apt-get install ${missing_deps[*]}"
  86. echo " Fedora: sudo dnf install ${missing_deps[*]}"
  87. exit 2
  88. fi
  89. }
  90. #############################################################################
  91. # Registry Validation
  92. #############################################################################
  93. validate_registry_file() {
  94. if [ ! -f "$REGISTRY_FILE" ]; then
  95. print_error "Registry file not found: $REGISTRY_FILE"
  96. exit 2
  97. fi
  98. if ! jq empty "$REGISTRY_FILE" 2>/dev/null; then
  99. print_error "Registry file is not valid JSON"
  100. exit 2
  101. fi
  102. print_success "Registry file is valid JSON"
  103. }
  104. validate_component_paths() {
  105. local category=$1
  106. local category_display=$2
  107. [ "$VERBOSE" = true ] && echo -e "\n${BOLD}Checking ${category_display}...${NC}"
  108. # Get all components in this category
  109. local components=$(jq -r ".components.${category}[]? | @json" "$REGISTRY_FILE" 2>/dev/null)
  110. if [ -z "$components" ]; then
  111. [ "$VERBOSE" = true ] && print_info "No ${category_display} found in registry"
  112. return
  113. fi
  114. while IFS= read -r component; do
  115. local id=$(echo "$component" | jq -r '.id')
  116. local path=$(echo "$component" | jq -r '.path')
  117. local name=$(echo "$component" | jq -r '.name')
  118. TOTAL_PATHS=$((TOTAL_PATHS + 1))
  119. # Check if file exists
  120. if [ -f "$REPO_ROOT/$path" ]; then
  121. VALID_PATHS=$((VALID_PATHS + 1))
  122. [ "$VERBOSE" = true ] && print_success "${category_display}: ${name} (${id})"
  123. else
  124. MISSING_PATHS=$((MISSING_PATHS + 1))
  125. MISSING_FILES+=("${category}:${id}|${name}|${path}")
  126. print_error "${category_display}: ${name} (${id}) - File not found: ${path}"
  127. # Try to find similar files if in fix mode
  128. if [ "$FIX_MODE" = true ]; then
  129. suggest_fix "$path" "$id"
  130. fi
  131. fi
  132. done <<< "$components"
  133. }
  134. suggest_fix() {
  135. local missing_path=$1
  136. local component_id=$2
  137. # Extract directory and filename
  138. local dir=$(dirname "$missing_path")
  139. local filename=$(basename "$missing_path")
  140. local base_dir=$(echo "$dir" | cut -d'/' -f1-3) # e.g., .opencode/command
  141. # Look for similar files in the expected directory and subdirectories
  142. local similar_files=$(find "$REPO_ROOT/$base_dir" -type f -name "*.md" 2>/dev/null | grep -i "$component_id" || true)
  143. if [ -n "$similar_files" ]; then
  144. echo -e " ${YELLOW}→ Possible matches:${NC}"
  145. while IFS= read -r file; do
  146. local rel_path="${file#$REPO_ROOT/}"
  147. echo -e " ${CYAN}${rel_path}${NC}"
  148. done <<< "$similar_files"
  149. fi
  150. }
  151. scan_for_orphaned_files() {
  152. [ "$VERBOSE" = true ] && echo -e "\n${BOLD}Scanning for orphaned files...${NC}"
  153. # Get all paths from registry
  154. local registry_paths=$(jq -r '.components | to_entries[] | .value[] | .path' "$REGISTRY_FILE" 2>/dev/null | sort -u)
  155. # Scan .opencode directory for markdown files
  156. local categories=("agent" "command" "tool" "plugin" "context")
  157. for category in "${categories[@]}"; do
  158. local category_dir="$REPO_ROOT/.opencode/$category"
  159. if [ ! -d "$category_dir" ]; then
  160. continue
  161. fi
  162. # Find all .md and .ts files (excluding node_modules)
  163. while IFS= read -r file; do
  164. local rel_path="${file#$REPO_ROOT/}"
  165. # Skip node_modules
  166. if [[ "$rel_path" == *"/node_modules/"* ]]; then
  167. continue
  168. fi
  169. # Check if this path is in registry
  170. if ! echo "$registry_paths" | grep -q "^${rel_path}$"; then
  171. ORPHANED_FILES=$((ORPHANED_FILES + 1))
  172. ORPHANED_COMPONENTS+=("$rel_path")
  173. [ "$VERBOSE" = true ] && print_warning "Orphaned file (not in registry): ${rel_path}"
  174. fi
  175. done < <(find "$category_dir" -type f \( -name "*.md" -o -name "*.ts" \) 2>/dev/null)
  176. done
  177. }
  178. #############################################################################
  179. # Dependency Validation
  180. #############################################################################
  181. check_dependency_exists() {
  182. local dep=$1
  183. # Parse dependency format: type:id
  184. if [[ ! "$dep" =~ ^([^:]+):(.+)$ ]]; then
  185. echo "invalid_format"
  186. return 1
  187. fi
  188. local dep_type="${BASH_REMATCH[1]}"
  189. local dep_id="${BASH_REMATCH[2]}"
  190. # Map dependency type to registry category
  191. local registry_category=""
  192. case "$dep_type" in
  193. agent)
  194. registry_category="agents"
  195. ;;
  196. subagent)
  197. registry_category="subagents"
  198. ;;
  199. command)
  200. registry_category="commands"
  201. ;;
  202. tool)
  203. registry_category="tools"
  204. ;;
  205. plugin)
  206. registry_category="plugins"
  207. ;;
  208. context)
  209. registry_category="contexts"
  210. ;;
  211. config)
  212. registry_category="config"
  213. ;;
  214. *)
  215. echo "unknown_type"
  216. return 1
  217. ;;
  218. esac
  219. # Check if component exists in registry
  220. # First try exact ID match
  221. local exists=$(jq -r ".components.${registry_category}[]? | select(.id == \"${dep_id}\") | .id" "$REGISTRY_FILE" 2>/dev/null)
  222. if [ -n "$exists" ]; then
  223. echo "found"
  224. return 0
  225. fi
  226. # For context dependencies, also try path-based lookup
  227. # Format: context:core/standards/code -> .opencode/context/core/standards/code.md
  228. if [ "$dep_type" = "context" ]; then
  229. local context_path=".opencode/context/${dep_id}.md"
  230. local exists_by_path=$(jq -r ".components.${registry_category}[]? | select(.path == \"${context_path}\") | .id" "$REGISTRY_FILE" 2>/dev/null)
  231. if [ -n "$exists_by_path" ]; then
  232. echo "found"
  233. return 0
  234. fi
  235. fi
  236. echo "not_found"
  237. return 1
  238. }
  239. validate_component_dependencies() {
  240. echo ""
  241. print_info "Validating component dependencies..."
  242. echo ""
  243. # Get all component types
  244. local component_types=$(jq -r '.components | keys[]' "$REGISTRY_FILE" 2>/dev/null)
  245. while IFS= read -r comp_type; do
  246. # Get all components of this type
  247. local components=$(jq -r ".components.${comp_type}[]? | @json" "$REGISTRY_FILE" 2>/dev/null)
  248. if [ -z "$components" ]; then
  249. continue
  250. fi
  251. while IFS= read -r component; do
  252. local id=$(echo "$component" | jq -r '.id')
  253. local name=$(echo "$component" | jq -r '.name')
  254. local dependencies=$(echo "$component" | jq -r '.dependencies[]?' 2>/dev/null)
  255. if [ -z "$dependencies" ]; then
  256. continue
  257. fi
  258. # Check each dependency
  259. while IFS= read -r dep; do
  260. if [ -z "$dep" ]; then
  261. continue
  262. fi
  263. local result=$(check_dependency_exists "$dep")
  264. case "$result" in
  265. found)
  266. [ "$VERBOSE" = true ] && print_success "Dependency OK: ${name} → ${dep}"
  267. ;;
  268. not_found)
  269. MISSING_DEPENDENCIES=$((MISSING_DEPENDENCIES + 1))
  270. MISSING_DEPS+=("${comp_type}|${id}|${name}|${dep}")
  271. print_error "Missing dependency: ${name} (${comp_type%s}) depends on \"${dep}\" (not found in registry)"
  272. ;;
  273. invalid_format)
  274. MISSING_DEPENDENCIES=$((MISSING_DEPENDENCIES + 1))
  275. MISSING_DEPS+=("${comp_type}|${id}|${name}|${dep}")
  276. print_error "Invalid dependency format: ${name} (${comp_type%s}) has invalid dependency \"${dep}\" (expected format: type:id)"
  277. ;;
  278. unknown_type)
  279. MISSING_DEPENDENCIES=$((MISSING_DEPENDENCIES + 1))
  280. MISSING_DEPS+=("${comp_type}|${id}|${name}|${dep}")
  281. print_error "Unknown dependency type: ${name} (${comp_type%s}) has unknown dependency type in \"${dep}\""
  282. ;;
  283. esac
  284. done <<< "$dependencies"
  285. done <<< "$components"
  286. done <<< "$component_types"
  287. }
  288. #############################################################################
  289. # Reporting
  290. #############################################################################
  291. print_summary() {
  292. echo ""
  293. echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
  294. echo -e "${BOLD}Validation Summary${NC}"
  295. echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
  296. echo ""
  297. echo -e "Total paths checked: ${CYAN}${TOTAL_PATHS}${NC}"
  298. echo -e "Valid paths: ${GREEN}${VALID_PATHS}${NC}"
  299. echo -e "Missing paths: ${RED}${MISSING_PATHS}${NC}"
  300. echo -e "Missing dependencies: ${RED}${MISSING_DEPENDENCIES}${NC}"
  301. if [ "$VERBOSE" = true ]; then
  302. echo -e "Orphaned files: ${YELLOW}${ORPHANED_FILES}${NC}"
  303. fi
  304. echo ""
  305. local has_errors=false
  306. # Check for missing paths
  307. if [ $MISSING_PATHS -gt 0 ]; then
  308. has_errors=true
  309. print_error "Found ${MISSING_PATHS} missing file(s)"
  310. echo ""
  311. echo "Missing files:"
  312. for entry in "${MISSING_FILES[@]}"; do
  313. IFS='|' read -r cat_id name path <<< "$entry"
  314. echo " - ${path} (${cat_id})"
  315. done
  316. echo ""
  317. if [ "$FIX_MODE" = false ]; then
  318. print_info "Run with --fix flag to see suggested fixes"
  319. echo ""
  320. fi
  321. fi
  322. # Check for missing dependencies
  323. if [ $MISSING_DEPENDENCIES -gt 0 ]; then
  324. has_errors=true
  325. print_error "Found ${MISSING_DEPENDENCIES} missing or invalid dependencies"
  326. echo ""
  327. echo "Missing dependencies:"
  328. for entry in "${MISSING_DEPS[@]}"; do
  329. IFS='|' read -r comp_type id name dep <<< "$entry"
  330. echo " - ${name} (${comp_type%s}) → ${dep}"
  331. done
  332. echo ""
  333. print_info "Fix by either:"
  334. echo " 1. Adding the missing component to the registry"
  335. echo " 2. Removing the dependency from the component's frontmatter"
  336. echo ""
  337. fi
  338. # Success case
  339. if [ "$has_errors" = false ]; then
  340. print_success "All registry paths are valid!"
  341. print_success "All component dependencies are valid!"
  342. if [ $ORPHANED_FILES -gt 0 ] && [ "$VERBOSE" = true ]; then
  343. echo ""
  344. print_warning "Found ${ORPHANED_FILES} orphaned file(s) not in registry"
  345. echo ""
  346. echo "Orphaned files:"
  347. for file in "${ORPHANED_COMPONENTS[@]}"; do
  348. echo " - $file"
  349. done
  350. echo ""
  351. echo "Consider adding these to registry.json or removing them."
  352. fi
  353. return 0
  354. else
  355. echo "Please fix these issues before proceeding."
  356. return 1
  357. fi
  358. }
  359. #############################################################################
  360. # Main
  361. #############################################################################
  362. main() {
  363. # Parse arguments
  364. while [ $# -gt 0 ]; do
  365. case "$1" in
  366. -v|--verbose)
  367. VERBOSE=true
  368. shift
  369. ;;
  370. -f|--fix)
  371. FIX_MODE=true
  372. VERBOSE=true
  373. shift
  374. ;;
  375. -h|--help)
  376. usage
  377. ;;
  378. *)
  379. echo "Unknown option: $1"
  380. usage
  381. ;;
  382. esac
  383. done
  384. print_header
  385. # Check dependencies
  386. check_dependencies
  387. # Validate registry file
  388. validate_registry_file
  389. echo ""
  390. print_info "Validating component paths..."
  391. echo ""
  392. # Validate each category
  393. validate_component_paths "agents" "Agents"
  394. validate_component_paths "subagents" "Subagents"
  395. validate_component_paths "commands" "Commands"
  396. validate_component_paths "tools" "Tools"
  397. validate_component_paths "plugins" "Plugins"
  398. validate_component_paths "contexts" "Contexts"
  399. validate_component_paths "config" "Config"
  400. # Validate component dependencies
  401. validate_component_dependencies
  402. # Scan for orphaned files if verbose
  403. if [ "$VERBOSE" = true ]; then
  404. scan_for_orphaned_files
  405. fi
  406. # Print summary and exit with appropriate code
  407. if print_summary; then
  408. exit 0
  409. else
  410. exit 1
  411. fi
  412. }
  413. main "$@"