Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 198 additions & 0 deletions compile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { getMacroDefines } from "./scripts/defines.ts";
import { exit } from "process";
import { join, resolve } from "path";
import { readFile, writeFile } from "fs/promises";

const outfile = process.platform === "win32" ? "claude.exe" : "claude";

// Use the currently running bun executable
const bunExe = process.execPath;

// Collect FEATURE_* env vars from environment
const features = Object.keys(process.env)
.filter(k => k.startsWith("FEATURE_"))
.map(k => k.replace("FEATURE_", ""));
Comment on lines +12 to +14
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Only enable feature flags when their value is 1.

The current code enables FEATURE_FOO=0, FEATURE_FOO=false, or an empty variable. Filter by the required enablement value before passing --feature.

 const features = Object.keys(process.env)
-    .filter(k => k.startsWith("FEATURE_"))
+    .filter(k => k.startsWith("FEATURE_") && process.env[k] === "1")
     .map(k => k.replace("FEATURE_", ""));

As per coding guidelines, “Feature flags in build must use environment variable format FEATURE_<FLAG_NAME>=1 for enablement”.

📝 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 features = Object.keys(process.env)
.filter(k => k.startsWith("FEATURE_"))
.map(k => k.replace("FEATURE_", ""));
const features = Object.keys(process.env)
.filter(k => k.startsWith("FEATURE_") && process.env[k] === "1")
.map(k => k.replace("FEATURE_", ""));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@compile.ts` around lines 12 - 14, The feature collection currently enables
any FEATURE_* key regardless of value; update the pipeline that builds features
(the code using Object.keys(process.env).filter(...).map(...)) to only include
keys where process.env[k] === "1" (i.e., check the env value equals the string
"1" in the filter that also verifies k.startsWith("FEATURE_")), then continue
mapping to strip the "FEATURE_" prefix (e.g., keep using k.replace("FEATURE_",
"") or k.slice(8)) so only truly enabled flags are passed as --feature.


// Auto-enable CHICAGO_MCP so @ant packages (computer-use-mcp, etc.)
// are bundled into the standalone exe. Without this flag, the feature-gated
// dynamic imports are tree-shaken and the native .node files are not embedded.
if (!features.includes("CHICAGO_MCP")) {
features.push("CHICAGO_MCP");
}

const defines = getMacroDefines();

// Build --define flags
const defineArgs = Object.entries(defines).flatMap(([k, v]) => [
"--define",
`${k}:${v}`,
]);

// Pass BUNDLED_MODE flag so ripgrepAsset.ts knows we're in compiled mode
const defineArgsWithBundled = [
...defineArgs,
"--define",
`BUNDLED_MODE:"true"`,
];

// Build --feature flags
const featureArgs = features.flatMap(f => ["--feature", f]);

// ─── Native module embedding ──────────────────────────────────────────────────
// bun build --compile embeds .node files as assets. When the bundler sees
// process.env.XXX_NODE_PATH with the var set to an absolute .node path,
// it rewrites the string to the bunfs asset path. This lets the runtime
// require() the embedded .node from within the compiled exe.
//
// Paths must use forward slashes and be absolute at compile time.
const repoRoot = resolve(__dirname);

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`),
};

// Build env with native paths (forward slashes for Bun compatibility)
const compileEnv: Record<string, string> = {
...process.env,
...Object.fromEntries(
Object.entries(nativeNodePaths).map(([k, v]) => [k, v.replace(/\\/g, "/")]),
),
};

// ─── Step 0: Generate ripgrep base64 asset ───────────────────────────────────
// Bun's bundler does not support ?url imports or arbitrary file embedding
// for non-.node files. The only reliable way to embed a binary into the
// compiled exe is to base64-encode it and store it as a JS string constant.
// At runtime, we decode to a temp file and execute.
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 ripgrepBinaries: Record<string, string> = {}

