UNPKG

aios-core

Version:

Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework

352 lines (308 loc) 10.7 kB
const diffLib = require('diff'); const chalk = require('chalk'); const yaml = require('js-yaml'); /** * Generates visual diffs for component modifications */ class DiffGenerator { constructor() { this.colors = { added: chalk.green, removed: chalk.red, unchanged: chalk.gray, header: chalk.cyan, lineNumber: chalk.yellow }; } /** * Generate a unified diff between two text contents * @param {string} originalContent - Original file content * @param {string} modifiedContent - Modified file content * @param {string} fileName - Name of the file being diffed * @param {Object} options - Diff options * @returns {string} Formatted diff output */ generateUnifiedDiff(originalContent, modifiedContent, fileName, options = {}) { const { contextLines = 3, showLineNumbers = true, colorize = true } = options; const patch = diffLib.createPatch( fileName, originalContent, modifiedContent, 'Current Version', 'Modified Version', { context: contextLines } ); if (!colorize) { return patch; } return this.colorizeDiff(patch, showLineNumbers); } /** * Generate a diff specifically for YAML content * @param {string} originalYaml - Original YAML content * @param {string} modifiedYaml - Modified YAML content * @param {string} componentName - Name of the component * @returns {Object} Structured diff with sections */ generateYamlDiff(originalYaml, modifiedYaml, componentName) { const original = yaml.load(originalYaml); const modified = yaml.load(modifiedYaml); const diff = { component: componentName, sections: {}, summary: { added: [], removed: [], modified: [] } }; // Compare top-level keys const allKeys = new Set([...Object.keys(original), ...Object.keys(modified)]); for (const key of allKeys) { if (!original.hasOwnProperty(key)) { diff.sections[key] = { status: 'added', value: modified[key] }; diff.summary.added.push(key); } else if (!modified.hasOwnProperty(key)) { diff.sections[key] = { status: 'removed', value: original[key] }; diff.summary.removed.push(key); } else if (JSON.stringify(original[key]) !== JSON.stringify(modified[key])) { diff.sections[key] = { status: 'modified', original: original[key], modified: modified[key], changes: this.compareValues(original[key], modified[key]) }; diff.summary.modified.push(key); } } return diff; } /** * Generate a structured diff for agents * @param {string} originalContent - Original agent content * @param {string} modifiedContent - Modified agent content * @param {string} agentName - Name of the agent * @returns {Object} Structured agent diff */ generateAgentDiff(originalContent, modifiedContent, agentName) { // Split content into YAML and markdown sections const originalParts = this.splitAgentContent(originalContent); const modifiedParts = this.splitAgentContent(modifiedContent); const yamlDiff = this.generateYamlDiff( originalParts.yaml, modifiedParts.yaml, agentName ); const markdownDiff = this.generateUnifiedDiff( originalParts.markdown, modifiedParts.markdown, `${agentName}.md`, { contextLines: 5 } ); return { agent: agentName, yamlChanges: yamlDiff, markdownChanges: markdownDiff, impactSummary: this.generateImpactSummary(yamlDiff) }; } /** * Generate a visual diff summary * @param {Object} diff - Structured diff object * @returns {string} Formatted summary */ generateDiffSummary(diff) { const lines = []; lines.push(this.colors.header('=== Modification Summary ===')); lines.push(''); if (diff.summary) { if (diff.summary.added.length > 0) { lines.push(this.colors.added(`+ Added (${diff.summary.added.length}):`)); diff.summary.added.forEach(item => { lines.push(this.colors.added(` + ${item}`)); }); lines.push(''); } if (diff.summary.modified.length > 0) { lines.push(this.colors.header(`~ Modified (${diff.summary.modified.length}):`)); diff.summary.modified.forEach(item => { lines.push(this.colors.header(` ~ ${item}`)); }); lines.push(''); } if (diff.summary.removed.length > 0) { lines.push(this.colors.removed(`- Removed (${diff.summary.removed.length}):`)); diff.summary.removed.forEach(item => { lines.push(this.colors.removed(` - ${item}`)); }); lines.push(''); } } return lines.join('\n'); } /** * Split agent content into YAML and markdown sections * @private */ splitAgentContent(content) { const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); if (!match) { throw new Error('Invalid agent content format'); } return { yaml: match[1], markdown: match[2] }; } /** * Compare two values and generate change details * @private */ compareValues(original, modified) { const changes = []; if (Array.isArray(original) && Array.isArray(modified)) { const added = modified.filter(item => !original.includes(item)); const removed = original.filter(item => !modified.includes(item)); if (added.length > 0) { changes.push({ type: 'added', items: added }); } if (removed.length > 0) { changes.push({ type: 'removed', items: removed }); } } else if (typeof original === 'object' && typeof modified === 'object') { const allKeys = new Set([...Object.keys(original), ...Object.keys(modified)]); for (const key of allKeys) { if (!original.hasOwnProperty(key)) { changes.push({ type: 'added', key, value: modified[key] }); } else if (!modified.hasOwnProperty(key)) { changes.push({ type: 'removed', key, value: original[key] }); } else if (JSON.stringify(original[key]) !== JSON.stringify(modified[key])) { changes.push({ type: 'modified', key, original: original[key], modified: modified[key] }); } } } else { changes.push({ type: 'value_changed', original, modified }); } return changes; } /** * Colorize a diff patch * @private */ colorizeDiff(patch, showLineNumbers) { const lines = patch.split('\n'); const colorized = []; let lineNumOriginal = 0; let lineNumModified = 0; for (const line of lines) { if (line.startsWith('@@')) { // Parse line numbers from hunk header const match = line.match(/@@ -(\d+),\d+ \+(\d+),\d+ @@/); if (match) { lineNumOriginal = parseInt(match[1]); lineNumModified = parseInt(match[2]); } colorized.push(this.colors.header(line)); } else if (line.startsWith('+')) { const lineNum = showLineNumbers ? `${lineNumModified.toString().padStart(4)}: ` : ''; colorized.push(this.colors.added(`+${lineNum}${line.substring(1)}`)); lineNumModified++; } else if (line.startsWith('-')) { const lineNum = showLineNumbers ? `${lineNumOriginal.toString().padStart(4)}: ` : ''; colorized.push(this.colors.removed(`-${lineNum}${line.substring(1)}`)); lineNumOriginal++; } else if (line.startsWith(' ')) { const lineNum = showLineNumbers ? `${lineNumOriginal.toString().padStart(4)}: ` : ''; colorized.push(this.colors.unchanged(` ${lineNum}${line.substring(1)}`)); lineNumOriginal++; lineNumModified++; } else { colorized.push(this.colors.header(line)); } } return colorized.join('\n'); } /** * Generate impact summary from YAML diff * @private */ generateImpactSummary(yamlDiff) { const impacts = []; // Check for dependency changes if (yamlDiff.sections.dependencies) { const changes = yamlDiff.sections.dependencies.changes || []; for (const change of changes) { if (change.type === 'added') { impacts.push(`New dependency added: ${change.items.join(', ')}`); } else if (change.type === 'removed') { impacts.push(`Dependency removed: ${change.items.join(', ')}`); } } } // Check for command changes if (yamlDiff.sections.commands) { impacts.push('Commands modified - users may need to update their workflows'); } // Check for persona changes if (yamlDiff.sections.persona) { impacts.push('Agent persona modified - behavior may change'); } return impacts; } /** * Generate a side-by-side diff view * @param {string} original - Original content * @param {string} modified - Modified content * @param {Object} options - Display options * @returns {string} Side-by-side diff */ generateSideBySideDiff(original, modified, options = {}) { const { width = 80, gutter = 3 } = options; const columnWidth = Math.floor((width - gutter) / 2); const originalLines = original.split('\n'); const modifiedLines = modified.split('\n'); const maxLines = Math.max(originalLines.length, modifiedLines.length); const output = []; output.push(this.colors.header('─'.repeat(width))); output.push( this.colors.header('Original'.padEnd(columnWidth)) + ' '.repeat(gutter) + this.colors.header('Modified'.padEnd(columnWidth)) ); output.push(this.colors.header('─'.repeat(width))); for (let i = 0; i < maxLines; i++) { const origLine = (originalLines[i] || '').substring(0, columnWidth); const modLine = (modifiedLines[i] || '').substring(0, columnWidth); let coloredOrig = origLine.padEnd(columnWidth); let coloredMod = modLine.padEnd(columnWidth); if (origLine !== modLine) { if (!originalLines[i]) { coloredMod = this.colors.added(coloredMod); } else if (!modifiedLines[i]) { coloredOrig = this.colors.removed(coloredOrig); } else { coloredOrig = this.colors.removed(coloredOrig); coloredMod = this.colors.added(coloredMod); } } output.push(coloredOrig + ' '.repeat(gutter) + coloredMod); } output.push(this.colors.header('─'.repeat(width))); return output.join('\n'); } } module.exports = DiffGenerator;