refactor(core): consolidate filesystem services (#30447)

This commit is contained in:
Dax
2026-06-02 16:09:26 -04:00
committed by GitHub
parent b93963e462
commit 604a5f781f
153 changed files with 2553 additions and 4312 deletions
+7 -7
View File
@@ -2,16 +2,16 @@ import * as path from "path"
import { Effect, Schema } from "effect"
import * as Tool from "./tool"
import { EventV2Bridge } from "@/event-v2-bridge"
import { FileWatcher } from "../file/watcher"
import { Watcher } from "@opencode-ai/core/filesystem/watcher"
import { InstanceState } from "@/effect/instance-state"
import { Patch } from "../patch"
import { createTwoFilesPatch, diffLines } from "diff"
import { assertExternalDirectoryEffect } from "./external-directory"
import { trimDiff } from "./edit"
import { LSP } from "@/lsp/lsp"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { FSUtil } from "@opencode-ai/core/fs-util"
import DESCRIPTION from "./apply_patch.txt"
import { File } from "../file"
import { FileSystem } from "@opencode-ai/core/filesystem"
import { Format } from "../format"
import * as Bom from "@/util/bom"
@@ -23,7 +23,7 @@ export const ApplyPatchTool = Tool.define(
"apply_patch",
Effect.gen(function* () {
const lsp = yield* LSP.Service
const afs = yield* AppFileSystem.Service
const afs = yield* FSUtil.Service
const format = yield* Format.Service
const events = yield* EventV2Bridge.Service
@@ -253,13 +253,13 @@ export const ApplyPatchTool = Tool.define(
if (yield* format.file(edited)) {
yield* Bom.syncFile(afs, edited, change.bom)
}
yield* events.publish(File.Event.Edited, { file: edited })
yield* events.publish(FileSystem.Event.Edited, { file: edited })
}
}
// Publish file change events
for (const update of updates) {
yield* events.publish(FileWatcher.Event.Updated, update)
yield* events.publish(Watcher.Event.Updated, update)
}
// Notify LSP of file changes and collect diagnostics
@@ -286,7 +286,7 @@ export const ApplyPatchTool = Tool.define(
for (const change of fileChanges) {
if (change.type === "delete") continue
const target = change.movePath ?? change.filePath
const block = LSP.Diagnostic.report(target, diagnostics[AppFileSystem.normalizePath(target)] ?? [])
const block = LSP.Diagnostic.report(target, diagnostics[FSUtil.normalizePath(target)] ?? [])
if (!block) continue
const rel = path.relative(instance.worktree, target).replaceAll("\\", "/")
output += `\n\nLSP errors detected in ${rel}, please fix:\n${block}`
+10 -10
View File
@@ -9,14 +9,14 @@ import * as Tool from "./tool"
import { LSP } from "@/lsp/lsp"
import { createTwoFilesPatch, diffLines } from "diff"
import DESCRIPTION from "./edit.txt"
import { File } from "../file"
import { FileWatcher } from "../file/watcher"
import { FileSystem } from "@opencode-ai/core/filesystem"
import { Watcher } from "@opencode-ai/core/filesystem/watcher"
import { EventV2Bridge } from "@/event-v2-bridge"
import { Format } from "../format"
import { InstanceState } from "@/effect/instance-state"
import { Snapshot } from "@/snapshot"
import { assertExternalDirectoryEffect } from "./external-directory"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { FSUtil } from "@opencode-ai/core/fs-util"
import * as Bom from "@/util/bom"
function normalizeLineEndings(text: string): string {
@@ -35,7 +35,7 @@ function convertToLineEnding(text: string, ending: "\n" | "\r\n"): string {
const locks = new Map<string, Semaphore.Semaphore>()
function lock(filePath: string) {
const resolvedFilePath = AppFileSystem.resolve(filePath)
const resolvedFilePath = FSUtil.resolve(filePath)
const hit = locks.get(resolvedFilePath)
if (hit) return hit
@@ -59,7 +59,7 @@ export const EditTool = Tool.define(
"edit",
Effect.gen(function* () {
const lsp = yield* LSP.Service
const afs = yield* AppFileSystem.Service
const afs = yield* FSUtil.Service
const format = yield* Format.Service
const events = yield* EventV2Bridge.Service
@@ -108,8 +108,8 @@ export const EditTool = Tool.define(
if (yield* format.file(filePath)) {
contentNew = yield* Bom.syncFile(afs, filePath, desiredBom)
}
yield* events.publish(File.Event.Edited, { file: filePath })
yield* events.publish(FileWatcher.Event.Updated, {
yield* events.publish(FileSystem.Event.Edited, { file: filePath })
yield* events.publish(Watcher.Event.Updated, {
file: filePath,
event: existed ? "change" : "add",
})
@@ -152,8 +152,8 @@ export const EditTool = Tool.define(
if (yield* format.file(filePath)) {
contentNew = yield* Bom.syncFile(afs, filePath, desiredBom)
}
yield* events.publish(File.Event.Edited, { file: filePath })
yield* events.publish(FileWatcher.Event.Updated, {
yield* events.publish(FileSystem.Event.Edited, { file: filePath })
yield* events.publish(Watcher.Event.Updated, {
file: filePath,
event: "change",
})
@@ -192,7 +192,7 @@ export const EditTool = Tool.define(
let output = "Edit applied successfully."
yield* lsp.touchFile(filePath, "document")
const diagnostics = yield* lsp.diagnostics()
const normalizedFilePath = AppFileSystem.normalizePath(filePath)
const normalizedFilePath = FSUtil.normalizePath(filePath)
const block = LSP.Diagnostic.report(filePath, diagnostics[normalizedFilePath] ?? [])
if (block) output += `\n\nLSP errors detected in this file, please fix:\n${block}`
@@ -4,7 +4,7 @@ import * as EffectLogger from "@opencode-ai/core/effect/logger"
import { InstanceState } from "@/effect/instance-state"
import type * as Tool from "./tool"
import { containsPath } from "../project/instance-context"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { FSUtil } from "@opencode-ai/core/fs-util"
type Kind = "file" | "directory"
@@ -23,14 +23,14 @@ export const assertExternalDirectoryEffect = Effect.fn("Tool.assertExternalDirec
if (options?.bypass) return
const ins = yield* InstanceState.context
const full = process.platform === "win32" ? AppFileSystem.normalizePath(target) : target
const full = process.platform === "win32" ? FSUtil.normalizePath(target) : target
if (containsPath(full, ins)) return
const kind = options?.kind ?? "file"
const dir = kind === "directory" ? full : path.dirname(full)
const glob =
process.platform === "win32"
? AppFileSystem.normalizePathPattern(path.join(dir, "*"))
? FSUtil.normalizePathPattern(path.join(dir, "*"))
: path.join(dir, "*").replaceAll("\\", "/")
yield* ctx.ask({
+3 -3
View File
@@ -2,8 +2,8 @@ import path from "path"
import { Effect, Option, Schema } from "effect"
import * as Stream from "effect/Stream"
import { InstanceState } from "@/effect/instance-state"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Ripgrep } from "../file/ripgrep"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { assertExternalDirectoryEffect } from "./external-directory"
import DESCRIPTION from "./glob.txt"
import * as Tool from "./tool"
@@ -20,7 +20,7 @@ export const GlobTool = Tool.define(
"glob",
Effect.gen(function* () {
const rg = yield* Ripgrep.Service
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const reference = yield* Reference.Service
return {
+5 -7
View File
@@ -2,8 +2,8 @@ import path from "path"
import { Schema } from "effect"
import { Effect, Option } from "effect"
import { InstanceState } from "@/effect/instance-state"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Ripgrep } from "../file/ripgrep"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { assertExternalDirectoryEffect } from "./external-directory"
import DESCRIPTION from "./grep.txt"
import * as Tool from "./tool"
@@ -24,7 +24,7 @@ export const Parameters = Schema.Struct({
export const GrepTool = Tool.define(
"grep",
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const rg = yield* Ripgrep.Service
const reference = yield* Reference.Service
@@ -64,7 +64,7 @@ export const GrepTool = Tool.define(
kind: requestedInfo?.type === "Directory" ? "directory" : "file",
})
const search = AppFileSystem.resolve(requested)
const search = FSUtil.resolve(requested)
const info = yield* fs.stat(search).pipe(Effect.catch(() => Effect.succeed(undefined)))
const cwd = info?.type === "Directory" ? search : path.dirname(search)
const file = info?.type === "Directory" ? undefined : [path.relative(cwd, search)]
@@ -79,9 +79,7 @@ export const GrepTool = Tool.define(
if (result.items.length === 0) return empty
const rows = result.items.map((item) => ({
path: AppFileSystem.resolve(
path.isAbsolute(item.path.text) ? item.path.text : path.join(cwd, item.path.text),
),
path: FSUtil.resolve(path.isAbsolute(item.path.text) ? item.path.text : path.join(cwd, item.path.text)),
line: item.line_number,
text: item.lines.text,
}))
+2 -2
View File
@@ -6,7 +6,7 @@ import DESCRIPTION from "./lsp.txt"
import { InstanceState } from "@/effect/instance-state"
import { pathToFileURL } from "url"
import { assertExternalDirectoryEffect } from "./external-directory"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { FSUtil } from "@opencode-ai/core/fs-util"
const operations = [
"goToDefinition",
@@ -38,7 +38,7 @@ export const LspTool = Tool.define(
"lsp",
Effect.gen(function* () {
const lsp = yield* LSP.Service
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
return {
description: DESCRIPTION,
parameters: Parameters,
+4 -4
View File
@@ -2,7 +2,7 @@ import { Effect, Option, Schema, Scope, Stream } from "effect"
import { NonNegativeInt } from "@opencode-ai/core/schema"
import * as path from "path"
import * as Tool from "./tool"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { LSP } from "@/lsp/lsp"
import DESCRIPTION from "./read.txt"
import { InstanceState } from "@/effect/instance-state"
@@ -39,7 +39,7 @@ export const Parameters = Schema.Struct({
export const ReadTool = Tool.define(
"read",
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const instruction = yield* Instruction.Service
const lsp = yield* LSP.Service
const reference = yield* Reference.Service
@@ -208,7 +208,7 @@ export const ReadTool = Tool.define(
filepath = path.resolve(instance.directory, filepath)
}
if (process.platform === "win32") {
filepath = AppFileSystem.normalizePath(filepath)
filepath = FSUtil.normalizePath(filepath)
}
yield* reference.ensure(filepath)
const title = path.relative(instance.worktree, filepath)
@@ -265,7 +265,7 @@ export const ReadTool = Tool.define(
const loaded = yield* instruction.resolve(ctx.messages, filepath, ctx.messageID)
const sample = yield* readSample(filepath, Number(stat.size), SAMPLE_BYTES)
const mime = sniffAttachmentMime(sample, AppFileSystem.mimeType(filepath))
const mime = sniffAttachmentMime(sample, FSUtil.mimeType(filepath))
const isImage = SUPPORTED_IMAGE_MIMES.has(mime)
if (isImage || isPdfAttachment(mime)) {
+4 -4
View File
@@ -34,7 +34,7 @@ import { Effect, Layer, Context } from "effect"
import { FetchHttpClient, HttpClient } from "effect/unstable/http"
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Ripgrep } from "../file/ripgrep"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { Format } from "../format"
import { InstanceState } from "@/effect/instance-state"
import { EffectBridge } from "@/effect/bridge"
@@ -42,7 +42,7 @@ import { Question } from "../question"
import { Todo } from "../session/todo"
import { LSP } from "@/lsp/lsp"
import { Instruction } from "../session/instruction"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { EventV2Bridge } from "@/event-v2-bridge"
import { Agent } from "../agent/agent"
import { Skill } from "../skill"
@@ -96,7 +96,7 @@ export const layer: Layer.Layer<
| Reference.Service
| LSP.Service
| Instruction.Service
| AppFileSystem.Service
| FSUtil.Service
| EventV2Bridge.Service
| HttpClient.HttpClient
| ChildProcessSpawner
@@ -380,7 +380,7 @@ export const defaultLayer = Layer.suspend(() =>
Layer.provide(Reference.defaultLayer),
Layer.provide(LSP.defaultLayer),
Layer.provide(Instruction.defaultLayer),
Layer.provide(AppFileSystem.defaultLayer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(FetchHttpClient.layer),
Layer.provide(Format.defaultLayer),
+6 -6
View File
@@ -9,7 +9,7 @@ import { InstanceState } from "@/effect/instance-state"
import { lazy } from "@/util/lazy"
import { Language, type Node } from "web-tree-sitter"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { fileURLToPath } from "url"
import { Config } from "@/config/config"
import { RuntimeFlags } from "@/effect/runtime-flags"
@@ -266,7 +266,7 @@ const parse = Effect.fn("ShellTool.parse")(function* (command: string, ps: boole
const ask = Effect.fn("ShellTool.ask")(function* (ctx: Tool.Context, scan: Scan) {
if (scan.dirs.size > 0) {
const globs = Array.from(scan.dirs).map((dir) => {
if (process.platform === "win32") return AppFileSystem.normalizePathPattern(path.join(dir, "*"))
if (process.platform === "win32") return FSUtil.normalizePathPattern(path.join(dir, "*"))
return path.join(dir, "*")
})
yield* ctx.ask({
@@ -336,7 +336,7 @@ export const ShellTool = Tool.define(
Effect.gen(function* () {
const config = yield* Config.Service
const spawner = yield* ChildProcessSpawner
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const trunc = yield* Truncate.Service
const plugin = yield* Plugin.Service
const flags = yield* RuntimeFlags.Service
@@ -348,16 +348,16 @@ export const ShellTool = Tool.define(
.pipe(Effect.catch(() => Effect.succeed([] as string[])))
const file = lines[0]?.trim()
if (!file) return
return AppFileSystem.normalizePath(file)
return FSUtil.normalizePath(file)
})
const resolvePath = Effect.fn("ShellTool.resolvePath")(function* (text: string, root: string, shell: string) {
if (process.platform === "win32") {
if (Shell.posix(shell) && text.startsWith("/") && AppFileSystem.windowsPath(text) === text) {
if (Shell.posix(shell) && text.startsWith("/") && FSUtil.windowsPath(text) === text) {
const file = yield* cygpath(shell, text)
if (file) return file
}
return AppFileSystem.normalizePath(path.resolve(root, AppFileSystem.windowsPath(text)))
return FSUtil.normalizePath(path.resolve(root, FSUtil.windowsPath(text)))
}
return path.resolve(root, text)
})
+1 -1
View File
@@ -2,7 +2,7 @@ import path from "path"
import { pathToFileURL } from "url"
import { Effect, Schema } from "effect"
import * as Stream from "effect/Stream"
import { Ripgrep } from "../file/ripgrep"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { Skill } from "../skill"
import * as Tool from "./tool"
import DESCRIPTION from "./skill.txt"
+3 -3
View File
@@ -2,7 +2,7 @@ import { NodePath } from "@effect/platform-node"
import { Cause, Duration, Effect, Layer, Option, Schedule, Context } from "effect"
import path from "path"
import type { Agent } from "../agent/agent"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { evaluate } from "@/permission/evaluate"
import { Config } from "@/config/config"
import { Identifier } from "../id/id"
@@ -50,7 +50,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Tr
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const cleanup = Effect.fn("Truncate.cleanup")(function* () {
const cutoff = Identifier.timestamp(
@@ -155,6 +155,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(NodePath.layer))
export const defaultLayer = layer.pipe(Layer.provide(FSUtil.defaultLayer), Layer.provide(NodePath.layer))
export * as Truncate from "./truncate"
+7 -7
View File
@@ -6,10 +6,10 @@ import { LSP } from "@/lsp/lsp"
import { createTwoFilesPatch } from "diff"
import DESCRIPTION from "./write.txt"
import { EventV2Bridge } from "@/event-v2-bridge"
import { File } from "../file"
import { FileWatcher } from "../file/watcher"
import { FileSystem } from "@opencode-ai/core/filesystem"
import { Watcher } from "@opencode-ai/core/filesystem/watcher"
import { Format } from "../format"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { InstanceState } from "@/effect/instance-state"
import { trimDiff } from "./edit"
import { assertExternalDirectoryEffect } from "./external-directory"
@@ -28,7 +28,7 @@ export const WriteTool = Tool.define(
"write",
Effect.gen(function* () {
const lsp = yield* LSP.Service
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const events = yield* EventV2Bridge.Service
const format = yield* Format.Service
@@ -65,8 +65,8 @@ export const WriteTool = Tool.define(
if (yield* format.file(filepath)) {
yield* Bom.syncFile(fs, filepath, desiredBom)
}
yield* events.publish(File.Event.Edited, { file: filepath })
yield* events.publish(FileWatcher.Event.Updated, {
yield* events.publish(FileSystem.Event.Edited, { file: filepath })
yield* events.publish(Watcher.Event.Updated, {
file: filepath,
event: exists ? "change" : "add",
})
@@ -74,7 +74,7 @@ export const WriteTool = Tool.define(
let output = "Wrote file successfully."
yield* lsp.touchFile(filepath, "document")
const diagnostics = yield* lsp.diagnostics()
const normalizedFilepath = AppFileSystem.normalizePath(filepath)
const normalizedFilepath = FSUtil.normalizePath(filepath)
let projectDiagnosticsCount = 0
for (const [file, issues] of Object.entries(diagnostics)) {
const current = file === normalizedFilepath