// Map platform+arch to filename
const allPlatforms: Array<{ key: string; subdir: string; file: string }> = [
{ key: 'windows_x64', subdir: 'x64-win32', file: 'rg.exe' },
{ key: 'darwin_x64', subdir: 'x64-darwin', file: 'rg' },
{ key: 'darwin_arm64', subdir: 'arm64-darwin', file: 'rg' },
{ key: 'linux_x64', subdir: 'x64-linux', file: 'rg' },
{ key: 'linux_arm64', subdir: 'arm64-linux', file: 'rg' },
];

// Only embed the current platform's binary to minimize exe size.
// The other platforms are available in the SDK for dev-mode fallback.
const currentPlatformKey = (() => {
if (process.platform === 'win32') return 'windows_x64'
if (process.platform === 'darwin') return process.arch === 'arm64' ? 'darwin_arm64' : 'darwin_x64'
return process.arch === 'arm64' ? 'linux_arm64' : 'linux_x64'
})()

for (const { key, subdir, file } of allPlatforms) {
if (key !== currentPlatformKey) continue // Skip other platforms
const binPath = join(rgCache, subdir, file);
try {
const data = await readFile(binPath);
ripgrepBinaries[key] = data.toString('base64');
console.log(`Encoded ${key}: ${data.length} bytes -> ${Math.round(data.length * 1.37)} chars`);
} catch (e) {
console.warn(`Warning: could not read ${binPath}: ${e}`);
}
Comment on lines +83 to +115
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Declared SDK dependency:"
node -e 'const p=require("./package.json"); console.log((p.dependencies||{})["@anthropic-ai/claude-agent-sdk"] || (p.devDependencies||{})["@anthropic-ai/claude-agent-sdk"])'

echo
echo "Hard-coded SDK cache references:"
rg -n -C2 '@anthropic-ai\+claude-agent-sdk@0\.2\.87|claude-agent-sdk@0\.2\.87|vendor/ripgrep'

echo
echo "Installed ripgrep vendor directories, if present:"
fd -p 'vendor/ripgrep' node_modules --max-depth 8 2>/dev/null || true

Repository: claude-code-best/claude-code

Length of output: 2499


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Full generateRipgrepAsset function in compile.ts ==="
sed -n '83,150p' compile.ts

echo
echo "=== How ripgrepBinaries is used/written ==="
rg -n -A5 "ripgrepBinaries" compile.ts | head -40

echo
echo "=== Check if there are dynamic SDK resolution patterns elsewhere ==="
rg -n "require.resolve|import.meta.resolve|resolve.*sdk" --type ts --type tsx -i | head -20

Repository: claude-code-best/claude-code

Length of output: 4337


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Look for require.resolve patterns in TypeScript files ==="
rg -n "require\.resolve|resolveFrom|createRequire|import\.meta" --type-add 'ts:*.ts' --type-add 'tsx:*.tsx' | head -20

echo
echo "=== Check bun's resolve capabilities ==="
rg -n "bun:.*resolve|Bun\.resolveSync|Bun\.resolve" | head -10

echo
echo "=== Look at package.json to understand dependency management ==="
head -50 package.json

echo
echo "=== Check how other paths reference node_modules packages ==="
rg -n "node_modules.*@anthropic" --type-add 'ts:*.ts' --type-add 'tsx:*.tsx' -C1 | head -30

Repository: claude-code-best/claude-code

Length of output: 2238


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Look for resolve patterns with createRequire ==="
rg -n -A5 "createRequire" compile.ts src/utils/ | head -40

echo
echo "=== Check how bun handles module resolution ==="
cat package.json | grep -A2 -B2 "anthropic"

echo
echo "=== Try to find if there's a way to dynamically resolve the package ==="
node -e "
const { createRequire } = require('module');
const req = createRequire(import.meta.url);
try {
  const pkgPath = req.resolve('@anthropic-ai/claude-agent-sdk/package.json');
  console.log('Resolved package:', pkgPath);
} catch(e) {
  console.log('Could not resolve:', e.message);
}
"

echo
echo "=== Check compile.ts for any dynamic resolution at all ==="
head -30 compile.ts

Repository: claude-code-best/claude-code

Length of output: 124


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Full compile.ts head to see imports ==="
head -40 compile.ts

echo
echo "=== Check if file paths are computed anywhere else ==="
grep -r "resolveSync\|resolve(" compile.ts src/ 2>/dev/null || echo "No resolve patterns found"

echo
echo "=== Look at the actual imports in compile.ts ==="
rg -n "^import\|^from" compile.ts | head -20

echo
echo "=== Check how the SDK is actually installed - bun.lockb or package-lock ==="
ls -la | grep -E "lock|package" 

echo
echo "=== Look for any bun-specific resolution ==="
rg -n "Bun\." compile.ts | head -10

Repository: claude-code-best/claude-code

Length of output: 37574


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check imports in compile.ts ==="
head -20 compile.ts

echo
echo "=== Check if createRequire is used anywhere in compile context ==="
rg -n "createRequire" compile.ts

echo
echo "=== Look at how ripgrep.ts (runtime) resolves the path dynamically ==="
cat -n src/utils/ripgrep.ts | head -30

echo
echo "=== Check the actual generated ripgrepAssetBase64.ts file to see if it exists ==="
ls -la src/utils/ripgrepAssetBase64.ts 2>/dev/null || echo "File does not exist yet"

echo
echo "=== Can we use require.resolve or import.meta.resolve in compile.ts? ==="
rg -n "require\.resolve|import\.meta\.resolve" compile.ts src/

Repository: claude-code-best/claude-code

Length of output: 980


🏁 Script executed:

#!/bin/bash
set -euoripefail

echo "=== Full ripgrep.ts to see dynamic resolution pattern ==="
cat -n src/utils/ripgrep.ts

echo
echo "=== Check if bun provides module resolution in compile scripts ==="
# Look for examples of how dependencies are resolved at build time
rg -n "package.json\|require.*resolve\|fileURLToPath" scripts/ --type ts -A2 | head -40

echo
echo "=== Verify the SDK path mismatch more clearly ==="
echo "Declared in package.json:"
jq '.dependencies["@anthropic-ai/claude-agent-sdk"]' package.json
echo
echo "Hard-coded in compile.ts:"
grep -n "@anthropic-ai" compile.ts | head -3

Repository: claude-code-best/claude-code

Length of output: 746


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Full ripgrep.ts to see dynamic resolution pattern ==="
cat -n src/utils/ripgrep.ts

echo
echo "=== Check if bun provides module resolution in compile scripts ==="
# Look for examples of how dependencies are resolved at build time
rg -n "package.json|require.*resolve|fileURLToPath" scripts/ --type ts -A2 | head -40

echo
echo "=== Verify the SDK path mismatch more clearly ==="
echo "Declared in package.json:"
jq '.dependencies["@anthropic-ai/claude-agent-sdk"]' package.json
echo
echo "Hard-coded in compile.ts:"
grep -n "@anthropic-ai" compile.ts | head -3

Repository: claude-code-best/claude-code

Length of output: 27419


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check actual package.json dependencies section ==="
cat package.json | grep -A50 '"dependencies"'

echo
echo "=== Check src/utils/ripgrepAsset.ts - where getRipgrepBinaryPath comes from ==="
cat -n src/utils/ripgrepAsset.ts

echo
echo "=== Confirm: does generateRipgrepAsset fail compile if ripgrep missing? ==="
grep -n "generateRipgrepAsset" compile.ts | head -5

Repository: claude-code-best/claude-code

Length of output: 8190


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Final verification: Check how to resolve SDK location dynamically ==="
echo "Checking if require.resolve or similar could work in compile.ts context:"
node -e "
try {
  const path = require.resolve('@anthropic-ai/claude-agent-sdk');
  console.log('Resolved SDK path:', path);
} catch (e) {
  console.log('Cannot resolve:', e.message);
}
"

echo
echo "=== Check if bun has native resolve capabilities ==="
grep -n "Bun\|bun:" compile.ts | head -5

Repository: claude-code-best/claude-code

Length of output: 717


Fail the build when the embedded ripgrep asset is missing, and resolve the SDK location dynamically.

When ripgrep cannot be read (line 114), the code warns but continues, writing an empty RIPGREP_BINARIES object to ripgrepAssetBase64.ts. This produces an executable that silently fails at runtime since the embedded binary is unavailable. Additionally, both compile.ts (line 85) and src/utils/ripgrepAsset.ts (lines 27–29) hard-code the SDK cache path @anthropic-ai+claude-agent-sdk@0.2.87+..., which is stale—the declared dependency is ^0.2.114. This mismatch breaks the dev-mode fallback.

Use require.resolve('@anthropic-ai/claude-agent-sdk') to resolve the installed SDK path dynamically at build time, and throw an error if the ripgrep binary cannot be read.

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

In `@compile.ts` around lines 83 - 115, In generateRipgrepAsset, stop only warning
when the ripgrep binary is missing and instead fail the build: if readFile for
the selected platform (used to populate ripgrepBinaries / RIPGREP_BINARIES
written to ripgrepAssetBase64.ts) throws, rethrow or call process.exit(1) with a
clear error so the build fails rather than emitting an empty asset. Also replace
the hard-coded SDK cache path by resolving the installed SDK at build time using
require.resolve('@anthropic-ai/claude-agent-sdk') (locate the package root from
that result) so rgCache is computed dynamically and not tied to the stale
`@anthropic-ai`+claude-agent-sdk@0.2.87+... string referenced in
generateRipgrepAsset and src/utils/ripgrepAsset.ts.

}

