Skip to content

feat: bundled 模式改用 base64 嵌入 ripgrep 二进制替代 argv0 派发#113

Closed
realorange1994 wants to merge 7 commits intoclaude-code-best:mainfrom
realorange1994:main
Closed

feat: bundled 模式改用 base64 嵌入 ripgrep 二进制替代 argv0 派发#113
realorange1994 wants to merge 7 commits intoclaude-code-best:mainfrom
realorange1994:main

Conversation

@realorange1994
Copy link
Copy Markdown

@realorange1994 realorange1994 commented Apr 4, 2026

改动

  • feat: 新增 compile.ts 构建脚本和 ripgrepAsset.ts,将 ripgrep 二进制以 base64 编码嵌入独立可执行文件
  • fix: bundled 模式下 ripgrep 策略从 bun 的 argv0 派发改为 base64 二进制提取(与官方方案一致)
  • fix: bundledMode.ts 新增 BUNDLED_MODE 编译时常量作为备用检测方式
  • chore: 新增 download-ripgrep.ts 脚本用于下载 ripgrep 二进制

背景

此前 bundled 模式下,grep/glob 工具依赖 bun 的 argv0='rg' 派发机制,假设 ripgrep 已静态编译进 bun
二进制中。这种方式不够可靠,且与官方 Claude Code 的实现不一致。

本 PR 切换为官方方案:将 ripgrep 二进制编码为 base64(ripgrepAssetBase64.ts),运行时通过 getRipgrepBinaryPath()
解压到临时目录直接执行,使 bundled 可执行文件中的 grep/glob 工具完全自包含,无需依赖系统 rg

文件变更

  • compile.ts — 构建打包脚本
  • src/utils/ripgrepAsset.ts — 二进制提取逻辑
  • src/utils/ripgrepAssetBase64.ts — base64 编码的 rg 二进制占位文件
  • src/utils/ripgrep.tsgetRipgrepConfig() 改为返回 builtin 模式 + 提取路径
  • src/utils/bundledMode.ts — 新增 BUNDLED_MODE 编译时常量备用检测
  • scripts/download-ripgrep.ts — 下载脚本修复
  • package.json — 新增依赖

测试

  • bun run build 构建成功
  • 打包后的 exe 无需系统 rg 即可正常使用 grep 工具
  • 开发模式下系统/vendor 的 rg 仍正常工作

Summary by CodeRabbit

  • New Features

    • Added ability to compile the application into a standalone executable.
  • Improvements

    • More reliable bundled-mode detection for compiled binaries.
    • Ripgrep is now embedded and extracted for standalone builds to ensure search functionality works after compilation.
    • Updated ripgrep download mirror for improved availability.

Gogs added 4 commits April 4, 2026 11:08
Bun's --compile mode does not support ?url imports or new URL() for
embedding non-.node binary files. This change uses base64 encoding to
bundle ripgrep binaries into the exe.

- compile.ts generates ripgrepAssetBase64.ts at build time, encoding all 5
  platform binaries (windows_x64, darwin_x64, darwin_arm64, linux_x64,
  linux_arm64) as base64 strings
- ripgrepAsset.ts decodes base64 to a temp file at runtime (compiled
  mode) or falls back to SDK's bundled ripgrep path (dev mode)
- ripgrep.ts refactored to async, with ripgrepCommand() now returning a
  Promise
- Added compile script to package.json
- compile.ts: inject BUNDLED_MODE="true" define so bundledMode.ts
  detects compiled exe correctly (Bun.embeddedFiles is always empty)
- bundledMode.ts: check BUNDLED_MODE compile-time flag as primary
  detection, fall back to Bun.embeddedFiles check
- ripgrepAsset.ts: switch from Bun.file() approach (which reads from
  filesystem, not embedded data) to require('./ripgrepAssetBase64.js')
  which loads the embedded base64 data in the compiled exe. Dev mode
  falls back to SDK path. Includes disk caching via version fingerprint.
