Răsfoiți Sursa

snapshot functionality

Dax Raad 7 luni în urmă
părinte
comite
11d042be25

+ 24 - 1
bun.lock

@@ -36,6 +36,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",
@@ -541,6 +542,8 @@
 
     "astro-expressive-code": ["[email protected]", "", { "dependencies": { "rehype-expressive-code": "^0.41.2" }, "peerDependencies": { "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0" } }, "sha512-HN0jWTnhr7mIV/2e6uu4PPRNNo/k4UEgTLZqbp3MrHU+caCARveG2yZxaZVBmxyiVdYqW5Pd3u3n2zjnshixbw=="],
 
+    "async-lock": ["[email protected]", "", {}, "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ=="],
+
     "atomic-sleep": ["[email protected]", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
 
     "available-typed-arrays": ["[email protected]", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
@@ -633,6 +636,8 @@
 
     "ci-info": ["[email protected]", "", {}, "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg=="],
 
+    "clean-git-ref": ["[email protected]", "", {}, "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw=="],
+
     "cli-boxes": ["[email protected]", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
 
     "cliui": ["[email protected]", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="],
@@ -669,6 +674,8 @@
 
     "cors": ["[email protected]", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
 
+    "crc-32": ["[email protected]", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="],
+
     "cross-fetch": ["[email protected]", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="],
 
     "crossws": ["[email protected]", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="],
@@ -725,6 +732,8 @@
 
     "diff-match-patch": ["[email protected]", "", {}, "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="],
 
+    "diff3": ["[email protected]", "", {}, "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g=="],
+
     "direction": ["[email protected]", "", { "bin": { "direction": "cli.js" } }, "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA=="],
 
     "dlv": ["[email protected]", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
@@ -939,6 +948,8 @@
 
     "ieee754": ["[email protected]", "", {}, "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="],
 
+    "ignore": ["[email protected]", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
+
     "import-meta-resolve": ["[email protected]", "", {}, "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw=="],
 
     "inherits": ["[email protected]", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
@@ -987,6 +998,8 @@
 
     "isarray": ["[email protected]", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
 
+    "isomorphic-git": ["[email protected]", "", { "dependencies": { "async-lock": "^1.4.1", "clean-git-ref": "^2.0.1", "crc-32": "^1.2.0", "diff3": "0.0.3", "ignore": "^5.1.4", "minimisted": "^2.0.0", "pako": "^1.0.10", "path-browserify": "^1.0.1", "pify": "^4.0.1", "readable-stream": "^3.4.0", "sha.js": "^2.4.9", "simple-get": "^4.0.1" }, "bin": { "isogit": "cli.cjs" } }, "sha512-NZCS7qpLkCZ1M/IrujYBD31sM6pd/fMVArK4fz4I7h6m0rUW2AsYU7S7zXeABuHL6HIfW6l53b4UQ/K441CQjg=="],
+
     "jmespath": ["[email protected]", "", {}, "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw=="],
 
     "jose": ["[email protected]", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="],
@@ -1169,6 +1182,8 @@
 
     "minimist": ["[email protected]", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
 
+    "minimisted": ["[email protected]", "", { "dependencies": { "minimist": "^1.2.5" } }, "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA=="],
+
     "mkdirp-classic": ["[email protected]", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
 
     "mri": ["[email protected]", "", {}, "sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w=="],
@@ -1247,7 +1262,7 @@
 
     "pagefind": ["[email protected]", "", { "optionalDependencies": { "@pagefind/darwin-arm64": "1.3.0", "@pagefind/darwin-x64": "1.3.0", "@pagefind/linux-arm64": "1.3.0", "@pagefind/linux-x64": "1.3.0", "@pagefind/windows-x64": "1.3.0" }, "bin": { "pagefind": "lib/runner/bin.cjs" } }, "sha512-8KPLGT5g9s+olKMRTU9LFekLizkVIu9tes90O1/aigJ0T5LmyPqTzGJrETnSw3meSYg58YH7JTzhTTW/3z6VAw=="],
 
-    "pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="],
+    "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
 
     "parse-entities": ["[email protected]", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
 
@@ -1257,6 +1272,8 @@
 
     "parseurl": ["[email protected]", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
 
+    "path-browserify": ["[email protected]", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
+
     "path-to-regexp": ["[email protected]", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],
 
     "pathe": ["[email protected]", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
@@ -1267,6 +1284,8 @@
 
     "picomatch": ["[email protected]", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
 
+    "pify": ["[email protected]", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="],
+
     "pino": ["[email protected]", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.0.0", "on-exit-leak-free": "^0.2.0", "pino-abstract-transport": "v0.5.0", "pino-std-serializers": "^4.0.0", "process-warning": "^1.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.1.0", "safe-stable-stringify": "^2.1.0", "sonic-boom": "^2.2.1", "thread-stream": "^0.15.1" }, "bin": { "pino": "bin.js" } }, "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg=="],
 
     "pino-abstract-transport": ["[email protected]", "", { "dependencies": { "duplexify": "^4.1.2", "split2": "^4.0.0" } }, "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ=="],
@@ -1417,6 +1436,8 @@
 
     "setprototypeof": ["[email protected]", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
 
+    "sha.js": ["[email protected]", "", { "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" }, "bin": { "sha.js": "./bin.js" } }, "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ=="],
+
     "sharp": ["[email protected]", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.2", "node-addon-api": "^6.1.0", "prebuild-install": "^7.1.1", "semver": "^7.5.4", "simple-get": "^4.0.1", "tar-fs": "^3.0.4", "tunnel-agent": "^0.6.0" } }, "sha512-0dap3iysgDkNaPOaOL4X/0akdu0ma62GcdC2NBQ+93eqpePdDdr2/LM0sFdDSMmN7yS+odyZtPsb7tx/cYBKnQ=="],
 
     "shiki": ["[email protected]", "", { "dependencies": { "@shikijs/core": "3.4.2", "@shikijs/engine-javascript": "3.4.2", "@shikijs/engine-oniguruma": "3.4.2", "@shikijs/langs": "3.4.2", "@shikijs/themes": "3.4.2", "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-wuxzZzQG8kvZndD7nustrNFIKYJ1jJoWIPaBpVe2+KHSvtzMi4SBjOxrigs8qeqce/l3U0cwiC+VAkLKSunHQQ=="],
@@ -1793,6 +1814,8 @@
 
     "token-types/ieee754": ["[email protected]", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
 
+    "unicode-trie/pako": ["[email protected]", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="],
+
     "unstorage/lru-cache": ["[email protected]", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
 
     "vscode-languageserver-protocol/vscode-jsonrpc": ["[email protected]", "", {}, "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw=="],

+ 1 - 0
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",

+ 26 - 0
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 <path>",
+  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)
+    })
+  },
+})

+ 17 - 0
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() {},
+})

+ 37 - 0
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 <file>",
+  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 <query>",
+  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))
+    })
+  },
+})

+ 5 - 64
packages/opencode/src/cli/cmd/debug.ts → packages/opencode/src/cli/cmd/debug/ripgrep.ts

@@ -1,52 +1,9 @@
-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"
+import { App } from "../../../app/app"
+import { Ripgrep } from "../../../file/ripgrep"
+import { bootstrap } from "../../bootstrap"
+import { cmd } from "../cmd"
 
-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 <file>",
-  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 <query>",
-  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({
+export const RipgrepCommand = cmd({
   command: "rg",
   builder: (yargs) =>
     yargs
@@ -128,19 +85,3 @@ const SearchCommand = cmd({
     console.log(JSON.stringify(results, null, 2))
   },
 })
-
-const FileReadCommand = cmd({
-  command: "file-read <path>",
-  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)
-    })
-  },
-})

+ 39 - 0
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 <commit>",
+  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")
+    })
+  },
+})

+ 85 - 0
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: "[email protected]",
+      },
+    })
+    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)
+  }
+}