UNPKG

@ace-sdk/cli

Version:

ACE CLI - Command-line tool for intelligent pattern learning and playbook management

311 lines 9.01 kB
/** * Git Context Service for AI-Trail Pattern Correlation * * Detects git context from the current working directory for * correlating execution traces with commits. * * @package @ace-sdk/cli * @since v2.2.0 * @see https://github.com/ce-dot-net/ace-sdk/issues/16 */ import { execSync } from 'child_process'; /** * Service for detecting git context from the current working directory. * * Provides graceful fallback when not in a git repository. * All git commands use child_process.execSync for consistency * with existing patterns in the codebase. */ export class GitContextService { cwd; cachedContext = null; cacheTimestamp = 0; cacheTtlMs; /** * Create a new GitContextService * * @param cwd Working directory (defaults to process.cwd()) * @param cacheTtlMs Cache TTL in milliseconds (defaults to 60000 = 1 minute) */ constructor(cwd, cacheTtlMs = 60000) { this.cwd = cwd || process.cwd(); this.cacheTtlMs = cacheTtlMs; } /** * Check if current directory is a git repository */ isGitRepository() { try { execSync('git rev-parse --git-dir', { cwd: this.cwd, stdio: 'pipe', encoding: 'utf8' }); return true; } catch { return false; } } /** * Get current HEAD commit hash */ getCurrentHead() { try { return execSync('git rev-parse HEAD', { cwd: this.cwd, stdio: 'pipe', encoding: 'utf8' }).trim(); } catch { return null; } } /** * Get current branch name */ getBranch() { try { return execSync('git rev-parse --abbrev-ref HEAD', { cwd: this.cwd, stdio: 'pipe', encoding: 'utf8' }).trim(); } catch { return null; } } /** * Get commit message for a given commit (first line) */ getCommitMessage(commitHash) { try { const ref = commitHash || 'HEAD'; return execSync(`git log -1 --format=%s ${ref}`, { cwd: this.cwd, stdio: 'pipe', encoding: 'utf8' }).trim(); } catch { return null; } } /** * Get commit timestamp in ISO 8601 format */ getCommitTimestamp(commitHash) { try { const ref = commitHash || 'HEAD'; return execSync(`git log -1 --format=%aI ${ref}`, { cwd: this.cwd, stdio: 'pipe', encoding: 'utf8' }).trim(); } catch { return null; } } /** * Get commit author name */ getCommitAuthor(commitHash) { try { const ref = commitHash || 'HEAD'; return execSync(`git log -1 --format=%an ${ref}`, { cwd: this.cwd, stdio: 'pipe', encoding: 'utf8' }).trim(); } catch { return null; } } /** * Get commit author email */ getCommitAuthorEmail(commitHash) { try { const ref = commitHash || 'HEAD'; return execSync(`git log -1 --format=%ae ${ref}`, { cwd: this.cwd, stdio: 'pipe', encoding: 'utf8' }).trim(); } catch { return null; } } /** * Get files changed in a commit */ getChangedFiles(commitHash) { try { const ref = commitHash || 'HEAD'; const output = execSync(`git diff-tree --no-commit-id --name-only -r ${ref}`, { cwd: this.cwd, stdio: 'pipe', encoding: 'utf8' }).trim(); return output ? output.split('\n').filter(Boolean) : []; } catch { return []; } } /** * Get parent commit hashes */ getParentCommits(commitHash) { try { const ref = commitHash || 'HEAD'; const output = execSync(`git log -1 --format=%P ${ref}`, { cwd: this.cwd, stdio: 'pipe', encoding: 'utf8' }).trim(); return output ? output.split(' ').filter(Boolean) : []; } catch { return []; } } /** * Get insertions and deletions for a commit */ getCommitStats(commitHash) { try { const ref = commitHash || 'HEAD'; const output = execSync(`git diff --stat --numstat ${ref}~1..${ref}`, { cwd: this.cwd, stdio: 'pipe', encoding: 'utf8' }).trim(); let insertions = 0; let deletions = 0; // Parse numstat output: each line is "insertions deletions filename" for (const line of output.split('\n')) { const parts = line.split('\t'); if (parts.length >= 2) { const ins = parseInt(parts[0], 10); const del = parseInt(parts[1], 10); if (!isNaN(ins)) insertions += ins; if (!isNaN(del)) deletions += del; } } return { insertions, deletions }; } catch { return null; } } /** * Detect full git context from current working directory. * * Returns null if not in a git repository. * Caches result for cacheTtlMs to avoid repeated git calls. * * @param commitHash Optional specific commit (defaults to HEAD) * @param includePrivate Include author email (default: false for privacy) */ detectContext(commitHash, includePrivate = false) { // Check cache const now = Date.now(); if (this.cachedContext && (now - this.cacheTimestamp) < this.cacheTtlMs) { return this.cachedContext; } // Check if git repo if (!this.isGitRepository()) { return null; } // Get basic info const hash = commitHash || this.getCurrentHead(); const branch = this.getBranch(); if (!hash || !branch) { return null; } // Build context const context = { commit_hash: hash, branch, files_changed: this.getChangedFiles(hash), commit_message: this.getCommitMessage(hash) || undefined, timestamp: this.getCommitTimestamp(hash) || undefined, author: this.getCommitAuthor(hash) || undefined, parent_commits: this.getParentCommits(hash) }; // Include email only if explicitly requested if (includePrivate) { context.author_email = this.getCommitAuthorEmail(hash) || undefined; } // Get stats (may fail for initial commit) const stats = this.getCommitStats(hash); if (stats) { context.insertions = stats.insertions; context.deletions = stats.deletions; } // Cache result this.cachedContext = context; this.cacheTimestamp = now; return context; } /** * Get commits made since a specific time. * Useful for finding commits made during a session. * * @param since Date to search from * @param limit Maximum commits to return (default: 10) */ getCommitsSince(since, limit = 10) { if (!this.isGitRepository()) { return []; } try { const sinceIso = since.toISOString(); const output = execSync(`git log --since="${sinceIso}" --format=%H -n ${limit}`, { cwd: this.cwd, stdio: 'pipe', encoding: 'utf8' }).trim(); if (!output) { return []; } const hashes = output.split('\n').filter(Boolean); return hashes .map(hash => this.detectContext(hash)) .filter((ctx) => ctx !== null); } catch { return []; } } /** * Clear the cached context */ clearCache() { this.cachedContext = null; this.cacheTimestamp = 0; } } /** * Create a GitContextService for the current directory */ export function createGitContextService(cwd) { return new GitContextService(cwd); } /** * Convenience function to detect git context from cwd. * Returns null if not in a git repository. */ export function detectGitContext(cwd) { const service = new GitContextService(cwd); return service.detectContext(); } //# sourceMappingURL=git-context.js.map