automagik-genie
Version:
Self-evolving AI agent orchestration framework with Model Context Protocol support
214 lines (189 loc) • 7.38 kB
JavaScript
/**
* Install git hooks - Advanced feature, opt-in only
*
* Usage: node install-hooks.cjs <project-dir> <package-root>
*
* Arguments:
* project-dir - User's project directory (where .git is located)
* package-root - Genie package installation directory (where hooks templates are)
*
* Warning: This modifies your .git/hooks/ directory.
* Only install if you understand what git hooks do.
*
* Hooks installed:
* - pre-commit: Validates commits (worktree access, cross-refs, token efficiency)
* - pre-push: Runs tests and validations before push
* - prepare-commit-msg: Adds Genie co-author attribution
*/
const fs = require('fs');
const path = require('path');
const GREEN = '\x1b[32m';
const RED = '\x1b[31m';
const YELLOW = '\x1b[33m';
const BLUE = '\x1b[34m';
const RESET = '\x1b[0m';
/**
* Install git hooks with proper error handling and user feedback
*/
function installGitHooks() {
console.log(`${BLUE}🧞 Genie Git Hooks Installer${RESET}`);
console.log('');
// Get directories from command-line arguments
const projectDir = process.argv[2] || process.cwd();
const packageRoot = process.argv[3] || __dirname;
const gitDir = path.join(projectDir, '.git');
const hooksSourceDir = path.join(packageRoot, '.genie', 'scripts', 'hooks');
// Check if we're in a git repository
if (!fs.existsSync(gitDir)) {
console.error(`${RED}✗ Error: Not a git repository${RESET}`);
console.log(` Project dir: ${projectDir}`);
console.log(' Run this command from the root of your Genie project.');
process.exit(1);
}
// Check if .git is a file (worktree) or directory (main repo)
const gitDirStats = fs.statSync(gitDir);
let gitHooksDir;
let isWorktree = false;
if (gitDirStats.isFile()) {
// Worktree: read the gitdir path from .git file
isWorktree = true;
const gitDirContent = fs.readFileSync(gitDir, 'utf8');
const match = gitDirContent.match(/gitdir:\s*(.+)/);
if (match) {
const worktreeGitDir = path.resolve(path.dirname(gitDir), match[1].trim());
// For worktrees, hooks are in the main .git/hooks directory
const mainGitDir = worktreeGitDir.replace(/\/worktrees\/[^/]+$/, '');
gitHooksDir = path.join(mainGitDir, 'hooks');
}
} else {
// Main repository
gitHooksDir = path.join(gitDir, 'hooks');
}
if (!gitHooksDir || !fs.existsSync(gitHooksDir)) {
console.error(`${RED}✗ Error: Cannot find .git/hooks directory${RESET}`);
process.exit(1);
}
// Check if hook templates exist
if (!fs.existsSync(hooksSourceDir)) {
console.error(`${RED}✗ Error: Hook templates not found${RESET}`);
console.log(` Expected: ${hooksSourceDir}`);
console.log(` Package root: ${packageRoot}`);
process.exit(1);
}
console.log(`${YELLOW}⚠ Advanced Feature Warning${RESET}`);
console.log('');
console.log(' Git hooks will modify your commit/push workflow:');
console.log(' - pre-commit: Validates worktree access, cross-refs, token usage');
console.log(' - pre-push: Runs tests before pushing');
console.log(' - prepare-commit-msg: Adds Genie co-author attribution');
console.log('');
console.log(` Hooks will be installed to: ${gitHooksDir}`);
if (isWorktree) {
console.log(` ${YELLOW}Note: You're in a worktree - hooks install to main repo${RESET}`);
}
console.log('');
// Define hooks to install
const hooks = [
{ name: 'pre-commit', extension: '.cjs', runtime: 'node' },
{ name: 'pre-push', extension: '.cjs', runtime: 'node' },
{ name: 'prepare-commit-msg', extension: '', runtime: 'python3' }
];
let installed = 0;
let skipped = 0;
let errors = [];
for (const hook of hooks) {
const source = path.join(hooksSourceDir, hook.name + hook.extension);
const dest = path.join(gitHooksDir, hook.name);
if (!fs.existsSync(source)) {
console.log(`${YELLOW}⊘${RESET} Skipping ${hook.name} (template not found)`);
skipped++;
continue;
}
try {
// Check if hook already exists
if (fs.existsSync(dest)) {
// Read existing hook to see if it's our wrapper
const existingContent = fs.readFileSync(dest, 'utf8');
if (existingContent.includes(source)) {
console.log(`${BLUE}↻${RESET} ${hook.name} (already installed, updating)`);
} else {
console.log(`${YELLOW}⚠${RESET} ${hook.name} (existing hook found, overwriting)`);
}
}
// Create relative symlink (portable across all systems)
// Symlinks work in:
// - Linux ✅
// - macOS ✅
// - Windows 10+ with Git for Windows (symlink support) ✅
// - GitHub Actions ✅
// - WSL ✅
// Relative path: .git/hooks → ../../.genie/scripts/hooks/<name>.<ext>
const relativePath = path.relative(gitHooksDir, source);
// Remove existing hook (file or symlink)
if (fs.existsSync(dest)) {
fs.unlinkSync(dest);
} else {
try {
// Try to remove if it's a broken symlink
if (fs.lstatSync(dest).isSymbolicLink()) {
fs.unlinkSync(dest);
}
} catch {
// Ignore - file doesn't exist
}
}
// Create relative symlink
try {
fs.symlinkSync(relativePath, dest);
fs.chmodSync(dest, 0o755);
} catch (err) {
// Fallback for Windows without symlink support: use wrapper script
if (err.code === 'EACCES' || err.code === 'EPERM') {
console.warn(`${YELLOW}⚠ Symlinks not supported, using wrapper script${RESET}`);
const wrapper = `#!/bin/sh\nexec node "$(dirname "$0")/${relativePath}" "$@"\n`;
fs.writeFileSync(dest, wrapper, { mode: 0o755 });
} else {
throw err;
}
}
console.log(`${GREEN}✓${RESET} ${hook.name} installed`);
installed++;
} catch (err) {
console.error(`${RED}✗${RESET} ${hook.name} failed: ${err.message}`);
errors.push({ hook: hook.name, error: err.message });
}
}
console.log('');
console.log(`${GREEN}═══════════════════════════════════════${RESET}`);
console.log(`${GREEN}Results:${RESET}`);
console.log(` ${GREEN}✓${RESET} Installed: ${installed}`);
if (skipped > 0) {
console.log(` ${YELLOW}⊘${RESET} Skipped: ${skipped}`);
}
if (errors.length > 0) {
console.log(` ${RED}✗${RESET} Errors: ${errors.length}`);
}
console.log('');
if (errors.length > 0) {
console.error(`${RED}Errors encountered:${RESET}`);
errors.forEach(e => console.error(` - ${e.hook}: ${e.error}`));
console.log('');
process.exit(1);
}
if (installed > 0) {
console.log(`${GREEN}✓ Hooks installed successfully!${RESET}`);
console.log('');
console.log(`${BLUE}Next steps:${RESET}`);
console.log(' - Hooks will now run automatically on commit/push');
console.log(' - To bypass hooks temporarily: git commit --no-verify');
console.log(' - To disable co-author: export GENIE_DISABLE_COAUTHOR=1');
console.log(' - To skip tests on push: export GENIE_SKIP_TESTS=1');
console.log('');
} else {
console.log(`${YELLOW}⚠ No hooks were installed${RESET}`);
process.exit(1);
}
}
// Run the installer
installGitHooks();