@veris-ai/daytona-opencode 0.1.0
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/.opencode/plugin/daytona/core/logger.d.ts +15 -0
- package/.opencode/plugin/daytona/core/logger.js +65 -0
- package/.opencode/plugin/daytona/core/logger.js.map +1 -0
- package/.opencode/plugin/daytona/core/logger.ts +71 -0
- package/.opencode/plugin/daytona/core/project-data-storage.d.ts +63 -0
- package/.opencode/plugin/daytona/core/project-data-storage.js +213 -0
- package/.opencode/plugin/daytona/core/project-data-storage.js.map +1 -0
- package/.opencode/plugin/daytona/core/project-data-storage.ts +234 -0
- package/.opencode/plugin/daytona/core/session-manager.d.ts +74 -0
- package/.opencode/plugin/daytona/core/session-manager.js +441 -0
- package/.opencode/plugin/daytona/core/session-manager.js.map +1 -0
- package/.opencode/plugin/daytona/core/session-manager.ts +471 -0
- package/.opencode/plugin/daytona/core/toast.d.ts +47 -0
- package/.opencode/plugin/daytona/core/toast.js +70 -0
- package/.opencode/plugin/daytona/core/toast.js.map +1 -0
- package/.opencode/plugin/daytona/core/toast.ts +96 -0
- package/.opencode/plugin/daytona/core/types.d.ts +53 -0
- package/.opencode/plugin/daytona/core/types.js +12 -0
- package/.opencode/plugin/daytona/core/types.js.map +1 -0
- package/.opencode/plugin/daytona/core/types.ts +72 -0
- package/.opencode/plugin/daytona/git/host-git-manager.d.ts +58 -0
- package/.opencode/plugin/daytona/git/host-git-manager.js +342 -0
- package/.opencode/plugin/daytona/git/host-git-manager.js.map +1 -0
- package/.opencode/plugin/daytona/git/host-git-manager.ts +372 -0
- package/.opencode/plugin/daytona/git/index.d.ts +5 -0
- package/.opencode/plugin/daytona/git/index.js +6 -0
- package/.opencode/plugin/daytona/git/index.js.map +1 -0
- package/.opencode/plugin/daytona/git/index.ts +6 -0
- package/.opencode/plugin/daytona/git/sandbox-git-manager.d.ts +22 -0
- package/.opencode/plugin/daytona/git/sandbox-git-manager.js +80 -0
- package/.opencode/plugin/daytona/git/sandbox-git-manager.js.map +1 -0
- package/.opencode/plugin/daytona/git/sandbox-git-manager.ts +92 -0
- package/.opencode/plugin/daytona/git/session-git-manager.d.ts +54 -0
- package/.opencode/plugin/daytona/git/session-git-manager.js +184 -0
- package/.opencode/plugin/daytona/git/session-git-manager.js.map +1 -0
- package/.opencode/plugin/daytona/git/session-git-manager.ts +203 -0
- package/.opencode/plugin/daytona/index.d.ts +125 -0
- package/.opencode/plugin/daytona/index.js +60 -0
- package/.opencode/plugin/daytona/index.js.map +1 -0
- package/.opencode/plugin/daytona/index.ts +73 -0
- package/.opencode/plugin/daytona/plugins/custom-tools.d.ts +122 -0
- package/.opencode/plugin/daytona/plugins/custom-tools.js +18 -0
- package/.opencode/plugin/daytona/plugins/custom-tools.js.map +1 -0
- package/.opencode/plugin/daytona/plugins/custom-tools.ts +21 -0
- package/.opencode/plugin/daytona/plugins/session-events.d.ts +10 -0
- package/.opencode/plugin/daytona/plugins/session-events.js +63 -0
- package/.opencode/plugin/daytona/plugins/session-events.js.map +1 -0
- package/.opencode/plugin/daytona/plugins/session-events.ts +61 -0
- package/.opencode/plugin/daytona/plugins/system-transform.d.ts +10 -0
- package/.opencode/plugin/daytona/plugins/system-transform.js +38 -0
- package/.opencode/plugin/daytona/plugins/system-transform.js.map +1 -0
- package/.opencode/plugin/daytona/plugins/system-transform.ts +43 -0
- package/.opencode/plugin/daytona/tools/bash.d.ts +19 -0
- package/.opencode/plugin/daytona/tools/bash.js +42 -0
- package/.opencode/plugin/daytona/tools/bash.js.map +1 -0
- package/.opencode/plugin/daytona/tools/bash.ts +51 -0
- package/.opencode/plugin/daytona/tools/edit.d.ts +21 -0
- package/.opencode/plugin/daytona/tools/edit.js +33 -0
- package/.opencode/plugin/daytona/tools/edit.js.map +1 -0
- package/.opencode/plugin/daytona/tools/edit.ts +44 -0
- package/.opencode/plugin/daytona/tools/get-preview-url.d.ts +17 -0
- package/.opencode/plugin/daytona/tools/get-preview-url.js +17 -0
- package/.opencode/plugin/daytona/tools/get-preview-url.js.map +1 -0
- package/.opencode/plugin/daytona/tools/get-preview-url.ts +26 -0
- package/.opencode/plugin/daytona/tools/git-sync.d.ts +12 -0
- package/.opencode/plugin/daytona/tools/git-sync.js +26 -0
- package/.opencode/plugin/daytona/tools/git-sync.js.map +1 -0
- package/.opencode/plugin/daytona/tools/git-sync.ts +36 -0
- package/.opencode/plugin/daytona/tools/glob.d.ts +17 -0
- package/.opencode/plugin/daytona/tools/glob.js +21 -0
- package/.opencode/plugin/daytona/tools/glob.js.map +1 -0
- package/.opencode/plugin/daytona/tools/glob.ts +30 -0
- package/.opencode/plugin/daytona/tools/grep.d.ts +17 -0
- package/.opencode/plugin/daytona/tools/grep.js +29 -0
- package/.opencode/plugin/daytona/tools/grep.js.map +1 -0
- package/.opencode/plugin/daytona/tools/grep.ts +39 -0
- package/.opencode/plugin/daytona/tools/ls.d.ts +17 -0
- package/.opencode/plugin/daytona/tools/ls.js +22 -0
- package/.opencode/plugin/daytona/tools/ls.js.map +1 -0
- package/.opencode/plugin/daytona/tools/ls.ts +32 -0
- package/.opencode/plugin/daytona/tools/multiedit.d.ts +25 -0
- package/.opencode/plugin/daytona/tools/multiedit.js +37 -0
- package/.opencode/plugin/daytona/tools/multiedit.js.map +1 -0
- package/.opencode/plugin/daytona/tools/multiedit.ts +54 -0
- package/.opencode/plugin/daytona/tools/read.d.ts +17 -0
- package/.opencode/plugin/daytona/tools/read.js +18 -0
- package/.opencode/plugin/daytona/tools/read.js.map +1 -0
- package/.opencode/plugin/daytona/tools/read.ts +27 -0
- package/.opencode/plugin/daytona/tools/veris-receipt.d.ts +26 -0
- package/.opencode/plugin/daytona/tools/veris-receipt.js +76 -0
- package/.opencode/plugin/daytona/tools/veris-receipt.js.map +1 -0
- package/.opencode/plugin/daytona/tools/veris-receipt.ts +103 -0
- package/.opencode/plugin/daytona/tools/write.d.ts +19 -0
- package/.opencode/plugin/daytona/tools/write.js +18 -0
- package/.opencode/plugin/daytona/tools/write.js.map +1 -0
- package/.opencode/plugin/daytona/tools/write.ts +27 -0
- package/.opencode/plugin/daytona/tools.d.ts +119 -0
- package/.opencode/plugin/daytona/tools.js +35 -0
- package/.opencode/plugin/daytona/tools.js.map +1 -0
- package/.opencode/plugin/daytona/tools.ts +45 -0
- package/.opencode/plugin/index.d.ts +9 -0
- package/.opencode/plugin/index.js +10 -0
- package/.opencode/plugin/index.js.map +1 -0
- package/.opencode/plugin/index.ts +11 -0
- package/README.md +87 -0
- package/package.json +52 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Daytona Platforms Inc.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { logger } from '../core/logger'
|
|
7
|
+
import { spawnSync } from 'child_process'
|
|
8
|
+
import { realpathSync } from 'fs'
|
|
9
|
+
import { isAbsolute, resolve as pathResolve } from 'path'
|
|
10
|
+
|
|
11
|
+
type ExecResult = {
|
|
12
|
+
ok: boolean
|
|
13
|
+
stdout: string
|
|
14
|
+
stderr: string
|
|
15
|
+
status: number | null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type ExecOptions = {
|
|
19
|
+
cwd?: string
|
|
20
|
+
env?: NodeJS.ProcessEnv
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Runs git with an explicit argument vector and no shell, so interpolated values
|
|
24
|
+
// (SSH URLs, refs, remote names) can never be interpreted as shell syntax.
|
|
25
|
+
function execGit(args: string[], options: ExecOptions = {}): ExecResult {
|
|
26
|
+
const res = spawnSync('git', args, {
|
|
27
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
28
|
+
encoding: 'utf8',
|
|
29
|
+
...options,
|
|
30
|
+
})
|
|
31
|
+
if (res.error) {
|
|
32
|
+
return { ok: false, stdout: '', stderr: res.error.message, status: null }
|
|
33
|
+
}
|
|
34
|
+
const status = typeof res.status === 'number' ? res.status : null
|
|
35
|
+
return { ok: status === 0, stdout: res.stdout ?? '', stderr: res.stderr ?? '', status }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// POSIX sh single-quoting (close, escape, reopen): GIT_SSH_COMMAND is parsed by the
|
|
39
|
+
// shell, so an unquoted known_hosts path containing spaces would split into ssh args.
|
|
40
|
+
function shellQuote(value: string): string {
|
|
41
|
+
return `'${value.replace(/'/g, `'\\''`)}'`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// When DAYTONA_SSH_KNOWN_HOSTS names a known_hosts file with the ssh.app.daytona.io
|
|
45
|
+
// host keys, pin host verification to it for sandbox git transfers. This lets the
|
|
46
|
+
// first noninteractive sync succeed without trust-on-first-use, and without changing
|
|
47
|
+
// SSH behavior for any other remote. Unset means inherited SSH behavior, as before.
|
|
48
|
+
//
|
|
49
|
+
// The value crosses two parsers. sh splits GIT_SSH_COMMAND into ssh's argv (outer
|
|
50
|
+
// single quotes), then OpenSSH splits the UserKnownHostsFile VALUE on whitespace as
|
|
51
|
+
// a file list (inner double quotes keep a spaced path as one file). OpenSSH's config
|
|
52
|
+
// grammar has no escape for a literal double quote inside a quoted value, so such
|
|
53
|
+
// paths are rejected instead of being silently mis-pinned.
|
|
54
|
+
function networkEnv(): NodeJS.ProcessEnv | undefined {
|
|
55
|
+
const knownHosts = process.env.DAYTONA_SSH_KNOWN_HOSTS?.trim()
|
|
56
|
+
if (!knownHosts) return undefined
|
|
57
|
+
if (knownHosts.includes('"')) {
|
|
58
|
+
throw new Error('DAYTONA_SSH_KNOWN_HOSTS must not contain a double quote (") character')
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
...process.env,
|
|
62
|
+
// GlobalKnownHostsFile=/dev/null: otherwise a matching entry in the system-wide
|
|
63
|
+
// /etc/ssh/ssh_known_hosts would also be accepted and verification would not be
|
|
64
|
+
// pinned to the configured file alone.
|
|
65
|
+
GIT_SSH_COMMAND: `ssh -o ${shellQuote(`UserKnownHostsFile="${knownHosts}"`)} -o GlobalKnownHostsFile=/dev/null -o StrictHostKeyChecking=yes`,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class HostGitManager {
|
|
70
|
+
// Per-repo serialization: one queue per git-common-dir. Linked worktrees (git worktree
|
|
71
|
+
// add) of the same repo share `.git/config` and refs, so they must share a queue to
|
|
72
|
+
// avoid racing on `remote add/remove` and `.git/config.lock`. Different repos still
|
|
73
|
+
// proceed in parallel.
|
|
74
|
+
private static operationQueues = new Map<string, Promise<void>>()
|
|
75
|
+
|
|
76
|
+
// cwd → queue-key cache. `git rev-parse --git-common-dir` shells out; caching avoids
|
|
77
|
+
// running it on every enqueue. Repo layout doesn't move at runtime, so this is stable.
|
|
78
|
+
private static queueKeyCache = new Map<string, string>()
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Resolve the serialization key for a worktree: the CANONICAL absolute path of its
|
|
82
|
+
* shared git dir (common across linked worktrees of the same repo). Canonicalizing
|
|
83
|
+
* with realpath collapses symlinked aliases onto the same key, so two callers
|
|
84
|
+
* reaching the same repo via different symlink paths still share a queue and can't
|
|
85
|
+
* race on `.git/config`. Falls back to (canonical) cwd if git can't identify a repo.
|
|
86
|
+
*/
|
|
87
|
+
private static queueKeyFor(cwd: string): string {
|
|
88
|
+
const cached = HostGitManager.queueKeyCache.get(cwd)
|
|
89
|
+
if (cached) return cached
|
|
90
|
+
const res = execGit(['rev-parse', '--git-common-dir'], { cwd })
|
|
91
|
+
const rawPath = !res.ok
|
|
92
|
+
? cwd
|
|
93
|
+
: (() => {
|
|
94
|
+
const out = res.stdout.trim()
|
|
95
|
+
return isAbsolute(out) ? out : pathResolve(cwd, out)
|
|
96
|
+
})()
|
|
97
|
+
let key: string
|
|
98
|
+
try {
|
|
99
|
+
key = realpathSync(rawPath)
|
|
100
|
+
} catch {
|
|
101
|
+
// Path may have been deleted between git resolution and our stat; use as-is
|
|
102
|
+
key = rawPath
|
|
103
|
+
}
|
|
104
|
+
HostGitManager.queueKeyCache.set(cwd, key)
|
|
105
|
+
return key
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Chain `fn` onto the queue for the repo containing `cwd`. The stored promise is
|
|
110
|
+
* always-resolves so a failure doesn't poison the chain; callers still see errors via
|
|
111
|
+
* `await` on the returned promise. Cleared from the map when the tail settles to avoid
|
|
112
|
+
* leaking entries for repos that are no longer active.
|
|
113
|
+
*/
|
|
114
|
+
private static enqueue<T>(cwd: string, fn: () => Promise<T>): Promise<T> {
|
|
115
|
+
const queueKey = HostGitManager.queueKeyFor(cwd)
|
|
116
|
+
const prev = HostGitManager.operationQueues.get(queueKey) ?? Promise.resolve()
|
|
117
|
+
const operation = prev.then(fn)
|
|
118
|
+
const stored: Promise<void> = operation.then(
|
|
119
|
+
() => undefined,
|
|
120
|
+
() => undefined,
|
|
121
|
+
)
|
|
122
|
+
HostGitManager.operationQueues.set(queueKey, stored)
|
|
123
|
+
stored.then(() => {
|
|
124
|
+
if (HostGitManager.operationQueues.get(queueKey) === stored) {
|
|
125
|
+
HostGitManager.operationQueues.delete(queueKey)
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
return operation
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Cached OID of an empty commit used to reserve branch refs (branches must point at commits, not blobs). */
|
|
132
|
+
private emptyCommitOidCache = new Map<string, string>()
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Checks if a git repository exists in the current directory
|
|
136
|
+
* @returns true if a git repo exists, false otherwise
|
|
137
|
+
*/
|
|
138
|
+
hasRepo(cwd?: string): boolean {
|
|
139
|
+
return execGit(['rev-parse', '--is-inside-work-tree'], cwd ? { cwd } : {}).ok
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Allocates the next available opencode/N branch number by scanning local refs and
|
|
144
|
+
* reserving the chosen number by creating the ref immediately.
|
|
145
|
+
*
|
|
146
|
+
* This avoids relying on OpenCode's project ID and works even in repos with no commits.
|
|
147
|
+
*/
|
|
148
|
+
allocateAndReserveBranchNumber(cwd: string, prefix = 'opencode'): number {
|
|
149
|
+
const start = Date.now()
|
|
150
|
+
if (!this.hasRepo(cwd)) {
|
|
151
|
+
throw new Error('No local git repository found.')
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const base = `refs/heads/${prefix}/`
|
|
155
|
+
const listRes = execGit(['for-each-ref', '--format=%(refname:strip=3)', base], { cwd })
|
|
156
|
+
if (!listRes.ok) throw new Error(listRes.stderr)
|
|
157
|
+
const list = listRes.stdout.trim()
|
|
158
|
+
const nums =
|
|
159
|
+
list.length === 0
|
|
160
|
+
? []
|
|
161
|
+
: list
|
|
162
|
+
.split('\n')
|
|
163
|
+
.map((s) => s.trim())
|
|
164
|
+
.filter(Boolean)
|
|
165
|
+
.map((s) => Number.parseInt(s, 10))
|
|
166
|
+
.filter((n) => Number.isFinite(n) && n > 0)
|
|
167
|
+
|
|
168
|
+
let n = (nums.length ? Math.max(...nums) : 0) + 1
|
|
169
|
+
const maxAttempts = 50 // Circuit-breaker
|
|
170
|
+
let attempts = 0
|
|
171
|
+
while (n < 1_000_000 && attempts < maxAttempts) {
|
|
172
|
+
attempts++
|
|
173
|
+
const ref = `${base}${n}`
|
|
174
|
+
if (this.refExists(cwd, ref)) {
|
|
175
|
+
n++
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
const oid = this.getOrCreateEmptyCommitOid(cwd)
|
|
179
|
+
const result = execGit(['update-ref', ref, oid], { cwd })
|
|
180
|
+
if (result.ok) {
|
|
181
|
+
logger.info(`[branch-alloc] reserved ${prefix}/${n} in ${Date.now() - start}ms`)
|
|
182
|
+
return n
|
|
183
|
+
} else {
|
|
184
|
+
// If we raced or hit an edge case, try the next number.
|
|
185
|
+
n++
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const oid = this.getOrCreateEmptyCommitOid(cwd)
|
|
189
|
+
const last = execGit(['update-ref', `${base}${n}`, oid], { cwd })
|
|
190
|
+
if (last.ok) {
|
|
191
|
+
logger.info(`[branch-alloc] reserved ${prefix}/${n} in ${Date.now() - start}ms`)
|
|
192
|
+
return n
|
|
193
|
+
}
|
|
194
|
+
throw new Error(`Failed to allocate branch number after ${attempts} attempts. Last error: ${last.stderr}`)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private refExists(cwd: string, ref: string): boolean {
|
|
198
|
+
return execGit(['show-ref', '--verify', '--quiet', ref], { cwd }).ok
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Commit OID a local ref points at, or '' if the ref does not exist. */
|
|
202
|
+
getRefOid(cwd: string, ref: string): string {
|
|
203
|
+
const res = execGit(['rev-parse', '--verify', '--quiet', ref], { cwd })
|
|
204
|
+
return res.ok ? res.stdout.trim() : ''
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Returns a commit OID that branch refs can point at. Uses HEAD if the repo has commits,
|
|
209
|
+
* otherwise creates and caches an empty commit (empty tree + commit). Branch refs must
|
|
210
|
+
* point at commits, not blobs.
|
|
211
|
+
*/
|
|
212
|
+
private getOrCreateEmptyCommitOid(cwd: string): string {
|
|
213
|
+
const cached = this.emptyCommitOidCache.get(cwd)
|
|
214
|
+
if (cached) return cached
|
|
215
|
+
const headRes = execGit(['rev-parse', 'HEAD'], { cwd })
|
|
216
|
+
const head = headRes.ok ? headRes.stdout.trim() : ''
|
|
217
|
+
if (head) {
|
|
218
|
+
this.emptyCommitOidCache.set(cwd, head)
|
|
219
|
+
return head
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Create an empty tree (idempotent) then a commit pointing at it.
|
|
223
|
+
const treeResult = spawnSync('git', ['hash-object', '-t', 'tree', '-w', '--stdin'], {
|
|
224
|
+
input: '',
|
|
225
|
+
cwd,
|
|
226
|
+
encoding: 'utf8',
|
|
227
|
+
})
|
|
228
|
+
const treeOid = treeResult.stdout?.trim()
|
|
229
|
+
if (treeResult.status !== 0 || !treeOid) {
|
|
230
|
+
const errorMsg =
|
|
231
|
+
treeResult.stderr?.toString() || treeResult.error?.message || String(treeResult.error || 'unknown')
|
|
232
|
+
throw new Error(`Failed to create empty tree: ${errorMsg}`)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Provide a default identity for reservation commits when repo has no user.name/user.email (e.g. CI).
|
|
236
|
+
const reservationCommitName = 'OpenCode Plugin'
|
|
237
|
+
const reservationCommitEmail = 'opencode@daytona.io'
|
|
238
|
+
const reservationCommitMessage = 'OpenCode reservation'
|
|
239
|
+
const commitEnv = {
|
|
240
|
+
...process.env,
|
|
241
|
+
GIT_AUTHOR_NAME: reservationCommitName,
|
|
242
|
+
GIT_AUTHOR_EMAIL: reservationCommitEmail,
|
|
243
|
+
GIT_COMMITTER_NAME: reservationCommitName,
|
|
244
|
+
GIT_COMMITTER_EMAIL: reservationCommitEmail,
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const commitRes = execGit(['commit-tree', treeOid, '-m', reservationCommitMessage], { cwd, env: commitEnv })
|
|
248
|
+
if (!commitRes.ok) throw new Error(`Failed to create empty commit: ${commitRes.stderr}`)
|
|
249
|
+
const commitOid = commitRes.stdout.trim()
|
|
250
|
+
this.emptyCommitOidCache.set(cwd, commitOid)
|
|
251
|
+
return commitOid
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Pushes local changes to the sandbox remote.
|
|
256
|
+
* @param remoteName Numbered remote (e.g. sandbox-2) matching opencode/N.
|
|
257
|
+
* @param sshUrl The SSH URL of the sandbox remote.
|
|
258
|
+
* @param branch The branch to push to.
|
|
259
|
+
* @param cwd Worktree path to run git in.
|
|
260
|
+
* @returns true if push succeeded, false if no repo exists. Throws if the push fails.
|
|
261
|
+
*/
|
|
262
|
+
async pushLocalToSandboxRemote(remoteName: string, sshUrl: string, branch: string, cwd: string): Promise<boolean> {
|
|
263
|
+
if (!this.hasRepo(cwd)) {
|
|
264
|
+
logger.warn('No local git repository found. Skipping push to sandbox.')
|
|
265
|
+
return false
|
|
266
|
+
}
|
|
267
|
+
logger.info(`Pushing to ${remoteName} (${sshUrl}) on branch ${branch}`)
|
|
268
|
+
await HostGitManager.enqueue(cwd, async () => {
|
|
269
|
+
const statusRes = execGit(['status', '--porcelain'], { cwd })
|
|
270
|
+
if (!statusRes.ok) {
|
|
271
|
+
throw new Error(statusRes.stderr)
|
|
272
|
+
}
|
|
273
|
+
if (statusRes.stdout.trim().length > 0) {
|
|
274
|
+
logger.warn('Local repository has uncommitted changes; pushing HEAD only (no auto-commit).')
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
this.setRemote(remoteName, sshUrl, cwd)
|
|
278
|
+
let attempts = 0
|
|
279
|
+
while (attempts < 3) {
|
|
280
|
+
const pushRes = execGit(['push', remoteName, `HEAD:${branch}`], { cwd, env: networkEnv() })
|
|
281
|
+
if (pushRes.ok) {
|
|
282
|
+
logger.info(`✓ Pushed local changes to ${remoteName}`)
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
attempts++
|
|
286
|
+
if (attempts >= 3) {
|
|
287
|
+
logger.error(`Error pushing to ${remoteName} after 3 attempts: ${pushRes.stderr}`)
|
|
288
|
+
throw new Error(pushRes.stderr)
|
|
289
|
+
}
|
|
290
|
+
logger.warn(`Push attempt ${attempts} failed, retrying...`)
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
return true
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private setRemote(remoteName: string, sshUrl: string, cwd: string): void {
|
|
297
|
+
// Remote may not exist yet — ignore this result. `remote add` below is the check that matters.
|
|
298
|
+
execGit(['remote', 'remove', remoteName], { cwd })
|
|
299
|
+
const addRes = execGit(['remote', 'add', remoteName, sshUrl], { cwd })
|
|
300
|
+
if (!addRes.ok) {
|
|
301
|
+
throw new Error(`Failed to configure sandbox remote '${remoteName}': ${addRes.stderr}`)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async pull(remoteName: string, sshUrl: string, branch: string, cwd: string, localBranch?: string): Promise<void> {
|
|
306
|
+
await HostGitManager.enqueue(cwd, async () => {
|
|
307
|
+
this.setRemote(remoteName, sshUrl, cwd)
|
|
308
|
+
let attempts = 0
|
|
309
|
+
let lastError: unknown = undefined
|
|
310
|
+
// The first pull attempt sometimes fails. I'm not sure what the cause is.
|
|
311
|
+
while (attempts < 3) {
|
|
312
|
+
try {
|
|
313
|
+
if (localBranch) {
|
|
314
|
+
// Fetch into FETCH_HEAD only (never into refs/heads) so we don't hit
|
|
315
|
+
// "refusing to fetch into branch checked out" when this branch is checked out.
|
|
316
|
+
const fetchRes = execGit(['fetch', remoteName, branch], { cwd, env: networkEnv() })
|
|
317
|
+
if (!fetchRes.ok) throw new Error(fetchRes.stderr)
|
|
318
|
+
|
|
319
|
+
const updateRefRes = execGit(['update-ref', `refs/heads/${localBranch}`, 'FETCH_HEAD'], { cwd })
|
|
320
|
+
if (!updateRefRes.ok) throw new Error(updateRefRes.stderr)
|
|
321
|
+
|
|
322
|
+
// Only reset working directory if we're currently on this branch
|
|
323
|
+
const currentBranchRes = execGit(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd })
|
|
324
|
+
const currentBranch = currentBranchRes.ok ? currentBranchRes.stdout.trim() : ''
|
|
325
|
+
if (currentBranch === localBranch) {
|
|
326
|
+
const resetRes = execGit(['reset', '--hard', `refs/heads/${localBranch}`], { cwd })
|
|
327
|
+
if (!resetRes.ok) throw new Error(resetRes.stderr)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
logger.info(`✓ Force pulled latest changes from sandbox into ${localBranch}`)
|
|
331
|
+
} else {
|
|
332
|
+
const pullRes = execGit(['pull', remoteName, branch], { cwd, env: networkEnv() })
|
|
333
|
+
if (!pullRes.ok) throw new Error(pullRes.stderr)
|
|
334
|
+
logger.info('✓ Pulled latest changes from sandbox')
|
|
335
|
+
}
|
|
336
|
+
return
|
|
337
|
+
} catch (e) {
|
|
338
|
+
lastError = e
|
|
339
|
+
attempts++
|
|
340
|
+
if (attempts >= 3) {
|
|
341
|
+
logger.error(`Error pulling from sandbox after 3 attempts: ${e}`)
|
|
342
|
+
} else {
|
|
343
|
+
logger.warn(`Pull attempt ${attempts} failed, retrying...`)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// If we got here, all attempts failed.
|
|
349
|
+
throw lastError ?? new Error('Pull failed after 3 attempts')
|
|
350
|
+
})
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async push(remoteName: string, sshUrl: string, branch: string, cwd: string): Promise<void> {
|
|
354
|
+
await HostGitManager.enqueue(cwd, async () => {
|
|
355
|
+
this.setRemote(remoteName, sshUrl, cwd)
|
|
356
|
+
let attempts = 0
|
|
357
|
+
while (attempts < 3) {
|
|
358
|
+
const pushRes = execGit(['push', remoteName, `HEAD:${branch}`], { cwd, env: networkEnv() })
|
|
359
|
+
if (pushRes.ok) {
|
|
360
|
+
logger.info('✓ Pushed changes to sandbox')
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
attempts++
|
|
364
|
+
if (attempts >= 3) {
|
|
365
|
+
logger.error(`Error pushing to sandbox after 3 attempts: ${pushRes.stderr}`)
|
|
366
|
+
throw new Error(pushRes.stderr)
|
|
367
|
+
}
|
|
368
|
+
logger.warn(`Push attempt ${attempts} failed, retrying...`)
|
|
369
|
+
}
|
|
370
|
+
})
|
|
371
|
+
}
|
|
372
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Daytona Platforms Inc.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import type { Sandbox } from '@daytona/sdk';
|
|
6
|
+
export declare class DaytonaSandboxGitManager {
|
|
7
|
+
private readonly sandbox;
|
|
8
|
+
private readonly repoPath;
|
|
9
|
+
constructor(sandbox: Sandbox, repoPath: string);
|
|
10
|
+
ensureDirectory(): Promise<void>;
|
|
11
|
+
private runGitCommand;
|
|
12
|
+
ensureRepo(): Promise<void>;
|
|
13
|
+
autoCommit(): Promise<boolean>;
|
|
14
|
+
getCurrentBranch(): Promise<string>;
|
|
15
|
+
/**
|
|
16
|
+
* Commit OID of the sandbox HEAD, or '' on an unborn branch (no commits yet).
|
|
17
|
+
* Any other git failure throws: callers use '' as "nothing to pull", and the delete
|
|
18
|
+
* path acts on it, so a masked error here could destroy unsynced commits.
|
|
19
|
+
*/
|
|
20
|
+
getHeadOid(): Promise<string>;
|
|
21
|
+
resetToRemote(branch: string): Promise<void>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Daytona Platforms Inc.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { logger } from '../core/logger';
|
|
6
|
+
export class DaytonaSandboxGitManager {
|
|
7
|
+
sandbox;
|
|
8
|
+
repoPath;
|
|
9
|
+
constructor(sandbox, repoPath) {
|
|
10
|
+
this.sandbox = sandbox;
|
|
11
|
+
this.repoPath = repoPath;
|
|
12
|
+
}
|
|
13
|
+
async ensureDirectory() {
|
|
14
|
+
await this.sandbox.fs.createFolder(this.repoPath, '755');
|
|
15
|
+
}
|
|
16
|
+
async runGitCommand(command) {
|
|
17
|
+
const result = await this.sandbox.process.executeCommand(command, this.repoPath);
|
|
18
|
+
const output = result.result.trim();
|
|
19
|
+
if (result.exitCode !== undefined && result.exitCode !== 0) {
|
|
20
|
+
const installHint = output.includes('git: command not found')
|
|
21
|
+
? '\nHint: git is not installed in this sandbox. Rebuild the sandbox image with git installed.'
|
|
22
|
+
: '';
|
|
23
|
+
throw new Error(`Git command failed: ${command}\nOutput: ${output || '(no output)'}${installHint}`);
|
|
24
|
+
}
|
|
25
|
+
return result.result;
|
|
26
|
+
}
|
|
27
|
+
async ensureRepo() {
|
|
28
|
+
await this.ensureDirectory();
|
|
29
|
+
const isGit = await this.runGitCommand('if [ -e .git ]; then git rev-parse --is-inside-work-tree; else echo false; fi');
|
|
30
|
+
if (isGit.trim() !== 'true') {
|
|
31
|
+
await this.runGitCommand('git init');
|
|
32
|
+
await this.runGitCommand('git config user.email "sandbox@example.com"');
|
|
33
|
+
await this.runGitCommand('git config user.name "Daytona Sandbox"');
|
|
34
|
+
logger.info(`Initialized git repo in sandbox at ${this.repoPath}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async autoCommit() {
|
|
38
|
+
// Check if there are any changes to commit
|
|
39
|
+
const status = await this.runGitCommand('git status --porcelain');
|
|
40
|
+
if (!status.trim()) {
|
|
41
|
+
logger.info(`No changes to commit in sandbox at ${this.repoPath}`);
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
await this.runGitCommand('git add .');
|
|
45
|
+
await this.runGitCommand('git commit -am "Auto-commit from Daytona plugin"');
|
|
46
|
+
logger.info(`Auto-committed changes in sandbox at ${this.repoPath}`);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
async getCurrentBranch() {
|
|
50
|
+
const branch = await this.runGitCommand('git rev-parse --abbrev-ref HEAD');
|
|
51
|
+
return branch.trim();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Commit OID of the sandbox HEAD, or '' on an unborn branch (no commits yet).
|
|
55
|
+
* Any other git failure throws: callers use '' as "nothing to pull", and the delete
|
|
56
|
+
* path acts on it, so a masked error here could destroy unsynced commits.
|
|
57
|
+
*/
|
|
58
|
+
async getHeadOid() {
|
|
59
|
+
const out = await this.runGitCommand('if oid=$(git rev-parse --verify --quiet HEAD); then echo "$oid"; else git rev-parse --is-inside-work-tree > /dev/null && echo UNBORN; fi');
|
|
60
|
+
const trimmed = out.trim();
|
|
61
|
+
return trimmed === 'UNBORN' ? '' : trimmed;
|
|
62
|
+
}
|
|
63
|
+
async resetToRemote(branch) {
|
|
64
|
+
// Check out the branch the host just pushed. Using -f (not -B) checks out the
|
|
65
|
+
// pushed commit instead of resetting the branch ref to the sandbox's current HEAD.
|
|
66
|
+
const checkout = await this.runGitCommand(`git checkout -f ${branch}`);
|
|
67
|
+
logger.info(`Checked out branch '${branch}': ${checkout}`);
|
|
68
|
+
await this.runGitCommand('git reset --hard');
|
|
69
|
+
await this.runGitCommand('git clean -fd');
|
|
70
|
+
logger.info('Reset sandbox worktree to pushed state.');
|
|
71
|
+
const status = await this.runGitCommand('git status --porcelain');
|
|
72
|
+
if (status.trim()) {
|
|
73
|
+
logger.warn(`Sandbox has uncommitted changes after reset:\n${status}`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
logger.info('No uncommitted changes in sandbox after reset.');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=sandbox-git-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandbox-git-manager.js","sourceRoot":"","sources":["sandbox-git-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Daytona Platforms Inc.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Sandbox } from '@daytona/sdk'
|
|
7
|
+
import { logger } from '../core/logger'
|
|
8
|
+
|
|
9
|
+
export class DaytonaSandboxGitManager {
|
|
10
|
+
constructor(
|
|
11
|
+
private readonly sandbox: Sandbox,
|
|
12
|
+
private readonly repoPath: string,
|
|
13
|
+
) {}
|
|
14
|
+
|
|
15
|
+
async ensureDirectory(): Promise<void> {
|
|
16
|
+
await this.sandbox.fs.createFolder(this.repoPath, '755')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private async runGitCommand(command: string): Promise<string> {
|
|
20
|
+
const result = await this.sandbox.process.executeCommand(command, this.repoPath)
|
|
21
|
+
const output = result.result.trim()
|
|
22
|
+
|
|
23
|
+
if (result.exitCode !== undefined && result.exitCode !== 0) {
|
|
24
|
+
const installHint = output.includes('git: command not found')
|
|
25
|
+
? '\nHint: git is not installed in this sandbox. Rebuild the sandbox image with git installed.'
|
|
26
|
+
: ''
|
|
27
|
+
throw new Error(`Git command failed: ${command}\nOutput: ${output || '(no output)'}${installHint}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return result.result
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async ensureRepo(): Promise<void> {
|
|
34
|
+
await this.ensureDirectory()
|
|
35
|
+
const isGit = await this.runGitCommand(
|
|
36
|
+
'if [ -e .git ]; then git rev-parse --is-inside-work-tree; else echo false; fi',
|
|
37
|
+
)
|
|
38
|
+
if (isGit.trim() !== 'true') {
|
|
39
|
+
await this.runGitCommand('git init')
|
|
40
|
+
await this.runGitCommand('git config user.email "sandbox@example.com"')
|
|
41
|
+
await this.runGitCommand('git config user.name "Daytona Sandbox"')
|
|
42
|
+
logger.info(`Initialized git repo in sandbox at ${this.repoPath}`)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async autoCommit(): Promise<boolean> {
|
|
47
|
+
// Check if there are any changes to commit
|
|
48
|
+
const status = await this.runGitCommand('git status --porcelain')
|
|
49
|
+
if (!status.trim()) {
|
|
50
|
+
logger.info(`No changes to commit in sandbox at ${this.repoPath}`)
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
await this.runGitCommand('git add .')
|
|
54
|
+
await this.runGitCommand('git commit -am "Auto-commit from Daytona plugin"')
|
|
55
|
+
logger.info(`Auto-committed changes in sandbox at ${this.repoPath}`)
|
|
56
|
+
return true
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async getCurrentBranch(): Promise<string> {
|
|
60
|
+
const branch = await this.runGitCommand('git rev-parse --abbrev-ref HEAD')
|
|
61
|
+
return branch.trim()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Commit OID of the sandbox HEAD, or '' on an unborn branch (no commits yet).
|
|
66
|
+
* Any other git failure throws: callers use '' as "nothing to pull", and the delete
|
|
67
|
+
* path acts on it, so a masked error here could destroy unsynced commits.
|
|
68
|
+
*/
|
|
69
|
+
async getHeadOid(): Promise<string> {
|
|
70
|
+
const out = await this.runGitCommand(
|
|
71
|
+
'if oid=$(git rev-parse --verify --quiet HEAD); then echo "$oid"; else git rev-parse --is-inside-work-tree > /dev/null && echo UNBORN; fi',
|
|
72
|
+
)
|
|
73
|
+
const trimmed = out.trim()
|
|
74
|
+
return trimmed === 'UNBORN' ? '' : trimmed
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async resetToRemote(branch: string): Promise<void> {
|
|
78
|
+
// Check out the branch the host just pushed. Using -f (not -B) checks out the
|
|
79
|
+
// pushed commit instead of resetting the branch ref to the sandbox's current HEAD.
|
|
80
|
+
const checkout = await this.runGitCommand(`git checkout -f ${branch}`)
|
|
81
|
+
logger.info(`Checked out branch '${branch}': ${checkout}`)
|
|
82
|
+
await this.runGitCommand('git reset --hard')
|
|
83
|
+
await this.runGitCommand('git clean -fd')
|
|
84
|
+
logger.info('Reset sandbox worktree to pushed state.')
|
|
85
|
+
const status = await this.runGitCommand('git status --porcelain')
|
|
86
|
+
if (status.trim()) {
|
|
87
|
+
logger.warn(`Sandbox has uncommitted changes after reset:\n${status}`)
|
|
88
|
+
} else {
|
|
89
|
+
logger.info('No uncommitted changes in sandbox after reset.')
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Daytona Platforms Inc.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import type { Sandbox } from '@daytona/sdk';
|
|
6
|
+
import type { PluginInput } from '@opencode-ai/plugin';
|
|
7
|
+
/**
|
|
8
|
+
* SessionGitManager: Combines DaytonaSandboxGitManager and HostGitManager for session lifecycle git operations.
|
|
9
|
+
*/
|
|
10
|
+
export declare class SessionGitManager {
|
|
11
|
+
private readonly sandboxGit;
|
|
12
|
+
private readonly hostGit;
|
|
13
|
+
private readonly sandbox;
|
|
14
|
+
private readonly repoPath;
|
|
15
|
+
private readonly worktree;
|
|
16
|
+
private readonly branch;
|
|
17
|
+
private readonly localBranch;
|
|
18
|
+
/** Numbered remote (sandbox-2) matches localBranch (opencode/2) */
|
|
19
|
+
private readonly remoteName;
|
|
20
|
+
constructor(sandbox: Sandbox, repoPath: string, worktree: string, branchNumber: number);
|
|
21
|
+
/**
|
|
22
|
+
* Allocate and reserve the next opencode/N number in the local repo at `worktree`.
|
|
23
|
+
* This keeps all host-git concerns inside the git manager layer.
|
|
24
|
+
*/
|
|
25
|
+
static allocateAndReserveBranchNumber(worktree: string, prefix?: string): number;
|
|
26
|
+
private static pendingSyncs;
|
|
27
|
+
/**
|
|
28
|
+
* Run `fn` after any in-flight sync for this session and track it until it settles.
|
|
29
|
+
* The caller of this invocation sees failures; waiters only observe completion.
|
|
30
|
+
*/
|
|
31
|
+
static enqueueSessionSync<T>(sessionId: string, fn: () => Promise<T>): Promise<T>;
|
|
32
|
+
/** Resolves when the session has no in-flight sync. Never rejects. */
|
|
33
|
+
static waitForPendingSync(sessionId: string): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Resolves when no session has an in-flight sync, or when `timeoutMs` elapses first
|
|
36
|
+
* (returns false in that case). Never rejects. The bound exists for shutdown, where a
|
|
37
|
+
* sync stalled on an unreachable sandbox must not wedge process exit; the delete path
|
|
38
|
+
* intentionally waits unbounded instead, because deleting mid-sync loses data.
|
|
39
|
+
*/
|
|
40
|
+
static waitForAllPendingSyncs(timeoutMs?: number): Promise<boolean>;
|
|
41
|
+
private getSshUrl;
|
|
42
|
+
hasLocalRepo(): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Initialize git in the sandbox and sync with host
|
|
45
|
+
* Used when a new sandbox is created for a session
|
|
46
|
+
*/
|
|
47
|
+
initializeAndSync(pluginCtx?: PluginInput): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Auto-commit in the sandbox and pull latest from host
|
|
50
|
+
* Used on session idle
|
|
51
|
+
* Returns true if changes were synced, false if no changes or no local repo
|
|
52
|
+
*/
|
|
53
|
+
autoCommitAndPull(pluginCtx?: PluginInput): Promise<boolean>;
|
|
54
|
+
}
|