UNPKG

aiwg

Version:

Cognitive architecture for AI-augmented software development with structured memory, ensemble validation, and closed-loop correction. FAIR-aligned artifacts, 84% cost reduction via human-in-the-loop, standards adopted by 100+ organizations.

302 lines (240 loc) 8.23 kB
/** * Git Hook Installer * * Installs and manages git pre-commit and pre-push hooks for AIWG validation. */ import { readFile, writeFile, chmod, access, mkdir } from 'fs/promises'; import { existsSync } from 'fs'; import { resolve } from 'path'; import { constants } from 'fs'; export type HookType = 'pre-commit' | 'pre-push'; export interface HookInstallOptions { force?: boolean; // Overwrite existing hook append?: boolean; // Append to existing hook configPath?: string; // Path to .aiwgrc.json } /** * Git Hook Installer */ export class GitHookInstaller { private projectRoot: string; private hooksDir: string; constructor(projectRoot: string) { this.projectRoot = projectRoot; this.hooksDir = resolve(projectRoot, '.git', 'hooks'); } /** * Install pre-commit hook */ async installPreCommitHook(options: HookInstallOptions = {}): Promise<void> { const hookContent = this.generatePreCommitHook(options.configPath); await this.installHook('pre-commit', hookContent, options); } /** * Install pre-push hook */ async installPrePushHook(options: HookInstallOptions = {}): Promise<void> { const hookContent = this.generatePrePushHook(options.configPath); await this.installHook('pre-push', hookContent, options); } /** * Uninstall hooks */ async uninstallHooks(projectRoot: string): Promise<void> { const hooksToRemove: HookType[] = ['pre-commit', 'pre-push']; for (const hookType of hooksToRemove) { const hookPath = resolve(projectRoot, '.git', 'hooks', hookType); if (existsSync(hookPath)) { const content = await readFile(hookPath, 'utf-8'); // Only remove if it's our hook if (content.includes('# AIWG')) { // Check if there's other content const lines = content.split('\n'); const aiwgStart = lines.findIndex(l => l.includes('# AIWG')); const aiwgEnd = lines.findIndex((l, i) => i > aiwgStart && l.includes('# END AIWG')); if (aiwgStart >= 0 && aiwgEnd >= 0) { // Remove only AIWG section const beforeAiwg = lines.slice(0, aiwgStart); const afterAiwg = lines.slice(aiwgEnd + 1); // Check if the content before AIWG is just shebang (part of our hook) const beforeContent = beforeAiwg.filter(l => l.trim().length > 0); const isOnlyShebang = beforeContent.length <= 1 && beforeContent.every(l => l.startsWith('#!')); // Check if after AIWG is meaningful content const afterContent = afterAiwg.filter(l => l.trim().length > 0); if (isOnlyShebang && afterContent.length === 0) { // File only contains our hook (shebang + AIWG), remove entirely await this.removeFile(hookPath); } else { // There's other content, keep it const newContent = [...beforeAiwg, ...afterAiwg].join('\n'); if (newContent.trim().length > 0) { await writeFile(hookPath, newContent, 'utf-8'); } else { // File would be empty, remove it await this.removeFile(hookPath); } } } else { // Entire file is AIWG hook, remove it await this.removeFile(hookPath); } } } } } /** * Check if hook is installed */ isInstalled(hookType: HookType): boolean { const hookPath = resolve(this.hooksDir, hookType); if (!existsSync(hookPath)) { return false; } try { const content = require('fs').readFileSync(hookPath, 'utf-8'); return content.includes('# AIWG'); } catch { return false; } } /** * Get hook content */ async getHookContent(hookType: HookType): Promise<string | null> { const hookPath = resolve(this.hooksDir, hookType); if (!existsSync(hookPath)) { return null; } return await readFile(hookPath, 'utf-8'); } /** * Verify git repository */ isGitRepository(): boolean { return existsSync(resolve(this.projectRoot, '.git')); } // Private methods private async installHook( hookType: HookType, content: string, options: HookInstallOptions ): Promise<void> { // Verify git repository if (!this.isGitRepository()) { throw new Error('Not a git repository'); } // Ensure hooks directory exists if (!existsSync(this.hooksDir)) { await mkdir(this.hooksDir, { recursive: true }); } const hookPath = resolve(this.hooksDir, hookType); // Check if hook already exists if (existsSync(hookPath) && !options.force && !options.append) { throw new Error(`Hook ${hookType} already exists. Use --force to overwrite or --append to add to existing hook.`); } let finalContent = content; if (options.append && existsSync(hookPath)) { // Append to existing hook const existing = await readFile(hookPath, 'utf-8'); // Check if AIWG section already exists if (existing.includes('# AIWG')) { throw new Error(`AIWG hook already exists in ${hookType}. Use --force to overwrite.`); } finalContent = `${existing}\n\n${content}`; } // Write hook file await writeFile(hookPath, finalContent, 'utf-8'); // Make executable await chmod(hookPath, 0o755); } private generatePreCommitHook(configPath?: string): string { const configArg = configPath ? ` --config ${configPath}` : ''; return `#!/bin/sh # AIWG pre-commit hook # Validates staged markdown and text files before commit echo "Running AIWG validation..." # Get staged files STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\\.(md|txt)$' || true) if [ -z "$STAGED_FILES" ]; then echo "No markdown or text files to validate" exit 0 fi # Run AIWG workflow node tools/cli/aiwg.mjs workflow $STAGED_FILES${configArg} RESULT=$? if [ $RESULT -ne 0 ]; then echo "" echo "AIWG validation failed. Please fix issues before committing." echo "To bypass this check, use: git commit --no-verify" exit 1 fi echo "AIWG validation passed" exit 0 # END AIWG `; } private generatePrePushHook(configPath?: string): string { const configArg = configPath ? ` --config ${configPath}` : ''; return `#!/bin/sh # AIWG pre-push hook # Validates all markdown and text files before push echo "Running AIWG validation before push..." # Find all markdown and text files FILES=$(find . -type f \\( -name "*.md" -o -name "*.txt" \\) -not -path "*/node_modules/*" -not -path "*/.git/*" || true) if [ -z "$FILES" ]; then echo "No markdown or text files to validate" exit 0 fi # Run AIWG workflow node tools/cli/aiwg.mjs workflow $FILES${configArg} RESULT=$? if [ $RESULT -ne 0 ]; then echo "" echo "AIWG validation failed. Please fix issues before pushing." echo "To bypass this check, use: git push --no-verify" exit 1 fi echo "AIWG validation passed" exit 0 # END AIWG `; } private async removeFile(path: string): Promise<void> { const fs = await import('fs/promises'); await fs.unlink(path); } /** * List all installed hooks */ async listInstalledHooks(): Promise<HookType[]> { const installed: HookType[] = []; const hookTypes: HookType[] = ['pre-commit', 'pre-push']; for (const hookType of hookTypes) { if (this.isInstalled(hookType)) { installed.push(hookType); } } return installed; } /** * Validate hook installation */ async validateHook(hookType: HookType): Promise<{ valid: boolean; error?: string }> { const hookPath = resolve(this.hooksDir, hookType); if (!existsSync(hookPath)) { return { valid: false, error: 'Hook file does not exist' }; } try { // Check if executable await access(hookPath, constants.X_OK); } catch { return { valid: false, error: 'Hook file is not executable' }; } // Check if contains AIWG marker const content = await readFile(hookPath, 'utf-8'); if (!content.includes('# AIWG')) { return { valid: false, error: 'Hook does not contain AIWG marker' }; } return { valid: true }; } }