// Generate TypeScript asset file
const assetFile = join(repoRoot, "src", "utils", "ripgrepAssetBase64.ts");
const content = `/**
* AUTO-GENERATED by compile.ts — do not edit manually.
* Ripgrep binaries encoded as base64 strings.
* Decoded at runtime to temp files for execution.
*/
export const RIPGREP_BINARIES: Record<string, string> = ${JSON.stringify(ripgrepBinaries, null, 2)};
`;
await writeFile(assetFile, content);
console.log(`Generated ${assetFile}`);
}

// ─── Step 1: Patch SDK ripgrep path ───────────────────────────────────────────
// The SDK's cli.js computes dy_ from import.meta.url which points to B:\~BUN\root\...
// in --compile mode. Patch it to use path.dirname(process.execPath) instead.
async function patchRipgrepPaths() {
// --- Patch bun cache SDK cli.js ---
const sdkCachePath = join(repoRoot,
"node_modules/.bun/@anthropic-ai+claude-agent-sdk@0.2.87+3c5d820c62823f0b/node_modules/@anthropic-ai/claude-agent-sdk/cli.js");
const sdkContent = await readFile(sdkCachePath, "utf-8");
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)");
}
Comment on lines +134 to +153
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "SDK patch targets/references:"
rg -n -C3 'patchRipgrepPaths|SDK patch did not match|dy_=|@anthropic-ai\+claude-agent-sdk@0\.2\.87'

