validate-registry.sh 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  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. 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. # 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. id=$(echo "$component" | jq -r '.id')
  303. local name
  304. name=$(echo "$component" | jq -r '.name')
  305. local dependencies
  306. dependencies=$(echo "$component" | jq -r '.dependencies[]?' 2>/dev/null)
  307. if [ -z "$dependencies" ]; then
  308. continue
  309. fi
  310. # Check each dependency
  311. while IFS= read -r dep; do
  312. if [ -z "$dep" ]; then
  313. continue
  314. fi
  315. local result
  316. result=$(check_dependency_exists "$dep")
  317. case "$result" in
  318. found)
  319. [ "$VERBOSE" = true ] && print_success "Dependency OK: ${name} → ${dep}"
  320. ;;
  321. not_found)
  322. MISSING_DEPENDENCIES=$((MISSING_DEPENDENCIES + 1))
  323. MISSING_DEPS+=("${comp_type}|${id}|${name}|${dep}")
  324. print_error "Missing dependency: ${name} (${comp_type%s}) depends on \"${dep}\" (not found in registry)"
  325. ;;
  326. invalid_format)
  327. MISSING_DEPENDENCIES=$((MISSING_DEPENDENCIES + 1))
  328. MISSING_DEPS+=("${comp_type}|${id}|${name}|${dep}")
  329. print_error "Invalid dependency format: ${name} (${comp_type%s}) has invalid dependency \"${dep}\" (expected format: type:id)"
  330. ;;
  331. unknown_type)
  332. MISSING_DEPENDENCIES=$((MISSING_DEPENDENCIES + 1))
  333. MISSING_DEPS+=("${comp_type}|${id}|${name}|${dep}")
  334. print_error "Unknown dependency type: ${name} (${comp_type%s}) has unknown dependency type in \"${dep}\""
  335. ;;
  336. esac
  337. done <<< "$dependencies"
  338. done <<< "$components"
  339. done <<< "$component_types"
  340. }
  341. #############################################################################
  342. # Reporting
  343. #############################################################################
  344. print_summary() {
  345. echo ""
  346. echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
  347. echo -e "${BOLD}Validation Summary${NC}"
  348. echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
  349. echo ""
  350. echo -e "Total paths checked: ${CYAN}${TOTAL_PATHS}${NC}"
  351. echo -e "Valid paths: ${GREEN}${VALID_PATHS}${NC}"
  352. echo -e "Missing paths: ${RED}${MISSING_PATHS}${NC}"
  353. echo -e "Missing dependencies: ${RED}${MISSING_DEPENDENCIES}${NC}"
  354. if [ "$VERBOSE" = true ]; then
  355. echo -e "Orphaned files: ${YELLOW}${ORPHANED_FILES}${NC}"
  356. fi
  357. echo ""
  358. local has_errors=false
  359. # Check for missing paths
  360. if [ $MISSING_PATHS -gt 0 ]; then
  361. has_errors=true
  362. print_error "Found ${MISSING_PATHS} missing file(s)"
  363. echo ""
  364. echo "Missing files:"
  365. for entry in "${MISSING_FILES[@]}"; do
  366. IFS='|' read -r cat_id name path <<< "$entry"
  367. echo " - ${path} (${cat_id})"
  368. done
  369. echo ""
  370. if [ "$FIX_MODE" = false ]; then
  371. print_info "Run with --fix flag to see suggested fixes"
  372. echo ""
  373. fi
  374. fi
  375. # Check for missing dependencies
  376. if [ $MISSING_DEPENDENCIES -gt 0 ]; then
  377. has_errors=true
  378. print_error "Found ${MISSING_DEPENDENCIES} missing or invalid dependencies"
  379. echo ""
  380. echo "Missing dependencies:"
  381. for entry in "${MISSING_DEPS[@]}"; do
  382. IFS='|' read -r comp_type id name dep <<< "$entry"
  383. echo " - ${name} (${comp_type%s}) → ${dep}"
  384. done
  385. echo ""
  386. print_info "Fix by either:"
  387. echo " 1. Adding the missing component to the registry"
  388. echo " 2. Removing the dependency from the component's frontmatter"
  389. echo ""
  390. fi
  391. # Success case
  392. if [ "$has_errors" = false ]; then
  393. print_success "All registry paths are valid!"
  394. print_success "All component dependencies are valid!"
  395. if [ $ORPHANED_FILES -gt 0 ] && [ "$VERBOSE" = true ]; then
  396. echo ""
  397. print_warning "Found ${ORPHANED_FILES} orphaned file(s) not in registry"
  398. echo ""
  399. echo "Orphaned files:"
  400. for file in "${ORPHANED_COMPONENTS[@]}"; do
  401. echo " - $file"
  402. done
  403. echo ""
  404. echo "Consider adding these to registry.json or removing them."
  405. fi
  406. return 0
  407. else
  408. echo "Please fix these issues before proceeding."
  409. return 1
  410. fi
  411. }
  412. #############################################################################
  413. # Main
  414. #############################################################################
  415. main() {
  416. # Parse arguments
  417. while [ $# -gt 0 ]; do
  418. case "$1" in
  419. -v|--verbose)
  420. VERBOSE=true
  421. shift
  422. ;;
  423. -f|--fix)
  424. FIX_MODE=true
  425. VERBOSE=true
  426. shift
  427. ;;
  428. -h|--help)
  429. usage
  430. ;;
  431. *)
  432. echo "Unknown option: $1"
  433. usage
  434. ;;
  435. esac
  436. done
  437. print_header
  438. # Check dependencies
  439. check_dependencies
  440. # Validate registry file
  441. validate_registry_file
  442. echo ""
  443. print_info "Validating component paths..."
  444. echo ""
  445. # Validate each category
  446. validate_component_paths "agents" "Agents"
  447. validate_component_paths "subagents" "Subagents"
  448. validate_component_paths "commands" "Commands"
  449. validate_component_paths "tools" "Tools"
  450. validate_component_paths "plugins" "Plugins"
  451. validate_component_paths "contexts" "Contexts"
  452. validate_component_paths "config" "Config"
  453. # Validate component dependencies
  454. validate_component_dependencies
  455. # Scan for orphaned files if verbose
  456. if [ "$VERBOSE" = true ]; then
  457. scan_for_orphaned_files
  458. fi
  459. # Print summary and exit with appropriate code
  460. if print_summary; then
  461. exit 0
  462. else
  463. exit 1
  464. fi
  465. }
  466. main "$@"