@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,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace detection logic
|
|
3
|
+
*
|
|
4
|
+
* Detects the workspace type based on what exists in the current directory:
|
|
5
|
+
* - existing-project: Has .openswe/ directory (fully initialized)
|
|
6
|
+
* - existing-repo: Has .git/ but no .openswe/ (can be adopted)
|
|
7
|
+
* - empty: No relevant markers (needs full setup)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { dirname, join, resolve } from "path"
|
|
11
|
+
import { getOpenSWEDir } from "./paths"
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Types
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
/** Types of workspace states we can detect */
|
|
18
|
+
export type WorkspaceType =
|
|
19
|
+
| "existing-project" // Has .openswe/ - load and continue
|
|
20
|
+
| "existing-repo" // Has .git/ but no .openswe/ - offer to adopt
|
|
21
|
+
| "empty" // Empty or no relevant markers - need setup
|
|
22
|
+
|
|
23
|
+
/** Result of workspace detection */
|
|
24
|
+
export interface WorkspaceResult {
|
|
25
|
+
/** The detected workspace type */
|
|
26
|
+
type: WorkspaceType
|
|
27
|
+
/** Absolute path to the project root */
|
|
28
|
+
projectRoot: string
|
|
29
|
+
/** Git remote URL (only for existing-repo type) */
|
|
30
|
+
remoteUrl?: string | null
|
|
31
|
+
/** Repository full name in owner/repo format (only for existing-repo type) */
|
|
32
|
+
repoFullName?: string | null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// Detection Functions
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if a directory contains an OpenSWE project
|
|
41
|
+
* @param path - Directory path to check
|
|
42
|
+
*/
|
|
43
|
+
export async function hasOpenSWEProject(path: string): Promise<boolean> {
|
|
44
|
+
const opensweDir = getOpenSWEDir(path)
|
|
45
|
+
// Try to check if it's a directory by checking if state.db exists
|
|
46
|
+
// or if the directory marker exists
|
|
47
|
+
try {
|
|
48
|
+
const { readdir } = await import("fs/promises")
|
|
49
|
+
await readdir(opensweDir)
|
|
50
|
+
return true
|
|
51
|
+
} catch {
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if a directory contains a git repository
|
|
58
|
+
* @param path - Directory path to check
|
|
59
|
+
*/
|
|
60
|
+
export async function hasGitRepo(path: string): Promise<boolean> {
|
|
61
|
+
const gitDir = join(path, ".git")
|
|
62
|
+
try {
|
|
63
|
+
const { readdir } = await import("fs/promises")
|
|
64
|
+
await readdir(gitDir)
|
|
65
|
+
return true
|
|
66
|
+
} catch {
|
|
67
|
+
// .git might be a file (for worktrees), check if it exists as a file
|
|
68
|
+
const gitFile = Bun.file(gitDir)
|
|
69
|
+
return gitFile.exists()
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check if a directory is empty (or only contains hidden files we can ignore)
|
|
75
|
+
* @param path - Directory path to check
|
|
76
|
+
*/
|
|
77
|
+
export async function isEmptyDirectory(path: string): Promise<boolean> {
|
|
78
|
+
try {
|
|
79
|
+
const { readdir } = await import("fs/promises")
|
|
80
|
+
const entries = await readdir(path)
|
|
81
|
+
// Consider empty if no entries or only .DS_Store
|
|
82
|
+
const significantEntries = entries.filter(
|
|
83
|
+
(e) => e !== ".DS_Store" && e !== ".gitkeep"
|
|
84
|
+
)
|
|
85
|
+
return significantEntries.length === 0
|
|
86
|
+
} catch {
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get the git remote origin URL
|
|
93
|
+
* @param path - Directory path (must contain .git)
|
|
94
|
+
* @returns Remote URL or null if not found
|
|
95
|
+
*/
|
|
96
|
+
export async function getGitRemoteUrl(path: string): Promise<string | null> {
|
|
97
|
+
try {
|
|
98
|
+
const proc = Bun.spawn(["git", "remote", "get-url", "origin"], {
|
|
99
|
+
cwd: path,
|
|
100
|
+
stdout: "pipe",
|
|
101
|
+
stderr: "pipe",
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const output = await new Response(proc.stdout).text()
|
|
105
|
+
const exitCode = await proc.exited
|
|
106
|
+
|
|
107
|
+
if (exitCode !== 0) {
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return output.trim() || null
|
|
112
|
+
} catch {
|
|
113
|
+
return null
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Parse a git remote URL to extract owner/repo format
|
|
119
|
+
* Handles both HTTPS and SSH URLs:
|
|
120
|
+
* - https://github.com/owner/repo.git
|
|
121
|
+
* - git@github.com:owner/repo.git
|
|
122
|
+
* - https://github.com/owner/repo
|
|
123
|
+
* - git@github.com:owner/repo
|
|
124
|
+
*
|
|
125
|
+
* @param remoteUrl - Git remote URL
|
|
126
|
+
* @returns owner/repo string or null if parsing fails
|
|
127
|
+
*/
|
|
128
|
+
export function parseRepoFullName(remoteUrl: string): string | null {
|
|
129
|
+
// SSH format: git@github.com:owner/repo.git
|
|
130
|
+
const sshMatch = remoteUrl.match(/git@[^:]+:([^/]+\/[^/]+?)(?:\.git)?$/)
|
|
131
|
+
if (sshMatch?.[1]) {
|
|
132
|
+
return sshMatch[1]
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// HTTPS format: https://github.com/owner/repo.git
|
|
136
|
+
const httpsMatch = remoteUrl.match(/https?:\/\/[^/]+\/([^/]+\/[^/]+?)(?:\.git)?$/)
|
|
137
|
+
if (httpsMatch?.[1]) {
|
|
138
|
+
return httpsMatch[1]
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return null
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Main workspace detection function
|
|
146
|
+
*
|
|
147
|
+
* Detection logic follows this order:
|
|
148
|
+
* 1. Check for .openswe/ (existing project) → load and continue
|
|
149
|
+
* 2. Check for .git/ (existing repo) → offer to adopt
|
|
150
|
+
* 3. Otherwise → empty directory, need full setup
|
|
151
|
+
*
|
|
152
|
+
* @param cwd - Current working directory to detect
|
|
153
|
+
* @returns Detection result with workspace type and relevant info
|
|
154
|
+
*/
|
|
155
|
+
export async function detectWorkspace(cwd: string): Promise<WorkspaceResult> {
|
|
156
|
+
let current = resolve(cwd)
|
|
157
|
+
|
|
158
|
+
while (true) {
|
|
159
|
+
// Prefer OpenSWE project if found at this level
|
|
160
|
+
if (await hasOpenSWEProject(current)) {
|
|
161
|
+
return {
|
|
162
|
+
type: "existing-project",
|
|
163
|
+
projectRoot: current,
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Otherwise see if this level is a git repo
|
|
168
|
+
if (await hasGitRepo(current)) {
|
|
169
|
+
const remoteUrl = await getGitRemoteUrl(current)
|
|
170
|
+
const repoFullName = remoteUrl ? parseRepoFullName(remoteUrl) : null
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
type: "existing-repo",
|
|
174
|
+
projectRoot: current,
|
|
175
|
+
remoteUrl,
|
|
176
|
+
repoFullName,
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const parent = dirname(current)
|
|
181
|
+
if (parent === current) {
|
|
182
|
+
break
|
|
183
|
+
}
|
|
184
|
+
current = parent
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
type: "empty",
|
|
189
|
+
projectRoot: resolve(cwd),
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ============================================================================
|
|
194
|
+
// Validation Helpers
|
|
195
|
+
// ============================================================================
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Validate that a directory can be used as a workspace
|
|
199
|
+
* - Must exist
|
|
200
|
+
* - Must be a directory (not a file)
|
|
201
|
+
* - Must be readable
|
|
202
|
+
*
|
|
203
|
+
* @param path - Path to validate
|
|
204
|
+
* @throws Error if validation fails
|
|
205
|
+
*/
|
|
206
|
+
export async function validateWorkspaceDirectory(path: string): Promise<void> {
|
|
207
|
+
const { stat } = await import("fs/promises")
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const stats = await stat(path)
|
|
211
|
+
if (!stats.isDirectory()) {
|
|
212
|
+
throw new Error(`Path is not a directory: ${path}`)
|
|
213
|
+
}
|
|
214
|
+
} catch (err) {
|
|
215
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
216
|
+
throw new Error(`Directory does not exist: ${path}`)
|
|
217
|
+
}
|
|
218
|
+
if ((err as NodeJS.ErrnoException).code === "EACCES") {
|
|
219
|
+
throw new Error(`Permission denied: ${path}`)
|
|
220
|
+
}
|
|
221
|
+
throw err
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace module
|
|
3
|
+
*
|
|
4
|
+
* Provides utilities for workspace detection and initialization:
|
|
5
|
+
* - Path utilities for .openswe/, .worktrees/, etc.
|
|
6
|
+
* - Workspace type detection (existing-project, existing-repo, empty)
|
|
7
|
+
* - Project initialization (directory creation)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Re-export all path utilities
|
|
11
|
+
export {
|
|
12
|
+
// Constants
|
|
13
|
+
OPENSWE_DIR,
|
|
14
|
+
WORKTREES_DIR,
|
|
15
|
+
STATE_DB_FILE,
|
|
16
|
+
LOGS_DIR,
|
|
17
|
+
// Path resolution
|
|
18
|
+
getOpenSWEDir,
|
|
19
|
+
getWorktreesDir,
|
|
20
|
+
getStateDatabasePath,
|
|
21
|
+
getLogsDir,
|
|
22
|
+
getWorktreePath,
|
|
23
|
+
// Naming utilities
|
|
24
|
+
sanitizeWorktreeName,
|
|
25
|
+
generateBranchName,
|
|
26
|
+
// Path validation
|
|
27
|
+
ensureAbsolutePath,
|
|
28
|
+
isPathInside,
|
|
29
|
+
} from "./paths"
|
|
30
|
+
|
|
31
|
+
// Re-export detection utilities
|
|
32
|
+
export {
|
|
33
|
+
// Types
|
|
34
|
+
type WorkspaceType,
|
|
35
|
+
type WorkspaceResult,
|
|
36
|
+
// Detection functions
|
|
37
|
+
detectWorkspace,
|
|
38
|
+
hasOpenSWEProject,
|
|
39
|
+
hasGitRepo,
|
|
40
|
+
isEmptyDirectory,
|
|
41
|
+
getGitRemoteUrl,
|
|
42
|
+
parseRepoFullName,
|
|
43
|
+
validateWorkspaceDirectory,
|
|
44
|
+
} from "./detect"
|
|
45
|
+
|
|
46
|
+
// Re-export initialization utilities
|
|
47
|
+
export {
|
|
48
|
+
// Types
|
|
49
|
+
type RepoInfo,
|
|
50
|
+
type InitResult,
|
|
51
|
+
// Initialization functions
|
|
52
|
+
initProject,
|
|
53
|
+
createOpenSWEDirectory,
|
|
54
|
+
createWorktreesDirectory,
|
|
55
|
+
isProjectInitialized,
|
|
56
|
+
cleanupProject,
|
|
57
|
+
} from "./init"
|
|
58
|
+
|
|
59
|
+
// Re-export project config utilities
|
|
60
|
+
export {
|
|
61
|
+
// Types
|
|
62
|
+
type ProjectConfig,
|
|
63
|
+
// Path utilities
|
|
64
|
+
getProjectConfigPath,
|
|
65
|
+
// CRUD functions
|
|
66
|
+
loadProjectConfig,
|
|
67
|
+
saveProjectConfig,
|
|
68
|
+
updateLastOpened,
|
|
69
|
+
createProjectConfig,
|
|
70
|
+
projectConfigExists,
|
|
71
|
+
} from "./project"
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project initialization
|
|
3
|
+
*
|
|
4
|
+
* Handles creating and setting up a new OpenSWE project:
|
|
5
|
+
* - Create .openswe/ directory structure
|
|
6
|
+
* - (Database initialization is handled separately)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { mkdir } from "fs/promises"
|
|
10
|
+
import {
|
|
11
|
+
getOpenSWEDir,
|
|
12
|
+
getWorktreesDir,
|
|
13
|
+
getLogsDir,
|
|
14
|
+
} from "./paths"
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Types
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
/** Information about the repository being linked */
|
|
21
|
+
export interface RepoInfo {
|
|
22
|
+
/** Full repository name in owner/repo format */
|
|
23
|
+
fullName: string
|
|
24
|
+
/** Git remote URL (SSH or HTTPS) */
|
|
25
|
+
remoteUrl: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Result of project initialization */
|
|
29
|
+
export interface InitResult {
|
|
30
|
+
/** Absolute path to the project root */
|
|
31
|
+
projectRoot: string
|
|
32
|
+
/** Path to .openswe directory */
|
|
33
|
+
opensweDir: string
|
|
34
|
+
/** Path to .worktrees directory */
|
|
35
|
+
worktreesDir: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// Directory Creation
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create the .openswe directory structure
|
|
44
|
+
* @param projectRoot - Absolute path to the project root
|
|
45
|
+
*/
|
|
46
|
+
export async function createOpenSWEDirectory(projectRoot: string): Promise<void> {
|
|
47
|
+
const opensweDir = getOpenSWEDir(projectRoot)
|
|
48
|
+
const logsDir = getLogsDir(projectRoot)
|
|
49
|
+
|
|
50
|
+
// Create .openswe/ and .openswe/logs/
|
|
51
|
+
await mkdir(opensweDir, { recursive: true })
|
|
52
|
+
await mkdir(logsDir, { recursive: true })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create the .worktrees directory
|
|
57
|
+
* @param projectRoot - Absolute path to the project root
|
|
58
|
+
*/
|
|
59
|
+
export async function createWorktreesDirectory(projectRoot: string): Promise<void> {
|
|
60
|
+
const worktreesDir = getWorktreesDir(projectRoot)
|
|
61
|
+
await mkdir(worktreesDir, { recursive: true })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// Full Initialization
|
|
66
|
+
// ============================================================================
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Initialize a new OpenSWE project
|
|
70
|
+
*
|
|
71
|
+
* This creates the necessary directory structure.
|
|
72
|
+
* Note: Database initialization is handled separately.
|
|
73
|
+
*
|
|
74
|
+
* @param projectRoot - Absolute path to the project root
|
|
75
|
+
* @param repoInfo - Information about the repository (optional, for logging)
|
|
76
|
+
* @returns Initialization result
|
|
77
|
+
*/
|
|
78
|
+
export async function initProject(
|
|
79
|
+
projectRoot: string,
|
|
80
|
+
repoInfo?: RepoInfo
|
|
81
|
+
): Promise<InitResult> {
|
|
82
|
+
const opensweDir = getOpenSWEDir(projectRoot)
|
|
83
|
+
const worktreesDir = getWorktreesDir(projectRoot)
|
|
84
|
+
|
|
85
|
+
// Create directory structure
|
|
86
|
+
await createOpenSWEDirectory(projectRoot)
|
|
87
|
+
await createWorktreesDirectory(projectRoot)
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
projectRoot,
|
|
91
|
+
opensweDir,
|
|
92
|
+
worktreesDir,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check if a project is fully initialized
|
|
98
|
+
* (has .openswe directory with required structure)
|
|
99
|
+
*
|
|
100
|
+
* @param projectRoot - Absolute path to the project root
|
|
101
|
+
*/
|
|
102
|
+
export async function isProjectInitialized(projectRoot: string): Promise<boolean> {
|
|
103
|
+
const opensweDir = getOpenSWEDir(projectRoot)
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const { stat } = await import("fs/promises")
|
|
107
|
+
const stats = await stat(opensweDir)
|
|
108
|
+
return stats.isDirectory()
|
|
109
|
+
} catch {
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Clean up a partially initialized project
|
|
116
|
+
* (removes .openswe and .worktrees directories)
|
|
117
|
+
*
|
|
118
|
+
* Use with caution - this is destructive!
|
|
119
|
+
*
|
|
120
|
+
* @param projectRoot - Absolute path to the project root
|
|
121
|
+
*/
|
|
122
|
+
export async function cleanupProject(projectRoot: string): Promise<void> {
|
|
123
|
+
const { rm } = await import("fs/promises")
|
|
124
|
+
|
|
125
|
+
const opensweDir = getOpenSWEDir(projectRoot)
|
|
126
|
+
const worktreesDir = getWorktreesDir(projectRoot)
|
|
127
|
+
|
|
128
|
+
// Remove directories (ignore errors if they don't exist)
|
|
129
|
+
await rm(opensweDir, { recursive: true, force: true })
|
|
130
|
+
await rm(worktreesDir, { recursive: true, force: true })
|
|
131
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path utilities for workspace directories
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent path resolution for OpenSWE project structure:
|
|
5
|
+
* - .openswe/ - Project state directory
|
|
6
|
+
* - .openswe/state.db - SQLite database
|
|
7
|
+
* - .openswe/logs/ - Log files
|
|
8
|
+
* - .worktrees/ - Git worktrees for sessions
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { join, resolve } from "path"
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Directory Names (Constants)
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
/** Name of the OpenSWE project state directory */
|
|
18
|
+
export const OPENSWE_DIR = ".openswe"
|
|
19
|
+
|
|
20
|
+
/** Name of the worktrees directory */
|
|
21
|
+
export const WORKTREES_DIR = ".worktrees"
|
|
22
|
+
|
|
23
|
+
/** Name of the SQLite database file */
|
|
24
|
+
export const STATE_DB_FILE = "state.db"
|
|
25
|
+
|
|
26
|
+
/** Name of the logs directory within .openswe */
|
|
27
|
+
export const LOGS_DIR = "logs"
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Path Resolution Functions
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolve the .openswe directory path for a project root
|
|
35
|
+
* @param projectRoot - Absolute path to the project root
|
|
36
|
+
*/
|
|
37
|
+
export function getOpenSWEDir(projectRoot: string): string {
|
|
38
|
+
return join(projectRoot, OPENSWE_DIR)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the .worktrees directory path for a project root
|
|
43
|
+
* @param projectRoot - Absolute path to the project root
|
|
44
|
+
*/
|
|
45
|
+
export function getWorktreesDir(projectRoot: string): string {
|
|
46
|
+
return join(projectRoot, WORKTREES_DIR)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the state.db path for a project root
|
|
51
|
+
* @param projectRoot - Absolute path to the project root
|
|
52
|
+
*/
|
|
53
|
+
export function getStateDatabasePath(projectRoot: string): string {
|
|
54
|
+
return join(projectRoot, OPENSWE_DIR, STATE_DB_FILE)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the logs directory path for a project root
|
|
59
|
+
* @param projectRoot - Absolute path to the project root
|
|
60
|
+
*/
|
|
61
|
+
export function getLogsDir(projectRoot: string): string {
|
|
62
|
+
return join(projectRoot, OPENSWE_DIR, LOGS_DIR)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolve a worktree path for a specific session/issue
|
|
67
|
+
* @param projectRoot - Absolute path to the project root
|
|
68
|
+
* @param name - Session name or issue number (will be sanitized)
|
|
69
|
+
*/
|
|
70
|
+
export function getWorktreePath(projectRoot: string, name: string | number): string {
|
|
71
|
+
const sanitized = sanitizeWorktreeName(name)
|
|
72
|
+
return join(projectRoot, WORKTREES_DIR, sanitized)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Resolve the log file path for a session
|
|
78
|
+
* @param projectRoot - Absolute path to the project root
|
|
79
|
+
* @param sessionId - Session UUID
|
|
80
|
+
*/
|
|
81
|
+
export function getSessionLogPath(projectRoot: string, sessionId: string): string {
|
|
82
|
+
return join(getLogsDir(projectRoot), `${sessionId}.log`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ============================================================================
|
|
86
|
+
// Naming Utilities
|
|
87
|
+
// ============================================================================
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Sanitize a name for use as a worktree directory/branch name
|
|
91
|
+
* - Converts to lowercase
|
|
92
|
+
* - Replaces spaces and special chars with hyphens
|
|
93
|
+
* - Removes consecutive hyphens
|
|
94
|
+
* - Trims leading/trailing hyphens
|
|
95
|
+
*/
|
|
96
|
+
export function sanitizeWorktreeName(name: string | number): string {
|
|
97
|
+
if (typeof name === "number") {
|
|
98
|
+
return `issue-${name}`
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return name
|
|
102
|
+
.toLowerCase()
|
|
103
|
+
.replace(/[^a-z0-9-]/g, "-") // Replace non-alphanumeric with hyphens
|
|
104
|
+
.replace(/-+/g, "-") // Collapse multiple hyphens
|
|
105
|
+
.replace(/^-|-$/g, "") // Trim leading/trailing hyphens
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Generate a branch name for a session
|
|
110
|
+
* @param name - Session name or issue number
|
|
111
|
+
* @param prefix - Branch prefix (default: "openswe")
|
|
112
|
+
*/
|
|
113
|
+
export function generateBranchName(name: string | number, prefix = "openswe"): string {
|
|
114
|
+
const sanitized = sanitizeWorktreeName(name)
|
|
115
|
+
return `${prefix}/${sanitized}`
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ============================================================================
|
|
119
|
+
// Path Validation
|
|
120
|
+
// ============================================================================
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Ensure a path is absolute
|
|
124
|
+
* @param path - Path to validate/resolve
|
|
125
|
+
* @param basePath - Base path for resolution if relative (defaults to cwd)
|
|
126
|
+
*/
|
|
127
|
+
export function ensureAbsolutePath(path: string, basePath?: string): string {
|
|
128
|
+
if (path.startsWith("/")) {
|
|
129
|
+
return path
|
|
130
|
+
}
|
|
131
|
+
return resolve(basePath ?? process.cwd(), path)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Check if a path is inside another path
|
|
136
|
+
* @param child - Potential child path
|
|
137
|
+
* @param parent - Potential parent path
|
|
138
|
+
*/
|
|
139
|
+
export function isPathInside(child: string, parent: string): boolean {
|
|
140
|
+
const resolvedChild = resolve(child)
|
|
141
|
+
const resolvedParent = resolve(parent)
|
|
142
|
+
return resolvedChild.startsWith(resolvedParent + "/") || resolvedChild === resolvedParent
|
|
143
|
+
}
|