auto-detect-components.sh 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893
  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. # Get directory and filename
  160. local dir
  161. dir=$(dirname "$wrong_path")
  162. local filename
  163. filename=$(basename "$wrong_path")
  164. # First, try to find exact filename match in category subdirectories
  165. # e.g., .opencode/agent/opencoder.md → .opencode/agent/core/opencoder.md
  166. local base_dir
  167. base_dir=$(echo "$dir" | cut -d'/' -f1-2) # e.g., .opencode/agent
  168. if [ -d "$REPO_ROOT/$base_dir" ]; then
  169. # Search recursively in the base directory for exact filename match
  170. while IFS= read -r candidate; do
  171. local candidate_rel
  172. candidate_rel="${candidate#$REPO_ROOT/}"
  173. local candidate_name
  174. candidate_name=$(basename "$candidate")
  175. # Exact filename match
  176. if [[ "$candidate_name" == "$filename" ]]; then
  177. echo "$candidate_rel"
  178. return 0
  179. fi
  180. done < <(find "$REPO_ROOT/$base_dir" -type f -name "$filename" 2>/dev/null)
  181. fi
  182. # Fallback: search for similar names in the entire .opencode directory
  183. local search_dirs=("$REPO_ROOT/$dir" "$REPO_ROOT/.opencode")
  184. for search_dir in "${search_dirs[@]}"; do
  185. if [ ! -d "$search_dir" ]; then
  186. continue
  187. fi
  188. # Find files with similar names
  189. while IFS= read -r candidate; do
  190. local candidate_rel
  191. candidate_rel="${candidate#$REPO_ROOT/}"
  192. local candidate_name
  193. candidate_name=$(basename "$candidate")
  194. # Simple similarity check
  195. if [[ "$candidate_name" == *"$filename"* ]] || [[ "$filename" == *"$candidate_name"* ]]; then
  196. echo "$candidate_rel"
  197. return 0
  198. fi
  199. done < <(find "$search_dir" -type f -name "*.md" 2>/dev/null)
  200. done
  201. return 1
  202. }
  203. validate_existing_entries() {
  204. if [ "$VALIDATE_EXISTING" = false ]; then
  205. return 0
  206. fi
  207. print_info "Validating existing registry entries..."
  208. echo ""
  209. # Get all component types from registry
  210. local component_types
  211. component_types=$(jq -r '.components | keys[]' "$REGISTRY_FILE" 2>/dev/null)
  212. while IFS= read -r comp_type; do
  213. # Get all components of this type
  214. local count
  215. count=$(jq -r ".components.${comp_type} | length" "$REGISTRY_FILE" 2>/dev/null)
  216. for ((i=0; i<count; i++)); do
  217. local id
  218. id=$(jq -r ".components.${comp_type}[$i].id" "$REGISTRY_FILE" 2>/dev/null)
  219. local name
  220. name=$(jq -r ".components.${comp_type}[$i].name" "$REGISTRY_FILE" 2>/dev/null)
  221. local path
  222. path=$(jq -r ".components.${comp_type}[$i].path" "$REGISTRY_FILE" 2>/dev/null)
  223. # Skip if path is null or empty
  224. if [ -z "$path" ] || [ "$path" = "null" ]; then
  225. continue
  226. fi
  227. local full_path="$REPO_ROOT/$path"
  228. # Check if file exists
  229. if [ ! -f "$full_path" ]; then
  230. print_warning "Component file not found: ${name} (${path})"
  231. # Try to find similar path
  232. local similar_path
  233. if similar_path=$(find_similar_path "$path"); then
  234. print_info "Found similar path: ${similar_path}"
  235. if [ "$AUTO_ADD" = true ] && [ "$DRY_RUN" = false ]; then
  236. fix_component_path "$comp_type" "$i" "$id" "$name" "$path" "$similar_path"
  237. else
  238. FIXED_COMPONENTS+=("${comp_type}|${i}|${id}|${name}|${path}|${similar_path}")
  239. echo " Would fix: ${path} → ${similar_path}"
  240. fi
  241. else
  242. # No similar path found, mark for removal
  243. if [ "$AUTO_ADD" = true ] && [ "$DRY_RUN" = false ]; then
  244. remove_component_from_registry "$comp_type" "$id" "$name" "$path"
  245. else
  246. REMOVED_COMPONENTS+=("${comp_type}|${id}|${name}|${path}")
  247. echo " Would remove: ${name} (deleted)"
  248. fi
  249. fi
  250. echo ""
  251. fi
  252. done
  253. done <<< "$component_types"
  254. }
  255. fix_component_path() {
  256. local comp_type=$1
  257. local index=$2
  258. local id=$3
  259. local name=$4
  260. local old_path=$5
  261. local new_path=$6
  262. local temp_file="${REGISTRY_FILE}.tmp"
  263. jq --arg type "$comp_type" \
  264. --argjson idx "$index" \
  265. --arg newpath "$new_path" \
  266. ".components[\$type][\$idx].path = \$newpath" \
  267. "$REGISTRY_FILE" > "$temp_file"
  268. if [ $? -eq 0 ]; then
  269. mv "$temp_file" "$REGISTRY_FILE"
  270. print_success "Fixed path for ${name}: ${old_path} → ${new_path}"
  271. TOTAL_FIXED=$((TOTAL_FIXED + 1))
  272. else
  273. print_error "Failed to fix path for ${name}"
  274. rm -f "$temp_file"
  275. return 1
  276. fi
  277. }
  278. remove_component_from_registry() {
  279. local comp_type=$1
  280. local id=$2
  281. local name=$3
  282. local path=$4
  283. local temp_file="${REGISTRY_FILE}.tmp"
  284. jq --arg type "$comp_type" \
  285. --arg id "$id" \
  286. ".components[\$type] = [.components[\$type][] | select(.id != \$id)]" \
  287. "$REGISTRY_FILE" > "$temp_file"
  288. if [ $? -eq 0 ]; then
  289. mv "$temp_file" "$REGISTRY_FILE"
  290. print_success "Removed deleted component: ${name}"
  291. TOTAL_REMOVED=$((TOTAL_REMOVED + 1))
  292. else
  293. print_error "Failed to remove component: ${name}"
  294. rm -f "$temp_file"
  295. return 1
  296. fi
  297. }
  298. #############################################################################
  299. # Component Detection
  300. #############################################################################
  301. extract_metadata_from_file() {
  302. local file=$1
  303. local id=""
  304. local name=""
  305. local description=""
  306. local tags=""
  307. local dependencies=""
  308. # Generate ID from filename first (needed for metadata lookup)
  309. local filename
  310. filename=$(basename "$file" .md)
  311. id=$(echo "$filename" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')
  312. # Try to extract from frontmatter (YAML)
  313. if grep -q "^---$" "$file" 2>/dev/null; then
  314. # Extract description from frontmatter
  315. description=$(sed -n '/^---$/,/^---$/p' "$file" | grep "^description:" | sed 's/^description: *//; s/^"//; s/"$//' | head -1)
  316. # Extract tags from frontmatter (YAML array format)
  317. # Handles both: tags: [tag1, tag2] and multi-line format
  318. local in_tags=false
  319. local frontmatter
  320. frontmatter=$(sed -n '/^---$/,/^---$/p' "$file")
  321. while IFS= read -r line; do
  322. if [[ "$line" =~ ^tags: ]]; then
  323. # Inline array format: tags: [tag1, tag2]
  324. if [[ "$line" =~ \[.*\] ]]; then
  325. tags=$(echo "$line" | sed 's/^tags: *\[//; s/\].*//; s/ //g')
  326. else
  327. in_tags=true
  328. fi
  329. elif [[ "$in_tags" == true ]]; then
  330. # Multi-line array format
  331. if [[ "$line" =~ ^---$ ]]; then
  332. # End of frontmatter
  333. in_tags=false
  334. elif [[ "$line" =~ ^[[:space:]]*- ]]; then
  335. local tag
  336. tag=$(echo "$line" | sed 's/^[[:space:]]*- *//')
  337. if [ -z "$tags" ]; then
  338. tags="$tag"
  339. else
  340. tags="$tags,$tag"
  341. fi
  342. elif [[ ! "$line" =~ ^[[:space:]] ]]; then
  343. in_tags=false
  344. fi
  345. fi
  346. done <<< "$frontmatter"
  347. # Extract dependencies from frontmatter (similar to tags)
  348. # Handles: dependencies: [dep1, dep2] or multi-line format
  349. local in_deps=false
  350. while IFS= read -r line; do
  351. if [[ "$line" =~ ^dependencies: ]]; then
  352. # Inline array format
  353. if [[ "$line" =~ \[.*\] ]]; then
  354. dependencies=$(echo "$line" | sed 's/^dependencies: *\[//; s/\].*//; s/ //g; s/"//g; s/'"'"'//g')
  355. else
  356. in_deps=true
  357. fi
  358. elif [[ "$in_deps" == true ]]; then
  359. # Multi-line array format
  360. if [[ "$line" =~ ^---$ ]]; then
  361. # End of frontmatter
  362. in_deps=false
  363. elif [[ "$line" =~ ^[[:space:]]*- ]]; then
  364. # Extract dependency (remove leading dash and whitespace)
  365. local dep
  366. dep=$(echo "$line" | sed 's/^[[:space:]]*- *//' | sed 's/"//g; s/'"'"'//g' | sed 's/#.*//' | xargs)
  367. # Skip empty lines and comments
  368. if [ -n "$dep" ] && [[ ! "$dep" =~ ^# ]]; then
  369. if [ -z "$dependencies" ]; then
  370. dependencies="$dep"
  371. else
  372. dependencies="$dependencies,$dep"
  373. fi
  374. fi
  375. elif [[ "$line" =~ ^[[:space:]]*# ]] || [[ -z "$line" ]]; then
  376. # Skip comments and empty lines within dependencies
  377. continue
  378. elif [[ ! "$line" =~ ^[[:space:]] ]]; then
  379. # Non-indented line means we've moved to a new field
  380. in_deps=false
  381. fi
  382. fi
  383. done <<< "$frontmatter"
  384. fi
  385. # If no description in frontmatter, try to get from first heading or paragraph
  386. if [ -z "$description" ]; then
  387. description=$(grep -m 1 "^# " "$file" | sed 's/^# //' || echo "")
  388. fi
  389. # Generate name from filename (capitalize words)
  390. name=$(echo "$filename" | sed 's/-/ /g' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2))}1')
  391. # Check if agent-metadata.json exists and merge metadata from it
  392. local metadata_file="$REPO_ROOT/.opencode/config/agent-metadata.json"
  393. if [ -f "$metadata_file" ] && command -v jq &> /dev/null; then
  394. # Try to find metadata for this agent ID
  395. local metadata_entry
  396. metadata_entry=$(jq -r ".agents[\"$id\"] // empty" "$metadata_file" 2>/dev/null)
  397. if [ -n "$metadata_entry" ] && [ "$metadata_entry" != "null" ]; then
  398. # Override name if present in metadata
  399. local meta_name
  400. meta_name=$(echo "$metadata_entry" | jq -r '.name // empty' 2>/dev/null)
  401. if [ -n "$meta_name" ] && [ "$meta_name" != "null" ]; then
  402. name="$meta_name"
  403. fi
  404. # Merge tags (prefer frontmatter, fallback to metadata)
  405. if [ -z "$tags" ]; then
  406. local meta_tags
  407. meta_tags=$(echo "$metadata_entry" | jq -r '.tags // [] | join(",")' 2>/dev/null)
  408. if [ -n "$meta_tags" ] && [ "$meta_tags" != "null" ]; then
  409. tags="$meta_tags"
  410. fi
  411. fi
  412. # Merge dependencies (prefer frontmatter, fallback to metadata)
  413. if [ -z "$dependencies" ]; then
  414. local meta_deps
  415. meta_deps=$(echo "$metadata_entry" | jq -r '.dependencies // [] | join(",")' 2>/dev/null)
  416. if [ -n "$meta_deps" ] && [ "$meta_deps" != "null" ]; then
  417. dependencies="$meta_deps"
  418. fi
  419. fi
  420. fi
  421. fi
  422. echo "${id}|${name}|${description}|${tags}|${dependencies}"
  423. }
  424. detect_component_type() {
  425. local path=$1
  426. # Handle category-based paths (e.g., .opencode/agent/core/openagent.md)
  427. # and flat paths (e.g., .opencode/agent/openagent.md)
  428. if [[ "$path" == *"/agent/subagents/"* ]]; then
  429. echo "subagent"
  430. elif [[ "$path" == *"/agent/"* ]]; then
  431. echo "agent"
  432. elif [[ "$path" == *"/command/"* ]]; then
  433. echo "command"
  434. elif [[ "$path" == *"/tool/"* ]]; then
  435. echo "tool"
  436. elif [[ "$path" == *"/plugin/"* ]]; then
  437. echo "plugin"
  438. elif [[ "$path" == *"/context/"* ]]; then
  439. echo "context"
  440. else
  441. echo "unknown"
  442. fi
  443. }
  444. extract_category_from_path() {
  445. local path=$1
  446. # Extract category from path like .opencode/agent/core/openagent.md → core
  447. # or .opencode/agent/subagents/code/tester.md → code (for subagents)
  448. if [[ "$path" == *"/agent/subagents/"* ]]; then
  449. # For subagents: .opencode/agent/subagents/code/tester.md → code
  450. echo "$path" | sed -E 's|.*/agent/subagents/([^/]+)/.*|\1|'
  451. elif [[ "$path" == *"/agent/"* ]]; then
  452. # For agents: .opencode/agent/core/openagent.md → core
  453. # Check if there's a category subdirectory
  454. local category
  455. category=$(echo "$path" | sed -E 's|.*/agent/([^/]+)/.*|\1|')
  456. # If category is the filename, it's a flat structure (no category)
  457. if [[ "$category" == *.md ]]; then
  458. echo "standard"
  459. else
  460. echo "$category"
  461. fi
  462. else
  463. echo "standard"
  464. fi
  465. }
  466. get_registry_key() {
  467. local type=$1
  468. case "$type" in
  469. config) echo "config" ;;
  470. *) echo "${type}s" ;;
  471. esac
  472. }
  473. scan_for_new_components() {
  474. print_info "Scanning for new components..."
  475. echo ""
  476. # Get all paths from registry
  477. local registry_paths
  478. registry_paths=$(jq -r '.components | to_entries[] | .value[] | .path' "$REGISTRY_FILE" 2>/dev/null | sort -u)
  479. # Scan .opencode directory
  480. local categories=("agent" "command" "tool" "plugin" "context")
  481. for category in "${categories[@]}"; do
  482. local category_dir="$REPO_ROOT/.opencode/$category"
  483. if [ ! -d "$category_dir" ]; then
  484. continue
  485. fi
  486. # Find all .md files recursively (excluding node_modules, tests, docs, templates)
  487. while IFS= read -r file; do
  488. local rel_path="${file#$REPO_ROOT/}"
  489. # Skip symlinks (backward compatibility links)
  490. if [ -L "$file" ]; then
  491. continue
  492. fi
  493. # Skip node_modules, tests, docs, templates
  494. if [[ "$rel_path" == *"/node_modules/"* ]] || \
  495. [[ "$rel_path" == *"/tests/"* ]] || \
  496. [[ "$rel_path" == *"/docs/"* ]] || \
  497. [[ "$rel_path" == *"/template"* ]] || \
  498. [[ "$rel_path" == *"README.md" ]] || \
  499. [[ "$rel_path" == *"index.md" ]]; then
  500. continue
  501. fi
  502. # Check if this path is in registry
  503. if ! echo "$registry_paths" | grep -q "^${rel_path}$"; then
  504. # Extract metadata
  505. local metadata
  506. metadata=$(extract_metadata_from_file "$file")
  507. IFS='|' read -r id name description tags dependencies <<< "$metadata"
  508. # Detect component type
  509. local comp_type
  510. comp_type=$(detect_component_type "$rel_path")
  511. # Extract category from path
  512. local comp_category
  513. comp_category=$(extract_category_from_path "$rel_path")
  514. if [ "$comp_type" != "unknown" ]; then
  515. NEW_COMPONENTS+=("${comp_type}|${id}|${name}|${description}|${rel_path}|${comp_category}|${tags}|${dependencies}")
  516. print_warning "New ${comp_type}: ${name} (${id})"
  517. echo " Path: ${rel_path}"
  518. echo " Category: ${comp_category}"
  519. [ -n "$description" ] && echo " Description: ${description}"
  520. [ -n "$tags" ] && echo " Tags: ${tags}"
  521. [ -n "$dependencies" ] && echo " Dependencies: ${dependencies}"
  522. # Check dependencies
  523. if [ -n "$dependencies" ]; then
  524. check_dependencies "$dependencies" "$name"
  525. fi
  526. echo ""
  527. fi
  528. fi
  529. done < <(find "$category_dir" -type f -name "*.md" 2>/dev/null)
  530. done
  531. }
  532. check_dependencies() {
  533. local deps_str=$1
  534. # local component_name=$2 # Unused
  535. if [ -z "$deps_str" ]; then
  536. return 0
  537. fi
  538. # Split dependencies by comma
  539. IFS=',' read -ra deps <<< "$deps_str"
  540. for dep in "${deps[@]}"; do
  541. dep=$(echo "$dep" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
  542. if [ -z "$dep" ]; then
  543. continue
  544. fi
  545. # Parse dependency: type:id
  546. if [[ ! "$dep" =~ ^([^:]+):(.+)$ ]]; then
  547. echo " ⚠ Invalid dependency format: ${dep} (expected type:id)"
  548. continue
  549. fi
  550. local dep_type="${BASH_REMATCH[1]}"
  551. local dep_id="${BASH_REMATCH[2]}"
  552. # Map to registry category
  553. local category=""
  554. case "$dep_type" in
  555. agent) category="agents" ;;
  556. subagent) category="subagents" ;;
  557. command) category="commands" ;;
  558. tool) category="tools" ;;
  559. plugin) category="plugins" ;;
  560. context) category="contexts" ;;
  561. config) category="config" ;;
  562. *)
  563. echo " ⚠ Unknown dependency type: ${dep}"
  564. continue
  565. ;;
  566. esac
  567. # Check if exists in registry
  568. local exists
  569. exists=$(jq -r ".components.${category}[]? | select(.id == \"${dep_id}\") | .id" "$REGISTRY_FILE" 2>/dev/null)
  570. if [ -z "$exists" ]; then
  571. echo " ⚠ Dependency not found in registry: ${dep}"
  572. fi
  573. done
  574. }
  575. add_component_to_registry() {
  576. local comp_type=$1
  577. local id=$2
  578. local name=$3
  579. local description=$4
  580. local path=$5
  581. local comp_category=${6:-"standard"}
  582. local tags_str=${7:-""}
  583. local deps_str=${8:-""}
  584. # Default description if empty
  585. if [ -z "$description" ]; then
  586. description="Component: ${name}"
  587. fi
  588. # Escape quotes and special characters in description
  589. description=$(echo "$description" | sed 's/"/\\"/g' | sed "s/'/\\'/g")
  590. # Convert comma-separated strings to JSON arrays
  591. local tags_json="[]"
  592. if [ -n "$tags_str" ]; then
  593. tags_json=$(echo "$tags_str" | awk -F',' '{printf "["; for(i=1;i<=NF;i++) {gsub(/^[ \t]+|[ \t]+$/, "", $i); printf "\"%s\"", $i; if(i<NF) printf ","}; printf "]"}')
  594. fi
  595. local deps_json="[]"
  596. if [ -n "$deps_str" ]; then
  597. deps_json=$(echo "$deps_str" | awk -F',' '{printf "["; for(i=1;i<=NF;i++) {gsub(/^[ \t]+|[ \t]+$/, "", $i); printf "\"%s\"", $i; if(i<NF) printf ","}; printf "]"}')
  598. fi
  599. # Get registry key (agents, subagents, commands, etc.)
  600. local registry_key
  601. registry_key=$(get_registry_key "$comp_type")
  602. # Use jq to properly construct JSON (avoids escaping issues)
  603. local temp_file="${REGISTRY_FILE}.tmp"
  604. jq --arg id "$id" \
  605. --arg name "$name" \
  606. --arg type "$comp_type" \
  607. --arg path "$path" \
  608. --arg desc "$description" \
  609. --arg cat "$comp_category" \
  610. --argjson tags "$tags_json" \
  611. --argjson deps "$deps_json" \
  612. ".components.${registry_key} += [{
  613. \"id\": \$id,
  614. \"name\": \$name,
  615. \"type\": \$type,
  616. \"path\": \$path,
  617. \"description\": \$desc,
  618. \"tags\": \$tags,
  619. \"dependencies\": \$deps,
  620. \"category\": \$cat
  621. }]" "$REGISTRY_FILE" > "$temp_file"
  622. if [ $? -eq 0 ]; then
  623. mv "$temp_file" "$REGISTRY_FILE"
  624. print_success "Added ${comp_type}: ${name} (category: ${comp_category})"
  625. else
  626. print_error "Failed to add ${comp_type}: ${name}"
  627. rm -f "$temp_file"
  628. return 1
  629. fi
  630. }
  631. #############################################################################
  632. # Main
  633. #############################################################################
  634. main() {
  635. # Parse arguments
  636. while [ $# -gt 0 ]; do
  637. case "$1" in
  638. -a|--auto-add)
  639. AUTO_ADD=true
  640. shift
  641. ;;
  642. -d|--dry-run)
  643. DRY_RUN=true
  644. shift
  645. ;;
  646. -s|--skip-validation)
  647. VALIDATE_EXISTING=false
  648. shift
  649. ;;
  650. -n|--no-security)
  651. SECURITY_CHECK=false
  652. shift
  653. ;;
  654. -h|--help)
  655. usage
  656. ;;
  657. *)
  658. echo "Unknown option: $1"
  659. usage
  660. ;;
  661. esac
  662. done
  663. print_header
  664. # Check dependencies
  665. if ! command -v jq &> /dev/null; then
  666. print_error "jq is required but not installed"
  667. exit 1
  668. fi
  669. # Validate registry file
  670. if [ ! -f "$REGISTRY_FILE" ]; then
  671. print_error "Registry file not found: $REGISTRY_FILE"
  672. exit 1
  673. fi
  674. if ! jq empty "$REGISTRY_FILE" 2>/dev/null; then
  675. print_error "Registry file is not valid JSON"
  676. exit 1
  677. fi
  678. # Run security checks
  679. run_security_checks
  680. # Validate existing entries (fixes and removals)
  681. validate_existing_entries
  682. # Scan for new components
  683. scan_for_new_components
  684. # Summary
  685. echo ""
  686. echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
  687. echo -e "${BOLD}Summary${NC}"
  688. echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
  689. echo ""
  690. # Display counts
  691. echo -e "Security Issues: ${MAGENTA}${TOTAL_SECURITY_ISSUES}${NC}"
  692. echo -e "Fixed Paths: ${GREEN}${TOTAL_FIXED}${NC}"
  693. echo -e "Removed Components: ${RED}${TOTAL_REMOVED}${NC}"
  694. echo -e "New Components: ${YELLOW}${#NEW_COMPONENTS[@]}${NC}"
  695. echo ""
  696. # Show pending fixes if in dry-run mode
  697. if [ ${#FIXED_COMPONENTS[@]} -gt 0 ] && [ "$DRY_RUN" = true ]; then
  698. echo -e "${BOLD}Pending Path Fixes:${NC}"
  699. for entry in "${FIXED_COMPONENTS[@]}"; do
  700. IFS='|' read -r comp_type index id name old_path new_path <<< "$entry"
  701. echo " • ${name}: ${old_path} → ${new_path}"
  702. done
  703. echo ""
  704. fi
  705. # Show pending removals if in dry-run mode
  706. if [ ${#REMOVED_COMPONENTS[@]} -gt 0 ] && [ "$DRY_RUN" = true ]; then
  707. echo -e "${BOLD}Pending Removals:${NC}"
  708. for entry in "${REMOVED_COMPONENTS[@]}"; do
  709. IFS='|' read -r comp_type id name path <<< "$entry"
  710. echo " • ${name} (${path})"
  711. done
  712. echo ""
  713. fi
  714. # Check if everything is up to date
  715. if [ ${#NEW_COMPONENTS[@]} -eq 0 ] && \
  716. [ ${#FIXED_COMPONENTS[@]} -eq 0 ] && \
  717. [ ${#REMOVED_COMPONENTS[@]} -eq 0 ] && \
  718. [ $TOTAL_FIXED -eq 0 ] && \
  719. [ $TOTAL_REMOVED -eq 0 ]; then
  720. print_success "Registry is up to date!"
  721. if [ $TOTAL_SECURITY_ISSUES -gt 0 ]; then
  722. echo ""
  723. print_warning "Please review and fix the ${TOTAL_SECURITY_ISSUES} security issue(s) found"
  724. fi
  725. exit 0
  726. fi
  727. # Add components if auto-add is enabled
  728. if [ "$AUTO_ADD" = true ] && [ "$DRY_RUN" = false ]; then
  729. if [ ${#NEW_COMPONENTS[@]} -gt 0 ]; then
  730. print_info "Adding new components to registry..."
  731. echo ""
  732. local added=0
  733. for entry in "${NEW_COMPONENTS[@]}"; do
  734. IFS='|' read -r comp_type id name description path comp_category tags dependencies <<< "$entry"
  735. if add_component_to_registry "$comp_type" "$id" "$name" "$description" "$path" "$comp_category" "$tags" "$dependencies"; then
  736. added=$((added + 1))
  737. fi
  738. done
  739. echo ""
  740. print_success "Added ${added} component(s) to registry"
  741. fi
  742. # Update timestamp
  743. jq '.metadata.lastUpdated = (now | strftime("%Y-%m-%d"))' "$REGISTRY_FILE" > "${REGISTRY_FILE}.tmp"
  744. mv "${REGISTRY_FILE}.tmp" "$REGISTRY_FILE"
  745. elif [ "$DRY_RUN" = true ]; then
  746. print_info "Dry run mode - no changes made to registry"
  747. echo ""
  748. echo "Run without --dry-run to apply these changes"
  749. else
  750. print_info "Run with --auto-add to apply these changes to registry"
  751. echo ""
  752. echo "Or manually update registry.json"
  753. fi
  754. # Final security warning
  755. if [ $TOTAL_SECURITY_ISSUES -gt 0 ]; then
  756. echo ""
  757. print_warning "⚠️ ${TOTAL_SECURITY_ISSUES} security issue(s) require attention"
  758. fi
  759. exit 0
  760. }
  761. main "$@"