vibe-codex
Version:
CLI tool to install development rules and git hooks with interactive configuration
392 lines (325 loc) • 10.5 kB
JavaScript
/**
* Git hooks installer
*/
const fs = require("fs-extra");
const path = require("path");
const chalk = require("chalk");
const logger = require("../utils/logger");
async function installGitHooks(config, advancedHooksConfig = null) {
try {
const hooksDir = path.join(".git", "hooks");
// Ensure hooks directory exists
await fs.ensureDir(hooksDir);
// Backup existing hooks
await backupExistingHooks(hooksDir);
// Generate hook scripts locally
logger.info("Generating hook scripts...");
// Determine which hooks to install based on enabled modules
const hooksToInstall = getRequiredHooks(config);
// Install each hook
for (const hookName of hooksToInstall) {
await installHook(hookName, hooksDir, config);
}
// Install advanced hooks if requested
if (advancedHooksConfig && advancedHooksConfig.enabled) {
await installAdvancedHooks(hooksDir, advancedHooksConfig);
}
} catch (error) {
logger.error("Failed to install git hooks:", error.message);
throw error;
}
}
async function backupExistingHooks(hooksDir) {
try {
const backupDir = path.join(hooksDir, ".backup");
await fs.ensureDir(backupDir);
const hooks = await fs.readdir(hooksDir);
for (const hook of hooks) {
if (!hook.includes(".sample") && !hook.startsWith(".")) {
const src = path.join(hooksDir, hook);
const dest = path.join(backupDir, `${hook}.${Date.now()}`);
const stats = await fs.stat(src);
if (stats.isFile()) {
await fs.copy(src, dest);
}
}
}
} catch (error) {
logger.debug("Failed to backup hooks:", error.message);
// Continue installation even if backup fails
}
}
function getRequiredHooks(config) {
const hooks = new Set(["pre-commit"]); // Always install pre-commit
if (config.modules.core?.commitMessageValidation) {
hooks.add("commit-msg");
}
if (config.modules.testing?.enabled) {
hooks.add("pre-push");
}
if (
config.modules.github?.enabled ||
config.modules["github-workflow"]?.enabled
) {
hooks.add("post-commit");
}
// Add hooks for issue update reminders
if (config.issueTracking?.enableReminders !== false) {
hooks.add("post-commit");
if (config.issueTracking?.updateOnPush) {
hooks.add("pre-push");
}
}
return Array.from(hooks);
}
async function installHook(hookName, hooksDir, config) {
try {
const hookContent = generateHookScript(hookName, config);
const hookPath = path.join(hooksDir, hookName);
await fs.writeFile(hookPath, hookContent);
await fs.chmod(hookPath, "755");
logger.info(`✓ Installed ${hookName} hook`);
} catch (error) {
logger.error(`Failed to install ${hookName} hook:`, error.message);
throw error;
}
}
function generateHookScript(hookName, config) {
// Common helper function for finding vibe-codex
const vibeCodexRunner = `
# Function to run vibe-codex with proper fallbacks
run_vibe_codex() {
# Try npx first (most common)
if command -v npx >/dev/null 2>&1; then
npx --no-install vibe-codex "$@"
# Try local installation
elif [ -x "./node_modules/.bin/vibe-codex" ]; then
./node_modules/.bin/vibe-codex "$@"
# Try yarn
elif command -v yarn >/dev/null 2>&1 && [ -f "yarn.lock" ]; then
yarn run vibe-codex "$@"
# Try pnpm
elif command -v pnpm >/dev/null 2>&1 && [ -f "pnpm-lock.yaml" ]; then
pnpm exec vibe-codex "$@"
# Try global installation
elif command -v vibe-codex >/dev/null 2>&1; then
vibe-codex "$@"
else
echo "❌ Error: vibe-codex not found!"
echo ""
echo "Please install vibe-codex using one of:"
echo " npm install --save-dev vibe-codex"
echo " npm install -g vibe-codex"
echo " yarn add --dev vibe-codex"
echo ""
return 1
fi
}
`;
const scripts = {
"pre-commit": `#!/bin/sh
# vibe-codex pre-commit hook
${vibeCodexRunner}
# Skip if SKIP_VIBE_CODEX is set
if [ "$SKIP_VIBE_CODEX" = "1" ] || [ "$SKIP_VIBE_CODEX" = "true" ]; then
echo "⚠️ Skipping vibe-codex checks (SKIP_VIBE_CODEX is set)"
exit 0
fi
# Run vibe-codex validation
echo "🔍 Running vibe-codex pre-commit checks..."
run_vibe_codex validate --hook pre-commit --modules core,patterns
if [ $? -ne 0 ]; then
echo ""
echo "💡 To skip these checks temporarily, use:"
echo " SKIP_VIBE_CODEX=1 git commit ..."
echo " or: git commit --no-verify"
exit 1
fi
# Check for secrets if git-secrets is installed
if command -v git-secrets >/dev/null 2>&1; then
git secrets --pre_commit_hook -- "$@"
fi
# Run additional checks based on configuration
${config.modules.quality?.enabled ? "npm run lint --if-present" : ""}
exit $?
`,
"commit-msg": `#!/bin/sh
# vibe-codex commit-msg hook
${vibeCodexRunner}
# Skip if SKIP_VIBE_CODEX is set
if [ "$SKIP_VIBE_CODEX" = "1" ] || [ "$SKIP_VIBE_CODEX" = "true" ]; then
echo "⚠️ Skipping vibe-codex checks (SKIP_VIBE_CODEX is set)"
exit 0
fi
# Validate commit message format
commit_regex='^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?: .{1,72}$'
commit_msg=$(cat "$1")
if ! echo "$commit_msg" | grep -qE "$commit_regex"; then
echo "❌ Invalid commit message format!"
echo ""
echo "Format: <type>(<scope>): <subject>"
echo ""
echo "Types: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert"
echo ""
echo "Example: feat(auth): add login functionality"
echo ""
echo "💡 To skip this check temporarily, use:"
echo " SKIP_VIBE_CODEX=1 git commit ..."
exit 1
fi
# Run vibe-codex commit-msg validation if available
run_vibe_codex validate --hook commit-msg --message "$1" 2>/dev/null || true
exit 0
`,
"pre-push": `#!/bin/sh
# vibe-codex pre-push hook
${vibeCodexRunner}
# Skip if SKIP_VIBE_CODEX is set
if [ "$SKIP_VIBE_CODEX" = "1" ] || [ "$SKIP_VIBE_CODEX" = "true" ]; then
echo "⚠️ Skipping vibe-codex checks (SKIP_VIBE_CODEX is set)"
exit 0
fi
# Check if pushing to protected branch
protected_branches="main master"
current_branch=$(git symbolic-ref HEAD | sed -e 's,.*/\\(.*\\),\\1,')
for branch in $protected_branches; do
if [ "$current_branch" = "$branch" ]; then
echo "❌ Direct push to $branch branch is not allowed!"
echo "Please create a feature branch and submit a pull request."
exit 1
fi
done
# Check for open PRs and ensure comments are reviewed
if command -v gh >/dev/null 2>&1; then
echo "📋 Checking for open PRs that need review..."
pr_number=$(gh pr list --head "$current_branch" --json number --jq '.[0].number' 2>/dev/null)
if [ -n "$pr_number" ]; then
# Check if PR has unresolved review comments
unresolved_count=$(gh api "repos/{owner}/{repo}/pulls/$pr_number/comments" --jq '[.[] | select(.in_reply_to_id == null)] | length' 2>/dev/null || echo 0)
if [ "$unresolved_count" -gt "0" ]; then
echo "⚠️ Warning: PR #$pr_number has $unresolved_count unresolved review comments"
echo ""
echo "Please review and address all PR comments before pushing."
echo "Run: gh pr view $pr_number --comments"
echo ""
read -p "Continue anyway? (y/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
fi
fi
fi
# Check for issue update reminders on push
${
config.issueTracking?.updateOnPush
? `
# Check if issues need updates before pushing
run_vibe_codex check-issue-updates --hook pre-push 2>/dev/null || true
`
: ""
}
# Run tests if testing module is enabled
${
config.modules.testing?.enabled
? `
echo "🧪 Running tests before push..."
if command -v npm >/dev/null 2>&1 && [ -f "package.json" ]; then
npm test --if-present
if [ $? -ne 0 ]; then
echo "❌ Tests failed. Push aborted."
exit 1
fi
fi
`
: ""
}
# Run full validation
echo "🔍 Running vibe-codex validation..."
run_vibe_codex validate --hook pre-push
if [ $? -ne 0 ]; then
echo ""
echo "💡 To skip these checks temporarily, use:"
echo " SKIP_VIBE_CODEX=1 git push ..."
exit 1
fi
exit 0
`,
"post-commit": `#!/bin/sh
# vibe-codex post-commit hook
${vibeCodexRunner}
# Skip if SKIP_VIBE_CODEX is set
if [ "$SKIP_VIBE_CODEX" = "1" ] || [ "$SKIP_VIBE_CODEX" = "true" ]; then
exit 0
fi
# Update issue status if GitHub module is enabled
${
config.modules.github?.enabled || config.modules["github-workflow"]?.enabled
? `
# Try to run issue progress tracking
run_vibe_codex track-progress --hook post-commit 2>/dev/null || true
`
: ""
}
# Check for issue update reminders
${
config.issueTracking?.enableReminders !== false
? `
# Run issue update reminder check
run_vibe_codex check-issue-updates --hook post-commit 2>/dev/null || true
`
: ""
}
exit 0
`,
};
return (
scripts[hookName] ||
`#!/bin/sh
# vibe-codex ${hookName} hook
# Custom hook - add your logic here
exit 0
`
);
}
/**
* Install advanced hooks based on selected categories
*/
async function installAdvancedHooks(hooksDir, advancedHooksConfig) {
const { getHooksForCategories } = require("../config/advanced-hooks");
const hooks = getHooksForCategories(advancedHooksConfig.categories);
logger.info(`Installing ${hooks.length} advanced hooks...`);
for (const hook of hooks) {
const hookPath = path.join(hooksDir, hook.type);
const sourcePath = path.join(__dirname, "../../hooks", hook.file);
// Check if source file exists
if (!(await fs.pathExists(sourcePath))) {
logger.warn(`Advanced hook file not found: ${hook.file}`);
continue;
}
// If hook already exists, append to it
if (await fs.pathExists(hookPath)) {
logger.debug(`Appending to existing ${hook.type} hook...`);
// Read existing content
const existingContent = await fs.readFile(hookPath, "utf-8");
// Read new hook content
const newContent = await fs.readFile(sourcePath, "utf-8");
// Create combined hook
const combinedContent = `${existingContent}
# Advanced hook: ${hook.description}
# Source: ${hook.file}
${newContent.replace("#!/bin/sh", "").replace("#!/bin/bash", "")}`;
await fs.writeFile(hookPath, combinedContent);
await fs.chmod(hookPath, "755");
logger.info(`✓ Updated ${hook.type} with ${hook.file}`);
} else {
// Copy new hook
await fs.copy(sourcePath, hookPath);
await fs.chmod(hookPath, "755");
logger.info(`✓ Installed ${hook.type} hook: ${hook.file}`);
}
}
}
module.exports = {
installGitHooks,
};