validate-registry.sh 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  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
  110. components=$(jq -r ".components.${category}[]? | @json" "$REGISTRY_FILE" 2>/dev/null)
  111. if [ -z "$components" ]; then
  112. [ "$VERBOSE" = true ] && print_info "No ${category_display} found in registry"
  113. return
  114. fi
  115. while IFS= read -r component; do
  116. local id
  117. id=$(echo "$component" | jq -r '.id')
  118. local path
  119. path=$(echo "$component" | jq -r '.path')
  120. local name
  121. name=$(echo "$component" | jq -r '.name')
  122. TOTAL_PATHS=$((TOTAL_PATHS + 1))
  123. # Check if file exists
  124. if [ -f "$REPO_ROOT/$path" ]; then
  125. VALID_PATHS=$((VALID_PATHS + 1))
  126. [ "$VERBOSE" = true ] && print_success "${category_display}: ${name} (${id})"
  127. else
  128. MISSING_PATHS=$((MISSING_PATHS + 1))
  129. MISSING_FILES+=("${category}:${id}|${name}|${path}")
  130. print_error "${category_display}: ${name} (${id}) - File not found: ${path}"
  131. # Try to find similar files if in fix mode
  132. if [ "$FIX_MODE" = true ]; then
  133. suggest_fix "$path" "$id"
  134. fi
  135. fi
  136. done <<< "$components"
  137. }
  138. suggest_fix() {
  139. local missing_path=$1
  140. local component_id=$2
  141. # Extract directory and filename
  142. local dir
  143. dir=$(dirname "$missing_path")
  144. # local filename=$(basename "$missing_path") # Unused
  145. local base_dir
  146. base_dir=$(echo "$dir" | cut -d'/' -f1-3) # e.g., .opencode/command
  147. # Look for similar files in the expected directory and subdirectories
  148. local similar_files
  149. similar_files=$(find "$REPO_ROOT/$base_dir" -type f -name "*.md" 2>/dev/null | grep -i "$component_id" || true)
  150. if [ -n "$similar_files" ]; then
  151. echo -e " ${YELLOW}→ Possible matches:${NC}"
  152. while IFS= read -r file; do
  153. local rel_path="${file#$REPO_ROOT/}"
  154. echo -e " ${CYAN}${rel_path}${NC}"
  155. done <<< "$similar_files"
  156. fi
  157. }
  158. scan_for_orphaned_files() {
  159. [ "$VERBOSE" = true ] && echo -e "\n${BOLD}Scanning for orphaned files...${NC}"
  160. # Get all paths from registry
  161. local registry_paths
  162. registry_paths=$(jq -r '.components | to_entries[] | .value[] | .path' "$REGISTRY_FILE" 2>/dev/null | sort -u)
  163. # Scan .opencode directory for markdown files
  164. local categories=("agent" "command" "tool" "plugin" "context")
  165. for category in "${categories[@]}"; do
  166. local category_dir="$REPO_ROOT/.opencode/$category"
  167. if [ ! -d "$category_dir" ]; then
  168. continue
  169. fi
  170. # Find all .md and .ts files (excluding node_modules)
  171. while IFS= read -r file; do
  172. local rel_path="${file#$REPO_ROOT/}"
  173. # Skip node_modules
  174. if [[ "$rel_path" == *"/node_modules/"* ]]; then
  175. continue
  176. fi
  177. # Skip README files
  178. if [[ "$rel_path" == *"README.md" ]]; then
  179. continue
  180. fi
  181. # Skip template files
  182. if [[ "$rel_path" == *"-template.md" ]]; then
  183. continue
  184. fi
  185. # Skip tool/plugin TypeScript files
  186. if [[ "$rel_path" == *"/tool/index.ts" ]] || [[ "$rel_path" == *"/tool/template/index.ts" ]]; then
  187. continue
  188. fi
  189. if [[ "$rel_path" == *"/plugin/agent-validator.ts" ]]; then
  190. continue
  191. fi
  192. # Skip plugin internal docs and tests
  193. if [[ "$rel_path" == *"/plugin/docs/"* ]] || [[ "$rel_path" == *"/plugin/tests/"* ]]; then
  194. continue
  195. fi
  196. # Skip scripts directories (internal CLI tools, not registry components)
  197. if [[ "$rel_path" == *"/scripts/"* ]]; then
  198. continue
  199. fi
  200. # Check if this path is in registry
  201. if ! echo "$registry_paths" | grep -q "^${rel_path}$"; then
  202. ORPHANED_FILES=$((ORPHANED_FILES + 1))
  203. ORPHANED_COMPONENTS+=("$rel_path")
  204. [ "$VERBOSE" = true ] && print_warning "Orphaned file (not in registry): ${rel_path}"
  205. fi
  206. done < <(find "$category_dir" -type f \( -name "*.md" -o -name "*.ts" \) 2>/dev/null)
  207. done
  208. }
  209. #############################################################################
  210. # Dependency Validation
  211. #############################################################################
  212. check_dependency_exists() {
  213. local dep=$1
  214. # Parse dependency format: type:id
  215. if [[ ! "$dep" =~ ^([^:]+):(.+)$ ]]; then
  216. echo "invalid_format"
  217. return 1
  218. fi
  219. local dep_type="${BASH_REMATCH[1]}"
  220. local dep_id="${BASH_REMATCH[2]}"
  221. # Map dependency type to registry category
  222. local registry_category=""
  223. case "$dep_type" in
  224. agent)
  225. registry_category="agents"
  226. ;;
  227. subagent)
  228. registry_category="subagents"
  229. ;;
  230. command)
  231. registry_category="commands"
  232. ;;
  233. tool)
  234. registry_category="tools"
  235. ;;
  236. plugin)
  237. registry_category="plugins"
  238. ;;
  239. context)
  240. registry_category="contexts"
  241. ;;
  242. config)
  243. registry_category="config"
  244. ;;
  245. *)
  246. echo "unknown_type"
  247. return 1
  248. ;;
  249. esac
  250. # Check if component exists in registry
  251. # First try exact ID match
  252. local exists
  253. exists=$(jq -r ".components.${registry_category}[]? | select(.id == \"${dep_id}\") | .id" "$REGISTRY_FILE" 2>/dev/null)
  254. if [ -n "$exists" ]; then
  255. echo "found"
  256. return 0
  257. fi
  258. # For context dependencies, also try path-based lookup
  259. # Format: context:core/standards/code -> .opencode/context/core/standards/code.md
  260. if [ "$dep_type" = "context" ]; then
  261. local context_path=".opencode/context/${dep_id}.md"
  262. local exists_by_path
  263. exists_by_path=$(jq -r ".components.${registry_category}[]? | select(.path == \"${context_path}\") | .id" "$REGISTRY_FILE" 2>/dev/null)
  264. if [ -n "$exists_by_path" ]; then
  265. echo "found"
  266. return 0
  267. fi
  268. fi
  269. echo "not_found"
  270. return 1
  271. }
  272. validate_component_dependencies() {
  273. echo ""
  274. print_info "Validating component dependencies..."
  275. echo ""
  276. # Get all component types
  277. local component_types
  278. component_types=$(jq -r '.components | keys[]' "$REGISTRY_FILE" 2>/dev/null)
  279. while IFS= read -r comp_type; do
  280. # Get all components of this type
  281. local components
  282. components=$(jq -r ".components.${comp_type}[]? | @json" "$REGISTRY_FILE" 2>/dev/null)
  283. if [ -z "$components" ]; then
  284. continue
  285. fi
  286. while IFS= read -r component; do
  287. local id
  288. id=$(echo "$component" | jq -r '.id')
  289. local name
  290. name=$(echo "$component" | jq -r '.name')
  291. local dependencies
  292. dependencies=$(echo "$component" | jq -r '.dependencies[]?' 2>/dev/null)
  293. if [ -z "$dependencies" ]; then
  294. continue
  295. fi
  296. # Check each dependency
  297. while IFS= read -r dep; do
  298. if [ -z "$dep" ]; then
  299. continue
  300. fi
  301. local result
  302. result=$(check_dependency_exists "$dep")
  303. case "$result" in
  304. found)
  305. [ "$VERBOSE" = true ] && print_success "Dependency OK: ${name} → ${dep}"
  306. ;;
  307. not_found)
  308. MISSING_DEPENDENCIES=$((MISSING_DEPENDENCIES + 1))
  309. MISSING_DEPS+=("${comp_type}|${id}|${name}|${dep}")
  310. print_error "Missing dependency: ${name} (${comp_type%s}) depends on \"${dep}\" (not found in registry)"
  311. ;;
  312. invalid_format)
  313. MISSING_DEPENDENCIES=$((MISSING_DEPENDENCIES + 1))
  314. MISSING_DEPS+=("${comp_type}|${id}|${name}|${dep}")
  315. print_error "Invalid dependency format: ${name} (${comp_type%s}) has invalid dependency \"${dep}\" (expected format: type:id)"
  316. ;;
  317. unknown_type)
  318. MISSING_DEPENDENCIES=$((MISSING_DEPENDENCIES + 1))
  319. MISSING_DEPS+=("${comp_type}|${id}|${name}|${dep}")
  320. print_error "Unknown dependency type: ${name} (${comp_type%s}) has unknown dependency type in \"${dep}\""
  321. ;;
  322. esac
  323. done <<< "$dependencies"
  324. done <<< "$components"
  325. done <<< "$component_types"
  326. }
  327. #############################################################################
  328. # Reporting
  329. #############################################################################
  330. print_summary() {
  331. echo ""
  332. echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
  333. echo -e "${BOLD}Validation Summary${NC}"
  334. echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
  335. echo ""
  336. echo -e "Total paths checked: ${CYAN}${TOTAL_PATHS}${NC}"
  337. echo -e "Valid paths: ${GREEN}${VALID_PATHS}${NC}"
  338. echo -e "Missing paths: ${RED}${MISSING_PATHS}${NC}"
  339. echo -e "Missing dependencies: ${RED}${MISSING_DEPENDENCIES}${NC}"
  340. if [ "$VERBOSE" = true ]; then
  341. echo -e "Orphaned files: ${YELLOW}${ORPHANED_FILES}${NC}"
  342. fi
  343. echo ""
  344. local has_errors=false
  345. # Check for missing paths
  346. if [ $MISSING_PATHS -gt 0 ]; then
  347. has_errors=true
  348. print_error "Found ${MISSING_PATHS} missing file(s)"
  349. echo ""
  350. echo "Missing files:"
  351. for entry in "${MISSING_FILES[@]}"; do
  352. IFS='|' read -r cat_id name path <<< "$entry"
  353. echo " - ${path} (${cat_id})"
  354. done
  355. echo ""
  356. if [ "$FIX_MODE" = false ]; then
  357. print_info "Run with --fix flag to see suggested fixes"
  358. echo ""
  359. fi
  360. fi
  361. # Check for missing dependencies
  362. if [ $MISSING_DEPENDENCIES -gt 0 ]; then
  363. has_errors=true
  364. print_error "Found ${MISSING_DEPENDENCIES} missing or invalid dependencies"
  365. echo ""
  366. echo "Missing dependencies:"
  367. for entry in "${MISSING_DEPS[@]}"; do
  368. IFS='|' read -r comp_type id name dep <<< "$entry"
  369. echo " - ${name} (${comp_type%s}) → ${dep}"
  370. done
  371. echo ""
  372. print_info "Fix by either:"
  373. echo " 1. Adding the missing component to the registry"
  374. echo " 2. Removing the dependency from the component's frontmatter"
  375. echo ""
  376. fi
  377. # Success case
  378. if [ "$has_errors" = false ]; then
  379. print_success "All registry paths are valid!"
  380. print_success "All component dependencies are valid!"
  381. if [ $ORPHANED_FILES -gt 0 ] && [ "$VERBOSE" = true ]; then
  382. echo ""
  383. print_warning "Found ${ORPHANED_FILES} orphaned file(s) not in registry"
  384. echo ""
  385. echo "Orphaned files:"
  386. for file in "${ORPHANED_COMPONENTS[@]}"; do
  387. echo " - $file"
  388. done
  389. echo ""
  390. echo "Consider adding these to registry.json or removing them."
  391. fi
  392. return 0
  393. else
  394. echo "Please fix these issues before proceeding."
  395. return 1
  396. fi
  397. }
  398. #############################################################################
  399. # Main
  400. #############################################################################
  401. main() {
  402. # Parse arguments
  403. while [ $# -gt 0 ]; do
  404. case "$1" in
  405. -v|--verbose)
  406. VERBOSE=true
  407. shift
  408. ;;
  409. -f|--fix)
  410. FIX_MODE=true
  411. VERBOSE=true
  412. shift
  413. ;;
  414. -h|--help)
  415. usage
  416. ;;
  417. *)
  418. echo "Unknown option: $1"
  419. usage
  420. ;;
  421. esac
  422. done
  423. print_header
  424. # Check dependencies
  425. check_dependencies
  426. # Validate registry file
  427. validate_registry_file
  428. echo ""
  429. print_info "Validating component paths..."
  430. echo ""
  431. # Validate each category
  432. validate_component_paths "agents" "Agents"
  433. validate_component_paths "subagents" "Subagents"
  434. validate_component_paths "commands" "Commands"
  435. validate_component_paths "tools" "Tools"
  436. validate_component_paths "plugins" "Plugins"
  437. validate_component_paths "contexts" "Contexts"
  438. validate_component_paths "config" "Config"
  439. # Validate component dependencies
  440. validate_component_dependencies
  441. # Scan for orphaned files if verbose
  442. if [ "$VERBOSE" = true ]; then
  443. scan_for_orphaned_files
  444. fi
  445. # Print summary and exit with appropriate code
  446. if print_summary; then
  447. exit 0
  448. else
  449. exit 1
  450. fi
  451. }
  452. main "$@"