(filePath: string)
| 87 | |
| 88 | /** Validates a single skill file. */ |
| 89 | export async function validateSkill(filePath: string): Promise<ValidationResult> { |
| 90 | const name = basename(join(filePath, '..')); |
| 91 | const failures: string[] = []; |
| 92 | |
| 93 | try { |
| 94 | const content = await readFile(filePath, {encoding: 'utf8'}); |
| 95 | const frontmatterRaw = content.match(/^---\n([\s\S]*?)\n---/); |
| 96 | |
| 97 | if (frontmatterRaw === null) { |
| 98 | failures.push('Missing or invalid frontmatter in SKILL.md'); |
| 99 | return {name, failures}; |
| 100 | } |
| 101 | |
| 102 | let frontmatter: unknown; |
| 103 | try { |
| 104 | frontmatter = parse(frontmatterRaw[1]); |
| 105 | } catch (e) { |
| 106 | failures.push(`Failed to parse YAML frontmatter: ${(e as Error).message}`); |
| 107 | return {name, failures}; |
| 108 | } |
| 109 | |
| 110 | const frontmatterData = skillFrontmatterSchema.safeParse(frontmatter); |
| 111 | if (frontmatterData.success === false) { |
| 112 | for (const issue of frontmatterData.error.issues) { |
| 113 | failures.push(`Schema validation failure: [${issue.path.join('.')}] ${issue.message}`); |
| 114 | } |
| 115 | return {name, failures}; |
| 116 | } |
| 117 | |
| 118 | // Check name match if data looks like an object with a name |
| 119 | if (frontmatterData.data?.name !== name) { |
| 120 | failures.push( |
| 121 | `Name mismatch. Expected "${name}", found "${frontmatterData.data?.name ?? '<UNKNOWN>'}"`, |
| 122 | ); |
| 123 | } |
| 124 | } catch (e) { |
| 125 | failures.push(`Unexpected error: ${(e as Error).message}`); |
| 126 | } |
| 127 | |
| 128 | return {name, failures}; |
| 129 | } |
no test coverage detected