From 4eb6fbdd3f7dba3c1b9333c014cce724bae71b60 Mon Sep 17 00:00:00 2001 From: Jonghyeok Park Date: Fri, 20 Feb 2026 14:30:01 +0900 Subject: [PATCH] feat: auto-detect formatter in post-edit hook (Biome/Prettier) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The post-edit-format hook was hardcoded to use Prettier. Projects using Biome had their code reformatted with Prettier defaults (e.g. double quotes overwriting single quotes). Now the hook walks up from the edited file to find the project root, then checks for config files: - biome.json / biome.jsonc → runs Biome - .prettierrc / prettier.config.* → runs Prettier - Neither found → skips formatting silently --- hooks/hooks.json | 2 +- scripts/hooks/post-edit-format.js | 73 ++++++++++++++++++++++++++----- 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/hooks/hooks.json b/hooks/hooks.json index d8c8c2d7..398d2fda 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -108,7 +108,7 @@ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/post-edit-format.js\"" } ], - "description": "Auto-format JS/TS files with Prettier after edits" + "description": "Auto-format JS/TS files after edits (auto-detects Biome or Prettier)" }, { "matcher": "Edit", diff --git a/scripts/hooks/post-edit-format.js b/scripts/hooks/post-edit-format.js index f3651b5c..257bd47b 100644 --- a/scripts/hooks/post-edit-format.js +++ b/scripts/hooks/post-edit-format.js @@ -1,14 +1,17 @@ #!/usr/bin/env node /** - * PostToolUse Hook: Auto-format JS/TS files with Prettier after edits + * PostToolUse Hook: Auto-format JS/TS files after edits * * Cross-platform (Windows, macOS, Linux) * * Runs after Edit tool use. If the edited file is a JS/TS file, - * formats it with Prettier. Fails silently if Prettier isn't installed. + * auto-detects the project formatter (Biome or Prettier) by looking + * for config files, then formats accordingly. + * Fails silently if no formatter is found or installed. */ const { execFileSync } = require('child_process'); +const fs = require('fs'); const path = require('path'); const MAX_STDIN = 1024 * 1024; // 1MB limit @@ -21,6 +24,52 @@ process.stdin.on('data', chunk => { } }); +function findProjectRoot(startDir) { + let dir = startDir; + while (dir !== path.dirname(dir)) { + if (fs.existsSync(path.join(dir, 'package.json'))) return dir; + dir = path.dirname(dir); + } + return startDir; +} + +function detectFormatter(projectRoot) { + const biomeConfigs = ['biome.json', 'biome.jsonc']; + for (const cfg of biomeConfigs) { + if (fs.existsSync(path.join(projectRoot, cfg))) return 'biome'; + } + + const prettierConfigs = [ + '.prettierrc', + '.prettierrc.json', + '.prettierrc.js', + '.prettierrc.cjs', + '.prettierrc.mjs', + '.prettierrc.yml', + '.prettierrc.yaml', + '.prettierrc.toml', + 'prettier.config.js', + 'prettier.config.cjs', + 'prettier.config.mjs', + ]; + for (const cfg of prettierConfigs) { + if (fs.existsSync(path.join(projectRoot, cfg))) return 'prettier'; + } + + return null; +} + +function getFormatterCommand(formatter, filePath) { + const npxBin = process.platform === 'win32' ? 'npx.cmd' : 'npx'; + if (formatter === 'biome') { + return { bin: npxBin, args: ['@biomejs/biome', 'format', '--write', filePath] }; + } + if (formatter === 'prettier') { + return { bin: npxBin, args: ['prettier', '--write', filePath] }; + } + return null; +} + process.stdin.on('end', () => { try { const input = JSON.parse(data); @@ -28,15 +77,19 @@ process.stdin.on('end', () => { if (filePath && /\.(ts|tsx|js|jsx)$/.test(filePath)) { try { - // Use npx.cmd on Windows to avoid shell: true which enables command injection - const npxBin = process.platform === 'win32' ? 'npx.cmd' : 'npx'; - execFileSync(npxBin, ['prettier', '--write', filePath], { - cwd: path.dirname(path.resolve(filePath)), - stdio: ['pipe', 'pipe', 'pipe'], - timeout: 15000 - }); + const projectRoot = findProjectRoot(path.dirname(path.resolve(filePath))); + const formatter = detectFormatter(projectRoot); + const cmd = getFormatterCommand(formatter, filePath); + + if (cmd) { + execFileSync(cmd.bin, cmd.args, { + cwd: projectRoot, + stdio: ['pipe', 'pipe', 'pipe'], + timeout: 15000 + }); + } } catch { - // Prettier not installed, file missing, or failed — non-blocking + // Formatter not installed, file missing, or failed — non-blocking } } } catch {