diff options
| author | Dax Raad <[email protected]> | 2026-02-18 19:41:14 -0500 |
|---|---|---|
| committer | Dax Raad <[email protected]> | 2026-02-18 19:41:14 -0500 |
| commit | 568eccb4c654e83382253eb0c1478d24585288aa (patch) | |
| tree | 111af95e7574da88ac72eb121eb8fb63115ddfb1 /packages | |
| parent | 3a07dd8d96e3e4cbc6787ae14add19b2d58023be (diff) | |
| download | opencode-568eccb4c654e83382253eb0c1478d24585288aa.tar.gz opencode-568eccb4c654e83382253eb0c1478d24585288aa.zip | |
Revert: all refactor commits migrating from Bun.file() to Filesystem module
Diffstat (limited to 'packages')
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) - }) - }) }) |
