@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
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git worktree management
|
|
3
|
+
*
|
|
4
|
+
* Provides functions for creating, listing, and removing git worktrees.
|
|
5
|
+
* Worktrees allow multiple working directories from a single repository,
|
|
6
|
+
* enabling parallel work on different issues.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { getWorktreesDir, getWorktreePath, generateBranchName, WORKTREES_DIR } from "../workspace/paths"
|
|
10
|
+
import { logger } from "../utils/logger"
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Types
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
/** Information about a git worktree */
|
|
17
|
+
export interface Worktree {
|
|
18
|
+
/** Absolute path to the worktree directory */
|
|
19
|
+
path: string
|
|
20
|
+
/** Current HEAD commit SHA */
|
|
21
|
+
head: string
|
|
22
|
+
/** Branch name (null for detached HEAD) */
|
|
23
|
+
branch: string | null
|
|
24
|
+
/** Whether this is the main worktree */
|
|
25
|
+
isMain: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Result of a worktree operation */
|
|
29
|
+
export interface WorktreeResult {
|
|
30
|
+
/** Whether the operation succeeded */
|
|
31
|
+
success: boolean
|
|
32
|
+
/** Error message if operation failed */
|
|
33
|
+
error?: string
|
|
34
|
+
/** Path to the created worktree (for create operations) */
|
|
35
|
+
worktreePath?: string
|
|
36
|
+
/** Branch name (for create operations) */
|
|
37
|
+
branchName?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// Directory Setup
|
|
42
|
+
// ============================================================================
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Ensure the .worktrees directory exists
|
|
46
|
+
*
|
|
47
|
+
* @param projectRoot - Absolute path to the project root
|
|
48
|
+
*/
|
|
49
|
+
export async function ensureWorktreesDir(projectRoot: string): Promise<void> {
|
|
50
|
+
const worktreesDir = getWorktreesDir(projectRoot)
|
|
51
|
+
|
|
52
|
+
// Create directory if it doesn't exist
|
|
53
|
+
const { mkdir } = await import("fs/promises")
|
|
54
|
+
await mkdir(worktreesDir, { recursive: true })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// Worktree Operations
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create a new git worktree with a new branch
|
|
63
|
+
*
|
|
64
|
+
* Creates a worktree at `.worktrees/{name}` with branch `openswe/{name}`.
|
|
65
|
+
*
|
|
66
|
+
* @param projectRoot - Absolute path to the project root
|
|
67
|
+
* @param name - Worktree name (issue number or custom name)
|
|
68
|
+
* @returns Result indicating success or failure
|
|
69
|
+
*/
|
|
70
|
+
export async function createWorktree(
|
|
71
|
+
projectRoot: string,
|
|
72
|
+
name: string | number
|
|
73
|
+
): Promise<WorktreeResult> {
|
|
74
|
+
// Ensure .worktrees directory exists
|
|
75
|
+
await ensureWorktreesDir(projectRoot)
|
|
76
|
+
|
|
77
|
+
const worktreePath = getWorktreePath(projectRoot, name)
|
|
78
|
+
const branchName = generateBranchName(name)
|
|
79
|
+
|
|
80
|
+
logger.debug("Creating worktree", {
|
|
81
|
+
projectRoot,
|
|
82
|
+
worktreePath,
|
|
83
|
+
branchName,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// Check if worktree already exists
|
|
87
|
+
const exists = await worktreeExists(projectRoot, name)
|
|
88
|
+
if (exists) {
|
|
89
|
+
logger.warn("Worktree already exists", worktreePath)
|
|
90
|
+
return {
|
|
91
|
+
success: false,
|
|
92
|
+
error: `Worktree already exists: ${worktreePath}`,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
// Create the worktree with a new branch
|
|
98
|
+
// git worktree add <path> -b <branch>
|
|
99
|
+
const proc = Bun.spawn(
|
|
100
|
+
["git", "worktree", "add", worktreePath, "-b", branchName],
|
|
101
|
+
{
|
|
102
|
+
cwd: projectRoot,
|
|
103
|
+
stdout: "pipe",
|
|
104
|
+
stderr: "pipe",
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
const stderr = await new Response(proc.stderr).text()
|
|
109
|
+
const exitCode = await proc.exited
|
|
110
|
+
|
|
111
|
+
if (exitCode !== 0) {
|
|
112
|
+
// Parse common errors
|
|
113
|
+
if (stderr.includes("already exists")) {
|
|
114
|
+
// Branch might exist, try without -b flag
|
|
115
|
+
logger.warn("Branch exists, retrying worktree creation", branchName)
|
|
116
|
+
const retryProc = Bun.spawn(
|
|
117
|
+
["git", "worktree", "add", worktreePath, branchName],
|
|
118
|
+
{
|
|
119
|
+
cwd: projectRoot,
|
|
120
|
+
stdout: "pipe",
|
|
121
|
+
stderr: "pipe",
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
const retryStderr = await new Response(retryProc.stderr).text()
|
|
126
|
+
const retryExitCode = await retryProc.exited
|
|
127
|
+
|
|
128
|
+
if (retryExitCode !== 0) {
|
|
129
|
+
logger.warn("Worktree creation retry failed", retryStderr.trim())
|
|
130
|
+
return {
|
|
131
|
+
success: false,
|
|
132
|
+
error: retryStderr.trim() || "Failed to create worktree",
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
logger.warn("Worktree creation failed", stderr.trim())
|
|
137
|
+
return {
|
|
138
|
+
success: false,
|
|
139
|
+
error: stderr.trim() || "Failed to create worktree",
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
success: true,
|
|
146
|
+
worktreePath,
|
|
147
|
+
branchName,
|
|
148
|
+
}
|
|
149
|
+
} catch (err) {
|
|
150
|
+
return {
|
|
151
|
+
success: false,
|
|
152
|
+
error: err instanceof Error ? err.message : "Failed to create worktree",
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* List all worktrees for a repository
|
|
159
|
+
*
|
|
160
|
+
* @param projectRoot - Absolute path to the project root
|
|
161
|
+
* @returns Array of worktree information
|
|
162
|
+
*/
|
|
163
|
+
export async function listWorktrees(projectRoot: string): Promise<Worktree[]> {
|
|
164
|
+
try {
|
|
165
|
+
const proc = Bun.spawn(
|
|
166
|
+
["git", "worktree", "list", "--porcelain"],
|
|
167
|
+
{
|
|
168
|
+
cwd: projectRoot,
|
|
169
|
+
stdout: "pipe",
|
|
170
|
+
stderr: "pipe",
|
|
171
|
+
}
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
const stdout = await new Response(proc.stdout).text()
|
|
175
|
+
const exitCode = await proc.exited
|
|
176
|
+
|
|
177
|
+
if (exitCode !== 0) {
|
|
178
|
+
return []
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Parse porcelain output
|
|
182
|
+
// Format:
|
|
183
|
+
// worktree /path/to/worktree
|
|
184
|
+
// HEAD <sha>
|
|
185
|
+
// branch refs/heads/branch-name
|
|
186
|
+
// <blank line>
|
|
187
|
+
const worktrees: Worktree[] = []
|
|
188
|
+
const entries = stdout.split("\n\n").filter((e) => e.trim())
|
|
189
|
+
|
|
190
|
+
for (const entry of entries) {
|
|
191
|
+
const lines = entry.split("\n")
|
|
192
|
+
let path = ""
|
|
193
|
+
let head = ""
|
|
194
|
+
let branch: string | null = null
|
|
195
|
+
let isMain = false
|
|
196
|
+
|
|
197
|
+
for (const line of lines) {
|
|
198
|
+
if (line.startsWith("worktree ")) {
|
|
199
|
+
path = line.slice(9)
|
|
200
|
+
} else if (line.startsWith("HEAD ")) {
|
|
201
|
+
head = line.slice(5)
|
|
202
|
+
} else if (line.startsWith("branch ")) {
|
|
203
|
+
// refs/heads/branch-name -> branch-name
|
|
204
|
+
branch = line.slice(7).replace("refs/heads/", "")
|
|
205
|
+
} else if (line === "bare") {
|
|
206
|
+
isMain = true
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Main worktree is the one that's not in .worktrees
|
|
211
|
+
if (path && !path.includes(`/${WORKTREES_DIR}/`)) {
|
|
212
|
+
isMain = true
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (path && head) {
|
|
216
|
+
worktrees.push({ path, head, branch, isMain })
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return worktrees
|
|
221
|
+
} catch {
|
|
222
|
+
return []
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Remove a git worktree
|
|
228
|
+
*
|
|
229
|
+
* @param projectRoot - Absolute path to the project root
|
|
230
|
+
* @param name - Worktree name (issue number or custom name)
|
|
231
|
+
* @param options - Remove options
|
|
232
|
+
* @returns Result indicating success or failure
|
|
233
|
+
*/
|
|
234
|
+
export async function removeWorktree(
|
|
235
|
+
projectRoot: string,
|
|
236
|
+
name: string | number,
|
|
237
|
+
options?: {
|
|
238
|
+
/** Force removal even if there are uncommitted changes */
|
|
239
|
+
force?: boolean
|
|
240
|
+
/** Also delete the associated branch */
|
|
241
|
+
deleteBranch?: boolean
|
|
242
|
+
}
|
|
243
|
+
): Promise<WorktreeResult> {
|
|
244
|
+
const worktreePath = getWorktreePath(projectRoot, name)
|
|
245
|
+
const branchName = generateBranchName(name)
|
|
246
|
+
const { force = false, deleteBranch = true } = options ?? {}
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
// Remove the worktree
|
|
250
|
+
const args = ["git", "worktree", "remove", worktreePath]
|
|
251
|
+
if (force) {
|
|
252
|
+
args.push("--force")
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const proc = Bun.spawn(args, {
|
|
256
|
+
cwd: projectRoot,
|
|
257
|
+
stdout: "pipe",
|
|
258
|
+
stderr: "pipe",
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
const stderr = await new Response(proc.stderr).text()
|
|
262
|
+
const exitCode = await proc.exited
|
|
263
|
+
|
|
264
|
+
if (exitCode !== 0) {
|
|
265
|
+
// If directory doesn't exist, that's fine
|
|
266
|
+
if (stderr.includes("is not a working tree") || stderr.includes("No such file")) {
|
|
267
|
+
// Continue to branch deletion if requested
|
|
268
|
+
} else {
|
|
269
|
+
return {
|
|
270
|
+
success: false,
|
|
271
|
+
error: stderr.trim() || "Failed to remove worktree",
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Delete the branch if requested
|
|
277
|
+
if (deleteBranch) {
|
|
278
|
+
const branchArgs = ["git", "branch", "-D", branchName]
|
|
279
|
+
const branchProc = Bun.spawn(branchArgs, {
|
|
280
|
+
cwd: projectRoot,
|
|
281
|
+
stdout: "pipe",
|
|
282
|
+
stderr: "pipe",
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
// Don't fail if branch doesn't exist
|
|
286
|
+
await branchProc.exited
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return { success: true }
|
|
290
|
+
} catch (err) {
|
|
291
|
+
return {
|
|
292
|
+
success: false,
|
|
293
|
+
error: err instanceof Error ? err.message : "Failed to remove worktree",
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Check if a worktree exists
|
|
300
|
+
*
|
|
301
|
+
* @param projectRoot - Absolute path to the project root
|
|
302
|
+
* @param name - Worktree name (issue number or custom name)
|
|
303
|
+
* @returns True if worktree exists
|
|
304
|
+
*/
|
|
305
|
+
export async function worktreeExists(
|
|
306
|
+
projectRoot: string,
|
|
307
|
+
name: string | number
|
|
308
|
+
): Promise<boolean> {
|
|
309
|
+
const worktreePath = getWorktreePath(projectRoot, name)
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const worktrees = await listWorktrees(projectRoot)
|
|
313
|
+
return worktrees.some((wt) => wt.path === worktreePath)
|
|
314
|
+
} catch {
|
|
315
|
+
return false
|
|
316
|
+
}
|
|
317
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub CLI (gh) wrapper
|
|
3
|
+
*
|
|
4
|
+
* Provides validation and authentication checking via the gh CLI.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Types
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
/** Result of checking gh CLI installation and authentication */
|
|
12
|
+
export interface GhCheckResult {
|
|
13
|
+
/** Whether gh CLI is installed */
|
|
14
|
+
installed: boolean
|
|
15
|
+
/** Whether user is authenticated */
|
|
16
|
+
authenticated: boolean
|
|
17
|
+
/** Error message if check failed */
|
|
18
|
+
error?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Result of validating a GitHub repository */
|
|
22
|
+
export interface RepoValidation {
|
|
23
|
+
/** Whether the repository exists */
|
|
24
|
+
exists: boolean
|
|
25
|
+
/** Whether the repository is accessible to the user */
|
|
26
|
+
accessible: boolean
|
|
27
|
+
/** Default branch name (e.g., "main") */
|
|
28
|
+
defaultBranch?: string
|
|
29
|
+
/** Whether the repository is private */
|
|
30
|
+
isPrivate?: boolean
|
|
31
|
+
/** Error message if validation failed */
|
|
32
|
+
error?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// gh CLI Check
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if gh CLI is installed and authenticated
|
|
41
|
+
*/
|
|
42
|
+
export async function checkGhCli(): Promise<GhCheckResult> {
|
|
43
|
+
// First, check if gh is installed
|
|
44
|
+
try {
|
|
45
|
+
const versionProc = Bun.spawn(["gh", "--version"], {
|
|
46
|
+
stdout: "pipe",
|
|
47
|
+
stderr: "pipe",
|
|
48
|
+
})
|
|
49
|
+
const versionExit = await versionProc.exited
|
|
50
|
+
|
|
51
|
+
if (versionExit !== 0) {
|
|
52
|
+
return {
|
|
53
|
+
installed: false,
|
|
54
|
+
authenticated: false,
|
|
55
|
+
error: "GitHub CLI (gh) is not installed. Install it from https://cli.github.com/",
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
return {
|
|
60
|
+
installed: false,
|
|
61
|
+
authenticated: false,
|
|
62
|
+
error: "GitHub CLI (gh) is not installed. Install it from https://cli.github.com/",
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check authentication status
|
|
67
|
+
try {
|
|
68
|
+
const authProc = Bun.spawn(["gh", "auth", "status"], {
|
|
69
|
+
stdout: "pipe",
|
|
70
|
+
stderr: "pipe",
|
|
71
|
+
})
|
|
72
|
+
const stderr = await new Response(authProc.stderr).text()
|
|
73
|
+
const authExit = await authProc.exited
|
|
74
|
+
|
|
75
|
+
if (authExit !== 0) {
|
|
76
|
+
// Check for specific error messages
|
|
77
|
+
if (stderr.includes("not logged in")) {
|
|
78
|
+
return {
|
|
79
|
+
installed: true,
|
|
80
|
+
authenticated: false,
|
|
81
|
+
error: "Not authenticated with GitHub. Run: gh auth login",
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
installed: true,
|
|
86
|
+
authenticated: false,
|
|
87
|
+
error: stderr.trim() || "Not authenticated with GitHub. Run: gh auth login",
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
installed: true,
|
|
93
|
+
authenticated: true,
|
|
94
|
+
}
|
|
95
|
+
} catch (err) {
|
|
96
|
+
return {
|
|
97
|
+
installed: true,
|
|
98
|
+
authenticated: false,
|
|
99
|
+
error: err instanceof Error ? err.message : "Failed to check authentication status",
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ============================================================================
|
|
105
|
+
// Repository Validation
|
|
106
|
+
// ============================================================================
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Validate that a GitHub repository exists and is accessible
|
|
110
|
+
*
|
|
111
|
+
* @param ownerRepo - Repository in "owner/repo" format
|
|
112
|
+
*/
|
|
113
|
+
export async function validateGhRepo(ownerRepo: string): Promise<RepoValidation> {
|
|
114
|
+
try {
|
|
115
|
+
const proc = Bun.spawn(
|
|
116
|
+
["gh", "repo", "view", ownerRepo, "--json", "name,defaultBranchRef,isPrivate"],
|
|
117
|
+
{
|
|
118
|
+
stdout: "pipe",
|
|
119
|
+
stderr: "pipe",
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
const stdout = await new Response(proc.stdout).text()
|
|
124
|
+
const stderr = await new Response(proc.stderr).text()
|
|
125
|
+
const exitCode = await proc.exited
|
|
126
|
+
|
|
127
|
+
if (exitCode !== 0) {
|
|
128
|
+
// Parse common error messages
|
|
129
|
+
if (stderr.includes("Could not resolve to a Repository")) {
|
|
130
|
+
return {
|
|
131
|
+
exists: false,
|
|
132
|
+
accessible: false,
|
|
133
|
+
error: `Repository '${ownerRepo}' not found. Check the name and try again.`,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (stderr.includes("HTTP 404")) {
|
|
137
|
+
return {
|
|
138
|
+
exists: false,
|
|
139
|
+
accessible: false,
|
|
140
|
+
error: `Repository '${ownerRepo}' not found or you don't have access.`,
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (stderr.includes("HTTP 403") || stderr.includes("must have push access")) {
|
|
144
|
+
return {
|
|
145
|
+
exists: true,
|
|
146
|
+
accessible: false,
|
|
147
|
+
error: `You don't have access to '${ownerRepo}'. Check your permissions.`,
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (stderr.includes("not logged in")) {
|
|
151
|
+
return {
|
|
152
|
+
exists: false,
|
|
153
|
+
accessible: false,
|
|
154
|
+
error: "Not authenticated with GitHub. Run: gh auth login",
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
exists: false,
|
|
160
|
+
accessible: false,
|
|
161
|
+
error: stderr.trim() || "Failed to validate repository",
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Parse JSON response
|
|
166
|
+
try {
|
|
167
|
+
const data = JSON.parse(stdout) as {
|
|
168
|
+
name?: string
|
|
169
|
+
defaultBranchRef?: { name?: string }
|
|
170
|
+
isPrivate?: boolean
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
exists: true,
|
|
175
|
+
accessible: true,
|
|
176
|
+
defaultBranch: data.defaultBranchRef?.name,
|
|
177
|
+
isPrivate: data.isPrivate,
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
// JSON parse failed but command succeeded - repo exists
|
|
181
|
+
return {
|
|
182
|
+
exists: true,
|
|
183
|
+
accessible: true,
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} catch (err) {
|
|
187
|
+
return {
|
|
188
|
+
exists: false,
|
|
189
|
+
accessible: false,
|
|
190
|
+
error: err instanceof Error ? err.message : "Failed to validate repository",
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Check if gh CLI is available (quick check, no auth validation)
|
|
197
|
+
*/
|
|
198
|
+
export async function isGhInstalled(): Promise<boolean> {
|
|
199
|
+
try {
|
|
200
|
+
const proc = Bun.spawn(["gh", "--version"], {
|
|
201
|
+
stdout: "pipe",
|
|
202
|
+
stderr: "pipe",
|
|
203
|
+
})
|
|
204
|
+
return (await proc.exited) === 0
|
|
205
|
+
} catch {
|
|
206
|
+
return false
|
|
207
|
+
}
|
|
208
|
+
}
|