BMAD-METHOD/tools/claude-code-hooks/install.js

220 lines
7.0 KiB
JavaScript

#!/usr/bin/env node
/**
* BMAD Claude Code Hooks Installer
* Installs hooks to a BMAD project for Claude Code integration
* Uses only Node.js built-in modules
*/
const fs = require('fs').promises;
const path = require('path');
const { existsSync } = require('fs');
async function install() {
try {
console.log('🚀 BMAD Claude Code Hooks Installer\n');
// Get project directory from command line or use current directory
const projectDir = process.argv[2] || process.cwd();
// Verify this is a BMAD project
const bmadCorePath = path.join(projectDir, '.bmad-core');
if (!existsSync(bmadCorePath)) {
console.log('❌ Error: Not a BMAD project directory');
console.log(` Looking for .bmad-core in: ${projectDir}`);
console.log('\nPlease run this from a directory where BMAD is installed.');
return;
}
console.log(`📂 Installing hooks for project: ${projectDir}`);
// Copy hook files to project
const hooksSourceDir = path.resolve(__dirname);
const hooksTargetDir = path.join(projectDir, '.bmad-core', 'hooks', 'claude-code');
console.log('📋 Copying hook files to project...');
await fs.mkdir(hooksTargetDir, { recursive: true });
// Copy hook files
const hookFiles = ['user-prompt-submit.js', 'pre-tool-use.js', 'post-tool-use.js', 'stop.js'];
for (const file of hookFiles) {
await fs.copyFile(
path.join(hooksSourceDir, file),
path.join(hooksTargetDir, file)
);
}
// Copy lib directory
const libSourceDir = path.join(hooksSourceDir, 'lib');
const libTargetDir = path.join(hooksTargetDir, 'lib');
await fs.mkdir(libTargetDir, { recursive: true });
const libFiles = await fs.readdir(libSourceDir);
for (const file of libFiles) {
await fs.copyFile(
path.join(libSourceDir, file),
path.join(libTargetDir, file)
);
}
// Check for existing project settings
const settingsPath = path.join(projectDir, '.claude', 'settings.json');
let settings = {};
if (existsSync(settingsPath)) {
console.log('📄 Found existing project Claude settings');
const settingsContent = await fs.readFile(settingsPath, 'utf8');
settings = JSON.parse(settingsContent);
// Backup existing settings
const backupPath = `${settingsPath}.backup.${Date.now()}`;
await fs.writeFile(backupPath, settingsContent);
console.log(`📦 Backed up settings to: ${path.basename(backupPath)}`);
}
// Build hook configuration with relative paths
const relativeHooksPath = path.join('.bmad-core', 'hooks', 'claude-code');
const hookConfig = {
hooks: {
UserPromptSubmit: [
{
name: "BMAD Context Loader",
description: "Automatically loads active story context and quality reminders",
hooks: [
{
type: "command",
command: `node "${path.join(relativeHooksPath, 'user-prompt-submit.js')}"`
}
]
}
],
PreToolUse: [
{
name: "BMAD Write Validator",
matcher: "Write|Edit|MultiEdit",
description: "Validates story requirements before code modifications",
hooks: [
{
type: "command",
command: `node "${path.join(relativeHooksPath, 'pre-tool-use.js')}"`
}
]
}
],
PostToolUse: [
{
name: "BMAD Progress Tracker",
matcher: "Write|Edit|MultiEdit|Bash",
description: "Updates story progress automatically",
hooks: [
{
type: "command",
command: `node "${path.join(relativeHooksPath, 'post-tool-use.js')}"`
}
]
}
],
Stop: [
{
name: "BMAD Session Summary",
description: "Generates quality summary and next steps",
hooks: [
{
type: "command",
command: `node "${path.join(relativeHooksPath, 'stop.js')}"`
}
]
}
]
}
};
// Merge with existing settings
if (!settings.hooks) {
settings.hooks = {};
}
// Create separate BMAD configuration file to avoid validation errors
const bmadConfig = {
enabled: true,
preset: "balanced",
hooks: {
userPromptSubmit: {
enabled: true,
autoLoadStory: true,
contextDepth: "current"
},
preToolUse: {
enabled: true,
blockSimulation: true,
requireTests: false,
maxRetries: 3
},
postToolUse: {
enabled: true,
updateProgress: true,
trackFiles: true
},
stop: {
enabled: true,
generateSummary: true,
saveContext: true
}
},
performance: {
cacheTimeout: 300000,
maxTokens: 4000,
alertThreshold: 500
}
};
// Write BMAD config to separate file
const bmadConfigPath = path.join(projectDir, '.claude', 'bmad-config.json');
await fs.writeFile(bmadConfigPath, JSON.stringify(bmadConfig, null, 2));
// Merge hooks (preserving existing non-BMAD hooks)
for (const [hookType, hookList] of Object.entries(hookConfig.hooks)) {
if (!settings.hooks[hookType]) {
settings.hooks[hookType] = [];
}
// Remove old BMAD hooks
settings.hooks[hookType] = settings.hooks[hookType].filter(
hook => !hook.name || !hook.name.startsWith('BMAD')
);
// Add new BMAD hooks
settings.hooks[hookType].push(...hookList);
}
// Write updated settings
await fs.mkdir(path.dirname(settingsPath), { recursive: true });
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2));
console.log('✅ BMAD Claude Code hooks installed successfully!\n');
console.log('📋 Installed hooks:');
console.log(' - Context Loader (UserPromptSubmit)');
console.log(' - Write Validator (PreToolUse)');
console.log(' - Progress Tracker (PostToolUse)');
console.log(' - Session Summary (Stop)\n');
console.log('🎯 Next steps:');
console.log(`1. Start Claude Code in your project: cd ${projectDir} && claude code .`);
console.log('2. BMAD hooks will automatically activate');
console.log('3. Use /reality-audit to validate implementations\n');
console.log('⚙️ To disable hooks temporarily:');
console.log('Edit .claude/bmad-config.json and set enabled to false');
console.log('Or use runtime command: *hooks-disable\n');
console.log('📄 Configuration stored in: .claude/bmad-config.json');
console.log('(Separate file avoids Claude Code validation errors)\n');
} catch (error) {
console.error('❌ Installation failed:', error.message);
process.exit(1);
}
}
// Run installer
install();