|
@@ -356,8 +356,9 @@ export const layer = Layer.effect(
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
const scan = Effect.fn("File.scan")(function* () {
|
|
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: [] }
|
|
const next: Entry = { files: [], dirs: [] }
|
|
|
|
|
|
|
|
if (isGlobalHome) {
|
|
if (isGlobalHome) {
|
|
@@ -366,14 +367,14 @@ export const layer = Layer.effect(
|
|
|
const ignoreNested = new Set(["node_modules", "dist", "build", "target", "vendor"])
|
|
const ignoreNested = new Set(["node_modules", "dist", "build", "target", "vendor"])
|
|
|
const shouldIgnoreName = (name: string) => name.startsWith(".") || protectedNames.has(name)
|
|
const shouldIgnoreName = (name: string) => name.startsWith(".") || protectedNames.has(name)
|
|
|
const shouldIgnoreNested = (name: string) => name.startsWith(".") || ignoreNested.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) {
|
|
for (const entry of top) {
|
|
|
if (entry.type !== "directory") continue
|
|
if (entry.type !== "directory") continue
|
|
|
if (shouldIgnoreName(entry.name)) continue
|
|
if (shouldIgnoreName(entry.name)) continue
|
|
|
dirs.add(entry.name + "/")
|
|
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(() => []))
|
|
const children = yield* appFs.readDirectoryEntries(base).pipe(Effect.orElseSucceed(() => []))
|
|
|
for (const child of children) {
|
|
for (const child of children) {
|
|
|
if (child.type !== "directory") continue
|
|
if (child.type !== "directory") continue
|
|
@@ -384,7 +385,7 @@ export const layer = Layer.effect(
|
|
|
|
|
|
|
|
next.dirs = Array.from(dirs).toSorted()
|
|
next.dirs = Array.from(dirs).toSorted()
|
|
|
} else {
|
|
} else {
|
|
|
- const files = yield* rg.files({ cwd: Instance.directory }).pipe(
|
|
|
|
|
|
|
+ const files = yield* rg.files({ cwd: ctx.directory }).pipe(
|
|
|
Stream.runCollect,
|
|
Stream.runCollect,
|
|
|
Effect.map((chunk) => [...chunk]),
|
|
Effect.map((chunk) => [...chunk]),
|
|
|
)
|
|
)
|
|
@@ -416,7 +417,7 @@ export const layer = Layer.effect(
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
const gitText = Effect.fnUntraced(function* (args: string[]) {
|
|
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* () {
|
|
const init = Effect.fn("File.init")(function* () {
|
|
@@ -424,7 +425,8 @@ export const layer = Layer.effect(
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
const status = Effect.fn("File.status")(function* () {
|
|
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([
|
|
const diffOutput = yield* gitText([
|
|
|
"-c",
|
|
"-c",
|
|
@@ -463,7 +465,7 @@ export const layer = Layer.effect(
|
|
|
if (untrackedOutput.trim()) {
|
|
if (untrackedOutput.trim()) {
|
|
|
for (const file of untrackedOutput.trim().split("\n")) {
|
|
for (const file of untrackedOutput.trim().split("\n")) {
|
|
|
const content = yield* appFs
|
|
const content = yield* appFs
|
|
|
- .readFileString(path.join(Instance.directory, file))
|
|
|
|
|
|
|
+ .readFileString(path.join(ctx.directory, file))
|
|
|
.pipe(Effect.catch(() => Effect.succeed<string | undefined>(undefined)))
|
|
.pipe(Effect.catch(() => Effect.succeed<string | undefined>(undefined)))
|
|
|
if (content === undefined) continue
|
|
if (content === undefined) continue
|
|
|
changed.push({
|
|
changed.push({
|
|
@@ -498,19 +500,22 @@ export const layer = Layer.effect(
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return changed.map((item) => {
|
|
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 {
|
|
return {
|
|
|
...item,
|
|
...item,
|
|
|
- path: path.relative(Instance.directory, full),
|
|
|
|
|
|
|
+ path: path.relative(ctx.directory, full),
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
const read: Interface["read"] = Effect.fn("File.read")(function* (file: string) {
|
|
const read: Interface["read"] = Effect.fn("File.read")(function* (file: string) {
|
|
|
using _ = log.time("read", { file })
|
|
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 (!Instance.containsPath(full, ctx)) {
|
|
|
|
|
+ throw new Error("Access denied: path escapes project directory")
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
if (isImageByExtension(file)) {
|
|
if (isImageByExtension(file)) {
|
|
|
const exists = yield* appFs.existsSafe(full)
|
|
const exists = yield* appFs.existsSafe(full)
|
|
@@ -553,13 +558,13 @@ export const layer = Layer.effect(
|
|
|
Effect.catch(() => Effect.succeed("")),
|
|
Effect.catch(() => Effect.succeed("")),
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- if (Instance.project.vcs === "git") {
|
|
|
|
|
|
|
+ if (ctx.project.vcs === "git") {
|
|
|
let diff = yield* gitText(["-c", "core.fsmonitor=false", "diff", "--", file])
|
|
let diff = yield* gitText(["-c", "core.fsmonitor=false", "diff", "--", file])
|
|
|
if (!diff.trim()) {
|
|
if (!diff.trim()) {
|
|
|
diff = yield* gitText(["-c", "core.fsmonitor=false", "diff", "--staged", "--", file])
|
|
diff = yield* gitText(["-c", "core.fsmonitor=false", "diff", "--staged", "--", file])
|
|
|
}
|
|
}
|
|
|
if (diff.trim()) {
|
|
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", {
|
|
const patch = structuredPatch(file, file, original, content, "old", "new", {
|
|
|
context: Infinity,
|
|
context: Infinity,
|
|
|
ignoreWhitespace: true,
|
|
ignoreWhitespace: true,
|
|
@@ -573,21 +578,24 @@ export const layer = Layer.effect(
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
const list = Effect.fn("File.list")(function* (dir?: string) {
|
|
const list = Effect.fn("File.list")(function* (dir?: string) {
|
|
|
|
|
+ const ctx = yield* InstanceState.context
|
|
|
const exclude = [".git", ".DS_Store"]
|
|
const exclude = [".git", ".DS_Store"]
|
|
|
let ignored = (_: string) => false
|
|
let ignored = (_: string) => false
|
|
|
- if (Instance.project.vcs === "git") {
|
|
|
|
|
|
|
+ if (ctx.project.vcs === "git") {
|
|
|
const ig = ignore()
|
|
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("")))
|
|
const gitignoreText = yield* appFs.readFileString(gitignore).pipe(Effect.catch(() => Effect.succeed("")))
|
|
|
if (gitignoreText) ig.add(gitignoreText)
|
|
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("")))
|
|
const ignoreText = yield* appFs.readFileString(ignoreFile).pipe(Effect.catch(() => Effect.succeed("")))
|
|
|
if (ignoreText) ig.add(ignoreText)
|
|
if (ignoreText) ig.add(ignoreText)
|
|
|
ignored = ig.ignores.bind(ig)
|
|
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 (!Instance.containsPath(resolved, ctx)) {
|
|
|
|
|
+ throw new Error("Access denied: path escapes project directory")
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
const entries = yield* appFs.readDirectoryEntries(resolved).pipe(Effect.orElseSucceed(() => []))
|
|
const entries = yield* appFs.readDirectoryEntries(resolved).pipe(Effect.orElseSucceed(() => []))
|
|
|
|
|
|
|
@@ -595,7 +603,7 @@ export const layer = Layer.effect(
|
|
|
for (const entry of entries) {
|
|
for (const entry of entries) {
|
|
|
if (exclude.includes(entry.name)) continue
|
|
if (exclude.includes(entry.name)) continue
|
|
|
const absolute = path.join(resolved, entry.name)
|
|
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"
|
|
const type = entry.type === "directory" ? "directory" : "file"
|
|
|
nodes.push({
|
|
nodes.push({
|
|
|
name: entry.name,
|
|
name: entry.name,
|