chore: delete filetime module (#22999)
This commit is contained in:
@@ -9,7 +9,6 @@ import { Account } from "@/account/account"
|
||||
import { Config } from "@/config"
|
||||
import { Git } from "@/git"
|
||||
import { Ripgrep } from "@/file/ripgrep"
|
||||
import { FileTime } from "@/file/time"
|
||||
import { File } from "@/file"
|
||||
import { FileWatcher } from "@/file/watcher"
|
||||
import { Storage } from "@/storage"
|
||||
@@ -58,7 +57,6 @@ export const AppLayer = Layer.mergeAll(
|
||||
Config.defaultLayer,
|
||||
Git.defaultLayer,
|
||||
Ripgrep.defaultLayer,
|
||||
FileTime.defaultLayer,
|
||||
File.defaultLayer,
|
||||
FileWatcher.defaultLayer,
|
||||
Storage.defaultLayer,
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
import { DateTime, Effect, Layer, Option, Semaphore, Context } from "effect"
|
||||
import { InstanceState } from "@/effect"
|
||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import type { SessionID } from "@/session/schema"
|
||||
import { Log } from "../util"
|
||||
|
||||
const log = Log.create({ service: "file.time" })
|
||||
|
||||
export type Stamp = {
|
||||
readonly read: Date
|
||||
readonly mtime: number | undefined
|
||||
readonly size: number | undefined
|
||||
}
|
||||
|
||||
const session = (reads: Map<SessionID, Map<string, Stamp>>, sessionID: SessionID) => {
|
||||
const value = reads.get(sessionID)
|
||||
if (value) return value
|
||||
|
||||
const next = new Map<string, Stamp>()
|
||||
reads.set(sessionID, next)
|
||||
return next
|
||||
}
|
||||
|
||||
interface State {
|
||||
reads: Map<SessionID, Map<string, Stamp>>
|
||||
locks: Map<string, Semaphore.Semaphore>
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly read: (sessionID: SessionID, file: string) => Effect.Effect<void>
|
||||
readonly get: (sessionID: SessionID, file: string) => Effect.Effect<Date | undefined>
|
||||
readonly assert: (sessionID: SessionID, filepath: string) => Effect.Effect<void>
|
||||
readonly withLock: <T>(filepath: string, fn: () => Effect.Effect<T>) => Effect.Effect<T>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/FileTime") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const fsys = yield* AppFileSystem.Service
|
||||
const disableCheck = yield* Flag.OPENCODE_DISABLE_FILETIME_CHECK
|
||||
|
||||
const stamp = Effect.fnUntraced(function* (file: string) {
|
||||
const info = yield* fsys.stat(file).pipe(Effect.catch(() => Effect.void))
|
||||
return {
|
||||
read: yield* DateTime.nowAsDate,
|
||||
mtime: info ? Option.getOrUndefined(info.mtime)?.getTime() : undefined,
|
||||
size: info ? Number(info.size) : undefined,
|
||||
}
|
||||
})
|
||||
const state = yield* InstanceState.make<State>(
|
||||
Effect.fn("FileTime.state")(() =>
|
||||
Effect.succeed({
|
||||
reads: new Map<SessionID, Map<string, Stamp>>(),
|
||||
locks: new Map<string, Semaphore.Semaphore>(),
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
const getLock = Effect.fn("FileTime.lock")(function* (filepath: string) {
|
||||
filepath = AppFileSystem.normalizePath(filepath)
|
||||
const locks = (yield* InstanceState.get(state)).locks
|
||||
const lock = locks.get(filepath)
|
||||
if (lock) return lock
|
||||
|
||||
const next = Semaphore.makeUnsafe(1)
|
||||
locks.set(filepath, next)
|
||||
return next
|
||||
})
|
||||
|
||||
const read = Effect.fn("FileTime.read")(function* (sessionID: SessionID, file: string) {
|
||||
file = AppFileSystem.normalizePath(file)
|
||||
const reads = (yield* InstanceState.get(state)).reads
|
||||
log.info("read", { sessionID, file })
|
||||
session(reads, sessionID).set(file, yield* stamp(file))
|
||||
})
|
||||
|
||||
const get = Effect.fn("FileTime.get")(function* (sessionID: SessionID, file: string) {
|
||||
file = AppFileSystem.normalizePath(file)
|
||||
const reads = (yield* InstanceState.get(state)).reads
|
||||
return reads.get(sessionID)?.get(file)?.read
|
||||
})
|
||||
|
||||
const assert = Effect.fn("FileTime.assert")(function* (sessionID: SessionID, filepath: string) {
|
||||
if (disableCheck) return
|
||||
filepath = AppFileSystem.normalizePath(filepath)
|
||||
|
||||
const reads = (yield* InstanceState.get(state)).reads
|
||||
const time = reads.get(sessionID)?.get(filepath)
|
||||
if (!time) throw new Error(`You must read file ${filepath} before overwriting it. Use the Read tool first`)
|
||||
|
||||
const next = yield* stamp(filepath)
|
||||
const changed = next.mtime !== time.mtime || next.size !== time.size
|
||||
if (!changed) return
|
||||
|
||||
throw new Error(
|
||||
`File ${filepath} has been modified since it was last read.\nLast modification: ${new Date(next.mtime ?? next.read.getTime()).toISOString()}\nLast read: ${time.read.toISOString()}\n\nPlease read the file again before modifying it.`,
|
||||
)
|
||||
})
|
||||
|
||||
const withLock = Effect.fn("FileTime.withLock")(function* <T>(filepath: string, fn: () => Effect.Effect<T>) {
|
||||
return yield* fn().pipe((yield* getLock(filepath)).withPermits(1))
|
||||
})
|
||||
|
||||
return Service.of({ read, get, assert, withLock })
|
||||
}),
|
||||
).pipe(Layer.orDie)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
|
||||
|
||||
export * as FileTime from "./time"
|
||||
@@ -70,7 +70,6 @@ export const Flag = {
|
||||
OPENCODE_EXPERIMENTAL_OXFMT: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_OXFMT"),
|
||||
OPENCODE_EXPERIMENTAL_LSP_TY: truthy("OPENCODE_EXPERIMENTAL_LSP_TY"),
|
||||
OPENCODE_EXPERIMENTAL_LSP_TOOL: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_LSP_TOOL"),
|
||||
OPENCODE_DISABLE_FILETIME_CHECK: Config.boolean("OPENCODE_DISABLE_FILETIME_CHECK").pipe(Config.withDefault(false)),
|
||||
OPENCODE_EXPERIMENTAL_PLAN_MODE: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_PLAN_MODE"),
|
||||
OPENCODE_EXPERIMENTAL_MARKDOWN: !falsy("OPENCODE_EXPERIMENTAL_MARKDOWN"),
|
||||
OPENCODE_MODELS_URL: process.env["OPENCODE_MODELS_URL"],
|
||||
|
||||
@@ -23,7 +23,6 @@ import MAX_STEPS from "../session/prompt/max-steps.txt"
|
||||
import { ToolRegistry } from "../tool"
|
||||
import { MCP } from "../mcp"
|
||||
import { LSP } from "../lsp"
|
||||
import { FileTime } from "../file/time"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { ulid } from "ulid"
|
||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
@@ -94,7 +93,6 @@ export const layer = Layer.effect(
|
||||
const fsys = yield* AppFileSystem.Service
|
||||
const mcp = yield* MCP.Service
|
||||
const lsp = yield* LSP.Service
|
||||
const filetime = yield* FileTime.Service
|
||||
const registry = yield* ToolRegistry.Service
|
||||
const truncate = yield* Truncate.Service
|
||||
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
||||
@@ -1183,7 +1181,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
]
|
||||
}
|
||||
|
||||
yield* filetime.read(input.sessionID, filepath)
|
||||
return [
|
||||
{
|
||||
messageID: info.id,
|
||||
@@ -1684,7 +1681,6 @@ export const defaultLayer = Layer.suspend(() =>
|
||||
Layer.provide(Permission.defaultLayer),
|
||||
Layer.provide(MCP.defaultLayer),
|
||||
Layer.provide(LSP.defaultLayer),
|
||||
Layer.provide(FileTime.defaultLayer),
|
||||
Layer.provide(ToolRegistry.defaultLayer),
|
||||
Layer.provide(Truncate.defaultLayer),
|
||||
Layer.provide(Provider.defaultLayer),
|
||||
|
||||
@@ -14,7 +14,6 @@ import { File } from "../file"
|
||||
import { FileWatcher } from "../file/watcher"
|
||||
import { Bus } from "../bus"
|
||||
import { Format } from "../format"
|
||||
import { FileTime } from "../file/time"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
import { assertExternalDirectoryEffect } from "./external-directory"
|
||||
@@ -44,7 +43,6 @@ export const EditTool = Tool.define(
|
||||
"edit",
|
||||
Effect.gen(function* () {
|
||||
const lsp = yield* LSP.Service
|
||||
const filetime = yield* FileTime.Service
|
||||
const afs = yield* AppFileSystem.Service
|
||||
const format = yield* Format.Service
|
||||
const bus = yield* Bus.Service
|
||||
@@ -70,52 +68,11 @@ export const EditTool = Tool.define(
|
||||
let diff = ""
|
||||
let contentOld = ""
|
||||
let contentNew = ""
|
||||
yield* filetime.withLock(filePath, () =>
|
||||
Effect.gen(function* () {
|
||||
if (params.oldString === "") {
|
||||
const existed = yield* afs.existsSafe(filePath)
|
||||
contentNew = params.newString
|
||||
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
|
||||
yield* ctx.ask({
|
||||
permission: "edit",
|
||||
patterns: [path.relative(Instance.worktree, filePath)],
|
||||
always: ["*"],
|
||||
metadata: {
|
||||
filepath: filePath,
|
||||
diff,
|
||||
},
|
||||
})
|
||||
yield* afs.writeWithDirs(filePath, params.newString)
|
||||
yield* format.file(filePath)
|
||||
yield* bus.publish(File.Event.Edited, { file: filePath })
|
||||
yield* bus.publish(FileWatcher.Event.Updated, {
|
||||
file: filePath,
|
||||
event: existed ? "change" : "add",
|
||||
})
|
||||
yield* filetime.read(ctx.sessionID, filePath)
|
||||
return
|
||||
}
|
||||
|
||||
const info = yield* afs.stat(filePath).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||
if (!info) throw new Error(`File ${filePath} not found`)
|
||||
if (info.type === "Directory") throw new Error(`Path is a directory, not a file: ${filePath}`)
|
||||
yield* filetime.assert(ctx.sessionID, filePath)
|
||||
contentOld = yield* afs.readFileString(filePath)
|
||||
|
||||
const ending = detectLineEnding(contentOld)
|
||||
const old = convertToLineEnding(normalizeLineEndings(params.oldString), ending)
|
||||
const next = convertToLineEnding(normalizeLineEndings(params.newString), ending)
|
||||
|
||||
contentNew = replace(contentOld, old, next, params.replaceAll)
|
||||
|
||||
diff = trimDiff(
|
||||
createTwoFilesPatch(
|
||||
filePath,
|
||||
filePath,
|
||||
normalizeLineEndings(contentOld),
|
||||
normalizeLineEndings(contentNew),
|
||||
),
|
||||
)
|
||||
yield* Effect.gen(function* () {
|
||||
if (params.oldString === "") {
|
||||
const existed = yield* afs.existsSafe(filePath)
|
||||
contentNew = params.newString
|
||||
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
|
||||
yield* ctx.ask({
|
||||
permission: "edit",
|
||||
patterns: [path.relative(Instance.worktree, filePath)],
|
||||
@@ -125,26 +82,62 @@ export const EditTool = Tool.define(
|
||||
diff,
|
||||
},
|
||||
})
|
||||
|
||||
yield* afs.writeWithDirs(filePath, contentNew)
|
||||
yield* afs.writeWithDirs(filePath, params.newString)
|
||||
yield* format.file(filePath)
|
||||
yield* bus.publish(File.Event.Edited, { file: filePath })
|
||||
yield* bus.publish(FileWatcher.Event.Updated, {
|
||||
file: filePath,
|
||||
event: "change",
|
||||
event: existed ? "change" : "add",
|
||||
})
|
||||
contentNew = yield* afs.readFileString(filePath)
|
||||
diff = trimDiff(
|
||||
createTwoFilesPatch(
|
||||
filePath,
|
||||
filePath,
|
||||
normalizeLineEndings(contentOld),
|
||||
normalizeLineEndings(contentNew),
|
||||
),
|
||||
)
|
||||
yield* filetime.read(ctx.sessionID, filePath)
|
||||
}).pipe(Effect.orDie),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const info = yield* afs.stat(filePath).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||
if (!info) throw new Error(`File ${filePath} not found`)
|
||||
if (info.type === "Directory") throw new Error(`Path is a directory, not a file: ${filePath}`)
|
||||
contentOld = yield* afs.readFileString(filePath)
|
||||
|
||||
const ending = detectLineEnding(contentOld)
|
||||
const old = convertToLineEnding(normalizeLineEndings(params.oldString), ending)
|
||||
const next = convertToLineEnding(normalizeLineEndings(params.newString), ending)
|
||||
|
||||
contentNew = replace(contentOld, old, next, params.replaceAll)
|
||||
|
||||
diff = trimDiff(
|
||||
createTwoFilesPatch(
|
||||
filePath,
|
||||
filePath,
|
||||
normalizeLineEndings(contentOld),
|
||||
normalizeLineEndings(contentNew),
|
||||
),
|
||||
)
|
||||
yield* ctx.ask({
|
||||
permission: "edit",
|
||||
patterns: [path.relative(Instance.worktree, filePath)],
|
||||
always: ["*"],
|
||||
metadata: {
|
||||
filepath: filePath,
|
||||
diff,
|
||||
},
|
||||
})
|
||||
|
||||
yield* afs.writeWithDirs(filePath, contentNew)
|
||||
yield* format.file(filePath)
|
||||
yield* bus.publish(File.Event.Edited, { file: filePath })
|
||||
yield* bus.publish(FileWatcher.Event.Updated, {
|
||||
file: filePath,
|
||||
event: "change",
|
||||
})
|
||||
contentNew = yield* afs.readFileString(filePath)
|
||||
diff = trimDiff(
|
||||
createTwoFilesPatch(
|
||||
filePath,
|
||||
filePath,
|
||||
normalizeLineEndings(contentOld),
|
||||
normalizeLineEndings(contentNew),
|
||||
),
|
||||
)
|
||||
}).pipe(Effect.orDie)
|
||||
|
||||
const filediff: Snapshot.FileDiff = {
|
||||
file: filePath,
|
||||
|
||||
@@ -7,7 +7,6 @@ import { createInterface } from "readline"
|
||||
import * as Tool from "./tool"
|
||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||
import { LSP } from "../lsp"
|
||||
import { FileTime } from "../file/time"
|
||||
import DESCRIPTION from "./read.txt"
|
||||
import { Instance } from "../project/instance"
|
||||
import { assertExternalDirectoryEffect } from "./external-directory"
|
||||
@@ -31,7 +30,6 @@ export const ReadTool = Tool.define(
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const instruction = yield* Instruction.Service
|
||||
const lsp = yield* LSP.Service
|
||||
const time = yield* FileTime.Service
|
||||
const scope = yield* Scope.Scope
|
||||
|
||||
const miss = Effect.fn("ReadTool.miss")(function* (filepath: string) {
|
||||
@@ -75,9 +73,8 @@ export const ReadTool = Tool.define(
|
||||
).pipe(Effect.map((items: string[]) => items.sort((a, b) => a.localeCompare(b))))
|
||||
})
|
||||
|
||||
const warm = Effect.fn("ReadTool.warm")(function* (filepath: string, sessionID: Tool.Context["sessionID"]) {
|
||||
const warm = Effect.fn("ReadTool.warm")(function* (filepath: string) {
|
||||
yield* lsp.touchFile(filepath, false).pipe(Effect.ignore, Effect.forkIn(scope))
|
||||
yield* time.read(sessionID, filepath)
|
||||
})
|
||||
|
||||
const run = Effect.fn("ReadTool.execute")(function* (params: z.infer<typeof parameters>, ctx: Tool.Context) {
|
||||
@@ -196,7 +193,7 @@ export const ReadTool = Tool.define(
|
||||
}
|
||||
output += "\n</content>"
|
||||
|
||||
yield* warm(filepath, ctx.sessionID)
|
||||
yield* warm(filepath)
|
||||
|
||||
if (loaded.length > 0) {
|
||||
output += `\n\n<system-reminder>\n${loaded.map((item) => item.content).join("\n\n")}\n</system-reminder>`
|
||||
|
||||
@@ -39,7 +39,6 @@ import { InstanceState } from "@/effect"
|
||||
import { Question } from "../question"
|
||||
import { Todo } from "../session/todo"
|
||||
import { LSP } from "../lsp"
|
||||
import { FileTime } from "../file/time"
|
||||
import { Instruction } from "../session/instruction"
|
||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||
import { Bus } from "../bus"
|
||||
@@ -80,7 +79,6 @@ export const layer: Layer.Layer<
|
||||
| Session.Service
|
||||
| Provider.Service
|
||||
| LSP.Service
|
||||
| FileTime.Service
|
||||
| Instruction.Service
|
||||
| AppFileSystem.Service
|
||||
| Bus.Service
|
||||
@@ -329,7 +327,6 @@ export const defaultLayer = Layer.suspend(() =>
|
||||
Layer.provide(Session.defaultLayer),
|
||||
Layer.provide(Provider.defaultLayer),
|
||||
Layer.provide(LSP.defaultLayer),
|
||||
Layer.provide(FileTime.defaultLayer),
|
||||
Layer.provide(Instruction.defaultLayer),
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(Bus.layer),
|
||||
|
||||
@@ -9,7 +9,6 @@ import { Bus } from "../bus"
|
||||
import { File } from "../file"
|
||||
import { FileWatcher } from "../file/watcher"
|
||||
import { Format } from "../format"
|
||||
import { FileTime } from "../file/time"
|
||||
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
|
||||
import { Instance } from "../project/instance"
|
||||
import { trimDiff } from "./edit"
|
||||
@@ -22,7 +21,6 @@ export const WriteTool = Tool.define(
|
||||
Effect.gen(function* () {
|
||||
const lsp = yield* LSP.Service
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const filetime = yield* FileTime.Service
|
||||
const bus = yield* Bus.Service
|
||||
const format = yield* Format.Service
|
||||
|
||||
@@ -41,7 +39,6 @@ export const WriteTool = Tool.define(
|
||||
|
||||
const exists = yield* fs.existsSafe(filepath)
|
||||
const contentOld = exists ? yield* fs.readFileString(filepath) : ""
|
||||
if (exists) yield* filetime.assert(ctx.sessionID, filepath)
|
||||
|
||||
const diff = trimDiff(createTwoFilesPatch(filepath, filepath, contentOld, params.content))
|
||||
yield* ctx.ask({
|
||||
@@ -61,7 +58,6 @@ export const WriteTool = Tool.define(
|
||||
file: filepath,
|
||||
event: exists ? "change" : "add",
|
||||
})
|
||||
yield* filetime.read(ctx.sessionID, filepath)
|
||||
|
||||
let output = "Wrote file successfully."
|
||||
yield* lsp.touchFile(filepath, true)
|
||||
|
||||
Reference in New Issue
Block a user