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

166 lines
4.8 KiB
TypeScript
Raw Normal View History

2025-06-27 15:29:20 +00:00
import { z } from "zod"
import { Bus } from "../bus"
2025-07-01 02:46:42 +00:00
import { $ } from "bun"
import { createPatch } from "diff"
import path from "path"
2025-07-02 00:06:11 +00:00
import { App } from "../app/app"
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"
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"]),
})
.openapi({
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(),
type: z.enum(["file", "directory"]),
2025-08-28 15:49:40 +00:00
ignored: z.boolean(),
2025-08-27 20:27:49 +00:00
})
.openapi({
ref: "FileNode",
})
export type Node = z.infer<typeof Node>
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 app = App.info()
if (!app.git) return []
const diffOutput = await $`git diff --numstat HEAD`.cwd(app.path.cwd).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(app.path.cwd).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(app.path.root, 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(app.path.cwd).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,
2025-07-09 13:16:10 +00:00
path: path.relative(app.path.cwd, path.join(app.path.root, 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 app = App.info()
const full = path.join(app.path.cwd, 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())
2025-07-02 00:06:11 +00:00
if (app.git) {
const rel = path.relative(app.path.root, full)
const diff = await $`git diff ${rel}`.cwd(app.path.root).quiet().nothrow().text()
if (diff.trim()) {
const original = await $`git show HEAD:${rel}`.cwd(app.path.root).quiet().nothrow().text()
2025-07-02 00:39:43 +00:00
const patch = createPatch(file, original, content, "old", "new", {
context: Infinity,
})
2025-07-02 00:39:43 +00:00
return { type: "patch", content: patch }
2025-07-01 02:46:42 +00:00
}
}
2025-07-02 00:39:43 +00:00
return { type: "raw", 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"]
2025-08-27 20:27:49 +00:00
const app = App.info()
2025-08-28 15:49:40 +00:00
let ignored = (_: string) => false
if (app.git) {
const gitignore = Bun.file(path.join(app.path.root, ".gitignore"))
if (await gitignore.exists()) {
const ig = ignore().add(await gitignore.text())
ignored = ig.ignores.bind(ig)
}
}
2025-08-27 20:27:49 +00:00
const resolved = dir ? path.join(app.path.cwd, dir) : app.path.cwd
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(app.path.cwd, fullPath)
2025-08-28 15:49:40 +00:00
const relativeToRoot = path.relative(app.path.root, fullPath)
const type = entry.isDirectory() ? "directory" : "file"
2025-08-27 20:27:49 +00:00
nodes.push({
name: entry.name,
path: relativePath,
2025-08-28 15:49:40 +00:00
type,
ignored: ignored(type === "directory" ? relativeToRoot + "/" : relativeToRoot),
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
}