#!/usr/bin/env node const fs = require('fs') const path = require('path') const yaml = require('js-yaml') const { execSync } = require('child_process') // Dynamic import for ES module let chalk // Initialize ES modules async function initializeModules () { if (!chalk) { chalk = (await import('chalk')).default } } /** * YAML Formatter and Linter for BMad-Method * Formats and validates YAML files and YAML embedded in Markdown */ async function formatYamlContent (content, filename) { await initializeModules() try { // First try to fix common YAML issues let fixedContent = content // Fix "commands :" -> "commands:" .replace(/^(\s*)(\w+)\s+:/gm, '$1$2:') // Fix inconsistent list indentation .replace(/^(\s*)-\s{3,}/gm, '$1- ') // Skip auto-fixing for .roomodes files - they have special nested structure if (!filename.includes('.roomodes')) { fixedContent = fixedContent // Fix unquoted list items that contain special characters or multiple parts .replace(/^(\s*)-\s+(.*)$/gm, (match, indent, content) => { // Skip if already quoted if (content.startsWith('"') && content.endsWith('"')) { return match } // If the content contains special YAML characters or looks complex, quote it // BUT skip if it looks like a proper YAML key-value pair (like "key: value") if ((content.includes(':') || content.includes('-') || content.includes('{') || content.includes('}')) && !content.match(/^\w+:\s/)) { // Remove any existing quotes first, escape internal quotes, then add proper quotes const cleanContent = content.replace(/^["']|["']$/g, '').replace(/"/g, '\\"') return `${indent}- "${cleanContent}"` } return match }) } // Debug: show what we're trying to parse if (fixedContent !== content) { console.log(chalk.blue(`šŸ”§ Applied YAML fixes to ${filename}`)) } // Parse and re-dump YAML to format it const parsed = yaml.load(fixedContent) const formatted = yaml.dump(parsed, { indent: 2, lineWidth: -1, // Disable line wrapping noRefs: true, sortKeys: false // Preserve key order }) return formatted } catch (error) { console.error(chalk.red(`āŒ YAML syntax error in ${filename}:`), error.message) console.error(chalk.yellow('šŸ’” Try manually fixing the YAML structure first')) return null } } async function processMarkdownFile (filePath) { await initializeModules() const content = fs.readFileSync(filePath, 'utf8') let modified = false let newContent = content // Fix untyped code blocks by adding 'text' type // Match ``` at start of line followed by newline, but only if it's an opening fence newContent = newContent.replace(/^```\n([\s\S]*?)\n```$/gm, '```text\n$1\n```') if (newContent !== content) { modified = true console.log(chalk.blue(`šŸ”§ Added 'text' type to untyped code blocks in ${filePath}`)) } // Find YAML code blocks const yamlBlockRegex = /```ya?ml\n([\s\S]*?)\n```/g let match const replacements = [] while ((match = yamlBlockRegex.exec(newContent)) !== null) { const [fullMatch, yamlContent] = match const formatted = await formatYamlContent(yamlContent, filePath) if (formatted !== null) { // Remove trailing newline that js-yaml adds const trimmedFormatted = formatted.replace(/\n$/, '') if (trimmedFormatted !== yamlContent) { modified = true console.log(chalk.green(`āœ“ Formatted YAML in ${filePath}`)) } replacements.push({ start: match.index, end: match.index + fullMatch.length, replacement: `\`\`\`yaml\n${trimmedFormatted}\n\`\`\`` }) } } // Apply replacements in reverse order to maintain indices for (let i = replacements.length - 1; i >= 0; i--) { const { start, end, replacement } = replacements[i] newContent = newContent.slice(0, start) + replacement + newContent.slice(end) } if (modified) { fs.writeFileSync(filePath, newContent) return true } return false } async function processYamlFile (filePath) { await initializeModules() const content = fs.readFileSync(filePath, 'utf8') const formatted = await formatYamlContent(content, filePath) if (formatted === null) { return false // Syntax error } if (formatted !== content) { fs.writeFileSync(filePath, formatted) return true } return false } async function lintYamlFile (filePath) { await initializeModules() try { // Use yaml-lint for additional validation execSync(`npx yaml-lint "${filePath}"`, { stdio: 'pipe' }) return true } catch (error) { console.error(chalk.red(`āŒ YAML lint error in ${filePath}:`)) console.error(error.stdout?.toString() || error.message) return false } } async function main () { await initializeModules() const args = process.argv.slice(2) const glob = require('glob') if (args.length === 0) { console.error('Usage: node yaml-format.js [file2] ...') process.exit(1) } let hasErrors = false let hasChanges = false const filesProcessed = [] // Expand glob patterns and collect all files const allFiles = [] for (const arg of args) { if (arg.includes('*')) { // It's a glob pattern const matches = glob.sync(arg) allFiles.push(...matches) } else { // It's a direct file path allFiles.push(arg) } } for (const filePath of allFiles) { if (!fs.existsSync(filePath)) { // Skip silently for glob patterns that don't match anything if (!args.some(arg => arg.includes('*') && filePath === arg)) { console.error(chalk.red(`āŒ File not found: ${filePath}`)) hasErrors = true } continue } const ext = path.extname(filePath).toLowerCase() const basename = path.basename(filePath).toLowerCase() try { let changed = false if (ext === '.md') { changed = await processMarkdownFile(filePath) } else if (ext === '.yaml' || ext === '.yml' || basename.includes('roomodes') || basename.includes('.yaml') || basename.includes('.yml')) { // Handle YAML files and special cases like .roomodes changed = await processYamlFile(filePath) // Also run linting const lintPassed = await lintYamlFile(filePath) if (!lintPassed) hasErrors = true } else { // Skip silently for unsupported files continue } if (changed) { hasChanges = true filesProcessed.push(filePath) } } catch (error) { console.error(chalk.red(`āŒ Error processing ${filePath}:`), error.message) hasErrors = true } } if (hasChanges) { console.log(chalk.green(`\n✨ YAML formatting completed! Modified ${filesProcessed.length} files:`)) filesProcessed.forEach(file => console.log(chalk.blue(` šŸ“ ${file}`))) } if (hasErrors) { console.error(chalk.red('\nšŸ’„ Some files had errors. Please fix them before committing.')) process.exit(1) } } if (require.main === module) { main().catch(error => { console.error('Error:', error) process.exit(1) }) } module.exports = { formatYamlContent, processMarkdownFile, processYamlFile }