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