auto-detect-components.sh 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  1. #!/usr/bin/env bash
  2. #############################################################################
  3. # Auto-Detect Components Script v2.0.0
  4. # Scans .opencode directory for new components not in registry
  5. # Validates existing entries, fixes typos, removes deleted components
  6. # Performs security checks on component files
  7. #############################################################################
  8. set -e
  9. # Colors
  10. RED='\033[0;31m'
  11. GREEN='\033[0;32m'
  12. YELLOW='\033[1;33m'
  13. BLUE='\033[0;34m'
  14. CYAN='\033[0;36m'
  15. MAGENTA='\033[0;35m'
  16. BOLD='\033[1m'
  17. NC='\033[0m'
  18. # Configuration
  19. REGISTRY_FILE="registry.json"
  20. REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
  21. AUTO_ADD=false
  22. DRY_RUN=false
  23. VALIDATE_EXISTING=true
  24. SECURITY_CHECK=true
  25. # Arrays to store components
  26. declare -a NEW_COMPONENTS
  27. declare -a FIXED_COMPONENTS
  28. declare -a REMOVED_COMPONENTS
  29. declare -a SECURITY_ISSUES
  30. # Counters
  31. TOTAL_FIXED=0
  32. TOTAL_REMOVED=0
  33. TOTAL_SECURITY_ISSUES=0
  34. #############################################################################
  35. # Utility Functions
  36. #############################################################################
  37. print_header() {
  38. echo -e "${CYAN}${BOLD}"
  39. echo "╔════════════════════════════════════════════════════════════════╗"
  40. echo "║ ║"
  41. echo "║ Auto-Detect Components v2.0.0 ║"
  42. echo "║ Enhanced with Security & Validation ║"
  43. echo "║ ║"
  44. echo "╚════════════════════════════════════════════════════════════════╝"
  45. echo -e "${NC}"
  46. }
  47. print_success() {
  48. echo -e "${GREEN}✓${NC} $1"
  49. }
  50. print_error() {
  51. echo -e "${RED}✗${NC} $1"
  52. }
  53. print_warning() {
  54. echo -e "${YELLOW}⚠${NC} $1"
  55. }
  56. print_info() {
  57. echo -e "${BLUE}ℹ${NC} $1"
  58. }
  59. print_security() {
  60. echo -e "${MAGENTA}🔒${NC} $1"
  61. }
  62. usage() {
  63. echo "Usage: $0 [OPTIONS]"
  64. echo ""
  65. echo "Options:"
  66. echo " -a, --auto-add Automatically add new components to registry"
  67. echo " -d, --dry-run Show what would be changed without modifying registry"
  68. echo " -s, --skip-validation Skip validation of existing registry entries"
  69. echo " -n, --no-security Skip security checks on component files"
  70. echo " -h, --help Show this help message"
  71. echo ""
  72. echo "Features:"
  73. echo " • Detects new components in .opencode directory"
  74. echo " • Validates existing registry entries"
  75. echo " • Auto-fixes typos and wrong paths"
  76. echo " • Removes entries for deleted components"
  77. echo " • Performs security checks (permissions, secrets, path validation)"
  78. echo ""
  79. exit 0
  80. }
  81. #############################################################################
  82. # Security Functions
  83. #############################################################################
  84. check_file_security() {
  85. local file=$1
  86. local issues=()
  87. # For markdown files, be less strict (they contain examples and documentation)
  88. if [[ "$file" == *.md ]]; then
  89. # Only check for executable permissions on markdown
  90. if [ -x "$file" ]; then
  91. issues+=("Markdown file should not be executable")
  92. fi
  93. # Check for actual secrets (not examples) - very specific patterns
  94. # Look for real API keys like sk-proj-xxxxx or ghp_xxxxx
  95. if grep -qE '(sk-proj-[a-zA-Z0-9]{40,}|ghp_[a-zA-Z0-9]{36,}|xox[baprs]-[a-zA-Z0-9-]{10,})' "$file" 2>/dev/null; then
  96. issues+=("Potential real API key detected")
  97. fi
  98. else
  99. # For non-markdown files, be more strict
  100. # Check file permissions (should not be world-writable)
  101. if [ -w "$file" ] && [ "$(stat -f '%A' "$file" 2>/dev/null || stat -c '%a' "$file" 2>/dev/null)" -gt 664 ]; then
  102. issues+=("File has overly permissive permissions")
  103. fi
  104. # Check for potential secrets
  105. if grep -qiE '(password|secret|api[_-]?key|token|credential|private[_-]?key).*[:=].*[a-zA-Z0-9]{20,}' "$file" 2>/dev/null; then
  106. issues+=("Potential hardcoded secrets detected")
  107. fi
  108. fi
  109. # Return issues
  110. if [ ${#issues[@]} -gt 0 ]; then
  111. printf '%s\n' "${issues[@]}"
  112. return 1
  113. fi
  114. return 0
  115. }
  116. run_security_checks() {
  117. if [ "$SECURITY_CHECK" = false ]; then
  118. return 0
  119. fi
  120. print_info "Running security checks..."
  121. echo ""
  122. local categories=("agent" "command" "tool" "plugin" "context")
  123. for category in "${categories[@]}"; do
  124. local category_dir="$REPO_ROOT/.opencode/$category"
  125. if [ ! -d "$category_dir" ]; then
  126. continue
  127. fi
  128. while IFS= read -r file; do
  129. local rel_path="${file#$REPO_ROOT/}"
  130. # Skip excluded directories
  131. if [[ "$rel_path" == *"/node_modules/"* ]] || \
  132. [[ "$rel_path" == *"/tests/"* ]] || \
  133. [[ "$rel_path" == *"/docs/"* ]]; then
  134. continue
  135. fi
  136. # Check security
  137. local security_output
  138. if ! security_output=$(check_file_security "$file"); then
  139. TOTAL_SECURITY_ISSUES=$((TOTAL_SECURITY_ISSUES + 1))
  140. SECURITY_ISSUES+=("${rel_path}|${security_output}")
  141. print_security "Security issue in: ${rel_path}"
  142. while IFS= read -r issue; do
  143. echo " - ${issue}"
  144. done <<< "$security_output"
  145. echo ""
  146. fi
  147. done < <(find "$category_dir" -type f -name "*.md" 2>/dev/null)
  148. done
  149. if [ $TOTAL_SECURITY_ISSUES -eq 0 ]; then
  150. print_success "No security issues found"
  151. echo ""
  152. fi
  153. }
  154. #############################################################################
  155. # Path Validation and Fixing
  156. #############################################################################
  157. find_similar_path() {
  158. local wrong_path=$1
  159. local threshold=3 # Maximum edit distance
  160. # Get directory and filename
  161. local dir=$(dirname "$wrong_path")
  162. local filename=$(basename "$wrong_path")
  163. # Search for similar files in the expected directory and nearby
  164. local search_dirs=("$REPO_ROOT/$dir" "$REPO_ROOT/.opencode")
  165. for search_dir in "${search_dirs[@]}"; do
  166. if [ ! -d "$search_dir" ]; then
  167. continue
  168. fi
  169. # Find files with similar names
  170. while IFS= read -r candidate; do
  171. local candidate_rel="${candidate#$REPO_ROOT/}"
  172. local candidate_name=$(basename "$candidate")
  173. # Simple similarity check (could be enhanced with Levenshtein distance)
  174. if [[ "$candidate_name" == *"$filename"* ]] || [[ "$filename" == *"$candidate_name"* ]]; then
  175. echo "$candidate_rel"
  176. return 0
  177. fi
  178. done < <(find "$search_dir" -type f -name "*.md" 2>/dev/null)
  179. done
  180. return 1
  181. }
  182. validate_existing_entries() {
  183. if [ "$VALIDATE_EXISTING" = false ]; then
  184. return 0
  185. fi
  186. print_info "Validating existing registry entries..."
  187. echo ""
  188. # Get all component types from registry
  189. local component_types=$(jq -r '.components | keys[]' "$REGISTRY_FILE" 2>/dev/null)
  190. while IFS= read -r comp_type; do
  191. # Get all components of this type
  192. local count=$(jq -r ".components.${comp_type} | length" "$REGISTRY_FILE" 2>/dev/null)
  193. for ((i=0; i<count; i++)); do
  194. local id=$(jq -r ".components.${comp_type}[$i].id" "$REGISTRY_FILE" 2>/dev/null)
  195. local name=$(jq -r ".components.${comp_type}[$i].name" "$REGISTRY_FILE" 2>/dev/null)
  196. local path=$(jq -r ".components.${comp_type}[$i].path" "$REGISTRY_FILE" 2>/dev/null)
  197. # Skip if path is null or empty
  198. if [ -z "$path" ] || [ "$path" = "null" ]; then
  199. continue
  200. fi
  201. local full_path="$REPO_ROOT/$path"
  202. # Check if file exists
  203. if [ ! -f "$full_path" ]; then
  204. print_warning "Component file not found: ${name} (${path})"
  205. # Try to find similar path
  206. local similar_path
  207. if similar_path=$(find_similar_path "$path"); then
  208. print_info "Found similar path: ${similar_path}"
  209. if [ "$AUTO_ADD" = true ] && [ "$DRY_RUN" = false ]; then
  210. fix_component_path "$comp_type" "$i" "$id" "$name" "$path" "$similar_path"
  211. else
  212. FIXED_COMPONENTS+=("${comp_type}|${i}|${id}|${name}|${path}|${similar_path}")
  213. echo " Would fix: ${path} → ${similar_path}"
  214. fi
  215. else
  216. # No similar path found, mark for removal
  217. if [ "$AUTO_ADD" = true ] && [ "$DRY_RUN" = false ]; then
  218. remove_component_from_registry "$comp_type" "$id" "$name" "$path"
  219. else
  220. REMOVED_COMPONENTS+=("${comp_type}|${id}|${name}|${path}")
  221. echo " Would remove: ${name} (deleted)"
  222. fi
  223. fi
  224. echo ""
  225. fi
  226. done
  227. done <<< "$component_types"
  228. }
  229. fix_component_path() {
  230. local comp_type=$1
  231. local index=$2
  232. local id=$3
  233. local name=$4
  234. local old_path=$5
  235. local new_path=$6
  236. local temp_file="${REGISTRY_FILE}.tmp"
  237. jq --arg type "$comp_type" \
  238. --argjson idx "$index" \
  239. --arg newpath "$new_path" \
  240. ".components[\$type][\$idx].path = \$newpath" \
  241. "$REGISTRY_FILE" > "$temp_file"
  242. if [ $? -eq 0 ]; then
  243. mv "$temp_file" "$REGISTRY_FILE"
  244. print_success "Fixed path for ${name}: ${old_path} → ${new_path}"
  245. TOTAL_FIXED=$((TOTAL_FIXED + 1))
  246. else
  247. print_error "Failed to fix path for ${name}"
  248. rm -f "$temp_file"
  249. return 1
  250. fi
  251. }
  252. remove_component_from_registry() {
  253. local comp_type=$1
  254. local id=$2
  255. local name=$3
  256. local path=$4
  257. local temp_file="${REGISTRY_FILE}.tmp"
  258. jq --arg type "$comp_type" \
  259. --arg id "$id" \
  260. ".components[\$type] = [.components[\$type][] | select(.id != \$id)]" \
  261. "$REGISTRY_FILE" > "$temp_file"
  262. if [ $? -eq 0 ]; then
  263. mv "$temp_file" "$REGISTRY_FILE"
  264. print_success "Removed deleted component: ${name}"
  265. TOTAL_REMOVED=$((TOTAL_REMOVED + 1))
  266. else
  267. print_error "Failed to remove component: ${name}"
  268. rm -f "$temp_file"
  269. return 1
  270. fi
  271. }
  272. #############################################################################
  273. # Component Detection
  274. #############################################################################
  275. extract_metadata_from_file() {
  276. local file=$1
  277. local id=""
  278. local name=""
  279. local description=""
  280. # Try to extract from frontmatter (YAML)
  281. if grep -q "^---$" "$file" 2>/dev/null; then
  282. # Extract description from frontmatter
  283. description=$(sed -n '/^---$/,/^---$/p' "$file" | grep "^description:" | sed 's/description: *"\?\(.*\)"\?/\1/' | head -1)
  284. fi
  285. # If no description in frontmatter, try to get from first heading or paragraph
  286. if [ -z "$description" ]; then
  287. description=$(grep -m 1 "^# " "$file" | sed 's/^# //' || echo "")
  288. fi
  289. # Generate ID from filename
  290. local filename=$(basename "$file" .md)
  291. id=$(echo "$filename" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')
  292. # Generate name from filename (capitalize words)
  293. name=$(echo "$filename" | sed 's/-/ /g' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2))}1')
  294. echo "${id}|${name}|${description}"
  295. }
  296. detect_component_type() {
  297. local path=$1
  298. if [[ "$path" == *"/agent/subagents/"* ]]; then
  299. echo "subagent"
  300. elif [[ "$path" == *"/agent/"* ]]; then
  301. echo "agent"
  302. elif [[ "$path" == *"/command/"* ]]; then
  303. echo "command"
  304. elif [[ "$path" == *"/tool/"* ]]; then
  305. echo "tool"
  306. elif [[ "$path" == *"/plugin/"* ]]; then
  307. echo "plugin"
  308. elif [[ "$path" == *"/context/"* ]]; then
  309. echo "context"
  310. else
  311. echo "unknown"
  312. fi
  313. }
  314. get_registry_key() {
  315. local type=$1
  316. case "$type" in
  317. config) echo "config" ;;
  318. *) echo "${type}s" ;;
  319. esac
  320. }
  321. scan_for_new_components() {
  322. print_info "Scanning for new components..."
  323. echo ""
  324. # Get all paths from registry
  325. local registry_paths=$(jq -r '.components | to_entries[] | .value[] | .path' "$REGISTRY_FILE" 2>/dev/null | sort -u)
  326. # Scan .opencode directory
  327. local categories=("agent" "command" "tool" "plugin" "context")
  328. for category in "${categories[@]}"; do
  329. local category_dir="$REPO_ROOT/.opencode/$category"
  330. if [ ! -d "$category_dir" ]; then
  331. continue
  332. fi
  333. # Find all .md files (excluding node_modules, tests, docs)
  334. while IFS= read -r file; do
  335. local rel_path="${file#$REPO_ROOT/}"
  336. # Skip node_modules, tests, docs, templates
  337. if [[ "$rel_path" == *"/node_modules/"* ]] || \
  338. [[ "$rel_path" == *"/tests/"* ]] || \
  339. [[ "$rel_path" == *"/docs/"* ]] || \
  340. [[ "$rel_path" == *"/template"* ]] || \
  341. [[ "$rel_path" == *"README.md" ]] || \
  342. [[ "$rel_path" == *"index.md" ]]; then
  343. continue
  344. fi
  345. # Check if this path is in registry
  346. if ! echo "$registry_paths" | grep -q "^${rel_path}$"; then
  347. # Extract metadata
  348. local metadata=$(extract_metadata_from_file "$file")
  349. IFS='|' read -r id name description <<< "$metadata"
  350. # Detect component type
  351. local comp_type=$(detect_component_type "$rel_path")
  352. if [ "$comp_type" != "unknown" ]; then
  353. NEW_COMPONENTS+=("${comp_type}|${id}|${name}|${description}|${rel_path}")
  354. print_warning "New ${comp_type}: ${name} (${id})"
  355. echo " Path: ${rel_path}"
  356. [ -n "$description" ] && echo " Description: ${description}"
  357. echo ""
  358. fi
  359. fi
  360. done < <(find "$category_dir" -type f -name "*.md" 2>/dev/null)
  361. done
  362. }
  363. add_component_to_registry() {
  364. local comp_type=$1
  365. local id=$2
  366. local name=$3
  367. local description=$4
  368. local path=$5
  369. # Default description if empty
  370. if [ -z "$description" ]; then
  371. description="Component: ${name}"
  372. fi
  373. # Escape quotes and special characters in description
  374. description=$(echo "$description" | sed 's/"/\\"/g' | sed "s/'/\\'/g")
  375. # Get registry key (agents, subagents, commands, etc.)
  376. local registry_key=$(get_registry_key "$comp_type")
  377. # Use jq to properly construct JSON (avoids escaping issues)
  378. local temp_file="${REGISTRY_FILE}.tmp"
  379. jq --arg id "$id" \
  380. --arg name "$name" \
  381. --arg type "$comp_type" \
  382. --arg path "$path" \
  383. --arg desc "$description" \
  384. ".components.${registry_key} += [{
  385. \"id\": \$id,
  386. \"name\": \$name,
  387. \"type\": \$type,
  388. \"path\": \$path,
  389. \"description\": \$desc,
  390. \"tags\": [],
  391. \"dependencies\": [],
  392. \"category\": \"standard\"
  393. }]" "$REGISTRY_FILE" > "$temp_file"
  394. if [ $? -eq 0 ]; then
  395. mv "$temp_file" "$REGISTRY_FILE"
  396. print_success "Added ${comp_type}: ${name}"
  397. else
  398. print_error "Failed to add ${comp_type}: ${name}"
  399. rm -f "$temp_file"
  400. return 1
  401. fi
  402. }
  403. #############################################################################
  404. # Main
  405. #############################################################################
  406. main() {
  407. # Parse arguments
  408. while [ $# -gt 0 ]; do
  409. case "$1" in
  410. -a|--auto-add)
  411. AUTO_ADD=true
  412. shift
  413. ;;
  414. -d|--dry-run)
  415. DRY_RUN=true
  416. shift
  417. ;;
  418. -s|--skip-validation)
  419. VALIDATE_EXISTING=false
  420. shift
  421. ;;
  422. -n|--no-security)
  423. SECURITY_CHECK=false
  424. shift
  425. ;;
  426. -h|--help)
  427. usage
  428. ;;
  429. *)
  430. echo "Unknown option: $1"
  431. usage
  432. ;;
  433. esac
  434. done
  435. print_header
  436. # Check dependencies
  437. if ! command -v jq &> /dev/null; then
  438. print_error "jq is required but not installed"
  439. exit 1
  440. fi
  441. # Validate registry file
  442. if [ ! -f "$REGISTRY_FILE" ]; then
  443. print_error "Registry file not found: $REGISTRY_FILE"
  444. exit 1
  445. fi
  446. if ! jq empty "$REGISTRY_FILE" 2>/dev/null; then
  447. print_error "Registry file is not valid JSON"
  448. exit 1
  449. fi
  450. # Run security checks
  451. run_security_checks
  452. # Validate existing entries (fixes and removals)
  453. validate_existing_entries
  454. # Scan for new components
  455. scan_for_new_components
  456. # Summary
  457. echo ""
  458. echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
  459. echo -e "${BOLD}Summary${NC}"
  460. echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
  461. echo ""
  462. # Display counts
  463. echo -e "Security Issues: ${MAGENTA}${TOTAL_SECURITY_ISSUES}${NC}"
  464. echo -e "Fixed Paths: ${GREEN}${TOTAL_FIXED}${NC}"
  465. echo -e "Removed Components: ${RED}${TOTAL_REMOVED}${NC}"
  466. echo -e "New Components: ${YELLOW}${#NEW_COMPONENTS[@]}${NC}"
  467. echo ""
  468. # Show pending fixes if in dry-run mode
  469. if [ ${#FIXED_COMPONENTS[@]} -gt 0 ] && [ "$DRY_RUN" = true ]; then
  470. echo -e "${BOLD}Pending Path Fixes:${NC}"
  471. for entry in "${FIXED_COMPONENTS[@]}"; do
  472. IFS='|' read -r comp_type index id name old_path new_path <<< "$entry"
  473. echo " • ${name}: ${old_path} → ${new_path}"
  474. done
  475. echo ""
  476. fi
  477. # Show pending removals if in dry-run mode
  478. if [ ${#REMOVED_COMPONENTS[@]} -gt 0 ] && [ "$DRY_RUN" = true ]; then
  479. echo -e "${BOLD}Pending Removals:${NC}"
  480. for entry in "${REMOVED_COMPONENTS[@]}"; do
  481. IFS='|' read -r comp_type id name path <<< "$entry"
  482. echo " • ${name} (${path})"
  483. done
  484. echo ""
  485. fi
  486. # Check if everything is up to date
  487. if [ ${#NEW_COMPONENTS[@]} -eq 0 ] && \
  488. [ ${#FIXED_COMPONENTS[@]} -eq 0 ] && \
  489. [ ${#REMOVED_COMPONENTS[@]} -eq 0 ] && \
  490. [ $TOTAL_FIXED -eq 0 ] && \
  491. [ $TOTAL_REMOVED -eq 0 ]; then
  492. print_success "Registry is up to date!"
  493. if [ $TOTAL_SECURITY_ISSUES -gt 0 ]; then
  494. echo ""
  495. print_warning "Please review and fix the ${TOTAL_SECURITY_ISSUES} security issue(s) found"
  496. fi
  497. exit 0
  498. fi
  499. # Add components if auto-add is enabled
  500. if [ "$AUTO_ADD" = true ] && [ "$DRY_RUN" = false ]; then
  501. if [ ${#NEW_COMPONENTS[@]} -gt 0 ]; then
  502. print_info "Adding new components to registry..."
  503. echo ""
  504. local added=0
  505. for entry in "${NEW_COMPONENTS[@]}"; do
  506. IFS='|' read -r comp_type id name description path <<< "$entry"
  507. if add_component_to_registry "$comp_type" "$id" "$name" "$description" "$path"; then
  508. added=$((added + 1))
  509. fi
  510. done
  511. echo ""
  512. print_success "Added ${added} component(s) to registry"
  513. fi
  514. # Update timestamp
  515. jq '.metadata.lastUpdated = (now | strftime("%Y-%m-%d"))' "$REGISTRY_FILE" > "${REGISTRY_FILE}.tmp"
  516. mv "${REGISTRY_FILE}.tmp" "$REGISTRY_FILE"
  517. elif [ "$DRY_RUN" = true ]; then
  518. print_info "Dry run mode - no changes made to registry"
  519. echo ""
  520. echo "Run without --dry-run to apply these changes"
  521. else
  522. print_info "Run with --auto-add to apply these changes to registry"
  523. echo ""
  524. echo "Or manually update registry.json"
  525. fi
  526. # Final security warning
  527. if [ $TOTAL_SECURITY_ISSUES -gt 0 ]; then
  528. echo ""
  529. print_warning "⚠️ ${TOTAL_SECURITY_ISSUES} security issue(s) require attention"
  530. fi
  531. exit 0
  532. }
  533. main "$@"