BMAD-METHOD/tools/fix-doc-links.js

173 lines
5.2 KiB
JavaScript

/**
* Documentation Link Fixer
*
* Reads the audit report generated by audit-doc-links.js and applies fixes
* to broken links where a single match was found.
*
* Usage:
* node tools/fix-doc-links.js # Dry-run (preview changes)
* node tools/fix-doc-links.js --apply # Apply changes
*/
const { readFileSync, writeFileSync, existsSync } = require('node:fs');
const { resolve, relative } = require('node:path');
const REPORT_PATH = resolve(__dirname, '.link-audit-report.json');
// Colors for console output
const colors = {
reset: '\u001B[0m',
red: '\u001B[31m',
green: '\u001B[32m',
yellow: '\u001B[33m',
cyan: '\u001B[36m',
dim: '\u001B[2m',
};
/**
* Load the audit report.
*/
function loadReport() {
if (!existsSync(REPORT_PATH)) {
console.error(`\n ${colors.red}Error:${colors.reset} No audit report found.`);
console.error(` Run 'node tools/audit-doc-links.js' first.\n`);
process.exit(1);
}
try {
const content = readFileSync(REPORT_PATH, 'utf-8');
return JSON.parse(content);
} catch (error) {
console.error(`\n ${colors.red}Error:${colors.reset} Failed to parse audit report.`);
console.error(` ${error.message}\n`);
process.exit(1);
}
}
/**
* Apply a fix to a file by replacing the original link with the suggested fix.
*/
function applyFix(filePath, originalLink, suggestedFix) {
const content = readFileSync(filePath, 'utf-8');
// Create the replacement pattern - we need to match the exact link in markdown syntax
// Original might have anchor, so we need to handle that
const originalWithoutAnchor = originalLink.split('#')[0];
const suggestedWithoutAnchor = suggestedFix.split('#')[0];
// Also preserve any anchor from the original if the fix doesn't include one
let finalFix = suggestedFix;
if (originalLink.includes('#') && !suggestedFix.includes('#')) {
const anchor = originalLink.slice(originalLink.indexOf('#'));
finalFix = suggestedFix + anchor;
}
// Replace the link - be careful to only replace inside markdown link syntax
const linkPattern = new RegExp(`\\]\\(${escapeRegex(originalLink)}\\)`, 'g');
const newContent = content.replace(linkPattern, `](${finalFix})`);
if (newContent === content) {
return false; // No change made
}
writeFileSync(filePath, newContent);
return true;
}
/**
* Escape special regex characters in a string.
*/
function escapeRegex(string) {
return string.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
}
/**
* Main function.
*/
async function main() {
const args = process.argv.slice(2);
const applyChanges = args.includes('--apply');
console.log('\n Documentation Link Fixer');
console.log(' ========================\n');
if (applyChanges) {
console.log(` ${colors.green}APPLYING CHANGES${colors.reset}\n`);
} else {
console.log(` ${colors.yellow}DRY RUN${colors.reset} - No files will be modified.`);
console.log(` Use --apply to apply changes.\n`);
}
const report = loadReport();
if (report.autoFixable.length === 0) {
console.log(` ${colors.green}${colors.reset} No auto-fixable links found.\n`);
process.exit(0);
}
console.log(` Found ${report.autoFixable.length} auto-fixable link(s).\n`);
const fixed = [];
const failed = [];
// Group fixes by file for efficiency
const byFile = {};
for (const item of report.autoFixable) {
if (!byFile[item.sourceFileAbsolute]) {
byFile[item.sourceFileAbsolute] = [];
}
byFile[item.sourceFileAbsolute].push(item);
}
for (const [filePath, items] of Object.entries(byFile)) {
const displayPath = relative(process.cwd(), filePath);
console.log(` ${colors.cyan}${displayPath}${colors.reset}`);
for (const item of items) {
console.log(` Line ${item.line}:`);
console.log(` ${colors.dim}Old:${colors.reset} ${item.originalLink}`);
console.log(` ${colors.dim}New:${colors.reset} ${item.suggestedFix}`);
if (applyChanges) {
try {
const success = applyFix(filePath, item.originalLink, item.suggestedFix);
if (success) {
console.log(` ${colors.green}✓ Fixed${colors.reset}`);
fixed.push(item);
} else {
console.log(` ${colors.yellow}⚠ No match found (may have been fixed already)${colors.reset}`);
}
} catch (error) {
console.log(` ${colors.red}✗ Error: ${error.message}${colors.reset}`);
failed.push({ ...item, error: error.message });
}
}
console.log();
}
}
// Summary
console.log(' ' + '─'.repeat(50));
console.log('\n SUMMARY\n');
if (applyChanges) {
console.log(` Fixed: ${colors.green}${fixed.length}${colors.reset}`);
if (failed.length > 0) {
console.log(` Failed: ${colors.red}${failed.length}${colors.reset}`);
}
console.log(`\n Run 'node tools/audit-doc-links.js' to verify remaining issues.\n`);
} else {
console.log(` Would fix: ${colors.yellow}${report.autoFixable.length}${colors.reset} link(s)`);
console.log(`\n To apply these fixes, run:`);
console.log(` node tools/fix-doc-links.js --apply\n`);
}
process.exit(failed.length > 0 ? 1 : 0);
}
main().catch((error) => {
console.error('Error:', error.message);
process.exit(1);
});