check-dependencies.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. #!/usr/bin/env bun
  2. /**
  3. * Comprehensive Registry Dependency Checker
  4. *
  5. * This script validates that:
  6. * 1. All component dependencies exist in the registry
  7. * 2. All profile components exist in the registry
  8. * 3. Critical infrastructure files are included
  9. * 4. No orphaned references exist
  10. *
  11. * Exit codes:
  12. * 0 = All checks passed
  13. * 1 = Missing dependencies found
  14. * 2 = Critical files missing
  15. * 3 = Configuration errors
  16. */
  17. import { existsSync, readFileSync } from 'fs';
  18. import { join, dirname } from 'path';
  19. import { fileURLToPath } from 'url';
  20. // Configuration
  21. const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '../..');
  22. const REGISTRY_FILE = join(REPO_ROOT, 'registry.json');
  23. const PROFILES_DIR = join(REPO_ROOT, '.opencode/profiles');
  24. // Colors
  25. const colors = {
  26. red: '\x1b[0;31m',
  27. green: '\x1b[0;32m',
  28. yellow: '\x1b[1;33m',
  29. blue: '\x1b[0;34m',
  30. cyan: '\x1b[0;36m',
  31. bold: '\x1b[1m',
  32. reset: '\x1b[0m',
  33. };
  34. // Counters
  35. let TOTAL_CHECKS = 0;
  36. let PASSED_CHECKS = 0;
  37. let FAILED_CHECKS = 0;
  38. // Issues
  39. const MISSING_PROFILE_COMPONENTS: string[] = [];
  40. const ORPHANED_DEPENDENCIES: string[] = [];
  41. const CRITICAL_FILES_MISSING: string[] = [];
  42. // CLI flags
  43. let VERBOSE = false;
  44. // Types
  45. interface Component {
  46. id: string;
  47. name: string;
  48. type: string;
  49. path: string;
  50. dependencies?: string[];
  51. }
  52. interface Registry {
  53. version: string;
  54. schema_version: string;
  55. repository: string;
  56. categories: Record<string, string>;
  57. components: {
  58. agents?: Component[];
  59. subagents?: Component[];
  60. commands?: Component[];
  61. tools?: Component[];
  62. plugins?: Component[];
  63. contexts?: Component[];
  64. config?: Component[];
  65. skills?: Component[];
  66. };
  67. profiles?: Record<string, {
  68. name: string;
  69. description: string;
  70. components: string[];
  71. }>;
  72. }
  73. // Critical files that must be in registry
  74. const CRITICAL_FILES = [
  75. { id: 'root-navigation', path: '.opencode/context/navigation.md', description: 'Root navigation file - ContextScout starts here' },
  76. { id: 'context-paths-config', path: '.opencode/context/core/config/paths.json', description: 'Context paths configuration - loaded via @ reference' },
  77. { id: 'context-system', path: '.opencode/context/core/context-system.md', description: 'Context system guide' },
  78. ];
  79. // Print functions
  80. function printHeader(): void {
  81. console.log(`${colors.cyan}${colors.bold}`);
  82. console.log('╔════════════════════════════════════════════════════════════════╗');
  83. console.log('║ ║');
  84. console.log('║ Comprehensive Dependency Checker v1.0.0 ║');
  85. console.log('║ ║');
  86. console.log('╚════════════════════════════════════════════════════════════════╝');
  87. console.log(`${colors.reset}`);
  88. }
  89. function printSuccess(msg: string): void {
  90. console.log(`${colors.green}✓${colors.reset} ${msg}`);
  91. }
  92. function printError(msg: string): void {
  93. console.log(`${colors.red}✗${colors.reset} ${msg}`);
  94. }
  95. function printWarning(msg: string): void {
  96. console.log(`${colors.yellow}⚠${colors.reset} ${msg}`);
  97. }
  98. function printInfo(msg: string): void {
  99. console.log(`${colors.blue}ℹ${colors.reset} ${msg}`);
  100. }
  101. function printStep(msg: string): void {
  102. console.log(`\n${colors.bold}${msg}${colors.reset}`);
  103. }
  104. function usage(): void {
  105. console.log('Usage: bun run scripts/registry/check-dependencies.ts [OPTIONS]');
  106. console.log('');
  107. console.log('Options:');
  108. console.log(' -v, --verbose Show detailed validation output');
  109. console.log(' -h, --help Show this help message');
  110. console.log('');
  111. console.log('Exit codes:');
  112. console.log(' 0 = All checks passed');
  113. console.log(' 1 = Missing dependencies found');
  114. console.log(' 2 = Critical files missing');
  115. process.exit(0);
  116. }
  117. // Load registry
  118. function loadRegistry(): Registry {
  119. if (!existsSync(REGISTRY_FILE)) {
  120. printError(`Registry file not found: ${REGISTRY_FILE}`);
  121. process.exit(3);
  122. }
  123. try {
  124. const content = readFileSync(REGISTRY_FILE, 'utf-8');
  125. const registry = JSON.parse(content) as Registry;
  126. printSuccess('Registry loaded successfully');
  127. return registry;
  128. } catch (error) {
  129. printError('Failed to parse registry.json');
  130. process.exit(3);
  131. }
  132. }
  133. // Load profiles
  134. function loadProfiles(): Record<string, any> {
  135. const profiles: Record<string, any> = {};
  136. const profileFiles = ['essential', 'developer', 'business', 'full', 'advanced'];
  137. for (const profileName of profileFiles) {
  138. const profilePath = join(PROFILES_DIR, profileName, 'profile.json');
  139. if (existsSync(profilePath)) {
  140. try {
  141. const content = readFileSync(profilePath, 'utf-8');
  142. profiles[profileName] = JSON.parse(content);
  143. } catch (error) {
  144. printWarning(`Failed to load profile: ${profileName}`);
  145. }
  146. }
  147. }
  148. const registry = loadRegistry();
  149. if (registry.profiles) {
  150. Object.assign(profiles, registry.profiles);
  151. }
  152. printSuccess(`Loaded ${Object.keys(profiles).length} profiles`);
  153. return profiles;
  154. }
  155. // Build component lookup map
  156. function buildComponentMap(registry: Registry): Map<string, Component> {
  157. const map = new Map<string, Component>();
  158. const categories = ['agents', 'subagents', 'commands', 'tools', 'plugins', 'contexts', 'config', 'skills'];
  159. for (const category of categories) {
  160. const components = registry.components[category as keyof Registry['components']];
  161. if (components) {
  162. for (const component of components) {
  163. const key = `${category.replace(/s$/, '')}:${component.id}`;
  164. map.set(key, component);
  165. map.set(component.id, component);
  166. }
  167. }
  168. }
  169. return map;
  170. }
  171. // Check if component exists
  172. function componentExists(dep: string, componentMap: Map<string, Component>): boolean {
  173. if (dep.includes('*')) return true; // Skip wildcards
  174. const match = dep.match(/^([^:]+):(.+)$/);
  175. if (!match) return false;
  176. const [, type, id] = match;
  177. const fullKey = `${type}:${id}`;
  178. return componentMap.has(fullKey) || componentMap.has(id);
  179. }
  180. // Check critical files
  181. function checkCriticalFiles(registry: Registry): void {
  182. printStep('Checking Critical Infrastructure Files...');
  183. const allPaths = new Set<string>();
  184. const allIds = new Set<string>();
  185. for (const category of Object.keys(registry.components)) {
  186. const components = registry.components[category as keyof Registry['components']];
  187. if (components) {
  188. for (const c of components) {
  189. allPaths.add(c.path);
  190. allIds.add(c.id);
  191. }
  192. }
  193. }
  194. for (const critical of CRITICAL_FILES) {
  195. TOTAL_CHECKS++;
  196. const hasPath = allPaths.has(critical.path);
  197. const hasId = allIds.has(critical.id);
  198. if (hasPath && hasId) {
  199. PASSED_CHECKS++;
  200. printSuccess(`${critical.description}`);
  201. } else {
  202. FAILED_CHECKS++;
  203. CRITICAL_FILES_MISSING.push(critical.id);
  204. printError(`MISSING CRITICAL FILE: ${critical.description}`);
  205. printInfo(` Expected ID: ${critical.id}`);
  206. printInfo(` Expected path: ${critical.path}`);
  207. }
  208. }
  209. }
  210. // Validate profiles
  211. function validateProfiles(profiles: Record<string, any>, componentMap: Map<string, Component>): void {
  212. printStep('Validating Profile Components...');
  213. for (const [profileName, profile] of Object.entries(profiles)) {
  214. if (VERBOSE) {
  215. printInfo(`Checking profile: ${profileName}`);
  216. }
  217. const components = profile.components || [];
  218. for (const componentRef of components) {
  219. TOTAL_CHECKS++;
  220. if (componentRef.includes('*')) {
  221. PASSED_CHECKS++;
  222. continue;
  223. }
  224. if (componentExists(componentRef, componentMap)) {
  225. PASSED_CHECKS++;
  226. if (VERBOSE) {
  227. printSuccess(` ${componentRef}`);
  228. }
  229. } else {
  230. FAILED_CHECKS++;
  231. MISSING_PROFILE_COMPONENTS.push(`${profileName}|${componentRef}`);
  232. printError(`Profile "${profileName}" references missing: ${componentRef}`);
  233. }
  234. }
  235. }
  236. }
  237. // Validate component dependencies
  238. function validateComponentDependencies(registry: Registry, componentMap: Map<string, Component>): void {
  239. printStep('Validating Component Dependencies...');
  240. const categories = Object.keys(registry.components) as Array<keyof Registry['components']>;
  241. for (const category of categories) {
  242. const components = registry.components[category];
  243. if (!components) continue;
  244. for (const component of components) {
  245. if (!component.dependencies || component.dependencies.length === 0) continue;
  246. for (const dep of component.dependencies) {
  247. if (!dep) continue;
  248. TOTAL_CHECKS++;
  249. if (componentExists(dep, componentMap)) {
  250. PASSED_CHECKS++;
  251. if (VERBOSE) {
  252. printSuccess(`${component.name} → ${dep}`);
  253. }
  254. } else {
  255. FAILED_CHECKS++;
  256. ORPHANED_DEPENDENCIES.push(`${category}|${component.id}|${dep}`);
  257. printError(`${component.name} (${category}) → missing: ${dep}`);
  258. }
  259. }
  260. }
  261. }
  262. }
  263. // Print summary
  264. function printSummary(): number {
  265. console.log('');
  266. console.log(`${colors.bold}═══════════════════════════════════════════════════════════════${colors.reset}`);
  267. console.log(`${colors.bold}Dependency Check Summary${colors.reset}`);
  268. console.log(`${colors.bold}═══════════════════════════════════════════════════════════════${colors.reset}`);
  269. console.log('');
  270. console.log(`Total checks: ${colors.cyan}${TOTAL_CHECKS}${colors.reset}`);
  271. console.log(`Passed: ${colors.green}${PASSED_CHECKS}${colors.reset}`);
  272. console.log(`Failed: ${colors.red}${FAILED_CHECKS}${colors.reset}`);
  273. console.log('');
  274. let exitCode = 0;
  275. if (CRITICAL_FILES_MISSING.length > 0) {
  276. exitCode = 2;
  277. printError(`CRITICAL: ${CRITICAL_FILES_MISSING.length} infrastructure file(s) missing!`);
  278. console.log('');
  279. console.log('Missing critical files:');
  280. for (const id of CRITICAL_FILES_MISSING) {
  281. const critical = CRITICAL_FILES.find(c => c.id === id);
  282. console.log(` - ${id}: ${critical?.description}`);
  283. console.log(` Path: ${critical?.path}`);
  284. }
  285. console.log('');
  286. }
  287. if (MISSING_PROFILE_COMPONENTS.length > 0) {
  288. exitCode = exitCode || 1;
  289. printError(`Found ${MISSING_PROFILE_COMPONENTS.length} missing profile component(s)`);
  290. console.log('');
  291. for (const entry of MISSING_PROFILE_COMPONENTS) {
  292. const [profile, component] = entry.split('|');
  293. console.log(` - Profile "${profile}" → ${component}`);
  294. }
  295. console.log('');
  296. }
  297. if (ORPHANED_DEPENDENCIES.length > 0) {
  298. exitCode = exitCode || 1;
  299. printError(`Found ${ORPHANED_DEPENDENCIES.length} orphaned dependency(ies)`);
  300. console.log('');
  301. for (const entry of ORPHANED_DEPENDENCIES) {
  302. const [category, id, dep] = entry.split('|');
  303. console.log(` - ${id} (${category}) → ${dep}`);
  304. }
  305. console.log('');
  306. }
  307. if (exitCode === 0) {
  308. printSuccess('All dependency checks passed!');
  309. console.log('');
  310. printInfo('No issues found. Registry is consistent.');
  311. } else {
  312. console.log(`${colors.yellow}Action required:${colors.reset}`);
  313. console.log(' 1. Add missing components to registry.json');
  314. console.log(' 2. Update profile references to use valid component IDs');
  315. console.log(' 3. Ensure all component dependencies exist');
  316. console.log('');
  317. console.log(`${colors.yellow}Prevention:${colors.reset}`);
  318. console.log(' Run this check before committing changes:');
  319. console.log(' bun run scripts/registry/check-dependencies.ts');
  320. }
  321. return exitCode;
  322. }
  323. // Main
  324. function main(): void {
  325. printHeader();
  326. // Parse arguments
  327. const args = process.argv.slice(2);
  328. for (const arg of args) {
  329. switch (arg) {
  330. case '-v':
  331. case '--verbose':
  332. VERBOSE = true;
  333. break;
  334. case '-h':
  335. case '--help':
  336. usage();
  337. break;
  338. default:
  339. console.log(`Unknown option: ${arg}`);
  340. usage();
  341. }
  342. }
  343. // Load data
  344. const registry = loadRegistry();
  345. const profiles = loadProfiles();
  346. const componentMap = buildComponentMap(registry);
  347. printInfo(`Loaded ${componentMap.size} components`);
  348. console.log('');
  349. // Run checks
  350. checkCriticalFiles(registry);
  351. validateProfiles(profiles, componentMap);
  352. validateComponentDependencies(registry, componentMap);
  353. // Print summary and exit
  354. const exitCode = printSummary();
  355. process.exit(exitCode);
  356. }
  357. main();