Kode-cli/src/utils/ripgrep.ts
2025-08-10 19:57:17 +08:00

168 lines
4.4 KiB
TypeScript

import { findActualExecutable } from 'spawn-rx'
import { memoize } from 'lodash-es'
import { fileURLToPath, resolve } from 'node:url'
import * as path from 'path'
import { logError } from './log'
import { execFileNoThrow } from './execFileNoThrow'
import { execFile } from 'child_process'
import debug from 'debug'
const __filename = fileURLToPath(import.meta.url)
const __dirname = resolve(
__filename,
process.env.NODE_ENV === 'test' ? '../..' : '.',
)
const d = debug('claude:ripgrep')
const useBuiltinRipgrep = !!process.env.USE_BUILTIN_RIPGREP
if (useBuiltinRipgrep) {
d('Using builtin ripgrep because USE_BUILTIN_RIPGREP is set')
}
const ripgrepPath = memoize(() => {
const { cmd } = findActualExecutable('rg', [])
d(`ripgrep initially resolved as: ${cmd}`)
if (cmd !== 'rg' && !useBuiltinRipgrep) {
// NB: If we're able to find ripgrep in $PATH, cmd will be an absolute
// path rather than just returning 'rg'
return cmd
} else {
// Use the one we ship in-box
const rgRoot = path.resolve(__dirname, 'vendor', 'ripgrep')
if (process.platform === 'win32') {
// NB: Ripgrep doesn't ship an aarch64 binary for Windows, boooooo
return path.resolve(rgRoot, 'x64-win32', 'rg.exe')
}
const ret = path.resolve(
rgRoot,
`${process.arch}-${process.platform}`,
'rg',
)
d('internal ripgrep resolved as: %s', ret)
return ret
}
})
export async function ripGrep(
args: string[],
target: string,
abortSignal: AbortSignal,
): Promise<string[]> {
await codesignRipgrepIfNecessary()
const rg = ripgrepPath()
d('ripgrep called: %s %o', rg, target, args)
// NB: When running interactively, ripgrep does not require a path as its last
// argument, but when run non-interactively, it will hang unless a path or file
// pattern is provided
return new Promise(resolve => {
execFile(
ripgrepPath(),
[...args, target],
{
maxBuffer: 1_000_000,
signal: abortSignal,
timeout: 10_000,
},
(error, stdout) => {
if (error) {
// Exit code 1 from ripgrep means "no matches found" - this is normal
if (error.code !== 1) {
d('ripgrep error: %o', error)
logError(error)
}
resolve([])
} else {
d('ripgrep succeeded with %s', stdout)
resolve(stdout.trim().split('\n').filter(Boolean))
}
},
)
})
}
// NB: We do something tricky here. We know that ripgrep processes common
// ignore files for us, so we just ripgrep for any character, which matches
// all non-empty files
export async function listAllContentFiles(
path: string,
abortSignal: AbortSignal,
limit: number,
): Promise<string[]> {
try {
d('listAllContentFiles called: %s', path)
return (await ripGrep(['-l', '.', path], path, abortSignal)).slice(0, limit)
} catch (e) {
d('listAllContentFiles failed: %o', e)
logError(e)
return []
}
}
let alreadyDoneSignCheck = false
async function codesignRipgrepIfNecessary() {
if (process.platform !== 'darwin' || alreadyDoneSignCheck) {
return
}
alreadyDoneSignCheck = true
// First, check to see if ripgrep is already signed
d('checking if ripgrep is already signed')
const lines = (
await execFileNoThrow(
'codesign',
['-vv', '-d', ripgrepPath()],
undefined,
undefined,
false,
)
).stdout.split('\n')
const needsSigned = lines.find(line => line.includes('linker-signed'))
if (!needsSigned) {
d('seems to be already signed')
return
}
try {
d('signing ripgrep')
const signResult = await execFileNoThrow('codesign', [
'--sign',
'-',
'--force',
'--preserve-metadata=entitlements,requirements,flags,runtime',
ripgrepPath(),
])
if (signResult.code !== 0) {
d('failed to sign ripgrep: %o', signResult)
logError(
`Failed to sign ripgrep: ${signResult.stdout} ${signResult.stderr}`,
)
}
d('removing quarantine')
const quarantineResult = await execFileNoThrow('xattr', [
'-d',
'com.apple.quarantine',
ripgrepPath(),
])
if (quarantineResult.code !== 0) {
d('failed to remove quarantine: %o', quarantineResult)
logError(
`Failed to remove quarantine: ${quarantineResult.stdout} ${quarantineResult.stderr}`,
)
}
} catch (e) {
d('failed during sign: %o', e)
logError(e)
}
}