@vladimirven/openswe 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/AGENTS.md +203 -0
- package/CLAUDE.md +203 -0
- package/README.md +166 -0
- package/bun.lock +447 -0
- package/bunfig.toml +4 -0
- package/package.json +42 -0
- package/src/app.tsx +84 -0
- package/src/components/App.tsx +526 -0
- package/src/components/ConfirmDialog.tsx +88 -0
- package/src/components/Footer.tsx +50 -0
- package/src/components/HelpModal.tsx +136 -0
- package/src/components/IssueSelectorModal.tsx +701 -0
- package/src/components/ManualSessionModal.tsx +191 -0
- package/src/components/PhaseProgress.tsx +45 -0
- package/src/components/Preview.tsx +249 -0
- package/src/components/ProviderSwitcherModal.tsx +156 -0
- package/src/components/ScrollableText.tsx +120 -0
- package/src/components/SessionCard.tsx +60 -0
- package/src/components/SessionList.tsx +79 -0
- package/src/components/SessionTerminal.tsx +89 -0
- package/src/components/StatusBar.tsx +84 -0
- package/src/components/ThemeSwitcherModal.tsx +237 -0
- package/src/components/index.ts +58 -0
- package/src/components/session-utils.ts +337 -0
- package/src/components/theme.ts +206 -0
- package/src/components/types.ts +215 -0
- package/src/config/defaults.ts +44 -0
- package/src/config/env.ts +67 -0
- package/src/config/global.ts +252 -0
- package/src/config/index.ts +171 -0
- package/src/config/types.ts +131 -0
- package/src/core/.gitkeep +0 -0
- package/src/core/index.ts +5 -0
- package/src/core/parser.ts +62 -0
- package/src/core/process-manager.ts +52 -0
- package/src/core/session.ts +423 -0
- package/src/core/tmux.ts +206 -0
- package/src/git/.gitkeep +0 -0
- package/src/git/index.ts +8 -0
- package/src/git/repo.ts +443 -0
- package/src/git/worktree.ts +317 -0
- package/src/github/.gitkeep +0 -0
- package/src/github/client.ts +208 -0
- package/src/github/index.ts +8 -0
- package/src/github/issues.ts +351 -0
- package/src/index.ts +369 -0
- package/src/prompts/.gitkeep +0 -0
- package/src/prompts/index.ts +1 -0
- package/src/prompts/swe-system.ts +22 -0
- package/src/providers/claude.ts +103 -0
- package/src/providers/index.ts +21 -0
- package/src/providers/opencode.ts +98 -0
- package/src/providers/registry.ts +53 -0
- package/src/providers/types.ts +117 -0
- package/src/store/buffers.ts +234 -0
- package/src/store/db.test.ts +579 -0
- package/src/store/db.ts +249 -0
- package/src/store/index.ts +101 -0
- package/src/store/project.ts +119 -0
- package/src/store/schema.sql +71 -0
- package/src/store/sessions.ts +454 -0
- package/src/store/types.ts +194 -0
- package/src/theme/context.tsx +170 -0
- package/src/theme/custom.ts +134 -0
- package/src/theme/index.ts +58 -0
- package/src/theme/loader.ts +264 -0
- package/src/theme/themes/aura.json +69 -0
- package/src/theme/themes/ayu.json +80 -0
- package/src/theme/themes/carbonfox.json +248 -0
- package/src/theme/themes/catppuccin-frappe.json +233 -0
- package/src/theme/themes/catppuccin-macchiato.json +233 -0
- package/src/theme/themes/catppuccin.json +112 -0
- package/src/theme/themes/cobalt2.json +228 -0
- package/src/theme/themes/cursor.json +249 -0
- package/src/theme/themes/dracula.json +219 -0
- package/src/theme/themes/everforest.json +241 -0
- package/src/theme/themes/flexoki.json +237 -0
- package/src/theme/themes/github.json +233 -0
- package/src/theme/themes/gruvbox.json +242 -0
- package/src/theme/themes/kanagawa.json +77 -0
- package/src/theme/themes/lucent-orng.json +237 -0
- package/src/theme/themes/material.json +235 -0
- package/src/theme/themes/matrix.json +77 -0
- package/src/theme/themes/mercury.json +252 -0
- package/src/theme/themes/monokai.json +221 -0
- package/src/theme/themes/nightowl.json +221 -0
- package/src/theme/themes/nord.json +223 -0
- package/src/theme/themes/one-dark.json +84 -0
- package/src/theme/themes/opencode.json +245 -0
- package/src/theme/themes/orng.json +249 -0
- package/src/theme/themes/osaka-jade.json +93 -0
- package/src/theme/themes/palenight.json +222 -0
- package/src/theme/themes/rosepine.json +234 -0
- package/src/theme/themes/solarized.json +223 -0
- package/src/theme/themes/synthwave84.json +226 -0
- package/src/theme/themes/tokyonight.json +243 -0
- package/src/theme/themes/vercel.json +245 -0
- package/src/theme/themes/vesper.json +218 -0
- package/src/theme/themes/zenburn.json +223 -0
- package/src/theme/types.ts +225 -0
- package/src/types/sql.d.ts +4 -0
- package/src/utils/ansi-parser.ts +225 -0
- package/src/utils/format.ts +46 -0
- package/src/utils/id.ts +15 -0
- package/src/utils/logger.ts +112 -0
- package/src/utils/prerequisites.ts +118 -0
- package/src/utils/shell.ts +9 -0
- package/src/wizard/flows.ts +419 -0
- package/src/wizard/index.ts +37 -0
- package/src/wizard/prompts.ts +190 -0
- package/src/workspace/detect.test.ts +51 -0
- package/src/workspace/detect.ts +223 -0
- package/src/workspace/index.ts +71 -0
- package/src/workspace/init.ts +131 -0
- package/src/workspace/paths.ts +143 -0
- package/src/workspace/project.ts +164 -0
- package/tsconfig.json +22 -0
package/src/git/repo.ts
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git repository operations
|
|
3
|
+
*
|
|
4
|
+
* Handles cloning repositories and git-related checks.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Types
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
/** Clone protocol options */
|
|
12
|
+
export type CloneProtocol = "ssh" | "https"
|
|
13
|
+
|
|
14
|
+
/** Clone operation result */
|
|
15
|
+
export interface CloneResult {
|
|
16
|
+
success: boolean
|
|
17
|
+
error?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Clone options */
|
|
21
|
+
export interface CloneOptions {
|
|
22
|
+
/** Protocol to use for cloning */
|
|
23
|
+
protocol?: CloneProtocol
|
|
24
|
+
/** Callback for progress messages */
|
|
25
|
+
onProgress?: (message: string) => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Git Installation Check
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if git is installed and available
|
|
34
|
+
*/
|
|
35
|
+
export async function checkGitInstalled(): Promise<boolean> {
|
|
36
|
+
try {
|
|
37
|
+
const proc = Bun.spawn(["git", "--version"], {
|
|
38
|
+
stdout: "pipe",
|
|
39
|
+
stderr: "pipe",
|
|
40
|
+
})
|
|
41
|
+
const exitCode = await proc.exited
|
|
42
|
+
return exitCode === 0
|
|
43
|
+
} catch {
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get the installed git version
|
|
50
|
+
*/
|
|
51
|
+
export async function getGitVersion(): Promise<string | null> {
|
|
52
|
+
try {
|
|
53
|
+
const proc = Bun.spawn(["git", "--version"], {
|
|
54
|
+
stdout: "pipe",
|
|
55
|
+
stderr: "pipe",
|
|
56
|
+
})
|
|
57
|
+
const output = await new Response(proc.stdout).text()
|
|
58
|
+
const exitCode = await proc.exited
|
|
59
|
+
|
|
60
|
+
if (exitCode !== 0) return null
|
|
61
|
+
|
|
62
|
+
// Parse "git version 2.39.0" -> "2.39.0"
|
|
63
|
+
const match = output.match(/git version (\S+)/)
|
|
64
|
+
return match?.[1] ?? null
|
|
65
|
+
} catch {
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// Clone URL Generation
|
|
72
|
+
// ============================================================================
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Generate a clone URL from owner/repo format
|
|
76
|
+
*
|
|
77
|
+
* @param ownerRepo - Repository in owner/repo format
|
|
78
|
+
* @param protocol - Clone protocol (ssh or https)
|
|
79
|
+
* @returns Clone URL
|
|
80
|
+
*/
|
|
81
|
+
export function getCloneUrl(ownerRepo: string, protocol: CloneProtocol = "ssh"): string {
|
|
82
|
+
if (protocol === "ssh") {
|
|
83
|
+
return `git@github.com:${ownerRepo}.git`
|
|
84
|
+
}
|
|
85
|
+
return `https://github.com/${ownerRepo}.git`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Parse owner/repo from a clone URL
|
|
90
|
+
* Handles both SSH and HTTPS URLs
|
|
91
|
+
*/
|
|
92
|
+
export function parseOwnerRepo(url: string): string | null {
|
|
93
|
+
// SSH format: git@github.com:owner/repo.git
|
|
94
|
+
const sshMatch = url.match(/git@github\.com:([^/]+\/[^/]+?)(?:\.git)?$/)
|
|
95
|
+
if (sshMatch?.[1]) {
|
|
96
|
+
return sshMatch[1]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// HTTPS format: https://github.com/owner/repo.git
|
|
100
|
+
const httpsMatch = url.match(/https?:\/\/github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/)
|
|
101
|
+
if (httpsMatch?.[1]) {
|
|
102
|
+
return httpsMatch[1]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Validate owner/repo format
|
|
110
|
+
*/
|
|
111
|
+
export function isValidOwnerRepo(input: string): boolean {
|
|
112
|
+
// Basic validation: owner/repo format
|
|
113
|
+
// Owner and repo can contain alphanumeric, hyphens, underscores
|
|
114
|
+
// Repo can also contain dots
|
|
115
|
+
const pattern = /^[a-zA-Z0-9_-]+\/[a-zA-Z0-9._-]+$/
|
|
116
|
+
return pattern.test(input)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ============================================================================
|
|
120
|
+
// Clone Operations
|
|
121
|
+
// ============================================================================
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Clone a repository into the target directory
|
|
125
|
+
*
|
|
126
|
+
* Clones directly into the target directory (not a subdirectory).
|
|
127
|
+
* Uses full history (no --depth 1) for proper worktree support.
|
|
128
|
+
*
|
|
129
|
+
* @param ownerRepo - Repository in owner/repo format
|
|
130
|
+
* @param targetDir - Directory to clone into (must exist and be empty)
|
|
131
|
+
* @param options - Clone options
|
|
132
|
+
*/
|
|
133
|
+
export async function cloneRepo(
|
|
134
|
+
ownerRepo: string,
|
|
135
|
+
targetDir: string,
|
|
136
|
+
options: CloneOptions = {}
|
|
137
|
+
): Promise<CloneResult> {
|
|
138
|
+
const { protocol = "ssh", onProgress } = options
|
|
139
|
+
|
|
140
|
+
const cloneUrl = getCloneUrl(ownerRepo, protocol)
|
|
141
|
+
|
|
142
|
+
onProgress?.(`Cloning ${ownerRepo} via ${protocol.toUpperCase()}...`)
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
// Clone into current directory using "." as target
|
|
146
|
+
// This requires the directory to be empty
|
|
147
|
+
const proc = Bun.spawn(
|
|
148
|
+
["git", "clone", cloneUrl, "."],
|
|
149
|
+
{
|
|
150
|
+
cwd: targetDir,
|
|
151
|
+
stdout: "pipe",
|
|
152
|
+
stderr: "pipe",
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
// Git outputs progress to stderr
|
|
157
|
+
const stderr = await new Response(proc.stderr).text()
|
|
158
|
+
const exitCode = await proc.exited
|
|
159
|
+
|
|
160
|
+
if (exitCode !== 0) {
|
|
161
|
+
// Parse common error messages
|
|
162
|
+
if (stderr.includes("Repository not found")) {
|
|
163
|
+
return {
|
|
164
|
+
success: false,
|
|
165
|
+
error: `Repository '${ownerRepo}' not found. Check the name and your access permissions.`,
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (stderr.includes("Permission denied")) {
|
|
169
|
+
return {
|
|
170
|
+
success: false,
|
|
171
|
+
error: `Permission denied. Check your SSH key or try HTTPS protocol.`,
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (stderr.includes("Could not resolve host")) {
|
|
175
|
+
return {
|
|
176
|
+
success: false,
|
|
177
|
+
error: `Could not connect to GitHub. Check your internet connection.`,
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (stderr.includes("already exists and is not an empty directory")) {
|
|
181
|
+
return {
|
|
182
|
+
success: false,
|
|
183
|
+
error: `Target directory is not empty. Choose an empty directory.`,
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (stderr.includes("Authentication failed")) {
|
|
187
|
+
return {
|
|
188
|
+
success: false,
|
|
189
|
+
error: `Authentication failed. Check your credentials or try SSH protocol.`,
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
success: false,
|
|
195
|
+
error: stderr.trim() || "Clone failed with unknown error",
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
onProgress?.(`Successfully cloned ${ownerRepo}`)
|
|
200
|
+
return { success: true }
|
|
201
|
+
} catch (err) {
|
|
202
|
+
return {
|
|
203
|
+
success: false,
|
|
204
|
+
error: err instanceof Error ? err.message : "Clone failed",
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Check if a directory is empty (safe for cloning into)
|
|
211
|
+
*/
|
|
212
|
+
export async function isDirectoryEmptyForClone(path: string): Promise<boolean> {
|
|
213
|
+
try {
|
|
214
|
+
const { readdir } = await import("fs/promises")
|
|
215
|
+
const entries = await readdir(path)
|
|
216
|
+
// Allow .DS_Store and other hidden files that don't matter
|
|
217
|
+
const significantEntries = entries.filter(
|
|
218
|
+
(e) => !e.startsWith(".") || e === ".git"
|
|
219
|
+
)
|
|
220
|
+
return significantEntries.length === 0
|
|
221
|
+
} catch {
|
|
222
|
+
return false
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ============================================================================
|
|
227
|
+
// Repository Checks
|
|
228
|
+
// ============================================================================
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Check if a directory is a git repository
|
|
232
|
+
*/
|
|
233
|
+
export async function isGitRepo(path: string): Promise<boolean> {
|
|
234
|
+
try {
|
|
235
|
+
const proc = Bun.spawn(["git", "rev-parse", "--git-dir"], {
|
|
236
|
+
cwd: path,
|
|
237
|
+
stdout: "pipe",
|
|
238
|
+
stderr: "pipe",
|
|
239
|
+
})
|
|
240
|
+
const exitCode = await proc.exited
|
|
241
|
+
return exitCode === 0
|
|
242
|
+
} catch {
|
|
243
|
+
return false
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get the current branch name
|
|
249
|
+
*/
|
|
250
|
+
export async function getCurrentBranch(repoPath: string): Promise<string | null> {
|
|
251
|
+
try {
|
|
252
|
+
const proc = Bun.spawn(["git", "branch", "--show-current"], {
|
|
253
|
+
cwd: repoPath,
|
|
254
|
+
stdout: "pipe",
|
|
255
|
+
stderr: "pipe",
|
|
256
|
+
})
|
|
257
|
+
const output = await new Response(proc.stdout).text()
|
|
258
|
+
const exitCode = await proc.exited
|
|
259
|
+
|
|
260
|
+
if (exitCode !== 0) return null
|
|
261
|
+
return output.trim() || null
|
|
262
|
+
} catch {
|
|
263
|
+
return null
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Get the default branch name (main or master)
|
|
269
|
+
*/
|
|
270
|
+
export async function getDefaultBranch(repoPath: string): Promise<string | null> {
|
|
271
|
+
try {
|
|
272
|
+
// Try to get from remote
|
|
273
|
+
const proc = Bun.spawn(
|
|
274
|
+
["git", "symbolic-ref", "refs/remotes/origin/HEAD", "--short"],
|
|
275
|
+
{
|
|
276
|
+
cwd: repoPath,
|
|
277
|
+
stdout: "pipe",
|
|
278
|
+
stderr: "pipe",
|
|
279
|
+
}
|
|
280
|
+
)
|
|
281
|
+
const output = await new Response(proc.stdout).text()
|
|
282
|
+
const exitCode = await proc.exited
|
|
283
|
+
|
|
284
|
+
if (exitCode === 0 && output.trim()) {
|
|
285
|
+
// Returns "origin/main" -> extract "main"
|
|
286
|
+
const branch = output.trim().replace("origin/", "")
|
|
287
|
+
return branch
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Fallback: check if main or master exists
|
|
291
|
+
const mainProc = Bun.spawn(["git", "show-ref", "--verify", "--quiet", "refs/heads/main"], {
|
|
292
|
+
cwd: repoPath,
|
|
293
|
+
stdout: "pipe",
|
|
294
|
+
stderr: "pipe",
|
|
295
|
+
})
|
|
296
|
+
if ((await mainProc.exited) === 0) return "main"
|
|
297
|
+
|
|
298
|
+
const masterProc = Bun.spawn(["git", "show-ref", "--verify", "--quiet", "refs/heads/master"], {
|
|
299
|
+
cwd: repoPath,
|
|
300
|
+
stdout: "pipe",
|
|
301
|
+
stderr: "pipe",
|
|
302
|
+
})
|
|
303
|
+
if ((await masterProc.exited) === 0) return "master"
|
|
304
|
+
|
|
305
|
+
return null
|
|
306
|
+
} catch {
|
|
307
|
+
return null
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ============================================================================
|
|
312
|
+
// Push Operations
|
|
313
|
+
// ============================================================================
|
|
314
|
+
|
|
315
|
+
/** Result of a push operation */
|
|
316
|
+
export interface PushBranchResult {
|
|
317
|
+
success: boolean
|
|
318
|
+
error?: string
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Push a branch to the remote origin
|
|
323
|
+
*
|
|
324
|
+
* @param repoPath - Path to the repository (can be a worktree)
|
|
325
|
+
* @param branchName - Branch name to push
|
|
326
|
+
* @param setUpstream - Whether to set upstream tracking (default: true)
|
|
327
|
+
*/
|
|
328
|
+
export async function pushBranch(
|
|
329
|
+
repoPath: string,
|
|
330
|
+
branchName: string,
|
|
331
|
+
setUpstream: boolean = true
|
|
332
|
+
): Promise<PushBranchResult> {
|
|
333
|
+
try {
|
|
334
|
+
const args = ["git", "push"]
|
|
335
|
+
if (setUpstream) {
|
|
336
|
+
args.push("-u")
|
|
337
|
+
}
|
|
338
|
+
args.push("origin", branchName)
|
|
339
|
+
|
|
340
|
+
const proc = Bun.spawn(args, {
|
|
341
|
+
cwd: repoPath,
|
|
342
|
+
stdout: "pipe",
|
|
343
|
+
stderr: "pipe",
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
const stderr = await new Response(proc.stderr).text()
|
|
347
|
+
const exitCode = await proc.exited
|
|
348
|
+
|
|
349
|
+
if (exitCode !== 0) {
|
|
350
|
+
// Parse common error messages
|
|
351
|
+
if (stderr.includes("Permission denied")) {
|
|
352
|
+
return {
|
|
353
|
+
success: false,
|
|
354
|
+
error: "Permission denied. Check your SSH key or credentials.",
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (stderr.includes("Could not resolve host")) {
|
|
358
|
+
return {
|
|
359
|
+
success: false,
|
|
360
|
+
error: "Could not connect to GitHub. Check your internet connection.",
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (stderr.includes("rejected")) {
|
|
364
|
+
return {
|
|
365
|
+
success: false,
|
|
366
|
+
error: "Push rejected. The remote branch may have diverged.",
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (stderr.includes("does not appear to be a git repository")) {
|
|
370
|
+
return {
|
|
371
|
+
success: false,
|
|
372
|
+
error: "No remote 'origin' configured for this repository.",
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
success: false,
|
|
378
|
+
error: stderr.trim() || "Push failed with unknown error",
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return { success: true }
|
|
383
|
+
} catch (err) {
|
|
384
|
+
return {
|
|
385
|
+
success: false,
|
|
386
|
+
error: err instanceof Error ? err.message : "Push failed",
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Get the number of commits ahead of a base branch
|
|
393
|
+
*
|
|
394
|
+
* @param repoPath - Path to the repository (can be a worktree)
|
|
395
|
+
* @param baseBranch - Base branch to compare against (default: "main")
|
|
396
|
+
* @returns Number of commits ahead, or 0 if unable to determine
|
|
397
|
+
*/
|
|
398
|
+
export async function getCommitsAhead(
|
|
399
|
+
repoPath: string,
|
|
400
|
+
baseBranch: string = "main"
|
|
401
|
+
): Promise<number> {
|
|
402
|
+
try {
|
|
403
|
+
// First try with origin/<base> which is more reliable
|
|
404
|
+
const proc = Bun.spawn(
|
|
405
|
+
["git", "rev-list", "--count", `origin/${baseBranch}..HEAD`],
|
|
406
|
+
{
|
|
407
|
+
cwd: repoPath,
|
|
408
|
+
stdout: "pipe",
|
|
409
|
+
stderr: "pipe",
|
|
410
|
+
}
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
const output = await new Response(proc.stdout).text()
|
|
414
|
+
const exitCode = await proc.exited
|
|
415
|
+
|
|
416
|
+
if (exitCode === 0) {
|
|
417
|
+
const count = parseInt(output.trim(), 10)
|
|
418
|
+
return isNaN(count) ? 0 : count
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Fallback to local branch comparison
|
|
422
|
+
const localProc = Bun.spawn(
|
|
423
|
+
["git", "rev-list", "--count", `${baseBranch}..HEAD`],
|
|
424
|
+
{
|
|
425
|
+
cwd: repoPath,
|
|
426
|
+
stdout: "pipe",
|
|
427
|
+
stderr: "pipe",
|
|
428
|
+
}
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
const localOutput = await new Response(localProc.stdout).text()
|
|
432
|
+
const localExitCode = await localProc.exited
|
|
433
|
+
|
|
434
|
+
if (localExitCode === 0) {
|
|
435
|
+
const count = parseInt(localOutput.trim(), 10)
|
|
436
|
+
return isNaN(count) ? 0 : count
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return 0
|
|
440
|
+
} catch {
|
|
441
|
+
return 0
|
|
442
|
+
}
|
|
443
|
+
}
|