From 1817aa6a171680f43e3e0aae884119e3df45db22 Mon Sep 17 00:00:00 2001 From: 0xMink <260166390+0xMink@users.noreply.github.com> Date: Thu, 21 May 2026 23:02:55 +0000 Subject: [PATCH 1/7] build: add @vscode/ripgrep as a build dependency --- pnpm-lock.yaml | 3 +++ src/package.json | 1 + 2 files changed, 4 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 15f5e74d0c..1517fa8651 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -788,6 +788,9 @@ importers: '@vitest/coverage-v8': specifier: ^3.2.3 version: 3.2.4(vitest@3.2.4) + '@vscode/ripgrep': + specifier: ^1.17.0 + version: 1.17.0 '@vscode/test-electron': specifier: ^2.5.2 version: 2.5.2 diff --git a/src/package.json b/src/package.json index 9bb5d430e3..66128079f6 100644 --- a/src/package.json +++ b/src/package.json @@ -554,6 +554,7 @@ "@types/tmp": "^0.2.6", "@types/turndown": "^5.0.5", "@types/vscode": "^1.84.0", + "@vscode/ripgrep": "^1.17.0", "@vscode/test-electron": "^2.5.2", "@vscode/vsce": "3.3.2", "ai": "^6.0.75", From bc984af2ea60d338de6a1354c35b8bea050d3a64 Mon Sep 17 00:00:00 2001 From: 0xMink <260166390+0xMink@users.noreply.github.com> Date: Thu, 21 May 2026 23:12:59 +0000 Subject: [PATCH 2/7] build: bundle the ripgrep binary into the extension dist/bin --- src/esbuild.mjs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/esbuild.mjs b/src/esbuild.mjs index 890318cd26..530c426403 100644 --- a/src/esbuild.mjs +++ b/src/esbuild.mjs @@ -85,6 +85,27 @@ async function main() { }) }, }, + { + name: "copyRipgrep", + setup(build) { + build.onEnd(async () => { + // Copy the ripgrep binary into dist/bin/ so it ships inside the + // VSIX. getBinPath() in src/services/ripgrep/index.ts falls back + // to this bundled copy when ripgrep cannot be located in the VS + // Code installation (e.g. VS Code Insiders' staged-install layout). + const { rgPath } = await import("@vscode/ripgrep") + if (!rgPath) { + throw new Error("[copyRipgrep] @vscode/ripgrep did not provide rgPath") + } + const rgDestDir = path.join(distDir, "bin") + fs.mkdirSync(rgDestDir, { recursive: true }) + const rgDest = path.join(rgDestDir, path.basename(rgPath)) + fs.copyFileSync(rgPath, rgDest) + fs.chmodSync(rgDest, 0o755) + console.log(`[copyRipgrep] Copied ${rgPath} to ${rgDest}`) + }) + }, + }, { name: "copyWasms", setup(build) { From 2c32fadeb4dfe9aa6fd5052973d42f24da44b23c Mon Sep 17 00:00:00 2001 From: 0xMink <260166390+0xMink@users.noreply.github.com> Date: Thu, 21 May 2026 23:34:42 +0000 Subject: [PATCH 3/7] fix: fall back to a bundled ripgrep when VS Code's copy is not found --- src/services/ripgrep/__tests__/index.spec.ts | 47 +++++++++++++++++++- src/services/ripgrep/index.ts | 19 +++++++- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/services/ripgrep/__tests__/index.spec.ts b/src/services/ripgrep/__tests__/index.spec.ts index 0c4d79f09e..4df598908a 100644 --- a/src/services/ripgrep/__tests__/index.spec.ts +++ b/src/services/ripgrep/__tests__/index.spec.ts @@ -1,6 +1,16 @@ // npx vitest run src/services/ripgrep/__tests__/index.spec.ts -import { truncateLine } from "../index" +import path from "path" +import { vi, describe, it, expect, beforeEach } from "vitest" + +import { truncateLine, getBinPath, bundledRgPath } from "../index" +import { fileExistsAtPath } from "../../../utils/fs" + +vi.mock("../../../utils/fs", () => ({ + fileExistsAtPath: vi.fn(), +})) + +const mockFileExists = vi.mocked(fileExistsAtPath) describe("Ripgrep line truncation", () => { // The default MAX_LINE_LENGTH is 500 in the implementation @@ -48,3 +58,38 @@ describe("Ripgrep line truncation", () => { expect(truncated).toContain("[truncated...]") }) }) + +describe("getBinPath bundled-ripgrep fallback", () => { + beforeEach(() => { + mockFileExists.mockReset() + }) + + it("falls back to the bundled ripgrep when ripgrep is absent under the VS Code app root", async () => { + // VS Code Insiders' staged-install layout: nothing under appRoot. + mockFileExists.mockImplementation(async (p: string) => p === bundledRgPath) + + const result = await getBinPath("/fake/vscode/app/root") + + expect(result).toBe(bundledRgPath) + }) + + it("prefers VS Code's own ripgrep over the bundled copy", async () => { + const appRoot = "/fake/vscode/app/root" + // Derive the binary name from bundledRgPath so this test tracks the + // module's own platform logic instead of duplicating it. + const vscodeRg = path.join(appRoot, "node_modules/@vscode/ripgrep/bin", path.basename(bundledRgPath)) + mockFileExists.mockImplementation(async (p: string) => p === vscodeRg || p === bundledRgPath) + + const result = await getBinPath(appRoot) + + expect(result).toBe(vscodeRg) + }) + + it("returns undefined when ripgrep exists nowhere", async () => { + mockFileExists.mockResolvedValue(false) + + const result = await getBinPath("/fake/vscode/app/root") + + expect(result).toBeUndefined() + }) +}) diff --git a/src/services/ripgrep/index.ts b/src/services/ripgrep/index.ts index 5dd800ac6f..04230f394e 100644 --- a/src/services/ripgrep/index.ts +++ b/src/services/ripgrep/index.ts @@ -80,7 +80,21 @@ export function truncateLine(line: string, maxLength: number = MAX_LINE_LENGTH): return line.length > maxLength ? line.substring(0, maxLength) + " [truncated...]" : line } /** - * Get the path to the ripgrep binary within the VSCode installation + * Path to the ripgrep binary bundled inside the extension itself. + * + * esbuild copies the platform `rg`/`rg.exe` into `dist/bin/` at build time + * (see the `copyRipgrep` plugin in esbuild.mjs). At runtime this module is + * bundled into `dist/extension.js`, so `__dirname` is the extension's `dist/` + * directory. + */ +export const bundledRgPath = path.join(__dirname, "bin", binName) + +/** + * Get the path to the ripgrep binary. + * + * Prefers the copy shipped inside the VS Code installation; falls back to the + * binary bundled with this extension when the VS Code copy cannot be located + * (e.g. VS Code Insiders' staged-install layout, see microsoft/vscode#252063). */ export async function getBinPath(vscodeAppRoot: string): Promise { const checkPath = async (pkgFolder: string) => { @@ -92,7 +106,8 @@ export async function getBinPath(vscodeAppRoot: string): Promise Date: Thu, 21 May 2026 23:50:01 +0000 Subject: [PATCH 4/7] ci: verify the bundled ripgrep binary is present in the VSIX --- .github/workflows/marketplace-publish.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/marketplace-publish.yml b/.github/workflows/marketplace-publish.yml index 1e0f7d7335..6bd0b09132 100644 --- a/.github/workflows/marketplace-publish.yml +++ b/.github/workflows/marketplace-publish.yml @@ -63,6 +63,7 @@ jobs: grep -q "extension/webview-ui/build/assets/index.js" /tmp/zoo-code-vsix-contents.txt grep -q "extension/assets/codicons/codicon.ttf" /tmp/zoo-code-vsix-contents.txt grep -q "extension/assets/vscode-material-icons/icons/3d.svg" /tmp/zoo-code-vsix-contents.txt + grep -q "extension/dist/bin/rg" /tmp/zoo-code-vsix-contents.txt - name: Validate packaged manifest identity run: | From 4b29feb4be343c2d67b33e3e23cf602d6ef96db8 Mon Sep 17 00:00:00 2001 From: 0xMink <260166390+0xMink@users.noreply.github.com> Date: Thu, 21 May 2026 23:50:27 +0000 Subject: [PATCH 5/7] chore: add changeset for bundled ripgrep fallback --- .changeset/bundle-ripgrep-fallback.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/bundle-ripgrep-fallback.md diff --git a/.changeset/bundle-ripgrep-fallback.md b/.changeset/bundle-ripgrep-fallback.md new file mode 100644 index 0000000000..b7c6a35002 --- /dev/null +++ b/.changeset/bundle-ripgrep-fallback.md @@ -0,0 +1,5 @@ +--- +"zoo-code": patch +--- + +Bundle a ripgrep binary with the extension and fall back to it when ripgrep cannot be located in the VS Code installation. Fixes "Could not find ripgrep binary" errors that break search_files / list_files on VS Code Insiders' newer staged-install layout. From 838fb9107f1f945af95fbbeb5e34eaffb456d1f8 Mon Sep 17 00:00:00 2001 From: 0xMink <260166390+0xMink@users.noreply.github.com> Date: Fri, 22 May 2026 00:39:06 +0000 Subject: [PATCH 6/7] fix: scope the bundled ripgrep path to platform and arch The published VSIX is universal but copyRipgrep bundled only the build host's rg. Because rg (no extension) is the binary name on both Linux and macOS, a macOS user could resolve a Linux-built dist/bin/rg and execute an incompatible binary. Bundle and resolve the binary under dist/bin/-/ so the fallback is used only on a host matching the bundled binary. --- .github/workflows/marketplace-publish.yml | 2 +- src/esbuild.mjs | 14 +++++++++----- src/services/ripgrep/__tests__/index.spec.ts | 7 +++++++ src/services/ripgrep/index.ts | 16 +++++++++++----- 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/.github/workflows/marketplace-publish.yml b/.github/workflows/marketplace-publish.yml index 6bd0b09132..2e6b8ee1bf 100644 --- a/.github/workflows/marketplace-publish.yml +++ b/.github/workflows/marketplace-publish.yml @@ -63,7 +63,7 @@ jobs: grep -q "extension/webview-ui/build/assets/index.js" /tmp/zoo-code-vsix-contents.txt grep -q "extension/assets/codicons/codicon.ttf" /tmp/zoo-code-vsix-contents.txt grep -q "extension/assets/vscode-material-icons/icons/3d.svg" /tmp/zoo-code-vsix-contents.txt - grep -q "extension/dist/bin/rg" /tmp/zoo-code-vsix-contents.txt + grep -qE "extension/dist/bin/[^/]+/rg" /tmp/zoo-code-vsix-contents.txt - name: Validate packaged manifest identity run: | diff --git a/src/esbuild.mjs b/src/esbuild.mjs index 530c426403..4fbb86141a 100644 --- a/src/esbuild.mjs +++ b/src/esbuild.mjs @@ -89,15 +89,19 @@ async function main() { name: "copyRipgrep", setup(build) { build.onEnd(async () => { - // Copy the ripgrep binary into dist/bin/ so it ships inside the - // VSIX. getBinPath() in src/services/ripgrep/index.ts falls back - // to this bundled copy when ripgrep cannot be located in the VS - // Code installation (e.g. VS Code Insiders' staged-install layout). + // Copy the ripgrep binary into dist/bin/-/ so it + // ships inside the universal VSIX. getBinPath() in + // src/services/ripgrep/index.ts falls back to this bundled copy + // when ripgrep cannot be located in the VS Code installation + // (e.g. VS Code Insiders' staged-install layout). The + // - subfolder keeps the runtime fallback from + // picking a binary built for a different OS (the name `rg` is + // shared by Linux and macOS). const { rgPath } = await import("@vscode/ripgrep") if (!rgPath) { throw new Error("[copyRipgrep] @vscode/ripgrep did not provide rgPath") } - const rgDestDir = path.join(distDir, "bin") + const rgDestDir = path.join(distDir, "bin", `${process.platform}-${process.arch}`) fs.mkdirSync(rgDestDir, { recursive: true }) const rgDest = path.join(rgDestDir, path.basename(rgPath)) fs.copyFileSync(rgPath, rgDest) diff --git a/src/services/ripgrep/__tests__/index.spec.ts b/src/services/ripgrep/__tests__/index.spec.ts index 4df598908a..298e83ff4a 100644 --- a/src/services/ripgrep/__tests__/index.spec.ts +++ b/src/services/ripgrep/__tests__/index.spec.ts @@ -92,4 +92,11 @@ describe("getBinPath bundled-ripgrep fallback", () => { expect(result).toBeUndefined() }) + + it("scopes the bundled ripgrep path to the current platform and arch", () => { + // Guards the universal VSIX from handing a wrong-OS binary to the + // fallback: the path must carry a - segment so a + // macOS host never resolves a Linux-built `dist/bin/rg`. + expect(bundledRgPath).toContain(path.join("bin", `${process.platform}-${process.arch}`)) + }) }) diff --git a/src/services/ripgrep/index.ts b/src/services/ripgrep/index.ts index 04230f394e..758799038d 100644 --- a/src/services/ripgrep/index.ts +++ b/src/services/ripgrep/index.ts @@ -82,12 +82,18 @@ export function truncateLine(line: string, maxLength: number = MAX_LINE_LENGTH): /** * Path to the ripgrep binary bundled inside the extension itself. * - * esbuild copies the platform `rg`/`rg.exe` into `dist/bin/` at build time - * (see the `copyRipgrep` plugin in esbuild.mjs). At runtime this module is - * bundled into `dist/extension.js`, so `__dirname` is the extension's `dist/` - * directory. + * esbuild copies the platform `rg`/`rg.exe` into `dist/bin/-/` + * at build time (see the `copyRipgrep` plugin in esbuild.mjs). At runtime this + * module is bundled into `dist/extension.js`, so `__dirname` is the extension's + * `dist/` directory. + * + * The `-` segment is required: the published VSIX is universal + * but carries only the build host's binary. Without it, a macOS user would + * resolve a Linux-built `dist/bin/rg` (the binary name `rg` is shared by Linux + * and macOS) and execute an incompatible binary. With it, the fallback resolves + * only on a host matching the bundled binary. */ -export const bundledRgPath = path.join(__dirname, "bin", binName) +export const bundledRgPath = path.join(__dirname, "bin", `${process.platform}-${process.arch}`, binName) /** * Get the path to the ripgrep binary. From db9f134c914f7373c889e1b9f3b3b29d6d0e8f31 Mon Sep 17 00:00:00 2001 From: 0xMink <260166390+0xMink@users.noreply.github.com> Date: Fri, 22 May 2026 00:51:31 +0000 Subject: [PATCH 7/7] docs: document the checkPath helper in getBinPath --- src/services/ripgrep/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/services/ripgrep/index.ts b/src/services/ripgrep/index.ts index 758799038d..cbfed13b4d 100644 --- a/src/services/ripgrep/index.ts +++ b/src/services/ripgrep/index.ts @@ -103,6 +103,10 @@ export const bundledRgPath = path.join(__dirname, "bin", `${process.platform}-${ * (e.g. VS Code Insiders' staged-install layout, see microsoft/vscode#252063). */ export async function getBinPath(vscodeAppRoot: string): Promise { + /** + * Resolve `/` under the VS Code application root, + * returning the absolute path when the ripgrep binary exists there. + */ const checkPath = async (pkgFolder: string) => { const fullPath = path.join(vscodeAppRoot, pkgFolder, binName) return (await fileExistsAtPath(fullPath)) ? fullPath : undefined