afv-library/tests/helpers/link-checker.ts
2026-04-21 10:32:25 +05:30

55 lines
1.5 KiB
TypeScript

import fs from "fs"
import path from "path"
export interface BrokenLink {
link: string
text: string
reason: string
}
/**
* Extracts relative markdown links from content.
* Ignores URLs (http/https), anchors-only (#heading), and mailto links.
*/
export function extractRelativeLinks(content: string): Array<{ text: string; link: string }> {
const linkRegex = /\[([^\]]*)\]\(([^)]+)\)/g
const links: Array<{ text: string; link: string }> = []
let match: RegExpExecArray | null
while ((match = linkRegex.exec(content)) !== null) {
const link = match[2]
if (link.startsWith("http://") || link.startsWith("https://") || link.startsWith("#") || link.startsWith("mailto:")) {
continue
}
const filePath = link.split("#")[0]
if (filePath) {
links.push({ text: match[1], link: filePath })
}
}
return links
}
/**
* Checks all relative markdown links in a SKILL.md resolve to existing files.
*/
export function findBrokenLinks(skillDir: string, content: string): BrokenLink[] {
const links = extractRelativeLinks(content)
const broken: BrokenLink[] = []
for (const { text, link } of links) {
const resolved = path.resolve(skillDir, link)
if (!resolved.startsWith(path.resolve(skillDir))) {
broken.push({ link, text, reason: `Escapes skill directory via ../` })
continue
}
if (!fs.existsSync(resolved)) {
broken.push({ link, text, reason: `File not found: ${link}` })
}
}
return broken
}