validate-registry.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. #!/usr/bin/env bun
  2. /**
  3. * Registry Validator Script (TypeScript/Bun version)
  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. import { existsSync, readFileSync, readdirSync } from 'fs';
  11. import { join, dirname } from 'path';
  12. import { fileURLToPath } from 'url';
  13. import { globSync } from 'glob';
  14. // Colors
  15. const colors = {
  16. red: '\x1b[0;31m',
  17. green: '\x1b[0;32m',
  18. yellow: '\x1b[1;33m',
  19. blue: '\x1b[0;34m',
  20. cyan: '\x1b[0;36m',
  21. bold: '\x1b[1m',
  22. reset: '\x1b[0m',
  23. };
  24. // Configuration
  25. const REGISTRY_FILE = 'registry.json';
  26. const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '../..');
  27. // Counters
  28. let TOTAL_PATHS = 0;
  29. let VALID_PATHS = 0;
  30. let MISSING_PATHS = 0;
  31. let ORPHANED_FILES = 0;
  32. let MISSING_DEPENDENCIES = 0;
  33. // Arrays to store results
  34. const MISSING_FILES: string[] = [];
  35. const ORPHANED_COMPONENTS: string[] = [];
  36. const MISSING_DEPS: string[] = [];
  37. // CLI flags
  38. let VERBOSE = false;
  39. let FIX_MODE = false;
  40. // Types
  41. interface Component {
  42. id: string;
  43. name: string;
  44. type: string;
  45. path: string;
  46. dependencies?: string[];
  47. [key: string]: any;
  48. }
  49. interface Registry {
  50. version: string;
  51. schema_version: string;
  52. repository: string;
  53. categories: Record<string, string>;
  54. components: {
  55. agents?: Component[];
  56. subagents?: Component[];
  57. commands?: Component[];
  58. tools?: Component[];
  59. plugins?: Component[];
  60. contexts?: Component[];
  61. config?: Component[];
  62. skills?: Component[];
  63. };
  64. }
  65. // Utility Functions
  66. function printHeader(): void {
  67. console.log(`${colors.cyan}${colors.bold}`);
  68. console.log('╔════════════════════════════════════════════════════════════════╗');
  69. console.log('║ ║');
  70. console.log('║ Registry Validator v2.0.0 (TypeScript) ║');
  71. console.log('║ ║');
  72. console.log('╚════════════════════════════════════════════════════════════════╝');
  73. console.log(`${colors.reset}`);
  74. }
  75. function printSuccess(msg: string): void {
  76. console.log(`${colors.green}✓${colors.reset} ${msg}`);
  77. }
  78. function printError(msg: string): void {
  79. console.log(`${colors.red}✗${colors.reset} ${msg}`);
  80. }
  81. function printWarning(msg: string): void {
  82. console.log(`${colors.yellow}⚠${colors.reset} ${msg}`);
  83. }
  84. function printInfo(msg: string): void {
  85. console.log(`${colors.blue}ℹ${colors.reset} ${msg}`);
  86. }
  87. function usage(): void {
  88. console.log('Usage: bun run scripts/registry/validate-registry.ts [OPTIONS]');
  89. console.log('');
  90. console.log('Options:');
  91. console.log(' -v, --verbose Show detailed validation output');
  92. console.log(' -f, --fix Suggest fixes for missing files');
  93. console.log(' -h, --help Show this help message');
  94. console.log('');
  95. console.log('Exit codes:');
  96. console.log(' 0 = All paths valid');
  97. console.log(' 1 = Missing files found');
  98. console.log(' 2 = Registry parse error or missing dependencies');
  99. process.exit(0);
  100. }
  101. // Registry Validation
  102. function validateRegistryFile(): Registry {
  103. const registryPath = join(REPO_ROOT, REGISTRY_FILE);
  104. if (!existsSync(registryPath)) {
  105. printError(`Registry file not found: ${REGISTRY_FILE}`);
  106. process.exit(2);
  107. }
  108. try {
  109. const content = readFileSync(registryPath, 'utf-8');
  110. const registry = JSON.parse(content) as Registry;
  111. printSuccess('Registry file is valid JSON');
  112. return registry;
  113. } catch (error) {
  114. printError('Registry file is not valid JSON');
  115. console.error(error);
  116. process.exit(2);
  117. }
  118. }
  119. function validateComponentPaths(
  120. category: keyof Registry['components'],
  121. categoryDisplay: string,
  122. registry: Registry
  123. ): void {
  124. console.error(`Checking ${categoryDisplay}...`);
  125. const components = registry.components[category];
  126. if (!components || components.length === 0) {
  127. console.error(`No ${categoryDisplay} found`);
  128. return;
  129. }
  130. for (const component of components) {
  131. const { id, path, name } = component;
  132. TOTAL_PATHS++;
  133. const fullPath = join(REPO_ROOT, path);
  134. if (existsSync(fullPath)) {
  135. VALID_PATHS++;
  136. if (VERBOSE) {
  137. printSuccess(`${categoryDisplay}: ${name} (${id})`);
  138. }
  139. } else {
  140. MISSING_PATHS++;
  141. MISSING_FILES.push(`${category}:${id}|${name}|${path}`);
  142. printError(`${categoryDisplay}: ${name} (${id}) - File not found: ${path}`);
  143. if (FIX_MODE) {
  144. suggestFix(path, id);
  145. }
  146. }
  147. }
  148. }
  149. function suggestFix(missingPath: string, componentId: string): void {
  150. const dir = dirname(missingPath);
  151. const baseDir = dir.split('/').slice(0, 3).join('/');
  152. const searchPath = join(REPO_ROOT, baseDir);
  153. try {
  154. // Search for markdown files matching the component ID
  155. const pattern = join(searchPath, '**', '*.md');
  156. const files = globSync(pattern, { nodir: true });
  157. const matches = files.filter(file =>
  158. file.toLowerCase().includes(componentId.toLowerCase())
  159. );
  160. if (matches.length > 0) {
  161. console.log(` ${colors.yellow}→ Possible matches:${colors.reset}`);
  162. matches.forEach(file => {
  163. const relPath = file.replace(REPO_ROOT + '/', '');
  164. console.log(` ${colors.cyan}${relPath}${colors.reset}`);
  165. });
  166. } else {
  167. console.log(` ${colors.yellow}→ No similar files found in ${baseDir}${colors.reset}`);
  168. }
  169. } catch (error) {
  170. console.log(` ${colors.yellow}→ Search in: ${baseDir}${colors.reset}`);
  171. console.log(` ${colors.yellow}→ Looking for files matching: ${componentId}${colors.reset}`);
  172. }
  173. }
  174. function checkDependencyExists(dep: string, registry: Registry): string {
  175. // Parse dependency format: type:id
  176. const match = dep.match(/^([^:]+):(.+)$/);
  177. if (!match) {
  178. return 'invalid_format';
  179. }
  180. const [, depType, depId] = match;
  181. // Map dependency type to registry category
  182. const categoryMap: Record<string, keyof Registry['components']> = {
  183. agent: 'agents',
  184. subagent: 'subagents',
  185. command: 'commands',
  186. tool: 'tools',
  187. plugin: 'plugins',
  188. context: 'contexts',
  189. config: 'config',
  190. skill: 'skills',
  191. };
  192. const registryCategory = categoryMap[depType];
  193. if (!registryCategory) {
  194. return 'unknown_type';
  195. }
  196. const components = registry.components[registryCategory];
  197. if (!components) {
  198. return 'not_found';
  199. }
  200. // Check if component exists in registry - exact ID match
  201. const exists = components.find((c) => c.id === depId);
  202. if (exists) {
  203. return 'found';
  204. }
  205. // For context dependencies, also try path-based lookup
  206. if (depType === 'context') {
  207. // Check for wildcard pattern (e.g., context:core/context-system/*)
  208. if (depId.includes('*')) {
  209. const prefix = depId.split('*')[0];
  210. const matches = components.find((c) =>
  211. c.path.startsWith(`.opencode/context/${prefix}`)
  212. );
  213. if (matches) {
  214. return 'found';
  215. }
  216. } else {
  217. // Try exact path match
  218. const contextPath = `.opencode/context/${depId}.md`;
  219. const existsByPath = components.find((c) => c.path === contextPath);
  220. if (existsByPath) {
  221. return 'found';
  222. }
  223. }
  224. }
  225. return 'not_found';
  226. }
  227. function validateComponentDependencies(registry: Registry): void {
  228. console.log('');
  229. printInfo('Validating component dependencies...');
  230. console.log('');
  231. const componentTypes = Object.keys(registry.components) as Array<
  232. keyof Registry['components']
  233. >;
  234. for (const compType of componentTypes) {
  235. const components = registry.components[compType];
  236. if (!components || components.length === 0) {
  237. continue;
  238. }
  239. for (const component of components) {
  240. const { id, name, dependencies } = component;
  241. if (!dependencies || dependencies.length === 0) {
  242. continue;
  243. }
  244. for (const dep of dependencies) {
  245. if (!dep) {
  246. continue;
  247. }
  248. const result = checkDependencyExists(dep, registry);
  249. switch (result) {
  250. case 'found':
  251. if (VERBOSE) {
  252. printSuccess(`Dependency OK: ${name} → ${dep}`);
  253. }
  254. break;
  255. case 'not_found':
  256. MISSING_DEPENDENCIES++;
  257. MISSING_DEPS.push(`${compType}|${id}|${name}|${dep}`);
  258. printError(
  259. `Missing dependency: ${name} (${compType.replace(/s$/, '')}) depends on "${dep}" (not found in registry)`
  260. );
  261. break;
  262. case 'invalid_format':
  263. MISSING_DEPENDENCIES++;
  264. MISSING_DEPS.push(`${compType}|${id}|${name}|${dep}`);
  265. printError(
  266. `Invalid dependency format: ${name} (${compType.replace(/s$/, '')}) has invalid dependency "${dep}" (expected format: type:id)`
  267. );
  268. break;
  269. case 'unknown_type':
  270. MISSING_DEPENDENCIES++;
  271. MISSING_DEPS.push(`${compType}|${id}|${name}|${dep}`);
  272. printError(
  273. `Unknown dependency type: ${name} (${compType.replace(/s$/, '')}) has unknown dependency type in "${dep}"`
  274. );
  275. break;
  276. }
  277. }
  278. }
  279. }
  280. }
  281. function scanDirectory(dir: string, registryPaths: Set<string>): void {
  282. if (!existsSync(dir)) {
  283. return;
  284. }
  285. const entries = readdirSync(dir, { withFileTypes: true });
  286. for (const entry of entries) {
  287. const fullPath = join(dir, entry.name);
  288. const relPath = fullPath.replace(REPO_ROOT + '/', '');
  289. if (entry.isDirectory()) {
  290. // Skip node_modules
  291. if (entry.name === 'node_modules') continue;
  292. // Skip plugin internal directories
  293. if (relPath.includes('/plugin/docs/') || relPath.includes('/plugin/tests/')) continue;
  294. // Skip scripts directories
  295. if (relPath.includes('/scripts/')) continue;
  296. scanDirectory(fullPath, registryPaths);
  297. } else if (entry.isFile()) {
  298. // Only check .md and .ts files
  299. if (!entry.name.endsWith('.md') && !entry.name.endsWith('.ts')) continue;
  300. // Skip exclusions
  301. if (entry.name === 'README.md') continue;
  302. if (entry.name.endsWith('-template.md')) continue;
  303. if (relPath.endsWith('/tool/index.ts')) continue;
  304. if (relPath.endsWith('/tool/template/index.ts')) continue;
  305. if (relPath.endsWith('/plugin/agent-validator.ts')) continue;
  306. // Skip skill support files (only SKILL.md needs to be in registry, all other files are copied with the skill)
  307. if (relPath.includes('/skills/') && !entry.name.match(/^SKILL\.md$/i)) continue;
  308. // Check if in registry
  309. if (!registryPaths.has(relPath)) {
  310. ORPHANED_FILES++;
  311. ORPHANED_COMPONENTS.push(relPath);
  312. if (VERBOSE) {
  313. printWarning(`Orphaned file (not in registry): ${relPath}`);
  314. }
  315. }
  316. }
  317. }
  318. }
  319. function scanForOrphanedFiles(registry: Registry): void {
  320. if (!VERBOSE) return;
  321. console.log('');
  322. console.log(`${colors.bold}Scanning for orphaned files...${colors.reset}`);
  323. // Get all paths from registry
  324. const registryPaths = new Set<string>();
  325. for (const category of Object.keys(registry.components)) {
  326. const components = registry.components[category as keyof Registry['components']];
  327. if (components) {
  328. components.forEach(c => registryPaths.add(c.path));
  329. }
  330. }
  331. const categories = ['agent', 'command', 'tool', 'plugin', 'context', 'skill'];
  332. for (const category of categories) {
  333. const categoryDir = join(REPO_ROOT, '.opencode', category);
  334. scanDirectory(categoryDir, registryPaths);
  335. }
  336. }
  337. function printSummary(): boolean {
  338. console.log('');
  339. console.log(`${colors.bold}═══════════════════════════════════════════════════════════════${colors.reset}`);
  340. console.log(`${colors.bold}Validation Summary${colors.reset}`);
  341. console.log(`${colors.bold}═══════════════════════════════════════════════════════════════${colors.reset}`);
  342. console.log('');
  343. console.log(`Total paths checked: ${colors.cyan}${TOTAL_PATHS}${colors.reset}`);
  344. console.log(`Valid paths: ${colors.green}${VALID_PATHS}${colors.reset}`);
  345. console.log(`Missing paths: ${colors.red}${MISSING_PATHS}${colors.reset}`);
  346. console.log(`Missing dependencies: ${colors.red}${MISSING_DEPENDENCIES}${colors.reset}`);
  347. if (VERBOSE) {
  348. console.log(`Orphaned files: ${colors.yellow}${ORPHANED_FILES}${colors.reset}`);
  349. }
  350. console.log('');
  351. let hasErrors = false;
  352. // Check for missing paths
  353. if (MISSING_PATHS > 0) {
  354. hasErrors = true;
  355. printError(`Found ${MISSING_PATHS} missing file(s)`);
  356. console.log('');
  357. console.log('Missing files:');
  358. for (const entry of MISSING_FILES) {
  359. const [catId, name, path] = entry.split('|');
  360. console.log(` - ${path} (${catId})`);
  361. }
  362. console.log('');
  363. if (!FIX_MODE) {
  364. printInfo('Run with --fix flag to see suggested fixes');
  365. console.log('');
  366. }
  367. }
  368. // Check for missing dependencies
  369. if (MISSING_DEPENDENCIES > 0) {
  370. hasErrors = true;
  371. printError(`Found ${MISSING_DEPENDENCIES} missing or invalid dependencies`);
  372. console.log('');
  373. console.log('Missing dependencies:');
  374. for (const entry of MISSING_DEPS) {
  375. const [compType, id, name, dep] = entry.split('|');
  376. console.log(` - ${name} (${compType.replace(/s$/, '')}) → ${dep}`);
  377. }
  378. console.log('');
  379. printInfo('Fix by either:');
  380. console.log(' 1. Adding the missing component to the registry');
  381. console.log(' 2. Removing the dependency from the component\'s frontmatter');
  382. console.log('');
  383. }
  384. // Success case
  385. if (!hasErrors) {
  386. printSuccess('All registry paths are valid!');
  387. printSuccess('All component dependencies are valid!');
  388. if (ORPHANED_FILES > 0 && VERBOSE) {
  389. console.log('');
  390. printWarning(`Found ${ORPHANED_FILES} orphaned file(s) not in registry`);
  391. console.log('');
  392. console.log('Orphaned files:');
  393. for (const file of ORPHANED_COMPONENTS) {
  394. console.log(` - ${file}`);
  395. }
  396. console.log('');
  397. console.log('Consider adding these to registry.json or removing them.');
  398. }
  399. return true;
  400. } else {
  401. console.log('Please fix these issues before proceeding.');
  402. return false;
  403. }
  404. }
  405. // Main
  406. function main(): void {
  407. // Parse arguments
  408. const args = process.argv.slice(2);
  409. for (const arg of args) {
  410. switch (arg) {
  411. case '-v':
  412. case '--verbose':
  413. VERBOSE = true;
  414. break;
  415. case '-f':
  416. case '--fix':
  417. FIX_MODE = true;
  418. VERBOSE = true;
  419. break;
  420. case '-h':
  421. case '--help':
  422. usage();
  423. break;
  424. default:
  425. console.log(`Unknown option: ${arg}`);
  426. usage();
  427. }
  428. }
  429. printHeader();
  430. // Validate registry file
  431. const registry = validateRegistryFile();
  432. console.log('');
  433. printInfo('Validating component paths...');
  434. console.log('');
  435. // Validate each category
  436. validateComponentPaths('agents', 'Agents', registry);
  437. validateComponentPaths('subagents', 'Subagents', registry);
  438. validateComponentPaths('commands', 'Commands', registry);
  439. validateComponentPaths('tools', 'Tools', registry);
  440. validateComponentPaths('plugins', 'Plugins', registry);
  441. validateComponentPaths('contexts', 'Contexts', registry);
  442. validateComponentPaths('config', 'Config', registry);
  443. validateComponentPaths('skills', 'Skills', registry);
  444. // Validate component dependencies
  445. validateComponentDependencies(registry);
  446. // Scan for orphaned files if verbose
  447. if (VERBOSE) {
  448. scanForOrphanedFiles(registry);
  449. }
  450. // Print summary and exit with appropriate code
  451. if (printSummary()) {
  452. process.exit(0);
  453. } else {
  454. process.exit(1);
  455. }
  456. }
  457. main();