echo
echo "Installed SDK cli.js files, if present:"
fd -p 'claude-agent-sdk/cli.js' node_modules --max-depth 8 2>/dev/null || true

Repository: claude-code-best/claude-code

Length of output: 3349


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Checking SDK version pinning ==="
echo "From package.json:"
rg '"@anthropic-ai/claude-agent-sdk"' package.json -A2 -B2

echo
echo "=== Checking for hard-coded paths in compile.ts ==="
rg '@anthropic-ai.*0\.2\.87' compile.ts -n

echo
echo "=== Checking if patchRipgrepPaths handles errors ==="
sed -n '134,153p' compile.ts | cat -n

echo
echo "=== Looking for error handling patterns in similar functions ==="
rg 'throw new Error|process\.exit|throw Error' compile.ts -n

Repository: claude-code-best/claude-code

Length of output: 1768


Make the SDK patch deterministic and fail-fast; remove hard-coded version strings.

The code hard-codes SDK version 0.2.87 in paths (lines 85, 137) while package.json specifies ^0.2.114. When the regex patch fails to match, the build continues with an unpatched SDK—potentially causing runtime failures that go undetected. If the installed SDK version differs from the hard-coded 0.2.87, the paths will not exist.

Throw an error on patch failure instead of silently logging a warning. Dynamically resolve the SDK path from node_modules rather than hard-coding a version string to avoid version mismatches and cache path mutations.

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

