bookworm-smart-assistant/hooks/check-lint.js

183 lines
4.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* PostToolUse Hook: 文件写入后运行 ESLint 检查
* 匹配器: Edit|Write|NotebookEdit
* 退出码: 0=通过(静默), 2=有错误(stderr反馈给Claude)
*
* v3.0 修复:
* - 使用 spawnSync + 数组参数,防止 filePath shell 注入
* - 检测 eslint 本地安装状态,避免 npx 远程下载
*/
const { spawnSync } = require('child_process');
const path = require('path');
const fs = require('fs');
const readStdin = require('./lib/read-stdin.js');
const LINT_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx', '.vue'];
const ESLINT_CONFIG_FILES = [
'eslint.config.js',
'eslint.config.mjs',
'eslint.config.cjs',
'eslint.config.ts',
'eslint.config.mts',
'eslint.config.cts',
'.eslintrc.js',
'.eslintrc.cjs',
'.eslintrc.yaml',
'.eslintrc.yml',
'.eslintrc.json',
'.eslintrc',
];
function findEslintConfig(filePath) {
let dir = path.dirname(filePath);
for (let i = 0; i < 15; i++) {
for (const configFile of ESLINT_CONFIG_FILES) {
const configPath = path.join(dir, configFile);
if (fs.existsSync(configPath)) {
return dir;
}
}
// 也检查 package.json 中的 eslintConfig 字段
const pkgPath = path.join(dir, 'package.json');
if (fs.existsSync(pkgPath)) {
try {
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
if (pkg.eslintConfig) {
return dir;
}
} catch (_) {
// 忽略解析错误
}
}
const parent = path.dirname(dir);
if (parent === dir) break;
dir = parent;
}
return null;
}
/**
* 检测 eslint 是否已本地安装(避免 npx 远程下载超时)
*/
function isEslintInstalled(projectRoot) {
const localBin = path.join(projectRoot, 'node_modules', '.bin', 'eslint');
// Windows 上检查 .cmd 变体
return fs.existsSync(localBin) ||
fs.existsSync(localBin + '.cmd') ||
fs.existsSync(localBin + '.ps1');
}
function main() {
readStdin({ maxSize: 1024 * 1024 }).then(input => {
const filePath = (input.tool_input && input.tool_input.file_path) || '';
const ext = path.extname(filePath).toLowerCase();
// 仅处理前端/JS/TS/Vue 文件
if (!LINT_EXTENSIONS.includes(ext)) {
process.exit(0);
return;
}
// 确认文件存在
if (!fs.existsSync(filePath)) {
process.exit(0);
return;
}
// 查找 ESLint 配置
const projectRoot = findEslintConfig(filePath);
if (!projectRoot) {
process.exit(0);
return;
}
// 检测 eslint 本地安装
if (!isEslintInstalled(projectRoot)) {
process.exit(0);
return;
}
// 使用 spawnSync + 数组参数,防止 shell 注入
const npxCmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';
const result = spawnSync(npxCmd, [
'eslint',
'--no-error-on-unmatched-pattern',
'--format', 'compact',
filePath,
], {
cwd: projectRoot,
timeout: 8000,
encoding: 'utf8',
shell: false,
stdio: ['pipe', 'pipe', 'pipe'],
});
// ESLint 通过(退出码 0
if (result.status === 0) {
process.exit(0);
return;
}
const output = (result.stdout || '') + (result.stderr || '');
// 检查是否是 eslint 未安装或命令找不到
if (
output.includes('not found') ||
output.includes('not recognized') ||
output.includes('Cannot find module') ||
output.includes('ERR_MODULE_NOT_FOUND')
) {
process.exit(0);
return;
}
// compact 格式: /path/file.ts: line X, col Y, Error - message (rule-name)
const lines = output.split('\n').filter(line => line.trim());
// 仅提取 Error 级别(忽略 Warning
const errorLines = lines.filter(line =>
/:\s*line\s+\d+.*Error\s+-\s+/.test(line)
);
if (errorLines.length === 0) {
process.exit(0);
return;
}
// 格式化错误信息
const fileName = path.basename(filePath);
const maxDisplay = 10;
const displayErrors = errorLines.slice(0, maxDisplay);
const summary = displayErrors
.map(line => {
const match = line.match(/:\s*line\s+(\d+),\s*col\s+(\d+),\s*Error\s+-\s+(.+)/);
if (match) {
return ` L${match[1]}:${match[2]} - ${match[3]}`;
}
return ` ${line.trim()}`;
})
.join('\n');
const message = [
`[ESLint] 在 ${fileName} 中发现 ${errorLines.length} 个错误:`,
summary,
errorLines.length > maxDisplay ? ` ... 还有 ${errorLines.length - maxDisplay} 个错误` : '',
'',
'请修复以上 ESLint 错误。',
].filter(Boolean).join('\n');
process.stderr.write(JSON.stringify({
continue: true,
suppressOutput: false,
systemMessage: message,
}));
process.exit(2);
}).catch(() => process.exit(0));
}
if (require.main === module) { main(); }