mirror of
https://github.com/daffy0208/ai-dev-standards.git
synced 2026-09-14 20:49:49 +08:00
a1abcd95f8
Co-authored-by: daffy0208 <207889146+daffy0208@users.noreply.github.com>
491 lines
13 KiB
TypeScript
491 lines
13 KiB
TypeScript
#!/usr/bin/env tsx
|
|
|
|
/**
|
|
* Validate Registries
|
|
*
|
|
* Enhanced validation with detailed error messages.
|
|
* Checks that all registries are in sync with directory structure.
|
|
*
|
|
* Usage: npm run validate:registries
|
|
*/
|
|
|
|
import fs from 'fs'
|
|
import path from 'path'
|
|
import { fileURLToPath } from 'url'
|
|
|
|
const __filename = fileURLToPath(import.meta.url)
|
|
const __dirname = path.dirname(__filename)
|
|
const ROOT = path.resolve(__dirname, '..')
|
|
|
|
// Color codes
|
|
const GREEN = '\x1b[32m'
|
|
const YELLOW = '\x1b[33m'
|
|
const RED = '\x1b[31m'
|
|
const BLUE = '\x1b[34m'
|
|
const RESET = '\x1b[0m'
|
|
|
|
interface ValidationResult {
|
|
passed: boolean
|
|
errors: string[]
|
|
warnings: string[]
|
|
}
|
|
|
|
interface SkillRegistryEntry {
|
|
name: string
|
|
description?: string
|
|
triggers?: string[]
|
|
}
|
|
|
|
interface SkillRegistry extends Record<string, unknown> {
|
|
total_skills: number
|
|
skills: SkillRegistryEntry[]
|
|
}
|
|
|
|
interface McpRegistryEntry {
|
|
id: string
|
|
}
|
|
|
|
interface McpRegistry extends Record<string, unknown> {
|
|
total_mcps: number
|
|
mcps: McpRegistryEntry[]
|
|
}
|
|
|
|
interface ToolRegistryEntry {
|
|
id: string
|
|
}
|
|
|
|
interface ToolRegistry extends Record<string, unknown> {
|
|
total_tools: number
|
|
tools: ToolRegistryEntry[]
|
|
}
|
|
|
|
interface ComponentRegistryEntry {
|
|
id: string
|
|
}
|
|
|
|
interface ComponentRegistry extends Record<string, unknown> {
|
|
total_components: number
|
|
components: ComponentRegistryEntry[]
|
|
}
|
|
|
|
interface IntegrationRegistryEntry {
|
|
id: string
|
|
}
|
|
|
|
interface IntegrationRegistry extends Record<string, unknown> {
|
|
total_integrations: number
|
|
integrations: IntegrationRegistryEntry[]
|
|
}
|
|
|
|
/**
|
|
* Validate skill-registry.json
|
|
*/
|
|
function validateSkillRegistry(): ValidationResult {
|
|
console.log(`${BLUE}📝 Validating skill-registry.json${RESET}`)
|
|
|
|
const result: ValidationResult = {
|
|
passed: true,
|
|
errors: [],
|
|
warnings: []
|
|
}
|
|
|
|
const registry = loadRegistry<SkillRegistry>('skill-registry.json', 'skills', result)
|
|
if (!registry) {
|
|
return result
|
|
}
|
|
|
|
// Check required fields
|
|
if (!registry.skills || !Array.isArray(registry.skills)) {
|
|
result.errors.push('skill-registry.json missing "skills" array')
|
|
result.passed = false
|
|
}
|
|
|
|
if (registry.total_skills === undefined) {
|
|
result.errors.push('skill-registry.json missing "total_skills" field')
|
|
result.passed = false
|
|
}
|
|
|
|
// Count actual skill directories
|
|
const skillsDir = path.join(ROOT, 'SKILLS')
|
|
const actualSkills = fs.readdirSync(skillsDir).filter(dir => {
|
|
const stat = fs.statSync(path.join(skillsDir, dir))
|
|
return stat.isDirectory() && dir !== '_TEMPLATE'
|
|
})
|
|
|
|
// Validate count matches
|
|
if (registry.skills && registry.total_skills !== registry.skills.length) {
|
|
result.errors.push(
|
|
`total_skills (${registry.total_skills}) doesn't match skills array length (${registry.skills.length})`
|
|
)
|
|
result.passed = false
|
|
}
|
|
|
|
if (registry.skills && registry.skills.length !== actualSkills.length) {
|
|
result.errors.push(
|
|
`Registry has ${registry.skills.length} skills but skills/ directory has ${actualSkills.length} directories`
|
|
)
|
|
result.passed = false
|
|
}
|
|
|
|
// Check for orphaned registry entries
|
|
if (registry.skills) {
|
|
const registryNames = new Set(registry.skills.map(skill => skill.name))
|
|
const actualNames = new Set(actualSkills)
|
|
|
|
for (const name of registryNames) {
|
|
if (!actualNames.has(name)) {
|
|
result.errors.push(`Orphaned registry entry: ${name} (directory doesn't exist)`)
|
|
result.passed = false
|
|
}
|
|
}
|
|
|
|
for (const name of actualNames) {
|
|
if (!registryNames.has(name)) {
|
|
result.errors.push(`Missing registry entry: ${name} (directory exists but not in registry)`)
|
|
result.passed = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for duplicate IDs
|
|
if (registry.skills) {
|
|
const names = registry.skills.map(skill => skill.name)
|
|
const uniqueNames = new Set(names)
|
|
if (names.length !== uniqueNames.size) {
|
|
result.errors.push('Duplicate skill names detected')
|
|
result.passed = false
|
|
}
|
|
}
|
|
|
|
// Validate each skill has required fields
|
|
if (registry.skills) {
|
|
for (const skill of registry.skills) {
|
|
if (!skill.name) {
|
|
result.errors.push(`Skill missing "name" field`)
|
|
result.passed = false
|
|
}
|
|
if (!skill.description) {
|
|
result.warnings.push(`Skill ${skill.name} missing "description" field`)
|
|
}
|
|
if (!skill.triggers || !Array.isArray(skill.triggers) || skill.triggers.length === 0) {
|
|
result.warnings.push(`Skill ${skill.name} missing or empty "triggers" array`)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (result.passed) {
|
|
console.log(
|
|
`${GREEN}✅ skill-registry.json is valid (${registry.skills?.length || 0} skills)${RESET}`
|
|
)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Validate mcp-registry.json
|
|
*/
|
|
function validateMCPRegistry(): ValidationResult {
|
|
console.log(`${BLUE}📝 Validating mcp-registry.json${RESET}`)
|
|
|
|
const result: ValidationResult = {
|
|
passed: true,
|
|
errors: [],
|
|
warnings: []
|
|
}
|
|
|
|
const registry = loadRegistry<McpRegistry>('mcp-registry.json', 'mcps', result)
|
|
if (!registry) {
|
|
return result
|
|
}
|
|
|
|
// Check required fields
|
|
if (!registry.mcps || !Array.isArray(registry.mcps)) {
|
|
result.errors.push('mcp-registry.json missing "mcps" array')
|
|
result.passed = false
|
|
}
|
|
|
|
if (registry.total_mcps === undefined) {
|
|
result.errors.push('mcp-registry.json missing "total_mcps" field')
|
|
result.passed = false
|
|
}
|
|
|
|
// Count actual MCP directories
|
|
const mcpDir = path.join(ROOT, 'MCP-SERVERS')
|
|
const actualMCPs = fs.readdirSync(mcpDir).filter(dir => {
|
|
const stat = fs.statSync(path.join(mcpDir, dir))
|
|
return stat.isDirectory()
|
|
})
|
|
|
|
// Validate count matches
|
|
if (registry.mcps && registry.total_mcps !== registry.mcps.length) {
|
|
result.errors.push(
|
|
`total_mcps (${registry.total_mcps}) doesn't match mcps array length (${registry.mcps.length})`
|
|
)
|
|
result.passed = false
|
|
}
|
|
|
|
if (registry.mcps && registry.mcps.length !== actualMCPs.length) {
|
|
result.errors.push(
|
|
`Registry has ${registry.mcps.length} MCPs but mcp-servers/ directory has ${actualMCPs.length} directories`
|
|
)
|
|
result.passed = false
|
|
}
|
|
|
|
// Check for orphaned registry entries
|
|
if (registry.mcps) {
|
|
const registryIds = new Set(registry.mcps.map(mcp => mcp.id))
|
|
const actualIds = new Set(actualMCPs)
|
|
|
|
for (const id of registryIds) {
|
|
if (!actualIds.has(id)) {
|
|
result.errors.push(`Orphaned registry entry: ${id} (directory doesn't exist)`)
|
|
result.passed = false
|
|
}
|
|
}
|
|
|
|
for (const id of actualIds) {
|
|
if (!registryIds.has(id)) {
|
|
result.errors.push(`Missing registry entry: ${id} (directory exists but not in registry)`)
|
|
result.passed = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for duplicate IDs
|
|
if (registry.mcps) {
|
|
const ids = registry.mcps.map(mcp => mcp.id)
|
|
const uniqueIds = new Set(ids)
|
|
if (ids.length !== uniqueIds.size) {
|
|
result.errors.push('Duplicate MCP IDs detected')
|
|
result.passed = false
|
|
}
|
|
}
|
|
|
|
if (result.passed) {
|
|
console.log(
|
|
`${GREEN}✅ mcp-registry.json is valid (${registry.mcps?.length || 0} MCPs)${RESET}`
|
|
)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Validate tool-registry.json
|
|
*/
|
|
function validateToolRegistry(): ValidationResult {
|
|
console.log(`${BLUE}📝 Validating tool-registry.json${RESET}`)
|
|
|
|
const result: ValidationResult = {
|
|
passed: true,
|
|
errors: [],
|
|
warnings: []
|
|
}
|
|
|
|
const registry = loadRegistry<ToolRegistry>('tool-registry.json', 'tools', result)
|
|
if (!registry) {
|
|
return result
|
|
}
|
|
|
|
if (registry.total_tools === undefined) {
|
|
result.errors.push('tool-registry.json missing "total_tools" field')
|
|
result.passed = false
|
|
}
|
|
|
|
if (!registry.tools || !Array.isArray(registry.tools)) {
|
|
result.errors.push('tool-registry.json missing "tools" array')
|
|
result.passed = false
|
|
}
|
|
|
|
if (registry.tools && registry.total_tools !== registry.tools.length) {
|
|
result.errors.push(
|
|
`total_tools (${registry.total_tools}) doesn't match tools array length (${registry.tools.length})`
|
|
)
|
|
result.passed = false
|
|
}
|
|
|
|
if (result.passed) {
|
|
console.log(
|
|
`${GREEN}✅ tool-registry.json is valid (${registry.tools?.length || 0} tools)${RESET}`
|
|
)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Validate component-registry.json
|
|
*/
|
|
function validateComponentRegistry(): ValidationResult {
|
|
console.log(`${BLUE}📝 Validating component-registry.json${RESET}`)
|
|
|
|
const result: ValidationResult = {
|
|
passed: true,
|
|
errors: [],
|
|
warnings: []
|
|
}
|
|
|
|
const registry = loadRegistry<ComponentRegistry>('component-registry.json', 'components', result)
|
|
if (!registry) {
|
|
return result
|
|
}
|
|
|
|
if (registry.total_components === undefined) {
|
|
result.errors.push('component-registry.json missing "total_components" field')
|
|
result.passed = false
|
|
}
|
|
|
|
if (!registry.components || !Array.isArray(registry.components)) {
|
|
result.errors.push('component-registry.json missing "components" array')
|
|
result.passed = false
|
|
}
|
|
|
|
if (registry.components && registry.total_components !== registry.components.length) {
|
|
result.errors.push(
|
|
`total_components (${registry.total_components}) doesn't match components array length (${registry.components.length})`
|
|
)
|
|
result.passed = false
|
|
}
|
|
|
|
if (result.passed) {
|
|
console.log(
|
|
`${GREEN}✅ component-registry.json is valid (${registry.components?.length || 0} components)${RESET}`
|
|
)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Validate integration-registry.json
|
|
*/
|
|
function validateIntegrationRegistry(): ValidationResult {
|
|
console.log(`${BLUE}📝 Validating integration-registry.json${RESET}`)
|
|
|
|
const result: ValidationResult = {
|
|
passed: true,
|
|
errors: [],
|
|
warnings: []
|
|
}
|
|
|
|
const registry = loadRegistry<IntegrationRegistry>(
|
|
'integration-registry.json',
|
|
'integrations',
|
|
result
|
|
)
|
|
if (!registry) {
|
|
return result
|
|
}
|
|
|
|
if (registry.total_integrations === undefined) {
|
|
result.errors.push('integration-registry.json missing "total_integrations" field')
|
|
result.passed = false
|
|
}
|
|
|
|
if (!registry.integrations || !Array.isArray(registry.integrations)) {
|
|
result.errors.push('integration-registry.json missing "integrations" array')
|
|
result.passed = false
|
|
}
|
|
|
|
if (registry.integrations && registry.total_integrations !== registry.integrations.length) {
|
|
result.errors.push(
|
|
`total_integrations (${registry.total_integrations}) doesn't match integrations array length (${registry.integrations.length})`
|
|
)
|
|
result.passed = false
|
|
}
|
|
|
|
if (result.passed) {
|
|
console.log(
|
|
`${GREEN}✅ integration-registry.json is valid (${registry.integrations?.length || 0} integrations)${RESET}`
|
|
)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Main execution
|
|
*/
|
|
function main(): void {
|
|
console.log(`\n${GREEN}🔍 AI Dev Standards - Registry Validator${RESET}\n`)
|
|
|
|
const results: ValidationResult[] = [
|
|
validateSkillRegistry(),
|
|
validateMCPRegistry(),
|
|
validateToolRegistry(),
|
|
validateComponentRegistry(),
|
|
validateIntegrationRegistry()
|
|
]
|
|
|
|
// Collect all errors and warnings
|
|
const allErrors: string[] = []
|
|
const allWarnings: string[] = []
|
|
|
|
for (const result of results) {
|
|
allErrors.push(...result.errors)
|
|
allWarnings.push(...result.warnings)
|
|
}
|
|
|
|
// Print summary
|
|
console.log(`\n${BLUE}📊 Validation Summary:${RESET}`)
|
|
|
|
if (allErrors.length > 0) {
|
|
console.log(`\n${RED}❌ Errors (${allErrors.length}):${RESET}`)
|
|
for (const error of allErrors) {
|
|
console.log(` ${RED}✗${RESET} ${error}`)
|
|
}
|
|
}
|
|
|
|
if (allWarnings.length > 0) {
|
|
console.log(`\n${YELLOW}⚠️ Warnings (${allWarnings.length}):${RESET}`)
|
|
for (const warning of allWarnings) {
|
|
console.log(` ${YELLOW}!${RESET} ${warning}`)
|
|
}
|
|
}
|
|
|
|
if (allErrors.length === 0) {
|
|
console.log(`\n${GREEN}✅ All registries are valid!${RESET}`)
|
|
|
|
if (allWarnings.length > 0) {
|
|
console.log(`${YELLOW} (${allWarnings.length} warnings)${RESET}`)
|
|
}
|
|
|
|
console.log()
|
|
process.exit(0)
|
|
} else {
|
|
console.log(`\n${RED}❌ Validation failed with ${allErrors.length} error(s)${RESET}`)
|
|
console.log(`\n${BLUE}To fix, run:${RESET} npm run generate:registries\n`)
|
|
process.exit(1)
|
|
}
|
|
}
|
|
|
|
main()
|
|
function loadRegistry<T extends Record<string, unknown>>(
|
|
fileName: string,
|
|
key: string,
|
|
result: ValidationResult
|
|
): T | null {
|
|
const registryPath = path.join(ROOT, 'META', fileName)
|
|
if (!fs.existsSync(registryPath)) {
|
|
result.errors.push(`${fileName} does not exist`)
|
|
result.passed = false
|
|
return null
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(fs.readFileSync(registryPath, 'utf-8')) as T
|
|
const section = (parsed as Record<string, unknown>)[key]
|
|
if (!Array.isArray(section)) {
|
|
result.errors.push(`${fileName} missing "${key}" array`)
|
|
result.passed = false
|
|
return null
|
|
}
|
|
return parsed
|
|
} catch (e) {
|
|
result.errors.push(`${fileName} is not valid JSON: ${e}`)
|
|
result.passed = false
|
|
return null
|
|
}
|
|
}
|