claude-hooks/bin/claude-hooks.js

284 lines
8.1 KiB
JavaScript
Executable File

#!/usr/bin/env node
const { Command } = require('commander');
const chalk = require('chalk');
const path = require('path');
const fs = require('fs-extra');
const os = require('os');
const program = new Command();
const packageRoot = path.dirname(__dirname);
// Helper to create hooks configuration
async function createHooksConfig() {
const claudeConfigDir = path.join(os.homedir(), '.config', 'claude');
const hooksConfigFile = path.join(claudeConfigDir, 'hooks.json');
await fs.ensureDir(claudeConfigDir);
const hooksConfig = {
hooks: {
UserPromptSubmit: `node ${path.join(packageRoot, 'hooks', 'context-monitor.js')}`,
PreToolUse: {
Bash: `node ${path.join(packageRoot, 'hooks', 'command-validator.js')}`
},
PostToolUse: {
'*': `node ${path.join(packageRoot, 'hooks', 'session-logger.js')}`
},
Stop: `node ${path.join(packageRoot, 'hooks', 'session-finalizer.js')}`
}
};
await fs.writeJson(hooksConfigFile, hooksConfig, { spaces: 2 });
return hooksConfigFile;
}
// Helper to test a hook script
async function testHookScript(scriptName, testInput) {
return new Promise((resolve) => {
const { spawn } = require('child_process');
const scriptPath = path.join(packageRoot, 'hooks', scriptName);
const child = spawn('node', [scriptPath], {
stdio: ['pipe', 'pipe', 'pipe'],
cwd: packageRoot
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => {
stdout += data.toString();
});
child.stderr.on('data', (data) => {
stderr += data.toString();
});
child.stdin.write(JSON.stringify(testInput));
child.stdin.end();
const timeout = setTimeout(() => {
child.kill();
resolve({ success: false, error: 'Timeout' });
}, 10000);
child.on('close', (code) => {
clearTimeout(timeout);
resolve({
success: code === 0,
stdout: stdout.trim(),
stderr: stderr.trim()
});
});
child.on('error', (error) => {
clearTimeout(timeout);
resolve({ success: false, error: error.message });
});
});
}
// Helper to check installation status
async function checkStatus() {
const hooksConfig = path.join(os.homedir(), '.config', 'claude', 'hooks.json');
const hasHooks = await fs.pathExists(hooksConfig);
console.log(chalk.blue('Claude Hooks Status:'));
console.log(hasHooks ? chalk.green('✓ Hooks configuration installed') : chalk.red('✗ No hooks configuration found'));
console.log(chalk.green('✓ Node.js runtime available'));
return hasHooks;
}
// Helper to remove hooks configuration
async function removeHooksConfig() {
const hooksConfig = path.join(os.homedir(), '.config', 'claude', 'hooks.json');
if (await fs.pathExists(hooksConfig)) {
await fs.remove(hooksConfig);
return true;
}
return false;
}
program
.name('claude-hooks')
.description('Intelligent hooks system for Claude Code')
.version(require('../package.json').version);
program
.command('init')
.description('Initialize Claude Hooks (run after npm install)')
.option('--force', 'Force reinstallation even if already configured')
.option('--auto-setup', 'Auto-setup during npm postinstall')
.option('--quiet', 'Minimal output')
.action(async (options) => {
if (!options.quiet) {
console.log(chalk.blue('Initializing Claude Hooks...'));
}
try {
// Check for existing installation
if (!options.force && await checkStatus()) {
if (!options.quiet) {
console.log(chalk.yellow('⚠️ Claude Hooks already configured. Use --force to reinstall.'));
}
return;
}
// Create runtime directories
const runtimeDir = path.join(packageRoot, '.claude_hooks');
await fs.ensureDir(path.join(runtimeDir, 'backups'));
await fs.ensureDir(path.join(runtimeDir, 'logs'));
await fs.ensureDir(path.join(runtimeDir, 'patterns'));
// Create hooks configuration
const configFile = await createHooksConfig();
if (!options.quiet) {
console.log(chalk.green('✅ Claude Hooks initialized successfully!'));
console.log(chalk.blue('Configuration file:'), configFile);
console.log(chalk.blue('Next steps:'));
console.log('1. Restart Claude Code to activate hooks');
console.log('2. Try: claude-hooks test');
}
} catch (error) {
console.error(chalk.red('❌ Initialization failed:'), error.message);
process.exit(1);
}
});
program
.command('status')
.description('Show Claude Hooks installation status')
.action(async () => {
await checkStatus();
});
program
.command('test')
.description('Run Claude Hooks tests')
.action(async () => {
console.log(chalk.blue('Running Claude Hooks tests...'));
let passed = 0;
let total = 0;
// Test 1: Check configuration
total++;
console.log('Testing configuration...');
if (await checkStatus()) {
console.log(chalk.green('✓ Configuration exists'));
passed++;
} else {
console.log(chalk.red('✗ Configuration missing'));
}
// Test 2: Test hook scripts
const hookTests = [
{
name: 'context-monitor.js',
input: { prompt: 'test prompt', context_size: 1000 }
},
{
name: 'command-validator.js',
input: { tool: 'Bash', parameters: { command: 'pip install requests' } }
},
{
name: 'session-logger.js',
input: { tool: 'Bash', parameters: { command: 'echo test' }, success: true }
},
{
name: 'session-finalizer.js',
input: {}
}
];
for (const test of hookTests) {
total++;
console.log(`Testing ${test.name}...`);
const result = await testHookScript(test.name, test.input);
if (result.success) {
console.log(chalk.green(`${test.name} working`));
passed++;
} else {
console.log(chalk.red(`${test.name} failed: ${result.error || result.stderr}`));
}
}
// Summary
console.log();
if (passed === total) {
console.log(chalk.green(`✅ All ${total} tests passed!`));
console.log(chalk.green('Claude Hooks is ready to use!'));
} else {
console.log(chalk.yellow(`⚠️ ${passed}/${total} tests passed`));
console.log(chalk.blue('Try: claude-hooks init --force'));
process.exit(1);
}
});
program
.command('uninstall')
.description('Remove Claude Hooks configuration')
.option('--quiet', 'Minimal output')
.action(async (options) => {
if (!options.quiet) {
console.log(chalk.blue('Uninstalling Claude Hooks...'));
}
try {
const removed = await removeHooksConfig();
if (removed) {
if (!options.quiet) {
console.log(chalk.green('✅ Claude Hooks configuration removed'));
console.log(chalk.yellow('Note: To completely remove the package: npm uninstall -g claude-hooks'));
}
} else {
if (!options.quiet) {
console.log(chalk.yellow('⚠️ No Claude Hooks configuration found'));
}
}
} catch (error) {
console.error(chalk.red('❌ Uninstall failed:'), error.message);
process.exit(1);
}
});
program
.command('backup')
.description('Manually trigger a backup')
.action(async () => {
console.log(chalk.blue('Creating manual backup...'));
try {
// Import backup manager
const { BackupManager } = require(path.join(packageRoot, 'lib', 'backup-manager'));
const backupManager = new BackupManager();
const backupId = await backupManager.createBackup(process.cwd(), {
trigger: 'manual',
timestamp: new Date().toISOString()
});
if (backupId) {
console.log(chalk.green(`✅ Backup created: ${backupId}`));
} else {
console.log(chalk.red('❌ Backup failed'));
process.exit(1);
}
} catch (error) {
console.error(chalk.red('❌ Backup failed:'), error.message);
process.exit(1);
}
});
program.parse();