@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.
Files changed (117) hide show
  1. package/AGENTS.md +203 -0
  2. package/CLAUDE.md +203 -0
  3. package/README.md +166 -0
  4. package/bun.lock +447 -0
  5. package/bunfig.toml +4 -0
  6. package/package.json +42 -0
  7. package/src/app.tsx +84 -0
  8. package/src/components/App.tsx +526 -0
  9. package/src/components/ConfirmDialog.tsx +88 -0
  10. package/src/components/Footer.tsx +50 -0
  11. package/src/components/HelpModal.tsx +136 -0
  12. package/src/components/IssueSelectorModal.tsx +701 -0
  13. package/src/components/ManualSessionModal.tsx +191 -0
  14. package/src/components/PhaseProgress.tsx +45 -0
  15. package/src/components/Preview.tsx +249 -0
  16. package/src/components/ProviderSwitcherModal.tsx +156 -0
  17. package/src/components/ScrollableText.tsx +120 -0
  18. package/src/components/SessionCard.tsx +60 -0
  19. package/src/components/SessionList.tsx +79 -0
  20. package/src/components/SessionTerminal.tsx +89 -0
  21. package/src/components/StatusBar.tsx +84 -0
  22. package/src/components/ThemeSwitcherModal.tsx +237 -0
  23. package/src/components/index.ts +58 -0
  24. package/src/components/session-utils.ts +337 -0
  25. package/src/components/theme.ts +206 -0
  26. package/src/components/types.ts +215 -0
  27. package/src/config/defaults.ts +44 -0
  28. package/src/config/env.ts +67 -0
  29. package/src/config/global.ts +252 -0
  30. package/src/config/index.ts +171 -0
  31. package/src/config/types.ts +131 -0
  32. package/src/core/.gitkeep +0 -0
  33. package/src/core/index.ts +5 -0
  34. package/src/core/parser.ts +62 -0
  35. package/src/core/process-manager.ts +52 -0
  36. package/src/core/session.ts +423 -0
  37. package/src/core/tmux.ts +206 -0
  38. package/src/git/.gitkeep +0 -0
  39. package/src/git/index.ts +8 -0
  40. package/src/git/repo.ts +443 -0
  41. package/src/git/worktree.ts +317 -0
  42. package/src/github/.gitkeep +0 -0
  43. package/src/github/client.ts +208 -0
  44. package/src/github/index.ts +8 -0
  45. package/src/github/issues.ts +351 -0
  46. package/src/index.ts +369 -0
  47. package/src/prompts/.gitkeep +0 -0
  48. package/src/prompts/index.ts +1 -0
  49. package/src/prompts/swe-system.ts +22 -0
  50. package/src/providers/claude.ts +103 -0
  51. package/src/providers/index.ts +21 -0
  52. package/src/providers/opencode.ts +98 -0
  53. package/src/providers/registry.ts +53 -0
  54. package/src/providers/types.ts +117 -0
  55. package/src/store/buffers.ts +234 -0
  56. package/src/store/db.test.ts +579 -0
  57. package/src/store/db.ts +249 -0
  58. package/src/store/index.ts +101 -0
  59. package/src/store/project.ts +119 -0
  60. package/src/store/schema.sql +71 -0
  61. package/src/store/sessions.ts +454 -0
  62. package/src/store/types.ts +194 -0
  63. package/src/theme/context.tsx +170 -0
  64. package/src/theme/custom.ts +134 -0
  65. package/src/theme/index.ts +58 -0
  66. package/src/theme/loader.ts +264 -0
  67. package/src/theme/themes/aura.json +69 -0
  68. package/src/theme/themes/ayu.json +80 -0
  69. package/src/theme/themes/carbonfox.json +248 -0
  70. package/src/theme/themes/catppuccin-frappe.json +233 -0
  71. package/src/theme/themes/catppuccin-macchiato.json +233 -0
  72. package/src/theme/themes/catppuccin.json +112 -0
  73. package/src/theme/themes/cobalt2.json +228 -0
  74. package/src/theme/themes/cursor.json +249 -0
  75. package/src/theme/themes/dracula.json +219 -0
  76. package/src/theme/themes/everforest.json +241 -0
  77. package/src/theme/themes/flexoki.json +237 -0
  78. package/src/theme/themes/github.json +233 -0
  79. package/src/theme/themes/gruvbox.json +242 -0
  80. package/src/theme/themes/kanagawa.json +77 -0
  81. package/src/theme/themes/lucent-orng.json +237 -0
  82. package/src/theme/themes/material.json +235 -0
  83. package/src/theme/themes/matrix.json +77 -0
  84. package/src/theme/themes/mercury.json +252 -0
  85. package/src/theme/themes/monokai.json +221 -0
  86. package/src/theme/themes/nightowl.json +221 -0
  87. package/src/theme/themes/nord.json +223 -0
  88. package/src/theme/themes/one-dark.json +84 -0
  89. package/src/theme/themes/opencode.json +245 -0
  90. package/src/theme/themes/orng.json +249 -0
  91. package/src/theme/themes/osaka-jade.json +93 -0
  92. package/src/theme/themes/palenight.json +222 -0
  93. package/src/theme/themes/rosepine.json +234 -0
  94. package/src/theme/themes/solarized.json +223 -0
  95. package/src/theme/themes/synthwave84.json +226 -0
  96. package/src/theme/themes/tokyonight.json +243 -0
  97. package/src/theme/themes/vercel.json +245 -0
  98. package/src/theme/themes/vesper.json +218 -0
  99. package/src/theme/themes/zenburn.json +223 -0
  100. package/src/theme/types.ts +225 -0
  101. package/src/types/sql.d.ts +4 -0
  102. package/src/utils/ansi-parser.ts +225 -0
  103. package/src/utils/format.ts +46 -0
  104. package/src/utils/id.ts +15 -0
  105. package/src/utils/logger.ts +112 -0
  106. package/src/utils/prerequisites.ts +118 -0
  107. package/src/utils/shell.ts +9 -0
  108. package/src/wizard/flows.ts +419 -0
  109. package/src/wizard/index.ts +37 -0
  110. package/src/wizard/prompts.ts +190 -0
  111. package/src/workspace/detect.test.ts +51 -0
  112. package/src/workspace/detect.ts +223 -0
  113. package/src/workspace/index.ts +71 -0
  114. package/src/workspace/init.ts +131 -0
  115. package/src/workspace/paths.ts +143 -0
  116. package/src/workspace/project.ts +164 -0
  117. package/tsconfig.json +22 -0
@@ -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
+ }