- ripgrep.ts: in bundled mode, call getRipgrepBinaryPath() to get the
  extracted temp path instead of using process.execPath with argv0
  (which requires native ripgrep embedding we don't have access to)
Bun's --compile mode does not support ?url imports or new URL() for
embedding non-.node binary files. This change uses base64 encoding to
bundle ripgrep binaries into the exe.

- compile.ts generates ripgrepAssetBase64.ts at build time, encoding all 5
  platform binaries (windows_x64, darwin_x64, darwin_arm64, linux_x64,
  linux_arm64) as base64 strings
- ripgrepAsset.ts decodes base64 to a temp file at runtime (compiled
  mode) or falls back to SDK's bundled ripgrep path (dev mode)
- ripgrep.ts refactored to async, with ripgrepCommand() now returning a
  Promise
- Added compile script to package.json
- compile.ts: inject BUNDLED_MODE="true" define so bundledMode.ts
  detects compiled exe correctly (Bun.embeddedFiles is always empty)
- bundledMode.ts: check BUNDLED_MODE compile-time flag as primary
  detection, fall back to Bun.embeddedFiles check
- ripgrepAsset.ts: switch from Bun.file() approach (which reads from
  filesystem, not embedded data) to require('./ripgrepAssetBase64.js')
  which loads the embedded base64 data in the compiled exe. Dev mode
  falls back to SDK path. Includes disk caching via version fingerprint.
- ripgrep.ts: in bundled mode, call getRipgrepBinaryPath() to get the
  extracted temp path instead of using process.execPath with argv0
  (which requires native ripgrep embedding we don't have access to)
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: baf515c2-7205-48b7-8c57-65e244cc85e2

📥 Commits

Reviewing files that changed from the base of the PR and between 00b9a68 and ebc9523.

📒 Files selected for processing (2)
  • src/utils/ripgrepAsset.ts
  • src/utils/ripgrepAssetBase64.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/utils/ripgrepAsset.ts

📝 Walkthrough

Walkthrough

New build tooling adds a Bun-based compile script that embeds platform-specific native modules and ripgrep binaries (as base64), patches an SDK runtime path for compiled mode, and introduces runtime utilities to resolve and extract embedded ripgrep binaries with caching and fallbacks.

Changes

Cohort / File(s) Summary
Build orchestration
compile.ts, package.json
Added compile.ts to assemble a standalone Bun executable: derive --define flags, set BUNDLED_MODE:"true", collect --feature flags, embed native .node modules via *_NODE_PATH env vars, generate ripgrep base64 asset file, patch SDK cli.js, and run bun build --compile. Added compile script to package.json.
Ripgrep asset generation
src/utils/ripgrepAssetBase64.ts (generated by compile.ts)
New generated file exporting RIPGREP_BINARIES: Record<string,string> containing platform/arch base64-encoded ripgrep blobs.
Ripgrep runtime & config
src/utils/ripgrepAsset.ts, src/utils/ripgrep.ts
New ripgrepAsset.ts exports getRipgrepBinaryPath() and preloadRipgrepBinary() to resolve/extract base64-embedded ripgrep to a deterministic temp location with on-disk version caching; ripgrep.ts switched bundled mode handling from re-invoking process.execPath to using the dedicated binary path.
Bundled-mode detection
src/utils/bundledMode.ts
Added `declare const BUNDLED_MODE: string
Download script
scripts/download-ripgrep.ts
Updated ripgrep prebuilt download base URL to use https://gh-proxy.com/... proxy endpoint.
SDK patching
(patched file inside node_modules/@anthropic-ai/claude-agent-sdk/.../cli.js)
compile.ts applies a runtime patch to the SDK's cached cli.js changing the ripgrep base-dir computation in --compile mode to use path.dirname(process.execPath).

Sequence Diagram(s)

sequenceDiagram
  participant Dev as Developer / CLI
  participant Compile as compile.ts
  participant FS as File System
  participant SDK as Claude Agent SDK (cli.js)
  participant Bun as Bun (build)
  Note over Compile,FS: compile.ts run via `bun run compile.ts`
  Dev->>Compile: invoke compile
  Compile->>FS: read SDK ripgrep binary for platform/arch
  FS-->>Compile: ripgrep binary bytes
  Compile->>FS: write `src/utils/ripgrepAssetBase64.ts` (base64 map)
  Compile->>FS: patch SDK `cli.js` in node_modules
  Compile->>Bun: spawn `bun build --compile` with defines, features, env
  Bun->>FS: read embedded native modules and asset map
  Bun-->>Compile: build success / failure
  alt failure
    Compile->>Dev: exit with code 1
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through code with a tiny drum,

Packed rg in base64, tucked it in my thumb,
I patched the SDK and called Bun to bake,
One executable out—no network to take! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is written in Chinese and describes the core change: replacing argv0 dispatch with base64-embedded ripgrep binaries in bundled mode. It accurately reflects the main objective of this PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/utils/ripgrep.ts (1)

25-30: 🛠️ Refactor suggestion | 🟠 Major

Remove dead 'embedded' mode from type union—it's never returned by getRipgrepConfig().

The RipgrepConfig type includes 'embedded' as a mode, but all code paths in getRipgrepConfig() return only 'system' or 'builtin'. This creates dead code and confuses maintainers.

Proposed fixes needed in three locations
  1. ripgrep.ts line 26 (RipgrepConfig type):
-  mode: 'system' | 'builtin' | 'embedded'
+  mode: 'system' | 'builtin'
  1. ripgrep.ts line 533 (getRipgrepStatus return type):
-  mode: 'system' | 'builtin' | 'embedded'
+  mode: 'system' | 'builtin'
  1. doctorDiagnostic.ts line 68 (DiagnosticInfo.ripgrepStatus type):
-    mode: 'system' | 'builtin' | 'embedded'
+    mode: 'system' | 'builtin'

Additionally, Doctor.tsx line 315 contains a dead code check (mode === "embedded") that should also be cleaned up once the type is narrowed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/ripgrep.ts` around lines 25 - 30, Remove the dead 'embedded'
variant everywhere: update the RipgrepConfig type (remove 'embedded' from mode
union), change the return type of getRipgrepStatus to only allow 'system' |
'builtin', and adjust DiagnosticInfo.ripgrepStatus to the narrowed union; then
delete the dead branch in Doctor.tsx that checks for mode === "embedded". Ensure
all references to the embedded mode (type literals, conditional checks) are
removed or updated so the code and types only reflect 'system' and 'builtin' and
re-run type checks.
🧹 Nitpick comments (1)
src/utils/ripgrepAsset.ts (1)

44-54: Dead code: ensureBase64Loaded() is never called.

The async function ensureBase64Loaded() and the globalBase64 variable are defined but never used. The synchronous path in getRipgrepBinaryPath() uses require() directly instead. Similarly, preloadRipgrepBinary() at lines 120-122 claims to be async but just calls the sync function.

♻️ Proposed fix to remove dead code or implement proper async preloading

Either remove the unused async machinery:

-// Global base64 data — loaded once on first access
-let globalBase64: Record<string, string> | null = null
-
 ...
-/**
- * Load base64 data asynchronously (first call only).
- * Subsequent calls use the cached global.
- */
-async function ensureBase64Loaded(): Promise<Record<string, string>> {
-  if (globalBase64 !== null) return globalBase64
-  // Dynamic import so the 6.9MB base64 string isn't loaded in dev mode
-  const mod = await import('./ripgrepAssetBase64.js')
-  globalBase64 = mod.RIPGREP_BINARIES ?? {}
-  return globalBase64
-}

Or implement proper async preloading:

 export async function preloadRipgrepBinary(): Promise<void> {
-  getRipgrepBinaryPath()
+  await ensureBase64Loaded()
+  getRipgrepBinaryPath()
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/ripgrepAsset.ts` around lines 44 - 54, The async helper
ensureBase64Loaded and the globalBase64 cache are dead code because
getRipgrepBinaryPath uses a synchronous require and preloadRipgrepBinary only
calls the sync path; either remove ensureBase64Loaded and globalBase64 and make
preloadRipgrepBinary a simple sync noop (or delete it), or refactor
getRipgrepBinaryPath to use ensureBase64Loaded (await it) and make
preloadRipgrepBinary call await ensureBase64Loaded so the module can be
preloaded asynchronously; update references to ensureBase64Loaded, globalBase64,
getRipgrepBinaryPath, and preloadRipgrepBinary accordingly so there are no
unused symbols left.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@compile.ts`:
- Around line 83-86: The hardcoded vendor path in generateRipgrepAsset (and the
other occurrences) will break on upgrades; replace the literal join(...) that
embeds `@anthropic-ai+claude-agent-sdk@0.2.87+...` with dynamic module
resolution (e.g., use require.resolve or import.meta.resolve to locate the
package root and then append `/vendor/ripgrep`), so the code discovers the
installed `@anthropic-ai/claude-agent-sdk` location at runtime; apply the same
change to the other occurrences (the generateRipgrepAsset function and the
references in src/utils/ripgrepAsset.ts) so they all derive the SDK path
dynamically instead of using a pinned version string.
- Around line 139-153: The regex-based patching of the minified SDK (operating
on sdkContent to produce patchedSdk using patterns matching Uy_ and dy_=Uy_) is
brittle and currently only emits a console.warn if no replacement occurred;
change this to fail the build by throwing or exiting with a non-zero status when
patchedSdk === sdkContent so the CI stops on mismatch, and add a clear error
message including sdkCachePath and the original fragment names (Uy_, dy_=Uy_) to
aid debugging; additionally, replace this fragile approach in the long term by
switching the patching logic from regex to an AST-based transform (or at minimum
add an SDK version check and a documented targeted version comment near the
patch code and function writeFile usage) so future minifier/name changes are
detected and handled safely.
- Around line 50-68: nativeNodePaths contains hardcoded arm64-darwin values
causing cross-platform build failures; update the path construction for
COMPUTER_USE_INPUT_NODE_PATH, COMPUTER_USE_SWIFT_NODE_PATH, MODIFIERS_NODE_PATH,
and URL_HANDLER_NODE_PATH inside the nativeNodePaths object to choose
architecture/platform dynamically (use process.arch and process.platform like
the AUDIO_CAPTURE_NODE_PATH/IMAGE_PROCESSOR_NODE_PATH entries) so the
join(repoRoot, ...) paths point to the correct prebuild/vendor subfolders for
the current platform; ensure any macOS-only modules still guard runtime loads
with process.platform checks and that the join and repoRoot symbols remain used
for path assembly.

In `@scripts/download-ripgrep.ts`:
- Line 21: Replace the third‑party proxy BASE_URL (`BASE_URL =
...gh-proxy.com...`) to use the official GitHub release URL constructed with
RG_VERSION, and add SHA256 checksum verification after the file is downloaded:
create an EXPECTED_CHECKSUMS mapping keyed by the release asset filename,
compute the downloaded file's SHA256, compare against EXPECTED_CHECKSUMS entry,
and abort (throw/log and exit) if the checksum does not match before any
extraction or execution; update the download/verify flow where the asset is
fetched to perform this check.

In `@src/utils/bundledMode.ts`:
- Around line 12-21: The docstring for isInBundledMode is inconsistent with the
implementation: the code checks the compile-time BUNDLED_MODE first and
Bun.embeddedFiles second, so update the comment to state "Primary check:
BUNDLED_MODE (compile-time constant) and Fallback: Bun.embeddedFiles" and adjust
any wording to reference the BUNDLED_MODE declaration and the isInBundledMode
function so the comment accurately reflects the code flow.

In `@src/utils/ripgrep.ts`:
- Around line 48-52: The current bundled extraction branch in isInBundledMode()
returns mode: 'builtin' (see getRipgrepBinaryPath() usage), which now maps to
"vendor" in the Doctor UI and misrepresents compiled executables; change the
returned mode value from 'builtin' to a distinct value that preserves the
previous UI label (e.g., 'embedded' or 'bundled') or add a new mode like
'bundled' so Doctor.tsx's mapping (which currently translates
'embedded'→'bundled' and 'builtin'→'vendor') can correctly display
compiled/embedded ripgrep; update the return in the bundled branch to use that
chosen mode and adjust any consumers expecting 'builtin' as needed.

In `@src/utils/ripgrepAsset.ts`:
- Around line 96-105: The cache fast-path returns filePath after calling
readFileSync(filePath) without verifying the file contents; change the check so
that after confirming storedTag === versionTag you read the file into a variable
(using readFileSync) and validate it is non-empty (size/length > 0) and ideally
stat the file (fs.statSync(filePath)) to ensure size > 0 and executable
permission before setting extractedPaths[key] = filePath and returning; on any
validation failure fall through to the extraction path and keep the existing
try/catch behavior.
- Around line 32-38: getPlatformKey currently always returns 'windows_x64' for
Windows and ignores process.arch, breaking Windows ARM64 support; update
getPlatformKey (which calls getPlatform and uses process.arch) to return
'windows_arm64' when getPlatform() === 'windows' and process.arch === 'arm64',
otherwise keep existing mappings for darwin and linux; after this change also
add 'windows_arm64' to the allPlatforms list in compile.ts and ensure the ARM64
ripgrep binary is sourced for packaging.
- Around line 107-113: The extracted ripgrep binary lacks execute permission
after writeFileSync, causing EACCES when running on Unix; after writing the file
at filePath (in the extraction routine in src/utils/ripgrepAsset.ts where
tmpDir, base64Data, writeFileSync, versionPath, versionTag, and extractedPaths
are used), set executable permissions (e.g. chmod 0o755) on filePath—use
fs.chmodSync or fs.promises.chmod (with a safe try/catch) so macOS/Linux can
execute the rg binary while leaving Windows behavior unchanged.

---

Outside diff comments:
In `@src/utils/ripgrep.ts`:
- Around line 25-30: Remove the dead 'embedded' variant everywhere: update the
RipgrepConfig type (remove 'embedded' from mode union), change the return type
of getRipgrepStatus to only allow 'system' | 'builtin', and adjust
DiagnosticInfo.ripgrepStatus to the narrowed union; then delete the dead branch
in Doctor.tsx that checks for mode === "embedded". Ensure all references to the
embedded mode (type literals, conditional checks) are removed or updated so the
code and types only reflect 'system' and 'builtin' and re-run type checks.

---

Nitpick comments:
In `@src/utils/ripgrepAsset.ts`:
- Around line 44-54: The async helper ensureBase64Loaded and the globalBase64
cache are dead code because getRipgrepBinaryPath uses a synchronous require and
preloadRipgrepBinary only calls the sync path; either remove ensureBase64Loaded
and globalBase64 and make preloadRipgrepBinary a simple sync noop (or delete
it), or refactor getRipgrepBinaryPath to use ensureBase64Loaded (await it) and
make preloadRipgrepBinary call await ensureBase64Loaded so the module can be
preloaded asynchronously; update references to ensureBase64Loaded, globalBase64,
getRipgrepBinaryPath, and preloadRipgrepBinary accordingly so there are no
unused symbols left.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3dffce1e-6b62-4412-bbfe-30a65b8abcb6

📥 Commits

Reviewing files that changed from the base of the PR and between 6fefff2 and 00b9a68.

📒 Files selected for processing (7)
  • compile.ts
  • package.json
  • scripts/download-ripgrep.ts
  • src/utils/bundledMode.ts
  • src/utils/ripgrep.ts
  • src/utils/ripgrepAsset.ts
  • src/utils/ripgrepAssetBase64.ts

Comment on lines +50 to +68
const nativeNodePaths: Record<string, string> = {
// @ant packages — macOS only. Path is used at compile time for Bun asset embedding.
// Runtime: TS files check process.platform !== "darwin" and skip native load.
COMPUTER_USE_INPUT_NODE_PATH: join(repoRoot,
"packages/@ant/computer-use-input/prebuilds/arm64-darwin/computer-use-input.node"),
COMPUTER_USE_SWIFT_NODE_PATH: join(repoRoot,
"packages/@ant/computer-use-swift/prebuilds/arm64-darwin/computer_use.node"),

// vendor modules — cross-platform (win32/linux/darwin)
AUDIO_CAPTURE_NODE_PATH: join(repoRoot,
`vendor/audio-capture/${process.arch}-${process.platform}/audio-capture.node`),
IMAGE_PROCESSOR_NODE_PATH: join(repoRoot,
`vendor/image-processor/${process.arch}-${process.platform}/image-processor.node`),
// modifiers and url-handler are macOS only — paths point to darwin builds
MODIFIERS_NODE_PATH: join(repoRoot,
`vendor/modifiers-napi/${process.arch}-darwin/modifiers.node`),
URL_HANDLER_NODE_PATH: join(repoRoot,
`vendor/url-handler/${process.arch}-darwin/url-handler.node`),
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Cross-platform build will fail: hardcoded arm64-darwin paths.

The native module paths for COMPUTER_USE_INPUT_NODE_PATH, COMPUTER_USE_SWIFT_NODE_PATH, MODIFIERS_NODE_PATH, and URL_HANDLER_NODE_PATH are hardcoded to arm64-darwin. Building on Windows or Linux will reference non-existent paths, causing Bun to fail when trying to embed these assets.

♻️ Proposed fix for platform-aware paths
 const nativeNodePaths: Record<string, string> = {
     // `@ant` packages — macOS only. Path is used at compile time for Bun asset embedding.
     // Runtime: TS files check process.platform !== "darwin" and skip native load.
-    COMPUTER_USE_INPUT_NODE_PATH: join(repoRoot,
-        "packages/@ant/computer-use-input/prebuilds/arm64-darwin/computer-use-input.node"),
-    COMPUTER_USE_SWIFT_NODE_PATH: join(repoRoot,
-        "packages/@ant/computer-use-swift/prebuilds/arm64-darwin/computer_use.node"),
+    ...(process.platform === 'darwin' ? {
+        COMPUTER_USE_INPUT_NODE_PATH: join(repoRoot,
+            `packages/@ant/computer-use-input/prebuilds/${process.arch}-darwin/computer-use-input.node`),
+        COMPUTER_USE_SWIFT_NODE_PATH: join(repoRoot,
+            `packages/@ant/computer-use-swift/prebuilds/${process.arch}-darwin/computer_use.node`),
+    } : {}),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const nativeNodePaths: Record<string, string> = {
// @ant packages — macOS only. Path is used at compile time for Bun asset embedding.
// Runtime: TS files check process.platform !== "darwin" and skip native load.
COMPUTER_USE_INPUT_NODE_PATH: join(repoRoot,
"packages/@ant/computer-use-input/prebuilds/arm64-darwin/computer-use-input.node"),
COMPUTER_USE_SWIFT_NODE_PATH: join(repoRoot,
"packages/@ant/computer-use-swift/prebuilds/arm64-darwin/computer_use.node"),
// vendor modules — cross-platform (win32/linux/darwin)
AUDIO_CAPTURE_NODE_PATH: join(repoRoot,
`vendor/audio-capture/${process.arch}-${process.platform}/audio-capture.node`),
IMAGE_PROCESSOR_NODE_PATH: join(repoRoot,
`vendor/image-processor/${process.arch}-${process.platform}/image-processor.node`),
// modifiers and url-handler are macOS only — paths point to darwin builds
MODIFIERS_NODE_PATH: join(repoRoot,
`vendor/modifiers-napi/${process.arch}-darwin/modifiers.node`),
URL_HANDLER_NODE_PATH: join(repoRoot,
`vendor/url-handler/${process.arch}-darwin/url-handler.node`),
};
const nativeNodePaths: Record<string, string> = {
// `@ant` packages — macOS only. Path is used at compile time for Bun asset embedding.
// Runtime: TS files check process.platform !== "darwin" and skip native load.
...(process.platform === 'darwin' ? {
COMPUTER_USE_INPUT_NODE_PATH: join(repoRoot,
`packages/@ant/computer-use-input/prebuilds/${process.arch}-darwin/computer-use-input.node`),
COMPUTER_USE_SWIFT_NODE_PATH: join(repoRoot,
`packages/@ant/computer-use-swift/prebuilds/${process.arch}-darwin/computer_use.node`),
} : {}),
// vendor modules — cross-platform (win32/linux/darwin)
AUDIO_CAPTURE_NODE_PATH: join(repoRoot,
`vendor/audio-capture/${process.arch}-${process.platform}/audio-capture.node`),
IMAGE_PROCESSOR_NODE_PATH: join(repoRoot,
`vendor/image-processor/${process.arch}-${process.platform}/image-processor.node`),
// modifiers and url-handler are macOS only — paths point to darwin builds
MODIFIERS_NODE_PATH: join(repoRoot,
`vendor/modifiers-napi/${process.arch}-darwin/modifiers.node`),
URL_HANDLER_NODE_PATH: join(repoRoot,
`vendor/url-handler/${process.arch}-darwin/url-handler.node`),
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@compile.ts` around lines 50 - 68, nativeNodePaths contains hardcoded
arm64-darwin values causing cross-platform build failures; update the path
construction for COMPUTER_USE_INPUT_NODE_PATH, COMPUTER_USE_SWIFT_NODE_PATH,
MODIFIERS_NODE_PATH, and URL_HANDLER_NODE_PATH inside the nativeNodePaths object
to choose architecture/platform dynamically (use process.arch and
process.platform like the AUDIO_CAPTURE_NODE_PATH/IMAGE_PROCESSOR_NODE_PATH
entries) so the join(repoRoot, ...) paths point to the correct prebuild/vendor
subfolders for the current platform; ensure any macOS-only modules still guard
runtime loads with process.platform checks and that the join and repoRoot
symbols remain used for path assembly.

Comment on lines +83 to +86
async function generateRipgrepAsset() {
const rgCache = join(repoRoot,
`node_modules/.bun/@anthropic-ai+claude-agent-sdk@0.2.87+3c5d820c62823f0b/node_modules/@anthropic-ai/claude-agent-sdk/vendor/ripgrep`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Hardcoded SDK version will break on upgrades.

The SDK path includes a pinned version string @0.2.87+3c5d820c62823f0b that will become invalid when the dependency is upgraded. This pattern appears in multiple files:

  • compile.ts:85
  • compile.ts:136-137
  • src/utils/ripgrepAsset.ts:27-29
♻️ Proposed fix using dynamic resolution
+import { resolve as resolvePath } from 'path'
+
+// Dynamically resolve SDK path using require.resolve
+function getSdkPath(): string {
+  try {
+    // This resolves through node_modules correctly
+    const sdkMain = require.resolve('@anthropic-ai/claude-agent-sdk')
+    return resolvePath(sdkMain, '..')
+  } catch {
+    throw new Error('Could not resolve `@anthropic-ai/claude-agent-sdk`')
+  }
+}
+
 async function generateRipgrepAsset() {
-    const rgCache = join(repoRoot,
-        `node_modules/.bun/@anthropic-ai+claude-agent-sdk@0.2.87+3c5d820c62823f0b/node_modules/@anthropic-ai/claude-agent-sdk/vendor/ripgrep`);
+    const sdkRoot = getSdkPath()
+    const rgCache = join(sdkRoot, 'vendor/ripgrep')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@compile.ts` around lines 83 - 86, The hardcoded vendor path in
generateRipgrepAsset (and the other occurrences) will break on upgrades; replace
the literal join(...) that embeds `@anthropic-ai+claude-agent-sdk@0.2.87+...`
with dynamic module resolution (e.g., use require.resolve or import.meta.resolve
to locate the package root and then append `/vendor/ripgrep`), so the code
discovers the installed `@anthropic-ai/claude-agent-sdk` location at runtime;
apply the same change to the other occurrences (the generateRipgrepAsset
function and the references in src/utils/ripgrepAsset.ts) so they all derive the
SDK path dynamically instead of using a pinned version string.

Comment on lines +139 to +153
const patchedSdk = sdkContent
.replace(
/import\{fileURLToPath as Uy_\}from"url";/,
";",
)
.replace(
/dy_=Uy_\(import\.meta\.url\),dy_=Z16\.join\(dy_,"\.\/"\)/,
"dy_=Z16.dirname(process.execPath)",
);
if (patchedSdk === sdkContent) {
console.warn("Warning: SDK patch did not match");
} else {
await writeFile(sdkCachePath, patchedSdk);
console.log("Patched SDK cli.js (bun cache)");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fragile regex patching of minified SDK code.

Patching minified JavaScript using regex patterns like Uy_ and dy_=Uy_ is extremely brittle. Any SDK update that changes minification output, variable naming, or code structure will silently break this patch. The warning at line 148-149 only logs; the build continues with potentially broken ripgrep resolution.

Consider:

  1. Failing the build if patch doesn't match instead of just warning
  2. Using AST-based patching for robustness
  3. Documenting the exact SDK version this targets and adding version checks
🛡️ Proposed fix to fail on patch mismatch
     if (patchedSdk === sdkContent) {
-        console.warn("Warning: SDK patch did not match");
+        console.error("ERROR: SDK patch did not match. The SDK may have been updated.");
+        console.error("Expected patterns: 'import{fileURLToPath as Uy_}from\"url\"' and 'dy_=Uy_(import.meta.url)'");
+        exit(1);
     } else {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const patchedSdk = sdkContent
.replace(
/import\{fileURLToPath as Uy_\}from"url";/,
";",
)
.replace(
/dy_=Uy_\(import\.meta\.url\),dy_=Z16\.join\(dy_,"\.\/"\)/,
"dy_=Z16.dirname(process.execPath)",
);
if (patchedSdk === sdkContent) {
console.warn("Warning: SDK patch did not match");
} else {
await writeFile(sdkCachePath, patchedSdk);
console.log("Patched SDK cli.js (bun cache)");
}
if (patchedSdk === sdkContent) {
console.error("ERROR: SDK patch did not match. The SDK may have been updated.");
console.error("Expected patterns: 'import{fileURLToPath as Uy_}from\"url\"' and 'dy_=Uy_(import.meta.url)'");
process.exit(1);
} else {
await writeFile(sdkCachePath, patchedSdk);
console.log("Patched SDK cli.js (bun cache)");
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@compile.ts` around lines 139 - 153, The regex-based patching of the minified
SDK (operating on sdkContent to produce patchedSdk using patterns matching Uy_
and dy_=Uy_) is brittle and currently only emits a console.warn if no
replacement occurred; change this to fail the build by throwing or exiting with
a non-zero status when patchedSdk === sdkContent so the CI stops on mismatch,
and add a clear error message including sdkCachePath and the original fragment
names (Uy_, dy_=Uy_) to aid debugging; additionally, replace this fragile
approach in the long term by switching the patching logic from regex to an
AST-based transform (or at minimum add an SDK version check and a documented
targeted version comment near the patch code and function writeFile usage) so
future minifier/name changes are detected and handled safely.


const RG_VERSION = '15.0.1'
const BASE_URL = `https://github.com/microsoft/ripgrep-prebuilt/releases/download/v${RG_VERSION}`
const BASE_URL = `https://gh-proxy.com/github.com/microsoft/ripgrep-prebuilt/releases/download/v${RG_VERSION}`
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Supply chain risk: using third-party GitHub proxy.

Using gh-proxy.com introduces a man-in-the-middle vector. If this proxy is compromised, malicious binaries could be served. Consider:

  1. Using the official GitHub URL (https://github.com/microsoft/ripgrep-prebuilt/releases/download/...)
  2. If proxy is needed for regional access, add checksum verification after download
🔒 Proposed fix using official GitHub URL
-const BASE_URL = `https://gh-proxy.com/github.com/microsoft/ripgrep-prebuilt/releases/download/v${RG_VERSION}`
+const BASE_URL = `https://github.com/microsoft/ripgrep-prebuilt/releases/download/v${RG_VERSION}`

If regional access is needed, consider adding SHA256 verification:

const EXPECTED_CHECKSUMS: Record<string, string> = {
  'ripgrep-v15.0.1-aarch64-apple-darwin.tar.gz': '<sha256>',
  // ... other platforms
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/download-ripgrep.ts` at line 21, Replace the third‑party proxy
BASE_URL (`BASE_URL = ...gh-proxy.com...`) to use the official GitHub release
URL constructed with RG_VERSION, and add SHA256 checksum verification after the
file is downloaded: create an EXPECTED_CHECKSUMS mapping keyed by the release
asset filename, compute the downloaded file's SHA256, compare against
EXPECTED_CHECKSUMS entry, and abort (throw/log and exit) if the checksum does
not match before any extraction or execution; update the download/verify flow
where the asset is fetched to perform this check.

Comment on lines 12 to +21
/**
* Detects if running as a Bun-compiled standalone executable.
* This checks for embedded files which are present in compiled binaries.
*
* Primary check: Bun.embeddedFiles (present in compiled binaries).
* Fallback: BUNDLED_MODE compile-time constant injected by compile.ts.
*/
// BUNDLED_MODE is injected at compile time by compile.ts --define flag.
declare const BUNDLED_MODE: string | undefined
export function isInBundledMode(): boolean {
if (typeof BUNDLED_MODE !== 'undefined') return true
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Docstring is inconsistent with implementation.

The comment says "Primary check: Bun.embeddedFiles" and "Fallback: BUNDLED_MODE", but the implementation does the opposite—BUNDLED_MODE is checked first (line 21) and Bun.embeddedFiles is the fallback.

📝 Proposed fix for docstring accuracy
 /**
  * Detects if running as a Bun-compiled standalone executable.
  *
- * Primary check: Bun.embeddedFiles (present in compiled binaries).
- * Fallback: BUNDLED_MODE compile-time constant injected by compile.ts.
+ * Primary check: BUNDLED_MODE compile-time constant injected by compile.ts.
+ * Fallback: Bun.embeddedFiles (present in compiled binaries with assets).
  */
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Detects if running as a Bun-compiled standalone executable.
* This checks for embedded files which are present in compiled binaries.
*
* Primary check: Bun.embeddedFiles (present in compiled binaries).
* Fallback: BUNDLED_MODE compile-time constant injected by compile.ts.
*/
// BUNDLED_MODE is injected at compile time by compile.ts --define flag.
declare const BUNDLED_MODE: string | undefined
export function isInBundledMode(): boolean {
if (typeof BUNDLED_MODE !== 'undefined') return true
/**
* Detects if running as a Bun-compiled standalone executable.
*
* Primary check: BUNDLED_MODE compile-time constant injected by compile.ts.
* Fallback: Bun.embeddedFiles (present in compiled binaries with assets).
*/
// BUNDLED_MODE is injected at compile time by compile.ts --define flag.
declare const BUNDLED_MODE: string | undefined
export function isInBundledMode(): boolean {
if (typeof BUNDLED_MODE !== 'undefined') return true
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/bundledMode.ts` around lines 12 - 21, The docstring for
isInBundledMode is inconsistent with the implementation: the code checks the
compile-time BUNDLED_MODE first and Bun.embeddedFiles second, so update the
comment to state "Primary check: BUNDLED_MODE (compile-time constant) and
Fallback: Bun.embeddedFiles" and adjust any wording to reference the
BUNDLED_MODE declaration and the isInBundledMode function so the comment
accurately reflects the code flow.

Comment on lines +48 to +52
// In bundled mode (compiled exe), ripgrep is embedded via base64.
// Extract to temp and execute from there.
if (isInBundledMode()) {
return {
mode: 'embedded',
command: process.execPath,
args: ['--no-config'],
argv0: 'rg',
}
const rgPath = getRipgrepBinaryPath()
return { mode: 'builtin', command: rgPath, args: [] }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Bundled mode now displays as "vendor" in Doctor UI.

Per context snippet 2 (src/screens/Doctor.tsx:313-320), the Doctor diagnostic maps modes:

  • "embedded""bundled" (old behavior)
  • "builtin""vendor" (new behavior)

With this change, compiled executables will show as "vendor" rather than "bundled", which may confuse users expecting to see bundled mode indicated. Consider whether the Doctor UI mapping should be updated or if a distinct mode value is needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/ripgrep.ts` around lines 48 - 52, The current bundled extraction
branch in isInBundledMode() returns mode: 'builtin' (see getRipgrepBinaryPath()
usage), which now maps to "vendor" in the Doctor UI and misrepresents compiled
executables; change the returned mode value from 'builtin' to a distinct value
that preserves the previous UI label (e.g., 'embedded' or 'bundled') or add a
new mode like 'bundled' so Doctor.tsx's mapping (which currently translates
'embedded'→'bundled' and 'builtin'→'vendor') can correctly display
compiled/embedded ripgrep; update the return in the bundled branch to use that
chosen mode and adjust any consumers expecting 'builtin' as needed.

Comment on lines +32 to +38
function getPlatformKey(): string {
const platform = getPlatform()
const arch = process.arch
if (platform === 'windows') return 'windows_x64'
if (platform === 'macos') return arch === 'arm64' ? 'darwin_arm64' : 'darwin_x64'
return arch === 'arm64' ? 'linux_arm64' : 'linux_x64'
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Windows ARM64 architecture not supported.

getPlatformKey() always returns 'windows_x64' for Windows regardless of process.arch. ARM64 Windows devices (e.g., Surface Pro X, newer Qualcomm-based laptops) would attempt to run an x64 binary, which may fail or run under emulation with degraded performance.

♻️ Proposed fix to support Windows ARM64
 function getPlatformKey(): string {
   const platform = getPlatform()
   const arch = process.arch
-  if (platform === 'windows') return 'windows_x64'
+  if (platform === 'windows') return arch === 'arm64' ? 'windows_arm64' : 'windows_x64'
   if (platform === 'macos') return arch === 'arm64' ? 'darwin_arm64' : 'darwin_x64'
   return arch === 'arm64' ? 'linux_arm64' : 'linux_x64'
 }

Note: This also requires adding 'windows_arm64' to allPlatforms in compile.ts and sourcing the ARM64 ripgrep binary.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function getPlatformKey(): string {
const platform = getPlatform()
const arch = process.arch
if (platform === 'windows') return 'windows_x64'
if (platform === 'macos') return arch === 'arm64' ? 'darwin_arm64' : 'darwin_x64'
return arch === 'arm64' ? 'linux_arm64' : 'linux_x64'
}
function getPlatformKey(): string {
const platform = getPlatform()
const arch = process.arch
if (platform === 'windows') return arch === 'arm64' ? 'windows_arm64' : 'windows_x64'
if (platform === 'macos') return arch === 'arm64' ? 'darwin_arm64' : 'darwin_x64'
return arch === 'arm64' ? 'linux_arm64' : 'linux_x64'
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/ripgrepAsset.ts` around lines 32 - 38, getPlatformKey currently
always returns 'windows_x64' for Windows and ignores process.arch, breaking
Windows ARM64 support; update getPlatformKey (which calls getPlatform and uses
process.arch) to return 'windows_arm64' when getPlatform() === 'windows' and
process.arch === 'arm64', otherwise keep existing mappings for darwin and linux;
after this change also add 'windows_arm64' to the allPlatforms list in
compile.ts and ensure the ARM64 ripgrep binary is sourced for packaging.

Comment on lines +96 to +105
// Fast cache check: read only the version tag (~50 bytes)
try {
const storedTag = readFileSync(versionPath, 'utf8')
if (storedTag === versionTag && readFileSync(filePath)) {
extractedPaths[key] = filePath
return filePath
}
} catch {
// Cache miss or stale
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Cache validation doesn't verify binary integrity.

The cache check at line 99 calls readFileSync(filePath) but doesn't verify the returned buffer is non-empty or valid. An empty or corrupted file would pass the existence check, and the function would return a path to a broken binary.

🛡️ Proposed fix for stricter cache validation
   try {
     const storedTag = readFileSync(versionPath, 'utf8')
-    if (storedTag === versionTag && readFileSync(filePath)) {
+    const fileContent = readFileSync(filePath)
+    // Verify version match and non-empty binary
+    if (storedTag === versionTag && fileContent.length > 0) {
       extractedPaths[key] = filePath
       return filePath
     }
   } catch {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/ripgrepAsset.ts` around lines 96 - 105, The cache fast-path returns
filePath after calling readFileSync(filePath) without verifying the file
contents; change the check so that after confirming storedTag === versionTag you
read the file into a variable (using readFileSync) and validate it is non-empty
(size/length > 0) and ideally stat the file (fs.statSync(filePath)) to ensure
size > 0 and executable permission before setting extractedPaths[key] = filePath
and returning; on any validation failure fall through to the extraction path and
keep the existing try/catch behavior.

xiaofeng and others added 3 commits April 4, 2026 13:49
writeFileSync defaults to 0o644 which lacks execute permission,
causing EACCES errors when Claude Code tries to spawn ripgrep.
@claude-code-best
Copy link
Copy Markdown
Owner

@realorange1994 可能我们不会以 base64 这种嵌入方式进行处理, 一个多平台原因, 一个是有现成的 vscode 的构建版本

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants