Skip to content

Commit 5b04278

Browse files
committed
Fix code search case: strip quotes around flag args
1 parent 1c300a6 commit 5b04278

File tree

2 files changed

+85
-10
lines changed

2 files changed

+85
-10
lines changed

sdk/src/__tests__/code-search.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,70 @@ describe('codeSearch', () => {
650650
expect(spawnArgs[gFlagIndices[1]! + 1]).toBe('*.tsx')
651651
})
652652

653+
it('should strip single quotes from glob pattern arguments (regression: spawn has no shell)', async () => {
654+
const searchPromise = codeSearch({
655+
projectPath: '/test/project',
656+
pattern: 'auth',
657+
flags: "-g 'authentication.knowledge.md'",
658+
})
659+
660+
const output = createRgJsonMatch('authentication.knowledge.md', 5, 'auth content')
661+
662+
mockProcess.stdout.emit('data', Buffer.from(output))
663+
mockProcess.emit('close', 0)
664+
665+
const result = await searchPromise
666+
const value = asCodeSearchResult(result[0])
667+
expect(value.stdout).toContain('authentication.knowledge.md:')
668+
669+
// Verify the quotes were stripped before passing to spawn
670+
const spawnArgs = mockSpawn.mock.calls[0]![1] as string[]
671+
expect(spawnArgs).toContain('authentication.knowledge.md')
672+
expect(spawnArgs).not.toContain("'authentication.knowledge.md'")
673+
})
674+
675+
it('should strip double quotes from glob pattern arguments', async () => {
676+
const searchPromise = codeSearch({
677+
projectPath: '/test/project',
678+
pattern: 'import',
679+
flags: '-g "*.ts"',
680+
})
681+
682+
const output = createRgJsonMatch('file.ts', 1, 'import foo')
683+
684+
mockProcess.stdout.emit('data', Buffer.from(output))
685+
mockProcess.emit('close', 0)
686+
687+
const result = await searchPromise
688+
const value = asCodeSearchResult(result[0])
689+
expect(value.stdout).toContain('file.ts:')
690+
691+
const spawnArgs = mockSpawn.mock.calls[0]![1] as string[]
692+
expect(spawnArgs).toContain('*.ts')
693+
expect(spawnArgs).not.toContain('"*.ts"')
694+
})
695+
696+
it('should strip quotes from multiple glob patterns', async () => {
697+
const searchPromise = codeSearch({
698+
projectPath: '/test/project',
699+
pattern: 'import',
700+
flags: "-g '*.ts' -g '*.tsx'",
701+
})
702+
703+
const output = createRgJsonMatch('file.tsx', 1, 'import React')
704+
705+
mockProcess.stdout.emit('data', Buffer.from(output))
706+
mockProcess.emit('close', 0)
707+
708+
await searchPromise
709+
710+
const spawnArgs = mockSpawn.mock.calls[0]![1] as string[]
711+
expect(spawnArgs).toContain('*.ts')
712+
expect(spawnArgs).toContain('*.tsx')
713+
expect(spawnArgs).not.toContain("'*.ts'")
714+
expect(spawnArgs).not.toContain("'*.tsx'")
715+
})
716+
653717
it('should not deduplicate flag-argument pairs', async () => {
654718
const searchPromise = codeSearch({
655719
projectPath: '/test/project',

sdk/src/tools/code-search.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { formatCodeSearchOutput } from '../../../common/src/util/format-code-sea
66
import { getBundledRgPath } from '../native/ripgrep'
77

88
import type { CodebuffToolOutput } from '../../../common/src/tools/list'
9+
import { Logger } from '@codebuff/common/types/contracts/logger'
910

1011
// Hidden directories to include in code search by default.
1112
// These are searched in addition to '.' to ensure important config/workflow files are discoverable.
@@ -27,6 +28,7 @@ export function codeSearch({
2728
globalMaxResults = 250,
2829
maxOutputStringLength = 20_000,
2930
timeoutSeconds = 10,
31+
logger,
3032
}: {
3133
projectPath: string
3234
pattern: string
@@ -36,6 +38,7 @@ export function codeSearch({
3638
globalMaxResults?: number
3739
maxOutputStringLength?: number
3840
timeoutSeconds?: number
41+
logger?: Logger
3942
}): Promise<CodebuffToolOutput<'code_search'>> {
4043
return new Promise((resolve) => {
4144
let isResolved = false
@@ -61,7 +64,12 @@ export function codeSearch({
6164

6265
// Parse flags - do NOT deduplicate to preserve flag-argument pairs like '-g *.ts'
6366
// Deduplicating would break up these pairs and cause errors
64-
const flagsArray = (flags || '').split(' ').filter(Boolean)
67+
// Strip surrounding quotes from each token since spawn() passes args directly
68+
// without shell interpretation (e.g. "'foo.md'" → "foo.md")
69+
const flagsArray = (flags || '')
70+
.split(' ')
71+
.filter(Boolean)
72+
.map((token) => token.replace(/^['"]|['"]$/g, ''))
6573

6674
// Use JSON output for robust parsing and early stopping
6775
// --no-config prevents user/system .ripgreprc from interfering
@@ -89,6 +97,9 @@ export function codeSearch({
8997
]
9098

9199
const rgPath = getBundledRgPath(import.meta.url)
100+
if (logger) {
101+
logger.info({ rgPath, args, searchCwd }, 'code-search: Spawning ripgrep process')
102+
}
92103
const childProcess = spawn(rgPath, args, {
93104
cwd: searchCwd,
94105
stdio: ['ignore', 'pipe', 'pipe'],
@@ -129,15 +140,15 @@ export function codeSearch({
129140
const hardKill = () => {
130141
try {
131142
childProcess.kill('SIGTERM')
132-
} catch {}
143+
} catch { }
133144
// Store timeout reference so it can be cleared if process closes normally
134145
killTimeoutId = setTimeout(() => {
135146
try {
136147
childProcess.kill('SIGKILL')
137148
} catch {
138149
try {
139150
childProcess.kill()
140-
} catch {}
151+
} catch { }
141152
}
142153
killTimeoutId = null
143154
}, 1000)
@@ -247,7 +258,7 @@ export function codeSearch({
247258
const finalOutput =
248259
formattedOutput.length > maxOutputStringLength
249260
? formattedOutput.substring(0, maxOutputStringLength) +
250-
'\n\n[Output truncated]'
261+
'\n\n[Output truncated]'
251262
: formattedOutput
252263

253264
const limitReason =
@@ -324,10 +335,10 @@ export function codeSearch({
324335
}
325336
}
326337
}
327-
} catch {}
338+
} catch { }
328339
}
329340
}
330-
} catch {}
341+
} catch { }
331342

332343
// Build final output from collected matches
333344
const limitedLines: string[] = []
@@ -369,14 +380,14 @@ export function codeSearch({
369380
const truncatedStdout =
370381
formattedOutput.length > maxOutputStringLength
371382
? formattedOutput.substring(0, maxOutputStringLength) +
372-
'\n\n[Output truncated]'
383+
'\n\n[Output truncated]'
373384
: formattedOutput
374385

375386
const truncatedStderr = stderrBuf
376387
? stderrBuf +
377-
(stderrBuf.length >= Math.floor(maxOutputStringLength / 5)
378-
? '\n\n[Error output truncated]'
379-
: '')
388+
(stderrBuf.length >= Math.floor(maxOutputStringLength / 5)
389+
? '\n\n[Error output truncated]'
390+
: '')
380391
: ''
381392

382393
settle({

0 commit comments

Comments
 (0)