#!/usr/bin/env node /** * Quality Gate Hook * * Runs lightweight quality checks after file edits. * - Targets one file when file_path is provided * - Falls back to no-op when language/tooling is unavailable * * For JS/TS files with Biome, this hook is skipped because * post-edit-format.js already runs `biome check --write`. * This hook still handles .json/.md files for Biome, and all * Prettier / Go / Python checks. */ 'use strict'; const fs = require('fs'); const path = require('path'); const { spawnSync } = require('child_process'); const { findProjectRoot, detectFormatter, resolveFormatterBin, } = require('../lib/resolve-formatter'); const MAX_STDIN = 1024 * 1024; function exec(command, args, cwd = process.cwd()) { return spawnSync(command, args, { cwd, encoding: 'utf8', env: process.env, }); } function log(msg) { process.stderr.write(`${msg}\n`); } function maybeRunQualityGate(filePath) { if (!filePath || !fs.existsSync(filePath)) { return; } const ext = path.extname(filePath).toLowerCase(); const fix = String(process.env.ECC_QUALITY_GATE_FIX || '').toLowerCase() === 'true'; const strict = String(process.env.ECC_QUALITY_GATE_STRICT || '').toLowerCase() === 'true'; if (['.ts', '.tsx', '.js', '.jsx', '.json', '.md'].includes(ext)) { const projectRoot = findProjectRoot(path.dirname(path.resolve(filePath))); const formatter = detectFormatter(projectRoot); if (formatter === 'biome') { // JS/TS already handled by post-edit-format via `biome check --write` if (['.ts', '.tsx', '.js', '.jsx'].includes(ext)) { return; } // .json / .md — still need quality gate const resolved = resolveFormatterBin(projectRoot, 'biome'); const args = [...resolved.prefix, 'check', filePath]; if (fix) args.push('--write'); const result = exec(resolved.bin, args, projectRoot); if (result.status !== 0 && strict) { log(`[QualityGate] Biome check failed for ${filePath}`); } return; } if (formatter === 'prettier') { const resolved = resolveFormatterBin(projectRoot, 'prettier'); const args = [...resolved.prefix, fix ? '--write' : '--check', filePath]; const result = exec(resolved.bin, args, projectRoot); if (result.status !== 0 && strict) { log(`[QualityGate] Prettier check failed for ${filePath}`); } return; } // No formatter configured — skip return; } if (ext === '.go' && fix) { exec('gofmt', ['-w', filePath]); return; } if (ext === '.py') { const args = ['format']; if (!fix) args.push('--check'); args.push(filePath); const r = exec('ruff', args); if (r.status !== 0 && strict) { log(`[QualityGate] Ruff check failed for ${filePath}`); } } } /** * Core logic — exported so run-with-flags.js can call directly. * * @param {string} rawInput - Raw JSON string from stdin * @returns {string} The original input (pass-through) */ function run(rawInput) { try { const input = JSON.parse(rawInput); const filePath = String(input.tool_input?.file_path || ''); maybeRunQualityGate(filePath); } catch { // Ignore parse errors. } return rawInput; } // ── stdin entry point (backwards-compatible) ──────────────────── if (require.main === module) { let raw = ''; process.stdin.setEncoding('utf8'); process.stdin.on('data', (chunk) => { if (raw.length < MAX_STDIN) { const remaining = MAX_STDIN - raw.length; raw += chunk.substring(0, remaining); } }); process.stdin.on('end', () => { const result = run(raw); process.stdout.write(result); }); } module.exports = { run };