UNPKG

aios-core

Version:

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

462 lines (398 loc) 11.9 kB
const { exec } = require('child_process'); const { promisify } = require('util'); const execAsync = promisify(exec); const _path = require('path'); const _fs = require('fs').promises; const chalk = require('chalk'); /** * Git operations wrapper for AIOS framework modifications */ class GitWrapper { constructor(options = {}) { this.rootPath = options.rootPath || process.cwd(); this.gitPath = options.gitPath || 'git'; this.defaultBranch = options.defaultBranch || 'main'; this.metaAgentPrefix = options.metaAgentPrefix || 'meta-agent/'; } /** * Execute a git command * @private */ async execGit(command, options = {}) { try { const { stdout, stderr } = await execAsync(`${this.gitPath} ${command}`, { cwd: this.rootPath, ...options }); if (stderr && !options.ignoreStderr) { console.warn(chalk.yellow(`Git warning: ${stderr}`)); } return stdout.trim(); } catch (_error) { throw new Error(`Git command failed: ${error.message}`); } } /** * Check if git is initialized * @returns {Promise<boolean>} */ async isGitInitialized() { try { await this.execGit('status'); return true; } catch (_error) { return false; } } /** * Initialize git repository if not already initialized * @returns {Promise<void>} */ async initializeRepository() { const initialized = await this.isGitInitialized(); if (!initialized) { await this.execGit('init'); console.log(chalk.green('✅ Git repository initialized')); } } /** * Get current branch name * @returns {Promise<string>} */ async getCurrentBranch() { return await this.execGit('rev-parse --abbrev-ref HEAD'); } /** * Create a new branch * @param {string} branchName - Name of the branch to create * @param {boolean} checkout - Whether to checkout the branch * @returns {Promise<void>} */ async createBranch(branchName, checkout = true) { try { if (checkout) { await this.execGit(`checkout -b ${branchName}`); } else { await this.execGit(`branch ${branchName}`); } console.log(chalk.green(`✅ Created branch: ${branchName}`)); } catch (_error) { // Branch might already exist if (error.message.includes('already exists')) { console.log(chalk.yellow(`Branch already exists: ${branchName}`)); if (checkout) { await this.checkoutBranch(branchName); } } else { throw error; } } } /** * Checkout an existing branch * @param {string} branchName - Name of the branch to checkout * @returns {Promise<void>} */ async checkoutBranch(branchName) { await this.execGit(`checkout ${branchName}`); console.log(chalk.green(`✅ Checked out branch: ${branchName}`)); } /** * Create a branch for meta-agent modifications * @param {string} modificationName - Name of the modification * @returns {Promise<string>} Branch name */ async createModificationBranch(modificationName) { const timestamp = new Date().toISOString().substring(0, 10); const branchName = `${this.metaAgentPrefix}${modificationName}-${timestamp}`; await this.createBranch(branchName); return branchName; } /** * Stage files for commit * @param {Array<string>} files - Files to stage * @returns {Promise<void>} */ async stageFiles(files) { if (!Array.isArray(files) || files.length === 0) { throw new Error('No files provided to stage'); } for (const file of files) { await this.execGit(`add "${file}"`); } console.log(chalk.green(`✅ Staged ${files.length} files`)); } /** * Commit changes with message * @param {string} message - Commit message * @param {Object} options - Commit options * @returns {Promise<string>} Commit hash */ async commit(message, options = {}) { const { author = 'aios-developer <aios-developer@aios-fullstack.local>', signoff = true } = options; let command = `commit -m "${message.replace(/"/g, '\\"')}"`; if (author) { command += ` --author="${author}"`; } if (signoff) { command += ' --signoff'; } const output = await this.execGit(command); const hashMatch = output.match(/\[[\w-]+ ([\w]+)\]/); const commitHash = hashMatch ? hashMatch[1] : 'unknown'; console.log(chalk.green(`✅ Committed: ${commitHash}`)); return commitHash; } /** * Create a commit for component modifications * @param {Array<string>} files - Files to commit * @param {string} message - Commit message * @param {Object} metadata - Additional metadata * @returns {Promise<string>} Commit hash */ async commitModification(files, message, metadata = {}) { await this.stageFiles(files); // Add metadata to commit message let fullMessage = message; if (metadata.componentType && metadata.componentName) { fullMessage = `${metadata.componentType}(${metadata.componentName}): ${message}`; } if (metadata.breakingChange) { fullMessage += '\n\nBREAKING CHANGE: ' + metadata.breakingChange; } if (metadata.approvedBy) { fullMessage += `\n\nApproved-by: ${metadata.approvedBy}`; } fullMessage += '\n\nGenerated by: aios-developer meta-agent'; return await this.commit(fullMessage); } /** * Get git status * @returns {Promise<Object>} Status information */ async getStatus() { const porcelainStatus = await this.execGit('status --porcelain'); const branch = await this.getCurrentBranch(); const files = { modified: [], added: [], deleted: [], untracked: [] }; if (porcelainStatus) { const lines = porcelainStatus.split('\n'); for (const line of lines) { if (!line) continue; const status = line.substring(0, 2); const filename = line.substring(3); if (status.includes('M')) files.modified.push(filename); else if (status.includes('A')) files.added.push(filename); else if (status.includes('D')) files.deleted.push(filename); else if (status === '??') files.untracked.push(filename); } } return { branch, clean: porcelainStatus === '', files }; } /** * Get commit history * @param {number} limit - Number of commits to retrieve * @returns {Promise<Array>} Commit history */ async getHistory(limit = 10) { const format = '%H|%an|%ae|%at|%s'; const output = await this.execGit(`log -${limit} --format="${format}"`); if (!output) return []; return output.split('\n').map(line => { const [hash, author, email, timestamp, subject] = line.split('|'); return { hash, author, email, date: new Date(parseInt(timestamp) * 1000), subject }; }); } /** * Check for conflicts * @returns {Promise<Array>} List of conflicted files */ async getConflicts() { try { const output = await this.execGit('diff --name-only --diff-filter=U'); return output ? output.split('\n').filter(Boolean) : []; } catch (_error) { return []; } } /** * Merge a branch * @param {string} branchName - Branch to merge * @param {Object} options - Merge options * @returns {Promise<Object>} Merge result */ async mergeBranch(branchName, options = {}) { const { strategy = 'recursive', message = null, noFastForward = true } = options; let command = `merge ${branchName}`; if (strategy) { command += ` --strategy=${strategy}`; } if (noFastForward) { command += ' --no-ff'; } if (message) { command += ` -m "${message.replace(/"/g, '\\"')}"`; } try { const output = await this.execGit(command); return { success: true, message: output }; } catch (_error) { // Check for conflicts const conflicts = await this.getConflicts(); if (conflicts.length > 0) { return { success: false, conflicts, error: 'Merge conflicts detected' }; } throw error; } } /** * Create a tag * @param {string} tagName - Name of the tag * @param {string} message - Tag message * @returns {Promise<void>} */ async createTag(tagName, message) { await this.execGit(`tag -a ${tagName} -m "${message.replace(/"/g, '\\"')}"`); console.log(chalk.green(`✅ Created tag: ${tagName}`)); } /** * Push changes to remote * @param {string} remote - Remote name * @param {string} branch - Branch name * @param {Object} options - Push options * @returns {Promise<void>} */ async push(remote = 'origin', branch = null, options = {}) { const currentBranch = branch || await this.getCurrentBranch(); let command = `push ${remote} ${currentBranch}`; if (options.tags) { command += ' --tags'; } if (options.force) { command += ' --force'; } if (options.setUpstream) { command = `push -u ${remote} ${currentBranch}`; } await this.execGit(command); console.log(chalk.green(`✅ Pushed to ${remote}/${currentBranch}`)); } /** * Get diff between commits or working tree * @param {Object} options - Diff options * @returns {Promise<string>} Diff output */ async getDiff(options = {}) { const { from = 'HEAD', to = null, files = [], nameOnly = false } = options; let command = 'diff'; if (nameOnly) { command += ' --name-only'; } if (to) { command += ` ${from} ${to}`; } else { command += ` ${from}`; } if (files.length > 0) { command += ` -- ${files.join(' ')}`; } return await this.execGit(command); } /** * Stash changes * @param {string} message - Stash message * @returns {Promise<void>} */ async stash(message = 'Meta-agent modifications') { await this.execGit(`stash push -m "${message}"`); console.log(chalk.green('✅ Changes stashed')); } /** * Apply stash * @param {string} stashRef - Stash reference * @returns {Promise<void>} */ async stashApply(stashRef = 'stash@{0}') { await this.execGit(`stash apply ${stashRef}`); console.log(chalk.green('✅ Stash applied')); } /** * Get remote information * @returns {Promise<Array>} Remote information */ async getRemotes() { const output = await this.execGit('remote -v'); if (!output) return []; const remotes = {}; output.split('\n').forEach(line => { const [name, url, type] = line.split(/\s+/); if (!remotes[name]) { remotes[name] = {}; } remotes[name][type.replace(/[()]/g, '')] = url; }); return Object.entries(remotes).map(([name, urls]) => ({ name, fetchUrl: urls.fetch, pushUrl: urls.push })); } /** * Generate commit message for modifications * @param {Object} modification - Modification details * @returns {string} Generated commit message */ generateCommitMessage(modification) { const { action, componentType, _componentName, summary, details = [], breakingChanges = [] } = modification; let message = `${action}(${componentType}): ${summary}`; if (details.length > 0) { message += '\n\n' + details.map(d => `- ${d}`).join('\n'); } if (breakingChanges.length > 0) { message += '\n\nBREAKING CHANGES:\n' + breakingChanges.map(bc => `- ${bc}`).join('\n'); } return message; } } module.exports = GitWrapper;