@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,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub issue fetching via gh CLI
|
|
3
|
+
*
|
|
4
|
+
* Provides functions for fetching and viewing GitHub issues.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Types
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
/** Issue state filter */
|
|
12
|
+
export type IssueState = "open" | "closed" | "all"
|
|
13
|
+
|
|
14
|
+
/** Issue label information */
|
|
15
|
+
export interface IssueLabel {
|
|
16
|
+
/** Label name */
|
|
17
|
+
name: string
|
|
18
|
+
/** Label color (hex without #) */
|
|
19
|
+
color: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** GitHub issue data */
|
|
23
|
+
export interface GitHubIssue {
|
|
24
|
+
/** Issue number */
|
|
25
|
+
number: number
|
|
26
|
+
/** Issue title */
|
|
27
|
+
title: string
|
|
28
|
+
/** Issue body/description (may be null) */
|
|
29
|
+
body: string | null
|
|
30
|
+
/** Issue state */
|
|
31
|
+
state: "OPEN" | "CLOSED"
|
|
32
|
+
/** Issue URL on GitHub */
|
|
33
|
+
url: string
|
|
34
|
+
/** Labels attached to the issue */
|
|
35
|
+
labels: IssueLabel[]
|
|
36
|
+
/** ISO timestamp when issue was created */
|
|
37
|
+
createdAt: string
|
|
38
|
+
/** ISO timestamp when issue was last updated */
|
|
39
|
+
updatedAt: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Options for fetching issues */
|
|
43
|
+
export interface FetchIssuesOptions {
|
|
44
|
+
/** Filter by state (default: "open") */
|
|
45
|
+
state?: IssueState
|
|
46
|
+
/** Filter by labels (AND logic) */
|
|
47
|
+
labels?: string[]
|
|
48
|
+
/** Maximum number of issues to fetch (default: 50) */
|
|
49
|
+
limit?: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Result of fetching issues */
|
|
53
|
+
export interface FetchIssuesResult {
|
|
54
|
+
/** Whether the fetch succeeded */
|
|
55
|
+
success: boolean
|
|
56
|
+
/** Fetched issues */
|
|
57
|
+
issues: GitHubIssue[]
|
|
58
|
+
/** Error message if fetch failed */
|
|
59
|
+
error?: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Result of fetching a single issue */
|
|
63
|
+
export interface FetchIssueResult {
|
|
64
|
+
/** Whether the fetch succeeded */
|
|
65
|
+
success: boolean
|
|
66
|
+
/** The fetched issue (null if not found) */
|
|
67
|
+
issue: GitHubIssue | null
|
|
68
|
+
/** Error message if fetch failed */
|
|
69
|
+
error?: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// Constants
|
|
74
|
+
// ============================================================================
|
|
75
|
+
|
|
76
|
+
/** Default timeout for GitHub API operations in milliseconds */
|
|
77
|
+
const DEFAULT_TIMEOUT_MS = 30000
|
|
78
|
+
|
|
79
|
+
// ============================================================================
|
|
80
|
+
// Issue Fetching
|
|
81
|
+
// ============================================================================
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Fetch issues from a GitHub repository
|
|
85
|
+
*
|
|
86
|
+
* @param ownerRepo - Repository in "owner/repo" format
|
|
87
|
+
* @param options - Fetch options (state, labels, limit)
|
|
88
|
+
* @returns Result with fetched issues or error
|
|
89
|
+
*/
|
|
90
|
+
export async function fetchIssues(
|
|
91
|
+
ownerRepo: string,
|
|
92
|
+
options?: FetchIssuesOptions
|
|
93
|
+
): Promise<FetchIssuesResult> {
|
|
94
|
+
const { state = "open", labels = [], limit = 50 } = options ?? {}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
// Build command arguments
|
|
98
|
+
const args = [
|
|
99
|
+
"gh",
|
|
100
|
+
"issue",
|
|
101
|
+
"list",
|
|
102
|
+
"--repo",
|
|
103
|
+
ownerRepo,
|
|
104
|
+
"--state",
|
|
105
|
+
state,
|
|
106
|
+
"--limit",
|
|
107
|
+
String(limit),
|
|
108
|
+
"--json",
|
|
109
|
+
"number,title,body,state,url,labels,createdAt,updatedAt",
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
// Add label filters
|
|
113
|
+
for (const label of labels) {
|
|
114
|
+
args.push("--label", label)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const proc = Bun.spawn(args, {
|
|
118
|
+
stdout: "pipe",
|
|
119
|
+
stderr: "pipe",
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// Race between process completion and timeout
|
|
123
|
+
const timeoutPromise = new Promise<"timeout">((resolve) =>
|
|
124
|
+
setTimeout(() => resolve("timeout"), DEFAULT_TIMEOUT_MS)
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
const raceResult = await Promise.race([proc.exited, timeoutPromise])
|
|
128
|
+
|
|
129
|
+
if (raceResult === "timeout") {
|
|
130
|
+
// Kill the process if it's still running
|
|
131
|
+
try {
|
|
132
|
+
proc.kill()
|
|
133
|
+
} catch {
|
|
134
|
+
// Ignore kill errors
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
success: false,
|
|
138
|
+
issues: [],
|
|
139
|
+
error: "Request timed out. Check your internet connection.",
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const stdout = await new Response(proc.stdout).text()
|
|
144
|
+
const stderr = await new Response(proc.stderr).text()
|
|
145
|
+
const exitCode = raceResult
|
|
146
|
+
|
|
147
|
+
if (exitCode !== 0) {
|
|
148
|
+
return {
|
|
149
|
+
success: false,
|
|
150
|
+
issues: [],
|
|
151
|
+
error: parseGhError(stderr),
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Parse JSON response
|
|
156
|
+
try {
|
|
157
|
+
const rawIssues = JSON.parse(stdout) as Array<{
|
|
158
|
+
number: number
|
|
159
|
+
title: string
|
|
160
|
+
body: string | null
|
|
161
|
+
state: string
|
|
162
|
+
url: string
|
|
163
|
+
labels: Array<{ name: string; color: string }>
|
|
164
|
+
createdAt: string
|
|
165
|
+
updatedAt: string
|
|
166
|
+
}>
|
|
167
|
+
|
|
168
|
+
const issues: GitHubIssue[] = rawIssues.map((raw) => ({
|
|
169
|
+
number: raw.number,
|
|
170
|
+
title: raw.title,
|
|
171
|
+
body: raw.body,
|
|
172
|
+
state: raw.state as "OPEN" | "CLOSED",
|
|
173
|
+
url: raw.url,
|
|
174
|
+
labels: raw.labels.map((l) => ({ name: l.name, color: l.color })),
|
|
175
|
+
createdAt: raw.createdAt,
|
|
176
|
+
updatedAt: raw.updatedAt,
|
|
177
|
+
}))
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
success: true,
|
|
181
|
+
issues,
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
return {
|
|
185
|
+
success: false,
|
|
186
|
+
issues: [],
|
|
187
|
+
error: "Failed to parse GitHub response",
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
} catch (err) {
|
|
191
|
+
const message = err instanceof Error ? err.message : "Unknown error"
|
|
192
|
+
return {
|
|
193
|
+
success: false,
|
|
194
|
+
issues: [],
|
|
195
|
+
error: `Failed to fetch issues: ${message}`,
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Fetch a single issue by number
|
|
202
|
+
*
|
|
203
|
+
* @param ownerRepo - Repository in "owner/repo" format
|
|
204
|
+
* @param issueNumber - Issue number
|
|
205
|
+
* @returns Result with the issue or error
|
|
206
|
+
*/
|
|
207
|
+
export async function getIssue(
|
|
208
|
+
ownerRepo: string,
|
|
209
|
+
issueNumber: number
|
|
210
|
+
): Promise<FetchIssueResult> {
|
|
211
|
+
try {
|
|
212
|
+
const proc = Bun.spawn(
|
|
213
|
+
[
|
|
214
|
+
"gh",
|
|
215
|
+
"issue",
|
|
216
|
+
"view",
|
|
217
|
+
String(issueNumber),
|
|
218
|
+
"--repo",
|
|
219
|
+
ownerRepo,
|
|
220
|
+
"--json",
|
|
221
|
+
"number,title,body,state,url,labels,createdAt,updatedAt",
|
|
222
|
+
],
|
|
223
|
+
{
|
|
224
|
+
stdout: "pipe",
|
|
225
|
+
stderr: "pipe",
|
|
226
|
+
}
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
const stdout = await new Response(proc.stdout).text()
|
|
230
|
+
const stderr = await new Response(proc.stderr).text()
|
|
231
|
+
const exitCode = await proc.exited
|
|
232
|
+
|
|
233
|
+
if (exitCode !== 0) {
|
|
234
|
+
return {
|
|
235
|
+
success: false,
|
|
236
|
+
issue: null,
|
|
237
|
+
error: parseGhError(stderr),
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Parse JSON response
|
|
242
|
+
try {
|
|
243
|
+
const raw = JSON.parse(stdout) as {
|
|
244
|
+
number: number
|
|
245
|
+
title: string
|
|
246
|
+
body: string | null
|
|
247
|
+
state: string
|
|
248
|
+
url: string
|
|
249
|
+
labels: Array<{ name: string; color: string }>
|
|
250
|
+
createdAt: string
|
|
251
|
+
updatedAt: string
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const issue: GitHubIssue = {
|
|
255
|
+
number: raw.number,
|
|
256
|
+
title: raw.title,
|
|
257
|
+
body: raw.body,
|
|
258
|
+
state: raw.state as "OPEN" | "CLOSED",
|
|
259
|
+
url: raw.url,
|
|
260
|
+
labels: raw.labels.map((l) => ({ name: l.name, color: l.color })),
|
|
261
|
+
createdAt: raw.createdAt,
|
|
262
|
+
updatedAt: raw.updatedAt,
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
success: true,
|
|
267
|
+
issue,
|
|
268
|
+
}
|
|
269
|
+
} catch {
|
|
270
|
+
return {
|
|
271
|
+
success: false,
|
|
272
|
+
issue: null,
|
|
273
|
+
error: "Failed to parse GitHub response",
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} catch (err) {
|
|
277
|
+
return {
|
|
278
|
+
success: false,
|
|
279
|
+
issue: null,
|
|
280
|
+
error: err instanceof Error ? err.message : "Failed to fetch issue",
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ============================================================================
|
|
286
|
+
// Helpers
|
|
287
|
+
// ============================================================================
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Parse gh CLI error messages into user-friendly strings
|
|
291
|
+
*/
|
|
292
|
+
function parseGhError(stderr: string): string {
|
|
293
|
+
const trimmed = stderr.trim()
|
|
294
|
+
|
|
295
|
+
if (trimmed.includes("not logged in")) {
|
|
296
|
+
return "Not authenticated with GitHub. Run: gh auth login"
|
|
297
|
+
}
|
|
298
|
+
if (trimmed.includes("Could not resolve to a Repository")) {
|
|
299
|
+
return "Repository not found. Check the repository name."
|
|
300
|
+
}
|
|
301
|
+
if (trimmed.includes("HTTP 404")) {
|
|
302
|
+
return "Repository not found or you don't have access."
|
|
303
|
+
}
|
|
304
|
+
if (trimmed.includes("HTTP 403")) {
|
|
305
|
+
return "Access denied. Check your permissions."
|
|
306
|
+
}
|
|
307
|
+
if (trimmed.includes("no issues match")) {
|
|
308
|
+
return "No issues found matching the criteria."
|
|
309
|
+
}
|
|
310
|
+
if (trimmed.includes("could not find issue")) {
|
|
311
|
+
return "Issue not found."
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return trimmed || "Failed to fetch from GitHub"
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Format relative time from an ISO timestamp
|
|
319
|
+
*
|
|
320
|
+
* @param isoString - ISO timestamp string
|
|
321
|
+
* @returns Human-readable relative time (e.g., "3 days ago")
|
|
322
|
+
*/
|
|
323
|
+
export function formatRelativeTime(isoString: string): string {
|
|
324
|
+
const date = new Date(isoString)
|
|
325
|
+
const now = new Date()
|
|
326
|
+
const diffMs = now.getTime() - date.getTime()
|
|
327
|
+
|
|
328
|
+
const seconds = Math.floor(diffMs / 1000)
|
|
329
|
+
const minutes = Math.floor(seconds / 60)
|
|
330
|
+
const hours = Math.floor(minutes / 60)
|
|
331
|
+
const days = Math.floor(hours / 24)
|
|
332
|
+
const weeks = Math.floor(days / 7)
|
|
333
|
+
const months = Math.floor(days / 30)
|
|
334
|
+
|
|
335
|
+
if (months > 0) {
|
|
336
|
+
return months === 1 ? "1 month ago" : `${months} months ago`
|
|
337
|
+
}
|
|
338
|
+
if (weeks > 0) {
|
|
339
|
+
return weeks === 1 ? "1 week ago" : `${weeks} weeks ago`
|
|
340
|
+
}
|
|
341
|
+
if (days > 0) {
|
|
342
|
+
return days === 1 ? "1 day ago" : `${days} days ago`
|
|
343
|
+
}
|
|
344
|
+
if (hours > 0) {
|
|
345
|
+
return hours === 1 ? "1 hour ago" : `${hours} hours ago`
|
|
346
|
+
}
|
|
347
|
+
if (minutes > 0) {
|
|
348
|
+
return minutes === 1 ? "1 min ago" : `${minutes} mins ago`
|
|
349
|
+
}
|
|
350
|
+
return "just now"
|
|
351
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* OpenSWE - AI-powered Software Engineering orchestration
|
|
4
|
+
*
|
|
5
|
+
* Main entry point with CLI argument parsing, config loading, and workspace detection.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// IMPORTANT: Must import preload before any JSX code to register Solid.js JSX runtime.
|
|
9
|
+
// This is needed because bunfig.toml is only read from cwd, not the package directory.
|
|
10
|
+
import "@opentui/solid/preload"
|
|
11
|
+
|
|
12
|
+
import { join } from "path"
|
|
13
|
+
import yargs from "yargs"
|
|
14
|
+
import { hideBin } from "yargs/helpers"
|
|
15
|
+
import { loadConfig, getConfigPath, type GlobalConfig, type AIBackend } from "./config"
|
|
16
|
+
import { logger, setLogLevel, initFileLogging } from "./utils/logger"
|
|
17
|
+
import { checkPrerequisites, formatPrerequisiteErrors } from "./utils/prerequisites"
|
|
18
|
+
import { closeDatabase } from "./store"
|
|
19
|
+
import {
|
|
20
|
+
detectWorkspace,
|
|
21
|
+
loadProjectConfig,
|
|
22
|
+
updateLastOpened,
|
|
23
|
+
getLogsDir,
|
|
24
|
+
type WorkspaceResult,
|
|
25
|
+
} from "./workspace"
|
|
26
|
+
import {
|
|
27
|
+
runEmptyDirectoryWizard,
|
|
28
|
+
runExistingRepoWizard,
|
|
29
|
+
runReconfigureWizard,
|
|
30
|
+
} from "./wizard"
|
|
31
|
+
|
|
32
|
+
// NOTE: startTUI is dynamically imported to ensure the preload runs first.
|
|
33
|
+
// Static imports are hoisted and parsed before any code executes, which would
|
|
34
|
+
// cause app.tsx to be parsed before the Solid.js JSX runtime is registered.
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// CLI Argument Types
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
interface CLIArgs {
|
|
41
|
+
repo?: string
|
|
42
|
+
setup?: boolean
|
|
43
|
+
status?: boolean
|
|
44
|
+
backend?: "opencode" | "claude"
|
|
45
|
+
model?: string
|
|
46
|
+
debug?: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// CLI Argument Parsing
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
// Parse CLI arguments
|
|
54
|
+
const argv = await yargs(hideBin(process.argv))
|
|
55
|
+
.scriptName("openswe")
|
|
56
|
+
.usage("$0 [options]")
|
|
57
|
+
.option("repo", {
|
|
58
|
+
alias: "r",
|
|
59
|
+
type: "string",
|
|
60
|
+
description: "GitHub repository (owner/repo) to link",
|
|
61
|
+
})
|
|
62
|
+
.option("setup", {
|
|
63
|
+
type: "boolean",
|
|
64
|
+
description: "Force re-run the setup wizard",
|
|
65
|
+
})
|
|
66
|
+
.option("status", {
|
|
67
|
+
type: "boolean",
|
|
68
|
+
description: "Show project status without TUI",
|
|
69
|
+
})
|
|
70
|
+
.option("backend", {
|
|
71
|
+
type: "string",
|
|
72
|
+
choices: ["opencode", "claude"] as const,
|
|
73
|
+
description: "AI backend to use",
|
|
74
|
+
})
|
|
75
|
+
.option("model", {
|
|
76
|
+
type: "string",
|
|
77
|
+
description: "AI model to use (e.g. claude-3-5-sonnet-20240620)",
|
|
78
|
+
})
|
|
79
|
+
.option("debug", {
|
|
80
|
+
type: "boolean",
|
|
81
|
+
description: "Enable debug logging",
|
|
82
|
+
})
|
|
83
|
+
.help()
|
|
84
|
+
.alias("h", "help")
|
|
85
|
+
.version("0.1.0")
|
|
86
|
+
.alias("v", "version")
|
|
87
|
+
.parse() as CLIArgs
|
|
88
|
+
|
|
89
|
+
// ============================================================================
|
|
90
|
+
// Main Function
|
|
91
|
+
// ============================================================================
|
|
92
|
+
|
|
93
|
+
async function main() {
|
|
94
|
+
// Load configuration with CLI overrides
|
|
95
|
+
const config = await loadConfig({
|
|
96
|
+
backend: argv.backend as AIBackend | undefined,
|
|
97
|
+
model: argv.model,
|
|
98
|
+
debug: argv.debug,
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// Apply log level from merged config
|
|
102
|
+
setLogLevel(config.advanced.logLevel)
|
|
103
|
+
|
|
104
|
+
logger.debug("OpenSWE starting...")
|
|
105
|
+
logger.debug("Config file path:", getConfigPath())
|
|
106
|
+
logger.debug("Loaded configuration:", config)
|
|
107
|
+
|
|
108
|
+
// Detect workspace type
|
|
109
|
+
const cwd = process.cwd()
|
|
110
|
+
const workspace = await detectWorkspace(cwd)
|
|
111
|
+
|
|
112
|
+
logger.debug("Workspace detection result:", workspace)
|
|
113
|
+
|
|
114
|
+
// Show status mode (uses workspace info)
|
|
115
|
+
if (argv.status) {
|
|
116
|
+
await showStatus(config, workspace)
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Handle workspace based on type
|
|
121
|
+
await handleWorkspace(workspace, config, argv)
|
|
122
|
+
|
|
123
|
+
exitApp(0)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ============================================================================
|
|
127
|
+
// Exit Handling
|
|
128
|
+
// ============================================================================
|
|
129
|
+
|
|
130
|
+
function exitApp(code: number): never {
|
|
131
|
+
try {
|
|
132
|
+
closeDatabase()
|
|
133
|
+
} catch {
|
|
134
|
+
// Ignore close errors during shutdown
|
|
135
|
+
}
|
|
136
|
+
process.exit(code)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// Workspace Handling
|
|
141
|
+
// ============================================================================
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Handle the detected workspace type
|
|
145
|
+
*/
|
|
146
|
+
async function handleWorkspace(
|
|
147
|
+
workspace: WorkspaceResult,
|
|
148
|
+
config: GlobalConfig,
|
|
149
|
+
args: CLIArgs
|
|
150
|
+
): Promise<void> {
|
|
151
|
+
// Handle --setup flag for reconfiguration
|
|
152
|
+
if (args.setup && workspace.type === "existing-project") {
|
|
153
|
+
const projectConfig = await loadProjectConfig(workspace.projectRoot)
|
|
154
|
+
if (projectConfig) {
|
|
155
|
+
const result = await runReconfigureWizard(
|
|
156
|
+
workspace.projectRoot,
|
|
157
|
+
projectConfig.repoFullName
|
|
158
|
+
)
|
|
159
|
+
if (result.cancelled) {
|
|
160
|
+
logger.info("Setup cancelled.")
|
|
161
|
+
exitApp(0)
|
|
162
|
+
}
|
|
163
|
+
if (!result.completed) {
|
|
164
|
+
logger.error("Setup failed:", result.error)
|
|
165
|
+
exitApp(1)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Continue to normal project handling after reconfiguration
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
switch (workspace.type) {
|
|
172
|
+
case "existing-project":
|
|
173
|
+
await handleExistingProject(workspace, config)
|
|
174
|
+
break
|
|
175
|
+
|
|
176
|
+
case "existing-repo":
|
|
177
|
+
await handleExistingRepo(workspace, args)
|
|
178
|
+
break
|
|
179
|
+
|
|
180
|
+
case "empty":
|
|
181
|
+
await handleEmptyDirectory(workspace, args)
|
|
182
|
+
break
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Handle existing OpenSWE project - load and continue
|
|
188
|
+
*/
|
|
189
|
+
async function handleExistingProject(
|
|
190
|
+
workspace: WorkspaceResult,
|
|
191
|
+
config: GlobalConfig
|
|
192
|
+
): Promise<void> {
|
|
193
|
+
// Initialize file logging
|
|
194
|
+
const logsDir = getLogsDir(workspace.projectRoot)
|
|
195
|
+
initFileLogging(join(logsDir, "openswe.log"))
|
|
196
|
+
|
|
197
|
+
// Check prerequisites before launching TUI
|
|
198
|
+
const prereqResult = await checkPrerequisites()
|
|
199
|
+
if (!prereqResult.success) {
|
|
200
|
+
logger.error(formatPrerequisiteErrors(prereqResult))
|
|
201
|
+
exitApp(1)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Log warnings if any
|
|
205
|
+
if (prereqResult.warnings.length > 0) {
|
|
206
|
+
for (const warning of prereqResult.warnings) {
|
|
207
|
+
logger.warn(warning)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Load project config and update lastOpened
|
|
212
|
+
const projectConfig = await loadProjectConfig(workspace.projectRoot)
|
|
213
|
+
if (projectConfig) {
|
|
214
|
+
await updateLastOpened(workspace.projectRoot)
|
|
215
|
+
logger.debug("Project config:", projectConfig)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Launch TUI - dynamic import to ensure preload runs first
|
|
219
|
+
const { startTUI } = await import("./app")
|
|
220
|
+
await startTUI(config, workspace.projectRoot)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Handle existing git repo - offer to adopt
|
|
225
|
+
*/
|
|
226
|
+
async function handleExistingRepo(
|
|
227
|
+
workspace: WorkspaceResult,
|
|
228
|
+
args: CLIArgs
|
|
229
|
+
): Promise<void> {
|
|
230
|
+
// If we don't have a repo name, we can't run the wizard
|
|
231
|
+
if (!workspace.repoFullName) {
|
|
232
|
+
logger.error("Could not detect repository name from git remote.")
|
|
233
|
+
logger.error("Please ensure you have a remote named 'origin' configured.")
|
|
234
|
+
exitApp(1)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Run the adoption wizard
|
|
238
|
+
const result = await runExistingRepoWizard(
|
|
239
|
+
workspace.projectRoot,
|
|
240
|
+
workspace.repoFullName,
|
|
241
|
+
workspace.remoteUrl ?? undefined
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
if (result.cancelled) {
|
|
245
|
+
logger.info("Setup cancelled.")
|
|
246
|
+
exitApp(0)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (!result.completed) {
|
|
250
|
+
logger.error("Setup failed:", result.error)
|
|
251
|
+
exitApp(1)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Wizard completed successfully - show summary
|
|
255
|
+
const updatedConfig = await loadConfig({
|
|
256
|
+
backend: args.backend as AIBackend | undefined,
|
|
257
|
+
debug: args.debug,
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
logger.debug("")
|
|
261
|
+
logConfigSummary(updatedConfig)
|
|
262
|
+
logger.debug("")
|
|
263
|
+
logger.debug("OpenSWE initialized (Phase 4 - wizard complete)")
|
|
264
|
+
logger.debug("Launching TUI...")
|
|
265
|
+
|
|
266
|
+
await handleExistingProject(
|
|
267
|
+
{
|
|
268
|
+
type: "existing-project",
|
|
269
|
+
projectRoot: workspace.projectRoot,
|
|
270
|
+
},
|
|
271
|
+
updatedConfig
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Handle empty directory - need full setup
|
|
277
|
+
*/
|
|
278
|
+
async function handleEmptyDirectory(
|
|
279
|
+
workspace: WorkspaceResult,
|
|
280
|
+
args: CLIArgs
|
|
281
|
+
): Promise<void> {
|
|
282
|
+
// Run the empty directory wizard
|
|
283
|
+
const result = await runEmptyDirectoryWizard(workspace.projectRoot, args.repo)
|
|
284
|
+
|
|
285
|
+
if (result.cancelled) {
|
|
286
|
+
logger.info("Setup cancelled.")
|
|
287
|
+
exitApp(0)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!result.completed) {
|
|
291
|
+
logger.error("Setup failed:", result.error)
|
|
292
|
+
exitApp(1)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Wizard completed successfully - show summary
|
|
296
|
+
const updatedConfig = await loadConfig({
|
|
297
|
+
backend: args.backend as AIBackend | undefined,
|
|
298
|
+
debug: args.debug,
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
logger.info("")
|
|
302
|
+
logConfigSummary(updatedConfig)
|
|
303
|
+
logger.info("")
|
|
304
|
+
logger.info("OpenSWE initialized (Phase 4 - wizard complete)")
|
|
305
|
+
logger.info("Launching TUI...")
|
|
306
|
+
|
|
307
|
+
await handleExistingProject(
|
|
308
|
+
{
|
|
309
|
+
type: "existing-project",
|
|
310
|
+
projectRoot: workspace.projectRoot,
|
|
311
|
+
},
|
|
312
|
+
updatedConfig
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ============================================================================
|
|
317
|
+
// Status Display
|
|
318
|
+
// ============================================================================
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Show project status (non-TUI mode)
|
|
322
|
+
*/
|
|
323
|
+
async function showStatus(config: GlobalConfig, workspace: WorkspaceResult): Promise<void> {
|
|
324
|
+
console.log("OpenSWE Status")
|
|
325
|
+
console.log("==============")
|
|
326
|
+
console.log("")
|
|
327
|
+
console.log("Configuration:")
|
|
328
|
+
console.log(` Config file: ${getConfigPath()}`)
|
|
329
|
+
console.log(` AI Backend: ${config.ai.backend}`)
|
|
330
|
+
console.log(` Log Level: ${config.advanced.logLevel}`)
|
|
331
|
+
console.log("")
|
|
332
|
+
console.log("Workspace:")
|
|
333
|
+
console.log(` Type: ${workspace.type}`)
|
|
334
|
+
console.log(` Path: ${workspace.projectRoot}`)
|
|
335
|
+
|
|
336
|
+
if (workspace.type === "existing-repo" && workspace.repoFullName) {
|
|
337
|
+
console.log(` Repository: ${workspace.repoFullName}`)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (workspace.type === "existing-project") {
|
|
341
|
+
console.log(" Status: Initialized")
|
|
342
|
+
// TODO: Phase 5 - show session count, active tasks, etc.
|
|
343
|
+
} else if (workspace.type === "existing-repo") {
|
|
344
|
+
console.log(" Status: Git repo detected (not yet initialized)")
|
|
345
|
+
} else {
|
|
346
|
+
console.log(" Status: Empty directory (needs setup)")
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
console.log("")
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Log configuration summary
|
|
354
|
+
*/
|
|
355
|
+
function logConfigSummary(config: GlobalConfig): void {
|
|
356
|
+
logger.debug("Configuration:")
|
|
357
|
+
logger.debug(` AI Backend: ${config.ai.backend}`)
|
|
358
|
+
logger.debug(` Log Level: ${config.advanced.logLevel}`)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ============================================================================
|
|
362
|
+
// Entry Point
|
|
363
|
+
// ============================================================================
|
|
364
|
+
|
|
365
|
+
// Run main
|
|
366
|
+
main().catch((err) => {
|
|
367
|
+
logger.error("Fatal error:", err)
|
|
368
|
+
exitApp(1)
|
|
369
|
+
})
|
|
File without changes
|