@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,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wizard flows
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates the first-run wizard based on workspace type.
|
|
5
|
+
* Coordinates prompts, validation, cloning, and configuration saving.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
promptRepoInput,
|
|
10
|
+
promptAiBackend,
|
|
11
|
+
promptAdoptRepo,
|
|
12
|
+
promptFetchIssues,
|
|
13
|
+
isCancelled,
|
|
14
|
+
intro,
|
|
15
|
+
outro,
|
|
16
|
+
note,
|
|
17
|
+
spinner,
|
|
18
|
+
logError,
|
|
19
|
+
logSuccess,
|
|
20
|
+
logWarning,
|
|
21
|
+
type AIBackendChoice,
|
|
22
|
+
} from "./prompts"
|
|
23
|
+
import { checkGhCli, validateGhRepo } from "../github/client"
|
|
24
|
+
import { cloneRepo, isValidOwnerRepo, getCloneUrl } from "../git/repo"
|
|
25
|
+
import { initProject } from "../workspace/init"
|
|
26
|
+
import {
|
|
27
|
+
saveProjectConfig,
|
|
28
|
+
createProjectConfig,
|
|
29
|
+
} from "../workspace/project"
|
|
30
|
+
import { saveGlobalConfig } from "../config/global"
|
|
31
|
+
import type { PartialConfig } from "../config/types"
|
|
32
|
+
import { fetchIssues } from "../github"
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Types
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
/** Result of a wizard flow */
|
|
39
|
+
export interface WizardResult {
|
|
40
|
+
/** Whether the wizard completed successfully */
|
|
41
|
+
completed: boolean
|
|
42
|
+
/** Whether the user cancelled the wizard */
|
|
43
|
+
cancelled: boolean
|
|
44
|
+
/** Error message if the wizard failed */
|
|
45
|
+
error?: string
|
|
46
|
+
/** Repository full name (owner/repo) if set */
|
|
47
|
+
repoFullName?: string
|
|
48
|
+
/** Selected AI backend */
|
|
49
|
+
aiBackend?: AIBackendChoice
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// Prerequisite Checks
|
|
54
|
+
// ============================================================================
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check prerequisites for the wizard (gh CLI)
|
|
58
|
+
* Returns true if all prerequisites are met
|
|
59
|
+
*/
|
|
60
|
+
async function checkPrerequisites(): Promise<{ ok: boolean; error?: string }> {
|
|
61
|
+
const ghCheck = await checkGhCli()
|
|
62
|
+
|
|
63
|
+
if (!ghCheck.installed) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
error: ghCheck.error ?? "GitHub CLI (gh) is not installed.",
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!ghCheck.authenticated) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
error: ghCheck.error ?? "Not authenticated with GitHub CLI.",
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { ok: true }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// Empty Directory Flow
|
|
82
|
+
// ============================================================================
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Run the wizard for an empty directory
|
|
86
|
+
*
|
|
87
|
+
* This flow:
|
|
88
|
+
* 1. Validates prerequisites (gh CLI)
|
|
89
|
+
* 2. Prompts for repository (or uses --repo flag value)
|
|
90
|
+
* 3. Validates the repository exists
|
|
91
|
+
* 4. Clones the repository
|
|
92
|
+
* 5. Prompts for configuration (AI backend)
|
|
93
|
+
* 6. Saves configuration
|
|
94
|
+
*
|
|
95
|
+
* @param cwd - Current working directory (empty directory)
|
|
96
|
+
* @param cliRepo - Repository from --repo flag (optional)
|
|
97
|
+
*/
|
|
98
|
+
export async function runEmptyDirectoryWizard(
|
|
99
|
+
cwd: string,
|
|
100
|
+
cliRepo?: string
|
|
101
|
+
): Promise<WizardResult> {
|
|
102
|
+
intro("OpenSWE Setup")
|
|
103
|
+
|
|
104
|
+
// Check prerequisites
|
|
105
|
+
const prereq = await checkPrerequisites()
|
|
106
|
+
if (!prereq.ok) {
|
|
107
|
+
logError(prereq.error!)
|
|
108
|
+
return { completed: false, cancelled: false, error: prereq.error }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Get repository (from flag or prompt)
|
|
112
|
+
let repoFullName: string
|
|
113
|
+
|
|
114
|
+
if (cliRepo) {
|
|
115
|
+
// Validate format
|
|
116
|
+
if (!isValidOwnerRepo(cliRepo)) {
|
|
117
|
+
logError(`Invalid repository format: ${cliRepo}`)
|
|
118
|
+
return {
|
|
119
|
+
completed: false,
|
|
120
|
+
cancelled: false,
|
|
121
|
+
error: "Invalid repository format. Use owner/repo format.",
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
repoFullName = cliRepo
|
|
125
|
+
note(`Repository: ${repoFullName}`, "From --repo flag")
|
|
126
|
+
} else {
|
|
127
|
+
// Prompt for repository
|
|
128
|
+
const repoResult = await promptRepoInput()
|
|
129
|
+
if (isCancelled(repoResult)) {
|
|
130
|
+
return { completed: false, cancelled: true }
|
|
131
|
+
}
|
|
132
|
+
repoFullName = repoResult
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Validate repository exists and is accessible
|
|
136
|
+
const s = spinner()
|
|
137
|
+
s.start(`Validating repository ${repoFullName}...`)
|
|
138
|
+
|
|
139
|
+
const validation = await validateGhRepo(repoFullName)
|
|
140
|
+
|
|
141
|
+
if (!validation.exists || !validation.accessible) {
|
|
142
|
+
s.stop(`Repository validation failed`)
|
|
143
|
+
logError(validation.error ?? `Cannot access repository: ${repoFullName}`)
|
|
144
|
+
return {
|
|
145
|
+
completed: false,
|
|
146
|
+
cancelled: false,
|
|
147
|
+
error: validation.error ?? "Repository validation failed",
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
s.stop(`Repository validated: ${repoFullName}`)
|
|
152
|
+
|
|
153
|
+
if (validation.isPrivate) {
|
|
154
|
+
note("This is a private repository", "Access")
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Clone repository
|
|
158
|
+
s.start(`Cloning ${repoFullName}...`)
|
|
159
|
+
|
|
160
|
+
const cloneResult = await cloneRepo(repoFullName, cwd, {
|
|
161
|
+
protocol: "ssh",
|
|
162
|
+
onProgress: (msg) => s.message(msg),
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
if (!cloneResult.success) {
|
|
166
|
+
s.stop("Clone failed")
|
|
167
|
+
|
|
168
|
+
// Suggest HTTPS if SSH failed
|
|
169
|
+
if (cloneResult.error?.includes("Permission denied")) {
|
|
170
|
+
logWarning("SSH clone failed. Trying HTTPS...")
|
|
171
|
+
|
|
172
|
+
s.start(`Cloning ${repoFullName} via HTTPS...`)
|
|
173
|
+
const httpsResult = await cloneRepo(repoFullName, cwd, {
|
|
174
|
+
protocol: "https",
|
|
175
|
+
onProgress: (msg) => s.message(msg),
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
if (!httpsResult.success) {
|
|
179
|
+
s.stop("HTTPS clone also failed")
|
|
180
|
+
logError(httpsResult.error ?? "Clone failed")
|
|
181
|
+
return {
|
|
182
|
+
completed: false,
|
|
183
|
+
cancelled: false,
|
|
184
|
+
error: httpsResult.error ?? "Clone failed",
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
s.stop(`Cloned ${repoFullName}`)
|
|
188
|
+
} else {
|
|
189
|
+
logError(cloneResult.error ?? "Clone failed")
|
|
190
|
+
return {
|
|
191
|
+
completed: false,
|
|
192
|
+
cancelled: false,
|
|
193
|
+
error: cloneResult.error ?? "Clone failed",
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
s.stop(`Cloned ${repoFullName}`)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Now gather configuration preferences
|
|
201
|
+
const configResult = await gatherConfiguration()
|
|
202
|
+
if (configResult.cancelled) {
|
|
203
|
+
return { completed: false, cancelled: true }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Initialize project
|
|
207
|
+
s.start("Initializing OpenSWE project...")
|
|
208
|
+
|
|
209
|
+
const repoUrl = getCloneUrl(repoFullName, "ssh")
|
|
210
|
+
await initProject(cwd, { fullName: repoFullName, remoteUrl: repoUrl })
|
|
211
|
+
|
|
212
|
+
// Save project config
|
|
213
|
+
const projectConfig = createProjectConfig(repoFullName, repoUrl)
|
|
214
|
+
await saveProjectConfig(cwd, projectConfig)
|
|
215
|
+
|
|
216
|
+
// Save global config
|
|
217
|
+
const globalConfig: PartialConfig = {
|
|
218
|
+
ai: { backend: configResult.aiBackend },
|
|
219
|
+
}
|
|
220
|
+
await saveGlobalConfig(globalConfig)
|
|
221
|
+
|
|
222
|
+
s.stop("Project initialized")
|
|
223
|
+
|
|
224
|
+
logSuccess("Created .openswe/ directory")
|
|
225
|
+
logSuccess("Created .worktrees/ directory")
|
|
226
|
+
logSuccess("Saved configuration")
|
|
227
|
+
|
|
228
|
+
outro("Setup complete! OpenSWE is ready to use.")
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
completed: true,
|
|
232
|
+
cancelled: false,
|
|
233
|
+
repoFullName,
|
|
234
|
+
aiBackend: configResult.aiBackend,
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ============================================================================
|
|
239
|
+
// Existing Repository Flow
|
|
240
|
+
// ============================================================================
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Run the wizard for an existing git repository
|
|
244
|
+
*
|
|
245
|
+
* This flow:
|
|
246
|
+
* 1. Asks user to confirm adoption
|
|
247
|
+
* 2. Prompts for configuration
|
|
248
|
+
* 3. Initializes OpenSWE project
|
|
249
|
+
* 4. Saves configuration
|
|
250
|
+
*
|
|
251
|
+
* @param cwd - Current working directory (git repo)
|
|
252
|
+
* @param repoFullName - Repository name in owner/repo format
|
|
253
|
+
* @param remoteUrl - Git remote URL (optional)
|
|
254
|
+
*/
|
|
255
|
+
export async function runExistingRepoWizard(
|
|
256
|
+
cwd: string,
|
|
257
|
+
repoFullName: string,
|
|
258
|
+
remoteUrl?: string
|
|
259
|
+
): Promise<WizardResult> {
|
|
260
|
+
intro("OpenSWE Setup")
|
|
261
|
+
|
|
262
|
+
const prereq = await checkPrerequisites()
|
|
263
|
+
if (!prereq.ok) {
|
|
264
|
+
logError(prereq.error!)
|
|
265
|
+
return { completed: false, cancelled: false, error: prereq.error }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
note(`Repository: ${repoFullName}`, "Detected")
|
|
269
|
+
|
|
270
|
+
// Ask to adopt
|
|
271
|
+
const adopt = await promptAdoptRepo(repoFullName)
|
|
272
|
+
if (isCancelled(adopt)) {
|
|
273
|
+
return { completed: false, cancelled: true }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!adopt) {
|
|
277
|
+
outro("Setup cancelled.")
|
|
278
|
+
return { completed: false, cancelled: true }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Ask permission to fetch issues immediately
|
|
282
|
+
const fetchConsent = await promptFetchIssues(repoFullName)
|
|
283
|
+
if (isCancelled(fetchConsent)) {
|
|
284
|
+
return { completed: false, cancelled: true }
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (fetchConsent) {
|
|
288
|
+
const issueSpinner = spinner()
|
|
289
|
+
issueSpinner.start("Fetching GitHub issues...")
|
|
290
|
+
|
|
291
|
+
const issuesResult = await fetchIssues(repoFullName, { state: "open", limit: 30 })
|
|
292
|
+
|
|
293
|
+
if (issuesResult.success) {
|
|
294
|
+
issueSpinner.stop(`Fetched ${issuesResult.issues.length} issues`)
|
|
295
|
+
if (issuesResult.issues.length === 0) {
|
|
296
|
+
note("No issues found. You can fetch again later from the TUI.", "Issues")
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
issueSpinner.stop("Issue fetch skipped")
|
|
300
|
+
logWarning(issuesResult.error ?? "Failed to fetch issues")
|
|
301
|
+
}
|
|
302
|
+
} else {
|
|
303
|
+
note("Issue fetching skipped. You can load issues later from the TUI.", "Skipped")
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Gather configuration
|
|
307
|
+
const configResult = await gatherConfiguration()
|
|
308
|
+
if (configResult.cancelled) {
|
|
309
|
+
return { completed: false, cancelled: true }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Initialize project
|
|
313
|
+
const s = spinner()
|
|
314
|
+
s.start("Initializing OpenSWE project...")
|
|
315
|
+
|
|
316
|
+
const repoUrl = remoteUrl ?? getCloneUrl(repoFullName, "ssh")
|
|
317
|
+
await initProject(cwd, { fullName: repoFullName, remoteUrl: repoUrl })
|
|
318
|
+
|
|
319
|
+
// Save project config
|
|
320
|
+
const projectConfig = createProjectConfig(repoFullName, repoUrl)
|
|
321
|
+
await saveProjectConfig(cwd, projectConfig)
|
|
322
|
+
|
|
323
|
+
// Save global config
|
|
324
|
+
const globalConfig: PartialConfig = {
|
|
325
|
+
ai: { backend: configResult.aiBackend },
|
|
326
|
+
}
|
|
327
|
+
await saveGlobalConfig(globalConfig)
|
|
328
|
+
|
|
329
|
+
s.stop("Project initialized")
|
|
330
|
+
|
|
331
|
+
logSuccess("Created .openswe/ directory")
|
|
332
|
+
logSuccess("Created .worktrees/ directory")
|
|
333
|
+
logSuccess("Saved configuration")
|
|
334
|
+
|
|
335
|
+
outro("Setup complete! OpenSWE is ready to use.")
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
completed: true,
|
|
339
|
+
cancelled: false,
|
|
340
|
+
repoFullName,
|
|
341
|
+
aiBackend: configResult.aiBackend,
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ============================================================================
|
|
346
|
+
// Configuration Gathering
|
|
347
|
+
// ============================================================================
|
|
348
|
+
|
|
349
|
+
interface ConfigurationResult {
|
|
350
|
+
cancelled: boolean
|
|
351
|
+
aiBackend?: AIBackendChoice
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Gather configuration preferences from the user
|
|
356
|
+
*/
|
|
357
|
+
async function gatherConfiguration(): Promise<ConfigurationResult> {
|
|
358
|
+
// AI backend
|
|
359
|
+
const backend = await promptAiBackend()
|
|
360
|
+
if (isCancelled(backend)) {
|
|
361
|
+
return { cancelled: true }
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
cancelled: false,
|
|
366
|
+
aiBackend: backend,
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ============================================================================
|
|
371
|
+
// Reconfiguration Flow
|
|
372
|
+
// ============================================================================
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Re-run configuration for an existing project (--setup flag)
|
|
376
|
+
*
|
|
377
|
+
* @param cwd - Project root directory
|
|
378
|
+
* @param repoFullName - Repository name
|
|
379
|
+
*/
|
|
380
|
+
export async function runReconfigureWizard(
|
|
381
|
+
cwd: string,
|
|
382
|
+
repoFullName: string
|
|
383
|
+
): Promise<WizardResult> {
|
|
384
|
+
intro("OpenSWE Reconfiguration")
|
|
385
|
+
|
|
386
|
+
const prereq = await checkPrerequisites()
|
|
387
|
+
if (!prereq.ok) {
|
|
388
|
+
logError(prereq.error!)
|
|
389
|
+
return { completed: false, cancelled: false, error: prereq.error }
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
note(`Repository: ${repoFullName}`, "Current Project")
|
|
393
|
+
|
|
394
|
+
// Gather configuration
|
|
395
|
+
const configResult = await gatherConfiguration()
|
|
396
|
+
if (configResult.cancelled) {
|
|
397
|
+
return { completed: false, cancelled: true }
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Update global config
|
|
401
|
+
const s = spinner()
|
|
402
|
+
s.start("Saving configuration...")
|
|
403
|
+
|
|
404
|
+
const globalConfig: PartialConfig = {
|
|
405
|
+
ai: { backend: configResult.aiBackend },
|
|
406
|
+
}
|
|
407
|
+
await saveGlobalConfig(globalConfig)
|
|
408
|
+
|
|
409
|
+
s.stop("Configuration saved")
|
|
410
|
+
|
|
411
|
+
outro("Reconfiguration complete!")
|
|
412
|
+
|
|
413
|
+
return {
|
|
414
|
+
completed: true,
|
|
415
|
+
cancelled: false,
|
|
416
|
+
repoFullName,
|
|
417
|
+
aiBackend: configResult.aiBackend,
|
|
418
|
+
}
|
|
419
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wizard module
|
|
3
|
+
*
|
|
4
|
+
* First-run wizard for project setup using @clack/prompts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Re-export prompts (individual components)
|
|
8
|
+
export {
|
|
9
|
+
// Prompt functions
|
|
10
|
+
promptRepoInput,
|
|
11
|
+
promptAiBackend,
|
|
12
|
+
promptAdoptRepo,
|
|
13
|
+
// Utility functions
|
|
14
|
+
isCancelled,
|
|
15
|
+
intro,
|
|
16
|
+
outro,
|
|
17
|
+
note,
|
|
18
|
+
handleCancel,
|
|
19
|
+
spinner,
|
|
20
|
+
log,
|
|
21
|
+
logInfo,
|
|
22
|
+
logSuccess,
|
|
23
|
+
logWarning,
|
|
24
|
+
logError,
|
|
25
|
+
// Types
|
|
26
|
+
type AIBackendChoice,
|
|
27
|
+
} from "./prompts"
|
|
28
|
+
|
|
29
|
+
// Re-export flows (orchestrated wizard sequences)
|
|
30
|
+
export {
|
|
31
|
+
// Flow functions
|
|
32
|
+
runEmptyDirectoryWizard,
|
|
33
|
+
runExistingRepoWizard,
|
|
34
|
+
runReconfigureWizard,
|
|
35
|
+
// Types
|
|
36
|
+
type WizardResult,
|
|
37
|
+
} from "./flows"
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wizard prompts
|
|
3
|
+
*
|
|
4
|
+
* Individual prompt components using @clack/prompts for the first-run wizard.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as p from "@clack/prompts"
|
|
8
|
+
import { isValidOwnerRepo } from "../git/repo"
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Types
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
/** AI backend options */
|
|
15
|
+
export type AIBackendChoice = "opencode" | "claude"
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Repository Input
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Prompt the user for a GitHub repository in owner/repo format
|
|
23
|
+
*
|
|
24
|
+
* @returns The repository string or a symbol if cancelled
|
|
25
|
+
*/
|
|
26
|
+
export async function promptRepoInput(): Promise<string | symbol> {
|
|
27
|
+
const repo = await p.text({
|
|
28
|
+
message: "Enter GitHub repository (owner/repo):",
|
|
29
|
+
placeholder: "owner/repo",
|
|
30
|
+
validate: (value) => {
|
|
31
|
+
if (!value.trim()) {
|
|
32
|
+
return "Repository is required"
|
|
33
|
+
}
|
|
34
|
+
if (!isValidOwnerRepo(value.trim())) {
|
|
35
|
+
return "Please enter a valid repository in owner/repo format"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
if (typeof repo === "string") {
|
|
41
|
+
return repo.trim()
|
|
42
|
+
}
|
|
43
|
+
return repo
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// AI Backend Selection
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Prompt the user to select an AI backend
|
|
52
|
+
*
|
|
53
|
+
* @returns The selected backend or a symbol if cancelled
|
|
54
|
+
*/
|
|
55
|
+
export async function promptAiBackend(): Promise<AIBackendChoice | symbol> {
|
|
56
|
+
const backend = await p.select({
|
|
57
|
+
message: "Select AI backend:",
|
|
58
|
+
options: [
|
|
59
|
+
{
|
|
60
|
+
value: "opencode" as const,
|
|
61
|
+
label: "OpenCode",
|
|
62
|
+
hint: "Open-source, self-hosted AI agents",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
value: "claude" as const,
|
|
66
|
+
label: "Claude",
|
|
67
|
+
hint: "Anthropic's Claude via API",
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return backend
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// Repository Adoption
|
|
77
|
+
// ============================================================================
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Prompt to adopt an existing repository
|
|
81
|
+
*
|
|
82
|
+
* @param repoName - The repository name to display
|
|
83
|
+
* @returns true/false or a symbol if cancelled
|
|
84
|
+
*/
|
|
85
|
+
export async function promptAdoptRepo(repoName: string): Promise<boolean | symbol> {
|
|
86
|
+
const adopt = await p.confirm({
|
|
87
|
+
message: `Initialize OpenSWE for ${repoName}?`,
|
|
88
|
+
initialValue: true,
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
return adopt
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// =========================================================================
|
|
95
|
+
// Issue Fetch Consent
|
|
96
|
+
// =========================================================================
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Ask for permission to fetch GitHub issues for a detected repository
|
|
100
|
+
*/
|
|
101
|
+
export async function promptFetchIssues(repoName: string): Promise<boolean | symbol> {
|
|
102
|
+
const consent = await p.confirm({
|
|
103
|
+
message: `Fetch GitHub issues for ${repoName}?`,
|
|
104
|
+
initialValue: true,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
return consent
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ============================================================================
|
|
111
|
+
// Utility Functions
|
|
112
|
+
// ============================================================================
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check if a prompt result indicates cancellation
|
|
116
|
+
*/
|
|
117
|
+
export function isCancelled(value: unknown): value is symbol {
|
|
118
|
+
return p.isCancel(value)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Display an intro message
|
|
123
|
+
*/
|
|
124
|
+
export function intro(message: string): void {
|
|
125
|
+
p.intro(message)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Display an outro message
|
|
130
|
+
*/
|
|
131
|
+
export function outro(message: string): void {
|
|
132
|
+
p.outro(message)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Display a note
|
|
137
|
+
*/
|
|
138
|
+
export function note(message: string, title?: string): void {
|
|
139
|
+
p.note(message, title)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Display a cancellation message and exit
|
|
144
|
+
*/
|
|
145
|
+
export function handleCancel(message = "Setup cancelled."): never {
|
|
146
|
+
p.cancel(message)
|
|
147
|
+
process.exit(0)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Create a spinner for long-running operations
|
|
152
|
+
*/
|
|
153
|
+
export function spinner(): ReturnType<typeof p.spinner> {
|
|
154
|
+
return p.spinner()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Log a message (styled)
|
|
159
|
+
*/
|
|
160
|
+
export function log(message: string): void {
|
|
161
|
+
p.log.message(message)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Log an info message
|
|
166
|
+
*/
|
|
167
|
+
export function logInfo(message: string): void {
|
|
168
|
+
p.log.info(message)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Log a success message
|
|
173
|
+
*/
|
|
174
|
+
export function logSuccess(message: string): void {
|
|
175
|
+
p.log.success(message)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Log a warning message
|
|
180
|
+
*/
|
|
181
|
+
export function logWarning(message: string): void {
|
|
182
|
+
p.log.warn(message)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Log an error message
|
|
187
|
+
*/
|
|
188
|
+
export function logError(message: string): void {
|
|
189
|
+
p.log.error(message)
|
|
190
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { mkdtemp, mkdir } from "fs/promises"
|
|
2
|
+
import { tmpdir } from "os"
|
|
3
|
+
import { join } from "path"
|
|
4
|
+
import { describe, expect, test } from "bun:test"
|
|
5
|
+
import { detectWorkspace } from "./detect"
|
|
6
|
+
|
|
7
|
+
async function createTempDir(name: string): Promise<string> {
|
|
8
|
+
return mkdtemp(join(tmpdir(), `${name}-`))
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe("detectWorkspace", () => {
|
|
12
|
+
test("detects existing project in an ancestor directory", async () => {
|
|
13
|
+
const base = await createTempDir("openswe-detect-project")
|
|
14
|
+
const projectRoot = join(base, "project")
|
|
15
|
+
const nested = join(projectRoot, "nested")
|
|
16
|
+
|
|
17
|
+
await mkdir(join(projectRoot, ".openswe"), { recursive: true })
|
|
18
|
+
await mkdir(nested, { recursive: true })
|
|
19
|
+
|
|
20
|
+
const result = await detectWorkspace(nested)
|
|
21
|
+
|
|
22
|
+
expect(result.type).toBe("existing-project")
|
|
23
|
+
expect(result.projectRoot).toBe(projectRoot)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test("detects git repo in an ancestor directory", async () => {
|
|
27
|
+
const base = await createTempDir("openswe-detect-git")
|
|
28
|
+
const repoRoot = join(base, "repo")
|
|
29
|
+
const nested = join(repoRoot, "nested")
|
|
30
|
+
|
|
31
|
+
await mkdir(join(repoRoot, ".git"), { recursive: true })
|
|
32
|
+
await mkdir(nested, { recursive: true })
|
|
33
|
+
|
|
34
|
+
const result = await detectWorkspace(nested)
|
|
35
|
+
|
|
36
|
+
expect(result.type).toBe("existing-repo")
|
|
37
|
+
expect(result.projectRoot).toBe(repoRoot)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test("returns empty when no markers are found", async () => {
|
|
41
|
+
const base = await createTempDir("openswe-detect-empty")
|
|
42
|
+
const nested = join(base, "child")
|
|
43
|
+
|
|
44
|
+
await mkdir(nested, { recursive: true })
|
|
45
|
+
|
|
46
|
+
const result = await detectWorkspace(nested)
|
|
47
|
+
|
|
48
|
+
expect(result.type).toBe("empty")
|
|
49
|
+
expect(result.projectRoot).toBe(nested)
|
|
50
|
+
})
|
|
51
|
+
})
|