fix: add clearer ripgrep install guidance

This commit is contained in:
Vasanthdev2004
2026-04-02 10:19:36 +05:30
parent 1a60509fdc
commit 2bade922ef
3 changed files with 88 additions and 3 deletions

View File

@@ -16,6 +16,10 @@ All of Claude Code's tools work — bash, file read/write/edit, grep, glob, agen
npm install -g @gitlawb/openclaude
```
If you install via npm and later see `ripgrep not found`, install ripgrep
system-wide and confirm `rg --version` works in the same terminal before
starting OpenClaude.
### Option B: From source (requires Bun)
Use Bun `1.3.11` or newer for source builds on Windows. Older Bun versions such as `1.3.4` can fail with a large batch of unresolved module errors during `bun run build`.

27
src/utils/ripgrep.test.ts Normal file
View File

@@ -0,0 +1,27 @@
import { expect, test } from 'bun:test'
import { wrapRipgrepUnavailableError } from './ripgrep.ts'
test('wrapRipgrepUnavailableError explains missing packaged fallback', () => {
const error = wrapRipgrepUnavailableError(
{ code: 'ENOENT', message: 'spawn rg ENOENT' },
{ mode: 'builtin', command: 'C:\\fake\\vendor\\ripgrep\\rg.exe' },
'win32',
)
expect(error.name).toBe('RipgrepUnavailableError')
expect(error.code).toBe('ENOENT')
expect(error.message).toContain('packaged ripgrep fallback')
expect(error.message).toContain('winget install BurntSushi.ripgrep.MSVC')
})
test('wrapRipgrepUnavailableError explains missing system ripgrep', () => {
const error = wrapRipgrepUnavailableError(
{ code: 'ENOENT', message: 'spawn rg ENOENT' },
{ mode: 'system', command: 'rg' },
'linux',
)
expect(error.message).toContain('system ripgrep binary was not found on PATH')
expect(error.message).toContain('apt install ripgrep')
})

View File

@@ -28,6 +28,8 @@ type RipgrepConfig = {
argv0?: string
}
type RipgrepErrorLike = Pick<NodeJS.ErrnoException, 'code' | 'message'>
const getRipgrepConfig = memoize((): RipgrepConfig => {
const userWantsSystemRipgrep = isEnvDefinedFalsy(
process.env.USE_BUILTIN_RIPGREP,
@@ -105,6 +107,52 @@ export class RipgrepTimeoutError extends Error {
}
}
export class RipgrepUnavailableError extends Error {
code?: string | number
constructor(
message: string,
public readonly config: Pick<RipgrepConfig, 'mode' | 'command'>,
code?: string | number,
) {
super(message)
this.name = 'RipgrepUnavailableError'
this.code = code
}
}
function getRipgrepInstallHint(platform = process.platform): string {
switch (platform) {
case 'win32':
return 'Install ripgrep and confirm `rg --version` works in the same terminal. Windows: `winget install BurntSushi.ripgrep.MSVC` or `choco install ripgrep`.'
case 'darwin':
return 'Install ripgrep and confirm `rg --version` works in the same terminal. macOS: `brew install ripgrep`.'
default:
return 'Install ripgrep and confirm `rg --version` works in the same terminal. Linux: use your distro package manager, for example `apt install ripgrep`.'
}
}
export function wrapRipgrepUnavailableError(
error: RipgrepErrorLike,
config = getRipgrepConfig(),
platform = process.platform,
): RipgrepUnavailableError {
const modeExplanation =
config.mode === 'builtin'
? 'This install could not locate its packaged ripgrep fallback.'
: config.mode === 'system'
? 'A working system ripgrep binary was not found on PATH.'
: 'The embedded ripgrep binary could not be started.'
const originalMessage = error.message ? ` Original error: ${error.message}` : ''
return new RipgrepUnavailableError(
`ripgrep (rg) is required for file search but could not be started. ${modeExplanation} ${getRipgrepInstallHint(platform)}${originalMessage}`,
config,
error.code,
)
}
function ripGrepRaw(
args: string[],
target: string,
@@ -275,7 +323,9 @@ async function ripGrepFileCount(
child.on('error', err => {
if (settled) return
settled = true
reject(err)
reject(
err.code === 'ENOENT' ? wrapRipgrepUnavailableError(err) : err,
)
})
})
}
@@ -337,7 +387,9 @@ export async function ripGrepStream(
child.on('error', err => {
if (settled) return
settled = true
reject(err)
reject(
err.code === 'ENOENT' ? wrapRipgrepUnavailableError(err) : err,
)
})
})
}
@@ -383,7 +435,9 @@ export async function ripGrep(
// These should be surfaced to the user rather than silently returning empty results
const CRITICAL_ERROR_CODES = ['ENOENT', 'EACCES', 'EPERM']
if (CRITICAL_ERROR_CODES.includes(error.code as string)) {
reject(error)
reject(
error.code === 'ENOENT' ? wrapRipgrepUnavailableError(error) : error,
)
return
}