mirror of
https://github.com/bmad-code-org/BMAD-METHOD.git
synced 2026-09-19 08:11:52 +08:00
513f440a23
* refactor(installer): restructure installer with clean separation of concerns Move tools/cli/ to tools/installer/ with major structural cleanup: - InstallPaths async factory for path resolution and directory creation - Config value object (frozen) replaces mutable config bag - ExistingInstall value object replaces stateful Detector class - OfficialModules + CustomModules + ExternalModuleManager replace monolithic ModuleManager - install() is prompt-free; all user interaction in ui.js - Update state returned explicitly instead of mutating customConfig - Delete dead code: dependency-resolver, _base-ide, IdeConfigManager, platform-codes helpers, npx wrapper, xml-utils - Flatten directory structure: custom/handler → custom-handler, tools/cli/ → tools/installer/, lib/ directories removed - Update all path references in package.json, tests, CI, and docs * fix(installer): guard ExistingInstall.version and surface module.yaml errors Guard ExistingInstall.version access with .installed check in uninstall.js, ui.js, and installer.js to prevent throwing on empty/partial _bmad dirs. Surface invalid module.yaml parse errors as warnings instead of silently returning empty results.
246 lines
7.5 KiB
JavaScript
Executable File
246 lines
7.5 KiB
JavaScript
Executable File
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const yaml = require('yaml');
|
|
const { execSync } = require('node: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:"
|
|
.replaceAll(/^(\s*)(\w+)\s+:/gm, '$1$2:')
|
|
// Fix inconsistent list indentation
|
|
.replaceAll(/^(\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
|
|
.replaceAll(/^(\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('}')) &&
|
|
!/^\w+:\s/.test(content)
|
|
) {
|
|
// Remove any existing quotes first, escape internal quotes, then add proper quotes
|
|
const cleanContent = content.replaceAll(/^["']|["']$/g, '').replaceAll('"', String.raw`\"`);
|
|
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.parse(fixedContent);
|
|
const formatted = yaml.stringify(parsed, {
|
|
indent: 2,
|
|
lineWidth: 0, // Disable line wrapping
|
|
sortKeys: false, // Preserve key order
|
|
});
|
|
// Ensure POSIX-compliant final newline
|
|
return formatted.endsWith('\n') ? formatted : formatted + '\n';
|
|
} 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.replaceAll(/^```\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) {
|
|
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 index = replacements.length - 1; index >= 0; index--) {
|
|
const { start, end, replacement } = replacements[index];
|
|
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 arguments_ = process.argv.slice(2);
|
|
const glob = require('glob');
|
|
|
|
if (arguments_.length === 0) {
|
|
console.error('Usage: node yaml-format.js <file1> [file2] ...');
|
|
process.exit(1);
|
|
}
|
|
|
|
let hasErrors = false;
|
|
let hasChanges = false;
|
|
let filesProcessed = [];
|
|
|
|
// Expand glob patterns and collect all files
|
|
const allFiles = [];
|
|
for (const argument of arguments_) {
|
|
if (argument.includes('*')) {
|
|
// It's a glob pattern
|
|
const matches = glob.sync(argument);
|
|
allFiles.push(...matches);
|
|
} else {
|
|
// It's a direct file path
|
|
allFiles.push(argument);
|
|
}
|
|
}
|
|
|
|
for (const filePath of allFiles) {
|
|
if (!fs.existsSync(filePath)) {
|
|
// Skip silently for glob patterns that don't match anything
|
|
if (!arguments_.some((argument) => argument.includes('*') && filePath === argument)) {
|
|
console.error(chalk.red(`❌ File not found: ${filePath}`));
|
|
hasErrors = true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const extension = path.extname(filePath).toLowerCase();
|
|
const basename = path.basename(filePath).toLowerCase();
|
|
|
|
try {
|
|
let changed = false;
|
|
if (extension === '.md') {
|
|
changed = await processMarkdownFile(filePath);
|
|
} else if (
|
|
extension === '.yaml' ||
|
|
extension === '.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:`));
|
|
for (const file of filesProcessed) 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 };
|