Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 31 additions & 21 deletions packages/opencode/src/file/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import ignore from "ignore"
import path from "path"
import z from "zod"
import { Global } from "../global"
import { Instance } from "../project/instance"
import { Log } from "../util"
import { Protected } from "./protected"
import { Ripgrep } from "./ripgrep"
Expand Down Expand Up @@ -356,8 +355,9 @@ export const layer = Layer.effect(
)

const scan = Effect.fn("File.scan")(function* () {
if (Instance.directory === path.parse(Instance.directory).root) return
const isGlobalHome = Instance.directory === Global.Path.home && Instance.project.id === "global"
const ctx = yield* InstanceState.context
if (ctx.directory === path.parse(ctx.directory).root) return
const isGlobalHome = ctx.directory === Global.Path.home && ctx.project.id === "global"
const next: Entry = { files: [], dirs: [] }

if (isGlobalHome) {
Expand All @@ -366,14 +366,14 @@ export const layer = Layer.effect(
const ignoreNested = new Set(["node_modules", "dist", "build", "target", "vendor"])
const shouldIgnoreName = (name: string) => name.startsWith(".") || protectedNames.has(name)
const shouldIgnoreNested = (name: string) => name.startsWith(".") || ignoreNested.has(name)
const top = yield* appFs.readDirectoryEntries(Instance.directory).pipe(Effect.orElseSucceed(() => []))
const top = yield* appFs.readDirectoryEntries(ctx.directory).pipe(Effect.orElseSucceed(() => []))

for (const entry of top) {
if (entry.type !== "directory") continue
if (shouldIgnoreName(entry.name)) continue
dirs.add(entry.name + "/")

const base = path.join(Instance.directory, entry.name)
const base = path.join(ctx.directory, entry.name)
const children = yield* appFs.readDirectoryEntries(base).pipe(Effect.orElseSucceed(() => []))
for (const child of children) {
if (child.type !== "directory") continue
Expand All @@ -384,7 +384,7 @@ export const layer = Layer.effect(

next.dirs = Array.from(dirs).toSorted()
} else {
const files = yield* rg.files({ cwd: Instance.directory }).pipe(
const files = yield* rg.files({ cwd: ctx.directory }).pipe(
Stream.runCollect,
Effect.map((chunk) => [...chunk]),
)
Expand Down Expand Up @@ -416,15 +416,16 @@ export const layer = Layer.effect(
})

const gitText = Effect.fnUntraced(function* (args: string[]) {
return (yield* git.run(args, { cwd: Instance.directory })).text()
return (yield* git.run(args, { cwd: (yield* InstanceState.context).directory })).text()
})

const init = Effect.fn("File.init")(function* () {
yield* ensure().pipe(Effect.forkIn(scope))
})

const status = Effect.fn("File.status")(function* () {
if (Instance.project.vcs !== "git") return []
const ctx = yield* InstanceState.context
if (ctx.project.vcs !== "git") return []

const diffOutput = yield* gitText([
"-c",
Expand Down Expand Up @@ -463,7 +464,7 @@ export const layer = Layer.effect(
if (untrackedOutput.trim()) {
for (const file of untrackedOutput.trim().split("\n")) {
const content = yield* appFs
.readFileString(path.join(Instance.directory, file))
.readFileString(path.join(ctx.directory, file))
.pipe(Effect.catch(() => Effect.succeed<string | undefined>(undefined)))
if (content === undefined) continue
changed.push({
Expand Down Expand Up @@ -498,19 +499,22 @@ export const layer = Layer.effect(
}

return changed.map((item) => {
const full = path.isAbsolute(item.path) ? item.path : path.join(Instance.directory, item.path)
const full = path.isAbsolute(item.path) ? item.path : path.join(ctx.directory, item.path)
return {
...item,
path: path.relative(Instance.directory, full),
path: path.relative(ctx.directory, full),
}
})
})

const read: Interface["read"] = Effect.fn("File.read")(function* (file: string) {
using _ = log.time("read", { file })
const full = path.join(Instance.directory, file)
const ctx = yield* InstanceState.context
const full = path.join(ctx.directory, file)

if (!Instance.containsPath(full)) throw new Error("Access denied: path escapes project directory")
if (!AppFileSystem.contains(ctx.directory, full) && (ctx.worktree === "/" || !AppFileSystem.contains(ctx.worktree, full))) {
throw new Error("Access denied: path escapes project directory")
}

if (isImageByExtension(file)) {
const exists = yield* appFs.existsSafe(full)
Expand Down Expand Up @@ -553,13 +557,13 @@ export const layer = Layer.effect(
Effect.catch(() => Effect.succeed("")),
)

if (Instance.project.vcs === "git") {
if (ctx.project.vcs === "git") {
let diff = yield* gitText(["-c", "core.fsmonitor=false", "diff", "--", file])
if (!diff.trim()) {
diff = yield* gitText(["-c", "core.fsmonitor=false", "diff", "--staged", "--", file])
}
if (diff.trim()) {
const original = yield* git.show(Instance.directory, "HEAD", file)
const original = yield* git.show(ctx.directory, "HEAD", file)
const patch = structuredPatch(file, file, original, content, "old", "new", {
context: Infinity,
ignoreWhitespace: true,
Expand All @@ -573,29 +577,35 @@ export const layer = Layer.effect(
})

const list = Effect.fn("File.list")(function* (dir?: string) {
const ctx = yield* InstanceState.context
const exclude = [".git", ".DS_Store"]
let ignored = (_: string) => false
if (Instance.project.vcs === "git") {
if (ctx.project.vcs === "git") {
const ig = ignore()
const gitignore = path.join(Instance.project.worktree, ".gitignore")
const gitignore = path.join(ctx.worktree, ".gitignore")
const gitignoreText = yield* appFs.readFileString(gitignore).pipe(Effect.catch(() => Effect.succeed("")))
if (gitignoreText) ig.add(gitignoreText)
const ignoreFile = path.join(Instance.project.worktree, ".ignore")
const ignoreFile = path.join(ctx.worktree, ".ignore")
const ignoreText = yield* appFs.readFileString(ignoreFile).pipe(Effect.catch(() => Effect.succeed("")))
if (ignoreText) ig.add(ignoreText)
ignored = ig.ignores.bind(ig)
}

const resolved = dir ? path.join(Instance.directory, dir) : Instance.directory
if (!Instance.containsPath(resolved)) throw new Error("Access denied: path escapes project directory")
const resolved = dir ? path.join(ctx.directory, dir) : ctx.directory
if (
!AppFileSystem.contains(ctx.directory, resolved) &&
(ctx.worktree === "/" || !AppFileSystem.contains(ctx.worktree, resolved))
) {
throw new Error("Access denied: path escapes project directory")
}

const entries = yield* appFs.readDirectoryEntries(resolved).pipe(Effect.orElseSucceed(() => []))

const nodes: Node[] = []
for (const entry of entries) {
if (exclude.includes(entry.name)) continue
const absolute = path.join(resolved, entry.name)
const file = path.relative(Instance.directory, absolute)
const file = path.relative(ctx.directory, absolute)
const type = entry.type === "directory" ? "directory" : "file"
nodes.push({
name: entry.name,
Expand Down
Loading