Revert: all refactor commits migrating from Bun.file() to Filesystem module
This commit is contained in:
42
.opencode/skill/bun-file-io/SKILL.md
Normal file
42
.opencode/skill/bun-file-io/SKILL.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
name: bun-file-io
|
||||||
|
description: Use this when you are working on file operations like reading, writing, scanning, or deleting files. It summarizes the preferred file APIs and patterns used in this repo. It also notes when to use filesystem helpers for directories.
|
||||||
|
---
|
||||||
|
|
||||||
|
## Use this when
|
||||||
|
|
||||||
|
- Editing file I/O or scans in `packages/opencode`
|
||||||
|
- Handling directory operations or external tools
|
||||||
|
|
||||||
|
## Bun file APIs (from Bun docs)
|
||||||
|
|
||||||
|
- `Bun.file(path)` is lazy; call `text`, `json`, `stream`, `arrayBuffer`, `bytes`, `exists` to read.
|
||||||
|
- Metadata: `file.size`, `file.type`, `file.name`.
|
||||||
|
- `Bun.write(dest, input)` writes strings, buffers, Blobs, Responses, or files.
|
||||||
|
- `Bun.file(...).delete()` deletes a file.
|
||||||
|
- `file.writer()` returns a FileSink for incremental writes.
|
||||||
|
- `Bun.Glob` + `Array.fromAsync(glob.scan({ cwd, absolute, onlyFiles, dot }))` for scans.
|
||||||
|
- Use `Bun.which` to find a binary, then `Bun.spawn` to run it.
|
||||||
|
- `Bun.readableStreamToText/Bytes/JSON` for stream output.
|
||||||
|
|
||||||
|
## When to use node:fs
|
||||||
|
|
||||||
|
- Use `node:fs/promises` for directories (`mkdir`, `readdir`, recursive operations).
|
||||||
|
|
||||||
|
## Repo patterns
|
||||||
|
|
||||||
|
- Prefer Bun APIs over Node `fs` for file access.
|
||||||
|
- Check `Bun.file(...).exists()` before reading.
|
||||||
|
- For binary/large files use `arrayBuffer()` and MIME checks via `file.type`.
|
||||||
|
- Use `Bun.Glob` + `Array.fromAsync` for scans.
|
||||||
|
- Decode tool stderr with `Bun.readableStreamToText`.
|
||||||
|
- For large writes, use `Bun.write(Bun.file(path), text)`.
|
||||||
|
|
||||||
|
NOTE: Bun.file(...).exists() will return `false` if the value is a directory.
|
||||||
|
Use Filesystem.exists(...) instead if path can be file or directory
|
||||||
|
|
||||||
|
## Quick checklist
|
||||||
|
|
||||||
|
- Use Bun APIs first.
|
||||||
|
- Use `path.join`/`path.resolve` for paths.
|
||||||
|
- Prefer promise `.catch(...)` over `try/catch` when possible.
|
||||||
@@ -3,12 +3,10 @@ import { tui } from "./app"
|
|||||||
import { Rpc } from "@/util/rpc"
|
import { Rpc } from "@/util/rpc"
|
||||||
import { type rpc } from "./worker"
|
import { type rpc } from "./worker"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { fileURLToPath } from "url"
|
|
||||||
import { UI } from "@/cli/ui"
|
import { UI } from "@/cli/ui"
|
||||||
import { iife } from "@/util/iife"
|
import { iife } from "@/util/iife"
|
||||||
import { Log } from "@/util/log"
|
import { Log } from "@/util/log"
|
||||||
import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
|
import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
|
||||||
import type { Event } from "@opencode-ai/sdk/v2"
|
import type { Event } from "@opencode-ai/sdk/v2"
|
||||||
import type { EventSource } from "./context/sdk"
|
import type { EventSource } from "./context/sdk"
|
||||||
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
|
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 distWorker = new URL("./cli/cmd/tui/worker.js", import.meta.url)
|
||||||
const workerPath = await iife(async () => {
|
const workerPath = await iife(async () => {
|
||||||
if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH
|
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
|
return localWorker
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -147,7 +147,8 @@ export namespace LSPClient {
|
|||||||
notify: {
|
notify: {
|
||||||
async open(input: { path: string }) {
|
async open(input: { path: string }) {
|
||||||
input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path)
|
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 extension = path.extname(input.path)
|
||||||
const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext"
|
const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext"
|
||||||
|
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export namespace LSPServer {
|
|||||||
"bin",
|
"bin",
|
||||||
"vue-language-server.js",
|
"vue-language-server.js",
|
||||||
)
|
)
|
||||||
if (!(await Filesystem.exists(js))) {
|
if (!(await Bun.file(js).exists())) {
|
||||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||||
await Bun.spawn([BunProc.which(), "install", "@vue/language-server"], {
|
await Bun.spawn([BunProc.which(), "install", "@vue/language-server"], {
|
||||||
cwd: Global.Path.bin,
|
cwd: Global.Path.bin,
|
||||||
@@ -173,14 +173,14 @@ export namespace LSPServer {
|
|||||||
if (!eslint) return
|
if (!eslint) return
|
||||||
log.info("spawning eslint server")
|
log.info("spawning eslint server")
|
||||||
const serverPath = path.join(Global.Path.bin, "vscode-eslint", "server", "out", "eslintServer.js")
|
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
|
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||||
log.info("downloading and building VS Code ESLint server")
|
log.info("downloading and building VS Code ESLint server")
|
||||||
const response = await fetch("https://github.com/microsoft/vscode-eslint/archive/refs/heads/main.zip")
|
const response = await fetch("https://github.com/microsoft/vscode-eslint/archive/refs/heads/main.zip")
|
||||||
if (!response.ok) return
|
if (!response.ok) return
|
||||||
|
|
||||||
const zipPath = path.join(Global.Path.bin, "vscode-eslint.zip")
|
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)
|
const ok = await Archive.extractZip(zipPath, Global.Path.bin)
|
||||||
.then(() => true)
|
.then(() => true)
|
||||||
@@ -242,7 +242,7 @@ export namespace LSPServer {
|
|||||||
|
|
||||||
const resolveBin = async (target: string) => {
|
const resolveBin = async (target: string) => {
|
||||||
const localBin = path.join(root, target)
|
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({
|
const candidates = Filesystem.up({
|
||||||
targets: [target],
|
targets: [target],
|
||||||
@@ -326,7 +326,7 @@ export namespace LSPServer {
|
|||||||
async spawn(root) {
|
async spawn(root) {
|
||||||
const localBin = path.join(root, "node_modules", ".bin", "biome")
|
const localBin = path.join(root, "node_modules", ".bin", "biome")
|
||||||
let bin: string | undefined
|
let bin: string | undefined
|
||||||
if (await Filesystem.exists(localBin)) bin = localBin
|
if (await Bun.file(localBin).exists()) bin = localBin
|
||||||
if (!bin) {
|
if (!bin) {
|
||||||
const found = Bun.which("biome")
|
const found = Bun.which("biome")
|
||||||
if (found) bin = found
|
if (found) bin = found
|
||||||
@@ -467,7 +467,7 @@ export namespace LSPServer {
|
|||||||
const potentialPythonPath = isWindows
|
const potentialPythonPath = isWindows
|
||||||
? path.join(venvPath, "Scripts", "python.exe")
|
? path.join(venvPath, "Scripts", "python.exe")
|
||||||
: path.join(venvPath, "bin", "python")
|
: path.join(venvPath, "bin", "python")
|
||||||
if (await Filesystem.exists(potentialPythonPath)) {
|
if (await Bun.file(potentialPythonPath).exists()) {
|
||||||
initialization["pythonPath"] = potentialPythonPath
|
initialization["pythonPath"] = potentialPythonPath
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -479,7 +479,7 @@ export namespace LSPServer {
|
|||||||
const potentialTyPath = isWindows
|
const potentialTyPath = isWindows
|
||||||
? path.join(venvPath, "Scripts", "ty.exe")
|
? path.join(venvPath, "Scripts", "ty.exe")
|
||||||
: path.join(venvPath, "bin", "ty")
|
: path.join(venvPath, "bin", "ty")
|
||||||
if (await Filesystem.exists(potentialTyPath)) {
|
if (await Bun.file(potentialTyPath).exists()) {
|
||||||
binary = potentialTyPath
|
binary = potentialTyPath
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -511,7 +511,7 @@ export namespace LSPServer {
|
|||||||
const args = []
|
const args = []
|
||||||
if (!binary) {
|
if (!binary) {
|
||||||
const js = path.join(Global.Path.bin, "node_modules", "pyright", "dist", "pyright-langserver.js")
|
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
|
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||||
await Bun.spawn([BunProc.which(), "install", "pyright"], {
|
await Bun.spawn([BunProc.which(), "install", "pyright"], {
|
||||||
cwd: Global.Path.bin,
|
cwd: Global.Path.bin,
|
||||||
@@ -536,7 +536,7 @@ export namespace LSPServer {
|
|||||||
const potentialPythonPath = isWindows
|
const potentialPythonPath = isWindows
|
||||||
? path.join(venvPath, "Scripts", "python.exe")
|
? path.join(venvPath, "Scripts", "python.exe")
|
||||||
: path.join(venvPath, "bin", "python")
|
: path.join(venvPath, "bin", "python")
|
||||||
if (await Filesystem.exists(potentialPythonPath)) {
|
if (await Bun.file(potentialPythonPath).exists()) {
|
||||||
initialization["pythonPath"] = potentialPythonPath
|
initialization["pythonPath"] = potentialPythonPath
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -571,7 +571,7 @@ export namespace LSPServer {
|
|||||||
process.platform === "win32" ? "language_server.bat" : "language_server.sh",
|
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")
|
const elixir = Bun.which("elixir")
|
||||||
if (!elixir) {
|
if (!elixir) {
|
||||||
log.error("elixir is required to run elixir-ls")
|
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")
|
const response = await fetch("https://github.com/elixir-lsp/elixir-ls/archive/refs/heads/master.zip")
|
||||||
if (!response.ok) return
|
if (!response.ok) return
|
||||||
const zipPath = path.join(Global.Path.bin, "elixir-ls.zip")
|
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)
|
const ok = await Archive.extractZip(zipPath, Global.Path.bin)
|
||||||
.then(() => true)
|
.then(() => true)
|
||||||
@@ -692,7 +692,7 @@ export namespace LSPServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tempPath = path.join(Global.Path.bin, assetName)
|
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") {
|
if (ext === "zip") {
|
||||||
const ok = await Archive.extractZip(tempPath, Global.Path.bin)
|
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" : ""))
|
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")
|
log.error("Failed to extract zls binary")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -857,7 +857,7 @@ export namespace LSPServer {
|
|||||||
// Stop at filesystem root
|
// Stop at filesystem root
|
||||||
const cargoTomlPath = path.join(currentDir, "Cargo.toml")
|
const cargoTomlPath = path.join(currentDir, "Cargo.toml")
|
||||||
try {
|
try {
|
||||||
const cargoTomlContent = await Filesystem.readText(cargoTomlPath)
|
const cargoTomlContent = await Bun.file(cargoTomlPath).text()
|
||||||
if (cargoTomlContent.includes("[workspace]")) {
|
if (cargoTomlContent.includes("[workspace]")) {
|
||||||
return currentDir
|
return currentDir
|
||||||
}
|
}
|
||||||
@@ -907,7 +907,7 @@ export namespace LSPServer {
|
|||||||
|
|
||||||
const ext = process.platform === "win32" ? ".exe" : ""
|
const ext = process.platform === "win32" ? ".exe" : ""
|
||||||
const direct = path.join(Global.Path.bin, "clangd" + ext)
|
const direct = path.join(Global.Path.bin, "clangd" + ext)
|
||||||
if (await Filesystem.exists(direct)) {
|
if (await Bun.file(direct).exists()) {
|
||||||
return {
|
return {
|
||||||
process: spawn(direct, args, {
|
process: spawn(direct, args, {
|
||||||
cwd: root,
|
cwd: root,
|
||||||
@@ -920,7 +920,7 @@ export namespace LSPServer {
|
|||||||
if (!entry.isDirectory()) continue
|
if (!entry.isDirectory()) continue
|
||||||
if (!entry.name.startsWith("clangd_")) continue
|
if (!entry.name.startsWith("clangd_")) continue
|
||||||
const candidate = path.join(Global.Path.bin, entry.name, "bin", "clangd" + ext)
|
const candidate = path.join(Global.Path.bin, entry.name, "bin", "clangd" + ext)
|
||||||
if (await Filesystem.exists(candidate)) {
|
if (await Bun.file(candidate).exists()) {
|
||||||
return {
|
return {
|
||||||
process: spawn(candidate, args, {
|
process: spawn(candidate, args, {
|
||||||
cwd: root,
|
cwd: root,
|
||||||
@@ -990,7 +990,7 @@ export namespace LSPServer {
|
|||||||
log.error("Failed to write clangd archive")
|
log.error("Failed to write clangd archive")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await Filesystem.write(archive, Buffer.from(buf))
|
await Bun.write(archive, buf)
|
||||||
|
|
||||||
const zip = name.endsWith(".zip")
|
const zip = name.endsWith(".zip")
|
||||||
const tar = name.endsWith(".tar.xz")
|
const tar = name.endsWith(".tar.xz")
|
||||||
@@ -1014,7 +1014,7 @@ export namespace LSPServer {
|
|||||||
await fs.rm(archive, { force: true })
|
await fs.rm(archive, { force: true })
|
||||||
|
|
||||||
const bin = path.join(Global.Path.bin, "clangd_" + tag, "bin", "clangd" + ext)
|
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")
|
log.error("Failed to extract clangd binary")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1045,7 +1045,7 @@ export namespace LSPServer {
|
|||||||
const args: string[] = []
|
const args: string[] = []
|
||||||
if (!binary) {
|
if (!binary) {
|
||||||
const js = path.join(Global.Path.bin, "node_modules", "svelte-language-server", "bin", "server.js")
|
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
|
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||||
await Bun.spawn([BunProc.which(), "install", "svelte-language-server"], {
|
await Bun.spawn([BunProc.which(), "install", "svelte-language-server"], {
|
||||||
cwd: Global.Path.bin,
|
cwd: Global.Path.bin,
|
||||||
@@ -1092,7 +1092,7 @@ export namespace LSPServer {
|
|||||||
const args: string[] = []
|
const args: string[] = []
|
||||||
if (!binary) {
|
if (!binary) {
|
||||||
const js = path.join(Global.Path.bin, "node_modules", "@astrojs", "language-server", "bin", "nodeServer.js")
|
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
|
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||||
await Bun.spawn([BunProc.which(), "install", "@astrojs/language-server"], {
|
await Bun.spawn([BunProc.which(), "install", "@astrojs/language-server"], {
|
||||||
cwd: Global.Path.bin,
|
cwd: Global.Path.bin,
|
||||||
@@ -1248,7 +1248,7 @@ export namespace LSPServer {
|
|||||||
const distPath = path.join(Global.Path.bin, "kotlin-ls")
|
const distPath = path.join(Global.Path.bin, "kotlin-ls")
|
||||||
const launcherScript =
|
const launcherScript =
|
||||||
process.platform === "win32" ? path.join(distPath, "kotlin-lsp.cmd") : path.join(distPath, "kotlin-lsp.sh")
|
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 (!installed) {
|
||||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||||
log.info("Downloading Kotlin Language Server from GitHub.")
|
log.info("Downloading Kotlin Language Server from GitHub.")
|
||||||
@@ -1307,7 +1307,7 @@ export namespace LSPServer {
|
|||||||
}
|
}
|
||||||
log.info("Installed Kotlin Language Server", { path: launcherScript })
|
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}.`)
|
log.error(`Failed to locate the Kotlin LS launcher script in the installed directory: ${distPath}.`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1336,7 +1336,7 @@ export namespace LSPServer {
|
|||||||
"src",
|
"src",
|
||||||
"server.js",
|
"server.js",
|
||||||
)
|
)
|
||||||
const exists = await Filesystem.exists(js)
|
const exists = await Bun.file(js).exists()
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||||
await Bun.spawn([BunProc.which(), "install", "yaml-language-server"], {
|
await Bun.spawn([BunProc.which(), "install", "yaml-language-server"], {
|
||||||
@@ -1443,7 +1443,7 @@ export namespace LSPServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tempPath = path.join(Global.Path.bin, assetName)
|
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,
|
// Unlike zls which is a single self-contained binary,
|
||||||
// lua-language-server needs supporting files (meta/, locale/, etc.)
|
// 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
|
// Binary is located in bin/ subdirectory within the extracted archive
|
||||||
bin = path.join(installDir, "bin", "lua-language-server" + (platform === "win32" ? ".exe" : ""))
|
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")
|
log.error("Failed to extract lua-language-server binary")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1516,7 +1516,7 @@ export namespace LSPServer {
|
|||||||
const args: string[] = []
|
const args: string[] = []
|
||||||
if (!binary) {
|
if (!binary) {
|
||||||
const js = path.join(Global.Path.bin, "node_modules", "intelephense", "lib", "intelephense.js")
|
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
|
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||||
await Bun.spawn([BunProc.which(), "install", "intelephense"], {
|
await Bun.spawn([BunProc.which(), "install", "intelephense"], {
|
||||||
cwd: Global.Path.bin,
|
cwd: Global.Path.bin,
|
||||||
@@ -1613,7 +1613,7 @@ export namespace LSPServer {
|
|||||||
const args: string[] = []
|
const args: string[] = []
|
||||||
if (!binary) {
|
if (!binary) {
|
||||||
const js = path.join(Global.Path.bin, "node_modules", "bash-language-server", "out", "cli.js")
|
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
|
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||||
await Bun.spawn([BunProc.which(), "install", "bash-language-server"], {
|
await Bun.spawn([BunProc.which(), "install", "bash-language-server"], {
|
||||||
cwd: Global.Path.bin,
|
cwd: Global.Path.bin,
|
||||||
@@ -1654,17 +1654,22 @@ export namespace LSPServer {
|
|||||||
|
|
||||||
if (!bin) {
|
if (!bin) {
|
||||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
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) {
|
if (!releaseResponse.ok) {
|
||||||
log.error("Failed to fetch terraform-ls release info")
|
log.error("Failed to fetch terraform-ls release info")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const release = (await releaseResponse.json()) as {
|
const release = (await releaseResponse.json()) as {
|
||||||
version?: string
|
tag_name?: string
|
||||||
builds?: { arch?: string; os?: string; url?: 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
|
const platform = process.platform
|
||||||
@@ -1673,21 +1678,23 @@ export namespace LSPServer {
|
|||||||
const tfArch = arch === "arm64" ? "arm64" : "amd64"
|
const tfArch = arch === "arm64" ? "arm64" : "amd64"
|
||||||
const tfPlatform = platform === "win32" ? "windows" : platform
|
const tfPlatform = platform === "win32" ? "windows" : platform
|
||||||
|
|
||||||
const builds = release.builds ?? []
|
const assetName = `terraform-ls_${version}_${tfPlatform}_${tfArch}.zip`
|
||||||
const build = builds.find((b) => b.arch === tfArch && b.os === tfPlatform)
|
|
||||||
if (!build?.url) {
|
const assets = release.assets ?? []
|
||||||
log.error(`Could not find build for ${tfPlatform}/${tfArch} terraform-ls release version ${release.version}`)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const downloadResponse = await fetch(build.url)
|
const downloadResponse = await fetch(asset.browser_download_url)
|
||||||
if (!downloadResponse.ok) {
|
if (!downloadResponse.ok) {
|
||||||
log.error("Failed to download terraform-ls")
|
log.error("Failed to download terraform-ls")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const tempPath = path.join(Global.Path.bin, "terraform-ls.zip")
|
const tempPath = path.join(Global.Path.bin, assetName)
|
||||||
if (downloadResponse.body) await Filesystem.writeStream(tempPath, downloadResponse.body)
|
await Bun.file(tempPath).write(downloadResponse)
|
||||||
|
|
||||||
const ok = await Archive.extractZip(tempPath, Global.Path.bin)
|
const ok = await Archive.extractZip(tempPath, Global.Path.bin)
|
||||||
.then(() => true)
|
.then(() => true)
|
||||||
@@ -1700,7 +1707,7 @@ export namespace LSPServer {
|
|||||||
|
|
||||||
bin = path.join(Global.Path.bin, "terraform-ls" + (platform === "win32" ? ".exe" : ""))
|
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")
|
log.error("Failed to extract terraform-ls binary")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1777,7 +1784,7 @@ export namespace LSPServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tempPath = path.join(Global.Path.bin, assetName)
|
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") {
|
if (ext === "zip") {
|
||||||
const ok = await Archive.extractZip(tempPath, Global.Path.bin)
|
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" : ""))
|
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")
|
log.error("Failed to extract texlab binary")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1825,7 +1832,7 @@ export namespace LSPServer {
|
|||||||
const args: string[] = []
|
const args: string[] = []
|
||||||
if (!binary) {
|
if (!binary) {
|
||||||
const js = path.join(Global.Path.bin, "node_modules", "dockerfile-language-server-nodejs", "lib", "server.js")
|
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
|
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||||
await Bun.spawn([BunProc.which(), "install", "dockerfile-language-server-nodejs"], {
|
await Bun.spawn([BunProc.which(), "install", "dockerfile-language-server-nodejs"], {
|
||||||
cwd: Global.Path.bin,
|
cwd: Global.Path.bin,
|
||||||
@@ -1983,7 +1990,7 @@ export namespace LSPServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tempPath = path.join(Global.Path.bin, assetName)
|
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") {
|
if (ext === "zip") {
|
||||||
const ok = await Archive.extractZip(tempPath, Global.Path.bin)
|
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" : ""))
|
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")
|
log.error("Failed to extract tinymist binary")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import { Filesystem } from "../util/filesystem"
|
|
||||||
|
|
||||||
export namespace McpAuth {
|
export namespace McpAuth {
|
||||||
export const Tokens = z.object({
|
export const Tokens = z.object({
|
||||||
@@ -54,22 +53,25 @@ export namespace McpAuth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function all(): Promise<Record<string, Entry>> {
|
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> {
|
export async function set(mcpName: string, entry: Entry, serverUrl?: string): Promise<void> {
|
||||||
|
const file = Bun.file(filepath)
|
||||||
const data = await all()
|
const data = await all()
|
||||||
// Always update serverUrl if provided
|
// Always update serverUrl if provided
|
||||||
if (serverUrl) {
|
if (serverUrl) {
|
||||||
entry.serverUrl = 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> {
|
export async function remove(mcpName: string): Promise<void> {
|
||||||
|
const file = Bun.file(filepath)
|
||||||
const data = await all()
|
const data = await all()
|
||||||
delete data[mcpName]
|
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> {
|
export async function updateTokens(mcpName: string, tokens: Tokens, serverUrl?: string): Promise<void> {
|
||||||
|
|||||||
@@ -86,7 +86,8 @@ export namespace Project {
|
|||||||
const gitBinary = Bun.which("git")
|
const gitBinary = Bun.which("git")
|
||||||
|
|
||||||
// cached id calculation
|
// 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())
|
.then((x) => x.trim())
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
|
|
||||||
@@ -124,7 +125,9 @@ export namespace Project {
|
|||||||
|
|
||||||
id = roots[0]
|
id = roots[0]
|
||||||
if (id) {
|
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]
|
const shortest = matches.sort((a, b) => a.length - b.length)[0]
|
||||||
if (!shortest) return
|
if (!shortest) return
|
||||||
const buffer = await Filesystem.readBytes(shortest)
|
const file = Bun.file(shortest)
|
||||||
const base64 = buffer.toString("base64")
|
const buffer = await file.arrayBuffer()
|
||||||
const mime = Filesystem.mimeType(shortest) || "image/png"
|
const base64 = Buffer.from(buffer).toString("base64")
|
||||||
|
const mime = file.type || "image/png"
|
||||||
const url = `data:${mime};base64,${base64}`
|
const url = `data:${mime};base64,${base64}`
|
||||||
await update({
|
await update({
|
||||||
projectID: input.id,
|
projectID: input.id,
|
||||||
@@ -377,8 +381,10 @@ export namespace Project {
|
|||||||
const data = fromRow(row)
|
const data = fromRow(row)
|
||||||
const valid: string[] = []
|
const valid: string[] = []
|
||||||
for (const dir of data.sandboxes) {
|
for (const dir of data.sandboxes) {
|
||||||
const s = Filesystem.stat(dir)
|
const stat = await Bun.file(dir)
|
||||||
if (s?.isDirectory()) valid.push(dir)
|
.stat()
|
||||||
|
.catch(() => undefined)
|
||||||
|
if (stat?.isDirectory()) valid.push(dir)
|
||||||
}
|
}
|
||||||
return valid
|
return valid
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import z from "zod"
|
|||||||
import { Installation } from "../installation"
|
import { Installation } from "../installation"
|
||||||
import { Flag } from "../flag/flag"
|
import { Flag } from "../flag/flag"
|
||||||
import { lazy } from "@/util/lazy"
|
import { lazy } from "@/util/lazy"
|
||||||
import { Filesystem } from "../util/filesystem"
|
|
||||||
|
|
||||||
// Try to import bundled snapshot (generated at build time)
|
// Try to import bundled snapshot (generated at build time)
|
||||||
// Falls back to undefined in dev mode when snapshot doesn't exist
|
// Falls back to undefined in dev mode when snapshot doesn't exist
|
||||||
@@ -86,7 +85,8 @@ export namespace ModelsDev {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Data = lazy(async () => {
|
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
|
if (result) return result
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const snapshot = await import("./models-snapshot")
|
const snapshot = await import("./models-snapshot")
|
||||||
@@ -104,6 +104,7 @@ export namespace ModelsDev {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function refresh() {
|
export async function refresh() {
|
||||||
|
const file = Bun.file(filepath)
|
||||||
const result = await fetch(`${url()}/api.json`, {
|
const result = await fetch(`${url()}/api.json`, {
|
||||||
headers: {
|
headers: {
|
||||||
"User-Agent": Installation.USER_AGENT,
|
"User-Agent": Installation.USER_AGENT,
|
||||||
@@ -115,7 +116,7 @@ export namespace ModelsDev {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
if (result && result.ok) {
|
if (result && result.ok) {
|
||||||
await Filesystem.write(filepath, await result.text())
|
await Bun.write(file, await result.text())
|
||||||
ModelsDev.Data.reset()
|
ModelsDev.Data.reset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import { Flag } from "../flag/flag"
|
|||||||
import { iife } from "@/util/iife"
|
import { iife } from "@/util/iife"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Filesystem } from "../util/filesystem"
|
|
||||||
|
|
||||||
// Direct imports for bundled providers
|
// Direct imports for bundled providers
|
||||||
import { createAmazonBedrock, type AmazonBedrockProviderSettings } from "@ai-sdk/amazon-bedrock"
|
import { createAmazonBedrock, type AmazonBedrockProviderSettings } from "@ai-sdk/amazon-bedrock"
|
||||||
@@ -1292,9 +1291,8 @@ export namespace Provider {
|
|||||||
if (cfg.model) return parseModel(cfg.model)
|
if (cfg.model) return parseModel(cfg.model)
|
||||||
|
|
||||||
const providers = await list()
|
const providers = await list()
|
||||||
const recent = (await Filesystem.readJson<{ recent?: { providerID: string; modelID: string }[] }>(
|
const recent = (await Bun.file(path.join(Global.Path.state, "model.json"))
|
||||||
path.join(Global.Path.state, "model.json"),
|
.json()
|
||||||
)
|
|
||||||
.then((x) => (Array.isArray(x.recent) ? x.recent : []))
|
.then((x) => (Array.isArray(x.recent) ? x.recent : []))
|
||||||
.catch(() => [])) as { providerID: string; modelID: string }[]
|
.catch(() => [])) as { providerID: string; modelID: string }[]
|
||||||
for (const entry of recent) {
|
for (const entry of recent) {
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export namespace InstructionPrompt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const file of globalFiles()) {
|
for (const file of globalFiles()) {
|
||||||
if (await Filesystem.exists(file)) {
|
if (await Bun.file(file).exists()) {
|
||||||
paths.add(path.resolve(file))
|
paths.add(path.resolve(file))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -120,7 +120,9 @@ export namespace InstructionPrompt {
|
|||||||
const paths = await systemPaths()
|
const paths = await systemPaths()
|
||||||
|
|
||||||
const files = Array.from(paths).map(async (p) => {
|
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 : ""
|
return content ? "Instructions from: " + p + "\n" + content : ""
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -162,7 +164,7 @@ export namespace InstructionPrompt {
|
|||||||
export async function find(dir: string) {
|
export async function find(dir: string) {
|
||||||
for (const file of FILES) {
|
for (const file of FILES) {
|
||||||
const filepath = path.resolve(path.join(dir, file))
|
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)) {
|
if (found && found !== target && !system.has(found) && !already.has(found) && !isClaimed(messageID, found)) {
|
||||||
claim(messageID, found)
|
claim(messageID, found)
|
||||||
const content = await Filesystem.readText(found).catch(() => undefined)
|
const content = await Bun.file(found)
|
||||||
|
.text()
|
||||||
|
.catch(() => undefined)
|
||||||
if (content) {
|
if (content) {
|
||||||
results.push({ filepath: found, content: "Instructions from: " + found + "\n" + content })
|
results.push({ filepath: found, content: "Instructions from: " + found + "\n" + content })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import path from "path"
|
|||||||
import os from "os"
|
import os from "os"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Filesystem } from "../util/filesystem"
|
|
||||||
import { Identifier } from "../id/id"
|
import { Identifier } from "../id/id"
|
||||||
import { MessageV2 } from "./message-v2"
|
import { MessageV2 } from "./message-v2"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
@@ -1083,9 +1082,11 @@ export namespace SessionPrompt {
|
|||||||
// have to normalize, symbol search returns absolute paths
|
// have to normalize, symbol search returns absolute paths
|
||||||
// Decode the pathname since URL constructor doesn't automatically decode it
|
// Decode the pathname since URL constructor doesn't automatically decode it
|
||||||
const filepath = fileURLToPath(part.url)
|
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"
|
part.mime = "application/x-directory"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1232,13 +1233,14 @@ export namespace SessionPrompt {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const file = Bun.file(filepath)
|
||||||
FileTime.read(input.sessionID, filepath)
|
FileTime.read(input.sessionID, filepath)
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
messageID: info.id,
|
messageID: info.id,
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
type: "text",
|
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,
|
synthetic: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1246,7 +1248,7 @@ export namespace SessionPrompt {
|
|||||||
messageID: info.id,
|
messageID: info.id,
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
type: "file",
|
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,
|
mime: part.mime,
|
||||||
filename: part.filename!,
|
filename: part.filename!,
|
||||||
source: part.source,
|
source: part.source,
|
||||||
@@ -1352,7 +1354,7 @@ export namespace SessionPrompt {
|
|||||||
// Switching from plan mode to build mode
|
// Switching from plan mode to build mode
|
||||||
if (input.agent.name !== "plan" && assistantMessage?.info.agent === "plan") {
|
if (input.agent.name !== "plan" && assistantMessage?.info.agent === "plan") {
|
||||||
const plan = Session.plan(input.session)
|
const plan = Session.plan(input.session)
|
||||||
const exists = await Filesystem.exists(plan)
|
const exists = await Bun.file(plan).exists()
|
||||||
if (exists) {
|
if (exists) {
|
||||||
const part = await Session.updatePart({
|
const part = await Session.updatePart({
|
||||||
id: Identifier.ascending("part"),
|
id: Identifier.ascending("part"),
|
||||||
@@ -1371,7 +1373,7 @@ export namespace SessionPrompt {
|
|||||||
// Entering plan mode
|
// Entering plan mode
|
||||||
if (input.agent.name === "plan" && assistantMessage?.info.agent !== "plan") {
|
if (input.agent.name === "plan" && assistantMessage?.info.agent !== "plan") {
|
||||||
const plan = Session.plan(input.session)
|
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 })
|
if (!exists) await fs.mkdir(path.dirname(plan), { recursive: true })
|
||||||
const part = await Session.updatePart({
|
const part = await Session.updatePart({
|
||||||
id: Identifier.ascending("part"),
|
id: Identifier.ascending("part"),
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Flag } from "@/flag/flag"
|
import { Flag } from "@/flag/flag"
|
||||||
import { lazy } from "@/util/lazy"
|
import { lazy } from "@/util/lazy"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { spawn, type ChildProcess } from "child_process"
|
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
|
// git.exe is typically at: C:\Program Files\Git\cmd\git.exe
|
||||||
// bash.exe is at: C:\Program Files\Git\bin\bash.exe
|
// bash.exe is at: C:\Program Files\Git\bin\bash.exe
|
||||||
const bash = path.join(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"
|
return process.env.COMSPEC || "cmd.exe"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import path from "path"
|
|||||||
import { mkdir } from "fs/promises"
|
import { mkdir } from "fs/promises"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import { Filesystem } from "../util/filesystem"
|
|
||||||
|
|
||||||
export namespace Discovery {
|
export namespace Discovery {
|
||||||
const log = Log.create({ service: "skill-discovery" })
|
const log = Log.create({ service: "skill-discovery" })
|
||||||
@@ -20,14 +19,14 @@ export namespace Discovery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function get(url: string, dest: string): Promise<boolean> {
|
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)
|
return fetch(url)
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
log.error("failed to download", { url, status: response.status })
|
log.error("failed to download", { url, status: response.status })
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (response.body) await Filesystem.writeStream(dest, response.body)
|
await Bun.write(dest, await response.text())
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@@ -89,7 +88,7 @@ export namespace Discovery {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const md = path.join(root, "SKILL.md")
|
const md = path.join(root, "SKILL.md")
|
||||||
if (await Filesystem.exists(md)) result.push(root)
|
if (await Bun.file(md).exists()) result.push(root)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { Log } from "../util/log"
|
|||||||
import { NamedError } from "@opencode-ai/util/error"
|
import { NamedError } from "@opencode-ai/util/error"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { readFileSync, readdirSync, existsSync } from "fs"
|
import { readFileSync, readdirSync } from "fs"
|
||||||
import * as schema from "./schema"
|
import * as schema from "./schema"
|
||||||
|
|
||||||
declare const OPENCODE_MIGRATIONS: { sql: string; timestamp: number }[] | undefined
|
declare const OPENCODE_MIGRATIONS: { sql: string; timestamp: number }[] | undefined
|
||||||
@@ -54,7 +54,7 @@ export namespace Database {
|
|||||||
const sql = dirs
|
const sql = dirs
|
||||||
.map((name) => {
|
.map((name) => {
|
||||||
const file = path.join(dir, name, "migration.sql")
|
const file = path.join(dir, name, "migration.sql")
|
||||||
if (!existsSync(file)) return
|
if (!Bun.file(file).size) return
|
||||||
return {
|
return {
|
||||||
sql: readFileSync(file, "utf-8"),
|
sql: readFileSync(file, "utf-8"),
|
||||||
timestamp: time(name),
|
timestamp: time(name),
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } fro
|
|||||||
import { SessionShareTable } from "../share/share.sql"
|
import { SessionShareTable } from "../share/share.sql"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { existsSync } from "fs"
|
import { existsSync } from "fs"
|
||||||
import { Filesystem } from "../util/filesystem"
|
|
||||||
|
|
||||||
export namespace JsonMigration {
|
export namespace JsonMigration {
|
||||||
const log = Log.create({ service: "json-migration" })
|
const log = Log.create({ service: "json-migration" })
|
||||||
@@ -83,7 +82,7 @@ export namespace JsonMigration {
|
|||||||
const count = end - start
|
const count = end - start
|
||||||
const tasks = new Array(count)
|
const tasks = new Array(count)
|
||||||
for (let i = 0; i < count; i++) {
|
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 results = await Promise.allSettled(tasks)
|
||||||
const items = new Array(count)
|
const items = new Array(count)
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export namespace Storage {
|
|||||||
cwd: path.join(project, projectDir),
|
cwd: path.join(project, projectDir),
|
||||||
absolute: true,
|
absolute: true,
|
||||||
})) {
|
})) {
|
||||||
const json = await Filesystem.readJson<any>(msgFile)
|
const json = await Bun.file(msgFile).json()
|
||||||
worktree = json.path?.root
|
worktree = json.path?.root
|
||||||
if (worktree) break
|
if (worktree) break
|
||||||
}
|
}
|
||||||
@@ -60,15 +60,18 @@ export namespace Storage {
|
|||||||
if (!id) continue
|
if (!id) continue
|
||||||
projectID = id
|
projectID = id
|
||||||
|
|
||||||
await Filesystem.writeJson(path.join(dir, "project", projectID + ".json"), {
|
await Bun.write(
|
||||||
id,
|
path.join(dir, "project", projectID + ".json"),
|
||||||
vcs: "git",
|
JSON.stringify({
|
||||||
worktree,
|
id,
|
||||||
time: {
|
vcs: "git",
|
||||||
created: Date.now(),
|
worktree,
|
||||||
initialized: Date.now(),
|
time: {
|
||||||
},
|
created: Date.now(),
|
||||||
})
|
initialized: Date.now(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
log.info(`migrating sessions for project ${projectID}`)
|
log.info(`migrating sessions for project ${projectID}`)
|
||||||
for await (const sessionFile of new Bun.Glob("storage/session/info/*.json").scan({
|
for await (const sessionFile of new Bun.Glob("storage/session/info/*.json").scan({
|
||||||
@@ -80,8 +83,8 @@ export namespace Storage {
|
|||||||
sessionFile,
|
sessionFile,
|
||||||
dest,
|
dest,
|
||||||
})
|
})
|
||||||
const session = await Filesystem.readJson<any>(sessionFile)
|
const session = await Bun.file(sessionFile).json()
|
||||||
await Filesystem.writeJson(dest, session)
|
await Bun.write(dest, JSON.stringify(session))
|
||||||
log.info(`migrating messages for session ${session.id}`)
|
log.info(`migrating messages for session ${session.id}`)
|
||||||
for await (const msgFile of new Bun.Glob(`storage/session/message/${session.id}/*.json`).scan({
|
for await (const msgFile of new Bun.Glob(`storage/session/message/${session.id}/*.json`).scan({
|
||||||
cwd: fullProjectDir,
|
cwd: fullProjectDir,
|
||||||
@@ -92,8 +95,8 @@ export namespace Storage {
|
|||||||
msgFile,
|
msgFile,
|
||||||
dest,
|
dest,
|
||||||
})
|
})
|
||||||
const message = await Filesystem.readJson<any>(msgFile)
|
const message = await Bun.file(msgFile).json()
|
||||||
await Filesystem.writeJson(dest, message)
|
await Bun.write(dest, JSON.stringify(message))
|
||||||
|
|
||||||
log.info(`migrating parts for message ${message.id}`)
|
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(
|
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,
|
cwd: dir,
|
||||||
absolute: true,
|
absolute: true,
|
||||||
})) {
|
})) {
|
||||||
const session = await Filesystem.readJson<any>(item)
|
const session = await Bun.file(item).json()
|
||||||
if (!session.projectID) continue
|
if (!session.projectID) continue
|
||||||
if (!session.summary?.diffs) continue
|
if (!session.summary?.diffs) continue
|
||||||
const { diffs } = session.summary
|
const { diffs } = session.summary
|
||||||
await Filesystem.write(path.join(dir, "session_diff", session.id + ".json"), JSON.stringify(diffs))
|
await Bun.file(path.join(dir, "session_diff", session.id + ".json")).write(JSON.stringify(diffs))
|
||||||
await Filesystem.writeJson(path.join(dir, "session", session.projectID, session.id + ".json"), {
|
await Bun.file(path.join(dir, "session", session.projectID, session.id + ".json")).write(
|
||||||
...session,
|
JSON.stringify({
|
||||||
summary: {
|
...session,
|
||||||
additions: diffs.reduce((sum: any, x: any) => sum + x.additions, 0),
|
summary: {
|
||||||
deletions: diffs.reduce((sum: any, x: any) => sum + x.deletions, 0),
|
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 state = lazy(async () => {
|
||||||
const dir = path.join(Global.Path.data, "storage")
|
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))
|
.then((x) => parseInt(x))
|
||||||
.catch(() => 0)
|
.catch(() => 0)
|
||||||
for (let index = migration; index < MIGRATIONS.length; index++) {
|
for (let index = migration; index < MIGRATIONS.length; index++) {
|
||||||
log.info("running migration", { index })
|
log.info("running migration", { index })
|
||||||
const migration = MIGRATIONS[index]
|
const migration = MIGRATIONS[index]
|
||||||
await migration(dir).catch(() => log.error("failed to run migration", { 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 {
|
return {
|
||||||
dir,
|
dir,
|
||||||
@@ -165,7 +171,7 @@ export namespace Storage {
|
|||||||
const target = path.join(dir, ...key) + ".json"
|
const target = path.join(dir, ...key) + ".json"
|
||||||
return withErrorHandling(async () => {
|
return withErrorHandling(async () => {
|
||||||
using _ = await Lock.read(target)
|
using _ = await Lock.read(target)
|
||||||
const result = await Filesystem.readJson<T>(target)
|
const result = await Bun.file(target).json()
|
||||||
return result as T
|
return result as T
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -175,10 +181,10 @@ export namespace Storage {
|
|||||||
const target = path.join(dir, ...key) + ".json"
|
const target = path.join(dir, ...key) + ".json"
|
||||||
return withErrorHandling(async () => {
|
return withErrorHandling(async () => {
|
||||||
using _ = await Lock.write(target)
|
using _ = await Lock.write(target)
|
||||||
const content = await Filesystem.readJson<T>(target)
|
const content = await Bun.file(target).json()
|
||||||
fn(content as T)
|
fn(content)
|
||||||
await Filesystem.writeJson(target, content)
|
await Bun.write(target, JSON.stringify(content, null, 2))
|
||||||
return content
|
return content as T
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +193,7 @@ export namespace Storage {
|
|||||||
const target = path.join(dir, ...key) + ".json"
|
const target = path.join(dir, ...key) + ".json"
|
||||||
return withErrorHandling(async () => {
|
return withErrorHandling(async () => {
|
||||||
using _ = await Lock.write(target)
|
using _ = await Lock.write(target)
|
||||||
await Filesystem.writeJson(target, content)
|
await Bun.write(target, JSON.stringify(content, null, 2))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export const EditTool = Tool.define("edit", {
|
|||||||
let contentNew = ""
|
let contentNew = ""
|
||||||
await FileTime.withLock(filePath, async () => {
|
await FileTime.withLock(filePath, async () => {
|
||||||
if (params.oldString === "") {
|
if (params.oldString === "") {
|
||||||
const existed = await Filesystem.exists(filePath)
|
const existed = await Bun.file(filePath).exists()
|
||||||
contentNew = params.newString
|
contentNew = params.newString
|
||||||
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
|
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
|
||||||
await ctx.ask({
|
await ctx.ask({
|
||||||
@@ -61,7 +61,7 @@ export const EditTool = Tool.define("edit", {
|
|||||||
diff,
|
diff,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
await Filesystem.write(filePath, params.newString)
|
await Bun.write(filePath, params.newString)
|
||||||
await Bus.publish(File.Event.Edited, {
|
await Bus.publish(File.Event.Edited, {
|
||||||
file: filePath,
|
file: filePath,
|
||||||
})
|
})
|
||||||
@@ -73,11 +73,12 @@ export const EditTool = Tool.define("edit", {
|
|||||||
return
|
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) throw new Error(`File ${filePath} not found`)
|
||||||
if (stats.isDirectory()) throw new Error(`Path is a directory, not a file: ${filePath}`)
|
if (stats.isDirectory()) throw new Error(`Path is a directory, not a file: ${filePath}`)
|
||||||
await FileTime.assert(ctx.sessionID, 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)
|
contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll)
|
||||||
|
|
||||||
diff = trimDiff(
|
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, {
|
await Bus.publish(File.Event.Edited, {
|
||||||
file: filePath,
|
file: filePath,
|
||||||
})
|
})
|
||||||
@@ -101,7 +102,7 @@ export const EditTool = Tool.define("edit", {
|
|||||||
file: filePath,
|
file: filePath,
|
||||||
event: "change",
|
event: "change",
|
||||||
})
|
})
|
||||||
contentNew = await Filesystem.readText(filePath)
|
contentNew = await file.text()
|
||||||
diff = trimDiff(
|
diff = trimDiff(
|
||||||
createTwoFilesPatch(filePath, filePath, normalizeLineEndings(contentOld), normalizeLineEndings(contentNew)),
|
createTwoFilesPatch(filePath, filePath, normalizeLineEndings(contentOld), normalizeLineEndings(contentNew)),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Tool } from "./tool"
|
import { Tool } from "./tool"
|
||||||
import { Filesystem } from "../util/filesystem"
|
|
||||||
import DESCRIPTION from "./glob.txt"
|
import DESCRIPTION from "./glob.txt"
|
||||||
import { Ripgrep } from "../file/ripgrep"
|
import { Ripgrep } from "../file/ripgrep"
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
@@ -46,7 +45,10 @@ export const GlobTool = Tool.define("glob", {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
const full = path.resolve(search, file)
|
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({
|
files.push({
|
||||||
path: full,
|
path: full,
|
||||||
mtime: stats,
|
mtime: stats,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Tool } from "./tool"
|
import { Tool } from "./tool"
|
||||||
import { Filesystem } from "../util/filesystem"
|
|
||||||
import { Ripgrep } from "../file/ripgrep"
|
import { Ripgrep } from "../file/ripgrep"
|
||||||
|
|
||||||
import DESCRIPTION from "./grep.txt"
|
import DESCRIPTION from "./grep.txt"
|
||||||
@@ -84,7 +83,8 @@ export const GrepTool = Tool.define("grep", {
|
|||||||
const lineNum = parseInt(lineNumStr, 10)
|
const lineNum = parseInt(lineNumStr, 10)
|
||||||
const lineText = lineTextParts.join("|")
|
const lineText = lineTextParts.join("|")
|
||||||
|
|
||||||
const stats = Filesystem.stat(filePath)
|
const file = Bun.file(filePath)
|
||||||
|
const stats = await file.stat().catch(() => null)
|
||||||
if (!stats) continue
|
if (!stats) continue
|
||||||
|
|
||||||
matches.push({
|
matches.push({
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import DESCRIPTION from "./lsp.txt"
|
|||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
import { pathToFileURL } from "url"
|
import { pathToFileURL } from "url"
|
||||||
import { assertExternalDirectory } from "./external-directory"
|
import { assertExternalDirectory } from "./external-directory"
|
||||||
import { Filesystem } from "../util/filesystem"
|
|
||||||
|
|
||||||
const operations = [
|
const operations = [
|
||||||
"goToDefinition",
|
"goToDefinition",
|
||||||
@@ -48,7 +47,7 @@ export const LspTool = Tool.define("lsp", {
|
|||||||
const relPath = path.relative(Instance.worktree, file)
|
const relPath = path.relative(Instance.worktree, file)
|
||||||
const title = `${args.operation} ${relPath}:${args.line}:${args.character}`
|
const title = `${args.operation} ${relPath}:${args.line}:${args.character}`
|
||||||
|
|
||||||
const exists = await Filesystem.exists(file)
|
const exists = await Bun.file(file).exists()
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
throw new Error(`File not found: ${file}`)
|
throw new Error(`File not found: ${file}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import DESCRIPTION from "./read.txt"
|
|||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
import { assertExternalDirectory } from "./external-directory"
|
import { assertExternalDirectory } from "./external-directory"
|
||||||
import { InstructionPrompt } from "../session/instruction"
|
import { InstructionPrompt } from "../session/instruction"
|
||||||
import { Filesystem } from "../util/filesystem"
|
|
||||||
|
|
||||||
const DEFAULT_READ_LIMIT = 2000
|
const DEFAULT_READ_LIMIT = 2000
|
||||||
const MAX_LINE_LENGTH = 2000
|
const MAX_LINE_LENGTH = 2000
|
||||||
@@ -35,7 +34,8 @@ export const ReadTool = Tool.define("read", {
|
|||||||
}
|
}
|
||||||
const title = path.relative(Instance.worktree, filepath)
|
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, {
|
await assertExternalDirectory(ctx, filepath, {
|
||||||
bypass: Boolean(ctx.extra?.["bypassCwdCheck"]),
|
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)
|
const instructions = await InstructionPrompt.resolve(ctx.messages, filepath, ctx.messageID)
|
||||||
|
|
||||||
// Exclude SVG (XML-based) and vnd.fastbidsheet (.fbs extension, commonly FlatBuffers schema files)
|
// Exclude SVG (XML-based) and vnd.fastbidsheet (.fbs extension, commonly FlatBuffers schema files)
|
||||||
const mime = Filesystem.mimeType(filepath)
|
const isImage =
|
||||||
const isImage = mime.startsWith("image/") && mime !== "image/svg+xml" && mime !== "image/vnd.fastbidsheet"
|
file.type.startsWith("image/") && file.type !== "image/svg+xml" && file.type !== "image/vnd.fastbidsheet"
|
||||||
const isPdf = mime === "application/pdf"
|
const isPdf = file.type === "application/pdf"
|
||||||
if (isImage || isPdf) {
|
if (isImage || isPdf) {
|
||||||
|
const mime = file.type
|
||||||
const msg = `${isImage ? "Image" : "PDF"} read successfully`
|
const msg = `${isImage ? "Image" : "PDF"} read successfully`
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
@@ -135,13 +136,13 @@ export const ReadTool = Tool.define("read", {
|
|||||||
{
|
{
|
||||||
type: "file",
|
type: "file",
|
||||||
mime,
|
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}`)
|
if (isBinary) throw new Error(`Cannot read binary file: ${filepath}`)
|
||||||
|
|
||||||
const stream = createReadStream(filepath, { encoding: "utf8" })
|
const stream = createReadStream(filepath, { encoding: "utf8" })
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { Identifier } from "../id/id"
|
|||||||
import { PermissionNext } from "../permission/next"
|
import { PermissionNext } from "../permission/next"
|
||||||
import type { Agent } from "../agent/agent"
|
import type { Agent } from "../agent/agent"
|
||||||
import { Scheduler } from "../scheduler"
|
import { Scheduler } from "../scheduler"
|
||||||
import { Filesystem } from "../util/filesystem"
|
|
||||||
|
|
||||||
export namespace Truncate {
|
export namespace Truncate {
|
||||||
export const MAX_LINES = 2000
|
export const MAX_LINES = 2000
|
||||||
@@ -92,7 +91,7 @@ export namespace Truncate {
|
|||||||
|
|
||||||
const id = Identifier.ascending("tool")
|
const id = Identifier.ascending("tool")
|
||||||
const filepath = path.join(DIR, id)
|
const filepath = path.join(DIR, id)
|
||||||
await Filesystem.write(filepath, text)
|
await Bun.write(Bun.file(filepath), text)
|
||||||
|
|
||||||
const hint = hasTaskTool(agent)
|
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.`
|
? `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.`
|
||||||
|
|||||||
@@ -26,8 +26,9 @@ export const WriteTool = Tool.define("write", {
|
|||||||
const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath)
|
const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath)
|
||||||
await assertExternalDirectory(ctx, filepath)
|
await assertExternalDirectory(ctx, filepath)
|
||||||
|
|
||||||
const exists = await Filesystem.exists(filepath)
|
const file = Bun.file(filepath)
|
||||||
const contentOld = exists ? await Filesystem.readText(filepath) : ""
|
const exists = await file.exists()
|
||||||
|
const contentOld = exists ? await file.text() : ""
|
||||||
if (exists) await FileTime.assert(ctx.sessionID, filepath)
|
if (exists) await FileTime.assert(ctx.sessionID, filepath)
|
||||||
|
|
||||||
const diff = trimDiff(createTwoFilesPatch(filepath, filepath, contentOld, params.content))
|
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, {
|
await Bus.publish(File.Event.Edited, {
|
||||||
file: filepath,
|
file: filepath,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { chmod, mkdir, readFile, writeFile } from "fs/promises"
|
import { mkdir, readFile, writeFile } from "fs/promises"
|
||||||
import { createWriteStream, existsSync, statSync } from "fs"
|
import { existsSync, statSync } from "fs"
|
||||||
import { lookup } from "mime-types"
|
import { lookup } from "mime-types"
|
||||||
import { realpathSync } from "fs"
|
import { realpathSync } from "fs"
|
||||||
import { dirname, join, relative } from "path"
|
import { dirname, join, relative } from "path"
|
||||||
import { Readable } from "stream"
|
|
||||||
import { pipeline } from "stream/promises"
|
|
||||||
|
|
||||||
export namespace Filesystem {
|
export namespace Filesystem {
|
||||||
// Fast sync version for metadata checks
|
// Fast sync version for metadata checks
|
||||||
@@ -70,25 +68,6 @@ export namespace Filesystem {
|
|||||||
return write(p, JSON.stringify(data, null, 2), mode)
|
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 {
|
export function mimeType(p: string): string {
|
||||||
return lookup(p) || "application/octet-stream"
|
return lookup(p) || "application/octet-stream"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import { createWriteStream } from "fs"
|
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
|
|
||||||
@@ -64,15 +63,13 @@ export namespace Log {
|
|||||||
Global.Path.log,
|
Global.Path.log,
|
||||||
options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log",
|
options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log",
|
||||||
)
|
)
|
||||||
|
const logfile = Bun.file(logpath)
|
||||||
await fs.truncate(logpath).catch(() => {})
|
await fs.truncate(logpath).catch(() => {})
|
||||||
const stream = createWriteStream(logpath, { flags: "a" })
|
const writer = logfile.writer()
|
||||||
write = async (msg: any) => {
|
write = async (msg: any) => {
|
||||||
return new Promise((resolve, reject) => {
|
const num = writer.write(msg)
|
||||||
stream.write(msg, (err) => {
|
writer.flush()
|
||||||
if (err) reject(err)
|
return num
|
||||||
else resolve(msg.length)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -285,125 +285,4 @@ describe("filesystem", () => {
|
|||||||
expect(Filesystem.mimeType("Makefile")).toBe("application/octet-stream")
|
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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user