From 0aafe20a85412e087ddc735aaa123d4a500b939a Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 12 May 2026 12:46:31 +0900 Subject: [PATCH] refactor: route raw Bun.file/write/hash/which/spawn through runtime shims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eliminates 19 unguarded `Bun.*` runtime call sites in the plugin bundle that crashed with `ReferenceError: Bun is not defined` under Electron. Per-tool-call hot paths (executed on every Read/Edit): - src/tools/hashline-edit/hash-computation.ts: Bun.hash.xxHash32 → bunHashXxh32 - src/tools/hashline-edit/hashline-edit-executor.ts: 8 sites via bunFile/bunWrite - src/hooks/hashline-read-enhancer/hook.ts: Bun.file → bunFile - src/hooks/hashline-edit-diff-enhancer/hook.ts: 2 sites via bunFile Plugin-load paths: - src/hooks/claude-code-hooks/config.ts and config-loader.ts: Bun.file → bunFile - src/features/claude-code-mcp-loader/loader.ts: Bun.file → bunFile - src/features/claude-code-plugin-loader/mcp-server-loader.ts: Bun.file → bunFile - src/features/team-mode/deps.ts: Bun.spawn → spawn shim - src/hooks/session-notification-utils.ts: Bun.which → bunWhich, also drops the bare `declare const Bun` ambient declaration - src/shared/binary-downloader.ts: Bun.write → bunWrite Pure mechanical API swaps. No control-flow or signature changes. --- src/features/claude-code-mcp-loader/loader.ts | 3 ++- .../mcp-server-loader.ts | 3 ++- src/features/team-mode/deps.ts | 3 ++- src/hooks/claude-code-hooks/config-loader.ts | 3 ++- src/hooks/claude-code-hooks/config.ts | 3 ++- src/hooks/hashline-edit-diff-enhancer/hook.ts | 5 +++-- src/hooks/hashline-read-enhancer/hook.ts | 3 ++- src/hooks/session-notification-utils.ts | 7 ++----- src/shared/binary-downloader.ts | 3 ++- src/tools/hashline-edit/hash-computation.ts | 3 ++- .../hashline-edit/hashline-edit-executor.ts | 17 +++++++++-------- 11 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/features/claude-code-mcp-loader/loader.ts b/src/features/claude-code-mcp-loader/loader.ts index 7be6a9ac7..54d5cb3e5 100644 --- a/src/features/claude-code-mcp-loader/loader.ts +++ b/src/features/claude-code-mcp-loader/loader.ts @@ -11,6 +11,7 @@ import type { import { transformMcpServer } from "./transformer" import { log } from "../../shared/logger" import { shouldLoadMcpServer } from "./scope-filter" +import { bunFile } from "../../shared/bun-file-shim" interface McpConfigPath { path: string @@ -37,7 +38,7 @@ async function loadMcpConfigFile( } try { - const content = await Bun.file(filePath).text() + const content = await bunFile(filePath).text() return JSON.parse(content) as ClaudeCodeMcpConfig } catch (error) { log(`Failed to load MCP config from ${filePath}`, error) diff --git a/src/features/claude-code-plugin-loader/mcp-server-loader.ts b/src/features/claude-code-plugin-loader/mcp-server-loader.ts index b0f0f8b8f..3804bdb7d 100644 --- a/src/features/claude-code-plugin-loader/mcp-server-loader.ts +++ b/src/features/claude-code-plugin-loader/mcp-server-loader.ts @@ -7,6 +7,7 @@ import type { ClaudeCodeMcpConfig } from "../claude-code-mcp-loader/types" import { log } from "../../shared/logger" import type { LoadedPlugin } from "./types" import { resolvePluginPaths } from "./plugin-path-resolver" +import { bunFile } from "../../shared/bun-file-shim" export async function loadPluginMcpServers( plugins: LoadedPlugin[], @@ -18,7 +19,7 @@ export async function loadPluginMcpServers( if (!plugin.mcpPath || !existsSync(plugin.mcpPath)) continue try { - const content = await Bun.file(plugin.mcpPath).text() + const content = await bunFile(plugin.mcpPath).text() let config = JSON.parse(content) as ClaudeCodeMcpConfig config = resolvePluginPaths(config, plugin.installPath) diff --git a/src/features/team-mode/deps.ts b/src/features/team-mode/deps.ts index 25db5f966..ecb06c238 100644 --- a/src/features/team-mode/deps.ts +++ b/src/features/team-mode/deps.ts @@ -1,4 +1,5 @@ import type { TeamModeConfig } from "../../config/schema/team-mode" +import { spawn } from "../../shared/bun-spawn-shim" export interface TeamModeDependencyReport { tmuxAvailable: boolean @@ -20,7 +21,7 @@ export async function checkTeamModeDependencies( async function probeBinary(cmd: string, args: string[]): Promise { try { - const proc = Bun.spawn({ cmd: [cmd, ...args], stdout: "pipe", stderr: "pipe" }) + const proc = spawn({ cmd: [cmd, ...args], stdout: "pipe", stderr: "pipe" }) const code = await proc.exited return code === 0 } catch { diff --git a/src/hooks/claude-code-hooks/config-loader.ts b/src/hooks/claude-code-hooks/config-loader.ts index a01abb5eb..ea494ffdb 100644 --- a/src/hooks/claude-code-hooks/config-loader.ts +++ b/src/hooks/claude-code-hooks/config-loader.ts @@ -3,6 +3,7 @@ import { join } from "path" import type { ClaudeHookEvent } from "./types" import { log } from "../../shared/logger" import { getOpenCodeConfigDir } from "../../shared" +import { bunFile } from "../../shared/bun-file-shim" const CONFIG_CACHE_TTL_MS = 30_000 @@ -61,7 +62,7 @@ async function loadConfigFromPath(path: string): Promise): string | undefined { async function captureOldContent(filePath: string): Promise { try { - const file = Bun.file(filePath) + const file = bunFile(filePath) if (await file.exists()) { return await file.text() } @@ -79,7 +80,7 @@ export function createHashlineEditDiffEnhancerHook(config: HashlineEditDiffEnhan let newContent: string try { - newContent = await Bun.file(filePath).text() + newContent = await bunFile(filePath).text() } catch { log("[hashline-edit-diff-enhancer] failed to read new content", { filePath }) return diff --git a/src/hooks/hashline-read-enhancer/hook.ts b/src/hooks/hashline-read-enhancer/hook.ts index 093312d4a..ded243d91 100644 --- a/src/hooks/hashline-read-enhancer/hook.ts +++ b/src/hooks/hashline-read-enhancer/hook.ts @@ -1,4 +1,5 @@ import type { PluginInput } from "@opencode-ai/plugin" +import { bunFile } from "../../shared/bun-file-shim" import { computeLineHash } from "../../tools/hashline-edit/hash-computation" const WRITE_SUCCESS_MARKER = "File written successfully." @@ -178,7 +179,7 @@ async function appendWriteHashlineOutput(output: { output: string; metadata: unk return } - const file = Bun.file(filePath) + const file = bunFile(filePath) if (!(await file.exists())) { return } diff --git a/src/hooks/session-notification-utils.ts b/src/hooks/session-notification-utils.ts index b3eb8e32d..0c690dca3 100644 --- a/src/hooks/session-notification-utils.ts +++ b/src/hooks/session-notification-utils.ts @@ -1,14 +1,11 @@ import { log } from "../shared/logger" - -declare const Bun: { - which(commandName: string): string | null -} +import { bunWhich } from "../shared/bun-which-shim" type Platform = "darwin" | "linux" | "win32" | "unsupported" async function findCommand(commandName: string): Promise { try { - return Bun.which(commandName) + return bunWhich(commandName) } catch (error) { log("[session-notification] failed to resolve command path", { commandName, diff --git a/src/shared/binary-downloader.ts b/src/shared/binary-downloader.ts index 16a8ff60b..a44206c2e 100644 --- a/src/shared/binary-downloader.ts +++ b/src/shared/binary-downloader.ts @@ -1,6 +1,7 @@ import { chmodSync, existsSync, mkdirSync, unlinkSync } from "node:fs"; import * as path from "node:path"; import { spawn } from "./bun-spawn-shim"; +import { bunWrite } from "./bun-file-shim"; import { validateArchiveEntries, type ArchiveEntry } from "./archive-entry-validator"; import { extractZip } from "./zip-extractor"; @@ -26,7 +27,7 @@ export async function downloadArchive(downloadUrl: string, archivePath: string): } const arrayBuffer = await response.arrayBuffer(); - await Bun.write(archivePath, arrayBuffer); + await bunWrite(archivePath, arrayBuffer); } export async function extractTarGz( diff --git a/src/tools/hashline-edit/hash-computation.ts b/src/tools/hashline-edit/hash-computation.ts index a6bf8da78..e5c31b67a 100644 --- a/src/tools/hashline-edit/hash-computation.ts +++ b/src/tools/hashline-edit/hash-computation.ts @@ -1,12 +1,13 @@ import { HASHLINE_DICT } from "./constants" import { createHashlineChunkFormatter } from "./hashline-chunk-formatter" +import { bunHashXxh32 } from "../../shared/bun-hash-shim" const RE_SIGNIFICANT = /[\p{L}\p{N}]/u function computeNormalizedLineHash(lineNumber: number, normalizedContent: string): string { const stripped = normalizedContent const seed = RE_SIGNIFICANT.test(stripped) ? 0 : lineNumber - const hash = Bun.hash.xxHash32(stripped, seed) + const hash = bunHashXxh32(stripped, seed) const index = hash % 256 return HASHLINE_DICT[index] } diff --git a/src/tools/hashline-edit/hashline-edit-executor.ts b/src/tools/hashline-edit/hashline-edit-executor.ts index 54509ab6c..7b450b880 100644 --- a/src/tools/hashline-edit/hashline-edit-executor.ts +++ b/src/tools/hashline-edit/hashline-edit-executor.ts @@ -1,5 +1,6 @@ import type { ToolContext } from "@opencode-ai/plugin/tool" import { publishToolMetadata } from "../../features/tool-metadata-store" +import { bunFile, bunWrite } from "../../shared/bun-file-shim" import { applyHashlineEditsWithReport } from "./edit-operations" import { countLineDiffs, generateUnifiedDiff } from "./diff-utils" import { canonicalizeFileText, restoreFileText } from "./file-text-canonicalization" @@ -94,7 +95,7 @@ export async function executeHashlineEditTool(args: HashlineEditArgs, context: T const edits = deleteMode ? [] : normalizeHashlineEdits(args.edits) - const file = Bun.file(filePath) + const file = bunFile(filePath) const exists = await file.exists() if (!exists && !deleteMode && !canCreateFromMissingFile(edits)) { return `Error: File not found: ${filePath}` @@ -102,7 +103,7 @@ export async function executeHashlineEditTool(args: HashlineEditArgs, context: T if (deleteMode) { if (!exists) return `Error: File not found: ${filePath}` - await Bun.file(filePath).delete() + await bunFile(filePath).delete() return `Successfully deleted ${filePath}` } @@ -122,11 +123,11 @@ export async function executeHashlineEditTool(args: HashlineEditArgs, context: T const writeContent = restoreFileText(canonicalNewContent, oldEnvelope) - await Bun.write(filePath, writeContent) + await bunWrite(filePath, writeContent) if (pluginCtx?.client) { await runFormattersForFile(pluginCtx.client as FormatterClient, context.directory, filePath) - const formattedContent = Buffer.from(await Bun.file(filePath).arrayBuffer()).toString("utf8") + const formattedContent = Buffer.from(await bunFile(filePath).arrayBuffer()).toString("utf8") if (formattedContent !== writeContent) { const formattedEnvelope = canonicalizeFileText(formattedContent) const formattedMeta = buildSuccessMeta( @@ -138,8 +139,8 @@ export async function executeHashlineEditTool(args: HashlineEditArgs, context: T ) await publishToolMetadata(metadataContext, formattedMeta) if (rename && rename !== filePath) { - await Bun.write(rename, formattedContent) - await Bun.file(filePath).delete() + await bunWrite(rename, formattedContent) + await bunFile(filePath).delete() return `Moved ${filePath} to ${rename}` } return `Updated ${filePath}` @@ -147,8 +148,8 @@ export async function executeHashlineEditTool(args: HashlineEditArgs, context: T } if (rename && rename !== filePath) { - await Bun.write(rename, writeContent) - await Bun.file(filePath).delete() + await bunWrite(rename, writeContent) + await bunFile(filePath).delete() } const effectivePath = rename && rename !== filePath ? rename : filePath