UNPKG

context-forge

Version:

AI orchestration platform with autonomous teams, enhancement planning, migration tools, 25+ slash commands, checkpoints & hooks. Multi-IDE: Claude, Cursor, Windsurf, Cline, Copilot

357 lines (355 loc) 12.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.GitDisciplineService = void 0; const events_1 = require("events"); const child_process_1 = require("child_process"); const util_1 = require("util"); const path_1 = __importDefault(require("path")); const fs_extra_1 = __importDefault(require("fs-extra")); const chalk_1 = __importDefault(require("chalk")); const handlebars_1 = __importDefault(require("handlebars")); const execAsync = (0, util_1.promisify)(child_process_1.exec); class GitDisciplineService extends events_1.EventEmitter { constructor(config, projectPath) { super(); this.stats = { totalCommits: 0, commitsByAgent: {}, averageCommitInterval: 0, branchesCreated: 0, tagsCreated: 0, complianceRate: 100, }; this.lastCommitTimes = new Map(); this.autoCommitIntervals = new Map(); this.config = config; this.projectPath = projectPath; // Load commit template const templatePath = path_1.default.join(__dirname, '../../templates/orchestration/git-commit.hbs'); const templateContent = fs_extra_1.default.readFileSync(templatePath, 'utf-8'); this.commitTemplate = handlebars_1.default.compile(templateContent); } /** * Initialize git discipline for the project */ async initialize() { console.log(chalk_1.default.blue('Initializing git discipline...')); // Ensure git is initialized await this.ensureGitRepo(); // Set up git config await this.configureGit(); // Create hooks directory await this.setupGitHooks(); console.log(chalk_1.default.green('Git discipline initialized')); } /** * Ensure project is a git repository */ async ensureGitRepo() { const gitPath = path_1.default.join(this.projectPath, '.git'); if (!(await fs_extra_1.default.pathExists(gitPath))) { console.log(chalk_1.default.yellow('Initializing git repository...')); await execAsync('git init', { cwd: this.projectPath }); } } /** * Configure git settings */ async configureGit() { try { // Set up user info if not configured await execAsync('git config user.name "Context Forge Orchestrator"', { cwd: this.projectPath, }); await execAsync('git config user.email "orchestrator@context-forge.ai"', { cwd: this.projectPath, }); } catch { // Ignore if already configured } } /** * Set up git hooks */ async setupGitHooks() { const hooksPath = path_1.default.join(this.projectPath, '.git', 'hooks'); await fs_extra_1.default.ensureDir(hooksPath); if (this.config.requireTests) { // Create pre-commit hook for test validation const preCommitHook = `#!/bin/bash # Pre-commit hook to ensure tests pass echo "Running tests before commit..." npm test if [ $? -ne 0 ]; then echo "Tests failed! Commit aborted." exit 1 fi echo "Tests passed. Proceeding with commit." exit 0 `; const hookPath = path_1.default.join(hooksPath, 'pre-commit'); await fs_extra_1.default.writeFile(hookPath, preCommitHook); await fs_extra_1.default.chmod(hookPath, 0o755); } } /** * Start auto-commit for an agent */ startAutoCommit(agentId, agentRole, sessionId) { if (!this.config.enabled) { return; } // Clear existing interval this.stopAutoCommit(agentId); // Set up new interval const interval = setInterval(async () => { await this.performAutoCommit(agentId, agentRole, sessionId); }, this.config.autoCommitInterval * 60 * 1000); this.autoCommitIntervals.set(agentId, interval); console.log(chalk_1.default.green(`Auto-commit enabled for ${agentId} (every ${this.config.autoCommitInterval} minutes)`)); } /** * Stop auto-commit for an agent */ stopAutoCommit(agentId) { const interval = this.autoCommitIntervals.get(agentId); if (interval) { clearInterval(interval); this.autoCommitIntervals.delete(agentId); } } /** * Perform auto-commit */ async performAutoCommit(agentId, agentRole, sessionId) { try { // Check for changes const { stdout: statusOutput } = await execAsync('git status --porcelain', { cwd: this.projectPath, }); if (!statusOutput.trim()) { console.log(chalk_1.default.gray(`No changes to commit for ${agentId}`)); return; } // Get list of changes const changes = statusOutput .trim() .split('\n') .map((line) => line.substring(3)); // Remove status prefix // Stage all changes await execAsync('git add -A', { cwd: this.projectPath }); // Create commit message const commitInfo = { agentId, agentRole, sessionId, changes: changes.slice(0, 5), // Limit to 5 items }; const commitMessage = this.generateCommitMessage(commitInfo); // Commit await execAsync(`git commit -m "${commitMessage}"`, { cwd: this.projectPath, }); // Update stats this.updateCommitStats(agentId); console.log(chalk_1.default.green(`✓ Auto-commit completed for ${agentId}`)); // Emit event this.emit('commit', { agentId, timestamp: new Date(), changes: changes.length, }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); if (!errorMessage.includes('nothing to commit')) { console.error(chalk_1.default.red(`Auto-commit failed for ${agentId}: ${errorMessage}`)); this.emit('commit-failed', { agentId, error: errorMessage }); } } } /** * Generate commit message from template */ generateCommitMessage(info) { const context = { commitType: 'Progress', taskDescription: `Auto-commit by ${info.agentId}`, agentId: info.agentId, agentRole: info.agentRole, sessionId: info.sessionId, timestamp: new Date().toISOString(), commitInterval: this.config.autoCommitInterval, changes: info.changes, tests: info.tests, blockers: info.blockers, nextSteps: info.nextSteps, }; // Use custom format if provided if (this.config.commitMessageFormat) { return this.config.commitMessageFormat .replace('$TASK', context.taskDescription) .replace('$DESCRIPTION', info.changes.join(', ')) .replace('$AGENT', info.agentId) .replace('$TIMESTAMP', context.timestamp); } return this.commitTemplate(context); } /** * Create feature branch */ async createFeatureBranch(branchName, agentId) { const sanitizedName = branchName .toLowerCase() .replace(/[^a-z0-9-]/g, '-') .replace(/-+/g, '-'); const fullBranchName = `feature/${sanitizedName}`; try { await execAsync(`git checkout -b ${fullBranchName}`, { cwd: this.projectPath, }); this.stats.branchesCreated++; console.log(chalk_1.default.green(`Created branch: ${fullBranchName}`)); this.emit('branch-created', { branch: fullBranchName, agentId, timestamp: new Date(), }); return fullBranchName; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes('already exists')) { // Switch to existing branch await execAsync(`git checkout ${fullBranchName}`, { cwd: this.projectPath, }); return fullBranchName; } throw error; } } /** * Tag stable version */ async tagStableVersion(tagName, message, agentId) { if (!this.config.tagStrategy) { return; } const tagPrefix = this.config.tagStrategy === 'stable' ? 'stable' : 'v'; const fullTag = `${tagPrefix}-${tagName}-${Date.now()}`; try { await execAsync(`git tag -a ${fullTag} -m "${message}"`, { cwd: this.projectPath }); this.stats.tagsCreated++; console.log(chalk_1.default.green(`Created tag: ${fullTag}`)); this.emit('tag-created', { tag: fullTag, agentId, timestamp: new Date(), }); } catch (error) { console.error(chalk_1.default.red(`Failed to create tag: ${error}`)); } } /** * Check compliance with commit interval */ checkCompliance(agentId) { const lastCommit = this.lastCommitTimes.get(agentId); if (!lastCommit) { return true; // No commits yet } const timeSinceLastCommit = Date.now() - lastCommit.getTime(); const maxInterval = this.config.autoCommitInterval * 60 * 1000 * 1.5; // 50% grace period return timeSinceLastCommit <= maxInterval; } /** * Update commit statistics */ updateCommitStats(agentId) { this.stats.totalCommits++; this.stats.commitsByAgent[agentId] = (this.stats.commitsByAgent[agentId] || 0) + 1; const now = new Date(); const lastCommit = this.lastCommitTimes.get(agentId); if (lastCommit) { const interval = (now.getTime() - lastCommit.getTime()) / (60 * 1000); this.updateAverageInterval(interval); } this.lastCommitTimes.set(agentId, now); this.stats.lastCommitTime = now; } /** * Update average commit interval */ updateAverageInterval(interval) { const totalIntervals = this.stats.totalCommits - 1; if (totalIntervals > 0) { this.stats.averageCommitInterval = (this.stats.averageCommitInterval * (totalIntervals - 1) + interval) / totalIntervals; } } /** * Get git statistics */ getStats() { // Calculate compliance rate let compliantAgents = 0; let totalAgents = 0; for (const [agentId] of this.autoCommitIntervals) { totalAgents++; if (this.checkCompliance(agentId)) { compliantAgents++; } } this.stats.complianceRate = totalAgents > 0 ? (compliantAgents / totalAgents) * 100 : 100; return { ...this.stats }; } /** * Get recent commits */ async getRecentCommits(limit = 10) { try { const { stdout } = await execAsync(`git log --oneline -${limit} --pretty=format:'%h|%s|%an|%ar'`, { cwd: this.projectPath }); return stdout .trim() .split('\n') .map((line) => { const [hash, message, author, time] = line.split('|'); return { hash, message, author, time }; }); } catch { return []; } } /** * Get current branch */ async getCurrentBranch() { try { const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd: this.projectPath, }); return stdout.trim(); } catch { return 'main'; } } /** * Clean up resources */ cleanup() { // Stop all auto-commit intervals for (const [agentId] of this.autoCommitIntervals) { this.stopAutoCommit(agentId); } } } exports.GitDisciplineService = GitDisciplineService; //# sourceMappingURL=gitDiscipline.js.map