From 11d042be25ee0509db323dc117724b0ac9e4610a Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 1 Jul 2025 12:06:38 -0400 Subject: snapshot functionality --- packages/opencode/package.json | 1 + packages/opencode/src/cli/cmd/debug.ts | 146 ------------------------ packages/opencode/src/cli/cmd/debug/file.ts | 26 +++++ packages/opencode/src/cli/cmd/debug/index.ts | 17 +++ packages/opencode/src/cli/cmd/debug/lsp.ts | 37 ++++++ packages/opencode/src/cli/cmd/debug/ripgrep.ts | 87 ++++++++++++++ packages/opencode/src/cli/cmd/debug/snapshot.ts | 39 +++++++ packages/opencode/src/snapshot/index.ts | 85 ++++++++++++++ 8 files changed, 292 insertions(+), 146 deletions(-) delete mode 100644 packages/opencode/src/cli/cmd/debug.ts create mode 100644 packages/opencode/src/cli/cmd/debug/file.ts create mode 100644 packages/opencode/src/cli/cmd/debug/index.ts create mode 100644 packages/opencode/src/cli/cmd/debug/lsp.ts create mode 100644 packages/opencode/src/cli/cmd/debug/ripgrep.ts create mode 100644 packages/opencode/src/cli/cmd/debug/snapshot.ts create mode 100644 packages/opencode/src/snapshot/index.ts (limited to 'packages') diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 6ccf2f969..fae2e3425 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -37,6 +37,7 @@ "env-paths": "3.0.0", "hono": "4.7.10", "hono-openapi": "0.4.8", + "isomorphic-git": "1.32.1", "open": "10.1.2", "remeda": "2.22.3", "ts-lsp-client": "1.0.3", diff --git a/packages/opencode/src/cli/cmd/debug.ts b/packages/opencode/src/cli/cmd/debug.ts deleted file mode 100644 index 6d218733b..000000000 --- a/packages/opencode/src/cli/cmd/debug.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { App } from "../../app/app" -import { Ripgrep } from "../../file/ripgrep" -import { File } from "../../file" -import { LSP } from "../../lsp" -import { Log } from "../../util/log" -import { bootstrap } from "../bootstrap" -import { cmd } from "./cmd" -import path from "path" - -export const DebugCommand = cmd({ - command: "debug", - builder: (yargs) => - yargs - .command(DiagnosticsCommand) - .command(RipgrepCommand) - .command(SymbolsCommand) - .command(FileReadCommand) - .demandCommand(), - async handler() {}, -}) - -const DiagnosticsCommand = cmd({ - command: "diagnostics ", - builder: (yargs) => - yargs.positional("file", { type: "string", demandOption: true }), - async handler(args) { - await bootstrap({ cwd: process.cwd() }, async () => { - await LSP.touchFile(args.file, true) - await LSP.touchFile(args.file, true) - console.log(await LSP.diagnostics()) - }) - }, -}) - -const SymbolsCommand = cmd({ - command: "symbols ", - builder: (yargs) => - yargs.positional("query", { type: "string", demandOption: true }), - async handler(args) { - await bootstrap({ cwd: process.cwd() }, async () => { - await LSP.touchFile("./src/index.ts", true) - using _ = Log.Default.time("symbols") - const results = await LSP.workspaceSymbol(args.query) - console.log(JSON.stringify(results, null, 2)) - }) - }, -}) - -const RipgrepCommand = cmd({ - command: "rg", - builder: (yargs) => - yargs - .command(TreeCommand) - .command(FilesCommand) - .command(SearchCommand) - .demandCommand(), - async handler() {}, -}) - -const TreeCommand = cmd({ - command: "tree", - builder: (yargs) => - yargs.option("limit", { - type: "number", - }), - async handler(args) { - await bootstrap({ cwd: process.cwd() }, async () => { - const app = App.info() - console.log(await Ripgrep.tree({ cwd: app.path.cwd, limit: args.limit })) - }) - }, -}) - -const FilesCommand = cmd({ - command: "files", - builder: (yargs) => - yargs - .option("query", { - type: "string", - description: "Filter files by query", - }) - .option("glob", { - type: "string", - description: "Glob pattern to match files", - }) - .option("limit", { - type: "number", - description: "Limit number of results", - }), - async handler(args) { - await bootstrap({ cwd: process.cwd() }, async () => { - const app = App.info() - const files = await Ripgrep.files({ - cwd: app.path.cwd, - query: args.query, - glob: args.glob, - limit: args.limit, - }) - console.log(files.join("\n")) - }) - }, -}) - -const SearchCommand = cmd({ - command: "search ", - builder: (yargs) => - yargs - .positional("pattern", { - type: "string", - demandOption: true, - description: "Search pattern", - }) - .option("glob", { - type: "array", - description: "File glob patterns", - }) - .option("limit", { - type: "number", - description: "Limit number of results", - }), - async handler(args) { - const results = await Ripgrep.search({ - cwd: process.cwd(), - pattern: args.pattern, - glob: args.glob as string[] | undefined, - limit: args.limit, - }) - console.log(JSON.stringify(results, null, 2)) - }, -}) - -const FileReadCommand = cmd({ - command: "file-read ", - builder: (yargs) => - yargs.positional("path", { - type: "string", - demandOption: true, - description: "File path to read", - }), - async handler(args) { - await bootstrap({ cwd: process.cwd() }, async () => { - const content = await File.read(path.resolve(args.path)) - console.log(content) - }) - }, -}) diff --git a/packages/opencode/src/cli/cmd/debug/file.ts b/packages/opencode/src/cli/cmd/debug/file.ts new file mode 100644 index 000000000..1d23ed851 --- /dev/null +++ b/packages/opencode/src/cli/cmd/debug/file.ts @@ -0,0 +1,26 @@ +import { File } from "../../../file" +import { bootstrap } from "../../bootstrap" +import { cmd } from "../cmd" +import path from "path" + +export const FileCommand = cmd({ + command: "file", + builder: (yargs) => yargs.command(FileReadCommand).demandCommand(), + async handler() {}, +}) + +const FileReadCommand = cmd({ + command: "read ", + builder: (yargs) => + yargs.positional("path", { + type: "string", + demandOption: true, + description: "File path to read", + }), + async handler(args) { + await bootstrap({ cwd: process.cwd() }, async () => { + const content = await File.read(path.resolve(args.path)) + console.log(content) + }) + }, +}) diff --git a/packages/opencode/src/cli/cmd/debug/index.ts b/packages/opencode/src/cli/cmd/debug/index.ts new file mode 100644 index 000000000..5a765997f --- /dev/null +++ b/packages/opencode/src/cli/cmd/debug/index.ts @@ -0,0 +1,17 @@ +import { cmd } from "../cmd" +import { FileCommand } from "./file" +import { LSPCommand } from "./lsp" +import { RipgrepCommand } from "./ripgrep" +import { SnapshotCommand } from "./snapshot" + +export const DebugCommand = cmd({ + command: "debug", + builder: (yargs) => + yargs + .command(LSPCommand) + .command(RipgrepCommand) + .command(FileCommand) + .command(SnapshotCommand) + .demandCommand(), + async handler() {}, +}) diff --git a/packages/opencode/src/cli/cmd/debug/lsp.ts b/packages/opencode/src/cli/cmd/debug/lsp.ts new file mode 100644 index 000000000..d596bf6c5 --- /dev/null +++ b/packages/opencode/src/cli/cmd/debug/lsp.ts @@ -0,0 +1,37 @@ +import { LSP } from "../../../lsp" +import { bootstrap } from "../../bootstrap" +import { cmd } from "../cmd" +import { Log } from "../../../util/log" + +export const LSPCommand = cmd({ + command: "lsp", + builder: (yargs) => + yargs.command(DiagnosticsCommand).command(SymbolsCommand).demandCommand(), + async handler() {}, +}) + +const DiagnosticsCommand = cmd({ + command: "diagnostics ", + builder: (yargs) => + yargs.positional("file", { type: "string", demandOption: true }), + async handler(args) { + await bootstrap({ cwd: process.cwd() }, async () => { + await LSP.touchFile(args.file, true) + console.log(await LSP.diagnostics()) + }) + }, +}) + +export const SymbolsCommand = cmd({ + command: "symbols ", + builder: (yargs) => + yargs.positional("query", { type: "string", demandOption: true }), + async handler(args) { + await bootstrap({ cwd: process.cwd() }, async () => { + await LSP.touchFile("./src/index.ts", true) + using _ = Log.Default.time("symbols") + const results = await LSP.workspaceSymbol(args.query) + console.log(JSON.stringify(results, null, 2)) + }) + }, +}) diff --git a/packages/opencode/src/cli/cmd/debug/ripgrep.ts b/packages/opencode/src/cli/cmd/debug/ripgrep.ts new file mode 100644 index 000000000..c71368671 --- /dev/null +++ b/packages/opencode/src/cli/cmd/debug/ripgrep.ts @@ -0,0 +1,87 @@ +import { App } from "../../../app/app" +import { Ripgrep } from "../../../file/ripgrep" +import { bootstrap } from "../../bootstrap" +import { cmd } from "../cmd" + +export const RipgrepCommand = cmd({ + command: "rg", + builder: (yargs) => + yargs + .command(TreeCommand) + .command(FilesCommand) + .command(SearchCommand) + .demandCommand(), + async handler() {}, +}) + +const TreeCommand = cmd({ + command: "tree", + builder: (yargs) => + yargs.option("limit", { + type: "number", + }), + async handler(args) { + await bootstrap({ cwd: process.cwd() }, async () => { + const app = App.info() + console.log(await Ripgrep.tree({ cwd: app.path.cwd, limit: args.limit })) + }) + }, +}) + +const FilesCommand = cmd({ + command: "files", + builder: (yargs) => + yargs + .option("query", { + type: "string", + description: "Filter files by query", + }) + .option("glob", { + type: "string", + description: "Glob pattern to match files", + }) + .option("limit", { + type: "number", + description: "Limit number of results", + }), + async handler(args) { + await bootstrap({ cwd: process.cwd() }, async () => { + const app = App.info() + const files = await Ripgrep.files({ + cwd: app.path.cwd, + query: args.query, + glob: args.glob, + limit: args.limit, + }) + console.log(files.join("\n")) + }) + }, +}) + +const SearchCommand = cmd({ + command: "search ", + builder: (yargs) => + yargs + .positional("pattern", { + type: "string", + demandOption: true, + description: "Search pattern", + }) + .option("glob", { + type: "array", + description: "File glob patterns", + }) + .option("limit", { + type: "number", + description: "Limit number of results", + }), + async handler(args) { + const results = await Ripgrep.search({ + cwd: process.cwd(), + pattern: args.pattern, + glob: args.glob as string[] | undefined, + limit: args.limit, + }) + console.log(JSON.stringify(results, null, 2)) + }, +}) diff --git a/packages/opencode/src/cli/cmd/debug/snapshot.ts b/packages/opencode/src/cli/cmd/debug/snapshot.ts new file mode 100644 index 000000000..a6d129d5a --- /dev/null +++ b/packages/opencode/src/cli/cmd/debug/snapshot.ts @@ -0,0 +1,39 @@ +import { Snapshot } from "../../../snapshot" +import { bootstrap } from "../../bootstrap" +import { cmd } from "../cmd" + +export const SnapshotCommand = cmd({ + command: "snapshot", + builder: (yargs) => + yargs + .command(SnapshotCreateCommand) + .command(SnapshotRestoreCommand) + .demandCommand(), + async handler() {}, +}) + +export const SnapshotCreateCommand = cmd({ + command: "create", + async handler() { + await bootstrap({ cwd: process.cwd() }, async () => { + const result = await Snapshot.create("test") + console.log(result) + }) + }, +}) + +export const SnapshotRestoreCommand = cmd({ + command: "restore ", + builder: (yargs) => + yargs.positional("commit", { + type: "string", + description: "commit", + demandOption: true, + }), + async handler(args) { + await bootstrap({ cwd: process.cwd() }, async () => { + await Snapshot.restore("test", args.commit) + console.log("restored") + }) + }, +}) diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts new file mode 100644 index 000000000..bf8ea05f6 --- /dev/null +++ b/packages/opencode/src/snapshot/index.ts @@ -0,0 +1,85 @@ +import { App } from "../app/app" +import { + add, + commit, + init, + checkout, + statusMatrix, + remove, +} from "isomorphic-git" +import path from "path" +import fs from "fs" +import { Ripgrep } from "../file/ripgrep" +import { Log } from "../util/log" + +export namespace Snapshot { + const log = Log.create({ service: "snapshot" }) + + export async function create(sessionID: string) { + const app = App.info() + const git = gitdir(sessionID) + const files = await Ripgrep.files({ + cwd: app.path.cwd, + limit: app.git ? undefined : 1000, + }) + // not a git repo and too big to snapshot + if (!app.git && files.length === 1000) return + await init({ + dir: app.path.cwd, + gitdir: git, + fs, + }) + const status = await statusMatrix({ + fs, + gitdir: git, + dir: app.path.cwd, + }) + await add({ + fs, + gitdir: git, + parallel: true, + dir: app.path.cwd, + filepath: files, + }) + for (const [file, _head, workdir, stage] of status) { + if (workdir === 0 && stage === 1) { + log.info("remove", { file }) + await remove({ + fs, + gitdir: git, + dir: app.path.cwd, + filepath: file, + }) + } + } + const result = await commit({ + fs, + gitdir: git, + dir: app.path.cwd, + message: "snapshot", + author: { + name: "opencode", + email: "mail@opencode.ai", + }, + }) + log.info("commit", { result }) + return result + } + + export async function restore(sessionID: string, commit: string) { + log.info("restore", { commit }) + const app = App.info() + await checkout({ + fs, + gitdir: gitdir(sessionID), + dir: app.path.cwd, + ref: commit, + force: true, + }) + } + + function gitdir(sessionID: string) { + const app = App.info() + return path.join(app.path.data, "snapshot", sessionID) + } +} -- cgit v1.2.3