automagik-genie
Version:
Self-evolving AI agent orchestration framework with Model Context Protocol support
208 lines (174 loc) • 4.92 kB
JavaScript
/**
* Validate Markdown Links Helper
*
* Check markdown links for broken references (files and anchors).
* Detects [text](path) links pointing to non-existent files or anchors.
*
* Usage:
* node validate-links.js <file-path> # Check links in file
* node validate-links.js <directory> # Check all .md files in directory
*
* Output:
* <file>:<line>: Broken link [text](path) - File not found
* <file>:<line>: Broken link [text](path#anchor) - Anchor not found
*/
const fs = require('fs');
const path = require('path');
/**
* Extract markdown links from text
* Returns: [{ text, href, line }]
*/
function extractLinks(content) {
const lines = content.split('\n');
const links = [];
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
lines.forEach((line, idx) => {
let match;
while ((match = linkRegex.exec(line)) !== null) {
const text = match[1];
const href = match[2];
// Skip external URLs
if (href.startsWith('http://') || href.startsWith('https://')) {
continue;
}
links.push({ text, href, line: idx + 1 });
}
});
return links;
}
/**
* Check if file exists
*/
function fileExists(filePath, basePath) {
const fullPath = path.resolve(path.dirname(basePath), filePath);
return fs.existsSync(fullPath);
}
/**
* Check if anchor exists in file
*/
function anchorExists(filePath, anchor, basePath) {
try {
const fullPath = path.resolve(path.dirname(basePath), filePath);
const content = fs.readFileSync(fullPath, 'utf-8');
// Convert anchor to heading format
// #my-heading → ## My Heading or ### My Heading, etc.
const headingText = anchor.toLowerCase().replace(/-/g, ' ');
const headingRegex = new RegExp(`^#+\\s+${headingText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'im');
return headingRegex.test(content);
} catch (err) {
return false;
}
}
/**
* Validate links in file
*/
function validateFile(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf-8');
const links = extractLinks(content);
const issues = [];
links.forEach(({ text, href, line }) => {
// Split href into path and anchor
const [linkPath, anchor] = href.split('#');
// Check if it's just an anchor (same file)
if (!linkPath && anchor) {
if (!anchorExists(filePath, anchor, filePath)) {
issues.push({
file: filePath,
line,
text,
href,
error: `Anchor not found: #${anchor}`,
});
}
return;
}
// Check if file exists
if (!fileExists(linkPath, filePath)) {
issues.push({
file: filePath,
line,
text,
href,
error: 'File not found',
});
return;
}
// If anchor specified, check if it exists in target file
if (anchor && !anchorExists(linkPath, anchor, filePath)) {
issues.push({
file: filePath,
line,
text,
href,
error: `Anchor not found: #${anchor}`,
});
}
});
return issues;
} catch (err) {
return [{ file: filePath, error: `Failed to read file: ${err.message}` }];
}
}
/**
* Validate all .md files in directory
*/
function validateDirectory(dirPath) {
const allIssues = [];
function scanDir(dir) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
entries.forEach(entry => {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory() && !entry.name.startsWith('.')) {
scanDir(fullPath);
} else if (entry.isFile() && entry.name.endsWith('.md')) {
const issues = validateFile(fullPath);
allIssues.push(...issues);
}
});
}
scanDir(dirPath);
return allIssues;
}
/**
* Main
*/
function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
console.error(`
Usage:
node validate-links.js <file-path> # Check links in single file
node validate-links.js <directory> # Check all .md files recursively
Output:
<file>:<line>: Broken link [text](path) - error description
Exit code:
0 = All links valid
1 = Broken links found
`);
process.exit(1);
}
const target = args[0];
if (!fs.existsSync(target)) {
console.error(`Error: Path not found: ${target}`);
process.exit(1);
}
const stat = fs.statSync(target);
const issues = stat.isDirectory()
? validateDirectory(target)
: validateFile(target);
if (issues.length === 0) {
console.log('All links valid');
process.exit(0);
}
issues.forEach(issue => {
if (issue.line) {
console.log(`${issue.file}:${issue.line}: Broken link [${issue.text}](${issue.href}) - ${issue.error}`);
} else {
console.log(`${issue.file}: ${issue.error}`);
}
});
process.exit(1);
}
main();