Skip to content

refactor(hooks): simplify Biome detection with process.cwd()#281

Closed
pythonstrup wants to merge 2 commits intoaffaan-m:mainfrom
pythonstrup:feat/refactor-post-edit-hooks
Closed

refactor(hooks): simplify Biome detection with process.cwd()#281
pythonstrup wants to merge 2 commits intoaffaan-m:mainfrom
pythonstrup:feat/refactor-post-edit-hooks

Conversation

@pythonstrup
Copy link
Copy Markdown
Contributor

@pythonstrup pythonstrup commented Feb 25, 2026

Summary

Simplifies the Biome detection logic in post-edit-format.js introduced by #252.

  • Replace walk-up directory traversal with process.cwd() — reduces 3 functions (46 lines) to 3 inline lines
  • Reduce filesystem I/O — from 5-10+ existsSync calls (walking up to /) to just 1-2 checks
  • Fix monorepo accuracy — walk-up could misidentify a sub-package's package.json as project root; process.cwd() matches the actual working directory Claude Code runs in
  • Preserve Prettier fallback — always attempts Prettier when Biome config is absent, maintaining backward compatibility with pre-feat(hooks): auto-detect formatter in post-edit hook (Biome/Prettier) #252 behavior
  • Use biome check --write instead of biome format --write for formatting + linting in one pass

Changed Files

File Change
scripts/hooks/post-edit-format.js Replaced findProjectRoot + detectFormatter + getFormatterCommand with inline process.cwd() detection
tests/hooks/hooks.test.js Added 4 tests for Biome support, updated existing cwd assertion

Test plan

  • node tests/run-all.js passes
  • Verify formatting works in a project with biome.json
  • Verify formatting works in a project with only Prettier
  • Verify silent failure when no formatter is installed

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Post-edit formatting detects Biome configs and uses Biome when present, otherwise falls back to Prettier.
    • Formatting limited to JavaScript/TypeScript files.
    • Runner selection can be driven by environment, supports multiple package managers, and handles Windows executables robustly.
    • Formatting now executes from the repository working directory and preserves non-blocking failure behavior.
  • Tests

    • Expanded tests to cover Biome detection and current-working-directory execution.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 25, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between acaeea4 and 7bbdfe3.

📒 Files selected for processing (1)
  • scripts/hooks/post-edit-format.js

📝 Walkthrough

Walkthrough

Replaces heuristic formatter detection with a runner-based execution model that selects an executable via CLAUDE_PACKAGE_MANAGER (or npm), uses process.cwd() for Biome vs Prettier detection, limits processing to JS/TS files, and invokes the chosen formatter via execFileSync with constructed bin+args.

Changes

Cohort / File(s) Summary
Hook Implementation
scripts/hooks/post-edit-format.js
Rework to runner model: introduced RUNNERS and getRunner() (handles Windows suffix), uses CLAUDE_PACKAGE_MANAGER override, file-type guard for JS/TS, detects Biome by checking process.cwd() for biome.json/biome.jsonc, builds bin+args accordingly (@biomejs/biome check --write or prettier --write), and calls execFileSync with cwd=process.cwd() while preserving non-blocking/silent-failure behavior and stdin/stdout handling. Removed walk-up project-root and previous formatter-detection helpers.
Tests
tests/hooks/hooks.test.js
Updated expectations to assert the hook uses process.cwd() for npx/runner invocations (replacing prior path-derived cwd checks) and added new Round 95 tests for Biome detection (presence of biome.json/biome.jsonc in cwd, use of cwd rather than walk-up, and Biome-specific flags/config checks).

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Dev as Developer (git edit)
participant Hook as post-edit-format.js
participant FS as Filesystem (process.cwd())
participant Runner as Package Manager Runner (npm/npx/yarn/pnpm)
participant Formatter as Formatter (Biome / Prettier)

Dev->>Hook: commit / post-edit event (file paths, stdin)
Hook->>FS: check file extensions (JS_TS_EXT)
Hook->>FS: stat process.cwd() for biome.json / biome.jsonc
alt Biome config exists
    Hook->>Runner: select runner via CLAUDE_PACKAGE_MANAGER / default
    Hook->>Runner: invoke runner -> `@biomejs/biome check --write <files>`
    Runner->>Formatter: execute Biome (rgba(0,128,0,0.5))
else No Biome
    Hook->>Runner: select runner via CLAUDE_PACKAGE_MANAGER / default
    Hook->>Runner: invoke runner -> `prettier --write <files>`
    Runner->>Formatter: execute Prettier (rgba(0,0,255,0.5))
