| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543 |
- #!/usr/bin/env bun
- /**
- * Registry Validator Script (TypeScript/Bun version)
- * 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
- */
- import { existsSync, readFileSync, readdirSync } from 'fs';
- import { join, dirname } from 'path';
- import { fileURLToPath } from 'url';
- import { globSync } from 'glob';
- // Colors
- const colors = {
- red: '\x1b[0;31m',
- green: '\x1b[0;32m',
- yellow: '\x1b[1;33m',
- blue: '\x1b[0;34m',
- cyan: '\x1b[0;36m',
- bold: '\x1b[1m',
- reset: '\x1b[0m',
- };
- // Configuration
- const REGISTRY_FILE = 'registry.json';
- const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '../..');
- // Counters
- let TOTAL_PATHS = 0;
- let VALID_PATHS = 0;
- let MISSING_PATHS = 0;
- let ORPHANED_FILES = 0;
- let MISSING_DEPENDENCIES = 0;
- // Arrays to store results
- const MISSING_FILES: string[] = [];
- const ORPHANED_COMPONENTS: string[] = [];
- const MISSING_DEPS: string[] = [];
- // CLI flags
- let VERBOSE = false;
- let FIX_MODE = false;
- // Types
- interface Component {
- id: string;
- name: string;
- type: string;
- path: string;
- dependencies?: string[];
- [key: string]: any;
- }
- interface Registry {
- version: string;
- schema_version: string;
- repository: string;
- categories: Record<string, string>;
- components: {
- agents?: Component[];
- subagents?: Component[];
- commands?: Component[];
- tools?: Component[];
- plugins?: Component[];
- contexts?: Component[];
- config?: Component[];
- skills?: Component[];
- };
- }
- // Utility Functions
- function printHeader(): void {
- console.log(`${colors.cyan}${colors.bold}`);
- console.log('╔════════════════════════════════════════════════════════════════╗');
- console.log('║ ║');
- console.log('║ Registry Validator v2.0.0 (TypeScript) ║');
- console.log('║ ║');
- console.log('╚════════════════════════════════════════════════════════════════╝');
- console.log(`${colors.reset}`);
- }
- function printSuccess(msg: string): void {
- console.log(`${colors.green}✓${colors.reset} ${msg}`);
- }
- function printError(msg: string): void {
- console.log(`${colors.red}✗${colors.reset} ${msg}`);
- }
- function printWarning(msg: string): void {
- console.log(`${colors.yellow}⚠${colors.reset} ${msg}`);
- }
- function printInfo(msg: string): void {
- console.log(`${colors.blue}ℹ${colors.reset} ${msg}`);
- }
- function usage(): void {
- console.log('Usage: bun run scripts/registry/validate-registry.ts [OPTIONS]');
- console.log('');
- console.log('Options:');
- console.log(' -v, --verbose Show detailed validation output');
- console.log(' -f, --fix Suggest fixes for missing files');
- console.log(' -h, --help Show this help message');
- console.log('');
- console.log('Exit codes:');
- console.log(' 0 = All paths valid');
- console.log(' 1 = Missing files found');
- console.log(' 2 = Registry parse error or missing dependencies');
- process.exit(0);
- }
- // Registry Validation
- function validateRegistryFile(): Registry {
- const registryPath = join(REPO_ROOT, REGISTRY_FILE);
-
- if (!existsSync(registryPath)) {
- printError(`Registry file not found: ${REGISTRY_FILE}`);
- process.exit(2);
- }
-
- try {
- const content = readFileSync(registryPath, 'utf-8');
- const registry = JSON.parse(content) as Registry;
- printSuccess('Registry file is valid JSON');
- return registry;
- } catch (error) {
- printError('Registry file is not valid JSON');
- console.error(error);
- process.exit(2);
- }
- }
- function validateComponentPaths(
- category: keyof Registry['components'],
- categoryDisplay: string,
- registry: Registry
- ): void {
- console.error(`Checking ${categoryDisplay}...`);
-
- const components = registry.components[category];
-
- if (!components || components.length === 0) {
- console.error(`No ${categoryDisplay} found`);
- return;
- }
-
- for (const component of components) {
- const { id, path, name } = component;
-
- TOTAL_PATHS++;
-
- const fullPath = join(REPO_ROOT, path);
-
- if (existsSync(fullPath)) {
- VALID_PATHS++;
- if (VERBOSE) {
- printSuccess(`${categoryDisplay}: ${name} (${id})`);
- }
- } else {
- MISSING_PATHS++;
- MISSING_FILES.push(`${category}:${id}|${name}|${path}`);
- printError(`${categoryDisplay}: ${name} (${id}) - File not found: ${path}`);
-
- if (FIX_MODE) {
- suggestFix(path, id);
- }
- }
- }
- }
- function suggestFix(missingPath: string, componentId: string): void {
- const dir = dirname(missingPath);
- const baseDir = dir.split('/').slice(0, 3).join('/');
- const searchPath = join(REPO_ROOT, baseDir);
-
- try {
- // Search for markdown files matching the component ID
- const pattern = join(searchPath, '**', '*.md');
- const files = globSync(pattern, { nodir: true });
-
- const matches = files.filter(file =>
- file.toLowerCase().includes(componentId.toLowerCase())
- );
-
- if (matches.length > 0) {
- console.log(` ${colors.yellow}→ Possible matches:${colors.reset}`);
- matches.forEach(file => {
- const relPath = file.replace(REPO_ROOT + '/', '');
- console.log(` ${colors.cyan}${relPath}${colors.reset}`);
- });
- } else {
- console.log(` ${colors.yellow}→ No similar files found in ${baseDir}${colors.reset}`);
- }
- } catch (error) {
- console.log(` ${colors.yellow}→ Search in: ${baseDir}${colors.reset}`);
- console.log(` ${colors.yellow}→ Looking for files matching: ${componentId}${colors.reset}`);
- }
- }
- function checkDependencyExists(dep: string, registry: Registry): string {
- // Parse dependency format: type:id
- const match = dep.match(/^([^:]+):(.+)$/);
-
- if (!match) {
- return 'invalid_format';
- }
-
- const [, depType, depId] = match;
-
- // Map dependency type to registry category
- const categoryMap: Record<string, keyof Registry['components']> = {
- agent: 'agents',
- subagent: 'subagents',
- command: 'commands',
- tool: 'tools',
- plugin: 'plugins',
- context: 'contexts',
- config: 'config',
- skill: 'skills',
- };
-
- const registryCategory = categoryMap[depType];
-
- if (!registryCategory) {
- return 'unknown_type';
- }
-
- const components = registry.components[registryCategory];
-
- if (!components) {
- return 'not_found';
- }
-
- // Check if component exists in registry - exact ID match
- const exists = components.find((c) => c.id === depId);
-
- if (exists) {
- return 'found';
- }
-
- // For context dependencies, also try path-based lookup
- if (depType === 'context') {
- // Check for wildcard pattern (e.g., context:core/context-system/*)
- if (depId.includes('*')) {
- const prefix = depId.split('*')[0];
- const matches = components.find((c) =>
- c.path.startsWith(`.opencode/context/${prefix}`)
- );
-
- if (matches) {
- return 'found';
- }
- } else {
- // Try exact path match
- const contextPath = `.opencode/context/${depId}.md`;
- const existsByPath = components.find((c) => c.path === contextPath);
-
- if (existsByPath) {
- return 'found';
- }
- }
- }
-
- return 'not_found';
- }
- function validateComponentDependencies(registry: Registry): void {
- console.log('');
- printInfo('Validating component dependencies...');
- console.log('');
-
- const componentTypes = Object.keys(registry.components) as Array<
- keyof Registry['components']
- >;
-
- for (const compType of componentTypes) {
- const components = registry.components[compType];
-
- if (!components || components.length === 0) {
- continue;
- }
-
- for (const component of components) {
- const { id, name, dependencies } = component;
-
- if (!dependencies || dependencies.length === 0) {
- continue;
- }
-
- for (const dep of dependencies) {
- if (!dep) {
- continue;
- }
-
- const result = checkDependencyExists(dep, registry);
-
- switch (result) {
- case 'found':
- if (VERBOSE) {
- printSuccess(`Dependency OK: ${name} → ${dep}`);
- }
- break;
- case 'not_found':
- MISSING_DEPENDENCIES++;
- MISSING_DEPS.push(`${compType}|${id}|${name}|${dep}`);
- printError(
- `Missing dependency: ${name} (${compType.replace(/s$/, '')}) depends on "${dep}" (not found in registry)`
- );
- break;
- case 'invalid_format':
- MISSING_DEPENDENCIES++;
- MISSING_DEPS.push(`${compType}|${id}|${name}|${dep}`);
- printError(
- `Invalid dependency format: ${name} (${compType.replace(/s$/, '')}) has invalid dependency "${dep}" (expected format: type:id)`
- );
- break;
- case 'unknown_type':
- MISSING_DEPENDENCIES++;
- MISSING_DEPS.push(`${compType}|${id}|${name}|${dep}`);
- printError(
- `Unknown dependency type: ${name} (${compType.replace(/s$/, '')}) has unknown dependency type in "${dep}"`
- );
- break;
- }
- }
- }
- }
- }
- function scanDirectory(dir: string, registryPaths: Set<string>): void {
- if (!existsSync(dir)) {
- return;
- }
- const entries = readdirSync(dir, { withFileTypes: true });
-
- for (const entry of entries) {
- const fullPath = join(dir, entry.name);
- const relPath = fullPath.replace(REPO_ROOT + '/', '');
-
- if (entry.isDirectory()) {
- // Skip node_modules
- if (entry.name === 'node_modules') continue;
- // Skip plugin internal directories
- if (relPath.includes('/plugin/docs/') || relPath.includes('/plugin/tests/')) continue;
- // Skip scripts directories
- if (relPath.includes('/scripts/')) continue;
-
- scanDirectory(fullPath, registryPaths);
- } else if (entry.isFile()) {
- // Only check .md and .ts files
- if (!entry.name.endsWith('.md') && !entry.name.endsWith('.ts')) continue;
-
- // Skip exclusions
- if (entry.name === 'README.md') continue;
- if (entry.name.endsWith('-template.md')) continue;
- if (relPath.endsWith('/tool/index.ts')) continue;
- if (relPath.endsWith('/tool/template/index.ts')) continue;
- if (relPath.endsWith('/plugin/agent-validator.ts')) continue;
-
- // Skip skill support files (only SKILL.md needs to be in registry, all other files are copied with the skill)
- if (relPath.includes('/skills/') && !entry.name.match(/^SKILL\.md$/i)) continue;
-
- // Check if in registry
- if (!registryPaths.has(relPath)) {
- ORPHANED_FILES++;
- ORPHANED_COMPONENTS.push(relPath);
- if (VERBOSE) {
- printWarning(`Orphaned file (not in registry): ${relPath}`);
- }
- }
- }
- }
- }
- function scanForOrphanedFiles(registry: Registry): void {
- if (!VERBOSE) return;
-
- console.log('');
- console.log(`${colors.bold}Scanning for orphaned files...${colors.reset}`);
-
- // Get all paths from registry
- const registryPaths = new Set<string>();
- for (const category of Object.keys(registry.components)) {
- const components = registry.components[category as keyof Registry['components']];
- if (components) {
- components.forEach(c => registryPaths.add(c.path));
- }
- }
-
- const categories = ['agent', 'command', 'tool', 'plugin', 'context', 'skill'];
-
- for (const category of categories) {
- const categoryDir = join(REPO_ROOT, '.opencode', category);
- scanDirectory(categoryDir, registryPaths);
- }
- }
- function printSummary(): boolean {
- console.log('');
- console.log(`${colors.bold}═══════════════════════════════════════════════════════════════${colors.reset}`);
- console.log(`${colors.bold}Validation Summary${colors.reset}`);
- console.log(`${colors.bold}═══════════════════════════════════════════════════════════════${colors.reset}`);
- console.log('');
- console.log(`Total paths checked: ${colors.cyan}${TOTAL_PATHS}${colors.reset}`);
- console.log(`Valid paths: ${colors.green}${VALID_PATHS}${colors.reset}`);
- console.log(`Missing paths: ${colors.red}${MISSING_PATHS}${colors.reset}`);
- console.log(`Missing dependencies: ${colors.red}${MISSING_DEPENDENCIES}${colors.reset}`);
-
- if (VERBOSE) {
- console.log(`Orphaned files: ${colors.yellow}${ORPHANED_FILES}${colors.reset}`);
- }
-
- console.log('');
-
- let hasErrors = false;
-
- // Check for missing paths
- if (MISSING_PATHS > 0) {
- hasErrors = true;
- printError(`Found ${MISSING_PATHS} missing file(s)`);
- console.log('');
- console.log('Missing files:');
- for (const entry of MISSING_FILES) {
- const [catId, name, path] = entry.split('|');
- console.log(` - ${path} (${catId})`);
- }
- console.log('');
-
- if (!FIX_MODE) {
- printInfo('Run with --fix flag to see suggested fixes');
- console.log('');
- }
- }
-
- // Check for missing dependencies
- if (MISSING_DEPENDENCIES > 0) {
- hasErrors = true;
- printError(`Found ${MISSING_DEPENDENCIES} missing or invalid dependencies`);
- console.log('');
- console.log('Missing dependencies:');
- for (const entry of MISSING_DEPS) {
- const [compType, id, name, dep] = entry.split('|');
- console.log(` - ${name} (${compType.replace(/s$/, '')}) → ${dep}`);
- }
- console.log('');
- printInfo('Fix by either:');
- console.log(' 1. Adding the missing component to the registry');
- console.log(' 2. Removing the dependency from the component\'s frontmatter');
- console.log('');
- }
-
- // Success case
- if (!hasErrors) {
- printSuccess('All registry paths are valid!');
- printSuccess('All component dependencies are valid!');
-
- if (ORPHANED_FILES > 0 && VERBOSE) {
- console.log('');
- printWarning(`Found ${ORPHANED_FILES} orphaned file(s) not in registry`);
- console.log('');
- console.log('Orphaned files:');
- for (const file of ORPHANED_COMPONENTS) {
- console.log(` - ${file}`);
- }
- console.log('');
- console.log('Consider adding these to registry.json or removing them.');
- }
-
- return true;
- } else {
- console.log('Please fix these issues before proceeding.');
- return false;
- }
- }
- // Main
- function main(): void {
- // Parse arguments
- const args = process.argv.slice(2);
-
- for (const arg of args) {
- switch (arg) {
- case '-v':
- case '--verbose':
- VERBOSE = true;
- break;
- case '-f':
- case '--fix':
- FIX_MODE = true;
- VERBOSE = true;
- break;
- case '-h':
- case '--help':
- usage();
- break;
- default:
- console.log(`Unknown option: ${arg}`);
- usage();
- }
- }
-
- printHeader();
-
- // Validate registry file
- const registry = validateRegistryFile();
-
- console.log('');
- printInfo('Validating component paths...');
- console.log('');
-
- // Validate each category
- validateComponentPaths('agents', 'Agents', registry);
- validateComponentPaths('subagents', 'Subagents', registry);
- validateComponentPaths('commands', 'Commands', registry);
- validateComponentPaths('tools', 'Tools', registry);
- validateComponentPaths('plugins', 'Plugins', registry);
- validateComponentPaths('contexts', 'Contexts', registry);
- validateComponentPaths('config', 'Config', registry);
- validateComponentPaths('skills', 'Skills', registry);
-
- // Validate component dependencies
- validateComponentDependencies(registry);
-
- // Scan for orphaned files if verbose
- if (VERBOSE) {
- scanForOrphanedFiles(registry);
- }
-
- // Print summary and exit with appropriate code
- if (printSummary()) {
- process.exit(0);
- } else {
- process.exit(1);
- }
- }
- main();
|