chad-code 1.3.4 → 1.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +27 -0
- package/Dockerfile +18 -0
- package/README.npm.md +63 -0
- package/bunfig.toml +7 -0
- package/package.json +99 -36
- package/parsers-config.ts +253 -0
- package/script/build.ts +167 -0
- package/script/publish-registries.ts +187 -0
- package/script/publish.ts +93 -0
- package/script/schema.ts +47 -0
- package/src/acp/README.md +164 -0
- package/src/acp/agent.ts +1086 -0
- package/src/acp/session.ts +101 -0
- package/src/acp/types.ts +22 -0
- package/src/agent/agent.ts +253 -0
- package/src/agent/generate.txt +75 -0
- package/src/agent/prompt/compaction.txt +12 -0
- package/src/agent/prompt/explore.txt +18 -0
- package/src/agent/prompt/summary.txt +11 -0
- package/src/agent/prompt/title.txt +36 -0
- package/src/auth/index.ts +70 -0
- package/src/bun/index.ts +130 -0
- package/src/bus/bus-event.ts +43 -0
- package/src/bus/global.ts +10 -0
- package/src/bus/index.ts +105 -0
- package/src/cli/bootstrap.ts +17 -0
- package/src/cli/cmd/acp.ts +69 -0
- package/src/cli/cmd/agent.ts +257 -0
- package/src/cli/cmd/auth.ts +132 -0
- package/src/cli/cmd/cmd.ts +7 -0
- package/src/cli/cmd/debug/agent.ts +28 -0
- package/src/cli/cmd/debug/config.ts +15 -0
- package/src/cli/cmd/debug/file.ts +91 -0
- package/src/cli/cmd/debug/index.ts +45 -0
- package/src/cli/cmd/debug/lsp.ts +48 -0
- package/src/cli/cmd/debug/ripgrep.ts +83 -0
- package/src/cli/cmd/debug/scrap.ts +15 -0
- package/src/cli/cmd/debug/skill.ts +15 -0
- package/src/cli/cmd/debug/snapshot.ts +48 -0
- package/src/cli/cmd/export.ts +88 -0
- package/src/cli/cmd/generate.ts +38 -0
- package/src/cli/cmd/github.ts +32 -0
- package/src/cli/cmd/import.ts +98 -0
- package/src/cli/cmd/mcp.ts +670 -0
- package/src/cli/cmd/models.ts +42 -0
- package/src/cli/cmd/pr.ts +112 -0
- package/src/cli/cmd/run.ts +374 -0
- package/src/cli/cmd/serve.ts +16 -0
- package/src/cli/cmd/session.ts +135 -0
- package/src/cli/cmd/stats.ts +402 -0
- package/src/cli/cmd/tui/app.tsx +705 -0
- package/src/cli/cmd/tui/attach.ts +32 -0
- package/src/cli/cmd/tui/component/border.tsx +21 -0
- package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
- package/src/cli/cmd/tui/component/dialog-command.tsx +124 -0
- package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
- package/src/cli/cmd/tui/component/dialog-model.tsx +232 -0
- package/src/cli/cmd/tui/component/dialog-provider.tsx +228 -0
- package/src/cli/cmd/tui/component/dialog-session-list.tsx +115 -0
- package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
- package/src/cli/cmd/tui/component/dialog-stash.tsx +86 -0
- package/src/cli/cmd/tui/component/dialog-status.tsx +162 -0
- package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
- package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
- package/src/cli/cmd/tui/component/did-you-know.tsx +85 -0
- package/src/cli/cmd/tui/component/logo.tsx +68 -0
- package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +654 -0
- package/src/cli/cmd/tui/component/prompt/history.tsx +108 -0
- package/src/cli/cmd/tui/component/prompt/index.tsx +1073 -0
- package/src/cli/cmd/tui/component/prompt/stash.tsx +101 -0
- package/src/cli/cmd/tui/component/textarea-keybindings.ts +73 -0
- package/src/cli/cmd/tui/component/tips.ts +92 -0
- package/src/cli/cmd/tui/component/todo-item.tsx +32 -0
- package/src/cli/cmd/tui/context/args.tsx +14 -0
- package/src/cli/cmd/tui/context/directory.ts +13 -0
- package/src/cli/cmd/tui/context/exit.tsx +23 -0
- package/src/cli/cmd/tui/context/helper.tsx +25 -0
- package/src/cli/cmd/tui/context/keybind.tsx +101 -0
- package/src/cli/cmd/tui/context/kv.tsx +49 -0
- package/src/cli/cmd/tui/context/local.tsx +392 -0
- package/src/cli/cmd/tui/context/prompt.tsx +18 -0
- package/src/cli/cmd/tui/context/route.tsx +46 -0
- package/src/cli/cmd/tui/context/sdk.tsx +75 -0
- package/src/cli/cmd/tui/context/sync.tsx +384 -0
- package/src/cli/cmd/tui/context/theme/aura.json +69 -0
- package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
- package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +233 -0
- package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +233 -0
- package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
- package/src/cli/cmd/tui/context/theme/chad.json +253 -0
- package/src/cli/cmd/tui/context/theme/cobalt2.json +228 -0
- package/src/cli/cmd/tui/context/theme/cursor.json +249 -0
- package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
- package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
- package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
- package/src/cli/cmd/tui/context/theme/github.json +233 -0
- package/src/cli/cmd/tui/context/theme/gruvbox.json +95 -0
- package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
- package/src/cli/cmd/tui/context/theme/lucent-orng.json +227 -0
- package/src/cli/cmd/tui/context/theme/material.json +235 -0
- package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
- package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
- package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
- package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
- package/src/cli/cmd/tui/context/theme/nord.json +223 -0
- package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
- package/src/cli/cmd/tui/context/theme/orng.json +245 -0
- package/src/cli/cmd/tui/context/theme/osaka-jade.json +93 -0
- package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
- package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
- package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
- package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
- package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
- package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
- package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
- package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
- package/src/cli/cmd/tui/context/theme.tsx +1137 -0
- package/src/cli/cmd/tui/event.ts +46 -0
- package/src/cli/cmd/tui/routes/home.tsx +138 -0
- package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +64 -0
- package/src/cli/cmd/tui/routes/session/dialog-message.tsx +109 -0
- package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +26 -0
- package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +47 -0
- package/src/cli/cmd/tui/routes/session/footer.tsx +88 -0
- package/src/cli/cmd/tui/routes/session/header.tsx +125 -0
- package/src/cli/cmd/tui/routes/session/index.tsx +1814 -0
- package/src/cli/cmd/tui/routes/session/permission.tsx +416 -0
- package/src/cli/cmd/tui/routes/session/sidebar.tsx +318 -0
- package/src/cli/cmd/tui/spawn.ts +48 -0
- package/src/cli/cmd/tui/thread.ts +111 -0
- package/src/cli/cmd/tui/ui/dialog-alert.tsx +57 -0
- package/src/cli/cmd/tui/ui/dialog-confirm.tsx +83 -0
- package/src/cli/cmd/tui/ui/dialog-export-options.tsx +204 -0
- package/src/cli/cmd/tui/ui/dialog-help.tsx +38 -0
- package/src/cli/cmd/tui/ui/dialog-prompt.tsx +77 -0
- package/src/cli/cmd/tui/ui/dialog-select.tsx +345 -0
- package/src/cli/cmd/tui/ui/dialog.tsx +171 -0
- package/src/cli/cmd/tui/ui/link.tsx +28 -0
- package/src/cli/cmd/tui/ui/spinner.ts +368 -0
- package/src/cli/cmd/tui/ui/toast.tsx +100 -0
- package/src/cli/cmd/tui/util/clipboard.ts +127 -0
- package/src/cli/cmd/tui/util/editor.ts +32 -0
- package/src/cli/cmd/tui/util/signal.ts +7 -0
- package/src/cli/cmd/tui/util/terminal.ts +114 -0
- package/src/cli/cmd/tui/util/transcript.ts +98 -0
- package/src/cli/cmd/tui/worker.ts +68 -0
- package/src/cli/cmd/uninstall.ts +344 -0
- package/src/cli/cmd/upgrade.ts +67 -0
- package/src/cli/cmd/web.ts +73 -0
- package/src/cli/error.ts +56 -0
- package/src/cli/network.ts +53 -0
- package/src/cli/ui.ts +92 -0
- package/src/cli/upgrade.ts +25 -0
- package/src/command/index.ts +131 -0
- package/src/command/template/initialize.txt +10 -0
- package/src/command/template/review.txt +97 -0
- package/src/config/config.ts +1124 -0
- package/src/config/markdown.ts +41 -0
- package/src/env/index.ts +26 -0
- package/src/file/ignore.ts +83 -0
- package/src/file/index.ts +411 -0
- package/src/file/ripgrep.ts +402 -0
- package/src/file/time.ts +64 -0
- package/src/file/watcher.ts +117 -0
- package/src/flag/flag.ts +52 -0
- package/src/format/formatter.ts +359 -0
- package/src/format/index.ts +137 -0
- package/src/global/index.ts +55 -0
- package/src/id/id.ts +73 -0
- package/src/ide/index.ts +77 -0
- package/src/index.ts +159 -0
- package/src/installation/index.ts +206 -0
- package/src/lsp/client.ts +252 -0
- package/src/lsp/index.ts +485 -0
- package/src/lsp/language.ts +119 -0
- package/src/lsp/server.ts +2023 -0
- package/src/mcp/auth.ts +135 -0
- package/src/mcp/index.ts +874 -0
- package/src/mcp/oauth-callback.ts +200 -0
- package/src/mcp/oauth-provider.ts +154 -0
- package/src/patch/index.ts +622 -0
- package/src/permission/arity.ts +163 -0
- package/src/permission/index.ts +210 -0
- package/src/permission/next.ts +268 -0
- package/src/plugin/index.ts +106 -0
- package/src/project/bootstrap.ts +31 -0
- package/src/project/instance.ts +78 -0
- package/src/project/project.ts +263 -0
- package/src/project/state.ts +65 -0
- package/src/project/vcs.ts +76 -0
- package/src/provider/auth.ts +143 -0
- package/src/provider/models-macro.ts +4 -0
- package/src/provider/models.ts +77 -0
- package/src/provider/provider.ts +516 -0
- package/src/provider/transform.ts +114 -0
- package/src/pty/index.ts +212 -0
- package/src/server/error.ts +36 -0
- package/src/server/mdns.ts +57 -0
- package/src/server/project.ts +79 -0
- package/src/server/server.ts +2866 -0
- package/src/server/tui.ts +71 -0
- package/src/session/compaction.ts +225 -0
- package/src/session/index.ts +469 -0
- package/src/session/llm.ts +213 -0
- package/src/session/message-v2.ts +742 -0
- package/src/session/message.ts +189 -0
- package/src/session/processor.ts +402 -0
- package/src/session/prompt/anthropic-20250930.txt +166 -0
- package/src/session/prompt/anthropic.txt +105 -0
- package/src/session/prompt/anthropic_spoof.txt +1 -0
- package/src/session/prompt/beast.txt +147 -0
- package/src/session/prompt/build-switch.txt +5 -0
- package/src/session/prompt/codex.txt +318 -0
- package/src/session/prompt/copilot-gpt-5.txt +143 -0
- package/src/session/prompt/gemini.txt +155 -0
- package/src/session/prompt/max-steps.txt +16 -0
- package/src/session/prompt/plan-reminder-anthropic.txt +67 -0
- package/src/session/prompt/plan.txt +26 -0
- package/src/session/prompt/qwen.txt +109 -0
- package/src/session/prompt.ts +1621 -0
- package/src/session/retry.ts +90 -0
- package/src/session/revert.ts +108 -0
- package/src/session/status.ts +76 -0
- package/src/session/summary.ts +194 -0
- package/src/session/system.ts +108 -0
- package/src/session/todo.ts +37 -0
- package/src/share/share-next.ts +194 -0
- package/src/share/share.ts +23 -0
- package/src/shell/shell.ts +67 -0
- package/src/skill/index.ts +1 -0
- package/src/skill/skill.ts +124 -0
- package/src/snapshot/index.ts +197 -0
- package/src/storage/storage.ts +226 -0
- package/src/tool/bash.ts +262 -0
- package/src/tool/bash.txt +116 -0
- package/src/tool/batch.ts +175 -0
- package/src/tool/batch.txt +24 -0
- package/src/tool/codesearch.ts +132 -0
- package/src/tool/codesearch.txt +12 -0
- package/src/tool/edit.ts +655 -0
- package/src/tool/edit.txt +10 -0
- package/src/tool/glob.ts +75 -0
- package/src/tool/glob.txt +6 -0
- package/src/tool/grep.ts +132 -0
- package/src/tool/grep.txt +8 -0
- package/src/tool/invalid.ts +17 -0
- package/src/tool/ls.ts +119 -0
- package/src/tool/ls.txt +1 -0
- package/src/tool/lsp.ts +94 -0
- package/src/tool/lsp.txt +19 -0
- package/src/tool/multiedit.ts +46 -0
- package/src/tool/multiedit.txt +41 -0
- package/src/tool/patch.ts +210 -0
- package/src/tool/patch.txt +1 -0
- package/src/tool/read.ts +191 -0
- package/src/tool/read.txt +12 -0
- package/src/tool/registry.ts +137 -0
- package/src/tool/skill.ts +77 -0
- package/src/tool/task.ts +167 -0
- package/src/tool/task.txt +60 -0
- package/src/tool/todo.ts +53 -0
- package/src/tool/todoread.txt +14 -0
- package/src/tool/todowrite.txt +167 -0
- package/src/tool/tool.ts +73 -0
- package/src/tool/webfetch.ts +182 -0
- package/src/tool/webfetch.txt +13 -0
- package/src/tool/websearch.ts +144 -0
- package/src/tool/websearch.txt +11 -0
- package/src/tool/write.ts +84 -0
- package/src/tool/write.txt +8 -0
- package/src/util/archive.ts +16 -0
- package/src/util/color.ts +19 -0
- package/src/util/context.ts +25 -0
- package/src/util/defer.ts +12 -0
- package/src/util/eventloop.ts +20 -0
- package/src/util/filesystem.ts +83 -0
- package/src/util/fn.ts +11 -0
- package/src/util/iife.ts +3 -0
- package/src/util/keybind.ts +102 -0
- package/src/util/lazy.ts +18 -0
- package/src/util/locale.ts +81 -0
- package/src/util/lock.ts +98 -0
- package/src/util/log.ts +180 -0
- package/src/util/queue.ts +32 -0
- package/src/util/rpc.ts +42 -0
- package/src/util/scrap.ts +10 -0
- package/src/util/signal.ts +12 -0
- package/src/util/timeout.ts +14 -0
- package/src/util/token.ts +7 -0
- package/src/util/wildcard.ts +54 -0
- package/src/worktree/index.ts +217 -0
- package/sst-env.d.ts +9 -0
- package/test/agent/agent.test.ts +448 -0
- package/test/bun.test.ts +53 -0
- package/test/cli/github-action.test.ts +129 -0
- package/test/cli/github-remote.test.ts +80 -0
- package/test/cli/tui/transcript.test.ts +297 -0
- package/test/config/agent-color.test.ts +66 -0
- package/test/config/config.test.ts +870 -0
- package/test/config/markdown.test.ts +89 -0
- package/test/file/ignore.test.ts +10 -0
- package/test/file/path-traversal.test.ts +115 -0
- package/test/fixture/fixture.ts +45 -0
- package/test/fixture/lsp/fake-lsp-server.js +77 -0
- package/test/ide/ide.test.ts +82 -0
- package/test/keybind.test.ts +421 -0
- package/test/lsp/client.test.ts +95 -0
- package/test/mcp/headers.test.ts +153 -0
- package/test/patch/patch.test.ts +348 -0
- package/test/permission/arity.test.ts +33 -0
- package/test/permission/next.test.ts +652 -0
- package/test/preload.ts +63 -0
- package/test/project/project.test.ts +120 -0
- package/test/provider/amazon-bedrock.test.ts +236 -0
- package/test/provider/provider.test.ts +2127 -0
- package/test/provider/transform.test.ts +980 -0
- package/test/server/session-select.test.ts +78 -0
- package/test/session/compaction.test.ts +251 -0
- package/test/session/message-v2.test.ts +570 -0
- package/test/session/retry.test.ts +131 -0
- package/test/session/revert-compact.test.ts +285 -0
- package/test/session/session.test.ts +71 -0
- package/test/skill/skill.test.ts +185 -0
- package/test/snapshot/snapshot.test.ts +939 -0
- package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
- package/test/tool/bash.test.ts +232 -0
- package/test/tool/grep.test.ts +109 -0
- package/test/tool/patch.test.ts +261 -0
- package/test/tool/read.test.ts +167 -0
- package/test/util/iife.test.ts +36 -0
- package/test/util/lazy.test.ts +50 -0
- package/test/util/timeout.test.ts +21 -0
- package/test/util/wildcard.test.ts +55 -0
- package/tsconfig.json +16 -0
- /package/{postinstall.mjs → script/postinstall.mjs} +0 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { Bus } from "@/bus"
|
|
2
|
+
import { Config } from "@/config/config"
|
|
3
|
+
import { ulid } from "ulid"
|
|
4
|
+
import { Provider } from "@/provider/provider"
|
|
5
|
+
import { Session } from "@/session"
|
|
6
|
+
import { MessageV2 } from "@/session/message-v2"
|
|
7
|
+
import { Storage } from "@/storage/storage"
|
|
8
|
+
import { Log } from "@/util/log"
|
|
9
|
+
import type * as SDK from "@opencode-ai/sdk/v2"
|
|
10
|
+
|
|
11
|
+
export namespace ShareNext {
|
|
12
|
+
const log = Log.create({ service: "share-next" })
|
|
13
|
+
|
|
14
|
+
async function url() {
|
|
15
|
+
return Config.get().then((x) => x.enterprise?.url ?? "https://opncd.ai")
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function init() {
|
|
19
|
+
Bus.subscribe(Session.Event.Updated, async (evt) => {
|
|
20
|
+
await sync(evt.properties.info.id, [
|
|
21
|
+
{
|
|
22
|
+
type: "session",
|
|
23
|
+
data: evt.properties.info,
|
|
24
|
+
},
|
|
25
|
+
])
|
|
26
|
+
})
|
|
27
|
+
Bus.subscribe(MessageV2.Event.Updated, async (evt) => {
|
|
28
|
+
await sync(evt.properties.info.sessionID, [
|
|
29
|
+
{
|
|
30
|
+
type: "message",
|
|
31
|
+
data: evt.properties.info,
|
|
32
|
+
},
|
|
33
|
+
])
|
|
34
|
+
if (evt.properties.info.role === "user") {
|
|
35
|
+
await sync(evt.properties.info.sessionID, [
|
|
36
|
+
{
|
|
37
|
+
type: "model",
|
|
38
|
+
data: [
|
|
39
|
+
await Provider.getModel(evt.properties.info.model.providerID, evt.properties.info.model.modelID).then(
|
|
40
|
+
(m) => m,
|
|
41
|
+
),
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
])
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
|
|
48
|
+
await sync(evt.properties.part.sessionID, [
|
|
49
|
+
{
|
|
50
|
+
type: "part",
|
|
51
|
+
data: evt.properties.part,
|
|
52
|
+
},
|
|
53
|
+
])
|
|
54
|
+
})
|
|
55
|
+
Bus.subscribe(Session.Event.Diff, async (evt) => {
|
|
56
|
+
await sync(evt.properties.sessionID, [
|
|
57
|
+
{
|
|
58
|
+
type: "session_diff",
|
|
59
|
+
data: evt.properties.diff,
|
|
60
|
+
},
|
|
61
|
+
])
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function create(sessionID: string) {
|
|
66
|
+
log.info("creating share", { sessionID })
|
|
67
|
+
const result = await fetch(`${await url()}/api/share`, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: {
|
|
70
|
+
"Content-Type": "application/json",
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify({ sessionID: sessionID }),
|
|
73
|
+
})
|
|
74
|
+
.then((x) => x.json())
|
|
75
|
+
.then((x) => x as { id: string; url: string; secret: string })
|
|
76
|
+
await Storage.write(["session_share", sessionID], result)
|
|
77
|
+
fullSync(sessionID)
|
|
78
|
+
return result
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function get(sessionID: string) {
|
|
82
|
+
return Storage.read<{
|
|
83
|
+
id: string
|
|
84
|
+
secret: string
|
|
85
|
+
url: string
|
|
86
|
+
}>(["session_share", sessionID])
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
type Data =
|
|
90
|
+
| {
|
|
91
|
+
type: "session"
|
|
92
|
+
data: SDK.Session
|
|
93
|
+
}
|
|
94
|
+
| {
|
|
95
|
+
type: "message"
|
|
96
|
+
data: SDK.Message
|
|
97
|
+
}
|
|
98
|
+
| {
|
|
99
|
+
type: "part"
|
|
100
|
+
data: SDK.Part
|
|
101
|
+
}
|
|
102
|
+
| {
|
|
103
|
+
type: "session_diff"
|
|
104
|
+
data: SDK.FileDiff[]
|
|
105
|
+
}
|
|
106
|
+
| {
|
|
107
|
+
type: "model"
|
|
108
|
+
data: SDK.Model[]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const queue = new Map<string, { timeout: NodeJS.Timeout; data: Map<string, Data> }>()
|
|
112
|
+
async function sync(sessionID: string, data: Data[]) {
|
|
113
|
+
const existing = queue.get(sessionID)
|
|
114
|
+
if (existing) {
|
|
115
|
+
for (const item of data) {
|
|
116
|
+
existing.data.set("id" in item ? (item.id as string) : ulid(), item)
|
|
117
|
+
}
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const dataMap = new Map<string, Data>()
|
|
122
|
+
for (const item of data) {
|
|
123
|
+
dataMap.set("id" in item ? (item.id as string) : ulid(), item)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const timeout = setTimeout(async () => {
|
|
127
|
+
const queued = queue.get(sessionID)
|
|
128
|
+
if (!queued) return
|
|
129
|
+
queue.delete(sessionID)
|
|
130
|
+
const share = await get(sessionID).catch(() => undefined)
|
|
131
|
+
if (!share) return
|
|
132
|
+
|
|
133
|
+
await fetch(`${await url()}/api/share/${share.id}/sync`, {
|
|
134
|
+
method: "POST",
|
|
135
|
+
headers: {
|
|
136
|
+
"Content-Type": "application/json",
|
|
137
|
+
},
|
|
138
|
+
body: JSON.stringify({
|
|
139
|
+
secret: share.secret,
|
|
140
|
+
data: Array.from(queued.data.values()),
|
|
141
|
+
}),
|
|
142
|
+
})
|
|
143
|
+
}, 1000)
|
|
144
|
+
queue.set(sessionID, { timeout, data: dataMap })
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function remove(sessionID: string) {
|
|
148
|
+
log.info("removing share", { sessionID })
|
|
149
|
+
const share = await get(sessionID)
|
|
150
|
+
if (!share) return
|
|
151
|
+
await fetch(`${await url()}/api/share/${share.id}`, {
|
|
152
|
+
method: "DELETE",
|
|
153
|
+
headers: {
|
|
154
|
+
"Content-Type": "application/json",
|
|
155
|
+
},
|
|
156
|
+
body: JSON.stringify({
|
|
157
|
+
secret: share.secret,
|
|
158
|
+
}),
|
|
159
|
+
})
|
|
160
|
+
await Storage.remove(["session_share", sessionID])
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function fullSync(sessionID: string) {
|
|
164
|
+
log.info("full sync", { sessionID })
|
|
165
|
+
const session = await Session.get(sessionID)
|
|
166
|
+
const diffs = await Session.diff(sessionID)
|
|
167
|
+
const messages = await Array.fromAsync(MessageV2.stream(sessionID))
|
|
168
|
+
const models = await Promise.all(
|
|
169
|
+
messages
|
|
170
|
+
.filter((m) => m.info.role === "user")
|
|
171
|
+
.map((m) => (m.info as SDK.UserMessage).model)
|
|
172
|
+
.map((m) => Provider.getModel(m.providerID, m.modelID).then((m) => m)),
|
|
173
|
+
)
|
|
174
|
+
await sync(sessionID, [
|
|
175
|
+
{
|
|
176
|
+
type: "session",
|
|
177
|
+
data: session,
|
|
178
|
+
},
|
|
179
|
+
...messages.map((x) => ({
|
|
180
|
+
type: "message" as const,
|
|
181
|
+
data: x.info,
|
|
182
|
+
})),
|
|
183
|
+
...messages.flatMap((x) => x.parts.map((y) => ({ type: "part" as const, data: y }))),
|
|
184
|
+
{
|
|
185
|
+
type: "session_diff",
|
|
186
|
+
data: diffs,
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
type: "model",
|
|
190
|
+
data: models,
|
|
191
|
+
},
|
|
192
|
+
])
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Log } from "../util/log"
|
|
2
|
+
|
|
3
|
+
export namespace Share {
|
|
4
|
+
const log = Log.create({ service: "share" })
|
|
5
|
+
|
|
6
|
+
// Share feature is not available in Chad Code
|
|
7
|
+
export async function sync(_key: string, _content: any) {
|
|
8
|
+
// No-op - share feature not available
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function init() {
|
|
12
|
+
// No-op - share feature not available
|
|
13
|
+
log.info("Share feature is not available in Chad Code")
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function create(_sessionID: string): Promise<{ url: string; secret: string }> {
|
|
17
|
+
throw new Error("Share feature is not available in Chad Code")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function remove(_sessionID: string, _secret: string): Promise<any> {
|
|
21
|
+
throw new Error("Share feature is not available in Chad Code")
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Flag } from "@/flag/flag"
|
|
2
|
+
import { lazy } from "@/util/lazy"
|
|
3
|
+
import path from "path"
|
|
4
|
+
import { spawn, type ChildProcess } from "child_process"
|
|
5
|
+
|
|
6
|
+
const SIGKILL_TIMEOUT_MS = 200
|
|
7
|
+
|
|
8
|
+
export namespace Shell {
|
|
9
|
+
export async function killTree(proc: ChildProcess, opts?: { exited?: () => boolean }): Promise<void> {
|
|
10
|
+
const pid = proc.pid
|
|
11
|
+
if (!pid || opts?.exited?.()) return
|
|
12
|
+
|
|
13
|
+
if (process.platform === "win32") {
|
|
14
|
+
await new Promise<void>((resolve) => {
|
|
15
|
+
const killer = spawn("taskkill", ["/pid", String(pid), "/f", "/t"], { stdio: "ignore" })
|
|
16
|
+
killer.once("exit", () => resolve())
|
|
17
|
+
killer.once("error", () => resolve())
|
|
18
|
+
})
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
process.kill(-pid, "SIGTERM")
|
|
24
|
+
await Bun.sleep(SIGKILL_TIMEOUT_MS)
|
|
25
|
+
if (!opts?.exited?.()) {
|
|
26
|
+
process.kill(-pid, "SIGKILL")
|
|
27
|
+
}
|
|
28
|
+
} catch (_e) {
|
|
29
|
+
proc.kill("SIGTERM")
|
|
30
|
+
await Bun.sleep(SIGKILL_TIMEOUT_MS)
|
|
31
|
+
if (!opts?.exited?.()) {
|
|
32
|
+
proc.kill("SIGKILL")
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const BLACKLIST = new Set(["fish", "nu"])
|
|
37
|
+
|
|
38
|
+
function fallback() {
|
|
39
|
+
if (process.platform === "win32") {
|
|
40
|
+
if (Flag.OPENCODE_GIT_BASH_PATH) return Flag.OPENCODE_GIT_BASH_PATH
|
|
41
|
+
const git = Bun.which("git")
|
|
42
|
+
if (git) {
|
|
43
|
+
// git.exe is typically at: C:\Program Files\Git\cmd\git.exe
|
|
44
|
+
// bash.exe is at: C:\Program Files\Git\bin\bash.exe
|
|
45
|
+
const bash = path.join(git, "..", "..", "bin", "bash.exe")
|
|
46
|
+
if (Bun.file(bash).size) return bash
|
|
47
|
+
}
|
|
48
|
+
return process.env.COMSPEC || "cmd.exe"
|
|
49
|
+
}
|
|
50
|
+
if (process.platform === "darwin") return "/bin/zsh"
|
|
51
|
+
const bash = Bun.which("bash")
|
|
52
|
+
if (bash) return bash
|
|
53
|
+
return "/bin/sh"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const preferred = lazy(() => {
|
|
57
|
+
const s = process.env.SHELL
|
|
58
|
+
if (s) return s
|
|
59
|
+
return fallback()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
export const acceptable = lazy(() => {
|
|
63
|
+
const s = process.env.SHELL
|
|
64
|
+
if (s && !BLACKLIST.has(process.platform === "win32" ? path.win32.basename(s) : path.basename(s))) return s
|
|
65
|
+
return fallback()
|
|
66
|
+
})
|
|
67
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./skill"
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import z from "zod"
|
|
2
|
+
import { Config } from "../config/config"
|
|
3
|
+
import { Instance } from "../project/instance"
|
|
4
|
+
import { NamedError } from "@opencode-ai/util/error"
|
|
5
|
+
import { ConfigMarkdown } from "../config/markdown"
|
|
6
|
+
import { Log } from "../util/log"
|
|
7
|
+
import { Global } from "@/global"
|
|
8
|
+
import { Filesystem } from "@/util/filesystem"
|
|
9
|
+
import { exists } from "fs/promises"
|
|
10
|
+
|
|
11
|
+
export namespace Skill {
|
|
12
|
+
const log = Log.create({ service: "skill" })
|
|
13
|
+
export const Info = z.object({
|
|
14
|
+
name: z.string(),
|
|
15
|
+
description: z.string(),
|
|
16
|
+
location: z.string(),
|
|
17
|
+
})
|
|
18
|
+
export type Info = z.infer<typeof Info>
|
|
19
|
+
|
|
20
|
+
export const InvalidError = NamedError.create(
|
|
21
|
+
"SkillInvalidError",
|
|
22
|
+
z.object({
|
|
23
|
+
path: z.string(),
|
|
24
|
+
message: z.string().optional(),
|
|
25
|
+
issues: z.custom<z.core.$ZodIssue[]>().optional(),
|
|
26
|
+
}),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
export const NameMismatchError = NamedError.create(
|
|
30
|
+
"SkillNameMismatchError",
|
|
31
|
+
z.object({
|
|
32
|
+
path: z.string(),
|
|
33
|
+
expected: z.string(),
|
|
34
|
+
actual: z.string(),
|
|
35
|
+
}),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
const OPENCODE_SKILL_GLOB = new Bun.Glob("{skill,skills}/**/SKILL.md")
|
|
39
|
+
const CLAUDE_SKILL_GLOB = new Bun.Glob("skills/**/SKILL.md")
|
|
40
|
+
|
|
41
|
+
export const state = Instance.state(async () => {
|
|
42
|
+
const skills: Record<string, Info> = {}
|
|
43
|
+
|
|
44
|
+
const addSkill = async (match: string) => {
|
|
45
|
+
const md = await ConfigMarkdown.parse(match)
|
|
46
|
+
if (!md) {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const parsed = Info.pick({ name: true, description: true }).safeParse(md.data)
|
|
51
|
+
if (!parsed.success) return
|
|
52
|
+
|
|
53
|
+
// Warn on duplicate skill names
|
|
54
|
+
if (skills[parsed.data.name]) {
|
|
55
|
+
log.warn("duplicate skill name", {
|
|
56
|
+
name: parsed.data.name,
|
|
57
|
+
existing: skills[parsed.data.name].location,
|
|
58
|
+
duplicate: match,
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
skills[parsed.data.name] = {
|
|
63
|
+
name: parsed.data.name,
|
|
64
|
+
description: parsed.data.description,
|
|
65
|
+
location: match,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Scan .claude/skills/ directories (project-level)
|
|
70
|
+
const claudeDirs = await Array.fromAsync(
|
|
71
|
+
Filesystem.up({
|
|
72
|
+
targets: [".claude"],
|
|
73
|
+
start: Instance.directory,
|
|
74
|
+
stop: Instance.worktree,
|
|
75
|
+
}),
|
|
76
|
+
)
|
|
77
|
+
// Also include global ~/.claude/skills/
|
|
78
|
+
const globalClaude = `${Global.Path.home}/.claude`
|
|
79
|
+
if (await exists(globalClaude)) {
|
|
80
|
+
claudeDirs.push(globalClaude)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const dir of claudeDirs) {
|
|
84
|
+
const matches = await Array.fromAsync(
|
|
85
|
+
CLAUDE_SKILL_GLOB.scan({
|
|
86
|
+
cwd: dir,
|
|
87
|
+
absolute: true,
|
|
88
|
+
onlyFiles: true,
|
|
89
|
+
followSymlinks: true,
|
|
90
|
+
dot: true,
|
|
91
|
+
}),
|
|
92
|
+
).catch((error) => {
|
|
93
|
+
log.error("failed .claude directory scan for skills", { dir, error })
|
|
94
|
+
return []
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
for (const match of matches) {
|
|
98
|
+
await addSkill(match)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Scan .opencode/skill/ directories
|
|
103
|
+
for (const dir of await Config.directories()) {
|
|
104
|
+
for await (const match of OPENCODE_SKILL_GLOB.scan({
|
|
105
|
+
cwd: dir,
|
|
106
|
+
absolute: true,
|
|
107
|
+
onlyFiles: true,
|
|
108
|
+
followSymlinks: true,
|
|
109
|
+
})) {
|
|
110
|
+
await addSkill(match)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return skills
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
export async function get(name: string) {
|
|
118
|
+
return state().then((x) => x[name])
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function all() {
|
|
122
|
+
return state().then((x) => Object.values(x))
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { $ } from "bun"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import fs from "fs/promises"
|
|
4
|
+
import { Log } from "../util/log"
|
|
5
|
+
import { Global } from "../global"
|
|
6
|
+
import z from "zod"
|
|
7
|
+
import { Config } from "../config/config"
|
|
8
|
+
import { Instance } from "../project/instance"
|
|
9
|
+
|
|
10
|
+
export namespace Snapshot {
|
|
11
|
+
const log = Log.create({ service: "snapshot" })
|
|
12
|
+
|
|
13
|
+
export async function track() {
|
|
14
|
+
if (Instance.project.vcs !== "git") return
|
|
15
|
+
const cfg = await Config.get()
|
|
16
|
+
if (cfg.snapshot === false) return
|
|
17
|
+
const git = gitdir()
|
|
18
|
+
if (await fs.mkdir(git, { recursive: true })) {
|
|
19
|
+
await $`git init`
|
|
20
|
+
.env({
|
|
21
|
+
...process.env,
|
|
22
|
+
GIT_DIR: git,
|
|
23
|
+
GIT_WORK_TREE: Instance.worktree,
|
|
24
|
+
})
|
|
25
|
+
.quiet()
|
|
26
|
+
.nothrow()
|
|
27
|
+
// Configure git to not convert line endings on Windows
|
|
28
|
+
await $`git --git-dir ${git} config core.autocrlf false`.quiet().nothrow()
|
|
29
|
+
log.info("initialized")
|
|
30
|
+
}
|
|
31
|
+
await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`.quiet().cwd(Instance.directory).nothrow()
|
|
32
|
+
const hash = await $`git --git-dir ${git} --work-tree ${Instance.worktree} write-tree`
|
|
33
|
+
.quiet()
|
|
34
|
+
.cwd(Instance.directory)
|
|
35
|
+
.nothrow()
|
|
36
|
+
.text()
|
|
37
|
+
log.info("tracking", { hash, cwd: Instance.directory, git })
|
|
38
|
+
return hash.trim()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const Patch = z.object({
|
|
42
|
+
hash: z.string(),
|
|
43
|
+
files: z.string().array(),
|
|
44
|
+
})
|
|
45
|
+
export type Patch = z.infer<typeof Patch>
|
|
46
|
+
|
|
47
|
+
export async function patch(hash: string): Promise<Patch> {
|
|
48
|
+
const git = gitdir()
|
|
49
|
+
await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`.quiet().cwd(Instance.directory).nothrow()
|
|
50
|
+
const result =
|
|
51
|
+
await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --name-only ${hash} -- .`
|
|
52
|
+
.quiet()
|
|
53
|
+
.cwd(Instance.directory)
|
|
54
|
+
.nothrow()
|
|
55
|
+
|
|
56
|
+
// If git diff fails, return empty patch
|
|
57
|
+
if (result.exitCode !== 0) {
|
|
58
|
+
log.warn("failed to get diff", { hash, exitCode: result.exitCode })
|
|
59
|
+
return { hash, files: [] }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const files = result.text()
|
|
63
|
+
return {
|
|
64
|
+
hash,
|
|
65
|
+
files: files
|
|
66
|
+
.trim()
|
|
67
|
+
.split("\n")
|
|
68
|
+
.map((x) => x.trim())
|
|
69
|
+
.filter(Boolean)
|
|
70
|
+
.map((x) => path.join(Instance.worktree, x)),
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function restore(snapshot: string) {
|
|
75
|
+
log.info("restore", { commit: snapshot })
|
|
76
|
+
const git = gitdir()
|
|
77
|
+
const result =
|
|
78
|
+
await $`git --git-dir ${git} --work-tree ${Instance.worktree} read-tree ${snapshot} && git --git-dir ${git} --work-tree ${Instance.worktree} checkout-index -a -f`
|
|
79
|
+
.quiet()
|
|
80
|
+
.cwd(Instance.worktree)
|
|
81
|
+
.nothrow()
|
|
82
|
+
|
|
83
|
+
if (result.exitCode !== 0) {
|
|
84
|
+
log.error("failed to restore snapshot", {
|
|
85
|
+
snapshot,
|
|
86
|
+
exitCode: result.exitCode,
|
|
87
|
+
stderr: result.stderr.toString(),
|
|
88
|
+
stdout: result.stdout.toString(),
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function revert(patches: Patch[]) {
|
|
94
|
+
const files = new Set<string>()
|
|
95
|
+
const git = gitdir()
|
|
96
|
+
for (const item of patches) {
|
|
97
|
+
for (const file of item.files) {
|
|
98
|
+
if (files.has(file)) continue
|
|
99
|
+
log.info("reverting", { file, hash: item.hash })
|
|
100
|
+
const result = await $`git --git-dir ${git} --work-tree ${Instance.worktree} checkout ${item.hash} -- ${file}`
|
|
101
|
+
.quiet()
|
|
102
|
+
.cwd(Instance.worktree)
|
|
103
|
+
.nothrow()
|
|
104
|
+
if (result.exitCode !== 0) {
|
|
105
|
+
const relativePath = path.relative(Instance.worktree, file)
|
|
106
|
+
const checkTree =
|
|
107
|
+
await $`git --git-dir ${git} --work-tree ${Instance.worktree} ls-tree ${item.hash} -- ${relativePath}`
|
|
108
|
+
.quiet()
|
|
109
|
+
.cwd(Instance.worktree)
|
|
110
|
+
.nothrow()
|
|
111
|
+
if (checkTree.exitCode === 0 && checkTree.text().trim()) {
|
|
112
|
+
log.info("file existed in snapshot but checkout failed, keeping", {
|
|
113
|
+
file,
|
|
114
|
+
})
|
|
115
|
+
} else {
|
|
116
|
+
log.info("file did not exist in snapshot, deleting", { file })
|
|
117
|
+
await fs.unlink(file).catch(() => {})
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
files.add(file)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function diff(hash: string) {
|
|
126
|
+
const git = gitdir()
|
|
127
|
+
await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`.quiet().cwd(Instance.directory).nothrow()
|
|
128
|
+
const result =
|
|
129
|
+
await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff ${hash} -- .`
|
|
130
|
+
.quiet()
|
|
131
|
+
.cwd(Instance.worktree)
|
|
132
|
+
.nothrow()
|
|
133
|
+
|
|
134
|
+
if (result.exitCode !== 0) {
|
|
135
|
+
log.warn("failed to get diff", {
|
|
136
|
+
hash,
|
|
137
|
+
exitCode: result.exitCode,
|
|
138
|
+
stderr: result.stderr.toString(),
|
|
139
|
+
stdout: result.stdout.toString(),
|
|
140
|
+
})
|
|
141
|
+
return ""
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return result.text().trim()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export const FileDiff = z
|
|
148
|
+
.object({
|
|
149
|
+
file: z.string(),
|
|
150
|
+
before: z.string(),
|
|
151
|
+
after: z.string(),
|
|
152
|
+
additions: z.number(),
|
|
153
|
+
deletions: z.number(),
|
|
154
|
+
})
|
|
155
|
+
.meta({
|
|
156
|
+
ref: "FileDiff",
|
|
157
|
+
})
|
|
158
|
+
export type FileDiff = z.infer<typeof FileDiff>
|
|
159
|
+
export async function diffFull(from: string, to: string): Promise<FileDiff[]> {
|
|
160
|
+
const git = gitdir()
|
|
161
|
+
const result: FileDiff[] = []
|
|
162
|
+
for await (const line of $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --no-renames --numstat ${from} ${to} -- .`
|
|
163
|
+
.quiet()
|
|
164
|
+
.cwd(Instance.directory)
|
|
165
|
+
.nothrow()
|
|
166
|
+
.lines()) {
|
|
167
|
+
if (!line) continue
|
|
168
|
+
const [additions, deletions, file] = line.split("\t")
|
|
169
|
+
const isBinaryFile = additions === "-" && deletions === "-"
|
|
170
|
+
const before = isBinaryFile
|
|
171
|
+
? ""
|
|
172
|
+
: await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} show ${from}:${file}`
|
|
173
|
+
.quiet()
|
|
174
|
+
.nothrow()
|
|
175
|
+
.text()
|
|
176
|
+
const after = isBinaryFile
|
|
177
|
+
? ""
|
|
178
|
+
: await $`git -c core.autocrlf=false --git-dir ${git} --work-tree ${Instance.worktree} show ${to}:${file}`
|
|
179
|
+
.quiet()
|
|
180
|
+
.nothrow()
|
|
181
|
+
.text()
|
|
182
|
+
result.push({
|
|
183
|
+
file,
|
|
184
|
+
before,
|
|
185
|
+
after,
|
|
186
|
+
additions: parseInt(additions),
|
|
187
|
+
deletions: parseInt(deletions),
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
return result
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function gitdir() {
|
|
194
|
+
const project = Instance.project
|
|
195
|
+
return path.join(Global.Path.data, "snapshot", project.id)
|
|
196
|
+
}
|
|
197
|
+
}
|