-
Notifications
You must be signed in to change notification settings - Fork 357
Expand file tree
/
Copy pathvalidate-registry.sh
More file actions
executable file
·560 lines (472 loc) · 17.8 KB
/
Copy pathvalidate-registry.sh
File metadata and controls
executable file
·560 lines (472 loc) · 17.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
#!/usr/bin/env bash
#############################################################################
# Registry Validator Script
# Validates that all paths in registry.json point to actual files
# Exit codes:
# 0 = All paths valid
# 1 = Missing files found
# 2 = Registry parse error or missing dependencies
#############################################################################
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
# Configuration
REGISTRY_FILE="registry.json"
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
VERBOSE=false
FIX_MODE=false
# Counters
TOTAL_PATHS=0
VALID_PATHS=0
MISSING_PATHS=0
ORPHANED_FILES=0
MISSING_DEPENDENCIES=0
# Arrays to store results
declare -a MISSING_FILES
declare -a ORPHANED_COMPONENTS
declare -a MISSING_DEPS
#############################################################################
# Utility Functions
#############################################################################
print_header() {
echo -e "${CYAN}${BOLD}"
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ ║"
echo "║ Registry Validator v1.0.0 ║"
echo "║ ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo -e "${NC}"
}
print_success() {
echo -e "${GREEN}✓${NC} $1"
}
print_error() {
echo -e "${RED}✗${NC} $1"
}
print_warning() {
echo -e "${YELLOW}⚠${NC} $1"
}
print_info() {
echo -e "${BLUE}ℹ${NC} $1"
}
usage() {
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " -v, --verbose Show detailed validation output"
echo " -f, --fix Suggest fixes for missing files"
echo " -h, --help Show this help message"
echo ""
echo "Exit codes:"
echo " 0 = All paths valid"
echo " 1 = Missing files found"
echo " 2 = Registry parse error or missing dependencies"
exit 0
}
#############################################################################
# Dependency Checks
#############################################################################
check_dependencies() {
local missing_deps=()
if ! command -v jq &> /dev/null; then
missing_deps+=("jq")
fi
if [ ${#missing_deps[@]} -ne 0 ]; then
print_error "Missing required dependencies: ${missing_deps[*]}"
echo ""
echo "Please install them:"
echo " macOS: brew install ${missing_deps[*]}"
echo " Ubuntu: sudo apt-get install ${missing_deps[*]}"
echo " Fedora: sudo dnf install ${missing_deps[*]}"
exit 2
fi
}
#############################################################################
# Registry Validation
#############################################################################
validate_registry_file() {
if [ ! -f "$REGISTRY_FILE" ]; then
print_error "Registry file not found: $REGISTRY_FILE"
exit 2
fi
if ! jq empty "$REGISTRY_FILE" 2>/dev/null; then
print_error "Registry file is not valid JSON"
exit 2
fi
print_success "Registry file is valid JSON"
}
validate_component_paths() {
local category=$1
local category_display=$2
echo "Checking ${category_display}..." >&2
# Get all components in this category
local components
components=$(jq -r ".components.${category}[]? | @json" "$REGISTRY_FILE" 2>/dev/null)
if [ -z "$components" ]; then
echo "No ${category_display} found" >&2
return
fi
while IFS= read -r component; do
local id
id=$(echo "$component" | jq -r '.id')
local path
path=$(echo "$component" | jq -r '.path')
local name
name=$(echo "$component" | jq -r '.name')
TOTAL_PATHS=$((TOTAL_PATHS + 1))
# Check if file exists
if [ -f "$REPO_ROOT/$path" ]; then
VALID_PATHS=$((VALID_PATHS + 1))
[ "$VERBOSE" = true ] && print_success "${category_display}: ${name} (${id})"
else
MISSING_PATHS=$((MISSING_PATHS + 1))
MISSING_FILES+=("${category}:${id}|${name}|${path}")
print_error "${category_display}: ${name} (${id}) - File not found: ${path}"
# Try to find similar files if in fix mode
if [ "$FIX_MODE" = true ]; then
suggest_fix "$path" "$id"
fi
fi
done <<< "$components"
return 0
}
suggest_fix() {
local missing_path=$1
local component_id=$2
# Extract directory and filename
local dir=""
local base_dir=""
dir=$(dirname "$missing_path")
base_dir=$(echo "$dir" | cut -d'/' -f1-3) # e.g., .opencode/command
# Look for similar files in the expected directory and subdirectories
local similar_files
similar_files=$(find "$REPO_ROOT/$base_dir" -type f -name "*.md" 2>/dev/null | grep -i "$component_id" || true)
if [ -n "$similar_files" ]; then
echo -e " ${YELLOW}→ Possible matches:${NC}"
while IFS= read -r file; do
local rel_path="${file#$REPO_ROOT/}"
echo -e " ${CYAN}${rel_path}${NC}"
done <<< "$similar_files"
fi
}
scan_for_orphaned_files() {
[ "$VERBOSE" = true ] && echo -e "\n${BOLD}Scanning for orphaned files...${NC}"
# Get all paths from registry
local registry_paths
registry_paths=$(jq -r '.components | to_entries[] | .value[] | .path' "$REGISTRY_FILE" 2>/dev/null | sort -u)
# Scan .opencode directory for markdown files
local categories=("agent" "command" "tool" "plugin" "context")
for category in "${categories[@]}"; do
local category_dir="$REPO_ROOT/.opencode/$category"
if [ ! -d "$category_dir" ]; then
continue
fi
# Find all .md and .ts files (excluding node_modules)
while IFS= read -r file; do
local rel_path="${file#$REPO_ROOT/}"
# Skip node_modules
if [[ "$rel_path" == *"/node_modules/"* ]]; then
continue
fi
# Skip README files
if [[ "$rel_path" == *"README.md" ]]; then
continue
fi
# Skip template files
if [[ "$rel_path" == *"-template.md" ]]; then
continue
fi
# Skip tool/plugin TypeScript files
if [[ "$rel_path" == *"/tool/index.ts" ]] || [[ "$rel_path" == *"/tool/template/index.ts" ]]; then
continue
fi
if [[ "$rel_path" == *"/plugin/agent-validator.ts" ]]; then
continue
fi
# Skip plugin internal docs and tests
if [[ "$rel_path" == *"/plugin/docs/"* ]] || [[ "$rel_path" == *"/plugin/tests/"* ]]; then
continue
fi
# Skip scripts directories (internal CLI tools, not registry components)
if [[ "$rel_path" == *"/scripts/"* ]]; then
continue
fi
# Check if this path is in registry
# shellcheck disable=SC2143
if ! echo "$registry_paths" | grep -q "^${rel_path}$"; then
ORPHANED_FILES=$((ORPHANED_FILES + 1))
ORPHANED_COMPONENTS+=("$rel_path")
[ "$VERBOSE" = true ] && print_warning "Orphaned file (not in registry): ${rel_path}"
fi
done < <(find "$category_dir" -type f \( -name "*.md" -o -name "*.ts" \) 2>/dev/null)
done
}
#############################################################################
# Dependency Validation
#############################################################################
check_dependency_exists() {
local dep=$1
# Parse dependency format: type:id
if [[ ! "$dep" =~ ^([^:]+):(.+)$ ]]; then
echo "invalid_format"
return 1
fi
local dep_type="${BASH_REMATCH[1]}"
local dep_id="${BASH_REMATCH[2]}"
# Map dependency type to registry category
local registry_category=""
case "$dep_type" in
agent)
registry_category="agents"
;;
subagent)
registry_category="subagents"
;;
command)
registry_category="commands"
;;
tool)
registry_category="tools"
;;
plugin)
registry_category="plugins"
;;
skill)
registry_category="skills"
;;
context)
registry_category="contexts"
;;
config)
registry_category="config"
;;
*)
echo "unknown_type"
return 1
;;
esac
# Check if component exists in registry
# First try exact ID match
local exists
exists=$(jq -r ".components.${registry_category}[]? | select(.id == \"${dep_id}\") | .id" "$REGISTRY_FILE" 2>/dev/null)
if [ -n "$exists" ]; then
echo "found"
return 0
fi
# For context dependencies, also try path-based lookup
# Format: context:core/standards/code -> .opencode/context/core/standards/code.md
if [ "$dep_type" = "context" ]; then
# Check for wildcard pattern (e.g., context:core/context-system/*)
if [[ "$dep_id" == *"*" ]]; then
# Extract prefix before wildcard
local prefix="${dep_id%%\**}"
# Check if any context files match the prefix
local matches
matches=$(jq -r ".components.${registry_category}[]? | select(.path | startswith(\".opencode/context/${prefix}\")) | .id" "$REGISTRY_FILE" 2>/dev/null | head -1)
if [ -n "$matches" ]; then
echo "found"
return 0
fi
else
# Try exact path match
local context_path=".opencode/context/${dep_id}.md"
local exists_by_path
exists_by_path=$(jq -r ".components.${registry_category}[]? | select(.path == \"${context_path}\") | .id" "$REGISTRY_FILE" 2>/dev/null)
if [ -n "$exists_by_path" ]; then
echo "found"
return 0
fi
fi
fi
echo "not_found"
return 1
}
validate_component_dependencies() {
echo ""
print_info "Validating component dependencies..."
echo ""
# Get all component types
local component_types
component_types=$(jq -r '.components | keys[]' "$REGISTRY_FILE" 2>/dev/null)
while IFS= read -r comp_type; do
# Get all components of this type
local components
components=$(jq -r ".components.${comp_type}[]? | @json" "$REGISTRY_FILE" 2>/dev/null)
if [ -z "$components" ]; then
continue
fi
while IFS= read -r component; do
local id=""
local path=""
local name=""
id=$(echo "$component" | jq -r '.id')
path=$(echo "$component" | jq -r '.path')
name=$(echo "$component" | jq -r '.name')
local dependencies
dependencies=$(echo "$component" | jq -r '.dependencies[]?' 2>/dev/null)
if [ -z "$dependencies" ]; then
continue
fi
# Check each dependency
while IFS= read -r dep; do
if [ -z "$dep" ]; then
continue
fi
local result
result=$(check_dependency_exists "$dep" || true)
case "$result" in
found)
[ "$VERBOSE" = true ] && print_success "Dependency OK: ${name} → ${dep}"
;;
not_found)
MISSING_DEPENDENCIES=$((MISSING_DEPENDENCIES + 1))
MISSING_DEPS+=("${comp_type}|${id}|${name}|${dep}")
print_error "Missing dependency: ${name} (${comp_type%s}) depends on \"${dep}\" (not found in registry)"
;;
invalid_format)
MISSING_DEPENDENCIES=$((MISSING_DEPENDENCIES + 1))
MISSING_DEPS+=("${comp_type}|${id}|${name}|${dep}")
print_error "Invalid dependency format: ${name} (${comp_type%s}) has invalid dependency \"${dep}\" (expected format: type:id)"
;;
unknown_type)
MISSING_DEPENDENCIES=$((MISSING_DEPENDENCIES + 1))
MISSING_DEPS+=("${comp_type}|${id}|${name}|${dep}")
print_error "Unknown dependency type: ${name} (${comp_type%s}) has unknown dependency type in \"${dep}\""
;;
esac
done <<< "$dependencies"
done <<< "$components"
done <<< "$component_types"
}
#############################################################################
# Reporting
#############################################################################
print_summary() {
echo ""
echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
echo -e "${BOLD}Validation Summary${NC}"
echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
echo ""
echo -e "Total paths checked: ${CYAN}${TOTAL_PATHS}${NC}"
echo -e "Valid paths: ${GREEN}${VALID_PATHS}${NC}"
echo -e "Missing paths: ${RED}${MISSING_PATHS}${NC}"
echo -e "Missing dependencies: ${RED}${MISSING_DEPENDENCIES}${NC}"
if [ "$VERBOSE" = true ]; then
echo -e "Orphaned files: ${YELLOW}${ORPHANED_FILES}${NC}"
fi
echo ""
local has_errors=false
# Check for missing paths
if [ $MISSING_PATHS -gt 0 ]; then
has_errors=true
print_error "Found ${MISSING_PATHS} missing file(s)"
echo ""
echo "Missing files:"
for entry in "${MISSING_FILES[@]}"; do
IFS='|' read -r cat_id name path <<< "$entry"
echo " - ${path} (${cat_id})"
done
echo ""
if [ "$FIX_MODE" = false ]; then
print_info "Run with --fix flag to see suggested fixes"
echo ""
fi
fi
# Check for missing dependencies
if [ $MISSING_DEPENDENCIES -gt 0 ]; then
has_errors=true
print_error "Found ${MISSING_DEPENDENCIES} missing or invalid dependencies"
echo ""
echo "Missing dependencies:"
for entry in "${MISSING_DEPS[@]}"; do
IFS='|' read -r comp_type id name dep <<< "$entry"
echo " - ${name} (${comp_type%s}) → ${dep}"
done
echo ""
print_info "Fix by either:"
echo " 1. Adding the missing component to the registry"
echo " 2. Removing the dependency from the component's frontmatter"
echo ""
fi
# Success case
if [ "$has_errors" = false ]; then
print_success "All registry paths are valid!"
print_success "All component dependencies are valid!"
if [ $ORPHANED_FILES -gt 0 ] && [ "$VERBOSE" = true ]; then
echo ""
print_warning "Found ${ORPHANED_FILES} orphaned file(s) not in registry"
echo ""
echo "Orphaned files:"
for file in "${ORPHANED_COMPONENTS[@]}"; do
echo " - $file"
done
echo ""
echo "Consider adding these to registry.json or removing them."
fi
return 0
else
echo "Please fix these issues before proceeding."
return 1
fi
}
#############################################################################
# Main
#############################################################################
main() {
# Parse arguments
while [ $# -gt 0 ]; do
case "$1" in
-v|--verbose)
VERBOSE=true
shift
;;
-f|--fix)
FIX_MODE=true
VERBOSE=true
shift
;;
-h|--help)
usage
;;
*)
echo "Unknown option: $1"
usage
;;
esac
done
print_header
# Check dependencies
check_dependencies
# Validate registry file
validate_registry_file
echo ""
print_info "Validating component paths..."
echo ""
# Validate each category
validate_component_paths "agents" "Agents"
validate_component_paths "subagents" "Subagents"
validate_component_paths "commands" "Commands"
validate_component_paths "tools" "Tools"
validate_component_paths "plugins" "Plugins"
validate_component_paths "contexts" "Contexts"
validate_component_paths "config" "Config"
# Validate component dependencies
validate_component_dependencies
# Scan for orphaned files if verbose
if [ "$VERBOSE" = true ]; then
scan_for_orphaned_files
fi
# Print summary and exit with appropriate code
if print_summary; then
exit 0
else
exit 1
fi
}
main "$@"