@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,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