In `@compile.ts` around lines 134 - 153, In patchRipgrepPaths, remove the
hard-coded SDK version in sdkCachePath and instead resolve the installed package
entry point (e.g., via require.resolve("@anthropic-ai/claude-agent-sdk/cli.js")
or using path.join(repoRoot, "node_modules", ...resolvePackageDir)) to locate
cli.js dynamically; if the resolved path does not exist or readFile fails throw
an Error; after performing the string replacements on sdkContent, if patchedSdk
=== sdkContent throw an Error (fail-fast) rather than console.warn so the build
stops on a non-matching patch; write the patched content with writeFile only
after a successful replacement.

}

// ─── Step 2: Run the compile ───────────────────────────────────────────────────
async function run() {
await generateRipgrepAsset();
await patchRipgrepPaths();

console.log("\nCompiling standalone executable with native modules...");
console.log(`Outfile: ${outfile}`);
console.log(`Defines: ${Object.keys(defines).join(", ")}`);
console.log(`Native modules:`);
for (const [k, v] of Object.entries(nativeNodePaths)) {
console.log(` ${k}=${v}`);
}

// Use Bun.spawn with CLI because Bun.build({ outfile, compile: true })
// does not reliably place the output file on Windows.
const result = Bun.spawnSync(
[
bunExe,
"build",
"--compile",
"--outfile=" + outfile,
...defineArgsWithBundled,
...featureArgs,
"src/entrypoints/cli.tsx",
],
{
stdio: ["inherit", "inherit", "inherit"],
env: compileEnv,
},
);

if (result.exitCode !== 0) {
console.error("Compile failed with exit code:", result.exitCode);
exit(1);
}

console.log(`\nCompiled standalone executable: ${outfile}`);
if (features.length > 0) {
console.log(`Features enabled: ${features.join(", ")}`);
}
}

