validate-registry.sh 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  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. echo "Checking ${category_display}..." >&2
  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. echo "No ${category_display} found" >&2
  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. local base_dir=""
  144. dir=$(dirname "$missing_path")
  145. base_dir=$(echo "$dir" | cut -d'/' -f1-3) # e.g., .opencode/command
  146. # Look for similar files in the expected directory and subdirectories
  147. local similar_files
  148. similar_files=$(find "$REPO_ROOT/$base_dir" -type f -name "*.md" 2>/dev/null | grep -i "$component_id" || true)
  149. if [ -n "$similar_files" ]; then
  150. echo -e " ${YELLOW}→ Possible matches:${NC}"
  151. while IFS= read -r file; do
  152. local rel_path="${file#$REPO_ROOT/}"
  153. echo -e " ${CYAN}${rel_path}${NC}"
  154. done <<< "$similar_files"
  155. fi
  156. }
  157. scan_for_orphaned_files() {
  158. [ "$VERBOSE" = true ] && echo -e "\n${BOLD}Scanning for orphaned files...${NC}"
  159. # Get all paths from registry
  160. local registry_paths
  161. registry_paths=$(jq -r '.components | to_entries[] | .value[] | .path' "$REGISTRY_FILE" 2>/dev/null | sort -u)
  162. # Scan .opencode directory for markdown files
  163. local categories=("agent" "command" "tool" "plugin" "context")
  164. for category in "${categories[@]}"; do
  165. local category_dir="$REPO_ROOT/.opencode/$category"
  166. if [ ! -d "$category_dir" ]; then
  167. continue
  168. fi
  169. # Find all .md and .ts files (excluding node_modules)
  170. while IFS= read -r file; do
  171. local rel_path="${file#$REPO_ROOT/}"
  172. # Skip node_modules
  173. if [[ "$rel_path" == *"/node_modules/"* ]]; then
  174. continue
  175. fi
  176. # Skip README files
  177. if [[ "$rel_path" == *"README.md" ]]; then
  178. continue
  179. fi
  180. # Skip template files
  181. if [[ "$rel_path" == *"-template.md" ]]; then
  182. continue
  183. fi
  184. # Skip tool/plugin TypeScript files
  185. if [[ "$rel_path" == *"/tool/index.ts" ]] || [[ "$rel_path" == *"/tool/template/index.ts" ]]; then
  186. continue
  187. fi
  188. if [[ "$rel_path" == *"/plugin/agent-validator.ts" ]]; then
  189. continue
  190. fi
  191. # Skip plugin internal docs and tests
  192. if [[ "$rel_path" == *"/plugin/docs/"* ]] || [[ "$rel_path" == *"/plugin/tests/"* ]]; then
  193. continue
  194. fi
  195. # Skip scripts directories (internal CLI tools, not registry components)
  196. if [[ "$rel_path" == *"/scripts/"* ]]; then
  197. continue
  198. fi
  199. # Check if this path is in registry
  200. # shellcheck disable=SC2143
  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. # Check for wildcard pattern (e.g., context:core/context-system/*)
  262. if [[ "$dep_id" == *"*" ]]; then
  263. # Extract prefix before wildcard
  264. local prefix="${dep_id%%\**}"
  265. # Check if any context files match the prefix
  266. local matches
  267. matches=$(jq -r ".components.${registry_category}[]? | select(.path | startswith(\".opencode/context/${prefix}\")) | .id" "$REGISTRY_FILE" 2>/dev/null | head -1)
  268. if [ -n "$matches" ]; then
  269. echo "found"
  270. return 0
  271. fi
  272. else
  273. # Try exact path match
  274. local context_path=".opencode/context/${dep_id}.md"
  275. local exists_by_path
  276. exists_by_path=$(jq -r ".components.${registry_category}[]? | select(.path == \"${context_path}\") | .id" "$REGISTRY_FILE" 2>/dev/null)
  277. if [ -n "$exists_by_path" ]; then
  278. echo "found"
  279. return 0
  280. fi
  281. fi
  282. fi
  283. echo "not_found"
  284. return 1
  285. }
  286. validate_component_dependencies() {
  287. echo ""
  288. print_info "Validating component dependencies..."
  289. echo ""
  290. # Get all component types
  291. local component_types
  292. component_types=$(jq -r '.components | keys[]' "$REGISTRY_FILE" 2>/dev/null)
  293. while IFS= read -r comp_type; do
  294. # Get all components of this type
  295. local components
  296. components=$(jq -r ".components.${comp_type}[]? | @json" "$REGISTRY_FILE" 2>/dev/null)
  297. if [ -z "$components" ]; then
  298. continue
  299. fi
  300. while IFS= read -r component; do
  301. local id=""
  302. local path=""
  303. local name=""
  304. id=$(echo "$component" | jq -r '.id')
  305. path=$(echo "$component" | jq -r '.path')
  306. name=$(echo "$component" | jq -r '.name')
  307. local dependencies
  308. dependencies=$(echo "$component" | jq -r '.dependencies[]?' 2>/dev/null)
  309. if [ -z "$dependencies" ]; then
  310. continue
  311. fi
  312. # Check each dependency
  313. while IFS= read -r dep; do
  314. if [ -z "$dep" ]; then
  315. continue
  316. fi
  317. local result
  318. result=$(check_dependency_exists "$dep")
  319. case "$result" in
  320. found)
  321. [ "$VERBOSE" = true ] && print_success "Dependency OK: ${name} → ${dep}"
  322. ;;
  323. not_found)
  324. MISSING_DEPENDENCIES=$((MISSING_DEPENDENCIES + 1))
  325. MISSING_DEPS+=("${comp_type}|${id}|${name}|${dep}")
  326. print_error "Missing dependency: ${name} (${comp_type%s}) depends on \"${dep}\" (not found in registry)"
  327. ;;
  328. invalid_format)
  329. MISSING_DEPENDENCIES=$((MISSING_DEPENDENCIES + 1))
  330. MISSING_DEPS+=("${comp_type}|${id}|${name}|${dep}")
  331. print_error "Invalid dependency format: ${name} (${comp_type%s}) has invalid dependency \"${dep}\" (expected format: type:id)"
  332. ;;
  333. unknown_type)
  334. MISSING_DEPENDENCIES=$((MISSING_DEPENDENCIES + 1))
  335. MISSING_DEPS+=("${comp_type}|${id}|${name}|${dep}")
  336. print_error "Unknown dependency type: ${name} (${comp_type%s}) has unknown dependency type in \"${dep}\""
  337. ;;
  338. esac
  339. done <<< "$dependencies"
  340. done <<< "$components"
  341. done <<< "$component_types"
  342. }
  343. #############################################################################
  344. # Reporting
  345. #############################################################################
  346. print_summary() {
  347. echo ""
  348. echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
  349. echo -e "${BOLD}Validation Summary${NC}"
  350. echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
  351. echo ""
  352. echo -e "Total paths checked: ${CYAN}${TOTAL_PATHS}${NC}"
  353. echo -e "Valid paths: ${GREEN}${VALID_PATHS}${NC}"
  354. echo -e "Missing paths: ${RED}${MISSING_PATHS}${NC}"
  355. echo -e "Missing dependencies: ${RED}${MISSING_DEPENDENCIES}${NC}"
  356. if [ "$VERBOSE" = true ]; then
  357. echo -e "Orphaned files: ${YELLOW}${ORPHANED_FILES}${NC}"
  358. fi
  359. echo ""
  360. local has_errors=false
  361. # Check for missing paths
  362. if [ $MISSING_PATHS -gt 0 ]; then
  363. has_errors=true
  364. print_error "Found ${MISSING_PATHS} missing file(s)"
  365. echo ""
  366. echo "Missing files:"
  367. for entry in "${MISSING_FILES[@]}"; do
  368. IFS='|' read -r cat_id name path <<< "$entry"
  369. echo " - ${path} (${cat_id})"
  370. done
  371. echo ""
  372. if [ "$FIX_MODE" = false ]; then
  373. print_info "Run with --fix flag to see suggested fixes"
  374. echo ""
  375. fi
  376. fi
  377. # Check for missing dependencies
  378. if [ $MISSING_DEPENDENCIES -gt 0 ]; then
  379. has_errors=true
  380. print_error "Found ${MISSING_DEPENDENCIES} missing or invalid dependencies"
  381. echo ""
  382. echo "Missing dependencies:"
  383. for entry in "${MISSING_DEPS[@]}"; do
  384. IFS='|' read -r comp_type id name dep <<< "$entry"
  385. echo " - ${name} (${comp_type%s}) → ${dep}"
  386. done
  387. echo ""
  388. print_info "Fix by either:"
  389. echo " 1. Adding the missing component to the registry"
  390. echo " 2. Removing the dependency from the component's frontmatter"
  391. echo ""
  392. fi
  393. # Success case
  394. if [ "$has_errors" = false ]; then
  395. print_success "All registry paths are valid!"
  396. print_success "All component dependencies are valid!"
  397. if [ $ORPHANED_FILES -gt 0 ] && [ "$VERBOSE" = true ]; then
  398. echo ""
  399. print_warning "Found ${ORPHANED_FILES} orphaned file(s) not in registry"
  400. echo ""
  401. echo "Orphaned files:"
  402. for file in "${ORPHANED_COMPONENTS[@]}"; do
  403. echo " - $file"
  404. done
  405. echo ""
  406. echo "Consider adding these to registry.json or removing them."
  407. fi
  408. return 0
  409. else
  410. echo "Please fix these issues before proceeding."
  411. return 1
  412. fi
  413. }
  414. #############################################################################
  415. # Main
  416. #############################################################################
  417. main() {
  418. # Parse arguments
  419. while [ $# -gt 0 ]; do
  420. case "$1" in
  421. -v|--verbose)
  422. VERBOSE=true
  423. shift
  424. ;;
  425. -f|--fix)
  426. FIX_MODE=true
  427. VERBOSE=true
  428. shift
  429. ;;
  430. -h|--help)
  431. usage
  432. ;;
  433. *)
  434. echo "Unknown option: $1"
  435. usage
  436. ;;
  437. esac
  438. done
  439. print_header
  440. # Check dependencies
  441. check_dependencies
  442. # Validate registry file
  443. validate_registry_file
  444. echo ""
  445. print_info "Validating component paths..."
  446. echo ""
  447. # Validate each category
  448. validate_component_paths "agents" "Agents"
  449. validate_component_paths "subagents" "Subagents"
  450. validate_component_paths "commands" "Commands"
  451. validate_component_paths "tools" "Tools"
  452. validate_component_paths "plugins" "Plugins"
  453. validate_component_paths "contexts" "Contexts"
  454. validate_component_paths "config" "Config"
  455. # Validate component dependencies
  456. validate_component_dependencies
  457. # Scan for orphaned files if verbose
  458. if [ "$VERBOSE" = true ]; then
  459. scan_for_orphaned_files
  460. fi
  461. # Print summary and exit with appropriate code
  462. if print_summary; then
  463. exit 0
  464. else
  465. exit 1
  466. fi
  467. }
  468. main "$@"