OpenWriterControle/scripts/registry/validate-registry.ts
InteLigent c799173dd0
align context and skills consistency (#192) (#198)
* fix(context): migrate delegation references to split workflow files

* chore(ci): add markdown link validation with regex skip rules

* fix(context): migrate index references to navigation

* fix(agent): allow limited bash for task status updates

* refactor(skills): migrate to .opencode/skills and normalize skill files

* fix(agent): align subagent permissions and workflow structure

* docs: reconcile workflow examples and design iteration references

* feat(skill): enforce schema checks in task validate command

* fix(install): improve colored output and skills collision grouping
2026-02-12 00:20:51 +00:00

543 lines
16 KiB
TypeScript
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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();