Initial commit
This commit is contained in:
199
hooks/on-file-change.js
Normal file
199
hooks/on-file-change.js
Normal file
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* On File Change Hook
|
||||
*
|
||||
* This hook runs when files are modified during a Claude Code session. (Claude Code modifies files)
|
||||
* Use it to automatically run checks, update related files, or provide feedback.
|
||||
*/
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = async (context) => {
|
||||
const { files, operation } = context;
|
||||
|
||||
const notifications = [];
|
||||
const warnings = [];
|
||||
const errors = [];
|
||||
|
||||
// Process each changed file
|
||||
for (const file of files) {
|
||||
const { filePath, changeType } = file;
|
||||
|
||||
// Get file extension
|
||||
const ext = path.extname(filePath);
|
||||
|
||||
// Run language-specific checks
|
||||
try {
|
||||
// JavaScript/TypeScript files
|
||||
if (['.js', '.jsx', '.ts', '.tsx'].includes(ext)) {
|
||||
await checkJavaScript(filePath, notifications, warnings, errors);
|
||||
}
|
||||
|
||||
// Python files
|
||||
if (['.py'].includes(ext)) {
|
||||
await checkPython(filePath, notifications, warnings, errors);
|
||||
}
|
||||
|
||||
// Test files
|
||||
if (isTestFile(filePath)) {
|
||||
notifications.push(`✅ Test file ${path.basename(filePath)} modified`);
|
||||
}
|
||||
|
||||
// Check if corresponding test file exists
|
||||
if (!isTestFile(filePath) && isSourceFile(filePath)) {
|
||||
const testFilePath = getTestFilePath(filePath);
|
||||
if (!fs.existsSync(testFilePath)) {
|
||||
warnings.push(`⚠️ No test file found for ${path.basename(filePath)}. Consider creating ${path.basename(testFilePath)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check file size
|
||||
if (fs.existsSync(filePath)) {
|
||||
const lines = fs.readFileSync(filePath, 'utf-8').split('\n').length;
|
||||
|
||||
if (lines > 600) {
|
||||
warnings.push(`⚠️ ${path.basename(filePath)} is ${lines} lines. Consider breaking into smaller modules.`);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error processing ${filePath}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Log changes
|
||||
console.error(JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
operation,
|
||||
filesChanged: files.length,
|
||||
notifications: notifications.length,
|
||||
warnings: warnings.length,
|
||||
errors: errors.length,
|
||||
}));
|
||||
|
||||
// Return feedback to user
|
||||
return {
|
||||
notifications: notifications.length > 0 ? notifications : undefined,
|
||||
warnings: warnings.length > 0 ? warnings : undefined,
|
||||
errors: errors.length > 0 ? errors : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Check JavaScript/TypeScript files
|
||||
*/
|
||||
async function checkJavaScript(filePath, notifications, warnings, errors) {
|
||||
if (!fs.existsSync(filePath)) return;
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
|
||||
// Check for console.log statements
|
||||
const consoleLogs = (content.match(/console\.log\(/g) || []).length;
|
||||
if (consoleLogs > 0) {
|
||||
warnings.push(`⚠️ Found ${consoleLogs} console.log statement(s) in ${path.basename(filePath)}`);
|
||||
}
|
||||
|
||||
// Check for TODO/FIXME
|
||||
const todos = (content.match(/\/\/\s*(TODO|FIXME)/g) || []).length;
|
||||
if (todos > 0) {
|
||||
notifications.push(`📝 Found ${todos} TODO/FIXME comment(s) in ${path.basename(filePath)}`);
|
||||
}
|
||||
|
||||
// Try to run ESLint if available
|
||||
try {
|
||||
execSync(`npx eslint --quiet "${filePath}"`, { encoding: 'utf-8', stdio: 'pipe' });
|
||||
notifications.push(`✅ ESLint: ${path.basename(filePath)} passed`);
|
||||
} catch (error) {
|
||||
// ESLint found issues or not installed
|
||||
if (error.stdout && error.stdout.length > 0) {
|
||||
const issueCount = (error.stdout.match(/error/g) || []).length;
|
||||
if (issueCount > 0) {
|
||||
warnings.push(`⚠️ ESLint found ${issueCount} issue(s) in ${path.basename(filePath)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for long functions (basic heuristic)
|
||||
const functions = content.match(/function\s+\w+\s*\([^)]*\)\s*\{/g) || [];
|
||||
for (const func of functions) {
|
||||
const funcStart = content.indexOf(func);
|
||||
const funcBody = content.slice(funcStart);
|
||||
const lines = funcBody.split('\n').slice(0, 100).join('\n');
|
||||
const braceCount = (lines.match(/\{/g) || []).length - (lines.match(/\}/g) || []).length;
|
||||
|
||||
if (lines.split('\n').length > 100 && braceCount === 0) {
|
||||
warnings.push(`Potentially long function detected in ${path.basename(filePath)}`);
|
||||
break; // Only warn once per file
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Python files
|
||||
*/
|
||||
async function checkPython(filePath, notifications, warnings, errors) {
|
||||
if (!fs.existsSync(filePath)) return;
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
|
||||
// Check for print statements (potential debug code)
|
||||
const prints = (content.match(/print\(/g) || []).length;
|
||||
if (prints > 0) {
|
||||
warnings.push(`Found ${prints} print statement(s) in ${path.basename(filePath)}`);
|
||||
}
|
||||
|
||||
// Try to run pylint if available
|
||||
try {
|
||||
execSync(`pylint --errors-only "${filePath}"`, { encoding: 'utf-8', stdio: 'pipe' });
|
||||
notifications.push(`✅ Pylint: ${path.basename(filePath)} passed`);
|
||||
} catch (error) {
|
||||
// Pylint found issues or not installed
|
||||
if (error.stdout && error.stdout.length > 0) {
|
||||
warnings.push(`Pylint found issues in ${path.basename(filePath)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file is a test file
|
||||
*/
|
||||
function isTestFile(filePath) {
|
||||
const fileName = path.basename(filePath).toLowerCase();
|
||||
return fileName.includes('.test.') ||
|
||||
fileName.includes('.spec.') ||
|
||||
fileName.includes('_test.') ||
|
||||
fileName.startsWith('test_') ||
|
||||
filePath.includes('/tests/') ||
|
||||
filePath.includes('/test/') ||
|
||||
filePath.includes('/__tests__/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file is a source file (not config, docs, etc.)
|
||||
*/
|
||||
function isSourceFile(filePath) {
|
||||
const ext = path.extname(filePath);
|
||||
const sourceExts = ['.js', '.jsx', '.ts', '.tsx', '.py', '.java', '.go', '.rs', '.rb', '.php'];
|
||||
return sourceExts.includes(ext) && !isTestFile(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the expected test file path for a source file
|
||||
*/
|
||||
function getTestFilePath(filePath) {
|
||||
const ext = path.extname(filePath);
|
||||
const baseName = path.basename(filePath, ext);
|
||||
const dirName = path.dirname(filePath);
|
||||
|
||||
// Try common test file patterns
|
||||
const patterns = [
|
||||
path.join(dirName, `${baseName}.test${ext}`),
|
||||
path.join(dirName, `${baseName}.spec${ext}`),
|
||||
path.join(dirName, '__tests__', `${baseName}.test${ext}`),
|
||||
path.join(dirName, '..', 'tests', `${baseName}.test${ext}`),
|
||||
];
|
||||
|
||||
// Return first pattern (most common)
|
||||
return patterns[0];
|
||||
}
|
||||
Reference in New Issue
Block a user