UNPKG

@deep-assistant/agent

Version:

A minimal, public domain AI CLI agent compatible with OpenCode's JSON interface. Bun-only runtime.

198 lines (183 loc) 6.32 kB
import { $ } from "bun" import path from "path" import fs from "fs/promises" import { Log } from "../util/log" import { Global } from "../global" import z from "zod" import { Config } from "../config/config" import { Instance } from "../project/instance" export namespace Snapshot { const log = Log.create({ service: "snapshot" }) export async function track() { if (Instance.project.vcs !== "git") return const cfg = await Config.get() if (cfg.snapshot === false) return const git = gitdir() if (await fs.mkdir(git, { recursive: true })) { await $`git init` .env({ ...process.env, GIT_DIR: git, GIT_WORK_TREE: Instance.worktree, }) .quiet() .nothrow() // Configure git to not convert line endings on Windows await $`git --git-dir ${git} config core.autocrlf false`.quiet().nothrow() log.info("initialized") } await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`.quiet().cwd(Instance.directory).nothrow() const hash = await $`git --git-dir ${git} --work-tree ${Instance.worktree} write-tree` .quiet() .cwd(Instance.directory) .nothrow() .text() log.info("tracking", { hash, cwd: Instance.directory, git }) return hash.trim() } export const Patch = z.object({ hash: z.string(), files: z.string().array(), }) export type Patch = z.infer<typeof Patch> export async function patch(hash: string): Promise<Patch> { const git = gitdir() await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`.quiet().cwd(Instance.directory).nothrow() const result = await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} diff --name-only ${hash} -- .` .quiet() .cwd(Instance.directory) .nothrow() // If git diff fails, return empty patch if (result.exitCode !== 0) { log.warn("failed to get diff", { hash, exitCode: result.exitCode }) return { hash, files: [] } } const files = result.text() return { hash, files: files .trim() .split("\n") .map((x) => x.trim()) .filter(Boolean) .map((x) => path.join(Instance.worktree, x)), } } export async function restore(snapshot: string) { log.info("restore", { commit: snapshot }) const git = gitdir() const result = await $`git --git-dir ${git} --work-tree ${Instance.worktree} read-tree ${snapshot} && git --git-dir ${git} --work-tree ${Instance.worktree} checkout-index -a -f` .quiet() .cwd(Instance.worktree) .nothrow() if (result.exitCode !== 0) { log.error("failed to restore snapshot", { snapshot, exitCode: result.exitCode, stderr: result.stderr.toString(), stdout: result.stdout.toString(), }) } } export async function revert(patches: Patch[]) { const files = new Set<string>() const git = gitdir() for (const item of patches) { for (const file of item.files) { if (files.has(file)) continue log.info("reverting", { file, hash: item.hash }) const result = await $`git --git-dir ${git} --work-tree ${Instance.worktree} checkout ${item.hash} -- ${file}` .quiet() .cwd(Instance.worktree) .nothrow() if (result.exitCode !== 0) { const relativePath = path.relative(Instance.worktree, file) const checkTree = await $`git --git-dir ${git} --work-tree ${Instance.worktree} ls-tree ${item.hash} -- ${relativePath}` .quiet() .cwd(Instance.worktree) .nothrow() if (checkTree.exitCode === 0 && checkTree.text().trim()) { log.info("file existed in snapshot but checkout failed, keeping", { file, }) } else { log.info("file did not exist in snapshot, deleting", { file }) await fs.unlink(file).catch(() => {}) } } files.add(file) } } } export async function diff(hash: string) { const git = gitdir() await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`.quiet().cwd(Instance.directory).nothrow() const result = await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} diff ${hash} -- .` .quiet() .cwd(Instance.worktree) .nothrow() if (result.exitCode !== 0) { log.warn("failed to get diff", { hash, exitCode: result.exitCode, stderr: result.stderr.toString(), stdout: result.stdout.toString(), }) return "" } return result.text().trim() } export const FileDiff = z .object({ file: z.string(), before: z.string(), after: z.string(), additions: z.number(), deletions: z.number(), }) .meta({ ref: "FileDiff", }) export type FileDiff = z.infer<typeof FileDiff> export async function diffFull(from: string, to: string): Promise<FileDiff[]> { const git = gitdir() const result: FileDiff[] = [] for await (const line of $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-renames --numstat ${from} ${to} -- .` .quiet() .cwd(Instance.directory) .nothrow() .lines()) { if (!line) continue const [additions, deletions, file] = line.split("\t") const isBinaryFile = additions === "-" && deletions === "-" const before = isBinaryFile ? "" : await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} show ${from}:${file}` .quiet() .nothrow() .text() const after = isBinaryFile ? "" : await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} show ${to}:${file}` .quiet() .nothrow() .text() result.push({ file, before, after, additions: parseInt(additions), deletions: parseInt(deletions), }) } return result } function gitdir() { const project = Instance.project return path.join(Global.Path.data, "snapshot", project.id) } }