end
Formatter-->>Hook: exit (success or error)
Hook-->>Dev: emit accumulated stdout/stderr (silent on missing bin)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐇 I hop the cwd to sniff for biome,
I pick a runner, dance on npm's dome,
If JSON sings, Biome scurries to write,
Else Prettier polishes through the night,
A rabbit cheers — code tidy, neat, and bright.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main change: simplifying Biome detection by replacing walk-up directory traversal with process.cwd().

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/hooks/post-edit-format.js`:
- Around line 40-43: The hook currently hardcodes npx via npxBin; change it to
honor the CLAUDE_PACKAGE_MANAGER env var (or project config) by mapping package
manager names to the correct runner and runner-args (e.g., npm -> ['npx'] or
['npm','exec','--'], pnpm -> ['pnpm','dlx'], yarn -> ['yarn','dlx'], bun ->
['bun','x']) and use that runner instead of npxBin; update the code that builds
args (the hasBiome branch and the prettier branch that use filePath) to prepend
the selected runner prefix (and handle Windows filename variants when needed) so
that commands like running `@biomejs/biome` or prettier are executed via the
configured package manager rather than always via npx.

In `@tests/hooks/hooks.test.js`:
- Around line 3669-3709: The tests create biome.json/biome.jsonc in testDir but
call runScript without setting its working directory, so process.cwd() inside
post-edit-format.js remains the test runner's cwd; update the three affected
tests to pass a cwd option to runScript (e.g., runScript(path.join(scriptsDir,
'post-edit-format.js'), stdinJson, { cwd: testDir })) so the child process runs
with testDir as its cwd (references: runScript, createTestDir, cleanupTestDir,
post-edit-format.js); keep cleanupTestDir(testDir) after the run.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3d63fd3 and 852d64a.

📒 Files selected for processing (2)
  • scripts/hooks/post-edit-format.js
  • tests/hooks/hooks.test.js

Comment thread scripts/hooks/post-edit-format.js Outdated
Comment thread tests/hooks/hooks.test.js Outdated
Comment on lines +3669 to +3709
if (await asyncTest('uses biome when biome.json exists in cwd', async () => {
const testDir = createTestDir();
fs.writeFileSync(path.join(testDir, 'biome.json'), '{}');
const testFile = path.join(testDir, 'app.ts');
fs.writeFileSync(testFile, 'const x = 1;');

const stdinJson = JSON.stringify({ tool_input: { file_path: testFile } });
const result = await runScript(path.join(scriptsDir, 'post-edit-format.js'), stdinJson);
assert.strictEqual(result.code, 0, 'Should exit 0 with biome.json in cwd');
assert.ok(result.stdout.includes('tool_input'), 'Should pass through stdin data');
cleanupTestDir(testDir);
})) passed++; else failed++;

if (await asyncTest('uses biome when biome.jsonc exists in cwd', async () => {
const testDir = createTestDir();
fs.writeFileSync(path.join(testDir, 'biome.jsonc'), '{}');
const testFile = path.join(testDir, 'app.tsx');
fs.writeFileSync(testFile, 'const x = 1;');

const stdinJson = JSON.stringify({ tool_input: { file_path: testFile } });
const result = await runScript(path.join(scriptsDir, 'post-edit-format.js'), stdinJson);
assert.strictEqual(result.code, 0, 'Should exit 0 with biome.jsonc in cwd');
assert.ok(result.stdout.includes('tool_input'), 'Should pass through stdin data');
cleanupTestDir(testDir);
})) passed++; else failed++;

if (await asyncTest('source uses process.cwd() for biome detection, not walk-up', async () => {
const formatSource = fs.readFileSync(path.join(scriptsDir, 'post-edit-format.js'), 'utf8');
assert.ok(formatSource.includes('process.cwd()'), 'Should use process.cwd() for biome detection');
assert.ok(!formatSource.includes('findBiomeRoot'), 'Should not have findBiomeRoot walk-up function');
})) passed++; else failed++;

if (await asyncTest('source contains biome check --write and BIOME_CONFIGS covers both extensions', async () => {
const formatSource = fs.readFileSync(path.join(scriptsDir, 'post-edit-format.js'), 'utf8');
assert.ok(formatSource.includes('@biomejs/biome'), 'Should reference @biomejs/biome package');
assert.ok(formatSource.includes('check'), 'Should include biome check subcommand');
assert.ok(formatSource.includes('--write'), 'Should include --write flag');
assert.ok(formatSource.includes('biome.json'), 'BIOME_CONFIGS should include biome.json');
assert.ok(formatSource.includes('biome.jsonc'), 'BIOME_CONFIGS should include biome.jsonc');
})) passed++; else failed++;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Biome behavior tests don’t execute from the Biome-configured cwd.

These tests create biome.json/biome.jsonc in testDir, but runScript(...) is called without a cwd override, so the hook still evaluates process.cwd() from the test runner’s cwd. That makes these checks pass even if cwd-based Biome detection is broken.

🧪 Proposed fix
-function runScript(scriptPath, input = '', env = {}) {
+function runScript(scriptPath, input = '', env = {}, options = {}) {
   return new Promise((resolve, reject) => {
     const proc = spawn('node', [scriptPath], {
       env: { ...process.env, ...env },
-      stdio: ['pipe', 'pipe', 'pipe']
+      stdio: ['pipe', 'pipe', 'pipe'],
+      cwd: options.cwd || process.cwd(),
     });
-const result = await runScript(path.join(scriptsDir, 'post-edit-format.js'), stdinJson);
+const result = await runScript(
+  path.join(scriptsDir, 'post-edit-format.js'),
+  stdinJson,
+  {},
+  { cwd: testDir }
+);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/hooks/hooks.test.js` around lines 3669 - 3709, The tests create
biome.json/biome.jsonc in testDir but call runScript without setting its working
directory, so process.cwd() inside post-edit-format.js remains the test runner's
cwd; update the three affected tests to pass a cwd option to runScript (e.g.,
runScript(path.join(scriptsDir, 'post-edit-format.js'), stdinJson, { cwd:
testDir })) so the child process runs with testDir as its cwd (references:
runScript, createTestDir, cleanupTestDir, post-edit-format.js); keep
cleanupTestDir(testDir) after the run.

