|
|
@@ -1,6 +1,7 @@
|
|
|
import z from "zod/v4"
|
|
|
import { Bus } from "../bus"
|
|
|
import { $ } from "bun"
|
|
|
+import type { BunFile } from "bun"
|
|
|
import { formatPatch, structuredPatch } from "diff"
|
|
|
import path from "path"
|
|
|
import fs from "fs"
|
|
|
@@ -41,6 +42,7 @@ export namespace File {
|
|
|
|
|
|
export const Content = z
|
|
|
.object({
|
|
|
+ type: z.literal("text"),
|
|
|
content: z.string(),
|
|
|
diff: z.string().optional(),
|
|
|
patch: z
|
|
|
@@ -61,12 +63,53 @@ export namespace File {
|
|
|
index: z.string().optional(),
|
|
|
})
|
|
|
.optional(),
|
|
|
+ encoding: z.literal("base64").optional(),
|
|
|
+ mimeType: z.string().optional(),
|
|
|
})
|
|
|
.meta({
|
|
|
ref: "FileContent",
|
|
|
})
|
|
|
export type Content = z.infer<typeof Content>
|
|
|
|
|
|
+ async function shouldEncode(file: BunFile): Promise<boolean> {
|
|
|
+ const type = file.type?.toLowerCase()
|
|
|
+ if (!type) return false
|
|
|
+
|
|
|
+ if (type.startsWith("text/")) return false
|
|
|
+ if (type.includes("charset=")) return false
|
|
|
+
|
|
|
+ const parts = type.split("/", 2)
|
|
|
+ const top = parts[0]
|
|
|
+ const rest = parts[1] ?? ""
|
|
|
+ const sub = rest.split(";", 1)[0]
|
|
|
+
|
|
|
+ const tops = ["image", "audio", "video", "font", "model", "multipart"]
|
|
|
+ if (tops.includes(top)) return true
|
|
|
+
|
|
|
+ if (type === "application/octet-stream") return true
|
|
|
+
|
|
|
+ const bins = [
|
|
|
+ "zip",
|
|
|
+ "gzip",
|
|
|
+ "bzip",
|
|
|
+ "compressed",
|
|
|
+ "binary",
|
|
|
+ "stream",
|
|
|
+ "pdf",
|
|
|
+ "msword",
|
|
|
+ "powerpoint",
|
|
|
+ "excel",
|
|
|
+ "ogg",
|
|
|
+ "exe",
|
|
|
+ "dmg",
|
|
|
+ "iso",
|
|
|
+ "rar",
|
|
|
+ ]
|
|
|
+ if (bins.some((mark) => sub.includes(mark))) return true
|
|
|
+
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
export const Event = {
|
|
|
Edited: Bus.event(
|
|
|
"file.edited",
|
|
|
@@ -188,14 +231,30 @@ export namespace File {
|
|
|
}))
|
|
|
}
|
|
|
|
|
|
- export async function read(file: string) {
|
|
|
+ export async function read(file: string): Promise<Content> {
|
|
|
using _ = log.time("read", { file })
|
|
|
const project = Instance.project
|
|
|
const full = path.join(Instance.directory, file)
|
|
|
- const content = await Bun.file(full)
|
|
|
+ const bunFile = Bun.file(full)
|
|
|
+
|
|
|
+ if (!(await bunFile.exists())) {
|
|
|
+ return { type: "text", content: "" }
|
|
|
+ }
|
|
|
+
|
|
|
+ const encode = await shouldEncode(bunFile)
|
|
|
+
|
|
|
+ if (encode) {
|
|
|
+ const buffer = await bunFile.arrayBuffer().catch(() => new ArrayBuffer(0))
|
|
|
+ const content = Buffer.from(buffer).toString("base64")
|
|
|
+ const mimeType = bunFile.type || "application/octet-stream"
|
|
|
+ return { type: "text", content, mimeType, encoding: "base64" }
|
|
|
+ }
|
|
|
+
|
|
|
+ const content = await bunFile
|
|
|
.text()
|
|
|
.catch(() => "")
|
|
|
.then((x) => x.trim())
|
|
|
+
|
|
|
if (project.vcs === "git") {
|
|
|
let diff = await $`git diff ${file}`.cwd(Instance.directory).quiet().nothrow().text()
|
|
|
if (!diff.trim()) diff = await $`git diff --staged ${file}`.cwd(Instance.directory).quiet().nothrow().text()
|
|
|
@@ -206,10 +265,10 @@ export namespace File {
|
|
|
ignoreWhitespace: true,
|
|
|
})
|
|
|
const diff = formatPatch(patch)
|
|
|
- return { content, patch, diff }
|
|
|
+ return { type: "text", content, patch, diff }
|
|
|
}
|
|
|
}
|
|
|
- return { content }
|
|
|
+ return { type: "text", content }
|
|
|
}
|
|
|
|
|
|
export async function list(dir?: string) {
|