diff --git a/src/utils/__tests__/bundledMode.test.ts b/src/utils/__tests__/bundledMode.test.ts new file mode 100644 index 0000000000..e83901f440 --- /dev/null +++ b/src/utils/__tests__/bundledMode.test.ts @@ -0,0 +1,32 @@ +import { afterEach, describe, expect, test } from 'bun:test' +import { isBunVirtualPath, isInBundledMode } from '../bundledMode.js' + +const originalArgv = [...process.argv] + +afterEach(() => { + process.argv = [...originalArgv] +}) + +describe('isBunVirtualPath', () => { + test('detects POSIX bun virtual filesystem paths', () => { + expect(isBunVirtualPath('/$bunfs/root/claude-sidecar')).toBe(true) + }) + + test('detects Windows bun virtual filesystem paths', () => { + expect(isBunVirtualPath('B:\\~BUN\\root\\claude-sidecar.exe')).toBe(true) + }) + + test('ignores regular filesystem paths', () => { + expect(isBunVirtualPath('/Applications/Claude.app/Contents/MacOS/claude-sidecar')).toBe(false) + }) +}) + +describe('isInBundledMode', () => { + test('treats bun-compile virtual entrypoints as bundled even when embeddedFiles is empty', () => { + process.argv = ['bun', '/$bunfs/root/claude-sidecar'] + + expect(Array.isArray(Bun.embeddedFiles)).toBe(true) + expect(Bun.embeddedFiles.length).toBe(0) + expect(isInBundledMode()).toBe(true) + }) +}) diff --git a/src/utils/bundledMode.ts b/src/utils/bundledMode.ts index f7e6c4dc33..3d914465b1 100644 --- a/src/utils/bundledMode.ts +++ b/src/utils/bundledMode.ts @@ -9,14 +9,31 @@ export function isRunningWithBun(): boolean { return process.versions.bun !== undefined } +const BUN_VIRTUAL_PATH_MARKERS = ['/$bunfs/', '/~bun/'] + +export function isBunVirtualPath(candidatePath: string | null | undefined): boolean { + if (!candidatePath) { + return false + } + + const normalized = candidatePath.replace(/\\/g, '/').toLowerCase() + return BUN_VIRTUAL_PATH_MARKERS.some(marker => normalized.includes(marker)) +} + /** * Detects if running as a Bun-compiled standalone executable. - * This checks for embedded files which are present in compiled binaries. + * Bun compile loads the entry module from Bun's virtual filesystem + * (`/$bunfs/...` on POSIX, `~BUN\\...` on Windows). `Bun.embeddedFiles` + * alone is not sufficient because compiled binaries can still report an + * empty embedded file list at runtime. */ export function isInBundledMode(): boolean { + if (typeof Bun === 'undefined') { + return false + } + return ( - typeof Bun !== 'undefined' && - Array.isArray(Bun.embeddedFiles) && - Bun.embeddedFiles.length > 0 + isBunVirtualPath(process.argv[1]) || + (Array.isArray(Bun.embeddedFiles) && Bun.embeddedFiles.length > 0) ) } diff --git a/src/utils/ripgrep.ts b/src/utils/ripgrep.ts index 8128aae779..37e0d398be 100644 --- a/src/utils/ripgrep.ts +++ b/src/utils/ripgrep.ts @@ -6,7 +6,7 @@ import { homedir } from 'os' import * as path from 'path' import { logEvent } from 'src/services/analytics/index.js' import { fileURLToPath } from 'url' -import { isInBundledMode } from './bundledMode.js' +import { isBunVirtualPath, isInBundledMode } from './bundledMode.js' import { logForDebugging } from './debug.js' import { isEnvDefinedFalsy } from './envUtils.js' import { execFileNoThrow } from './execFileNoThrow.js' @@ -22,8 +22,6 @@ const __dirname = path.join( process.env.NODE_ENV === 'test' ? '../../../' : '../', ) -const BUN_VIRTUAL_PATH_MARKERS = ['$bunfs', '~BUN'] - type RipgrepConfig = { mode: 'system' | 'builtin' | 'embedded' | 'unavailable' command: string @@ -31,11 +29,6 @@ type RipgrepConfig = { argv0?: string } -function isBunVirtualPath(candidatePath: string): boolean { - const normalized = candidatePath.replace(/\\/g, '/') - return BUN_VIRTUAL_PATH_MARKERS.some(marker => normalized.includes(marker)) -} - export function isUsableBuiltinRipgrepPath(candidatePath: string): boolean { return !isBunVirtualPath(candidatePath) && existsSync(candidatePath) }