Copy link
Copy Markdown
Owner

@affaan-m affaan-m left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review: this PR has merge conflicts. Please rebase or resolve.

Replace walk-up directory traversal (findBiomeRoot) with simple
process.cwd() check for biome.json/biome.jsonc. Claude Code already
runs from the project root, so walking up from the edited file is
unnecessary complexity.
@pythonstrup pythonstrup force-pushed the feat/refactor-post-edit-hooks branch from 852d64a to acaeea4 Compare February 25, 2026 02:18
dlx (pnpm dlx, yarn dlx) downloads packages from registry on every
invocation, ignoring locally installed versions. Since this hook runs
on every file edit, dlx causes unnecessary network requests, slow
formatting, offline failures, and potential version mismatches.

Switch to exec (pnpm exec, yarn exec) which resolves from local
node_modules first, matching npx/bunx behavior.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
scripts/hooks/post-edit-format.js (1)

29-30: Normalize CLAUDE_PACKAGE_MANAGER before lookup.

Case or whitespace in env values (e.g., PNPM, yarn) currently falls back to npm. Normalizing avoids surprising runner selection.

Suggested fix
 function getRunner() {
-  const pm = process.env.CLAUDE_PACKAGE_MANAGER;
+  const pm = process.env.CLAUDE_PACKAGE_MANAGER?.trim().toLowerCase();
   const runner = pm && RUNNERS[pm] ? RUNNERS[pm] : RUNNERS.npm;

As per coding guidelines scripts/**/*.js: “Support multiple package managers: npm, pnpm, yarn, bun, with configurable selection via CLAUDE_PACKAGE_MANAGER env var or project config”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/hooks/post-edit-format.js` around lines 29 - 30, The env lookup for
CLAUDE_PACKAGE_MANAGER currently uses the raw value and can miss matches due to
case or surrounding whitespace; normalize the variable by reading
process.env.CLAUDE_PACKAGE_MANAGER into pm, then trim whitespace and convert to
lower-case before using it to index RUNNERS (so the pm lookup in the runner
assignment uses the normalized value); update the variable referenced in the
runner assignment (pm, RUNNERS, runner) to use this normalized string to ensure
correct selection of npm, pnpm, yarn, or bun.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/hooks/post-edit-format.js`:
- Around line 25-33: The RUNNERS entry for Bun currently uses bin: 'bunx' which
causes getRunner's Windows codepath to append '.cmd' and rely on a
non-guaranteed bunx shim; update the RUNNERS.bun entry to use bin: 'bun' and set
args: ['x'] instead (so getRunner returns runner.bin === 'bun' and runner.args
=== ['x']), ensuring any existing logic in getRunner that appends '.cmd' still
works but now invokes Bun via "bun x" cross-platform; change only the RUNNERS
object (and any tests/config that reference RUNNERS.bun) to this pair of
properties.

---

Nitpick comments:
In `@scripts/hooks/post-edit-format.js`:
- Around line 29-30: The env lookup for CLAUDE_PACKAGE_MANAGER currently uses
the raw value and can miss matches due to case or surrounding whitespace;
normalize the variable by reading process.env.CLAUDE_PACKAGE_MANAGER into pm,
then trim whitespace and convert to lower-case before using it to index RUNNERS
(so the pm lookup in the runner assignment uses the normalized value); update
the variable referenced in the runner assignment (pm, RUNNERS, runner) to use
this normalized string to ensure correct selection of npm, pnpm, yarn, or bun.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 852d64a and acaeea4.

📒 Files selected for processing (2)
  • scripts/hooks/post-edit-format.js
  • tests/hooks/hooks.test.js

Comment thread scripts/hooks/post-edit-format.js Outdated
Comment on lines +25 to +33
bun: { bin: 'bunx', args: [] },
};

