validate-registry.sh 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  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. return 0
  138. }
  139. suggest_fix() {
  140. local missing_path=$1
  141. local component_id=$2
  142. # Extract directory and filename
  143. local dir=""
  144. local base_dir=""
  145. dir=$(dirname "$missing_path")
  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. # shellcheck disable=SC2143
  202. if ! echo "$registry_paths" | grep -q "^${rel_path}$"; then
  203. ORPHANED_FILES=$((ORPHANED_FILES + 1))
  204. ORPHANED_COMPONENTS+=("$rel_path")
  205. [ "$VERBOSE" = true ] && print_warning "Orphaned file (not in registry): ${rel_path}"
  206. fi
  207. done < <(find "$category_dir" -type f \( -name "*.md" -o -name "*.ts" \) 2>/dev/null)
  208. done
  209. }
  210. #############################################################################
  211. # Dependency Validation
  212. #############################################################################
  213. check_dependency_exists() {
  214. local dep=$1
  215. # Parse dependency format: type:id
  216. if [[ ! "$dep" =~ ^([^:]+):(.+)$ ]]; then
  217. echo "invalid_format"
  218. return 1
  219. fi
  220. local dep_type="${BASH_REMATCH[1]}"
  221. local dep_id="${BASH_REMATCH[2]}"
  222. # Map dependency type to registry category
  223. local registry_category=""
  224. case "$dep_type" in
  225. agent)
  226. registry_category="agents"
  227. ;;
  228. subagent)
  229. registry_category="subagents"
  230. ;;
  231. command)
  232. registry_category="commands"
  233. ;;
  234. tool)
  235. registry_category="tools"
  236. ;;
  237. plugin)
  238. registry_category="plugins"
  239. ;;
  240. skill)
  241. registry_category="skills"
  242. ;;
  243. context)
  244. registry_category="contexts"
  245. ;;
  246. config)
  247. registry_category="config"
  248. ;;
  249. *)
  250. echo "unknown_type"
  251. return 1
  252. ;;
  253. esac
  254. # Check if component exists in registry
  255. # First try exact ID match
  256. local exists
  257. exists=$(jq -r ".components.${registry_category}[]? | select(.id == \"${dep_id}\") | .id" "$REGISTRY_FILE" 2>/dev/null)
  258. if [ -n "$exists" ]; then
  259. echo "found"
  260. return 0
  261. fi
  262. # For context dependencies, also try path-based lookup
  263. # Format: context:core/standards/code -> .opencode/context/core/standards/code.md
  264. if [ "$dep_type" = "context" ]; then
  265. # Check for wildcard pattern (e.g., context:core/context-system/*)
  266. if [[ "$dep_id" == *"*" ]]; then
  267. # Extract prefix before wildcard
  268. local prefix="${dep_id%%\**}"
  269. # Check if any context files match the prefix
  270. local matches
  271. matches=$(jq -r ".components.${registry_category}[]? | select(.path | startswith(\".opencode/context/${prefix}\")) | .id" "$REGISTRY_FILE" 2>/dev/null | head -1)
  272. if [ -n "$matches" ]; then
  273. echo "found"
  274. return 0
  275. fi
  276. else
  277. # Try exact path match
  278. local context_path=".opencode/context/${dep_id}.md"
  279. local exists_by_path
  280. exists_by_path=$(jq -r ".components.${registry_category}[]? | select(.path == \"${context_path}\") | .id" "$REGISTRY_FILE" 2>/dev/null)
  281. if [ -n "$exists_by_path" ]; then
  282. echo "found"
  283. return 0
  284. fi
  285. fi
  286. fi
  287. echo "not_found"
  288. return 1
  289. }
  290. validate_component_dependencies() {
  291. echo ""
  292. print_info "Validating component dependencies..."
  293. echo ""
  294. # Get all component types
  295. local component_types
  296. component_types=$(jq -r '.components | keys[]' "$REGISTRY_FILE" 2>/dev/null)
  297. while IFS= read -r comp_type; do
  298. # Get all components of this type
  299. local components
  300. components=$(jq -r ".components.${comp_type}[]? | @json" "$REGISTRY_FILE" 2>/dev/null)
  301. if [ -z "$components" ]; then
  302. continue
  303. fi
  304. while IFS= read -r component; do
  305. local id=""
  306. local path=""
  307. local name=""
  308. id=$(echo "$component" | jq -r '.id')
  309. path=$(echo "$component" | jq -r '.path')
  310. name=$(echo "$component" | jq -r '.name')
  311. local dependencies
  312. dependencies=$(echo "$component" | jq -r '.dependencies[]?' 2>/dev/null)
  313. if [ -z "$dependencies" ]; then
  314. continue
  315. fi
  316. # Check each dependency
  317. while IFS= read -r dep; do
  318. if [ -z "$dep" ]; then
  319. continue
  320. fi
  321. local result
  322. result=$(check_dependency_exists "$dep" || true)
  323. case "$result" in
  324. found)
  325. [ "$VERBOSE" = true ] && print_success "Dependency OK: ${name} → ${dep}"
  326. ;;
  327. not_found)
  328. MISSING_DEPENDENCIES=$((MISSING_DEPENDENCIES + 1))
  329. MISSING_DEPS+=("${comp_type}|${id}|${name}|${dep}")
  330. print_error "Missing dependency: ${name} (${comp_type%s}) depends on \"${dep}\" (not found in registry)"
  331. ;;
  332. invalid_format)
  333. MISSING_DEPENDENCIES=$((MISSING_DEPENDENCIES + 1))
  334. MISSING_DEPS+=("${comp_type}|${id}|${name}|${dep}")
  335. print_error "Invalid dependency format: ${name} (${comp_type%s}) has invalid dependency \"${dep}\" (expected format: type:id)"
  336. ;;
  337. unknown_type)
  338. MISSING_DEPENDENCIES=$((MISSING_DEPENDENCIES + 1))
  339. MISSING_DEPS+=("${comp_type}|${id}|${name}|${dep}")
  340. print_error "Unknown dependency type: ${name} (${comp_type%s}) has unknown dependency type in \"${dep}\""
  341. ;;
  342. esac
  343. done <<< "$dependencies"
  344. done <<< "$components"
  345. done <<< "$component_types"
  346. }
  347. #############################################################################
  348. # Reporting
  349. #############################################################################
  350. print_summary() {
  351. echo ""
  352. echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
  353. echo -e "${BOLD}Validation Summary${NC}"
  354. echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
  355. echo ""
  356. echo -e "Total paths checked: ${CYAN}${TOTAL_PATHS}${NC}"
  357. echo -e "Valid paths: ${GREEN}${VALID_PATHS}${NC}"
  358. echo -e "Missing paths: ${RED}${MISSING_PATHS}${NC}"
  359. echo -e "Missing dependencies: ${RED}${MISSING_DEPENDENCIES}${NC}"
  360. if [ "$VERBOSE" = true ]; then
  361. echo -e "Orphaned files: ${YELLOW}${ORPHANED_FILES}${NC}"
  362. fi
  363. echo ""
  364. local has_errors=false
  365. # Check for missing paths
  366. if [ $MISSING_PATHS -gt 0 ]; then
  367. has_errors=true
  368. print_error "Found ${MISSING_PATHS} missing file(s)"
  369. echo ""
  370. echo "Missing files:"
  371. for entry in "${MISSING_FILES[@]}"; do
  372. IFS='|' read -r cat_id name path <<< "$entry"
  373. echo " - ${path} (${cat_id})"
  374. done
  375. echo ""
  376. if [ "$FIX_MODE" = false ]; then
  377. print_info "Run with --fix flag to see suggested fixes"
  378. echo ""
  379. fi
  380. fi
  381. # Check for missing dependencies
  382. if [ $MISSING_DEPENDENCIES -gt 0 ]; then
  383. has_errors=true
  384. print_error "Found ${MISSING_DEPENDENCIES} missing or invalid dependencies"
  385. echo ""
  386. echo "Missing dependencies:"
  387. for entry in "${MISSING_DEPS[@]}"; do
  388. IFS='|' read -r comp_type id name dep <<< "$entry"
  389. echo " - ${name} (${comp_type%s}) → ${dep}"
  390. done
  391. echo ""
  392. print_info "Fix by either:"
  393. echo " 1. Adding the missing component to the registry"
  394. echo " 2. Removing the dependency from the component's frontmatter"
  395. echo ""
  396. fi
  397. # Success case
  398. if [ "$has_errors" = false ]; then
  399. print_success "All registry paths are valid!"
  400. print_success "All component dependencies are valid!"
  401. if [ $ORPHANED_FILES -gt 0 ] && [ "$VERBOSE" = true ]; then
  402. echo ""
  403. print_warning "Found ${ORPHANED_FILES} orphaned file(s) not in registry"
  404. echo ""
  405. echo "Orphaned files:"
  406. for file in "${ORPHANED_COMPONENTS[@]}"; do
  407. echo " - $file"
  408. done
  409. echo ""
  410. echo "Consider adding these to registry.json or removing them."
  411. fi
  412. return 0
  413. else
  414. echo "Please fix these issues before proceeding."
  415. return 1
  416. fi
  417. }
  418. #############################################################################
  419. # Main
  420. #############################################################################
  421. main() {
  422. # Parse arguments
  423. while [ $# -gt 0 ]; do
  424. case "$1" in
  425. -v|--verbose)
  426. VERBOSE=true
  427. shift
  428. ;;
  429. -f|--fix)
  430. FIX_MODE=true
  431. VERBOSE=true
  432. shift
  433. ;;
  434. -h|--help)
  435. usage
  436. ;;
  437. *)
  438. echo "Unknown option: $1"
  439. usage
  440. ;;
  441. esac
  442. done
  443. print_header
  444. # Check dependencies
  445. check_dependencies
  446. # Validate registry file
  447. validate_registry_file
  448. echo ""
  449. print_info "Validating component paths..."
  450. echo ""
  451. # Validate each category
  452. validate_component_paths "agents" "Agents"
  453. validate_component_paths "subagents" "Subagents"
  454. validate_component_paths "commands" "Commands"
  455. validate_component_paths "tools" "Tools"
  456. validate_component_paths "plugins" "Plugins"
  457. validate_component_paths "contexts" "Contexts"
  458. validate_component_paths "config" "Config"
  459. # Validate component dependencies
  460. validate_component_dependencies
  461. # Scan for orphaned files if verbose
  462. if [ "$VERBOSE" = true ]; then
  463. scan_for_orphaned_files
  464. fi
  465. # Print summary and exit with appropriate code
  466. if print_summary; then
  467. exit 0
  468. else
  469. exit 1
  470. fi
  471. }
  472. main "$@"