aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
250 lines (236 loc) • 8.52 kB
JavaScript
/**
* 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';
/**
* Git Hook Installer
*/
export class GitHookInstaller {
projectRoot;
hooksDir;
constructor(projectRoot) {
this.projectRoot = projectRoot;
this.hooksDir = resolve(projectRoot, '.git', 'hooks');
}
/**
* Install pre-commit hook
*/
async installPreCommitHook(options = {}) {
const hookContent = this.generatePreCommitHook(options.configPath);
await this.installHook('pre-commit', hookContent, options);
}
/**
* Install pre-push hook
*/
async installPrePushHook(options = {}) {
const hookContent = this.generatePrePushHook(options.configPath);
await this.installHook('pre-push', hookContent, options);
}
/**
* Uninstall hooks
*/
async uninstallHooks(projectRoot) {
const hooksToRemove = ['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) {
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) {
const hookPath = resolve(this.hooksDir, hookType);
if (!existsSync(hookPath)) {
return null;
}
return await readFile(hookPath, 'utf-8');
}
/**
* Verify git repository
*/
isGitRepository() {
return existsSync(resolve(this.projectRoot, '.git'));
}
// Private methods
async installHook(hookType, content, options) {
// 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);
}
generatePreCommitHook(configPath) {
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
`;
}
generatePrePushHook(configPath) {
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
`;
}
async removeFile(path) {
const fs = await import('fs/promises');
await fs.unlink(path);
}
/**
* List all installed hooks
*/
async listInstalledHooks() {
const installed = [];
const hookTypes = ['pre-commit', 'pre-push'];
for (const hookType of hookTypes) {
if (this.isInstalled(hookType)) {
installed.push(hookType);
}
}
return installed;
}
/**
* Validate hook installation
*/
async validateHook(hookType) {
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 };
}
}
//# sourceMappingURL=git-hooks.js.map