function getRunner() {
const pm = process.env.CLAUDE_PACKAGE_MANAGER;
const runner = pm && RUNNERS[pm] ? RUNNERS[pm] : RUNNERS.npm;
const bin = process.platform === 'win32' && !runner.bin.endsWith('.cmd')
? `${runner.bin}.cmd`
: runner.bin;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Bun bunx command Windows support bunx.cmd shim official documentation

💡 Result:

  • Windows support (official): Bun supports Windows (x64); the installer docs state Windows 10 version 1809 or later is required. [1]
  • bunx (official): bunx is an alias for bun x and is auto-installed when you install bun (Bun’s equivalent of npx / yarn dlx). [2]
  • bunx.cmd shim: Bun’s official docs do not document a bunx.cmd shim. The bunx.cmd “shim” behavior is discussed as a Windows packaging/installation quirk (e.g., Scoop creating bunx.cmd even though bunx is an .exe) in Bun’s issue tracker. [3]

Sources: [1] Bun Installation docs (Windows requirements) • [2] Bun bunx CLI docs • [3] oven-sh/bun issue #10599 (bunx.cmd shim discussion)


🏁 Script executed:

# Check the actual structure of the file to confirm the code context
cat -n scripts/hooks/post-edit-format.js | head -40

Repository: affaan-m/everything-claude-code

Length of output: 1523


Use bun x form to avoid Windows compatibility risk.

RUNNERS.bun uses bunx, and the Windows code path (line 31–33) appends .cmd to create bunx.cmd. However, Bun's official documentation does not guarantee that a bunx.cmd shim exists; this is a packaging quirk handled by some installers (e.g., Scoop) but not guaranteed by Bun itself. If the shim is missing, Bun formatting silently fails on Windows, violating the cross-platform compatibility requirement.

Use bin: 'bun' with args: ['x'] instead, which aligns with Bun's official behavior and avoids the undocumented shim dependency.

Suggested fix
 const RUNNERS = {
   npm:  { bin: 'npx',  args: [] },
   pnpm: { bin: 'pnpm', args: ['dlx'] },
   yarn: { bin: 'yarn', args: ['dlx'] },
-  bun:  { bin: 'bunx', args: [] },
+  bun:  { bin: 'bun',  args: ['x'] },
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
bun: { bin: 'bunx', args: [] },
};
function getRunner() {
const pm = process.env.CLAUDE_PACKAGE_MANAGER;
const runner = pm && RUNNERS[pm] ? RUNNERS[pm] : RUNNERS.npm;
const bin = process.platform === 'win32' && !runner.bin.endsWith('.cmd')
? `${runner.bin}.cmd`
: runner.bin;
bun: { bin: 'bun', args: ['x'] },
};
function getRunner() {
const pm = process.env.CLAUDE_PACKAGE_MANAGER;
const runner = pm && RUNNERS[pm] ? RUNNERS[pm] : RUNNERS.npm;
const bin = process.platform === 'win32' && !runner.bin.endsWith('.cmd')
? `${runner.bin}.cmd`
: runner.bin;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/hooks/post-edit-format.js` around lines 25 - 33, The RUNNERS entry
for Bun currently uses bin: 'bunx' which causes getRunner's Windows codepath to
append '.cmd' and rely on a non-guaranteed bunx shim; update the RUNNERS.bun
entry to use bin: 'bun' and set args: ['x'] instead (so getRunner returns
runner.bin === 'bun' and runner.args === ['x']), ensuring any existing logic in
getRunner that appends '.cmd' still works but now invokes Bun via "bun x"
cross-platform; change only the RUNNERS object (and any tests/config that
reference RUNNERS.bun) to this pair of properties.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants