fix(search): accept file paths instead of crashing with ENOTDIR (#827)#832
Closed
Dave-London wants to merge 2 commits intomainfrom
Closed
fix(search): accept file paths instead of crashing with ENOTDIR (#827)#832Dave-London wants to merge 2 commits intomainfrom
Dave-London wants to merge 2 commits intomainfrom
Conversation
`mcp__pare-search__search` and `count` previously used the user-supplied `path` as the spawn `cwd` unconditionally. When `path` pointed to a file (which the schema description explicitly promises to support — "Directory or file to search in"), Node threw `spawn ENOTDIR` because cwd must be a directory. This adds `resolveSearchPath()` in the runner, which: - returns process.cwd() + "." for an undefined path (existing behaviour) - returns the directory itself + "." for a directory path - returns the file's parent dir + the absolute file path for a file - throws a clear "path does not exist" error for missing paths `search` and `count` now route through it. `count` additionally passes `--with-filename` when the target is a single file so its parser keeps the `file:count` shape. `find` (fd) genuinely needs a directory root, so it surfaces a typed `path must be a directory for find` error instead of leaking ENOTDIR. Closes #827.
The new resolveSearchPath export in @paretools/search/src/lib/search-runner.ts
broke vi.mock() in both small-servers smoke suites — vitest fails the import
when a code path under test references a missing export on a mocked module.
Default the mock to the directory case ({cwd: path, target: ".", isFile: false})
so existing tests stay green; tests that need to exercise the file-input path
can override per-test.
Caught only by smoke job — package-level tests pass without this since they
don't mock search-runner.
Owner
Author
This was referenced Apr 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
pare-search__searchandpare-search__countcrashing withspawn ENOTDIRwhenpathpoints to a file. The schema description already promises file or directory; now both work.pare-search__find(fd) genuinely walks directories, so it now surfaces a typedpath must be a directory for finderror instead of leaking the raw NodeENOTDIR.path does not exist: <path>error.Closes #827.
Root cause
packages/server-search/src/tools/search.ts(andcount.ts,find.ts) used the user-suppliedpathas the spawncwdunconditionally:child_process.spawnthrowsENOTDIRsynchronously whencwdis set to a non-directory path, so the error leaked out as{ "error": "spawn ENOTDIR" }.Fix
New helper
resolveSearchPath(path)inlib/search-runner.tsreturns{ cwd, target, isFile }:undefined→cwd = process.cwd(),target = "."cwd = <dir>,target = "."(existing behaviour)cwd = dirname(file),target = <absolute-file-path>,isFile = truepath does not exist: <path>search.tsandcount.tsroute through it and appendtargetinstead of a hard-coded".".count.tsalso adds--with-filenamewhenisFile, because rg drops thefile:prefix when given a single file and the parser expectsfile:count.find.tsresolves the path the same way but throws when the input is a file (fd is not meaningful for a single file root).Before / after
Test plan
pnpm --filter @paretools/search build— cleanpnpm --filter @paretools/search test— 88/88 passing (added 5 unit tests forresolveSearchPath, plus 4 integration tests covering search/count file-path success and find/missing-path typed errors)pnpm linton touched files — cleanprettier --checkon touched files — clean