cloudaxe-opencode/packages/opencode/src/file/index.ts

203 lines
5.7 KiB
TypeScript
Raw Normal View History

import z from "zod/v4"
2025-06-27 15:29:20 +00:00
import { Bus } from "../bus"
2025-07-01 02:46:42 +00:00
import { $ } from "bun"
2025-09-04 17:39:34 +00:00
import { formatPatch, structuredPatch } from "diff"
2025-07-01 02:46:42 +00:00
import path from "path"
2025-07-02 00:06:11 +00:00
import fs from "fs"
2025-08-28 15:49:40 +00:00
import ignore from "ignore"
2025-07-02 00:06:11 +00:00
import { Log } from "../util/log"
import { Instance } from "../project/instance"
2025-06-27 15:29:20 +00:00
export namespace File {
2025-07-02 00:39:43 +00:00
const log = Log.create({ service: "file" })
2025-07-02 00:06:11 +00:00
2025-07-09 13:16:10 +00:00
export const Info = z
.object({
path: z.string(),
added: z.number().int(),
removed: z.number().int(),
status: z.enum(["added", "deleted", "modified"]),
})
.meta({
2025-07-09 13:16:10 +00:00
ref: "File",
})
export type Info = z.infer<typeof Info>
2025-08-27 20:27:49 +00:00
export const Node = z
.object({
name: z.string(),
path: z.string(),
2025-09-04 17:39:34 +00:00
absolute: z.string(),
2025-08-27 20:27:49 +00:00
type: z.enum(["file", "directory"]),
2025-08-28 15:49:40 +00:00
ignored: z.boolean(),
2025-08-27 20:27:49 +00:00
})
.meta({
2025-08-27 20:27:49 +00:00
ref: "FileNode",
})
export type Node = z.infer<typeof Node>
2025-09-04 17:39:34 +00:00
export const Content = z
.object({
content: z.string(),
diff: z.string().optional(),
patch: z
.object({
oldFileName: z.string(),
newFileName: z.string(),
oldHeader: z.string().optional(),
newHeader: z.string().optional(),
hunks: z.array(
z.object({
oldStart: z.number(),
oldLines: z.number(),
newStart: z.number(),
newLines: z.number(),
lines: z.array(z.string()),
}),
),
index: z.string().optional(),
})
.optional(),
})
.meta({
2025-09-04 17:39:34 +00:00
ref: "FileContent",
})
export type Content = z.infer<typeof Content>
2025-06-27 15:29:20 +00:00
export const Event = {
Edited: Bus.event(
"file.edited",
z.object({
file: z.string(),
}),
),
}
2025-07-01 02:46:42 +00:00
2025-07-02 00:39:43 +00:00
export async function status() {
const project = Instance.project
if (project.vcs !== "git") return []
2025-07-02 00:39:43 +00:00
const diffOutput = await $`git diff --numstat HEAD`.cwd(Instance.directory).quiet().nothrow().text()
2025-07-02 00:39:43 +00:00
2025-07-09 13:16:10 +00:00
const changedFiles: Info[] = []
2025-07-02 00:39:43 +00:00
if (diffOutput.trim()) {
const lines = diffOutput.trim().split("\n")
for (const line of lines) {
const [added, removed, filepath] = line.split("\t")
changedFiles.push({
2025-07-09 13:16:10 +00:00
path: filepath,
2025-07-02 00:39:43 +00:00
added: added === "-" ? 0 : parseInt(added, 10),
removed: removed === "-" ? 0 : parseInt(removed, 10),
status: "modified",
})
}
}
const untrackedOutput = await $`git ls-files --others --exclude-standard`
.cwd(Instance.directory)
.quiet()
.nothrow()
.text()
2025-07-02 00:39:43 +00:00
if (untrackedOutput.trim()) {
const untrackedFiles = untrackedOutput.trim().split("\n")
for (const filepath of untrackedFiles) {
try {
const content = await Bun.file(path.join(Instance.worktree, filepath)).text()
2025-07-02 00:39:43 +00:00
const lines = content.split("\n").length
changedFiles.push({
2025-07-09 13:16:10 +00:00
path: filepath,
2025-07-02 00:39:43 +00:00
added: lines,
removed: 0,
status: "added",
})
} catch {
continue
}
}
}
// Get deleted files
const deletedOutput = await $`git diff --name-only --diff-filter=D HEAD`
.cwd(Instance.directory)
.quiet()
.nothrow()
.text()
2025-07-02 00:39:43 +00:00
if (deletedOutput.trim()) {
const deletedFiles = deletedOutput.trim().split("\n")
for (const filepath of deletedFiles) {
changedFiles.push({
2025-07-09 13:16:10 +00:00
path: filepath,
2025-07-02 00:39:43 +00:00
added: 0,
removed: 0, // Could get original line count but would require another git command
status: "deleted",
})
}
}
return changedFiles.map((x) => ({
...x,
path: path.relative(Instance.directory, path.join(Instance.worktree, x.path)),
2025-07-02 00:39:43 +00:00
}))
}
2025-07-01 02:46:42 +00:00
export async function read(file: string) {
2025-07-02 00:06:11 +00:00
using _ = log.time("read", { file })
const project = Instance.project
const full = path.join(Instance.directory, file)
2025-07-02 00:39:43 +00:00
const content = await Bun.file(full)
.text()
2025-07-02 00:45:50 +00:00
.catch(() => "")
2025-07-02 00:39:43 +00:00
.then((x) => x.trim())
if (project.vcs === "git") {
const diff = await $`git diff ${file}`.cwd(Instance.directory).quiet().nothrow().text()
if (diff.trim()) {
const original = await $`git show HEAD:${file}`.cwd(Instance.directory).quiet().nothrow().text()
2025-09-05 20:58:37 +00:00
const patch = structuredPatch(file, file, original, content, "old", "new", {
2025-07-02 00:39:43 +00:00
context: Infinity,
})
2025-09-05 20:58:37 +00:00
const diff = formatPatch(patch)
2025-09-04 17:39:34 +00:00
return { content, patch, diff }
2025-07-01 02:46:42 +00:00
}
}
2025-09-04 17:39:34 +00:00
return { content }
2025-07-01 02:46:42 +00:00
}
2025-08-27 20:27:49 +00:00
export async function list(dir?: string) {
2025-08-28 15:49:40 +00:00
const exclude = [".git", ".DS_Store"]
const project = Instance.project
2025-08-28 15:49:40 +00:00
let ignored = (_: string) => false
if (project.vcs === "git") {
const gitignore = Bun.file(path.join(Instance.worktree, ".gitignore"))
2025-08-28 15:49:40 +00:00
if (await gitignore.exists()) {
const ig = ignore().add(await gitignore.text())
ignored = ig.ignores.bind(ig)
}
}
const resolved = dir ? path.join(Instance.directory, dir) : Instance.directory
2025-08-27 20:27:49 +00:00
const nodes: Node[] = []
for (const entry of await fs.promises.readdir(resolved, { withFileTypes: true })) {
2025-08-28 15:49:40 +00:00
if (exclude.includes(entry.name)) continue
2025-08-27 20:27:49 +00:00
const fullPath = path.join(resolved, entry.name)
const relativePath = path.relative(Instance.directory, fullPath)
2025-08-28 15:49:40 +00:00
const type = entry.isDirectory() ? "directory" : "file"
2025-08-27 20:27:49 +00:00
nodes.push({
name: entry.name,
path: relativePath,
2025-09-04 17:39:34 +00:00
absolute: fullPath,
2025-08-28 15:49:40 +00:00
type,
2025-09-02 11:20:08 +00:00
ignored: ignored(type === "directory" ? relativePath + "/" : relativePath),
2025-08-27 20:27:49 +00:00
})
}
return nodes.sort((a, b) => {
if (a.type !== b.type) {
return a.type === "directory" ? -1 : 1
}
return a.name.localeCompare(b.name)
})
}
2025-06-27 15:29:20 +00:00
}