#!/usr/bin/env bun /** * Unified markdown formatter for Claude Code files. * * Runs formatting tools in sequence: * 1. format-markdown-tables.ts - Table alignment and separator spacing * 2. Custom fixes (XML tag spacing, etc.) * 3. markdownlint-cli2 --fix - Standard markdown linting * * Usage: * bun scripts/format-markdown.ts [path] # Format files * bun scripts/format-markdown.ts --check [path] # Check only (exit 1 if issues) * bun scripts/format-markdown.ts --dry-run [path] # Show what would change * * Examples: * bun scripts/format-markdown.ts # Format all .md files * bun scripts/format-markdown.ts outfitter/ # Format specific dir * bun scripts/format-markdown.ts path/to/file.md # Format single file * bun scripts/format-markdown.ts --check # CI mode */ import { spawn } from "bun"; import { statSync, existsSync } from "fs"; import { dirname, join, resolve } from "path"; const SCRIPTS_DIR = dirname(new URL(import.meta.url).pathname); /** * Result of a single formatting step. */ interface FormatResult { /** Name of the formatting step */ step: string; /** Whether the step completed without errors */ success: boolean; /** Output from the formatting tool */ output: string; /** Whether files were modified */ changed: boolean; } async function runCommand( cmd: string[], description: string ): Promise { const proc = spawn({ cmd, stdout: "pipe", stderr: "pipe", }); const stdout = await new Response(proc.stdout).text(); const stderr = await new Response(proc.stderr).text(); const exitCode = await proc.exited; const output = stdout + stderr; const changed = output.includes("Fixed") || output.includes("fixed"); return { step: description, success: exitCode === 0, output: output.trim(), changed, }; } async function checkToolAvailable(tool: string): Promise { try { const proc = spawn({ cmd: ["which", tool], stdout: "pipe", stderr: "pipe", }); await proc.exited; return proc.exitCode === 0; } catch { return false; } } async function formatTables( path: string, dryRun: boolean ): Promise { const script = join(SCRIPTS_DIR, "format-markdown-tables.ts"); const args = ["bun", script]; if (!dryRun) { args.push("--fix"); } args.push(path); return runCommand(args, "Table formatting"); } async function formatXmlTags( path: string, dryRun: boolean ): Promise { const script = join(SCRIPTS_DIR, "lint-xml-tags.ts"); if (!existsSync(script)) { return { step: "XML tag formatting", success: true, output: "Skipped (script not found)", changed: false, }; } const args = ["bun", script]; if (!dryRun) { args.push("--fix"); } args.push(path); return runCommand(args, "XML tag formatting"); } async function runMarkdownlint( path: string, dryRun: boolean, isFile: boolean ): Promise { // Check if markdownlint-cli2 is available const hasMarkdownlint = await checkToolAvailable("markdownlint-cli2"); if (!hasMarkdownlint) { return { step: "markdownlint-cli2", success: true, output: "Skipped (markdownlint-cli2 not installed)\nInstall: bun add -g markdownlint-cli2", changed: false, }; } const args = ["markdownlint-cli2"]; if (!dryRun) { args.push("--fix"); } // Always ignore config globs - we specify our own targets args.push("--no-globs"); // Handle path - literal file (: prefix) or glob pattern for directories if (isFile) { args.push(`:${path}`); // : prefix = literal file path } else { args.push(join(path, "**/*.md")); } return runCommand(args, "markdownlint-cli2"); } function printResult(result: FormatResult, verbose: boolean) { const icon = result.success ? "✓" : "✗"; const color = result.success ? "\x1b[32m" : "\x1b[31m"; const reset = "\x1b[0m"; console.log(`${color}${icon}${reset} ${result.step}`); if (verbose || !result.success) { if (result.output) { const indented = result.output .split("\n") .map((line) => ` ${line}`) .join("\n"); console.log(indented); } } else if (result.changed) { console.log(" Files formatted"); } } async function main() { const args = process.argv.slice(2); const check = args.includes("--check"); const dryRun = args.includes("--dry-run") || check; const verbose = args.includes("--verbose") || args.includes("-v"); const paths = args.filter((arg) => !arg.startsWith("-")); const targetPath = resolve(paths[0] || "."); // Detect if target is a file or directory let isFile = false; try { const stat = statSync(targetPath); isFile = stat.isFile(); } catch { console.error(`Error: Path not found: ${targetPath}`); process.exit(1); } const mode = check ? "check" : dryRun ? "dry-run" : "format"; console.log(`\x1b[34mMarkdown Formatter\x1b[0m (${mode} mode)`); console.log(`Target: ${targetPath}\n`); const results: FormatResult[] = []; // Step 1: Format tables console.log("Running formatters...\n"); results.push(await formatTables(targetPath, dryRun)); printResult(results[results.length - 1], verbose); // Step 2: Format XML tags results.push(await formatXmlTags(targetPath, dryRun)); printResult(results[results.length - 1], verbose); // Step 3: Run markdownlint results.push(await runMarkdownlint(targetPath, dryRun, isFile)); printResult(results[results.length - 1], verbose); // Summary console.log(""); const failures = results.filter((r) => !r.success); const changes = results.filter((r) => r.changed); if (failures.length > 0) { console.log(`\x1b[31m✗ ${failures.length} step(s) failed\x1b[0m`); process.exit(1); } else if (check && changes.length > 0) { console.log(`\x1b[33m⚠ ${changes.length} step(s) would make changes\x1b[0m`); console.log("Run without --check to apply fixes"); process.exit(1); } else if (changes.length > 0) { console.log(`\x1b[32m✓ Formatting complete (${changes.length} step(s) made changes)\x1b[0m`); } else { console.log("\x1b[32m✓ All files formatted correctly\x1b[0m"); } } main().catch((err) => { console.error(err); process.exit(1); });