summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2026-02-18 19:41:14 -0500
committerDax Raad <[email protected]>2026-02-18 19:41:14 -0500
commit568eccb4c654e83382253eb0c1478d24585288aa (patch)
tree111af95e7574da88ac72eb121eb8fb63115ddfb1 /packages
parent3a07dd8d96e3e4cbc6787ae14add19b2d58023be (diff)
downloadopencode-568eccb4c654e83382253eb0c1478d24585288aa.tar.gz
opencode-568eccb4c654e83382253eb0c1478d24585288aa.zip
Revert: all refactor commits migrating from Bun.file() to Filesystem module
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/src/cli/cmd/tui/thread.ts4
-rw-r--r--packages/opencode/src/lsp/client.ts3
-rw-r--r--packages/opencode/src/lsp/server.ts95
-rw-r--r--packages/opencode/src/mcp/auth.ts10
-rw-r--r--packages/opencode/src/project/project.ts20
-rw-r--r--packages/opencode/src/provider/models.ts7
-rw-r--r--packages/opencode/src/provider/provider.ts6
-rw-r--r--packages/opencode/src/session/instruction.ts12
-rw-r--r--packages/opencode/src/session/prompt.ts16
-rw-r--r--packages/opencode/src/shell/shell.ts3
-rw-r--r--packages/opencode/src/skill/discovery.ts7
-rw-r--r--packages/opencode/src/storage/db.ts4
-rw-r--r--packages/opencode/src/storage/json-migration.ts3
-rw-r--r--packages/opencode/src/storage/storage.ts68
-rw-r--r--packages/opencode/src/tool/edit.ts13
-rw-r--r--packages/opencode/src/tool/glob.ts6
-rw-r--r--packages/opencode/src/tool/grep.ts4
-rw-r--r--packages/opencode/src/tool/lsp.ts3
-rw-r--r--packages/opencode/src/tool/read.ts15
-rw-r--r--packages/opencode/src/tool/truncation.ts3
-rw-r--r--packages/opencode/src/tool/write.ts7
-rw-r--r--packages/opencode/src/util/filesystem.ts25
-rw-r--r--packages/opencode/src/util/log.ts13
-rw-r--r--packages/opencode/test/util/filesystem.test.ts121
24 files changed, 174 insertions, 294 deletions
diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts
index 50f63c3df..9eb296032 100644
--- a/packages/opencode/src/cli/cmd/tui/thread.ts
+++ b/packages/opencode/src/cli/cmd/tui/thread.ts
@@ -3,12 +3,10 @@ import { tui } from "./app"
import { Rpc } from "@/util/rpc"
import { type rpc } from "./worker"
import path from "path"
-import { fileURLToPath } from "url"
import { UI } from "@/cli/ui"
import { iife } from "@/util/iife"
import { Log } from "@/util/log"
import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
-import { Filesystem } from "@/util/filesystem"
import type { Event } from "@opencode-ai/sdk/v2"
import type { EventSource } from "./context/sdk"
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
@@ -101,7 +99,7 @@ export const TuiThreadCommand = cmd({
const distWorker = new URL("./cli/cmd/tui/worker.js", import.meta.url)
const workerPath = await iife(async () => {
if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH
- if (await Filesystem.exists(fileURLToPath(distWorker))) return distWorker
+ if (await Bun.file(distWorker).exists()) return distWorker
return localWorker
})
try {
diff --git a/packages/opencode/src/lsp/client.ts b/packages/opencode/src/lsp/client.ts
index 084ccf831..8704b65ac 100644
--- a/packages/opencode/src/lsp/client.ts
+++ b/packages/opencode/src/lsp/client.ts
@@ -147,7 +147,8 @@ export namespace LSPClient {
notify: {
async open(input: { path: string }) {
input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path)
- const text = await Filesystem.readText(input.path)
+ const file = Bun.file(input.path)
+ const text = await file.text()
const extension = path.extname(input.path)
const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext"
diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts
index a4ebeb5a2..0200be226 100644
--- a/packages/opencode/src/lsp/server.ts
+++ b/packages/opencode/src/lsp/server.ts
@@ -131,7 +131,7 @@ export namespace LSPServer {
"bin",
"vue-language-server.js",
)
- if (!(await Filesystem.exists(js))) {
+ if (!(await Bun.file(js).exists())) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
await Bun.spawn([BunProc.which(), "install", "@vue/language-server"], {
cwd: Global.Path.bin,
@@ -173,14 +173,14 @@ export namespace LSPServer {
if (!eslint) return
log.info("spawning eslint server")
const serverPath = path.join(Global.Path.bin, "vscode-eslint", "server", "out", "eslintServer.js")
- if (!(await Filesystem.exists(serverPath))) {
+ if (!(await Bun.file(serverPath).exists())) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("downloading and building VS Code ESLint server")
const response = await fetch("https://github.com/microsoft/vscode-eslint/archive/refs/heads/main.zip")
if (!response.ok) return
const zipPath = path.join(Global.Path.bin, "vscode-eslint.zip")
- if (response.body) await Filesystem.writeStream(zipPath, response.body)
+ await Bun.file(zipPath).write(response)
const ok = await Archive.extractZip(zipPath, Global.Path.bin)
.then(() => true)
@@ -242,7 +242,7 @@ export namespace LSPServer {
const resolveBin = async (target: string) => {
const localBin = path.join(root, target)
- if (await Filesystem.exists(localBin)) return localBin
+ if (await Bun.file(localBin).exists()) return localBin
const candidates = Filesystem.up({
targets: [target],
@@ -326,7 +326,7 @@ export namespace LSPServer {
async spawn(root) {
const localBin = path.join(root, "node_modules", ".bin", "biome")
let bin: string | undefined
- if (await Filesystem.exists(localBin)) bin = localBin
+ if (await Bun.file(localBin).exists()) bin = localBin
if (!bin) {
const found = Bun.which("biome")
if (found) bin = found
@@ -467,7 +467,7 @@ export namespace LSPServer {
const potentialPythonPath = isWindows
? path.join(venvPath, "Scripts", "python.exe")
: path.join(venvPath, "bin", "python")
- if (await Filesystem.exists(potentialPythonPath)) {
+ if (await Bun.file(potentialPythonPath).exists()) {
initialization["pythonPath"] = potentialPythonPath
break
}
@@ -479,7 +479,7 @@ export namespace LSPServer {
const potentialTyPath = isWindows
? path.join(venvPath, "Scripts", "ty.exe")
: path.join(venvPath, "bin", "ty")
- if (await Filesystem.exists(potentialTyPath)) {
+ if (await Bun.file(potentialTyPath).exists()) {
binary = potentialTyPath
break
}
@@ -511,7 +511,7 @@ export namespace LSPServer {
const args = []
if (!binary) {
const js = path.join(Global.Path.bin, "node_modules", "pyright", "dist", "pyright-langserver.js")
- if (!(await Filesystem.exists(js))) {
+ if (!(await Bun.file(js).exists())) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
await Bun.spawn([BunProc.which(), "install", "pyright"], {
cwd: Global.Path.bin,
@@ -536,7 +536,7 @@ export namespace LSPServer {
const potentialPythonPath = isWindows
? path.join(venvPath, "Scripts", "python.exe")
: path.join(venvPath, "bin", "python")
- if (await Filesystem.exists(potentialPythonPath)) {
+ if (await Bun.file(potentialPythonPath).exists()) {
initialization["pythonPath"] = potentialPythonPath
break
}
@@ -571,7 +571,7 @@ export namespace LSPServer {
process.platform === "win32" ? "language_server.bat" : "language_server.sh",
)
- if (!(await Filesystem.exists(binary))) {
+ if (!(await Bun.file(binary).exists())) {
const elixir = Bun.which("elixir")
if (!elixir) {
log.error("elixir is required to run elixir-ls")
@@ -584,7 +584,7 @@ export namespace LSPServer {
const response = await fetch("https://github.com/elixir-lsp/elixir-ls/archive/refs/heads/master.zip")
if (!response.ok) return
const zipPath = path.join(Global.Path.bin, "elixir-ls.zip")
- if (response.body) await Filesystem.writeStream(zipPath, response.body)
+ await Bun.file(zipPath).write(response)
const ok = await Archive.extractZip(zipPath, Global.Path.bin)
.then(() => true)
@@ -692,7 +692,7 @@ export namespace LSPServer {
}
const tempPath = path.join(Global.Path.bin, assetName)
- if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body)
+ await Bun.file(tempPath).write(downloadResponse)
if (ext === "zip") {
const ok = await Archive.extractZip(tempPath, Global.Path.bin)
@@ -710,7 +710,7 @@ export namespace LSPServer {
bin = path.join(Global.Path.bin, "zls" + (platform === "win32" ? ".exe" : ""))
- if (!(await Filesystem.exists(bin))) {
+ if (!(await Bun.file(bin).exists())) {
log.error("Failed to extract zls binary")
return
}
@@ -857,7 +857,7 @@ export namespace LSPServer {
// Stop at filesystem root
const cargoTomlPath = path.join(currentDir, "Cargo.toml")
try {
- const cargoTomlContent = await Filesystem.readText(cargoTomlPath)
+ const cargoTomlContent = await Bun.file(cargoTomlPath).text()
if (cargoTomlContent.includes("[workspace]")) {
return currentDir
}
@@ -907,7 +907,7 @@ export namespace LSPServer {
const ext = process.platform === "win32" ? ".exe" : ""
const direct = path.join(Global.Path.bin, "clangd" + ext)
- if (await Filesystem.exists(direct)) {
+ if (await Bun.file(direct).exists()) {
return {
process: spawn(direct, args, {
cwd: root,
@@ -920,7 +920,7 @@ export namespace LSPServer {
if (!entry.isDirectory()) continue
if (!entry.name.startsWith("clangd_")) continue
const candidate = path.join(Global.Path.bin, entry.name, "bin", "clangd" + ext)
- if (await Filesystem.exists(candidate)) {
+ if (await Bun.file(candidate).exists()) {
return {
process: spawn(candidate, args, {
cwd: root,
@@ -990,7 +990,7 @@ export namespace LSPServer {
log.error("Failed to write clangd archive")
return
}
- await Filesystem.write(archive, Buffer.from(buf))
+ await Bun.write(archive, buf)
const zip = name.endsWith(".zip")
const tar = name.endsWith(".tar.xz")
@@ -1014,7 +1014,7 @@ export namespace LSPServer {
await fs.rm(archive, { force: true })
const bin = path.join(Global.Path.bin, "clangd_" + tag, "bin", "clangd" + ext)
- if (!(await Filesystem.exists(bin))) {
+ if (!(await Bun.file(bin).exists())) {
log.error("Failed to extract clangd binary")
return
}
@@ -1045,7 +1045,7 @@ export namespace LSPServer {
const args: string[] = []
if (!binary) {
const js = path.join(Global.Path.bin, "node_modules", "svelte-language-server", "bin", "server.js")
- if (!(await Filesystem.exists(js))) {
+ if (!(await Bun.file(js).exists())) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
await Bun.spawn([BunProc.which(), "install", "svelte-language-server"], {
cwd: Global.Path.bin,
@@ -1092,7 +1092,7 @@ export namespace LSPServer {
const args: string[] = []
if (!binary) {
const js = path.join(Global.Path.bin, "node_modules", "@astrojs", "language-server", "bin", "nodeServer.js")
- if (!(await Filesystem.exists(js))) {
+ if (!(await Bun.file(js).exists())) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
await Bun.spawn([BunProc.which(), "install", "@astrojs/language-server"], {
cwd: Global.Path.bin,
@@ -1248,7 +1248,7 @@ export namespace LSPServer {
const distPath = path.join(Global.Path.bin, "kotlin-ls")
const launcherScript =
process.platform === "win32" ? path.join(distPath, "kotlin-lsp.cmd") : path.join(distPath, "kotlin-lsp.sh")
- const installed = await Filesystem.exists(launcherScript)
+ const installed = await Bun.file(launcherScript).exists()
if (!installed) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("Downloading Kotlin Language Server from GitHub.")
@@ -1307,7 +1307,7 @@ export namespace LSPServer {
}
log.info("Installed Kotlin Language Server", { path: launcherScript })
}
- if (!(await Filesystem.exists(launcherScript))) {
+ if (!(await Bun.file(launcherScript).exists())) {
log.error(`Failed to locate the Kotlin LS launcher script in the installed directory: ${distPath}.`)
return
}
@@ -1336,7 +1336,7 @@ export namespace LSPServer {
"src",
"server.js",
)
- const exists = await Filesystem.exists(js)
+ const exists = await Bun.file(js).exists()
if (!exists) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
await Bun.spawn([BunProc.which(), "install", "yaml-language-server"], {
@@ -1443,7 +1443,7 @@ export namespace LSPServer {
}
const tempPath = path.join(Global.Path.bin, assetName)
- if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body)
+ await Bun.file(tempPath).write(downloadResponse)
// Unlike zls which is a single self-contained binary,
// lua-language-server needs supporting files (meta/, locale/, etc.)
@@ -1482,7 +1482,7 @@ export namespace LSPServer {
// Binary is located in bin/ subdirectory within the extracted archive
bin = path.join(installDir, "bin", "lua-language-server" + (platform === "win32" ? ".exe" : ""))
- if (!(await Filesystem.exists(bin))) {
+ if (!(await Bun.file(bin).exists())) {
log.error("Failed to extract lua-language-server binary")
return
}
@@ -1516,7 +1516,7 @@ export namespace LSPServer {
const args: string[] = []
if (!binary) {
const js = path.join(Global.Path.bin, "node_modules", "intelephense", "lib", "intelephense.js")
- if (!(await Filesystem.exists(js))) {
+ if (!(await Bun.file(js).exists())) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
await Bun.spawn([BunProc.which(), "install", "intelephense"], {
cwd: Global.Path.bin,
@@ -1613,7 +1613,7 @@ export namespace LSPServer {
const args: string[] = []
if (!binary) {
const js = path.join(Global.Path.bin, "node_modules", "bash-language-server", "out", "cli.js")
- if (!(await Filesystem.exists(js))) {
+ if (!(await Bun.file(js).exists())) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
await Bun.spawn([BunProc.which(), "install", "bash-language-server"], {
cwd: Global.Path.bin,
@@ -1654,17 +1654,22 @@ export namespace LSPServer {
if (!bin) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
- log.info("downloading terraform-ls from HashiCorp releases")
+ log.info("downloading terraform-ls from GitHub releases")
- const releaseResponse = await fetch("https://api.releases.hashicorp.com/v1/releases/terraform-ls/latest")
+ const releaseResponse = await fetch("https://api.github.com/repos/hashicorp/terraform-ls/releases/latest")
if (!releaseResponse.ok) {
log.error("Failed to fetch terraform-ls release info")
return
}
const release = (await releaseResponse.json()) as {
- version?: string
- builds?: { arch?: string; os?: string; url?: string }[]
+ tag_name?: string
+ assets?: { name?: string; browser_download_url?: string }[]
+ }
+ const version = release.tag_name?.replace("v", "")
+ if (!version) {
+ log.error("terraform-ls release did not include a version tag")
+ return
}
const platform = process.platform
@@ -1673,21 +1678,23 @@ export namespace LSPServer {
const tfArch = arch === "arm64" ? "arm64" : "amd64"
const tfPlatform = platform === "win32" ? "windows" : platform
- const builds = release.builds ?? []
- const build = builds.find((b) => b.arch === tfArch && b.os === tfPlatform)
- if (!build?.url) {
- log.error(`Could not find build for ${tfPlatform}/${tfArch} terraform-ls release version ${release.version}`)
+ const assetName = `terraform-ls_${version}_${tfPlatform}_${tfArch}.zip`
+
+ const assets = release.assets ?? []
+ const asset = assets.find((a) => a.name === assetName)
+ if (!asset?.browser_download_url) {
+ log.error(`Could not find asset ${assetName} in terraform-ls release`)
return
}
- const downloadResponse = await fetch(build.url)
+ const downloadResponse = await fetch(asset.browser_download_url)
if (!downloadResponse.ok) {
log.error("Failed to download terraform-ls")
return
}
- const tempPath = path.join(Global.Path.bin, "terraform-ls.zip")
- if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body)
+ const tempPath = path.join(Global.Path.bin, assetName)
+ await Bun.file(tempPath).write(downloadResponse)
const ok = await Archive.extractZip(tempPath, Global.Path.bin)
.then(() => true)
@@ -1700,7 +1707,7 @@ export namespace LSPServer {
bin = path.join(Global.Path.bin, "terraform-ls" + (platform === "win32" ? ".exe" : ""))
- if (!(await Filesystem.exists(bin))) {
+ if (!(await Bun.file(bin).exists())) {
log.error("Failed to extract terraform-ls binary")
return
}
@@ -1777,7 +1784,7 @@ export namespace LSPServer {
}
const tempPath = path.join(Global.Path.bin, assetName)
- if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body)
+ await Bun.file(tempPath).write(downloadResponse)
if (ext === "zip") {
const ok = await Archive.extractZip(tempPath, Global.Path.bin)
@@ -1796,7 +1803,7 @@ export namespace LSPServer {
bin = path.join(Global.Path.bin, "texlab" + (platform === "win32" ? ".exe" : ""))
- if (!(await Filesystem.exists(bin))) {
+ if (!(await Bun.file(bin).exists())) {
log.error("Failed to extract texlab binary")
return
}
@@ -1825,7 +1832,7 @@ export namespace LSPServer {
const args: string[] = []
if (!binary) {
const js = path.join(Global.Path.bin, "node_modules", "dockerfile-language-server-nodejs", "lib", "server.js")
- if (!(await Filesystem.exists(js))) {
+ if (!(await Bun.file(js).exists())) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
await Bun.spawn([BunProc.which(), "install", "dockerfile-language-server-nodejs"], {
cwd: Global.Path.bin,
@@ -1983,7 +1990,7 @@ export namespace LSPServer {
}
const tempPath = path.join(Global.Path.bin, assetName)
- if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body)
+ await Bun.file(tempPath).write(downloadResponse)
if (ext === "zip") {
const ok = await Archive.extractZip(tempPath, Global.Path.bin)
@@ -2001,7 +2008,7 @@ export namespace LSPServer {
bin = path.join(Global.Path.bin, "tinymist" + (platform === "win32" ? ".exe" : ""))
- if (!(await Filesystem.exists(bin))) {
+ if (!(await Bun.file(bin).exists())) {
log.error("Failed to extract tinymist binary")
return
}
diff --git a/packages/opencode/src/mcp/auth.ts b/packages/opencode/src/mcp/auth.ts
index 399986376..0f91a35b8 100644
--- a/packages/opencode/src/mcp/auth.ts
+++ b/packages/opencode/src/mcp/auth.ts
@@ -1,7 +1,6 @@
import path from "path"
import z from "zod"
import { Global } from "../global"
-import { Filesystem } from "../util/filesystem"
export namespace McpAuth {
export const Tokens = z.object({
@@ -54,22 +53,25 @@ export namespace McpAuth {
}
export async function all(): Promise<Record<string, Entry>> {
- return Filesystem.readJson<Record<string, Entry>>(filepath).catch(() => ({}))
+ const file = Bun.file(filepath)
+ return file.json().catch(() => ({}))
}
export async function set(mcpName: string, entry: Entry, serverUrl?: string): Promise<void> {
+ const file = Bun.file(filepath)
const data = await all()
// Always update serverUrl if provided
if (serverUrl) {
entry.serverUrl = serverUrl
}
- await Filesystem.writeJson(filepath, { ...data, [mcpName]: entry }, 0o600)
+ await Bun.write(file, JSON.stringify({ ...data, [mcpName]: entry }, null, 2), { mode: 0o600 })
}
export async function remove(mcpName: string): Promise<void> {
+ const file = Bun.file(filepath)
const data = await all()
delete data[mcpName]
- await Filesystem.writeJson(filepath, data, 0o600)
+ await Bun.write(file, JSON.stringify(data, null, 2), { mode: 0o600 })
}
export async function updateTokens(mcpName: string, tokens: Tokens, serverUrl?: string): Promise<void> {
diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts
index 63c1c4cad..8fa0f6c6f 100644
--- a/packages/opencode/src/project/project.ts
+++ b/packages/opencode/src/project/project.ts
@@ -86,7 +86,8 @@ export namespace Project {
const gitBinary = Bun.which("git")
// cached id calculation
- let id = await Filesystem.readText(path.join(dotgit, "opencode"))
+ let id = await Bun.file(path.join(dotgit, "opencode"))
+ .text()
.then((x) => x.trim())
.catch(() => undefined)
@@ -124,7 +125,9 @@ export namespace Project {
id = roots[0]
if (id) {
- void Filesystem.write(path.join(dotgit, "opencode"), id).catch(() => undefined)
+ void Bun.file(path.join(dotgit, "opencode"))
+ .write(id)
+ .catch(() => undefined)
}
}
@@ -274,9 +277,10 @@ export namespace Project {
)
const shortest = matches.sort((a, b) => a.length - b.length)[0]
if (!shortest) return
- const buffer = await Filesystem.readBytes(shortest)
- const base64 = buffer.toString("base64")
- const mime = Filesystem.mimeType(shortest) || "image/png"
+ const file = Bun.file(shortest)
+ const buffer = await file.arrayBuffer()
+ const base64 = Buffer.from(buffer).toString("base64")
+ const mime = file.type || "image/png"
const url = `data:${mime};base64,${base64}`
await update({
projectID: input.id,
@@ -377,8 +381,10 @@ export namespace Project {
const data = fromRow(row)
const valid: string[] = []
for (const dir of data.sandboxes) {
- const s = Filesystem.stat(dir)
- if (s?.isDirectory()) valid.push(dir)
+ const stat = await Bun.file(dir)
+ .stat()
+ .catch(() => undefined)
+ if (stat?.isDirectory()) valid.push(dir)
}
return valid
}
diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts
index bae331784..0960176e2 100644
--- a/packages/opencode/src/provider/models.ts
+++ b/packages/opencode/src/provider/models.ts
@@ -5,7 +5,6 @@ import z from "zod"
import { Installation } from "../installation"
import { Flag } from "../flag/flag"
import { lazy } from "@/util/lazy"
-import { Filesystem } from "../util/filesystem"
// Try to import bundled snapshot (generated at build time)
// Falls back to undefined in dev mode when snapshot doesn't exist
@@ -86,7 +85,8 @@ export namespace ModelsDev {
}
export const Data = lazy(async () => {
- const result = await Filesystem.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath).catch(() => {})
+ const file = Bun.file(Flag.OPENCODE_MODELS_PATH ?? filepath)
+ const result = await file.json().catch(() => {})
if (result) return result
// @ts-ignore
const snapshot = await import("./models-snapshot")
@@ -104,6 +104,7 @@ export namespace ModelsDev {
}
export async function refresh() {
+ const file = Bun.file(filepath)
const result = await fetch(`${url()}/api.json`, {
headers: {
"User-Agent": Installation.USER_AGENT,
@@ -115,7 +116,7 @@ export namespace ModelsDev {
})
})
if (result && result.ok) {
- await Filesystem.write(filepath, await result.text())
+ await Bun.write(file, await result.text())
ModelsDev.Data.reset()
}
}
diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts
index 6480625e9..d94d0cbb2 100644
--- a/packages/opencode/src/provider/provider.ts
+++ b/packages/opencode/src/provider/provider.ts
@@ -16,7 +16,6 @@ import { Flag } from "../flag/flag"
import { iife } from "@/util/iife"
import { Global } from "../global"
import path from "path"
-import { Filesystem } from "../util/filesystem"
// Direct imports for bundled providers
import { createAmazonBedrock, type AmazonBedrockProviderSettings } from "@ai-sdk/amazon-bedrock"
@@ -1292,9 +1291,8 @@ export namespace Provider {
if (cfg.model) return parseModel(cfg.model)
const providers = await list()
- const recent = (await Filesystem.readJson<{ recent?: { providerID: string; modelID: string }[] }>(
- path.join(Global.Path.state, "model.json"),
- )
+ const recent = (await Bun.file(path.join(Global.Path.state, "model.json"))
+ .json()
.then((x) => (Array.isArray(x.recent) ? x.recent : []))
.catch(() => [])) as { providerID: string; modelID: string }[]
for (const entry of recent) {
diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts
index d65ada278..6fb2a7aeb 100644
--- a/packages/opencode/src/session/instruction.ts
+++ b/packages/opencode/src/session/instruction.ts
@@ -85,7 +85,7 @@ export namespace InstructionPrompt {
}
for (const file of globalFiles()) {
- if (await Filesystem.exists(file)) {
+ if (await Bun.file(file).exists()) {
paths.add(path.resolve(file))
break
}
@@ -120,7 +120,9 @@ export namespace InstructionPrompt {
const paths = await systemPaths()
const files = Array.from(paths).map(async (p) => {
- const content = await Filesystem.readText(p).catch(() => "")
+ const content = await Bun.file(p)
+ .text()
+ .catch(() => "")
return content ? "Instructions from: " + p + "\n" + content : ""
})
@@ -162,7 +164,7 @@ export namespace InstructionPrompt {
export async function find(dir: string) {
for (const file of FILES) {
const filepath = path.resolve(path.join(dir, file))
- if (await Filesystem.exists(filepath)) return filepath
+ if (await Bun.file(filepath).exists()) return filepath
}
}
@@ -180,7 +182,9 @@ export namespace InstructionPrompt {
if (found && found !== target && !system.has(found) && !already.has(found) && !isClaimed(messageID, found)) {
claim(messageID, found)
- const content = await Filesystem.readText(found).catch(() => undefined)
+ const content = await Bun.file(found)
+ .text()
+ .catch(() => undefined)
if (content) {
results.push({ filepath: found, content: "Instructions from: " + found + "\n" + content })
}
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index 6ca93979e..d1f407258 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -2,7 +2,6 @@ import path from "path"
import os from "os"
import fs from "fs/promises"
import z from "zod"
-import { Filesystem } from "../util/filesystem"
import { Identifier } from "../id/id"
import { MessageV2 } from "./message-v2"
import { Log } from "../util/log"
@@ -1083,9 +1082,11 @@ export namespace SessionPrompt {
// have to normalize, symbol search returns absolute paths
// Decode the pathname since URL constructor doesn't automatically decode it
const filepath = fileURLToPath(part.url)
- const s = Filesystem.stat(filepath)
+ const stat = await Bun.file(filepath)
+ .stat()
+ .catch(() => undefined)
- if (s?.isDirectory()) {
+ if (stat?.isDirectory()) {
part.mime = "application/x-directory"
}
@@ -1232,13 +1233,14 @@ export namespace SessionPrompt {
]
}
+ const file = Bun.file(filepath)
FileTime.read(input.sessionID, filepath)
return [
{
messageID: info.id,
sessionID: input.sessionID,
type: "text",
- text: `Called the Read tool with the following input: {"filePath":"${filepath}"}`,
+ text: `Called the Read tool with the following input: {\"filePath\":\"${filepath}\"}`,
synthetic: true,
},
{
@@ -1246,7 +1248,7 @@ export namespace SessionPrompt {
messageID: info.id,
sessionID: input.sessionID,
type: "file",
- url: `data:${part.mime};base64,` + (await Filesystem.readBytes(filepath)).toString("base64"),
+ url: `data:${part.mime};base64,` + Buffer.from(await file.bytes()).toString("base64"),
mime: part.mime,
filename: part.filename!,
source: part.source,
@@ -1352,7 +1354,7 @@ export namespace SessionPrompt {
// Switching from plan mode to build mode
if (input.agent.name !== "plan" && assistantMessage?.info.agent === "plan") {
const plan = Session.plan(input.session)
- const exists = await Filesystem.exists(plan)
+ const exists = await Bun.file(plan).exists()
if (exists) {
const part = await Session.updatePart({
id: Identifier.ascending("part"),
@@ -1371,7 +1373,7 @@ export namespace SessionPrompt {
// Entering plan mode
if (input.agent.name === "plan" && assistantMessage?.info.agent !== "plan") {
const plan = Session.plan(input.session)
- const exists = await Filesystem.exists(plan)
+ const exists = await Bun.file(plan).exists()
if (!exists) await fs.mkdir(path.dirname(plan), { recursive: true })
const part = await Session.updatePart({
id: Identifier.ascending("part"),
diff --git a/packages/opencode/src/shell/shell.ts b/packages/opencode/src/shell/shell.ts
index e7b7cdb3e..2e8d48bfd 100644
--- a/packages/opencode/src/shell/shell.ts
+++ b/packages/opencode/src/shell/shell.ts
@@ -1,6 +1,5 @@
import { Flag } from "@/flag/flag"
import { lazy } from "@/util/lazy"
-import { Filesystem } from "@/util/filesystem"
import path from "path"
import { spawn, type ChildProcess } from "child_process"
@@ -44,7 +43,7 @@ export namespace Shell {
// git.exe is typically at: C:\Program Files\Git\cmd\git.exe
// bash.exe is at: C:\Program Files\Git\bin\bash.exe
const bash = path.join(git, "..", "..", "bin", "bash.exe")
- if (Filesystem.stat(bash)?.size) return bash
+ if (Bun.file(bash).size) return bash
}
return process.env.COMSPEC || "cmd.exe"
}
diff --git a/packages/opencode/src/skill/discovery.ts b/packages/opencode/src/skill/discovery.ts
index 846002cda..a4bf97d7a 100644
--- a/packages/opencode/src/skill/discovery.ts
+++ b/packages/opencode/src/skill/discovery.ts
@@ -2,7 +2,6 @@ import path from "path"
import { mkdir } from "fs/promises"
import { Log } from "../util/log"
import { Global } from "../global"
-import { Filesystem } from "../util/filesystem"
export namespace Discovery {
const log = Log.create({ service: "skill-discovery" })
@@ -20,14 +19,14 @@ export namespace Discovery {
}
async function get(url: string, dest: string): Promise<boolean> {
- if (await Filesystem.exists(dest)) return true
+ if (await Bun.file(dest).exists()) return true
return fetch(url)
.then(async (response) => {
if (!response.ok) {
log.error("failed to download", { url, status: response.status })
return false
}
- if (response.body) await Filesystem.writeStream(dest, response.body)
+ await Bun.write(dest, await response.text())
return true
})
.catch((err) => {
@@ -89,7 +88,7 @@ export namespace Discovery {
)
const md = path.join(root, "SKILL.md")
- if (await Filesystem.exists(md)) result.push(root)
+ if (await Bun.file(md).exists()) result.push(root)
}),
)
diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts
index 6d7bfd728..0974cbe7b 100644
--- a/packages/opencode/src/storage/db.ts
+++ b/packages/opencode/src/storage/db.ts
@@ -10,7 +10,7 @@ import { Log } from "../util/log"
import { NamedError } from "@opencode-ai/util/error"
import z from "zod"
import path from "path"
-import { readFileSync, readdirSync, existsSync } from "fs"
+import { readFileSync, readdirSync } from "fs"
import * as schema from "./schema"
declare const OPENCODE_MIGRATIONS: { sql: string; timestamp: number }[] | undefined
@@ -54,7 +54,7 @@ export namespace Database {
const sql = dirs
.map((name) => {
const file = path.join(dir, name, "migration.sql")
- if (!existsSync(file)) return
+ if (!Bun.file(file).size) return
return {
sql: readFileSync(file, "utf-8"),
timestamp: time(name),
diff --git a/packages/opencode/src/storage/json-migration.ts b/packages/opencode/src/storage/json-migration.ts
index 268442dcf..e0684ce3c 100644
--- a/packages/opencode/src/storage/json-migration.ts
+++ b/packages/opencode/src/storage/json-migration.ts
@@ -7,7 +7,6 @@ import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } fro
import { SessionShareTable } from "../share/share.sql"
import path from "path"
import { existsSync } from "fs"
-import { Filesystem } from "../util/filesystem"
export namespace JsonMigration {
const log = Log.create({ service: "json-migration" })
@@ -83,7 +82,7 @@ export namespace JsonMigration {
const count = end - start
const tasks = new Array(count)
for (let i = 0; i < count; i++) {
- tasks[i] = Filesystem.readJson(files[start + i])
+ tasks[i] = Bun.file(files[start + i]).json()
}
const results = await Promise.allSettled(tasks)
const items = new Array(count)
diff --git a/packages/opencode/src/storage/storage.ts b/packages/opencode/src/storage/storage.ts
index f5459ee49..18f2d67e7 100644
--- a/packages/opencode/src/storage/storage.ts
+++ b/packages/opencode/src/storage/storage.ts
@@ -39,7 +39,7 @@ export namespace Storage {
cwd: path.join(project, projectDir),
absolute: true,
})) {
- const json = await Filesystem.readJson<any>(msgFile)
+ const json = await Bun.file(msgFile).json()
worktree = json.path?.root
if (worktree) break
}
@@ -60,15 +60,18 @@ export namespace Storage {
if (!id) continue
projectID = id
- await Filesystem.writeJson(path.join(dir, "project", projectID + ".json"), {
- id,
- vcs: "git",
- worktree,
- time: {
- created: Date.now(),
- initialized: Date.now(),
- },
- })
+ await Bun.write(
+ path.join(dir, "project", projectID + ".json"),
+ JSON.stringify({
+ id,
+ vcs: "git",
+ worktree,
+ time: {
+ created: Date.now(),
+ initialized: Date.now(),
+ },
+ }),
+ )
log.info(`migrating sessions for project ${projectID}`)
for await (const sessionFile of new Bun.Glob("storage/session/info/*.json").scan({
@@ -80,8 +83,8 @@ export namespace Storage {
sessionFile,
dest,
})
- const session = await Filesystem.readJson<any>(sessionFile)
- await Filesystem.writeJson(dest, session)
+ const session = await Bun.file(sessionFile).json()
+ await Bun.write(dest, JSON.stringify(session))
log.info(`migrating messages for session ${session.id}`)
for await (const msgFile of new Bun.Glob(`storage/session/message/${session.id}/*.json`).scan({
cwd: fullProjectDir,
@@ -92,8 +95,8 @@ export namespace Storage {
msgFile,
dest,
})
- const message = await Filesystem.readJson<any>(msgFile)
- await Filesystem.writeJson(dest, message)
+ const message = await Bun.file(msgFile).json()
+ await Bun.write(dest, JSON.stringify(message))
log.info(`migrating parts for message ${message.id}`)
for await (const partFile of new Bun.Glob(`storage/session/part/${session.id}/${message.id}/*.json`).scan(
@@ -120,32 +123,35 @@ export namespace Storage {
cwd: dir,
absolute: true,
})) {
- const session = await Filesystem.readJson<any>(item)
+ const session = await Bun.file(item).json()
if (!session.projectID) continue
if (!session.summary?.diffs) continue
const { diffs } = session.summary
- await Filesystem.write(path.join(dir, "session_diff", session.id + ".json"), JSON.stringify(diffs))
- await Filesystem.writeJson(path.join(dir, "session", session.projectID, session.id + ".json"), {
- ...session,
- summary: {
- additions: diffs.reduce((sum: any, x: any) => sum + x.additions, 0),
- deletions: diffs.reduce((sum: any, x: any) => sum + x.deletions, 0),
- },
- })
+ await Bun.file(path.join(dir, "session_diff", session.id + ".json")).write(JSON.stringify(diffs))
+ await Bun.file(path.join(dir, "session", session.projectID, session.id + ".json")).write(
+ JSON.stringify({
+ ...session,
+ summary: {
+ additions: diffs.reduce((sum: any, x: any) => sum + x.additions, 0),
+ deletions: diffs.reduce((sum: any, x: any) => sum + x.deletions, 0),
+ },
+ }),
+ )
}
},
]
const state = lazy(async () => {
const dir = path.join(Global.Path.data, "storage")
- const migration = await Filesystem.readJson<string>(path.join(dir, "migration"))
+ const migration = await Bun.file(path.join(dir, "migration"))
+ .json()
.then((x) => parseInt(x))
.catch(() => 0)
for (let index = migration; index < MIGRATIONS.length; index++) {
log.info("running migration", { index })
const migration = MIGRATIONS[index]
await migration(dir).catch(() => log.error("failed to run migration", { index }))
- await Filesystem.write(path.join(dir, "migration"), (index + 1).toString())
+ await Bun.write(path.join(dir, "migration"), (index + 1).toString())
}
return {
dir,
@@ -165,7 +171,7 @@ export namespace Storage {
const target = path.join(dir, ...key) + ".json"
return withErrorHandling(async () => {
using _ = await Lock.read(target)
- const result = await Filesystem.readJson<T>(target)
+ const result = await Bun.file(target).json()
return result as T
})
}
@@ -175,10 +181,10 @@ export namespace Storage {
const target = path.join(dir, ...key) + ".json"
return withErrorHandling(async () => {
using _ = await Lock.write(target)
- const content = await Filesystem.readJson<T>(target)
- fn(content as T)
- await Filesystem.writeJson(target, content)
- return content
+ const content = await Bun.file(target).json()
+ fn(content)
+ await Bun.write(target, JSON.stringify(content, null, 2))
+ return content as T
})
}
@@ -187,7 +193,7 @@ export namespace Storage {
const target = path.join(dir, ...key) + ".json"
return withErrorHandling(async () => {
using _ = await Lock.write(target)
- await Filesystem.writeJson(target, content)
+ await Bun.write(target, JSON.stringify(content, null, 2))
})
}
diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts
index 7a097d3fe..d84f6ec34 100644
--- a/packages/opencode/src/tool/edit.ts
+++ b/packages/opencode/src/tool/edit.ts
@@ -49,7 +49,7 @@ export const EditTool = Tool.define("edit", {
let contentNew = ""
await FileTime.withLock(filePath, async () => {
if (params.oldString === "") {
- const existed = await Filesystem.exists(filePath)
+ const existed = await Bun.file(filePath).exists()
contentNew = params.newString
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
await ctx.ask({
@@ -61,7 +61,7 @@ export const EditTool = Tool.define("edit", {
diff,
},
})
- await Filesystem.write(filePath, params.newString)
+ await Bun.write(filePath, params.newString)
await Bus.publish(File.Event.Edited, {
file: filePath,
})
@@ -73,11 +73,12 @@ export const EditTool = Tool.define("edit", {
return
}
- const stats = Filesystem.stat(filePath)
+ const file = Bun.file(filePath)
+ const stats = await file.stat().catch(() => {})
if (!stats) throw new Error(`File ${filePath} not found`)
if (stats.isDirectory()) throw new Error(`Path is a directory, not a file: ${filePath}`)
await FileTime.assert(ctx.sessionID, filePath)
- contentOld = await Filesystem.readText(filePath)
+ contentOld = await file.text()
contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll)
diff = trimDiff(
@@ -93,7 +94,7 @@ export const EditTool = Tool.define("edit", {
},
})
- await Filesystem.write(filePath, contentNew)
+ await file.write(contentNew)
await Bus.publish(File.Event.Edited, {
file: filePath,
})
@@ -101,7 +102,7 @@ export const EditTool = Tool.define("edit", {
file: filePath,
event: "change",
})
- contentNew = await Filesystem.readText(filePath)
+ contentNew = await file.text()
diff = trimDiff(
createTwoFilesPatch(filePath, filePath, normalizeLineEndings(contentOld), normalizeLineEndings(contentNew)),
)
diff --git a/packages/opencode/src/tool/glob.ts b/packages/opencode/src/tool/glob.ts
index a2611246c..9df1eedca 100644
--- a/packages/opencode/src/tool/glob.ts
+++ b/packages/opencode/src/tool/glob.ts
@@ -1,7 +1,6 @@
import z from "zod"
import path from "path"
import { Tool } from "./tool"
-import { Filesystem } from "../util/filesystem"
import DESCRIPTION from "./glob.txt"
import { Ripgrep } from "../file/ripgrep"
import { Instance } from "../project/instance"
@@ -46,7 +45,10 @@ export const GlobTool = Tool.define("glob", {
break
}
const full = path.resolve(search, file)
- const stats = Filesystem.stat(full)?.mtime.getTime() ?? 0
+ const stats = await Bun.file(full)
+ .stat()
+ .then((x) => x.mtime.getTime())
+ .catch(() => 0)
files.push({
path: full,
mtime: stats,
diff --git a/packages/opencode/src/tool/grep.ts b/packages/opencode/src/tool/grep.ts
index 00497d4e3..41ed494de 100644
--- a/packages/opencode/src/tool/grep.ts
+++ b/packages/opencode/src/tool/grep.ts
@@ -1,6 +1,5 @@
import z from "zod"
import { Tool } from "./tool"
-import { Filesystem } from "../util/filesystem"
import { Ripgrep } from "../file/ripgrep"
import DESCRIPTION from "./grep.txt"
@@ -84,7 +83,8 @@ export const GrepTool = Tool.define("grep", {
const lineNum = parseInt(lineNumStr, 10)
const lineText = lineTextParts.join("|")
- const stats = Filesystem.stat(filePath)
+ const file = Bun.file(filePath)
+ const stats = await file.stat().catch(() => null)
if (!stats) continue
matches.push({
diff --git a/packages/opencode/src/tool/lsp.ts b/packages/opencode/src/tool/lsp.ts
index 52aef0f9e..ca352280b 100644
--- a/packages/opencode/src/tool/lsp.ts
+++ b/packages/opencode/src/tool/lsp.ts
@@ -6,7 +6,6 @@ import DESCRIPTION from "./lsp.txt"
import { Instance } from "../project/instance"
import { pathToFileURL } from "url"
import { assertExternalDirectory } from "./external-directory"
-import { Filesystem } from "../util/filesystem"
const operations = [
"goToDefinition",
@@ -48,7 +47,7 @@ export const LspTool = Tool.define("lsp", {
const relPath = path.relative(Instance.worktree, file)
const title = `${args.operation} ${relPath}:${args.line}:${args.character}`
- const exists = await Filesystem.exists(file)
+ const exists = await Bun.file(file).exists()
if (!exists) {
throw new Error(`File not found: ${file}`)
}
diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts
index c981ac16e..80ca95900 100644
--- a/packages/opencode/src/tool/read.ts
+++ b/packages/opencode/src/tool/read.ts
@@ -10,7 +10,6 @@ import DESCRIPTION from "./read.txt"
import { Instance } from "../project/instance"
import { assertExternalDirectory } from "./external-directory"
import { InstructionPrompt } from "../session/instruction"
-import { Filesystem } from "../util/filesystem"
const DEFAULT_READ_LIMIT = 2000
const MAX_LINE_LENGTH = 2000
@@ -35,7 +34,8 @@ export const ReadTool = Tool.define("read", {
}
const title = path.relative(Instance.worktree, filepath)
- const stat = Filesystem.stat(filepath)
+ const file = Bun.file(filepath)
+ const stat = await file.stat().catch(() => undefined)
await assertExternalDirectory(ctx, filepath, {
bypass: Boolean(ctx.extra?.["bypassCwdCheck"]),
@@ -118,10 +118,11 @@ export const ReadTool = Tool.define("read", {
const instructions = await InstructionPrompt.resolve(ctx.messages, filepath, ctx.messageID)
// Exclude SVG (XML-based) and vnd.fastbidsheet (.fbs extension, commonly FlatBuffers schema files)
- const mime = Filesystem.mimeType(filepath)
- const isImage = mime.startsWith("image/") && mime !== "image/svg+xml" && mime !== "image/vnd.fastbidsheet"
- const isPdf = mime === "application/pdf"
+ const isImage =
+ file.type.startsWith("image/") && file.type !== "image/svg+xml" && file.type !== "image/vnd.fastbidsheet"
+ const isPdf = file.type === "application/pdf"
if (isImage || isPdf) {
+ const mime = file.type
const msg = `${isImage ? "Image" : "PDF"} read successfully`
return {
title,
@@ -135,13 +136,13 @@ export const ReadTool = Tool.define("read", {
{
type: "file",
mime,
- url: `data:${mime};base64,${Buffer.from(await Filesystem.readBytes(filepath)).toString("base64")}`,
+ url: `data:${mime};base64,${Buffer.from(await file.bytes()).toString("base64")}`,
},
],
}
}
- const isBinary = await isBinaryFile(filepath, Number(stat.size))
+ const isBinary = await isBinaryFile(filepath, stat.size)
if (isBinary) throw new Error(`Cannot read binary file: ${filepath}`)
const stream = createReadStream(filepath, { encoding: "utf8" })
diff --git a/packages/opencode/src/tool/truncation.ts b/packages/opencode/src/tool/truncation.ts
index 4cc524aee..84e799c13 100644
--- a/packages/opencode/src/tool/truncation.ts
+++ b/packages/opencode/src/tool/truncation.ts
@@ -5,7 +5,6 @@ import { Identifier } from "../id/id"
import { PermissionNext } from "../permission/next"
import type { Agent } from "../agent/agent"
import { Scheduler } from "../scheduler"
-import { Filesystem } from "../util/filesystem"
export namespace Truncate {
export const MAX_LINES = 2000
@@ -92,7 +91,7 @@ export namespace Truncate {
const id = Identifier.ascending("tool")
const filepath = path.join(DIR, id)
- await Filesystem.write(filepath, text)
+ await Bun.write(Bun.file(filepath), text)
const hint = hasTaskTool(agent)
? `The tool call succeeded but the output was truncated. Full output saved to: ${filepath}\nUse the Task tool to have explore agent process this file with Grep and Read (with offset/limit). Do NOT read the full file yourself - delegate to save context.`
diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts
index 8c1e53cca..eca64d303 100644
--- a/packages/opencode/src/tool/write.ts
+++ b/packages/opencode/src/tool/write.ts
@@ -26,8 +26,9 @@ export const WriteTool = Tool.define("write", {
const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath)
await assertExternalDirectory(ctx, filepath)
- const exists = await Filesystem.exists(filepath)
- const contentOld = exists ? await Filesystem.readText(filepath) : ""
+ const file = Bun.file(filepath)
+ const exists = await file.exists()
+ const contentOld = exists ? await file.text() : ""
if (exists) await FileTime.assert(ctx.sessionID, filepath)
const diff = trimDiff(createTwoFilesPatch(filepath, filepath, contentOld, params.content))
@@ -41,7 +42,7 @@ export const WriteTool = Tool.define("write", {
},
})
- await Filesystem.write(filepath, params.content)
+ await Bun.write(filepath, params.content)
await Bus.publish(File.Event.Edited, {
file: filepath,
})
diff --git a/packages/opencode/src/util/filesystem.ts b/packages/opencode/src/util/filesystem.ts
index b60b06e08..7b196eb84 100644
--- a/packages/opencode/src/util/filesystem.ts
+++ b/packages/opencode/src/util/filesystem.ts
@@ -1,10 +1,8 @@
-import { chmod, mkdir, readFile, writeFile } from "fs/promises"
-import { createWriteStream, existsSync, statSync } from "fs"
+import { mkdir, readFile, writeFile } from "fs/promises"
+import { existsSync, statSync } from "fs"
import { lookup } from "mime-types"
import { realpathSync } from "fs"
import { dirname, join, relative } from "path"
-import { Readable } from "stream"
-import { pipeline } from "stream/promises"
export namespace Filesystem {
// Fast sync version for metadata checks
@@ -70,25 +68,6 @@ export namespace Filesystem {
return write(p, JSON.stringify(data, null, 2), mode)
}
- export async function writeStream(
- p: string,
- stream: ReadableStream<Uint8Array> | Readable,
- mode?: number,
- ): Promise<void> {
- const dir = dirname(p)
- if (!existsSync(dir)) {
- await mkdir(dir, { recursive: true })
- }
-
- const nodeStream = stream instanceof ReadableStream ? Readable.fromWeb(stream as any) : stream
- const writeStream = createWriteStream(p)
- await pipeline(nodeStream, writeStream)
-
- if (mode) {
- await chmod(p, mode)
- }
- }
-
export function mimeType(p: string): string {
return lookup(p) || "application/octet-stream"
}
diff --git a/packages/opencode/src/util/log.ts b/packages/opencode/src/util/log.ts
index c62d59299..6941310bb 100644
--- a/packages/opencode/src/util/log.ts
+++ b/packages/opencode/src/util/log.ts
@@ -1,6 +1,5 @@
import path from "path"
import fs from "fs/promises"
-import { createWriteStream } from "fs"
import { Global } from "../global"
import z from "zod"
@@ -64,15 +63,13 @@ export namespace Log {
Global.Path.log,
options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log",
)
+ const logfile = Bun.file(logpath)
await fs.truncate(logpath).catch(() => {})
- const stream = createWriteStream(logpath, { flags: "a" })
+ const writer = logfile.writer()
write = async (msg: any) => {
- return new Promise((resolve, reject) => {
- stream.write(msg, (err) => {
- if (err) reject(err)
- else resolve(msg.length)
- })
- })
+ const num = writer.write(msg)
+ writer.flush()
+ return num
}
}
diff --git a/packages/opencode/test/util/filesystem.test.ts b/packages/opencode/test/util/filesystem.test.ts
index 0f5447937..3c3da0fc7 100644
--- a/packages/opencode/test/util/filesystem.test.ts
+++ b/packages/opencode/test/util/filesystem.test.ts
@@ -285,125 +285,4 @@ describe("filesystem", () => {
expect(Filesystem.mimeType("Makefile")).toBe("application/octet-stream")
})
})
-
- describe("writeStream()", () => {
- test("writes from Web ReadableStream", async () => {
- await using tmp = await tmpdir()
- const filepath = path.join(tmp.path, "streamed.txt")
- const content = "Hello from stream!"
- const encoder = new TextEncoder()
- const stream = new ReadableStream({
- start(controller) {
- controller.enqueue(encoder.encode(content))
- controller.close()
- },
- })
-
- await Filesystem.writeStream(filepath, stream)
-
- expect(await fs.readFile(filepath, "utf-8")).toBe(content)
- })
-
- test("writes from Node.js Readable stream", async () => {
- await using tmp = await tmpdir()
- const filepath = path.join(tmp.path, "node-streamed.txt")
- const content = "Hello from Node stream!"
- const { Readable } = await import("stream")
- const stream = Readable.from([content])
-
- await Filesystem.writeStream(filepath, stream)
-
- expect(await fs.readFile(filepath, "utf-8")).toBe(content)
- })
-
- test("writes binary data from Web ReadableStream", async () => {
- await using tmp = await tmpdir()
- const filepath = path.join(tmp.path, "binary.dat")
- const binaryData = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0xff])
- const stream = new ReadableStream({
- start(controller) {
- controller.enqueue(binaryData)
- controller.close()
- },
- })
-
- await Filesystem.writeStream(filepath, stream)
-
- const read = await fs.readFile(filepath)
- expect(Buffer.from(read)).toEqual(Buffer.from(binaryData))
- })
-
- test("writes large content in chunks", async () => {
- await using tmp = await tmpdir()
- const filepath = path.join(tmp.path, "large.txt")
- const chunks = ["chunk1", "chunk2", "chunk3", "chunk4", "chunk5"]
- const stream = new ReadableStream({
- start(controller) {
- for (const chunk of chunks) {
- controller.enqueue(new TextEncoder().encode(chunk))
- }
- controller.close()
- },
- })
-
- await Filesystem.writeStream(filepath, stream)
-
- expect(await fs.readFile(filepath, "utf-8")).toBe(chunks.join(""))
- })
-
- test("creates parent directories", async () => {
- await using tmp = await tmpdir()
- const filepath = path.join(tmp.path, "nested", "deep", "streamed.txt")
- const content = "nested stream content"
- const stream = new ReadableStream({
- start(controller) {
- controller.enqueue(new TextEncoder().encode(content))
- controller.close()
- },
- })
-
- await Filesystem.writeStream(filepath, stream)
-
- expect(await fs.readFile(filepath, "utf-8")).toBe(content)
- })
-
- test("writes with permissions", async () => {
- await using tmp = await tmpdir()
- const filepath = path.join(tmp.path, "protected-stream.txt")
- const content = "secret stream content"
- const stream = new ReadableStream({
- start(controller) {
- controller.enqueue(new TextEncoder().encode(content))
- controller.close()
- },
- })
-
- await Filesystem.writeStream(filepath, stream, 0o600)
-
- const stats = await fs.stat(filepath)
- if (process.platform !== "win32") {
- expect(stats.mode & 0o777).toBe(0o600)
- }
- })
-
- test("writes executable with permissions", async () => {
- await using tmp = await tmpdir()
- const filepath = path.join(tmp.path, "script.sh")
- const content = "#!/bin/bash\necho hello"
- const stream = new ReadableStream({
- start(controller) {
- controller.enqueue(new TextEncoder().encode(content))
- controller.close()
- },
- })
-
- await Filesystem.writeStream(filepath, stream, 0o755)
-
- const stats = await fs.stat(filepath)
- if (process.platform !== "win32") {
- expect(stats.mode & 0o777).toBe(0o755)
- }
- expect(await fs.readFile(filepath, "utf-8")).toBe(content)
- })
- })
})