@aaswe/codebase-ai
Version:
AI-Assisted Software Engineering (AASWE) - Rich codebase context for IDE LLMs
496 lines (478 loc) âĸ 19.3 kB
JavaScript
"use strict";
/**
* Git Hooks Manager
*
* Manages Git hooks for TTL file versioning and automatic re-analysis
* when TTL files are modified by developers.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GitHooksManager = void 0;
const events_1 = require("events");
const promises_1 = require("fs/promises");
const fs_1 = require("fs");
const path_1 = require("path");
const simple_git_1 = __importDefault(require("simple-git"));
const logger_1 = __importDefault(require("../../utils/logger"));
/**
* Git Hooks Manager for TTL File Versioning
*/
class GitHooksManager extends events_1.EventEmitter {
config;
git;
isInitialized = false;
constructor(config) {
super();
this.config = config;
this.git = (0, simple_git_1.default)(config.projectRoot);
}
/**
* Initialize Git hooks for TTL file monitoring
*/
async initialize() {
if (this.isInitialized) {
return;
}
try {
logger_1.default.info('đ§ Initializing Git hooks for TTL file versioning...');
// Check if we're in a Git repository
const isRepo = await this.git.checkIsRepo();
if (!isRepo) {
logger_1.default.warn('â ī¸ Not a Git repository - Git hooks will not be installed');
return;
}
// Create .git/hooks directory if it doesn't exist
const hooksDir = (0, path_1.join)(this.config.projectRoot, '.git', 'hooks');
await (0, promises_1.mkdir)(hooksDir, { recursive: true });
// Install hooks based on configuration
if (this.config.enablePreCommitHook) {
await this.installPreCommitHook(hooksDir);
}
if (this.config.enablePostCommitHook) {
await this.installPostCommitHook(hooksDir);
}
if (this.config.enablePostMergeHook) {
await this.installPostMergeHook(hooksDir);
}
this.isInitialized = true;
logger_1.default.info('â
Git hooks initialized successfully');
}
catch (error) {
logger_1.default.error('â Failed to initialize Git hooks', { error });
throw error;
}
}
/**
* Install pre-commit hook to validate TTL files before commit
*/
async installPreCommitHook(hooksDir) {
const hookPath = (0, path_1.join)(hooksDir, 'pre-commit');
const hookScript = `#!/bin/sh
# AASWE Pre-commit Hook - TTL File Validation
# Auto-generated by AASWE Git Hooks Manager
echo "đ AASWE: Checking TTL files before commit..."
# Get staged TTL files
STAGED_TTL_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E "\\.(ttl|module-knowledge\\.ttl)$" || true)
if [ -n "$STAGED_TTL_FILES" ]; then
echo "đ Found staged TTL files:"
echo "$STAGED_TTL_FILES"
# Validate TTL files
node -e "
const { GitHooksManager } = require('./dist/services/git-integration/GitHooksManager');
const manager = new GitHooksManager({
projectRoot: process.cwd(),
ttlDirectories: ['./knowledge', './clean-knowledge', './.aaswe/knowledge'],
ttlPatterns: ['**/*.ttl', '**/*.module-knowledge.ttl'],
enablePreCommitHook: true,
enablePostCommitHook: true,
enablePostMergeHook: true,
autoReanalyzeOnTTLChange: true
});
manager.validateStagedTTLFiles().then(valid => {
if (!valid) {
console.error('â TTL file validation failed');
process.exit(1);
}
console.log('â
TTL files validated successfully');
}).catch(err => {
console.error('â TTL validation error:', err.message);
process.exit(1);
});
"
if [ $? -ne 0 ]; then
echo "â Pre-commit hook failed - TTL file validation errors"
exit 1
fi
fi
echo "â
AASWE: Pre-commit checks passed"
exit 0
`;
await (0, promises_1.writeFile)(hookPath, hookScript);
await (0, promises_1.chmod)(hookPath, 0o755);
logger_1.default.info('â
Pre-commit hook installed');
}
/**
* Install post-commit hook to trigger re-analysis after TTL changes
*/
async installPostCommitHook(hooksDir) {
const hookPath = (0, path_1.join)(hooksDir, 'post-commit');
const hookScript = `#!/bin/sh
# AASWE Post-commit Hook - TTL File Re-analysis
# Auto-generated by AASWE Git Hooks Manager
echo "đ AASWE: Checking for TTL file changes in commit..."
# Get TTL files changed in the last commit
CHANGED_TTL_FILES=$(git diff-tree --no-commit-id --name-only -r HEAD | grep -E "\\.(ttl|module-knowledge\\.ttl)$" || true)
if [ -n "$CHANGED_TTL_FILES" ]; then
echo "đ TTL files changed in commit:"
echo "$CHANGED_TTL_FILES"
# Trigger re-analysis in background
echo "đ Triggering automatic re-analysis..."
nohup node -e "
const { GitHooksManager } = require('./dist/services/git-integration/GitHooksManager');
const manager = new GitHooksManager({
projectRoot: process.cwd(),
ttlDirectories: ['./knowledge', './clean-knowledge', './.aaswe/knowledge'],
ttlPatterns: ['**/*.ttl', '**/*.module-knowledge.ttl'],
enablePreCommitHook: true,
enablePostCommitHook: true,
enablePostMergeHook: true,
autoReanalyzeOnTTLChange: true
});
manager.handlePostCommitReanalysis().catch(console.error);
" > /dev/null 2>&1 &
echo "â
AASWE: Re-analysis triggered in background"
else
echo "âšī¸ AASWE: No TTL files changed in this commit"
fi
exit 0
`;
await (0, promises_1.writeFile)(hookPath, hookScript);
await (0, promises_1.chmod)(hookPath, 0o755);
logger_1.default.info('â
Post-commit hook installed');
}
/**
* Install post-merge hook to handle TTL conflicts and re-analysis
*/
async installPostMergeHook(hooksDir) {
const hookPath = (0, path_1.join)(hooksDir, 'post-merge');
const hookScript = `#!/bin/sh
# AASWE Post-merge Hook - TTL File Conflict Resolution
# Auto-generated by AASWE Git Hooks Manager
echo "đ AASWE: Checking for TTL file changes after merge..."
# Check if merge brought in TTL file changes
MERGED_TTL_FILES=$(git diff-tree --no-commit-id --name-only -r HEAD~1..HEAD | grep -E "\\.(ttl|module-knowledge\\.ttl)$" || true)
if [ -n "$MERGED_TTL_FILES" ]; then
echo "đ TTL files affected by merge:"
echo "$MERGED_TTL_FILES"
# Trigger conflict resolution and re-analysis
echo "đ§ Resolving TTL conflicts and triggering re-analysis..."
node -e "
const { GitHooksManager } = require('./dist/services/git-integration/GitHooksManager');
const manager = new GitHooksManager({
projectRoot: process.cwd(),
ttlDirectories: ['./knowledge', './clean-knowledge', './.aaswe/knowledge'],
ttlPatterns: ['**/*.ttl', '**/*.module-knowledge.ttl'],
enablePreCommitHook: true,
enablePostCommitHook: true,
enablePostMergeHook: true,
autoReanalyzeOnTTLChange: true
});
manager.handlePostMergeReanalysis().catch(console.error);
"
echo "â
AASWE: Post-merge processing completed"
else
echo "âšī¸ AASWE: No TTL files affected by merge"
fi
exit 0
`;
await (0, promises_1.writeFile)(hookPath, hookScript);
await (0, promises_1.chmod)(hookPath, 0o755);
logger_1.default.info('â
Post-merge hook installed');
}
/**
* Validate staged TTL files before commit
*/
async validateStagedTTLFiles() {
try {
const stagedFiles = await this.git.diff(['--cached', '--name-only']);
const ttlFiles = stagedFiles.split('\n').filter(file => file.match(/\.(ttl|module-knowledge\.ttl)$/));
if (ttlFiles.length === 0) {
return true; // No TTL files to validate
}
logger_1.default.info(`đ Validating ${ttlFiles.length} staged TTL files...`);
for (const filePath of ttlFiles) {
const fullPath = (0, path_1.join)(this.config.projectRoot, filePath);
if (!(0, fs_1.existsSync)(fullPath)) {
continue; // File was deleted
}
try {
const content = await (0, promises_1.readFile)(fullPath, 'utf-8');
const isValid = await this.validateTTLContent(content, filePath);
if (!isValid) {
logger_1.default.error(`â Invalid TTL file: ${filePath}`);
return false;
}
}
catch (error) {
logger_1.default.error(`â Error validating TTL file ${filePath}:`, error);
return false;
}
}
logger_1.default.info('â
All staged TTL files are valid');
return true;
}
catch (error) {
logger_1.default.error('â Failed to validate staged TTL files', { error });
return false;
}
}
/**
* Handle post-commit re-analysis
*/
async handlePostCommitReanalysis() {
try {
const commitHash = await this.git.revparse(['HEAD']);
const changedFiles = await this.getChangedTTLFiles(commitHash);
if (changedFiles.length === 0) {
return;
}
logger_1.default.info(`đ Post-commit: Re-analyzing ${changedFiles.length} TTL files...`);
const event = {
type: 'post-commit',
ttlChanges: changedFiles,
timestamp: new Date(),
commitHash
};
this.emit('git-hook-event', event);
if (this.config.autoReanalyzeOnTTLChange) {
await this.triggerReanalysis(changedFiles);
}
}
catch (error) {
logger_1.default.error('â Post-commit re-analysis failed', { error });
}
}
/**
* Handle post-merge re-analysis and conflict resolution
*/
async handlePostMergeReanalysis() {
try {
const commitHash = await this.git.revparse(['HEAD']);
const changedFiles = await this.getChangedTTLFiles(commitHash, 'HEAD~1..HEAD');
if (changedFiles.length === 0) {
return;
}
logger_1.default.info(`đ Post-merge: Processing ${changedFiles.length} TTL files...`);
// Check for merge conflicts in TTL files
const conflictedFiles = await this.detectTTLConflicts(changedFiles);
if (conflictedFiles.length > 0) {
logger_1.default.warn(`â ī¸ Detected ${conflictedFiles.length} TTL files with potential conflicts`);
await this.resolveTTLConflicts(conflictedFiles);
}
const event = {
type: 'post-merge',
ttlChanges: changedFiles,
timestamp: new Date(),
commitHash
};
this.emit('git-hook-event', event);
if (this.config.autoReanalyzeOnTTLChange) {
await this.triggerReanalysis(changedFiles);
}
}
catch (error) {
logger_1.default.error('â Post-merge re-analysis failed', { error });
}
}
/**
* Get changed TTL files from a commit
*/
async getChangedTTLFiles(commitHash, range) {
try {
const diffArgs = range ? [range] : ['--name-status', `${commitHash}~1..${commitHash}`];
const diff = await this.git.diff(diffArgs);
const changes = [];
const lines = diff.split('\n').filter(line => line.trim());
for (const line of lines) {
const [status, filePath] = line.split('\t');
if (filePath && filePath.match(/\.(ttl|module-knowledge\.ttl)$/)) {
let changeType;
switch (status) {
case 'A':
changeType = 'added';
break;
case 'D':
changeType = 'deleted';
break;
default:
changeType = 'modified';
}
changes.push({
filePath,
changeType,
timestamp: new Date(),
commitHash
});
}
}
return changes;
}
catch (error) {
logger_1.default.error('â Failed to get changed TTL files', { error });
return [];
}
}
/**
* Validate TTL content
*/
async validateTTLContent(content, filePath) {
try {
// Basic TTL syntax validation
const lines = content.split('\n');
let hasValidTriples = false;
for (const line of lines) {
const trimmed = line.trim();
// Skip empty lines and comments
if (!trimmed || trimmed.startsWith('#'))
continue;
// Check for basic triple structure
if (trimmed.includes(' ') && (trimmed.endsWith('.') || trimmed.endsWith(';'))) {
hasValidTriples = true;
}
else if (trimmed.length > 0) {
logger_1.default.warn(`â ī¸ Potential syntax issue in ${filePath}: ${trimmed}`);
}
}
if (!hasValidTriples) {
logger_1.default.error(`â No valid triples found in ${filePath}`);
return false;
}
return true;
}
catch (error) {
logger_1.default.error(`â TTL validation error for ${filePath}:`, error);
return false;
}
}
/**
* Detect TTL conflicts after merge
*/
async detectTTLConflicts(changedFiles) {
const conflictedFiles = [];
for (const change of changedFiles) {
const fullPath = (0, path_1.join)(this.config.projectRoot, change.filePath);
if (!(0, fs_1.existsSync)(fullPath))
continue;
try {
const content = await (0, promises_1.readFile)(fullPath, 'utf-8');
// Check for Git conflict markers
if (content.includes('<<<<<<<') || content.includes('>>>>>>>') || content.includes('=======')) {
conflictedFiles.push(change.filePath);
}
}
catch (error) {
logger_1.default.error(`â Error checking conflicts in ${change.filePath}:`, error);
}
}
return conflictedFiles;
}
/**
* Resolve TTL conflicts by triggering re-analysis
*/
async resolveTTLConflicts(conflictedFiles) {
logger_1.default.info(`đ§ Resolving conflicts in ${conflictedFiles.length} TTL files...`);
// For now, log the conflicts and suggest manual resolution
// In a full implementation, this could trigger automatic re-analysis
// to regenerate clean TTL files
for (const filePath of conflictedFiles) {
logger_1.default.warn(`â ī¸ Manual resolution required for: ${filePath}`);
logger_1.default.info(`đĄ Consider running: aaswe analyze --output ${filePath.split('/')[0]}`);
}
}
/**
* Trigger re-analysis for changed TTL files
*/
async triggerReanalysis(changedFiles) {
try {
logger_1.default.info(`đ Triggering re-analysis for ${changedFiles.length} changed TTL files...`);
// Skip Git operations during re-analysis to prevent SSH prompts
logger_1.default.info('âšī¸ Skipping Git-triggered re-analysis to prevent SSH authentication prompts');
logger_1.default.info('đĄ TTL files have been updated - manual re-analysis can be triggered with: codebase-ai analyze');
}
catch (error) {
logger_1.default.error('â Re-analysis failed', { error });
}
}
/**
* Uninstall Git hooks
*/
async uninstall() {
try {
const hooksDir = (0, path_1.join)(this.config.projectRoot, '.git', 'hooks');
const hooks = ['pre-commit', 'post-commit', 'post-merge'];
for (const hook of hooks) {
const hookPath = (0, path_1.join)(hooksDir, hook);
if ((0, fs_1.existsSync)(hookPath)) {
try {
const content = await (0, promises_1.readFile)(hookPath, 'utf-8');
// Only remove if it's our hook
if (content.includes('AASWE Git Hooks Manager')) {
await (0, promises_1.writeFile)(hookPath + '.backup', content);
// Remove the hook file or replace with backup if it existed
logger_1.default.info(`â
Uninstalled ${hook} hook (backup created)`);
}
}
catch (error) {
logger_1.default.warn(`â ī¸ Could not uninstall ${hook} hook:`, error);
}
}
}
logger_1.default.info('â
Git hooks uninstalled successfully');
}
catch (error) {
logger_1.default.error('â Failed to uninstall Git hooks', { error });
}
}
/**
* Get Git hooks status
*/
async getStatus() {
try {
const hooksDir = (0, path_1.join)(this.config.projectRoot, '.git', 'hooks');
const hooks = ['pre-commit', 'post-commit', 'post-merge'];
const status = {
isGitRepo: await this.git.checkIsRepo(),
hooksInstalled: {},
lastActivity: null
};
for (const hook of hooks) {
const hookPath = (0, path_1.join)(hooksDir, hook);
if ((0, fs_1.existsSync)(hookPath)) {
const stats = await (0, promises_1.stat)(hookPath);
const content = await (0, promises_1.readFile)(hookPath, 'utf-8');
status.hooksInstalled[hook] = {
exists: true,
isAASWE: content.includes('AASWE Git Hooks Manager'),
lastModified: stats.mtime,
executable: (stats.mode & 0o111) !== 0
};
}
else {
status.hooksInstalled[hook] = {
exists: false,
isAASWE: false
};
}
}
return status;
}
catch (error) {
logger_1.default.error('â Failed to get Git hooks status', { error });
return { error: error instanceof Error ? error.message : 'Unknown error' };
}
}
}
exports.GitHooksManager = GitHooksManager;
//# sourceMappingURL=GitHooksManager.js.map