284 lines
8.1 KiB
JavaScript
Executable File
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(); |