run();
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
],
"scripts": {
"build": "bun run build.ts",
"compile": "bun run compile.ts",
"build:vite": "vite build && bun run scripts/post-build.ts",
"build:vite:only": "vite build",
"build:bun": "bun run build.ts",
Expand Down
7 changes: 6 additions & 1 deletion src/utils/bundledMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@ export function isRunningWithBun(): boolean {

/**
* 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
return (
Comment on lines +14 to 22
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Align the bundled-mode check with the documented precedence.

The comment says Bun.embeddedFiles is primary and BUNDLED_MODE is fallback, but the code checks BUNDLED_MODE first and treats any defined value as enabled. Make the comment match the code, or check the actual value explicitly.

- * 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).
...
-  if (typeof BUNDLED_MODE !== 'undefined') return true
+  if (BUNDLED_MODE === 'true') 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 14 - 22, The comment and
implementation disagree: isInBundledMode currently treats any defined
BUNDLED_MODE as true before checking Bun.embeddedFiles; either update the
comment or change isInBundledMode to honor the documented precedence. Modify
isInBundledMode so it first checks Bun?.embeddedFiles (truthy -> return true),
then falls back to explicitly checking BUNDLED_MODE's value (e.g., compare to
"true" or boolean equivalent) — reference the isInBundledMode function, the
BUNDLED_MODE declaration, and Bun.embeddedFiles when making the change.

typeof Bun !== 'undefined' &&
Array.isArray(Bun.embeddedFiles) &&
Expand Down
13 changes: 5 additions & 8 deletions src/utils/ripgrep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as path from 'path'
import { logEvent } from 'src/services/analytics/index.js'
import { fileURLToPath } from 'url'
import { isInBundledMode } from './bundledMode.js'
import { getRipgrepBinaryPath } from './ripgrepAsset.js'
import { logForDebugging } from './debug.js'
import { isEnvDefinedFalsy } from './envUtils.js'
import { execFileNoThrow } from './execFileNoThrow.js'
Expand Down Expand Up @@ -44,15 +45,11 @@ const getRipgrepConfig = memoize((): RipgrepConfig => {
}
}

// In bundled (native) mode, ripgrep is statically compiled into bun-internal
// and dispatches based on argv[0]. We spawn ourselves with argv0='rg'.
// 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: [] }
Comment on lines +48 to +52
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Keep compiled ripgrep distinct from vendored ripgrep.

Returning mode: 'builtin' for the extracted base64 binary makes src/screens/Doctor.tsx:269-275 display compiled ripgrep as vendor instead of bundled. Either preserve an embedded/compiled mode here, or update the Doctor display logic to recognize this extracted bundled path.

🤖 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 code returns { mode:
'builtin', command: rgPath, args: [] } when isInBundledMode() is true, causing
the Doctor UI to mislabel an embedded/compiled ripgrep as vendor; change the
returned mode to a distinct value (e.g., 'embedded' or 'bundled_compiled') from
isInBundledMode() so downstream code can distinguish it, and update any
consumers (e.g., the Doctor display logic that checks mode) to recognize the new
mode name; specifically modify the return in getRipgrepBinaryPath()/where
isInBundledMode() is handled and ensure Doctor.tsx's ripgrep-mode handling
accepts the new mode string.

}

const rgRoot = path.resolve(__dirname, 'vendor', 'ripgrep')
Expand Down
122 changes: 122 additions & 0 deletions src/utils/ripgrepAsset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* Gets the ripgrep binary path for the current platform/arch.
*
* In compiled mode: decodes base64 from ripgrepAssetBase64.ts, writes to temp,
* and caches on disk so subsequent starts skip the decode.
*
* In dev mode: falls back to SDK's bundled ripgrep path.
*
* BUNDLED_MODE is a compile-time constant injected by compile.ts --define flag.
*/
import { writeFileSync, readFileSync } from 'fs'
import { mkdirSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { getPlatform } from './platform.js'

// In-memory cache: platform+arch -> absolute path to extracted temp file
const extractedPaths: Record<string, string> = {}

// Global base64 data — loaded once on first access
let globalBase64: Record<string, string> | null = null

// SDK's bundled ripgrep path (used as fallback in dev mode)
function getSdkRipgrepPath(): string {
const p = getPlatform()
const arch = process.arch
if (p === 'windows') return join(process.cwd(), 'node_modules/.bun/@anthropic-ai+claude-agent-sdk@0.2.87+3c5d820c62823f0b/node_modules/@anthropic-ai/claude-agent-sdk/vendor/ripgrep/x64-win32/rg.exe')
if (p === 'macos') return join(process.cwd(), 'node_modules/.bun/@anthropic-ai+claude-agent-sdk@0.2.87+3c5d820c62823f0b/node_modules/@anthropic-ai/claude-agent-sdk/vendor/ripgrep', arch === 'arm64' ? 'arm64-darwin/rg' : 'x64-darwin/rg')
return join(process.cwd(), 'node_modules/.bun/@anthropic-ai+claude-agent-sdk@0.2.87+3c5d820c62823f0b/node_modules/@anthropic-ai/claude-agent-sdk/vendor/ripgrep', arch === 'arm64' ? 'arm64-linux/rg' : 'x64-linux/rg')
}
Comment on lines +24 to +30
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Declared SDK dependency:"
node -e 'const p=require("./package.json"); console.log((p.dependencies||{})["@anthropic-ai/claude-agent-sdk"] || (p.devDependencies||{})["@anthropic-ai/claude-agent-sdk"])'

echo
echo "Hard-coded SDK cache references:"
rg -n -C2 '@anthropic-ai\+claude-agent-sdk@0\.2\.87|claude-agent-sdk@0\.2\.87|claude-agent-sdk/vendor/ripgrep'

echo
echo "Installed Bun cache SDK dirs, if present:"
fd -p '@anthropic-ai+claude-agent-sdk' node_modules/.bun --max-depth 2 2>/dev/null || true

Repository: claude-code-best/claude-code

Length of output: 1841


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Content of ripgrepAsset.ts at specified lines ==="
sed -n '20,35p' src/utils/ripgrepAsset.ts | cat -n

echo
echo "=== Content at lines 85-95 (also applies to comment) ==="
sed -n '85,95p' src/utils/ripgrepAsset.ts | cat -n

echo
echo "=== Check if require.resolve or similar is used elsewhere ==="
rg -n 'require\.resolve|import\.meta\.resolve|resolve\(.*@anthropic-ai' src/ --type ts --type tsx | head -20

echo
echo "=== Check what functions are exported from ripgrepAsset.ts ==="
rg -n 'export' src/utils/ripgrepAsset.ts

echo
echo "=== Full ripgrepAsset.ts file size ==="
wc -l src/utils/ripgrepAsset.ts

Repository: claude-code-best/claude-code

Length of output: 1823


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check if require.resolve or similar is used elsewhere ==="
rg -n 'require\.resolve|import\.meta\.resolve' src/ | head -20

echo
echo "=== Check how SDK is resolved in other places ==="
rg -n '@anthropic-ai/claude-agent-sdk' src/ | grep -v 'node_modules'

echo
echo "=== Full ripgrepAsset.ts to understand complete context ==="
wc -l src/utils/ripgrepAsset.ts
cat -n src/utils/ripgrepAsset.ts

Repository: claude-code-best/claude-code

Length of output: 133


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Full ripgrepAsset.ts file ==="
cat -n src/utils/ripgrepAsset.ts

echo
echo "=== Check for process.cwd() usage in the repo ==="
rg 'process\.cwd\(\)' src/ -c

echo
echo "=== Check how SDK packages are typically resolved ==="
rg '@anthropic-ai' src/ -l

Repository: claude-code-best/claude-code

Length of output: 10486


Use dynamic module resolution instead of hard-coded SDK path.

The function hard-codes @anthropic-ai/claude-agent-sdk@0.2.87+3c5d820c62823f0b, while package.json declares ^0.2.114. The hash will break whenever the lock file is regenerated or the SDK version updates. Additionally, process.cwd() points to the user's project directory for a CLI, not the package root where the SDK is actually installed.

Resolve the vendor directory dynamically (e.g., via require.resolve() or import.meta.resolve()) instead, and provide clear error handling for compiled mode if the embedded asset is unavailable.

This affects the dev-mode fallback at lines 88–91 as well.

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

In `@src/utils/ripgrepAsset.ts` around lines 24 - 30, getSdkRipgrepPath currently
hard-codes the full vendor path including a locked package hash and uses
process.cwd(), which will break across installs; replace that hard-coded
resolution by using require.resolve (or import.meta.resolve in ESM) to locate
the installed `@anthropic-ai/claude-agent-sdk` package entry point and then derive
the vendor/ripgrep subpath from that resolved location in getSdkRipgrepPath(),
falling back to platform/arch branches only after resolving the package base;
also update the dev-mode fallback logic that tries to access the embedded asset
(the block around the existing dev-mode fallback) to surface a clear error when
the resolved path is unavailable in compiled mode and to avoid relying on
process.cwd() for package resolution.


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'
}

// BUNDLED_MODE is injected at compile time by compile.ts --define flag.
// In dev mode, this variable is undefined.
declare const BUNDLED_MODE: string | undefined

/**
* 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
}

/**
* Get the ripgrep binary path for the current platform/arch.
* In compiled mode: decodes base64, extracts to temp, caches by version fingerprint.
* In dev mode: returns SDK path directly.
*/
export function getRipgrepBinaryPath(): string {
const key = getPlatformKey()
if (extractedPaths[key]) return extractedPaths[key]

const tmpDir = join(tmpdir(), 'claude-code-ripgrep')
const filename = key === 'windows_x64' ? 'rg.exe' : 'rg'
const filePath = join(tmpDir, filename)
const versionPath = join(tmpDir, `${key}.version`)

// Dev mode: use SDK path directly
if (typeof BUNDLED_MODE === 'undefined') {
const sdkPath = getSdkRipgrepPath()
extractedPaths[key] = sdkPath
return sdkPath
}
Comment on lines +70 to +75
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use the same bundled-mode detection as callers.

ripgrep.ts enters this resolver when isInBundledMode() is true, but this file treats BUNDLED_MODE === undefined as dev mode. If the fallback Bun.embeddedFiles detection is what made bundled mode true, this returns the SDK path instead of extracting the embedded binary.

+import { isInBundledMode } from 'src/utils/bundledMode.js'
...
-  if (typeof BUNDLED_MODE === 'undefined') {
+  if (!isInBundledMode()) {
📝 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
// Dev mode: use SDK path directly
if (typeof BUNDLED_MODE === 'undefined') {
const sdkPath = getSdkRipgrepPath()
extractedPaths[key] = sdkPath
return sdkPath
}
import { isInBundledMode } from 'src/utils/bundledMode.js'
// ... (rest of imports and code)
// Dev mode: use SDK path directly
if (!isInBundledMode()) {
const sdkPath = getSdkRipgrepPath()
extractedPaths[key] = sdkPath
return sdkPath
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/ripgrepAsset.ts` around lines 70 - 75, The current dev-mode branch
uses typeof BUNDLED_MODE === 'undefined' which mismatches callers (like
ripgrep.ts) that use isInBundledMode() or Bun.embeddedFiles to detect bundled
mode; change the condition in the resolver to use the same bundled-mode
detection as callers (e.g., call isInBundledMode() or replicate its
Bun.embeddedFiles check) so that when bundled mode is true you do not return
getSdkRipgrepPath() but instead proceed with extracting the embedded binary;
update the branch around BUNDLED_MODE/getSdkRipgrepPath() to rely on
isInBundledMode()/Bun.embeddedFiles consistent with ripgrep.ts.


// Compiled mode: must use base64 decode (synchronous path — loaded eagerly from embedded module)
// In the compiled exe, require() resolves to the embedded ripgrepAssetBase64.js
let base64Data: string | undefined
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const RIPGREP_BINARIES: Record<string, string> = require('./ripgrepAssetBase64.js').RIPGREP_BINARIES
base64Data = RIPGREP_BINARIES[key]
} catch {
// require failed — fall back to SDK path
}

if (!base64Data) {
const sdkPath = getSdkRipgrepPath()
extractedPaths[key] = sdkPath
return sdkPath
}

const versionTag = `b64:${base64Data.length}:${base64Data.slice(0, 16)}:${base64Data.slice(-16)}`

// 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
}

// Decode and extract
mkdirSync(tmpDir, { recursive: true })
const buffer = Buffer.from(base64Data, 'base64')
writeFileSync(filePath, buffer, { mode: 0o755 })
writeFileSync(versionPath, versionTag, 'utf8')
Comment on lines +65 to +111
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Harden the temp executable cache before trusting it.

The cache path is predictable and shared under tmpdir(), and the fast path trusts versionPath plus any readable rg file. Use a user-owned 0700 directory, verify ownership/permissions, and write via an atomic temp-file rename before returning an executable path.

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

In `@src/utils/ripgrepAsset.ts` around lines 65 - 111, The temp-cache is currently
trusted without validating directory ownership/permissions or using atomic
writes; update the logic around tmpDir/versionPath/filePath (used in the
compiled-mode branch that reads RIPGREP_BINARIES and fills extractedPaths) to:
ensure tmpDir is created with mode 0o700 and owned by the current process user
(on POSIX check uid/gid), refuse to trust the cached versionPath/filePath if
ownership or permissions are not correct, and when decoding base64Data write to
a unique temp file inside tmpDir and atomically rename it to filePath (set file
mode 0o700/0o755 as appropriate) before writing versionPath, so cached
executables are both user-private and written atomically.

extractedPaths[key] = filePath
return filePath
}

/**
* Async version — preloads base64 data before extracting.
* Call this early (e.g., during startup) to avoid decode delay on first grep.
*/
export async function preloadRipgrepBinary(): Promise<void> {
getRipgrepBinaryPath()
}
8 changes: 8 additions & 0 deletions src/utils/ripgrepAssetBase64.ts

Large diffs are not rendered by default.

Loading