orchestrix
Version:
Orchestrix - Universal AI Agent Framework for Coordinated AI-Driven Development
240 lines (207 loc) ⢠7.51 kB
JavaScript
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 Orchestrix
* 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 <file1> [file2] ...');
process.exit(1);
}
let hasErrors = false;
let hasChanges = false;
let 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 };