import matter from 'gray-matter'; import { ValidationResult, ValidationIssue, ValidatorOptions } from './types.js'; import { validateName, validateVersion, validateDescription, validateHomepage, validateRepository, validateApiBase, validateContent, extractMetadata, } from './rules.js'; import { fetchSkillContent, isUrl } from './fetcher.js'; import * as fs from 'fs'; import * as path from 'path'; export function validateSkill( content: string, filename: string, _options: ValidatorOptions = {} ): ValidationResult { const errors: ValidationIssue[] = []; const warnings: ValidationIssue[] = []; // Check for frontmatter markers if (!content.startsWith('---')) { errors.push({ code: 'missing-frontmatter', message: 'File must start with YAML frontmatter (---)', }); return { valid: false, file: filename, errors, warnings, parsed: null }; } // Parse frontmatter let parsed: matter.GrayMatterFile; try { parsed = matter(content); } catch (err) { errors.push({ code: 'invalid-frontmatter', message: `Failed to parse frontmatter: ${err instanceof Error ? err.message : 'Unknown error'}`, }); return { valid: false, file: filename, errors, warnings, parsed: null }; } const data = parsed.data as Record; // Validate required fields const nameError = validateName(data); if (nameError) errors.push(nameError); const versionError = validateVersion(data); if (versionError) errors.push(versionError); const descResult = validateDescription(data); if (descResult.error) errors.push(descResult.error); if (descResult.warning) warnings.push(descResult.warning); // Validate optional URL fields const homepageResult = validateHomepage(data); if (homepageResult.error) errors.push(homepageResult.error); if (homepageResult.warning) warnings.push(homepageResult.warning); const repoResult = validateRepository(data); if (repoResult.error) errors.push(repoResult.error); if (repoResult.warning) warnings.push(repoResult.warning); const apiBaseError = validateApiBase(data); if (apiBaseError) errors.push(apiBaseError); // Validate content body const contentResult = validateContent(parsed.content); errors.push(...contentResult.errors); warnings.push(...contentResult.warnings); // Extract metadata for result const metadata = extractMetadata(data); return { valid: errors.length === 0, file: filename, errors, warnings, parsed: metadata, }; } export async function validateSkillFile( filepath: string, options: ValidatorOptions = {} ): Promise { const absolutePath = path.resolve(filepath); if (!fs.existsSync(absolutePath)) { return { valid: false, file: filepath, errors: [{ code: 'file-not-found', message: `File not found: ${filepath}` }], warnings: [], parsed: null, }; } const content = fs.readFileSync(absolutePath, 'utf-8'); return validateSkill(content, filepath, options); } export async function validateSkillUrl( url: string, options: ValidatorOptions = {} ): Promise { try { const content = await fetchSkillContent(url); return validateSkill(content, url, options); } catch (err) { return { valid: false, file: url, errors: [ { code: 'fetch-failed', message: err instanceof Error ? err.message : 'Failed to fetch URL', }, ], warnings: [], parsed: null, }; } } export async function validateSkillInput( input: string, options: ValidatorOptions = {} ): Promise { if (isUrl(input)) { return validateSkillUrl(input, options); } return validateSkillFile(input, options); }